From bc05c60bc052e44655ee4ffac81019583111e568 Mon Sep 17 00:00:00 2001 From: lukaseder Date: Wed, 21 Jun 2017 15:57:11 +0200 Subject: [PATCH] [#6352] Add Result.formatChart() to produce ASCII text charts --- jOOQ/src/main/java/org/jooq/ChartFormat.java | 502 ++++++++++++++++++ jOOQ/src/main/java/org/jooq/Result.java | 32 ++ .../main/java/org/jooq/impl/ResultImpl.java | 204 +++++++ 3 files changed, 738 insertions(+) create mode 100644 jOOQ/src/main/java/org/jooq/ChartFormat.java diff --git a/jOOQ/src/main/java/org/jooq/ChartFormat.java b/jOOQ/src/main/java/org/jooq/ChartFormat.java new file mode 100644 index 0000000000..7cddf89c03 --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/ChartFormat.java @@ -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 80 x 25. + */ + 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 80. + */ + public ChartFormat width(int newWidth) { + return dimensions(newWidth, height); + } + + /** + * The chart width. + */ + public int width() { + return width; + } + + /** + * The new chart height, defaulting to 25. + */ + 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 0. + */ + 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 true. + */ + 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 { 1 }. + */ + 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 { 'X' }. + */ + 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 true. + */ + 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 true. + */ + 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 true. + */ + 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 \n. + */ + 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 ###,###.00. + */ + 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 + } +} diff --git a/jOOQ/src/main/java/org/jooq/Result.java b/jOOQ/src/main/java/org/jooq/Result.java index 207312930a..d8e59f0128 100644 --- a/jOOQ/src/main/java/org/jooq/Result.java +++ b/jOOQ/src/main/java/org/jooq/Result.java @@ -630,6 +630,10 @@ public interface Result extends List, Attachable { */ String formatXML(XMLFormat format); + String formatChart(); + + String formatChart(ChartFormat format); + /** * Get this result as a set of INSERT statements. *

@@ -747,6 +751,20 @@ public interface Result extends List, 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 extends List, 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}. * diff --git a/jOOQ/src/main/java/org/jooq/impl/ResultImpl.java b/jOOQ/src/main/java/org/jooq/impl/ResultImpl.java index e1eeecf27d..9b8cd82f22 100644 --- a/jOOQ/src/main/java/org/jooq/impl/ResultImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/ResultImpl.java @@ -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 implements Result { writer.append(newline).append(format.indentString(recordLevel)).append(""); } + @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> groups = new TreeMap>(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(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 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 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();