[#6352] Add Result.formatChart() to produce ASCII text charts

This commit is contained in:
lukaseder 2017-06-21 15:57:11 +02:00
parent 8e23952fc5
commit bc05c60bc0
3 changed files with 738 additions and 0 deletions

View File

@ -0,0 +1,502 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Other licenses:
* -----------------------------------------------------------------------------
* Commercial licenses for this work are available. These replace the above
* ASL 2.0 and offer limited warranties, support, maintenance, and commercial
* database integrations.
*
* For more information, please visit: http://www.jooq.org/licenses
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package org.jooq;
import java.text.DecimalFormat;
/**
* @author Lukas Eder
*/
public final class ChartFormat {
public static final ChartFormat DEFAULT = new ChartFormat();
final Output output;
final Type type;
final Display display;
final int width;
final int height;
final int category;
final boolean categoryAsText;
final int[] values;
final char[] shades;
final boolean showHorizontalLegend;
final boolean showVerticalLegend;
final String newline;
final DecimalFormat numericFormat;
public ChartFormat() {
this(
Output.ASCII,
Type.AREA,
Display.STACKED,
80,
25,
0,
true,
new int[] { 1 },
new char[] { '█', '▓', '▒', '░' },
true,
true,
"\n",
new DecimalFormat("###,###.00")
);
}
private ChartFormat(
Output output,
Type type,
Display display,
int width,
int height,
int category,
boolean categoryAsText,
int[] values,
char[] shades,
boolean showHorizontalLegend,
boolean showVerticalLegend,
String newline,
DecimalFormat numericFormat
) {
this.output = output;
this.type = type;
this.display = display;
this.width = width;
this.height = height;
this.category = category;
this.categoryAsText = categoryAsText;
this.values = values;
this.shades = shades;
this.showHorizontalLegend = showHorizontalLegend;
this.showVerticalLegend = showVerticalLegend;
this.newline = newline;
this.numericFormat = numericFormat;
}
/**
* The new output format, defaulting to {@link Output#ASCII}.
*/
public ChartFormat output(Output newOutput) {
return new ChartFormat(
newOutput,
type,
display,
width,
height,
category,
categoryAsText,
values,
shades,
showHorizontalLegend,
showVerticalLegend,
newline,
numericFormat
);
}
/**
* The output format.
*/
public Output output() {
return output;
}
/**
* The new chart type, defaulting to {@link Area}.
*/
public ChartFormat type(Type newType) {
return new ChartFormat(
output,
newType,
display,
width,
height,
category,
categoryAsText,
values,
shades,
showHorizontalLegend,
showVerticalLegend,
newline,
numericFormat
);
}
public Type type() {
return type;
}
/**
* The new display format, defaulting to {@link Display#STACKED}.
*/
public ChartFormat display(Display newDisplay) {
return new ChartFormat(
output,
type,
newDisplay,
width,
height,
category,
categoryAsText,
values,
shades,
showHorizontalLegend,
showVerticalLegend,
newline,
numericFormat
);
}
/**
* The display format.
*/
public Display display() {
return display;
}
/**
* The new chart dimensions, defaulting to <code>80 x 25</code>.
*/
public ChartFormat dimensions(int newWidth, int newHeight) {
return new ChartFormat(
output,
type,
display,
newWidth,
newHeight,
category,
categoryAsText,
values,
shades,
showHorizontalLegend,
showVerticalLegend,
newline,
numericFormat
);
}
/**
* The new chart width, defaulting to <code>80</code>.
*/
public ChartFormat width(int newWidth) {
return dimensions(newWidth, height);
}
/**
* The chart width.
*/
public int width() {
return width;
}
/**
* The new chart height, defaulting to <code>25</code>.
*/
public ChartFormat height(int newHeight) {
return dimensions(width, newHeight);
}
/**
* The chart height.
*/
public int height() {
return height;
}
/**
* The new category source column number, defaulting to <code>0</code>.
*/
public ChartFormat category(int newCategory) {
return new ChartFormat(
output,
type,
display,
width,
height,
newCategory,
categoryAsText,
values,
shades,
showHorizontalLegend,
showVerticalLegend,
newline,
numericFormat
);
}
/**
* The category source column number.
*/
public int category() {
return category;
}
/**
* The new category as text value, defaulting to <code>true</code>.
*/
public ChartFormat categoryAsText(boolean newCategoryAsText) {
return new ChartFormat(
output,
type,
display,
width,
height,
category,
newCategoryAsText,
values,
shades,
showHorizontalLegend,
showVerticalLegend,
newline,
numericFormat
);
}
/**
* The category as text value.
*/
public boolean categoryAsText() {
return categoryAsText;
}
/**
* The new value source column numbers, defaulting to <code>{ 1 }</code>.
*/
public ChartFormat values(int... newValues) {
return new ChartFormat(
output,
type,
display,
width,
height,
category,
categoryAsText,
newValues,
shades,
showHorizontalLegend,
showVerticalLegend,
newline,
numericFormat
);
}
/**
* The value source column numbers.
*/
public int[] values() {
return values;
}
/**
* The new column shades, defaulting to <code>{ 'X' }</code>.
*/
public ChartFormat shades(char... newShades) {
return new ChartFormat(
output,
type,
display,
width,
height,
category,
categoryAsText,
values,
newShades,
showHorizontalLegend,
showVerticalLegend,
newline,
numericFormat
);
}
/**
* The value column shades.
*/
public char[] shades() {
return shades;
}
/**
* Whether to show legends, defaulting to <code>true</code>.
*/
public ChartFormat showLegends(boolean newShowHorizontalLegend, boolean newShowVerticalLegend) {
return new ChartFormat(
output,
type,
display,
width,
height,
category,
categoryAsText,
values,
shades,
newShowHorizontalLegend,
newShowVerticalLegend,
newline,
numericFormat
);
}
/**
* Whether to show the horizontal legend, defaulting to <code>true</code>.
*/
public ChartFormat showHorizontalLegend(boolean newShowHorizontalLegend) {
return showLegends(newShowHorizontalLegend, showVerticalLegend);
}
/**
* Whether to show the horizontal legend.
*/
public boolean showHorizontalLegend() {
return showHorizontalLegend;
}
/**
* Whether to show the vertical legend, defaulting to <code>true</code>.
*/
public ChartFormat showVerticalLegend(boolean newShowVerticalLegend) {
return showLegends(showHorizontalLegend, newShowVerticalLegend);
}
/**
* Whether to show the vertical legend.
*/
public boolean showVerticalLegend() {
return showVerticalLegend;
}
/**
* The new newline character, defaulting to <code>\n</code>.
*/
public ChartFormat newline(String newNewline) {
return new ChartFormat(
output,
type,
display,
width,
height,
category,
categoryAsText,
values,
shades,
showHorizontalLegend,
showVerticalLegend,
newNewline,
numericFormat
);
}
/**
* The newline character.
*/
public String newline() {
return newline;
}
/**
* The new numeric format, defaulting to <code>###,###.00</code>.
*/
public ChartFormat numericFormat(DecimalFormat newNumericFormat) {
return new ChartFormat(
output,
type,
display,
width,
height,
category,
categoryAsText,
values,
shades,
showHorizontalLegend,
showVerticalLegend,
newline,
newNumericFormat
);
}
/**
* The numeric format.
*/
public DecimalFormat numericFormat() {
return numericFormat;
}
/**
* The chart output format.
*/
public enum Output {
/**
* An ASCII chart.
*/
ASCII,
// /**
// * An ANSI escape sequenced chart.
// */
// ANSI,
//
// /**
// * An SVG chart.
// */
// SVG,
}
public static enum Type {
/**
* An area chart.
*/
AREA,
}
public static enum Display {
/**
* The areas are located in front of one another.
*/
DEFAULT,
/**
* The areas are stacked on top of one another.
*/
STACKED,
/**
* The areas stack up to 100%.
*/
HUNDRED_PERCENT_STACKED
}
}

View File

@ -630,6 +630,10 @@ public interface Result<R extends Record> extends List<R>, Attachable {
*/
String formatXML(XMLFormat format);
String formatChart();
String formatChart(ChartFormat format);
/**
* Get this result as a set of <code>INSERT</code> statements.
* <p>
@ -747,6 +751,20 @@ public interface Result<R extends Record> extends List<R>, Attachable {
*/
void formatXML(OutputStream stream, XMLFormat format) throws IOException;
/**
* Like {@link #formatChart()}, but the data is output onto an {@link OutputStream}.
*
* @throws IOException - an unchecked wrapper for {@link java.io.IOException}, if anything goes wrong.
*/
void formatChart(OutputStream stream) throws IOException;
/**
* Like {@link #formatChart(ChartFormat)}, but the data is output onto an {@link OutputStream}.
*
* @throws IOException - an unchecked wrapper for {@link java.io.IOException}, if anything goes wrong.
*/
void formatChart(OutputStream stream, ChartFormat format) throws IOException;
/**
* Like {@link #formatInsert()}, but the data is output onto an {@link OutputStream}.
*
@ -859,6 +877,20 @@ public interface Result<R extends Record> extends List<R>, Attachable {
*/
void formatXML(Writer writer, XMLFormat format) throws IOException;
/**
* Like {@link #formatChart()}, but the data is output onto a {@link Writer}.
*
* @throws IOException - an unchecked wrapper for {@link java.io.IOException}, if anything goes wrong.
*/
void formatChart(Writer writer) throws IOException;
/**
* Like {@link #formatChart(ChartFormat)}, but the data is output onto a {@link Writer}.
*
* @throws IOException - an unchecked wrapper for {@link java.io.IOException}, if anything goes wrong.
*/
void formatChart(Writer writer, ChartFormat format) throws IOException;
/**
* Like {@link #formatInsert()}, but the data is output onto a {@link Writer}.
*

View File

@ -67,6 +67,7 @@ import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import javax.xml.bind.DatatypeConverter;
import javax.xml.parsers.DocumentBuilder;
@ -74,6 +75,8 @@ import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.jooq.CSVFormat;
import org.jooq.ChartFormat;
import org.jooq.ChartFormat.Display;
import org.jooq.Configuration;
import org.jooq.Constants;
import org.jooq.Converter;
@ -1222,6 +1225,207 @@ final class ResultImpl<R extends Record> implements Result<R> {
writer.append(newline).append(format.indentString(recordLevel)).append("</record>");
}
@Override
public final String formatChart() {
StringWriter writer = new StringWriter();
formatChart(writer);
return writer.toString();
}
@Override
public final String formatChart(ChartFormat format) {
StringWriter writer = new StringWriter();
formatChart(writer, format);
return writer.toString();
}
@Override
public final void formatChart(OutputStream stream) {
formatChart(new OutputStreamWriter(stream));
}
@Override
public final void formatChart(OutputStream stream, ChartFormat format) {
formatChart(new OutputStreamWriter(stream), format);
}
@Override
public final void formatChart(Writer writer) {
formatChart(writer, ChartFormat.DEFAULT);
}
@Override
public final void formatChart(Writer writer, ChartFormat format) {
try {
Field<?> category = field(format.category());
TreeMap<Object, Result<R>> groups = new TreeMap<Object, Result<R>>(intoGroups(format.category()));
if (!format.categoryAsText()) {
if (Date.class.isAssignableFrom(category.getType())) {
Date categoryMin = (Date) groups.firstKey();
Date categoryMax = (Date) groups.lastKey();
for (Date i = categoryMin; i.before(categoryMax); i = new Date(i.getYear(), i.getMonth(), i.getDate() + 1))
if (!groups.containsKey(i))
groups.put(i, (Result) DSL.using(configuration).newResult(fields()));
}
}
List<?> categories = new ArrayList<Object>(groups.keySet());
int categoryPadding = 1;
int categoryWidth = 0;
for (Object o : categories)
categoryWidth = Math.max(categoryWidth, ("" + o).length());
double axisMin = Double.POSITIVE_INFINITY;
double axisMax = Double.NEGATIVE_INFINITY;
for (Result<R> values : groups.values()) {
double sum = 0;
for (int i = 0; i < format.values().length; i++) {
if (format.display() == Display.DEFAULT)
sum = 0;
for (Record r : values)
sum = sum + r.get(format.values()[i], double.class);
if (sum < axisMin)
axisMin = sum;
if (sum > axisMax)
axisMax = sum;
}
}
int verticalLegendWidth = format.showVerticalLegend()
? Math.max(
format.numericFormat().format(axisMin).length(),
format.numericFormat().format(axisMax).length()
)
: 0;
int horizontalLegendHeight = format.showHorizontalLegend() ? 1 : 0;
int verticalBorderWidth = format.showVerticalLegend() ? 1 : 0;
int horizontalBorderHeight = format.showHorizontalLegend() ? 1 : 0;
int chartHeight = format.height() - horizontalLegendHeight - horizontalBorderHeight;
int chartWidth = format.width() - verticalLegendWidth - verticalBorderWidth;
double barWidth = (double) chartWidth / groups.size();
double axisStep = (axisMax - axisMin) / (chartHeight - 1);
for (int y = chartHeight - 1; y >= 0; y--) {
double axisLegend = axisMax - (axisStep * (chartHeight - 1 - y));
double axisLegendPercent = (axisLegend - axisMin) / (axisMax - axisMin);
if (format.showVerticalLegend()) {
String axisLegendString = (format.display() == Display.HUNDRED_PERCENT_STACKED)
? format.numericFormat().format(axisLegendPercent * 100.0) + "%"
: format.numericFormat().format(axisLegend);
for (int x = axisLegendString.length(); x < verticalLegendWidth; x++)
writer.write(' ');
writer.write(axisLegendString);
for (int x = 0; x < verticalBorderWidth; x++)
writer.write('|');
}
for (int x = 0; x < chartWidth; x++) {
int index = (int) (x / barWidth);
Result<R> group = groups.get(categories.get(index));
double[] values = new double[format.values().length];
for (Record record : group)
for (int i = 0; i < values.length; i++)
values[i] = values[i] + record.get(format.values()[i], double.class);
if (format.display() == Display.STACKED || format.display() == Display.HUNDRED_PERCENT_STACKED)
for (int i = 1; i < values.length; i++)
values[i] = values[i] + values[i - 1];
if (format.display() == Display.HUNDRED_PERCENT_STACKED)
for (int i = 0; i < values.length; i++)
values[i] = values[i] / values[values.length - 1];
int shadeIndex = -1;
for (int i = values.length - 1; i >= 0; i--)
if ((format.display() == Display.HUNDRED_PERCENT_STACKED ? axisLegendPercent : axisLegend) > values[i])
break;
else
shadeIndex = i;
if (shadeIndex == -1)
writer.write(' ');
else
writer.write(format.shades()[shadeIndex % format.shades().length]);
}
writer.write(format.newline());
}
if (format.showHorizontalLegend()) {
for (int y = 0; y < horizontalBorderHeight; y++) {
if (format.showVerticalLegend()) {
for (int x = 0; x < verticalLegendWidth; x++)
writer.write('-');
for (int x = 0; x < verticalBorderWidth; x++)
writer.write('+');
}
for (int x = 0; x < chartWidth; x++)
writer.write('-');
writer.write(format.newline());
}
for (int y = 0; y < horizontalLegendHeight; y++) {
if (format.showVerticalLegend()) {
for (int x = 0; x < verticalLegendWidth; x++)
writer.write(' ');
for (int x = 0; x < verticalBorderWidth; x++)
writer.write('|');
}
double rounding = 0.0;
for (double x = 0.0; x < chartWidth;) {
String label = "" + categories.get((int) (x / barWidth));
int length = label.length();
double padding = Math.max(categoryPadding, (barWidth - length) / 2);
rounding = (rounding + padding - Math.floor(padding)) % 1;
x = x + (padding + rounding);
for (int i = 0; i < (int) (padding + rounding); i++)
writer.write(' ');
x = x + length;
if (x >= chartWidth)
break;
writer.write(label);
rounding = (rounding + padding - Math.floor(padding)) % 1;
x = x + (padding + rounding);
for (int i = 0; i < (int) (padding + rounding); i++)
writer.write(' ');
}
writer.write(format.newline());
}
}
}
catch (java.io.IOException e) {
throw new IOException("Exception while writing Chart", e);
}
}
@Override
public final String formatInsert() {
StringWriter writer = new StringWriter();