From 6db80eba8d88496254aa8d793b19a1b96dde1c39 Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Mon, 18 Nov 2024 23:27:27 +0100 Subject: [PATCH 01/21] =?UTF-8?q?Upgrades=20sirius-web=20=F0=9F=95=B8?= =?UTF-8?q?=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 02e7e2d13..95b657552 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ dev-43.1.0 - dev-84.5.0 + dev-85.3.0 dev-58.3.0 @@ -160,5 +160,17 @@ sevenzipjbinding-all-platforms 16.02-2.01 + + + + org.jfree + jfreechart + 1.5.3 + + + org.apache.xmlgraphics + batik-all + 1.18 + From 2809faf4c3f663e3c9ac6c68b15ca7ebf9b98003 Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Tue, 26 Nov 2024 16:26:33 +0100 Subject: [PATCH 02/21] =?UTF-8?q?Upgrades=20Sirius=20=E2=98=84=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 95b657552..1e524a20f 100644 --- a/pom.xml +++ b/pom.xml @@ -17,8 +17,8 @@ https://www.sirius-lib.net - dev-43.1.0 - dev-85.3.0 + dev-43.2.0 + dev-85.4.1 dev-58.3.0 From b237f2a23bddcd5aacd0d17cfa06ba4bebea0d2f Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Tue, 3 Dec 2024 17:19:18 +0100 Subject: [PATCH 03/21] =?UTF-8?q?Removes=20dependency=20on=20JFreeChart=20?= =?UTF-8?q?=E2=9A=B0=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …as the library will not be used due to "outdated" graphics. OX-9272 --- pom.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pom.xml b/pom.xml index 1e524a20f..6ae9e82c2 100644 --- a/pom.xml +++ b/pom.xml @@ -162,11 +162,6 @@ - - org.jfree - jfreechart - 1.5.3 - org.apache.xmlgraphics batik-all From d9035eb44fed578c668de9e81923d48ce8e65181 Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Thu, 5 Dec 2024 02:47:08 +0100 Subject: [PATCH 04/21] =?UTF-8?q?Adds=20data=20container=20for=20charts=20?= =?UTF-8?q?=F0=9F=93=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OX-9272 --- src/main/java/sirius/biz/charts/Dataset.java | 314 +++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 src/main/java/sirius/biz/charts/Dataset.java diff --git a/src/main/java/sirius/biz/charts/Dataset.java b/src/main/java/sirius/biz/charts/Dataset.java new file mode 100644 index 000000000..0c9a2b05f --- /dev/null +++ b/src/main/java/sirius/biz/charts/Dataset.java @@ -0,0 +1,314 @@ +/* + * Made with all the love in the world + * by scireum in Remshalden, Germany + * + * Copyright by scireum GmbH + * http://www.scireum.de - info@scireum.de + */ + +package sirius.biz.charts; + +import sirius.kernel.commons.Strings; +import sirius.kernel.di.std.Named; + +import javax.annotation.Nonnull; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Optional; +import java.util.SequencedSet; +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * Represents a dataset for charts and tables. + * + * @param the type of the numeric values + */ +public class Dataset implements Named { + + /** + * Contains colour hex-strings which can be used for charts. + */ + public static final List COLORS = List.of("#5eb526", // green + "#497ebf", // blue + "#e74c3c", // red + "#f89406", // orange + "#f1c40f", // yellow + "#9b59b6", // purple + "#1abc9c", // teal + "#bf4d00", // maroon + "#9abddf", // light blue + "#f7b1a3", // light red + "#b6e2d4" // light teal + ); + + private String name; + + private String color; + + private final LinkedHashMap slices = new LinkedHashMap<>(); + + private Optional sum = Optional.empty(); + + /** + * Helper that allows to format the numeric values of the slices. + */ + private Function formatter = Object::toString; + + /** + * Flag to determine if the rendered charts or tables should also display percentages. + */ + private boolean percentagesShown = false; + + /** + * Adds a slice to the dataset. + * + * @param label the label of the slice + * @param quantity the quantity of the slice + * @return the dataset itself for fluent method calls + */ + public Dataset addSlice(String label, N quantity) { + if (Strings.isEmpty(label)) { + throw new IllegalArgumentException("Label must not be empty."); + } + if (quantity.doubleValue() < 0.0) { + throw new IllegalArgumentException("Quantity must be non-negative."); + } + + slices.compute(label, (key, slice) -> { + if (slice == null) { + return new Slice(slices.size(), label, quantity); + } + slice.setQuantity(quantity); + return slice; + }); + sum = Optional.empty(); + + return this; + } + + /** + * Retrieves a slice by its label. + * + * @param label the label of the slice + * @return the slice with the given label or an empty optional if no such slice exists + */ + public Optional resolveSlice(String label) { + return Optional.ofNullable(slices.get(label)); + } + + /** + * Assigns a name to the dataset. + * + * @param name the name to assign + * @return the dataset itself for fluent method calls + */ + public Dataset withName(String name) { + this.name = name; + return this; + } + + /** + * Assigns a color to the dataset. + * + * @param color the color to assign + * @return the dataset itself for fluent method calls + */ + public Dataset withColor(String color) { + this.color = color; + return this; + } + + /** + * Enables the display of percentages for each slice. + * + * @return the dataset itself for fluent method calls + */ + public Dataset withPercentagesShown() { + this.percentagesShown = true; + return this; + } + + /** + * Enables the display of percentages for each slice. + * + * @param percentagesShown determines if the percentages should be displayed + * @return the dataset itself for fluent method calls + */ + public Dataset withPercentagesShown(boolean percentagesShown) { + this.percentagesShown = percentagesShown; + return this; + } + + /** + * Sets a formatter for the numeric values of the slices. + * + * @param formatter the formatter to use + * @return the dataset itself for fluent method calls + */ + public Dataset withFormatter(Function formatter) { + this.formatter = formatter; + return this; + } + + /** + * Computes a new dataset containing the percentages of all slices of this dataset. + * + * @return a new dataset containing the percentages of all slices + */ + public Dataset computePercentageDataset() { + var result = new Dataset().withFormatter(number -> String.format("%.2f %%", number)); + for (Slice slice : slices.values()) { + result.addSlice(slice.getLabel(), slice.percentageValue()); + } + return result; + } + + /** + * Determines the sum of all slices. + * + * @return the sum of all slices + */ + public double sum() { + if (sum.isEmpty()) { + sum = Optional.of(slices.values().stream().map(Slice::getQuantity).mapToDouble(Number::doubleValue).sum()); + } + return sum.get(); + } + + /** + * Streams all slices of the dataset. + * + * @return a stream of all slices + */ + public Stream stream() { + return slices.values().stream(); + } + + @Nonnull + @Override + public String getName() { + return name; + } + + public String getColor() { + return color; + } + + public SequencedSet getLabels() { + return slices.sequencedKeySet(); + } + + public List getSlices() { + return List.copyOf(slices.values()); + } + + /** + * Represents a slice of the datset. + */ + public class Slice { + private final int index; + private final String label; + private N quantity; + private final String color; + + private Slice(int index, String label, N quantity) { + this.index = index; + this.label = label; + this.quantity = quantity; + this.color = COLORS.get(index % COLORS.size()); + } + + /** + * Converts the quantity to a double value. + * + * @return the quantity as a double value + */ + public double doubleValue() { + return quantity.doubleValue(); + } + + /** + * Computes the percentage of this slice. + * + * @return the percentage of this slice + */ + public double percentageValue() { + // the "+ 1.0e-20" is to avoid division by zero; the value is small enough to not affect the result + return 100 * quantity.doubleValue() / (sum() + 1.0e-20); + } + + /** + * Formats the value of this slice as a string. + * + * @return the formatted value of this slice + */ + public String formatValue() { + return formatter.apply(quantity); + } + + /** + * Formats the percentage of this slice as a string. + * + * @return the formatted percentage of this slice + */ + public String formatPercentage() { + if (!percentagesShown) { + return ""; + } + return String.format("%.2f %%", percentageValue()); + } + + /** + * Formats the quantity as a string, potentially including a percentage. + * + * @return the formatted quantity + */ + public String formatQuantity() { + if (!percentagesShown) { + return formatValue(); + } + return new StringBuilder(quantity.toString()).append(String.format(" (%s)", formatPercentage())).toString(); + } + + /** + * Fetches the equivalent slice from a previous dataset. + * + * @param previousDataset the previous dataset to fetch the slice from + * @return the equivalent slice from the previous dataset, or an empty optional if no such slice exists + */ + public Optional fetchPreviousSlice(Dataset previousDataset) { + return previousDataset.resolveSlice(label); + } + + /** + * Fetches the quantity of the equivalent slice from a previous dataset. + * + * @param previousDataset the previous dataset to fetch the quantity from + * @return the quantity of the equivalent slice from the previous dataset, or an empty optional if no such slice exists + */ + public Optional fetchPreviousQuantity(Dataset previousDataset) { + return fetchPreviousSlice(previousDataset).map(Slice::getQuantity); + } + + public int getIndex() { + return index; + } + + public String getLabel() { + return label; + } + + public N getQuantity() { + return quantity; + } + + private void setQuantity(N quantity) { + this.quantity = quantity; + } + + public String getColor() { + return color; + } + } +} From 57bf5ef9c864e36dfe6cd32fcc5c8b642d6d4ca7 Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Thu, 5 Dec 2024 02:47:51 +0100 Subject: [PATCH 05/21] =?UTF-8?q?Adds=20pie=20and=20spider=20charts=20?= =?UTF-8?q?=F0=9F=93=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …that can be rendered as SVGs (for later use in PDFs). OX-9272 --- .../java/sirius/biz/charts/BaseChart.java | 225 +++++++++++ src/main/java/sirius/biz/charts/PieChart.java | 248 ++++++++++++ .../java/sirius/biz/charts/SpiderChart.java | 352 ++++++++++++++++++ 3 files changed, 825 insertions(+) create mode 100644 src/main/java/sirius/biz/charts/BaseChart.java create mode 100644 src/main/java/sirius/biz/charts/PieChart.java create mode 100644 src/main/java/sirius/biz/charts/SpiderChart.java diff --git a/src/main/java/sirius/biz/charts/BaseChart.java b/src/main/java/sirius/biz/charts/BaseChart.java new file mode 100644 index 000000000..d68eb4716 --- /dev/null +++ b/src/main/java/sirius/biz/charts/BaseChart.java @@ -0,0 +1,225 @@ +/* + * Made with all the love in the world + * by scireum in Remshalden, Germany + * + * Copyright by scireum GmbH + * http://www.scireum.de - info@scireum.de + */ + +package sirius.biz.charts; + +import org.apache.batik.anim.dom.SVGDOMImplementation; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.awt.Dimension; + +/** + * Provides an interface for charts that can be rendered as SVG. + */ +public abstract class BaseChart { + + /** + * Contains the tag name for circle elements, {@value}. + */ + protected static final String TAG_CIRCLE = "circle"; + + /** + * Contains the tag name for group elements, {@value}. + */ + protected static final String TAG_G = "g"; + + /** + * Contains the tag name for path elements, {@value}. + */ + protected static final String TAG_PATH = "path"; + + /** + * Contains the tag name for SVG root elements, {@value}. + */ + protected static final String TAG_SVG = "svg"; + + /** + * Contains the tag name for text elements, {@value}. + */ + protected static final String TAG_TEXT = "text"; + + /** + * Contains the attribute name for the center x coordinate, {@value}. + */ + protected static final String ATTRIBUTE_CX = "cx"; + + /** + * Contains the attribute name for the center y coordinate, {@value}. + */ + protected static final String ATTRIBUTE_CY = "cy"; + + /** + * Contains the attribute name for path definitions, {@value}. + */ + protected static final String ATTRIBUTE_D = "d"; + + /** + * Contains the attribute name for the fill colour, {@value}. + */ + protected static final String ATTRIBUTE_FILL = "fill"; + + /** + * Contains the attribute name for the fill opacity, {@value}. + */ + protected static final String ATTRIBUTE_FILL_OPACITY = "fill-opacity"; + + /** + * Contains the attribute name for the font size, {@value}. + */ + protected static final String ATTRIBUTE_FONT_SIZE = "font-size"; + + /** + * Contains the attribute name for the radius, {@value}. + */ + protected static final String ATTRIBUTE_R = "r"; + + /** + * Contains the attribute name for the stroke colour, {@value}. + */ + protected static final String ATTRIBUTE_STROKE = "stroke"; + + /** + * Contains the attribute name for the stroke width, {@value}. + */ + protected static final String ATTRIBUTE_STROKE_WIDTH = "stroke-width"; + + /** + * Contains the attribute name for the text anchor, {@value}. + */ + protected static final String ATTRIBUTE_TEXT_ANCHOR = "text-anchor"; + + /** + * Contains the attribute name for the view box, {@value}. + */ + protected static final String ATTRIBUTE_VIEW_BOX = "viewBox"; + + /** + * Contains the attribute name for the x coordinate, {@value}. + */ + protected static final String ATTRIBUTE_X = "x"; + + /** + * Contains the attribute name for the y coordinate, {@value}. + */ + protected static final String ATTRIBUTE_Y = "y"; + + /** + * Contains the value for the {@linkplain #ATTRIBUTE_FILL fill attribute} to avoid filling, {@value}. + */ + protected static final String VALUE_FILL_NONE = "none"; + + /** + * Contains the value for the {@linkplain #ATTRIBUTE_TEXT_ANCHOR text anchor attribute} to align text at the start, + * {@value}. + */ + protected static final String VALUE_TEXT_ANCHOR_START = "start"; + + /** + * Contains the value for the {@linkplain #ATTRIBUTE_TEXT_ANCHOR text anchor attribute} to align text centered, + * {@value}. + */ + protected static final String VALUE_TEXT_ANCHOR_MIDDLE = "middle"; + + /** + * Contains the value for the {@linkplain #ATTRIBUTE_TEXT_ANCHOR text anchor attribute} to align text at the end, + * {@value}. + */ + protected static final String VALUE_TEXT_ANCHOR_END = "end"; + + /** + * Contains the black colour as hex-string, {@value}. The value is used as primary colour for charts. + */ + protected static final String COLOR_BLACK = "#000000"; + + /** + * Contains the gray colour as hex-string, {@value}. The value is used as secondary colour for charts. + */ + protected static final String COLOR_GRAY = "#808080"; + + /** + * Contains the light gray colour as hex-string, {@value}. The value is used as secondary colour for charts. + */ + protected static final String COLOR_LIGHT_GRAY = "#c0c0c0"; + + /** + * Renders the chart as SVG. + * + * @param bounds the dimensions of the viewport + * @return the SVG representation of the chart + */ + public abstract Element toSvg(Dimension bounds); + + /** + * Creates an empty SVG element with the relevant attributes set. + * + * @return an empty SVG element + */ + protected static Element createSvgElement() { + return SVGDOMImplementation.getDOMImplementation() + .createDocument(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_SVG, null) + .getDocumentElement(); + } + + /** + * Creates an empty SVG element with the view box centered. + * + * @param bounds the dimensions of the viewport + * @return an empty SVG element with the view box centered + */ + protected static Element createSvgElementWithCenteredViewbox(Dimension bounds) { + Element svgElement = createSvgElement(); + svgElement.setAttribute(ATTRIBUTE_VIEW_BOX, + String.format("%f %f %f %f", + -0.5 * bounds.width, + -0.5 * bounds.height, + (double) bounds.width, + (double) bounds.height)); + return svgElement; + } + + /** + * Creates an empty, unattached group element. + * + * @param document the document to create the element in + * @return an empty, unattached group element + */ + protected static Element createGroupElement(Document document) { + return document.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_G); + } + + /** + * Creates an empty, unattached path element. + * + * @param document the document to create the element in + * @return an empty, unattached path element + */ + protected static Element createPathElement(Document document) { + return document.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_PATH); + } + + /** + * Creates an empty, unattached circle element. + * + * @param document the document to create the element in + * @return an empty, unattached circle element + */ + protected static Element createCircleElement(Document document) { + return document.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_CIRCLE); + } + + /** + * Creates an empty, unattached text element. + * + * @param document the document to create the element in + * @return an empty, unattached text element + */ + protected static Element createTextElement(Document document) { + return document.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_TEXT); + } +} diff --git a/src/main/java/sirius/biz/charts/PieChart.java b/src/main/java/sirius/biz/charts/PieChart.java new file mode 100644 index 000000000..dff13bba4 --- /dev/null +++ b/src/main/java/sirius/biz/charts/PieChart.java @@ -0,0 +1,248 @@ +/* + * Made with all the love in the world + * by scireum in Remshalden, Germany + * + * Copyright by scireum GmbH + * http://www.scireum.de - info@scireum.de + */ + +package sirius.biz.charts; + +import org.w3c.dom.Element; +import sirius.kernel.commons.Strings; + +import java.awt.Dimension; +import java.awt.geom.Point2D; +import java.util.concurrent.atomic.DoubleAdder; + +/** + * Represents a pie chart which can be rendered as SVG. + * + * @param the type of the numeric values + */ +public class PieChart extends BaseChart { + + private static final double TWO_PI = 2.0 * Math.PI; + + private static final double RING_WIDTH = 4.0; + + private Dataset dataset; + + /** + * Flag to determine if the pie chart should be rendered as a ring rather than filled. + */ + private boolean ring = false; + + /** + * Sets up the chart with the given dataset. + * + * @param dataset the dataset to render + * @return the chart itself for fluent method calls + */ + public PieChart withDataset(Dataset dataset) { + this.dataset = dataset; + return this; + } + + /** + * Enables the rendering of the pie chart as a ring rather than filled. + * + * @return the chart itself for fluent method calls + */ + public PieChart asRing() { + this.ring = true; + return this; + } + + public Dataset getDataset() { + return dataset; + } + + @Override + public Element toSvg(Dimension bounds) { + Element svgElement = createSvgElementWithCenteredViewbox(bounds); + + Element pieGroupElement = createGroupElement(svgElement.getOwnerDocument()); + svgElement.appendChild(pieGroupElement); + + Element labelsGroupElement = createGroupElement(svgElement.getOwnerDocument()); + svgElement.appendChild(labelsGroupElement); + + double radius = 0.5 * Math.min(bounds.width, bounds.height) - 5.0; + + // determines the multipliers to represent each slice in degrees and as percent + double multiplierRadians = 2.0 * Math.PI / dataset.sum(); + + DoubleAdder accumulatedRadians = new DoubleAdder(); + Point2D pin = new Point2D.Double(); + Point2D label = new Point2D.Double(); + Point2D previousLabel = new Point2D.Double(); + dataset.stream().forEach(slice -> { + double radians = slice.doubleValue() * multiplierRadians; + if (radians <= 0.0) { + return; + } + + double startRadians = accumulatedRadians.doubleValue(); + accumulatedRadians.add(radians); + double endRadians = accumulatedRadians.doubleValue(); + + pieGroupElement.appendChild(createPathElementForSlice(svgElement, + startRadians, + endRadians, + radius, + slice.getColor())); + + double halfRadians = 0.5 * (startRadians + endRadians); + pin.setLocation(Math.sin(halfRadians) * (radius - 0.5 * RING_WIDTH), + -Math.cos(halfRadians) * (radius - 0.5 * RING_WIDTH)); + + label.setLocation((pin.getX() > 0 ? 0.5 : -0.5) * bounds.width, pin.getY()); + String textAnchor = pin.getX() > 0 ? VALUE_TEXT_ANCHOR_END : VALUE_TEXT_ANCHOR_START; + + // skip overlapping labels, relying on an externally printed legend + if (label.distanceSq(previousLabel) < 25.0) { + return; + } + previousLabel.setLocation(label); + + Element labelGroupElement = createGroupElement(svgElement.getOwnerDocument()); + labelsGroupElement.appendChild(labelGroupElement); + + Element labelCircle = createCircleElement(svgElement.getOwnerDocument()); + labelCircle.setAttribute(ATTRIBUTE_CX, Double.toString(pin.getX())); + labelCircle.setAttribute(ATTRIBUTE_CY, Double.toString(pin.getY())); + labelCircle.setAttribute(ATTRIBUTE_R, "0.4"); + labelCircle.setAttribute(ATTRIBUTE_FILL, COLOR_BLACK); + labelGroupElement.appendChild(labelCircle); + + Element labelPath = createPathElement(svgElement.getOwnerDocument()); + labelPath.setAttribute(ATTRIBUTE_D, + String.format("M %f %f L %f %f", + pin.getX(), + pin.getY(), + label.getX(), + label.getY())); + labelPath.setAttribute(ATTRIBUTE_STROKE, COLOR_BLACK); + labelPath.setAttribute(ATTRIBUTE_STROKE_WIDTH, "0.2"); + labelGroupElement.appendChild(labelPath); + + labelGroupElement.appendChild(createTextElementForLabel(svgElement, + label.getX(), + label.getY() - 1.5, + 3.0, + COLOR_BLACK, + textAnchor, + slice.getLabel())); + labelGroupElement.appendChild(createTextElementForLabel(svgElement, + label.getX(), + label.getY() + 3.5, + 3.0, + COLOR_GRAY, + textAnchor, + slice.formatQuantity())); + }); + + return svgElement; + } + + private Element createPathElementForSlice(Element svgElement, + double startRadians, + double endRadians, + double radius, + String color) { + Element piecePath = createPathElement(svgElement.getOwnerDocument()); + + piecePath.setAttribute(ATTRIBUTE_D, assemblePathDefinitionForSlice(startRadians, endRadians, radius)); + piecePath.setAttribute(ATTRIBUTE_STROKE, COLOR_GRAY); + piecePath.setAttribute(ATTRIBUTE_STROKE_WIDTH, "0.1"); + + if (Strings.isFilled(color)) { + piecePath.setAttribute(ATTRIBUTE_FILL, color); + } + + return piecePath; + } + + private String assemblePathDefinitionForSlice(double startRadians, double endRadians, double radius) { + + // if the start and end point are precisely the same, we need to add a small offset to avoid rendering issues + double delta = endRadians - startRadians; + double offset = (Math.abs(delta % TWO_PI) < 2.0e-4) ? 1.0e-4 : 0.0; + + // cache the sine and cosine values, as they are relatively expensive to compute + double startSine = Math.sin(startRadians + offset); + double startCosine = Math.cos(startRadians + offset); + double endSine = Math.sin(endRadians - offset); + double endCosine = Math.cos(endRadians - offset); + + double startX = startSine * radius; + double startY = -startCosine * radius; + + double endX = endSine * radius; + double endY = -endCosine * radius; + + double innerRadius = radius - RING_WIDTH; + int largeArcFlag = delta > Math.PI ? 1 : 0; + + // draw a ring segment + if (ring && innerRadius > 0) { + double innerStartX = startSine * innerRadius; + double innerStartY = -startCosine * innerRadius; + + double innerEndX = endSine * innerRadius; + double innerEndY = -endCosine * innerRadius; + + return String.format("M %f %f A %f %f 0 %d 1 %f %f L %f %f A %f %f 0 %d 0 %f %f Z", + startX, + startY, + radius, + radius, + largeArcFlag, + endX, + endY, + innerEndX, + innerEndY, + innerRadius, + innerRadius, + largeArcFlag, + innerStartX, + innerStartY); + } + + // draw a pie slice + return String.format("M %f %f A %f %f 0 %d 1 %f %f L 0 0 Z", + startX, + startY, + radius, + radius, + largeArcFlag, + endX, + endY); + } + + private Element createTextElementForLabel(Element svgElement, + double x, + double y, + double fontSize, + String fontColor, + String textAnchor, + String text) { + Element valueLabel = createTextElement(svgElement.getOwnerDocument()); + valueLabel.setTextContent(text); + + valueLabel.setAttribute(ATTRIBUTE_X, Double.toString(x)); + valueLabel.setAttribute(ATTRIBUTE_Y, Double.toString(y)); + valueLabel.setAttribute(ATTRIBUTE_FONT_SIZE, Double.toString(fontSize)); + + if (Strings.isFilled(fontColor)) { + valueLabel.setAttribute(ATTRIBUTE_FILL, fontColor); + } + + if (Strings.isFilled(textAnchor)) { + valueLabel.setAttribute(ATTRIBUTE_TEXT_ANCHOR, textAnchor); + } + + return valueLabel; + } +} diff --git a/src/main/java/sirius/biz/charts/SpiderChart.java b/src/main/java/sirius/biz/charts/SpiderChart.java new file mode 100644 index 000000000..befee1628 --- /dev/null +++ b/src/main/java/sirius/biz/charts/SpiderChart.java @@ -0,0 +1,352 @@ +/* + * Made with all the love in the world + * by scireum in Remshalden, Germany + * + * Copyright by scireum GmbH + * http://www.scireum.de - info@scireum.de + */ + +package sirius.biz.charts; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import sirius.kernel.commons.Strings; + +import javax.annotation.Nullable; +import java.awt.Dimension; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.SequencedCollection; +import java.util.SequencedSet; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * Represents a spider chart (radar chart) which can be rendered as SVG. + * + * @param the type of the numeric values + */ +public class SpiderChart extends BaseChart { + + private static final double TICK_LENGTH = 2.0; + + private final SequencedSet labels; + + private final List> datasets = new ArrayList<>(); + + private final List marks = new ArrayList<>(); + + private boolean rings = false; + + /** + * Helper that allows to format the numeric values of the slices. + */ + private Function formatter = Object::toString; + + /** + * Creates a new spider chart with the given axis labels. + * + * @param labels the labels for the chart axes + */ + public SpiderChart(String... labels) { + this(List.of(labels)); + } + + /** + * Creates a new spider chart with the given axis labels. + * + * @param labels the labels for the chart axes + */ + public SpiderChart(SequencedCollection labels) { + if (labels.size() < 3) { + throw new IllegalArgumentException("At least three keys are required."); + } + + this.labels = new LinkedHashSet<>(labels); + } + + /** + * Adds the given dataset to the chart. + * + * @param dataset the dataset to add + * @return the chart itself for fluent method calls + */ + public SpiderChart addDataset(Dataset dataset) { + if (!labels.equals(dataset.getLabels())) { + throw new IllegalArgumentException("Incompatible dataset labels."); + } + this.datasets.add(dataset); + return this; + } + + /** + * Adds a dataset with the given values to the chart. + * + * @param values the values of the dataset + * @return the chart itself for fluent method calls + */ + public SpiderChart addDataset(List values) { + return addDataset(null, values); + } + + /** + * Adds a dataset with the given values to the chart. + * + * @param name the name of the dataset + * @param values the values of the dataset + * @return the chart itself for fluent method calls + */ + public SpiderChart addDataset(@Nullable String name, List values) { + if (values.size() != labels.size()) { + throw new IllegalArgumentException("Incompatible dataset size."); + } + + var dataset = new Dataset().withName(name); + AtomicInteger labelCounter = new AtomicInteger(); + labels.forEach(label -> { + int labelIndex = labelCounter.getAndIncrement(); + dataset.addSlice(label, values.get(labelIndex)); + }); + + return addDataset(dataset); + } + + /** + * Sets the marks for the chart that are rendered as small ticks on the axes. + * + * @param marks the marks to set + * @return the chart itself for fluent method calls + * @see #withRings() + */ + public SpiderChart withMarks(List marks) { + this.marks.clear(); + this.marks.addAll(marks); + this.marks.sort(Comparator.comparingDouble(Number::doubleValue)); + return this; + } + + /** + * Enables drawing of rings around the chart, connecting the equivalent marks of all the axes. + * + * @return the chart itself for fluent method calls + * @see #withMarks(List) + */ + public SpiderChart withRings() { + this.rings = true; + return this; + } + + /** + * Sets a formatter for the numeric values of the marks. + * + * @param formatter the formatter to use + * @return the chart itself for fluent method calls + */ + public SpiderChart withFormatter(Function formatter) { + this.formatter = formatter; + return this; + } + + public SequencedSet getLabels() { + return Collections.unmodifiableSequencedSet(labels); + } + + public List> getDatasets() { + return Collections.unmodifiableList(datasets); + } + + @Override + public Element toSvg(Dimension bounds) { + Element svgElement = createSvgElementWithCenteredViewbox(bounds); + + Element backgroundGroupElement = createGroupElement(svgElement.getOwnerDocument()); + svgElement.appendChild(backgroundGroupElement); + + Element axesGroupElement = createGroupElement(svgElement.getOwnerDocument()); + backgroundGroupElement.appendChild(axesGroupElement); + + Element labelsGroupElement = createGroupElement(svgElement.getOwnerDocument()); + backgroundGroupElement.appendChild(labelsGroupElement); + + Element graphsGroupElement = createGroupElement(svgElement.getOwnerDocument()); + svgElement.appendChild(graphsGroupElement); + + double radius = 0.5 * Math.min(bounds.width, bounds.height) - 7.5; + double normaliser = computeNormaliser(); + + // draw scale with circles and labels + drawAxes(svgElement.getOwnerDocument(), radius, normaliser, axesGroupElement, labelsGroupElement); + + AtomicInteger datasetCounter = new AtomicInteger(); + datasets.forEach(dataset -> { + int datasetIndex = datasetCounter.getAndIncrement(); + StringBuilder pathDefinition = new StringBuilder(); + + dataset.withColor(Dataset.COLORS.get(datasetIndex % Dataset.COLORS.size())); + + AtomicInteger labelCounter = new AtomicInteger(); + labels.forEach(label -> { + int labelIndex = labelCounter.getAndIncrement(); + + double radians = 2.0 * Math.PI * labelIndex / labels.size(); + double sine = Math.sin(radians); + double cosine = Math.cos(radians); + + double value = dataset.resolveSlice(label) + .map(Dataset.Slice::getQuantity) + .map(Number::doubleValue) + .orElse(0.0); + double normalizedValue = radius * value / normaliser; + + pathDefinition.append(pathDefinition.isEmpty() ? "M" : "L") + .append(' ') + .append(sine * normalizedValue) + .append(' ') + .append(-cosine * normalizedValue); + }); + + Element valuePath = createPathElement(svgElement.getOwnerDocument()); + valuePath.setAttribute(ATTRIBUTE_D, pathDefinition.append(" Z").toString()); + valuePath.setAttribute(ATTRIBUTE_STROKE, dataset.getColor()); + valuePath.setAttribute(ATTRIBUTE_FILL, dataset.getColor()); + valuePath.setAttribute(ATTRIBUTE_FILL_OPACITY, "0.5"); + + // graphs are drawn in reverse order to ensure that the first dataset is on top + if (graphsGroupElement.hasChildNodes()) { + graphsGroupElement.insertBefore(valuePath, graphsGroupElement.getFirstChild()); + } else { + graphsGroupElement.appendChild(valuePath); + } + }); + + return svgElement; + } + + /** + * Computes the normaliser for the chart, which is used to scale the values to the chart size. The value is either + * the largest mark or the largest value in the datasets. + * + * @return the normaliser to scale the chart + */ + private double computeNormaliser() { + Stream numbers = marks.isEmpty() ? + datasets.stream().flatMap(dataset -> dataset.stream().map(Dataset.Slice::getQuantity)) : + marks.stream(); + return numbers.mapToDouble(Number::doubleValue).max().orElse(1.0); + } + + private void drawAxes(Document document, + double radius, + double normaliser, + Element axesGroupElement, + Element labelsGroupElement) { + + // first, draw the rings (if enabled) and numeric/value labels along the upright axis + marks.forEach(mark -> { + double markRadius = radius * mark.doubleValue() / normaliser; + + if (rings) { + Element circleElement = createCircleElement(document); + circleElement.setAttribute(ATTRIBUTE_CX, "0"); + circleElement.setAttribute(ATTRIBUTE_CY, "0"); + circleElement.setAttribute(ATTRIBUTE_R, Double.toString(markRadius)); + circleElement.setAttribute(ATTRIBUTE_STROKE, COLOR_LIGHT_GRAY); + circleElement.setAttribute(ATTRIBUTE_FILL, VALUE_FILL_NONE); + circleElement.setAttribute(ATTRIBUTE_STROKE_WIDTH, "0.1"); + axesGroupElement.appendChild(circleElement); + } + + Element markTextElement = createTextElement(document); + markTextElement.setTextContent(formatter.apply(mark)); + markTextElement.setAttribute(ATTRIBUTE_X, Double.toString(1 + 0.5 * TICK_LENGTH)); + markTextElement.setAttribute(ATTRIBUTE_Y, Double.toString(-markRadius + 0.75)); + markTextElement.setAttribute(ATTRIBUTE_FONT_SIZE, Double.toString(3.0)); + markTextElement.setAttribute(ATTRIBUTE_FILL, COLOR_BLACK); + labelsGroupElement.appendChild(markTextElement); + }); + + // then, draw the actual axes and descriptive labels + AtomicInteger labelCounter = new AtomicInteger(); + labels.forEach(label -> { + int labelIndex = labelCounter.getAndIncrement(); + + double radians = 2.0 * Math.PI * labelIndex / labels.size(); + double sine = Math.sin(radians); + double cosine = Math.cos(radians); + + // draw axis + double axisRadius = radius + 2.5; + Element axisPath = createPathElement(document); + axisPath.setAttribute(ATTRIBUTE_D, String.format("M 0 0 L %f %f", sine * axisRadius, -cosine * axisRadius)); + axisPath.setAttribute(ATTRIBUTE_STROKE, COLOR_BLACK); + axisPath.setAttribute(ATTRIBUTE_STROKE_WIDTH, "0.2"); + axesGroupElement.appendChild(axisPath); + + // draw marks/ticks along the axis + marks.forEach(mark -> { + double markRadius = radius * mark.doubleValue() / normaliser; + double markX = sine * markRadius; + double markY = -cosine * markRadius; + + Element markPath = createPathElement(document); + markPath.setAttribute(ATTRIBUTE_D, + String.format("M %f %f L %f %f", + markX + 0.5 * TICK_LENGTH * cosine, + markY + 0.5 * TICK_LENGTH * sine, + markX - 0.5 * TICK_LENGTH * cosine, + markY - 0.5 * TICK_LENGTH * sine)); + markPath.setAttribute(ATTRIBUTE_STROKE, COLOR_BLACK); + markPath.setAttribute(ATTRIBUTE_STROKE_WIDTH, "0.2"); + axesGroupElement.appendChild(markPath); + }); + + // draw label at the end of the axis + double labelRadius = radius + 3.5; + labelsGroupElement.appendChild(createTextElementForAxisLabel(document, + sine * labelRadius, + -cosine * labelRadius, + label, + COLOR_BLACK)); + }); + } + + private Element createTextElementForAxisLabel(Document document, double x, double y, String label, String color) { + String textAnchor; + if (x < -0.1) { + textAnchor = VALUE_TEXT_ANCHOR_END; + } else if (x > 0.1) { + textAnchor = VALUE_TEXT_ANCHOR_START; + } else { + textAnchor = VALUE_TEXT_ANCHOR_MIDDLE; + } + + double yOffset; + if (y < -0.1) { + yOffset = 0.0; + } else if (y > 0.1) { + yOffset = 2.5; + } else { + yOffset = 0.75; + } + + Element labelElement = createTextElement(document); + labelElement.setTextContent(label); + + labelElement.setAttribute(ATTRIBUTE_X, Double.toString(x)); + labelElement.setAttribute(ATTRIBUTE_Y, Double.toString(y + yOffset)); + labelElement.setAttribute(ATTRIBUTE_FONT_SIZE, Double.toString(3.0)); + + if (Strings.isFilled(color)) { + labelElement.setAttribute(ATTRIBUTE_FILL, color); + } + + // compute text anchor + labelElement.setAttribute(ATTRIBUTE_TEXT_ANCHOR, textAnchor); + + return labelElement; + } +} From 2a5d29f84ffe5ae836fe5ba7129f29765aba8d1d Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Thu, 5 Dec 2024 02:48:41 +0100 Subject: [PATCH 06/21] =?UTF-8?q?Adds=20helper=20for=20obtaining=20a=20PDF?= =?UTF-8?q?-compatible=20SCG=20chart=20=F0=9F=96=A8=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OX-9272 --- src/main/java/sirius/biz/charts/Charts.java | 47 +++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/main/java/sirius/biz/charts/Charts.java diff --git a/src/main/java/sirius/biz/charts/Charts.java b/src/main/java/sirius/biz/charts/Charts.java new file mode 100644 index 000000000..4093514c5 --- /dev/null +++ b/src/main/java/sirius/biz/charts/Charts.java @@ -0,0 +1,47 @@ +/* + * Made with all the love in the world + * by scireum in Remshalden, Germany + * + * Copyright by scireum GmbH + * http://www.scireum.de - info@scireum.de + */ + +package sirius.biz.charts; + +import com.lowagie.text.xml.XmlDomWriter; +import org.w3c.dom.Element; +import sirius.kernel.di.std.Register; +import sirius.web.templates.pdf.TagliatellePDFContentHandler; + +import java.awt.Dimension; +import java.io.StringWriter; + +/** + * Provides helpers to work with charts. + */ +@Register(classes = Charts.class) +public class Charts { + + /** + * Exports a chart as SVG string that is compatible with {@linkplain TagliatellePDFContentHandler PDF rendering}. + * + * @param chart the chart to export + * @param bounds the dimensions of the viewport + * @return the SVG string representing the chart + */ + public String exportChartForPdf(BaseChart chart, Dimension bounds) { + + // we need to clean the SVG code a bit to make it compatible with the PDF renderer + Element element = chart.toSvg(bounds); + element.setAttribute("style", + String.format("display: block; width: 100%%; height: %dmm; page-break-inside: avoid;", + bounds.height)); + + // note that the string writer uses a string buffer internally; no additional buffering or flushing is required + StringWriter out = new StringWriter(); + XmlDomWriter xmlOut = new XmlDomWriter(); + xmlOut.setOutput(out); + xmlOut.write(element); + return out.toString(); + } +} From 6971bdc546bb8596d5d442fd90a61cadbf377fd9 Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Thu, 5 Dec 2024 02:52:29 +0100 Subject: [PATCH 07/21] =?UTF-8?q?Bumps=20Sirius=20dependencies=20?= =?UTF-8?q?=F0=9F=AA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …to the versions used in OX release 2024.12 OX-9272 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 6ae9e82c2..3160eeda3 100644 --- a/pom.xml +++ b/pom.xml @@ -17,8 +17,8 @@ https://www.sirius-lib.net - dev-43.2.0 - dev-85.4.1 + dev-43.2.1 + dev-85.5.0 dev-58.3.0 From bcf0fbe99f0d1dbc42d6ea34251a23918f4d6ee1 Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Wed, 11 Dec 2024 12:27:03 +0100 Subject: [PATCH 08/21] =?UTF-8?q?Removes=20blank=20line=20=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OX-9272 Co-authored-by: Sascha Bieberstein --- src/main/java/sirius/biz/charts/Charts.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/sirius/biz/charts/Charts.java b/src/main/java/sirius/biz/charts/Charts.java index 4093514c5..969c44936 100644 --- a/src/main/java/sirius/biz/charts/Charts.java +++ b/src/main/java/sirius/biz/charts/Charts.java @@ -30,7 +30,6 @@ public class Charts { * @return the SVG string representing the chart */ public String exportChartForPdf(BaseChart chart, Dimension bounds) { - // we need to clean the SVG code a bit to make it compatible with the PDF renderer Element element = chart.toSvg(bounds); element.setAttribute("style", From 9e920cabb1fb60e3f344af7e1a0fd8639c5ff3c0 Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Wed, 11 Dec 2024 12:34:10 +0100 Subject: [PATCH 09/21] =?UTF-8?q?Inlines=20helpers=20=F0=9F=A5=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …as requested by @sabieber during review. OX-9272 --- .../java/sirius/biz/charts/BaseChart.java | 41 ------------------- src/main/java/sirius/biz/charts/PieChart.java | 22 ++++++---- .../java/sirius/biz/charts/SpiderChart.java | 26 +++++++----- 3 files changed, 31 insertions(+), 58 deletions(-) diff --git a/src/main/java/sirius/biz/charts/BaseChart.java b/src/main/java/sirius/biz/charts/BaseChart.java index d68eb4716..574d26c94 100644 --- a/src/main/java/sirius/biz/charts/BaseChart.java +++ b/src/main/java/sirius/biz/charts/BaseChart.java @@ -9,7 +9,6 @@ package sirius.biz.charts; import org.apache.batik.anim.dom.SVGDOMImplementation; -import org.w3c.dom.Document; import org.w3c.dom.Element; import java.awt.Dimension; @@ -182,44 +181,4 @@ protected static Element createSvgElementWithCenteredViewbox(Dimension bounds) { (double) bounds.height)); return svgElement; } - - /** - * Creates an empty, unattached group element. - * - * @param document the document to create the element in - * @return an empty, unattached group element - */ - protected static Element createGroupElement(Document document) { - return document.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_G); - } - - /** - * Creates an empty, unattached path element. - * - * @param document the document to create the element in - * @return an empty, unattached path element - */ - protected static Element createPathElement(Document document) { - return document.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_PATH); - } - - /** - * Creates an empty, unattached circle element. - * - * @param document the document to create the element in - * @return an empty, unattached circle element - */ - protected static Element createCircleElement(Document document) { - return document.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_CIRCLE); - } - - /** - * Creates an empty, unattached text element. - * - * @param document the document to create the element in - * @return an empty, unattached text element - */ - protected static Element createTextElement(Document document) { - return document.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_TEXT); - } } diff --git a/src/main/java/sirius/biz/charts/PieChart.java b/src/main/java/sirius/biz/charts/PieChart.java index dff13bba4..fe2b646c6 100644 --- a/src/main/java/sirius/biz/charts/PieChart.java +++ b/src/main/java/sirius/biz/charts/PieChart.java @@ -8,6 +8,7 @@ package sirius.biz.charts; +import org.apache.batik.anim.dom.SVGDOMImplementation; import org.w3c.dom.Element; import sirius.kernel.commons.Strings; @@ -62,10 +63,12 @@ public Dataset getDataset() { public Element toSvg(Dimension bounds) { Element svgElement = createSvgElementWithCenteredViewbox(bounds); - Element pieGroupElement = createGroupElement(svgElement.getOwnerDocument()); + Element pieGroupElement = + svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_G); svgElement.appendChild(pieGroupElement); - Element labelsGroupElement = createGroupElement(svgElement.getOwnerDocument()); + Element labelsGroupElement = + svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_G); svgElement.appendChild(labelsGroupElement); double radius = 0.5 * Math.min(bounds.width, bounds.height) - 5.0; @@ -106,17 +109,20 @@ public Element toSvg(Dimension bounds) { } previousLabel.setLocation(label); - Element labelGroupElement = createGroupElement(svgElement.getOwnerDocument()); + Element labelGroupElement = + svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_G); labelsGroupElement.appendChild(labelGroupElement); - Element labelCircle = createCircleElement(svgElement.getOwnerDocument()); + Element labelCircle = + svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_CIRCLE); labelCircle.setAttribute(ATTRIBUTE_CX, Double.toString(pin.getX())); labelCircle.setAttribute(ATTRIBUTE_CY, Double.toString(pin.getY())); labelCircle.setAttribute(ATTRIBUTE_R, "0.4"); labelCircle.setAttribute(ATTRIBUTE_FILL, COLOR_BLACK); labelGroupElement.appendChild(labelCircle); - Element labelPath = createPathElement(svgElement.getOwnerDocument()); + Element labelPath = + svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_PATH); labelPath.setAttribute(ATTRIBUTE_D, String.format("M %f %f L %f %f", pin.getX(), @@ -151,7 +157,8 @@ private Element createPathElementForSlice(Element svgElement, double endRadians, double radius, String color) { - Element piecePath = createPathElement(svgElement.getOwnerDocument()); + Element piecePath = + svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_PATH); piecePath.setAttribute(ATTRIBUTE_D, assemblePathDefinitionForSlice(startRadians, endRadians, radius)); piecePath.setAttribute(ATTRIBUTE_STROKE, COLOR_GRAY); @@ -228,7 +235,8 @@ private Element createTextElementForLabel(Element svgElement, String fontColor, String textAnchor, String text) { - Element valueLabel = createTextElement(svgElement.getOwnerDocument()); + Element valueLabel = + svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_TEXT); valueLabel.setTextContent(text); valueLabel.setAttribute(ATTRIBUTE_X, Double.toString(x)); diff --git a/src/main/java/sirius/biz/charts/SpiderChart.java b/src/main/java/sirius/biz/charts/SpiderChart.java index befee1628..09ac8131e 100644 --- a/src/main/java/sirius/biz/charts/SpiderChart.java +++ b/src/main/java/sirius/biz/charts/SpiderChart.java @@ -8,6 +8,7 @@ package sirius.biz.charts; +import org.apache.batik.anim.dom.SVGDOMImplementation; import org.w3c.dom.Document; import org.w3c.dom.Element; import sirius.kernel.commons.Strings; @@ -163,16 +164,20 @@ public List> getDatasets() { public Element toSvg(Dimension bounds) { Element svgElement = createSvgElementWithCenteredViewbox(bounds); - Element backgroundGroupElement = createGroupElement(svgElement.getOwnerDocument()); + Element backgroundGroupElement = + svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_G); svgElement.appendChild(backgroundGroupElement); - Element axesGroupElement = createGroupElement(svgElement.getOwnerDocument()); + Element axesGroupElement = + svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_G); backgroundGroupElement.appendChild(axesGroupElement); - Element labelsGroupElement = createGroupElement(svgElement.getOwnerDocument()); + Element labelsGroupElement = + svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_G); backgroundGroupElement.appendChild(labelsGroupElement); - Element graphsGroupElement = createGroupElement(svgElement.getOwnerDocument()); + Element graphsGroupElement = + svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_G); svgElement.appendChild(graphsGroupElement); double radius = 0.5 * Math.min(bounds.width, bounds.height) - 7.5; @@ -209,7 +214,8 @@ public Element toSvg(Dimension bounds) { .append(-cosine * normalizedValue); }); - Element valuePath = createPathElement(svgElement.getOwnerDocument()); + Element valuePath = + svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_PATH); valuePath.setAttribute(ATTRIBUTE_D, pathDefinition.append(" Z").toString()); valuePath.setAttribute(ATTRIBUTE_STROKE, dataset.getColor()); valuePath.setAttribute(ATTRIBUTE_FILL, dataset.getColor()); @@ -250,7 +256,7 @@ private void drawAxes(Document document, double markRadius = radius * mark.doubleValue() / normaliser; if (rings) { - Element circleElement = createCircleElement(document); + Element circleElement = document.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_CIRCLE); circleElement.setAttribute(ATTRIBUTE_CX, "0"); circleElement.setAttribute(ATTRIBUTE_CY, "0"); circleElement.setAttribute(ATTRIBUTE_R, Double.toString(markRadius)); @@ -260,7 +266,7 @@ private void drawAxes(Document document, axesGroupElement.appendChild(circleElement); } - Element markTextElement = createTextElement(document); + Element markTextElement = document.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_TEXT); markTextElement.setTextContent(formatter.apply(mark)); markTextElement.setAttribute(ATTRIBUTE_X, Double.toString(1 + 0.5 * TICK_LENGTH)); markTextElement.setAttribute(ATTRIBUTE_Y, Double.toString(-markRadius + 0.75)); @@ -280,7 +286,7 @@ private void drawAxes(Document document, // draw axis double axisRadius = radius + 2.5; - Element axisPath = createPathElement(document); + Element axisPath = document.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_PATH); axisPath.setAttribute(ATTRIBUTE_D, String.format("M 0 0 L %f %f", sine * axisRadius, -cosine * axisRadius)); axisPath.setAttribute(ATTRIBUTE_STROKE, COLOR_BLACK); axisPath.setAttribute(ATTRIBUTE_STROKE_WIDTH, "0.2"); @@ -292,7 +298,7 @@ private void drawAxes(Document document, double markX = sine * markRadius; double markY = -cosine * markRadius; - Element markPath = createPathElement(document); + Element markPath = document.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_PATH); markPath.setAttribute(ATTRIBUTE_D, String.format("M %f %f L %f %f", markX + 0.5 * TICK_LENGTH * cosine, @@ -333,7 +339,7 @@ private Element createTextElementForAxisLabel(Document document, double x, doubl yOffset = 0.75; } - Element labelElement = createTextElement(document); + Element labelElement = document.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_TEXT); labelElement.setTextContent(label); labelElement.setAttribute(ATTRIBUTE_X, Double.toString(x)); From e7aa697d31438b24d4e41db2bb541573d9b41d07 Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Wed, 11 Dec 2024 12:41:45 +0100 Subject: [PATCH 10/21] =?UTF-8?q?Moves=20remaining=20static=20methods=20in?= =?UTF-8?q?to=20helper=20class=20=F0=9F=9A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …as requested by @sabieber during review. OX-9272 --- .../java/sirius/biz/charts/BaseChart.java | 29 ------------------- src/main/java/sirius/biz/charts/Charts.java | 29 +++++++++++++++++++ src/main/java/sirius/biz/charts/PieChart.java | 2 +- .../java/sirius/biz/charts/SpiderChart.java | 2 +- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/main/java/sirius/biz/charts/BaseChart.java b/src/main/java/sirius/biz/charts/BaseChart.java index 574d26c94..5db6ae22b 100644 --- a/src/main/java/sirius/biz/charts/BaseChart.java +++ b/src/main/java/sirius/biz/charts/BaseChart.java @@ -8,7 +8,6 @@ package sirius.biz.charts; -import org.apache.batik.anim.dom.SVGDOMImplementation; import org.w3c.dom.Element; import java.awt.Dimension; @@ -153,32 +152,4 @@ public abstract class BaseChart { * @return the SVG representation of the chart */ public abstract Element toSvg(Dimension bounds); - - /** - * Creates an empty SVG element with the relevant attributes set. - * - * @return an empty SVG element - */ - protected static Element createSvgElement() { - return SVGDOMImplementation.getDOMImplementation() - .createDocument(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_SVG, null) - .getDocumentElement(); - } - - /** - * Creates an empty SVG element with the view box centered. - * - * @param bounds the dimensions of the viewport - * @return an empty SVG element with the view box centered - */ - protected static Element createSvgElementWithCenteredViewbox(Dimension bounds) { - Element svgElement = createSvgElement(); - svgElement.setAttribute(ATTRIBUTE_VIEW_BOX, - String.format("%f %f %f %f", - -0.5 * bounds.width, - -0.5 * bounds.height, - (double) bounds.width, - (double) bounds.height)); - return svgElement; - } } diff --git a/src/main/java/sirius/biz/charts/Charts.java b/src/main/java/sirius/biz/charts/Charts.java index 969c44936..b91c40bb3 100644 --- a/src/main/java/sirius/biz/charts/Charts.java +++ b/src/main/java/sirius/biz/charts/Charts.java @@ -9,6 +9,7 @@ package sirius.biz.charts; import com.lowagie.text.xml.XmlDomWriter; +import org.apache.batik.anim.dom.SVGDOMImplementation; import org.w3c.dom.Element; import sirius.kernel.di.std.Register; import sirius.web.templates.pdf.TagliatellePDFContentHandler; @@ -43,4 +44,32 @@ public String exportChartForPdf(BaseChart chart, Dimension bounds) { xmlOut.write(element); return out.toString(); } + + /** + * Creates an empty SVG element with the relevant attributes set. + * + * @return an empty SVG element + */ + protected static Element createSvgElement() { + return SVGDOMImplementation.getDOMImplementation() + .createDocument(SVGDOMImplementation.SVG_NAMESPACE_URI, BaseChart.TAG_SVG, null) + .getDocumentElement(); + } + + /** + * Creates an empty SVG element with the view box centered. + * + * @param bounds the dimensions of the viewport + * @return an empty SVG element with the view box centered + */ + protected static Element createSvgElementWithCenteredViewbox(Dimension bounds) { + Element svgElement = createSvgElement(); + svgElement.setAttribute(BaseChart.ATTRIBUTE_VIEW_BOX, + String.format("%f %f %f %f", + -0.5 * bounds.width, + -0.5 * bounds.height, + (double) bounds.width, + (double) bounds.height)); + return svgElement; + } } diff --git a/src/main/java/sirius/biz/charts/PieChart.java b/src/main/java/sirius/biz/charts/PieChart.java index fe2b646c6..b228ee162 100644 --- a/src/main/java/sirius/biz/charts/PieChart.java +++ b/src/main/java/sirius/biz/charts/PieChart.java @@ -61,7 +61,7 @@ public Dataset getDataset() { @Override public Element toSvg(Dimension bounds) { - Element svgElement = createSvgElementWithCenteredViewbox(bounds); + Element svgElement = Charts.createSvgElementWithCenteredViewbox(bounds); Element pieGroupElement = svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_G); diff --git a/src/main/java/sirius/biz/charts/SpiderChart.java b/src/main/java/sirius/biz/charts/SpiderChart.java index 09ac8131e..d16888004 100644 --- a/src/main/java/sirius/biz/charts/SpiderChart.java +++ b/src/main/java/sirius/biz/charts/SpiderChart.java @@ -162,7 +162,7 @@ public List> getDatasets() { @Override public Element toSvg(Dimension bounds) { - Element svgElement = createSvgElementWithCenteredViewbox(bounds); + Element svgElement = Charts.createSvgElementWithCenteredViewbox(bounds); Element backgroundGroupElement = svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_G); From 9b2fad4b9773668d5fd32c855f167f4e59c5cc7e Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Wed, 11 Dec 2024 12:43:04 +0100 Subject: [PATCH 11/21] =?UTF-8?q?Inlines=20another=20helper=20=F0=9F=AB=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OX-9272 --- src/main/java/sirius/biz/charts/Charts.java | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/main/java/sirius/biz/charts/Charts.java b/src/main/java/sirius/biz/charts/Charts.java index b91c40bb3..149aa54cd 100644 --- a/src/main/java/sirius/biz/charts/Charts.java +++ b/src/main/java/sirius/biz/charts/Charts.java @@ -45,17 +45,6 @@ public String exportChartForPdf(BaseChart chart, Dimension bounds) { return out.toString(); } - /** - * Creates an empty SVG element with the relevant attributes set. - * - * @return an empty SVG element - */ - protected static Element createSvgElement() { - return SVGDOMImplementation.getDOMImplementation() - .createDocument(SVGDOMImplementation.SVG_NAMESPACE_URI, BaseChart.TAG_SVG, null) - .getDocumentElement(); - } - /** * Creates an empty SVG element with the view box centered. * @@ -63,7 +52,11 @@ protected static Element createSvgElement() { * @return an empty SVG element with the view box centered */ protected static Element createSvgElementWithCenteredViewbox(Dimension bounds) { - Element svgElement = createSvgElement(); + Element svgElement = SVGDOMImplementation.getDOMImplementation() + .createDocument(SVGDOMImplementation.SVG_NAMESPACE_URI, + BaseChart.TAG_SVG, + null) + .getDocumentElement(); svgElement.setAttribute(BaseChart.ATTRIBUTE_VIEW_BOX, String.format("%f %f %f %f", -0.5 * bounds.width, From c98cb8f24f6c0135faf41f4dd62c112130ef6aa7 Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Wed, 11 Dec 2024 12:45:34 +0100 Subject: [PATCH 12/21] =?UTF-8?q?Renames=20variables=20=F0=9F=93=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …as requested by @sabieber during review. OX-9272 Co-authored-by: Sascha Bieberstein --- src/main/java/sirius/biz/charts/Charts.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/sirius/biz/charts/Charts.java b/src/main/java/sirius/biz/charts/Charts.java index 149aa54cd..70ce1914f 100644 --- a/src/main/java/sirius/biz/charts/Charts.java +++ b/src/main/java/sirius/biz/charts/Charts.java @@ -38,11 +38,11 @@ public String exportChartForPdf(BaseChart chart, Dimension bounds) { bounds.height)); // note that the string writer uses a string buffer internally; no additional buffering or flushing is required - StringWriter out = new StringWriter(); - XmlDomWriter xmlOut = new XmlDomWriter(); - xmlOut.setOutput(out); - xmlOut.write(element); - return out.toString(); + StringWriter writer = new StringWriter(); + XmlDomWriter xmlWriter = new XmlDomWriter(); + xmlWriter.setOutput(writer); + xmlWriter.write(element); + return writer.toString(); } /** From 49dc88c6d4a26b22f903e39b729050c4e8b60384 Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Wed, 11 Dec 2024 13:00:46 +0100 Subject: [PATCH 13/21] =?UTF-8?q?Uses=20`Math.TAU`=20=F0=9F=90=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …as inspired by a review comment by @sabieber. OX-9272 --- src/main/java/sirius/biz/charts/PieChart.java | 6 ++---- src/main/java/sirius/biz/charts/SpiderChart.java | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/sirius/biz/charts/PieChart.java b/src/main/java/sirius/biz/charts/PieChart.java index b228ee162..f1b353fd9 100644 --- a/src/main/java/sirius/biz/charts/PieChart.java +++ b/src/main/java/sirius/biz/charts/PieChart.java @@ -23,8 +23,6 @@ */ public class PieChart extends BaseChart { - private static final double TWO_PI = 2.0 * Math.PI; - private static final double RING_WIDTH = 4.0; private Dataset dataset; @@ -74,7 +72,7 @@ public Element toSvg(Dimension bounds) { double radius = 0.5 * Math.min(bounds.width, bounds.height) - 5.0; // determines the multipliers to represent each slice in degrees and as percent - double multiplierRadians = 2.0 * Math.PI / dataset.sum(); + double multiplierRadians = Math.TAU / dataset.sum(); DoubleAdder accumulatedRadians = new DoubleAdder(); Point2D pin = new Point2D.Double(); @@ -175,7 +173,7 @@ private String assemblePathDefinitionForSlice(double startRadians, double endRad // if the start and end point are precisely the same, we need to add a small offset to avoid rendering issues double delta = endRadians - startRadians; - double offset = (Math.abs(delta % TWO_PI) < 2.0e-4) ? 1.0e-4 : 0.0; + double offset = (Math.abs(delta % Math.TAU) < 2.0e-4) ? 1.0e-4 : 0.0; // cache the sine and cosine values, as they are relatively expensive to compute double startSine = Math.sin(startRadians + offset); diff --git a/src/main/java/sirius/biz/charts/SpiderChart.java b/src/main/java/sirius/biz/charts/SpiderChart.java index d16888004..284611a9c 100644 --- a/src/main/java/sirius/biz/charts/SpiderChart.java +++ b/src/main/java/sirius/biz/charts/SpiderChart.java @@ -197,7 +197,7 @@ public Element toSvg(Dimension bounds) { labels.forEach(label -> { int labelIndex = labelCounter.getAndIncrement(); - double radians = 2.0 * Math.PI * labelIndex / labels.size(); + double radians = Math.TAU * labelIndex / labels.size(); double sine = Math.sin(radians); double cosine = Math.cos(radians); @@ -280,7 +280,7 @@ private void drawAxes(Document document, labels.forEach(label -> { int labelIndex = labelCounter.getAndIncrement(); - double radians = 2.0 * Math.PI * labelIndex / labels.size(); + double radians = Math.TAU * labelIndex / labels.size(); double sine = Math.sin(radians); double cosine = Math.cos(radians); From ffb311292fb12c58009d2c2fe5364874c9f0994d Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Wed, 11 Dec 2024 14:02:25 +0100 Subject: [PATCH 14/21] =?UTF-8?q?Renames=20abstract=20base=20class=20?= =?UTF-8?q?=F0=9F=93=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …as requested by @sabieber during review, OX-9272 --- .../java/sirius/biz/charts/{BaseChart.java => Chart.java} | 2 +- src/main/java/sirius/biz/charts/Charts.java | 6 +++--- src/main/java/sirius/biz/charts/PieChart.java | 2 +- src/main/java/sirius/biz/charts/SpiderChart.java | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) rename src/main/java/sirius/biz/charts/{BaseChart.java => Chart.java} (99%) diff --git a/src/main/java/sirius/biz/charts/BaseChart.java b/src/main/java/sirius/biz/charts/Chart.java similarity index 99% rename from src/main/java/sirius/biz/charts/BaseChart.java rename to src/main/java/sirius/biz/charts/Chart.java index 5db6ae22b..2cbdc6212 100644 --- a/src/main/java/sirius/biz/charts/BaseChart.java +++ b/src/main/java/sirius/biz/charts/Chart.java @@ -15,7 +15,7 @@ /** * Provides an interface for charts that can be rendered as SVG. */ -public abstract class BaseChart { +public abstract class Chart { /** * Contains the tag name for circle elements, {@value}. diff --git a/src/main/java/sirius/biz/charts/Charts.java b/src/main/java/sirius/biz/charts/Charts.java index 70ce1914f..d44fd05e4 100644 --- a/src/main/java/sirius/biz/charts/Charts.java +++ b/src/main/java/sirius/biz/charts/Charts.java @@ -30,7 +30,7 @@ public class Charts { * @param bounds the dimensions of the viewport * @return the SVG string representing the chart */ - public String exportChartForPdf(BaseChart chart, Dimension bounds) { + public String exportChartForPdf(Chart chart, Dimension bounds) { // we need to clean the SVG code a bit to make it compatible with the PDF renderer Element element = chart.toSvg(bounds); element.setAttribute("style", @@ -54,10 +54,10 @@ public String exportChartForPdf(BaseChart chart, Dimension bounds) { protected static Element createSvgElementWithCenteredViewbox(Dimension bounds) { Element svgElement = SVGDOMImplementation.getDOMImplementation() .createDocument(SVGDOMImplementation.SVG_NAMESPACE_URI, - BaseChart.TAG_SVG, + Chart.TAG_SVG, null) .getDocumentElement(); - svgElement.setAttribute(BaseChart.ATTRIBUTE_VIEW_BOX, + svgElement.setAttribute(Chart.ATTRIBUTE_VIEW_BOX, String.format("%f %f %f %f", -0.5 * bounds.width, -0.5 * bounds.height, diff --git a/src/main/java/sirius/biz/charts/PieChart.java b/src/main/java/sirius/biz/charts/PieChart.java index f1b353fd9..3fbdc9166 100644 --- a/src/main/java/sirius/biz/charts/PieChart.java +++ b/src/main/java/sirius/biz/charts/PieChart.java @@ -21,7 +21,7 @@ * * @param the type of the numeric values */ -public class PieChart extends BaseChart { +public class PieChart extends Chart { private static final double RING_WIDTH = 4.0; diff --git a/src/main/java/sirius/biz/charts/SpiderChart.java b/src/main/java/sirius/biz/charts/SpiderChart.java index 284611a9c..72fc7a9e8 100644 --- a/src/main/java/sirius/biz/charts/SpiderChart.java +++ b/src/main/java/sirius/biz/charts/SpiderChart.java @@ -31,7 +31,7 @@ * * @param the type of the numeric values */ -public class SpiderChart extends BaseChart { +public class SpiderChart extends Chart { private static final double TICK_LENGTH = 2.0; From abda5ac31837c221cbd6c498a52253a83a0305b8 Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Wed, 11 Dec 2024 14:30:05 +0100 Subject: [PATCH 15/21] =?UTF-8?q?Adapts=20gray=20colors=20to=20design=20sy?= =?UTF-8?q?stem=20definitions=20=F0=9F=8E=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …as suggested by @sabieber during review. OX-9272 --- src/main/java/sirius/biz/charts/Chart.java | 12 ++++++++---- src/main/java/sirius/biz/charts/PieChart.java | 4 ++-- src/main/java/sirius/biz/charts/SpiderChart.java | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/sirius/biz/charts/Chart.java b/src/main/java/sirius/biz/charts/Chart.java index 2cbdc6212..beb19f4b0 100644 --- a/src/main/java/sirius/biz/charts/Chart.java +++ b/src/main/java/sirius/biz/charts/Chart.java @@ -136,14 +136,18 @@ public abstract class Chart { protected static final String COLOR_BLACK = "#000000"; /** - * Contains the gray colour as hex-string, {@value}. The value is used as secondary colour for charts. + * Contains a dark gray colour as hex-string, {@value}. Its design system equivalent is {@code sirius-gray-dark}. + *

+ * The value is used as secondary colour for charts. */ - protected static final String COLOR_GRAY = "#808080"; + protected static final String COLOR_GRAY_DARK = "#808080"; /** - * Contains the light gray colour as hex-string, {@value}. The value is used as secondary colour for charts. + * Contains the light gray colour as hex-string, {@value}. Its design system equivalent is {@code sirius-gray-light}. + *

+ * The value is used as secondary colour for charts. */ - protected static final String COLOR_LIGHT_GRAY = "#c0c0c0"; + protected static final String COLOR_GRAY_LIGHT = "#c7c7c7"; /** * Renders the chart as SVG. diff --git a/src/main/java/sirius/biz/charts/PieChart.java b/src/main/java/sirius/biz/charts/PieChart.java index 3fbdc9166..51d48c56e 100644 --- a/src/main/java/sirius/biz/charts/PieChart.java +++ b/src/main/java/sirius/biz/charts/PieChart.java @@ -142,7 +142,7 @@ public Element toSvg(Dimension bounds) { label.getX(), label.getY() + 3.5, 3.0, - COLOR_GRAY, + COLOR_GRAY_DARK, textAnchor, slice.formatQuantity())); }); @@ -159,7 +159,7 @@ private Element createPathElementForSlice(Element svgElement, svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_PATH); piecePath.setAttribute(ATTRIBUTE_D, assemblePathDefinitionForSlice(startRadians, endRadians, radius)); - piecePath.setAttribute(ATTRIBUTE_STROKE, COLOR_GRAY); + piecePath.setAttribute(ATTRIBUTE_STROKE, COLOR_GRAY_DARK); piecePath.setAttribute(ATTRIBUTE_STROKE_WIDTH, "0.1"); if (Strings.isFilled(color)) { diff --git a/src/main/java/sirius/biz/charts/SpiderChart.java b/src/main/java/sirius/biz/charts/SpiderChart.java index 72fc7a9e8..7b46e182a 100644 --- a/src/main/java/sirius/biz/charts/SpiderChart.java +++ b/src/main/java/sirius/biz/charts/SpiderChart.java @@ -260,7 +260,7 @@ private void drawAxes(Document document, circleElement.setAttribute(ATTRIBUTE_CX, "0"); circleElement.setAttribute(ATTRIBUTE_CY, "0"); circleElement.setAttribute(ATTRIBUTE_R, Double.toString(markRadius)); - circleElement.setAttribute(ATTRIBUTE_STROKE, COLOR_LIGHT_GRAY); + circleElement.setAttribute(ATTRIBUTE_STROKE, COLOR_GRAY_LIGHT); circleElement.setAttribute(ATTRIBUTE_FILL, VALUE_FILL_NONE); circleElement.setAttribute(ATTRIBUTE_STROKE_WIDTH, "0.1"); axesGroupElement.appendChild(circleElement); From db3ac87e581e00a28f019bec0a45c730bbd6be29 Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Wed, 11 Dec 2024 14:37:56 +0100 Subject: [PATCH 16/21] =?UTF-8?q?Renames=20spider=20to=20radar=20chart=20?= =?UTF-8?q?=F0=9F=95=B7=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …as requested by @sabieber during review. OX-9272 --- .../{SpiderChart.java => RadarChart.java} | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) rename src/main/java/sirius/biz/charts/{SpiderChart.java => RadarChart.java} (96%) diff --git a/src/main/java/sirius/biz/charts/SpiderChart.java b/src/main/java/sirius/biz/charts/RadarChart.java similarity index 96% rename from src/main/java/sirius/biz/charts/SpiderChart.java rename to src/main/java/sirius/biz/charts/RadarChart.java index 7b46e182a..cf0e639ec 100644 --- a/src/main/java/sirius/biz/charts/SpiderChart.java +++ b/src/main/java/sirius/biz/charts/RadarChart.java @@ -31,7 +31,7 @@ * * @param the type of the numeric values */ -public class SpiderChart extends Chart { +public class RadarChart extends Chart { private static final double TICK_LENGTH = 2.0; @@ -53,7 +53,7 @@ public class SpiderChart extends Chart { * * @param labels the labels for the chart axes */ - public SpiderChart(String... labels) { + public RadarChart(String... labels) { this(List.of(labels)); } @@ -62,7 +62,7 @@ public SpiderChart(String... labels) { * * @param labels the labels for the chart axes */ - public SpiderChart(SequencedCollection labels) { + public RadarChart(SequencedCollection labels) { if (labels.size() < 3) { throw new IllegalArgumentException("At least three keys are required."); } @@ -76,7 +76,7 @@ public SpiderChart(SequencedCollection labels) { * @param dataset the dataset to add * @return the chart itself for fluent method calls */ - public SpiderChart addDataset(Dataset dataset) { + public RadarChart addDataset(Dataset dataset) { if (!labels.equals(dataset.getLabels())) { throw new IllegalArgumentException("Incompatible dataset labels."); } @@ -90,7 +90,7 @@ public SpiderChart addDataset(Dataset dataset) { * @param values the values of the dataset * @return the chart itself for fluent method calls */ - public SpiderChart addDataset(List values) { + public RadarChart addDataset(List values) { return addDataset(null, values); } @@ -101,7 +101,7 @@ public SpiderChart addDataset(List values) { * @param values the values of the dataset * @return the chart itself for fluent method calls */ - public SpiderChart addDataset(@Nullable String name, List values) { + public RadarChart addDataset(@Nullable String name, List values) { if (values.size() != labels.size()) { throw new IllegalArgumentException("Incompatible dataset size."); } @@ -123,7 +123,7 @@ public SpiderChart addDataset(@Nullable String name, List values) { * @return the chart itself for fluent method calls * @see #withRings() */ - public SpiderChart withMarks(List marks) { + public RadarChart withMarks(List marks) { this.marks.clear(); this.marks.addAll(marks); this.marks.sort(Comparator.comparingDouble(Number::doubleValue)); @@ -136,7 +136,7 @@ public SpiderChart withMarks(List marks) { * @return the chart itself for fluent method calls * @see #withMarks(List) */ - public SpiderChart withRings() { + public RadarChart withRings() { this.rings = true; return this; } @@ -147,7 +147,7 @@ public SpiderChart withRings() { * @param formatter the formatter to use * @return the chart itself for fluent method calls */ - public SpiderChart withFormatter(Function formatter) { + public RadarChart withFormatter(Function formatter) { this.formatter = formatter; return this; } From 6991f3ff3e0e1057225c58b59a19fe10520a521f Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Wed, 11 Dec 2024 16:27:58 +0100 Subject: [PATCH 17/21] =?UTF-8?q?Introduces=20a=20doughnut=20chart=20?= =?UTF-8?q?=F0=9F=8D=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In order to make the chart more easily accessible, a new class is introduced. However, the functionality all resides in the pie chart, as a doughnut chart can degenerate to a pie chart. The renaming was suggested by @sabieber during review. Writing adapted to BE, as is done in Wikipedia. OX-9272 --- .../java/sirius/biz/charts/DoughnutChart.java | 24 +++++++++++++++ src/main/java/sirius/biz/charts/PieChart.java | 29 +++++++------------ 2 files changed, 34 insertions(+), 19 deletions(-) create mode 100644 src/main/java/sirius/biz/charts/DoughnutChart.java diff --git a/src/main/java/sirius/biz/charts/DoughnutChart.java b/src/main/java/sirius/biz/charts/DoughnutChart.java new file mode 100644 index 000000000..94ec7c12d --- /dev/null +++ b/src/main/java/sirius/biz/charts/DoughnutChart.java @@ -0,0 +1,24 @@ +/* + * Made with all the love in the world + * by scireum in Remshalden, Germany + * + * Copyright by scireum GmbH + * http://www.scireum.de - info@scireum.de + */ + +package sirius.biz.charts; + +/** + * Represents a doughnut chart which can be rendered as SVG. + * + * @param the type of the numeric values + */ +public class DoughnutChart extends PieChart { + + /** + * Creates an empty doughnut chart. + */ + public DoughnutChart() { + this.doughnut = true; + } +} diff --git a/src/main/java/sirius/biz/charts/PieChart.java b/src/main/java/sirius/biz/charts/PieChart.java index 51d48c56e..944cb8e9f 100644 --- a/src/main/java/sirius/biz/charts/PieChart.java +++ b/src/main/java/sirius/biz/charts/PieChart.java @@ -20,17 +20,18 @@ * Represents a pie chart which can be rendered as SVG. * * @param the type of the numeric values + * @see DoughnutChart */ public class PieChart extends Chart { - private static final double RING_WIDTH = 4.0; + protected static final double DOUGHNUT_WIDTH = 4.0; - private Dataset dataset; + protected Dataset dataset; /** - * Flag to determine if the pie chart should be rendered as a ring rather than filled. + * Flag to determine if the pie chart should be rendered as a doughnut rather than filled. */ - private boolean ring = false; + protected boolean doughnut = false; /** * Sets up the chart with the given dataset. @@ -43,16 +44,6 @@ public PieChart withDataset(Dataset dataset) { return this; } - /** - * Enables the rendering of the pie chart as a ring rather than filled. - * - * @return the chart itself for fluent method calls - */ - public PieChart asRing() { - this.ring = true; - return this; - } - public Dataset getDataset() { return dataset; } @@ -95,8 +86,8 @@ public Element toSvg(Dimension bounds) { slice.getColor())); double halfRadians = 0.5 * (startRadians + endRadians); - pin.setLocation(Math.sin(halfRadians) * (radius - 0.5 * RING_WIDTH), - -Math.cos(halfRadians) * (radius - 0.5 * RING_WIDTH)); + pin.setLocation(Math.sin(halfRadians) * (radius - 0.5 * DOUGHNUT_WIDTH), + -Math.cos(halfRadians) * (radius - 0.5 * DOUGHNUT_WIDTH)); label.setLocation((pin.getX() > 0 ? 0.5 : -0.5) * bounds.width, pin.getY()); String textAnchor = pin.getX() > 0 ? VALUE_TEXT_ANCHOR_END : VALUE_TEXT_ANCHOR_START; @@ -187,11 +178,11 @@ private String assemblePathDefinitionForSlice(double startRadians, double endRad double endX = endSine * radius; double endY = -endCosine * radius; - double innerRadius = radius - RING_WIDTH; + double innerRadius = radius - DOUGHNUT_WIDTH; int largeArcFlag = delta > Math.PI ? 1 : 0; - // draw a ring segment - if (ring && innerRadius > 0) { + // draw a doughnut segment + if (doughnut && innerRadius > 0) { double innerStartX = startSine * innerRadius; double innerStartY = -startCosine * innerRadius; From 5e2cbec6bd324f7b3b9f9894a056e5ffd5df4949 Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Wed, 11 Dec 2024 16:32:34 +0100 Subject: [PATCH 18/21] =?UTF-8?q?Renamed=20constants=20=F0=9F=93=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …as requested by @sabieber during review. OX-9272 --- src/main/java/sirius/biz/charts/Chart.java | 10 ++++----- src/main/java/sirius/biz/charts/PieChart.java | 16 +++++++------- .../java/sirius/biz/charts/RadarChart.java | 21 ++++++++++--------- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/main/java/sirius/biz/charts/Chart.java b/src/main/java/sirius/biz/charts/Chart.java index beb19f4b0..8529447d4 100644 --- a/src/main/java/sirius/biz/charts/Chart.java +++ b/src/main/java/sirius/biz/charts/Chart.java @@ -25,7 +25,7 @@ public abstract class Chart { /** * Contains the tag name for group elements, {@value}. */ - protected static final String TAG_G = "g"; + protected static final String TAG_GROUP = "g"; /** * Contains the tag name for path elements, {@value}. @@ -45,17 +45,17 @@ public abstract class Chart { /** * Contains the attribute name for the center x coordinate, {@value}. */ - protected static final String ATTRIBUTE_CX = "cx"; + protected static final String ATTRIBUTE_CENTER_X = "cx"; /** * Contains the attribute name for the center y coordinate, {@value}. */ - protected static final String ATTRIBUTE_CY = "cy"; + protected static final String ATTRIBUTE_CENTER_Y = "cy"; /** * Contains the attribute name for path definitions, {@value}. */ - protected static final String ATTRIBUTE_D = "d"; + protected static final String ATTRIBUTE_DEFINITION = "d"; /** * Contains the attribute name for the fill colour, {@value}. @@ -75,7 +75,7 @@ public abstract class Chart { /** * Contains the attribute name for the radius, {@value}. */ - protected static final String ATTRIBUTE_R = "r"; + protected static final String ATTRIBUTE_RADIUS = "r"; /** * Contains the attribute name for the stroke colour, {@value}. diff --git a/src/main/java/sirius/biz/charts/PieChart.java b/src/main/java/sirius/biz/charts/PieChart.java index 944cb8e9f..9c276bdf3 100644 --- a/src/main/java/sirius/biz/charts/PieChart.java +++ b/src/main/java/sirius/biz/charts/PieChart.java @@ -53,11 +53,11 @@ public Element toSvg(Dimension bounds) { Element svgElement = Charts.createSvgElementWithCenteredViewbox(bounds); Element pieGroupElement = - svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_G); + svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_GROUP); svgElement.appendChild(pieGroupElement); Element labelsGroupElement = - svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_G); + svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_GROUP); svgElement.appendChild(labelsGroupElement); double radius = 0.5 * Math.min(bounds.width, bounds.height) - 5.0; @@ -99,20 +99,20 @@ public Element toSvg(Dimension bounds) { previousLabel.setLocation(label); Element labelGroupElement = - svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_G); + svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_GROUP); labelsGroupElement.appendChild(labelGroupElement); Element labelCircle = svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_CIRCLE); - labelCircle.setAttribute(ATTRIBUTE_CX, Double.toString(pin.getX())); - labelCircle.setAttribute(ATTRIBUTE_CY, Double.toString(pin.getY())); - labelCircle.setAttribute(ATTRIBUTE_R, "0.4"); + labelCircle.setAttribute(ATTRIBUTE_CENTER_X, Double.toString(pin.getX())); + labelCircle.setAttribute(ATTRIBUTE_CENTER_Y, Double.toString(pin.getY())); + labelCircle.setAttribute(ATTRIBUTE_RADIUS, "0.4"); labelCircle.setAttribute(ATTRIBUTE_FILL, COLOR_BLACK); labelGroupElement.appendChild(labelCircle); Element labelPath = svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_PATH); - labelPath.setAttribute(ATTRIBUTE_D, + labelPath.setAttribute(ATTRIBUTE_DEFINITION, String.format("M %f %f L %f %f", pin.getX(), pin.getY(), @@ -149,7 +149,7 @@ private Element createPathElementForSlice(Element svgElement, Element piecePath = svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_PATH); - piecePath.setAttribute(ATTRIBUTE_D, assemblePathDefinitionForSlice(startRadians, endRadians, radius)); + piecePath.setAttribute(ATTRIBUTE_DEFINITION, assemblePathDefinitionForSlice(startRadians, endRadians, radius)); piecePath.setAttribute(ATTRIBUTE_STROKE, COLOR_GRAY_DARK); piecePath.setAttribute(ATTRIBUTE_STROKE_WIDTH, "0.1"); diff --git a/src/main/java/sirius/biz/charts/RadarChart.java b/src/main/java/sirius/biz/charts/RadarChart.java index cf0e639ec..c542a7831 100644 --- a/src/main/java/sirius/biz/charts/RadarChart.java +++ b/src/main/java/sirius/biz/charts/RadarChart.java @@ -165,19 +165,19 @@ public Element toSvg(Dimension bounds) { Element svgElement = Charts.createSvgElementWithCenteredViewbox(bounds); Element backgroundGroupElement = - svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_G); + svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_GROUP); svgElement.appendChild(backgroundGroupElement); Element axesGroupElement = - svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_G); + svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_GROUP); backgroundGroupElement.appendChild(axesGroupElement); Element labelsGroupElement = - svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_G); + svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_GROUP); backgroundGroupElement.appendChild(labelsGroupElement); Element graphsGroupElement = - svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_G); + svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_GROUP); svgElement.appendChild(graphsGroupElement); double radius = 0.5 * Math.min(bounds.width, bounds.height) - 7.5; @@ -216,7 +216,7 @@ public Element toSvg(Dimension bounds) { Element valuePath = svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_PATH); - valuePath.setAttribute(ATTRIBUTE_D, pathDefinition.append(" Z").toString()); + valuePath.setAttribute(ATTRIBUTE_DEFINITION, pathDefinition.append(" Z").toString()); valuePath.setAttribute(ATTRIBUTE_STROKE, dataset.getColor()); valuePath.setAttribute(ATTRIBUTE_FILL, dataset.getColor()); valuePath.setAttribute(ATTRIBUTE_FILL_OPACITY, "0.5"); @@ -257,9 +257,9 @@ private void drawAxes(Document document, if (rings) { Element circleElement = document.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_CIRCLE); - circleElement.setAttribute(ATTRIBUTE_CX, "0"); - circleElement.setAttribute(ATTRIBUTE_CY, "0"); - circleElement.setAttribute(ATTRIBUTE_R, Double.toString(markRadius)); + circleElement.setAttribute(ATTRIBUTE_CENTER_X, "0"); + circleElement.setAttribute(ATTRIBUTE_CENTER_Y, "0"); + circleElement.setAttribute(ATTRIBUTE_RADIUS, Double.toString(markRadius)); circleElement.setAttribute(ATTRIBUTE_STROKE, COLOR_GRAY_LIGHT); circleElement.setAttribute(ATTRIBUTE_FILL, VALUE_FILL_NONE); circleElement.setAttribute(ATTRIBUTE_STROKE_WIDTH, "0.1"); @@ -287,7 +287,8 @@ private void drawAxes(Document document, // draw axis double axisRadius = radius + 2.5; Element axisPath = document.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_PATH); - axisPath.setAttribute(ATTRIBUTE_D, String.format("M 0 0 L %f %f", sine * axisRadius, -cosine * axisRadius)); + axisPath.setAttribute(ATTRIBUTE_DEFINITION, + String.format("M 0 0 L %f %f", sine * axisRadius, -cosine * axisRadius)); axisPath.setAttribute(ATTRIBUTE_STROKE, COLOR_BLACK); axisPath.setAttribute(ATTRIBUTE_STROKE_WIDTH, "0.2"); axesGroupElement.appendChild(axisPath); @@ -299,7 +300,7 @@ private void drawAxes(Document document, double markY = -cosine * markRadius; Element markPath = document.createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_PATH); - markPath.setAttribute(ATTRIBUTE_D, + markPath.setAttribute(ATTRIBUTE_DEFINITION, String.format("M %f %f L %f %f", markX + 0.5 * TICK_LENGTH * cosine, markY + 0.5 * TICK_LENGTH * sine, From 5d944a5f2fe2c50ef6bfcb0a59cbb7e00af8a206 Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Wed, 11 Dec 2024 16:39:03 +0100 Subject: [PATCH 19/21] =?UTF-8?q?Writes=20numbers=20differently=20?= =?UTF-8?q?=E2=9C=8D=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …as suggested by @sabieber during review. OX-9272 --- src/main/java/sirius/biz/charts/PieChart.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/sirius/biz/charts/PieChart.java b/src/main/java/sirius/biz/charts/PieChart.java index 9c276bdf3..6a0a73e2d 100644 --- a/src/main/java/sirius/biz/charts/PieChart.java +++ b/src/main/java/sirius/biz/charts/PieChart.java @@ -164,7 +164,7 @@ private String assemblePathDefinitionForSlice(double startRadians, double endRad // if the start and end point are precisely the same, we need to add a small offset to avoid rendering issues double delta = endRadians - startRadians; - double offset = (Math.abs(delta % Math.TAU) < 2.0e-4) ? 1.0e-4 : 0.0; + double offset = (Math.abs(delta % Math.TAU) < 0.0002) ? 0.0001 : 0.0; // cache the sine and cosine values, as they are relatively expensive to compute double startSine = Math.sin(startRadians + offset); From 1cd2fc0bb16101d024113f8683b142e41bf0dbe9 Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Wed, 11 Dec 2024 17:28:56 +0100 Subject: [PATCH 20/21] =?UTF-8?q?Moves=20the=20internal=20helper=20back=20?= =?UTF-8?q?=F0=9F=8E=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …as I misunderstood @sabieber's review feedback. OX-9272 --- src/main/java/sirius/biz/charts/Chart.java | 22 +++++++++++++++++++ src/main/java/sirius/biz/charts/Charts.java | 22 ------------------- src/main/java/sirius/biz/charts/PieChart.java | 2 +- .../java/sirius/biz/charts/RadarChart.java | 2 +- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/main/java/sirius/biz/charts/Chart.java b/src/main/java/sirius/biz/charts/Chart.java index 8529447d4..31ba6bceb 100644 --- a/src/main/java/sirius/biz/charts/Chart.java +++ b/src/main/java/sirius/biz/charts/Chart.java @@ -8,6 +8,7 @@ package sirius.biz.charts; +import org.apache.batik.anim.dom.SVGDOMImplementation; import org.w3c.dom.Element; import java.awt.Dimension; @@ -156,4 +157,25 @@ public abstract class Chart { * @return the SVG representation of the chart */ public abstract Element toSvg(Dimension bounds); + + /** + * Creates an empty SVG element with the view box centered. + * + * @param bounds the dimensions of the viewport + * @return an empty SVG element with the view box centered + */ + protected Element createSvgElementWithCenteredViewbox(Dimension bounds) { + Element svgElement = SVGDOMImplementation.getDOMImplementation() + .createDocument(SVGDOMImplementation.SVG_NAMESPACE_URI, + Chart.TAG_SVG, + null) + .getDocumentElement(); + svgElement.setAttribute(Chart.ATTRIBUTE_VIEW_BOX, + String.format("%f %f %f %f", + -0.5 * bounds.width, + -0.5 * bounds.height, + (double) bounds.width, + (double) bounds.height)); + return svgElement; + } } diff --git a/src/main/java/sirius/biz/charts/Charts.java b/src/main/java/sirius/biz/charts/Charts.java index d44fd05e4..e3e130942 100644 --- a/src/main/java/sirius/biz/charts/Charts.java +++ b/src/main/java/sirius/biz/charts/Charts.java @@ -9,7 +9,6 @@ package sirius.biz.charts; import com.lowagie.text.xml.XmlDomWriter; -import org.apache.batik.anim.dom.SVGDOMImplementation; import org.w3c.dom.Element; import sirius.kernel.di.std.Register; import sirius.web.templates.pdf.TagliatellePDFContentHandler; @@ -44,25 +43,4 @@ public String exportChartForPdf(Chart chart, Dimension bounds) { xmlWriter.write(element); return writer.toString(); } - - /** - * Creates an empty SVG element with the view box centered. - * - * @param bounds the dimensions of the viewport - * @return an empty SVG element with the view box centered - */ - protected static Element createSvgElementWithCenteredViewbox(Dimension bounds) { - Element svgElement = SVGDOMImplementation.getDOMImplementation() - .createDocument(SVGDOMImplementation.SVG_NAMESPACE_URI, - Chart.TAG_SVG, - null) - .getDocumentElement(); - svgElement.setAttribute(Chart.ATTRIBUTE_VIEW_BOX, - String.format("%f %f %f %f", - -0.5 * bounds.width, - -0.5 * bounds.height, - (double) bounds.width, - (double) bounds.height)); - return svgElement; - } } diff --git a/src/main/java/sirius/biz/charts/PieChart.java b/src/main/java/sirius/biz/charts/PieChart.java index 6a0a73e2d..d8c0a31d0 100644 --- a/src/main/java/sirius/biz/charts/PieChart.java +++ b/src/main/java/sirius/biz/charts/PieChart.java @@ -50,7 +50,7 @@ public Dataset getDataset() { @Override public Element toSvg(Dimension bounds) { - Element svgElement = Charts.createSvgElementWithCenteredViewbox(bounds); + Element svgElement = createSvgElementWithCenteredViewbox(bounds); Element pieGroupElement = svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_GROUP); diff --git a/src/main/java/sirius/biz/charts/RadarChart.java b/src/main/java/sirius/biz/charts/RadarChart.java index c542a7831..d0c9a8726 100644 --- a/src/main/java/sirius/biz/charts/RadarChart.java +++ b/src/main/java/sirius/biz/charts/RadarChart.java @@ -162,7 +162,7 @@ public List> getDatasets() { @Override public Element toSvg(Dimension bounds) { - Element svgElement = Charts.createSvgElementWithCenteredViewbox(bounds); + Element svgElement = createSvgElementWithCenteredViewbox(bounds); Element backgroundGroupElement = svgElement.getOwnerDocument().createElementNS(SVGDOMImplementation.SVG_NAMESPACE_URI, TAG_GROUP); From 09fccfd67ab2013877d55c53cf2a37a3dfec0a3a Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Wed, 11 Dec 2024 18:36:53 +0100 Subject: [PATCH 21/21] =?UTF-8?q?Improves=20documentation=20and=20wording?= =?UTF-8?q?=20=E2=9C=8D=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …as implied by a review comment by @sabieber. OX-9272 --- src/main/java/sirius/biz/charts/Dataset.java | 165 ++++++++++-------- src/main/java/sirius/biz/charts/PieChart.java | 12 +- .../java/sirius/biz/charts/RadarChart.java | 10 +- 3 files changed, 104 insertions(+), 83 deletions(-) diff --git a/src/main/java/sirius/biz/charts/Dataset.java b/src/main/java/sirius/biz/charts/Dataset.java index 0c9a2b05f..170bcf0ec 100644 --- a/src/main/java/sirius/biz/charts/Dataset.java +++ b/src/main/java/sirius/biz/charts/Dataset.java @@ -20,9 +20,10 @@ import java.util.stream.Stream; /** - * Represents a dataset for charts and tables. + * Represents a dataset for charts and tables. A dateset is understood to consist of a series of {@linkplain Quantity + * quantities}. * - * @param the type of the numeric values + * @param the numeric type used for the quantities */ public class Dataset implements Named { @@ -42,59 +43,73 @@ public class Dataset implements Named { "#b6e2d4" // light teal ); + /** + * The optional name of the dataset. + */ private String name; + /** + * An optional primary color to use when displaying the dataset. + */ private String color; - private final LinkedHashMap slices = new LinkedHashMap<>(); + /** + * The individual quantities of the dataset, indexed by their label. + */ + private final LinkedHashMap quantities = new LinkedHashMap<>(); - private Optional sum = Optional.empty(); + /** + * The cached sum of all quantities, used to avoid renewed computation when invoking {@link #sum()} multiple times. + * + * @see #sum() + */ + private Optional cachedSum = Optional.empty(); /** - * Helper that allows to format the numeric values of the slices. + * Helper method that allows to format the numeric quantities as needed. Defaults to {@link Object#toString()}. */ private Function formatter = Object::toString; /** - * Flag to determine if the rendered charts or tables should also display percentages. + * Flag to determine if the rendered charts or tables should also display percentages besides the raw quantities. */ private boolean percentagesShown = false; /** - * Adds a slice to the dataset. + * Adds a quantity to the dataset. * - * @param label the label of the slice - * @param quantity the quantity of the slice + * @param label the label of the quantity + * @param value the value of the quantity * @return the dataset itself for fluent method calls */ - public Dataset addSlice(String label, N quantity) { + public Dataset addQuantity(String label, N value) { if (Strings.isEmpty(label)) { throw new IllegalArgumentException("Label must not be empty."); } - if (quantity.doubleValue() < 0.0) { + if (value.doubleValue() < 0.0) { throw new IllegalArgumentException("Quantity must be non-negative."); } - slices.compute(label, (key, slice) -> { - if (slice == null) { - return new Slice(slices.size(), label, quantity); + quantities.compute(label, (key, quantity) -> { + if (quantity == null) { + return new Quantity(quantities.size(), label, value); } - slice.setQuantity(quantity); - return slice; + quantity.setValue(value); + return quantity; }); - sum = Optional.empty(); + cachedSum = Optional.empty(); return this; } /** - * Retrieves a slice by its label. + * Retrieves a quantity by its label. * - * @param label the label of the slice - * @return the slice with the given label or an empty optional if no such slice exists + * @param label the label of the quantity to retrieve + * @return the quantity with the given label, or an empty optional if no such quantity exists */ - public Optional resolveSlice(String label) { - return Optional.ofNullable(slices.get(label)); + public Optional resolveQuantity(String label) { + return Optional.ofNullable(quantities.get(label)); } /** @@ -120,7 +135,7 @@ public Dataset withColor(String color) { } /** - * Enables the display of percentages for each slice. + * Enables the display of percentages for each quantity. * * @return the dataset itself for fluent method calls */ @@ -130,7 +145,7 @@ public Dataset withPercentagesShown() { } /** - * Enables the display of percentages for each slice. + * Enables the additional display of percentages for each quantity. * * @param percentagesShown determines if the percentages should be displayed * @return the dataset itself for fluent method calls @@ -141,7 +156,7 @@ public Dataset withPercentagesShown(boolean percentagesShown) { } /** - * Sets a formatter for the numeric values of the slices. + * Sets a formatter for the numeric values of the quantities. * * @param formatter the formatter to use * @return the dataset itself for fluent method calls @@ -152,37 +167,41 @@ public Dataset withFormatter(Function formatter) { } /** - * Computes a new dataset containing the percentages of all slices of this dataset. + * Computes a new dataset containing the percentages of all quantities of this dataset. * - * @return a new dataset containing the percentages of all slices + * @return a new dataset containing the percentages of all quantities */ public Dataset computePercentageDataset() { var result = new Dataset().withFormatter(number -> String.format("%.2f %%", number)); - for (Slice slice : slices.values()) { - result.addSlice(slice.getLabel(), slice.percentageValue()); + for (Quantity quantity : quantities.values()) { + result.addQuantity(quantity.getLabel(), quantity.percentageValue()); } return result; } /** - * Determines the sum of all slices. + * Determines the sum of all quantities. * - * @return the sum of all slices + * @return the sum of all quantities */ public double sum() { - if (sum.isEmpty()) { - sum = Optional.of(slices.values().stream().map(Slice::getQuantity).mapToDouble(Number::doubleValue).sum()); + if (cachedSum.isEmpty()) { + cachedSum = Optional.of(quantities.values() + .stream() + .map(Quantity::getValue) + .mapToDouble(Number::doubleValue) + .sum()); } - return sum.get(); + return cachedSum.get(); } /** - * Streams all slices of the dataset. + * Streams all quantities of the dataset. * - * @return a stream of all slices + * @return a stream of all quantities */ - public Stream stream() { - return slices.values().stream(); + public Stream stream() { + return quantities.values().stream(); } @Nonnull @@ -196,61 +215,63 @@ public String getColor() { } public SequencedSet getLabels() { - return slices.sequencedKeySet(); + return quantities.sequencedKeySet(); } - public List getSlices() { - return List.copyOf(slices.values()); + public List getQuantities() { + return List.copyOf(quantities.values()); } /** - * Represents a slice of the datset. + * Represents a single quantity of the dataset. Effectively, a quantity has a numeric value and additional metadata. */ - public class Slice { + public class Quantity { private final int index; private final String label; - private N quantity; + private N value; private final String color; - private Slice(int index, String label, N quantity) { + private Quantity(int index, String label, N value) { this.index = index; this.label = label; - this.quantity = quantity; + this.value = value; this.color = COLORS.get(index % COLORS.size()); } /** - * Converts the quantity to a double value. + * Computes the equivalent double-precision value of this quantity. * - * @return the quantity as a double value + * @return the quantity's value as a double-precision number */ public double doubleValue() { - return quantity.doubleValue(); + return value.doubleValue(); } /** - * Computes the percentage of this slice. + * Computes the percentage of this quantity in comparison to the sum of all quantities. * - * @return the percentage of this slice + * @return the percentage of this quantity in comparison to the sum of all quantities as a double-precision number */ public double percentageValue() { // the "+ 1.0e-20" is to avoid division by zero; the value is small enough to not affect the result - return 100 * quantity.doubleValue() / (sum() + 1.0e-20); + return 100 * value.doubleValue() / (sum() + 1.0e-20); } /** - * Formats the value of this slice as a string. + * Formats the value of this quantity as a string. * - * @return the formatted value of this slice + * @return the formatted value of this quantity + * @see #withFormatter(Function) */ public String formatValue() { - return formatter.apply(quantity); + return formatter.apply(value); } /** - * Formats the percentage of this slice as a string. + * Formats the percentage of this quantity as a string. * - * @return the formatted percentage of this slice + * @return the formatted percentage of this quantity + * @see #percentageValue() */ public String formatPercentage() { if (!percentagesShown) { @@ -260,7 +281,7 @@ public String formatPercentage() { } /** - * Formats the quantity as a string, potentially including a percentage. + * Formats the quantity as a string, potentially also including a percentage. * * @return the formatted quantity */ @@ -268,27 +289,27 @@ public String formatQuantity() { if (!percentagesShown) { return formatValue(); } - return new StringBuilder(quantity.toString()).append(String.format(" (%s)", formatPercentage())).toString(); + return new StringBuilder(formatValue()).append(String.format(" (%s)", formatPercentage())).toString(); } /** - * Fetches the equivalent slice from a previous dataset. + * Fetches the equivalent quantity from a previous dataset. * - * @param previousDataset the previous dataset to fetch the slice from - * @return the equivalent slice from the previous dataset, or an empty optional if no such slice exists + * @param previousDataset the previous dataset to fetch the quantity from + * @return the equivalent quantity from the previous dataset, or an empty optional if no such quantity exists */ - public Optional fetchPreviousSlice(Dataset previousDataset) { - return previousDataset.resolveSlice(label); + public Optional fetchPreviousQuantity(Dataset previousDataset) { + return previousDataset.resolveQuantity(label); } /** - * Fetches the quantity of the equivalent slice from a previous dataset. + * Fetches the value of the equivalent quantity from a previous dataset. * * @param previousDataset the previous dataset to fetch the quantity from - * @return the quantity of the equivalent slice from the previous dataset, or an empty optional if no such slice exists + * @return the value of the equivalent quantity from the previous dataset, or an empty optional if no such quantity exists */ - public Optional fetchPreviousQuantity(Dataset previousDataset) { - return fetchPreviousSlice(previousDataset).map(Slice::getQuantity); + public Optional fetchPreviousValue(Dataset previousDataset) { + return fetchPreviousQuantity(previousDataset).map(Quantity::getValue); } public int getIndex() { @@ -299,12 +320,12 @@ public String getLabel() { return label; } - public N getQuantity() { - return quantity; + public N getValue() { + return value; } - private void setQuantity(N quantity) { - this.quantity = quantity; + private void setValue(N value) { + this.value = value; } public String getColor() { diff --git a/src/main/java/sirius/biz/charts/PieChart.java b/src/main/java/sirius/biz/charts/PieChart.java index d8c0a31d0..2355a612d 100644 --- a/src/main/java/sirius/biz/charts/PieChart.java +++ b/src/main/java/sirius/biz/charts/PieChart.java @@ -62,15 +62,15 @@ public Element toSvg(Dimension bounds) { double radius = 0.5 * Math.min(bounds.width, bounds.height) - 5.0; - // determines the multipliers to represent each slice in degrees and as percent + // determines the multiplier to represent each quantity in radians double multiplierRadians = Math.TAU / dataset.sum(); DoubleAdder accumulatedRadians = new DoubleAdder(); Point2D pin = new Point2D.Double(); Point2D label = new Point2D.Double(); Point2D previousLabel = new Point2D.Double(); - dataset.stream().forEach(slice -> { - double radians = slice.doubleValue() * multiplierRadians; + dataset.stream().forEach(quantity -> { + double radians = quantity.doubleValue() * multiplierRadians; if (radians <= 0.0) { return; } @@ -83,7 +83,7 @@ public Element toSvg(Dimension bounds) { startRadians, endRadians, radius, - slice.getColor())); + quantity.getColor())); double halfRadians = 0.5 * (startRadians + endRadians); pin.setLocation(Math.sin(halfRadians) * (radius - 0.5 * DOUGHNUT_WIDTH), @@ -128,14 +128,14 @@ public Element toSvg(Dimension bounds) { 3.0, COLOR_BLACK, textAnchor, - slice.getLabel())); + quantity.getLabel())); labelGroupElement.appendChild(createTextElementForLabel(svgElement, label.getX(), label.getY() + 3.5, 3.0, COLOR_GRAY_DARK, textAnchor, - slice.formatQuantity())); + quantity.formatQuantity())); }); return svgElement; diff --git a/src/main/java/sirius/biz/charts/RadarChart.java b/src/main/java/sirius/biz/charts/RadarChart.java index d0c9a8726..20ed388aa 100644 --- a/src/main/java/sirius/biz/charts/RadarChart.java +++ b/src/main/java/sirius/biz/charts/RadarChart.java @@ -44,7 +44,7 @@ public class RadarChart extends Chart { private boolean rings = false; /** - * Helper that allows to format the numeric values of the slices. + * Helper that allows to format the numeric values of the quantities. */ private Function formatter = Object::toString; @@ -110,7 +110,7 @@ public RadarChart addDataset(@Nullable String name, List values) { AtomicInteger labelCounter = new AtomicInteger(); labels.forEach(label -> { int labelIndex = labelCounter.getAndIncrement(); - dataset.addSlice(label, values.get(labelIndex)); + dataset.addQuantity(label, values.get(labelIndex)); }); return addDataset(dataset); @@ -201,8 +201,8 @@ public Element toSvg(Dimension bounds) { double sine = Math.sin(radians); double cosine = Math.cos(radians); - double value = dataset.resolveSlice(label) - .map(Dataset.Slice::getQuantity) + double value = dataset.resolveQuantity(label) + .map(Dataset.Quantity::getValue) .map(Number::doubleValue) .orElse(0.0); double normalizedValue = radius * value / normaliser; @@ -240,7 +240,7 @@ public Element toSvg(Dimension bounds) { */ private double computeNormaliser() { Stream numbers = marks.isEmpty() ? - datasets.stream().flatMap(dataset -> dataset.stream().map(Dataset.Slice::getQuantity)) : + datasets.stream().flatMap(dataset -> dataset.stream().map(Dataset.Quantity::getValue)) : marks.stream(); return numbers.mapToDouble(Number::doubleValue).max().orElse(1.0); }