From 9ee7acd9d16e59224ee5184c89374f06d6592ad3 Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期日, 04 十月 2026 17:47:34 +0800
Subject: [PATCH] feat(holiday): 新增节假日综合运输客流量走势图
---
traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayTrendChartService.java | 1015 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 1,015 insertions(+), 0 deletions(-)
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayTrendChartService.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayTrendChartService.java
new file mode 100644
index 0000000..8d359c4
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayTrendChartService.java
@@ -0,0 +1,1015 @@
+package com.trafficaudit.holiday.service;
+
+import lombok.Data;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.StandardChartTheme;
+import org.jfree.chart.axis.AxisLocation;
+import org.jfree.chart.axis.CategoryAxis;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.axis.SymbolAxis;
+import org.jfree.chart.axis.ValueAxis;
+import org.jfree.chart.labels.StandardPieSectionLabelGenerator;
+import org.jfree.chart.plot.CategoryPlot;
+import org.jfree.chart.plot.PiePlot;
+import org.jfree.chart.plot.Plot;
+import org.jfree.chart.plot.PieLabelRecord;
+import org.jfree.chart.plot.PiePlotState;
+import org.jfree.chart.text.TextFragment;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.category.StackedBarRenderer;
+import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
+import org.jfree.chart.ui.RectangleEdge;
+import org.jfree.chart.ui.RectangleInsets;
+import org.jfree.chart.ui.TextAnchor;
+import org.jfree.data.DefaultKeyedValues;
+import org.jfree.data.KeyedValues;
+import org.jfree.data.category.CategoryDataset;
+import org.jfree.data.category.DefaultCategoryDataset;
+import org.jfree.data.general.DefaultPieDataset;
+import org.jfree.data.xy.XYDataset;
+import org.jfree.data.xy.XYSeries;
+import org.jfree.data.xy.XYSeriesCollection;
+import org.springframework.stereotype.Service;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.FontMetrics;
+import java.awt.Graphics2D;
+import java.awt.Paint;
+import java.awt.Shape;
+import java.awt.image.BufferedImage;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Path2D;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.io.ByteArrayOutputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.math.BigDecimal;
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.IdentityHashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class HolidayTrendChartService {
+
+ private static final Font BASE_FONT = new Font("Microsoft YaHei", Font.PLAIN, 20);
+ private static final Font TICK_FONT = BASE_FONT.deriveFont(Font.BOLD, 22f);
+ private static final Font LABEL_FONT = BASE_FONT.deriveFont(Font.BOLD, 22f);
+ private static final Font ITEM_LABEL_FONT = BASE_FONT.deriveFont(Font.BOLD, 20f);
+ private static final Font LEGEND_FONT = BASE_FONT.deriveFont(Font.BOLD, 22f);
+ private static final Font STACK_LABEL_FONT = BASE_FONT.deriveFont(Font.BOLD, 17f);
+
+ private static final Color AXIS_COLOR = Color.BLACK;
+ private static final Color MUTED_TEXT_COLOR = new Color(102, 102, 102);
+ private static final double UNIT_LABEL_BAND = 58.0;
+ private static final double RIGHT_LABEL_BAND = 74.0;
+ private static final double ITEM_LABEL_GAP = 12.0;
+ private static final Color ITEM_LABEL_COLOR = new Color(55, 55, 55);
+ private static final Color[] SERIES_COLORS = {
+ new Color(192, 80, 77),
+ new Color(79, 129, 189),
+ new Color(155, 187, 89),
+ new Color(128, 100, 162),
+ new Color(75, 172, 198)
+ };
+
+ static {
+ System.setProperty("java.awt.headless", "true");
+ }
+
+ public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis) {
+ return lineChart(yAxisLabel, series, percentAxis, 1100, 590);
+ }
+
+ public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis,
+ int width, int height) {
+ XYSeriesCollection dataset = new XYSeriesCollection();
+ for (int seriesIndex = 0; seriesIndex < series.size(); seriesIndex++) {
+ ChartSeries item = series.get(seriesIndex);
+ XYSeries xySeries = new XYSeries(item.getName(), false, false);
+ for (int i = 0; i < item.getValues().size(); i++) {
+ BigDecimal value = item.getValues().get(i);
+ if (value != null) {
+ xySeries.add(i, value.doubleValue());
+ }
+ }
+ dataset.addSeries(xySeries);
+ }
+ JFreeChart chart = ChartFactory.createXYLineChart(
+ null, null, null, dataset, PlotOrientation.VERTICAL, true, false, false);
+ applyTheme(chart);
+ chart.setBackgroundPaint(Color.WHITE);
+ chart.setBorderVisible(false);
+ chart.getLegend().setPosition(RectangleEdge.BOTTOM);
+ chart.getLegend().setItemFont(LEGEND_FONT);
+ chart.getLegend().setFrame(new org.jfree.chart.block.BlockBorder(0.0, 0.0, 0.0, 0.0));
+ chart.getLegend().setBackgroundPaint(Color.WHITE);
+ chart.getLegend().setItemPaint(MUTED_TEXT_COLOR);
+
+ XYPlot plot = chart.getXYPlot();
+ stylePlot(plot);
+ plot.setDomainGridlinesVisible(false);
+ plot.setRangeGridlinesVisible(false);
+ plot.setDomainTickBandPaint(null);
+ plot.setRangeTickBandPaint(null);
+ SymbolAxis domainAxis = new SymbolAxis(null, DAY_LABELS.toArray(new String[0]));
+ domainAxis.setGridBandPaint(new Color(0, 0, 0, 0));
+ domainAxis.setGridBandAlternatePaint(new Color(0, 0, 0, 0));
+ domainAxis.setLowerMargin(0.04);
+ domainAxis.setUpperMargin(0.04);
+ plot.setDomainAxis(domainAxis);
+ styleDomainAxis(plot.getDomainAxis());
+
+ NumberAxis rangeAxis = new NumberAxis();
+ rangeAxis.setAutoRangeIncludesZero(false);
+ rangeAxis.setLowerMargin(0.12);
+ rangeAxis.setUpperMargin(0.16);
+ rangeAxis.setTickLabelFont(TICK_FONT);
+ rangeAxis.setLabelFont(LABEL_FONT);
+ rangeAxis.setTickLabelPaint(MUTED_TEXT_COLOR);
+ rangeAxis.setLabelPaint(AXIS_COLOR);
+ rangeAxis.setAxisLinePaint(AXIS_COLOR);
+ rangeAxis.setTickMarkPaint(AXIS_COLOR);
+ if (percentAxis) {
+ rangeAxis.setRange(0.0, 1.0);
+ rangeAxis.setAutoRangeIncludesZero(true);
+ }
+ NumberFormat valueFormat = percentAxis
+ ? NumberFormat.getPercentInstance() : new DecimalFormat("#,##0.##");
+ valueFormat.setMaximumFractionDigits(percentAxis ? 0 : 2);
+ rangeAxis.setNumberFormatOverride(valueFormat);
+ plot.setRangeAxis(rangeAxis);
+ plot.setAxisOffset(new RectangleInsets(2.0, 2.0, 2.0, 2.0));
+
+ XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(true, true);
+ renderer.setDefaultShapesVisible(true);
+ renderer.setDefaultShapesFilled(true);
+ renderer.setDefaultLinesVisible(true);
+ renderer.setDefaultItemLabelsVisible(false);
+ for (int i = 0; i < series.size(); i++) {
+ renderer.setSeriesPaint(i, seriesColor(i));
+ renderer.setSeriesFillPaint(i, seriesColor(i));
+ renderer.setSeriesShape(i, seriesShape(i));
+ renderer.setSeriesStroke(i, new BasicStroke(6.0f,
+ BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
+ }
+ plot.setRenderer(renderer);
+ return render(chart, width, height, yAxisLabel, true, percentAxis);
+ }
+
+ public byte[] pieChart(String valueSuffix, List<ChartCategory> categories,
+ int width, int height) {
+ DefaultPieDataset<String> dataset = new DefaultPieDataset<>();
+ for (ChartCategory category : categories) {
+ dataset.setValue(category.getName(), category.getValue());
+ }
+ PiePlot<String> piePlot = new WaterRightPiePlot<>(dataset);
+ JFreeChart chart = new JFreeChart(null, JFreeChart.DEFAULT_TITLE_FONT, piePlot, true);
+ applyTheme(chart);
+ chart.setBackgroundPaint(Color.WHITE);
+ chart.setBorderVisible(false);
+ chart.getLegend().setPosition(RectangleEdge.BOTTOM);
+ chart.getLegend().setItemFont(LEGEND_FONT);
+ chart.getLegend().setFrame(new org.jfree.chart.block.BlockBorder(0.0, 0.0, 0.0, 0.0));
+ chart.getLegend().setBackgroundPaint(Color.WHITE);
+ chart.getLegend().setItemPaint(MUTED_TEXT_COLOR);
+
+ PiePlot plot = piePlot;
+ plot.setBackgroundPaint(Color.WHITE);
+ plot.setOutlineVisible(false);
+ plot.setShadowPaint(null);
+ plot.setLabelFont(ITEM_LABEL_FONT);
+ plot.setLabelPaint(ITEM_LABEL_COLOR);
+ plot.setLabelBackgroundPaint(Color.WHITE);
+ plot.setLabelOutlinePaint(new Color(160, 160, 160));
+ plot.setLabelShadowPaint(null);
+ plot.setLabelLinkPaint(Color.BLACK);
+ plot.setLabelLinkStroke(new BasicStroke(2.0f));
+ plot.setLabelGap(0.02);
+ plot.setMaximumLabelWidth(0.24);
+ plot.setSimpleLabels(false);
+ plot.setInteriorGap(0.06);
+ plot.setStartAngle(90.0);
+ for (ChartCategory category : categories) {
+ plot.setSectionOutlinePaint(category.getName(), Color.WHITE);
+ plot.setSectionOutlineStroke(category.getName(), new BasicStroke(1.2f));
+ }
+ plot.setLabelGenerator(new StandardPieSectionLabelGenerator(
+ "{0} {2}", new DecimalFormat("0"), new DecimalFormat("0.00%")));
+ int index = 0;
+ for (ChartCategory category : categories) {
+ plot.setSectionPaint(category.getName(), seriesColor(index++));
+ }
+ return render(chart, width, height, valueSuffix, false, false);
+ }
+
+ public byte[] stackedBarChart(String yAxisLabel, List<String> categories,
+ List<ChartSeries> series, boolean percentAxis,
+ int width, int height) {
+ DefaultCategoryDataset dataset = new DefaultCategoryDataset();
+ for (ChartSeries item : series) {
+ for (int i = 0; i < categories.size() && i < item.getValues().size(); i++) {
+ BigDecimal value = item.getValues().get(i);
+ if (value != null) {
+ dataset.addValue(value, item.getName(), categories.get(i));
+ }
+ }
+ }
+ JFreeChart chart = ChartFactory.createStackedBarChart(
+ null, null, null, dataset, PlotOrientation.HORIZONTAL, true, false, false);
+ applyTheme(chart);
+ chart.setBackgroundPaint(Color.WHITE);
+ chart.setBorderVisible(false);
+ chart.getLegend().setPosition(RectangleEdge.BOTTOM);
+ chart.getLegend().setItemFont(LEGEND_FONT);
+ chart.getLegend().setFrame(new org.jfree.chart.block.BlockBorder(0.0, 0.0, 0.0, 0.0));
+ chart.getLegend().setBackgroundPaint(Color.WHITE);
+ chart.getLegend().setItemPaint(MUTED_TEXT_COLOR);
+
+ CategoryPlot plot = chart.getCategoryPlot();
+ plot.setBackgroundPaint(Color.WHITE);
+ plot.setDomainGridlinesVisible(false);
+ plot.setRangeGridlinesVisible(false);
+ plot.setOutlineVisible(false);
+ plot.setAxisOffset(new RectangleInsets(2.0, 2.0, 2.0, 2.0));
+ // Horizontal stacked bars: value axis along the bottom, no vertical axis line.
+ plot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_LEFT);
+ CategoryAxis domainAxis = plot.getDomainAxis();
+ domainAxis.setAxisLineVisible(false);
+ domainAxis.setTickMarksVisible(false);
+ styleDomainAxis(domainAxis);
+ if (series.size() >= 3) {
+ RectangleInsets insets = plot.getInsets();
+ plot.setInsets(new RectangleInsets(insets.getTop(), insets.getLeft(),
+ insets.getBottom(), insets.getRight() + RIGHT_LABEL_BAND));
+ }
+ NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
+ rangeAxis.setTickLabelFont(TICK_FONT);
+ rangeAxis.setLabelFont(LABEL_FONT);
+ rangeAxis.setTickLabelPaint(MUTED_TEXT_COLOR);
+ rangeAxis.setLabelPaint(AXIS_COLOR);
+ rangeAxis.setAxisLinePaint(AXIS_COLOR);
+ rangeAxis.setTickMarkPaint(AXIS_COLOR);
+ rangeAxis.setAutoRangeIncludesZero(true);
+ rangeAxis.setLowerMargin(0.05);
+ rangeAxis.setUpperMargin(0.12);
+ if (percentAxis) {
+ rangeAxis.setRange(0.0, 1.0);
+ }
+ NumberFormat valueFormat = percentAxis
+ ? NumberFormat.getPercentInstance() : new DecimalFormat("#,##0.##");
+ valueFormat.setMaximumFractionDigits(percentAxis ? 0 : 2);
+ rangeAxis.setNumberFormatOverride(valueFormat);
+
+ StackedBarRenderer renderer = new StackedBarRenderer();
+ renderer.setDefaultItemLabelsVisible(false);
+ renderer.setBarPainter(new org.jfree.chart.renderer.category.StandardBarPainter());
+ renderer.setShadowVisible(false);
+ renderer.setItemMargin(0.05);
+ for (int i = 0; i < series.size(); i++) {
+ renderer.setSeriesPaint(i, seriesColor(i));
+ }
+ plot.setRenderer(renderer);
+ return render(chart, width, height, yAxisLabel, true, percentAxis);
+ }
+
+ private static class WaterRightPiePlot<K extends Comparable<K>> extends PiePlot<K> {
+ private DefaultKeyedValues<K> waterKeys;
+
+ private WaterRightPiePlot(org.jfree.data.general.PieDataset<K> dataset) {
+ super(dataset);
+ }
+
+ @Override
+ protected void drawLeftLabels(KeyedValues<K> leftKeys, Graphics2D g2,
+ Rectangle2D plotArea, Rectangle2D linkArea,
+ float maxLabelWidth, PiePlotState state) {
+ DefaultKeyedValues<K> filtered = new DefaultKeyedValues<>();
+ waterKeys = new DefaultKeyedValues<>();
+ for (int i = 0; i < leftKeys.getItemCount(); i++) {
+ K key = leftKeys.getKey(i);
+ if ("姘磋矾".equals(String.valueOf(key))) {
+ waterKeys.addValue(key, leftKeys.getValue(i));
+ } else {
+ filtered.addValue(key, leftKeys.getValue(i));
+ }
+ }
+ super.drawLeftLabels(filtered, g2, plotArea, linkArea, maxLabelWidth, state);
+ }
+
+ @Override
+ protected void drawRightLabels(KeyedValues<K> rightKeys, Graphics2D g2,
+ Rectangle2D plotArea, Rectangle2D linkArea,
+ float maxLabelWidth, PiePlotState state) {
+ DefaultKeyedValues<K> merged = new DefaultKeyedValues<>();
+ if (waterKeys != null) {
+ for (int i = 0; i < waterKeys.getItemCount(); i++) {
+ merged.addValue(waterKeys.getKey(i), waterKeys.getValue(i));
+ }
+ }
+ for (int i = 0; i < rightKeys.getItemCount(); i++) {
+ merged.addValue(rightKeys.getKey(i), rightKeys.getValue(i));
+ }
+ super.drawRightLabels(merged, g2, plotArea, linkArea, maxLabelWidth, state);
+ }
+
+ @Override
+ protected void drawLeftLabel(Graphics2D g2, PiePlotState state, PieLabelRecord record) {
+ recolorLabel(record);
+ super.drawLeftLabel(g2, state, record);
+ }
+
+ @Override
+ protected void drawRightLabel(Graphics2D g2, PiePlotState state, PieLabelRecord record) {
+ recolorLabel(record);
+ super.drawRightLabel(g2, state, record);
+ }
+
+ private void recolorLabel(PieLabelRecord record) {
+ Paint paint = getSectionPaint(record.getKey());
+ Object label = record.getLabel();
+ if (paint == null || label == null) return;
+ recolorPaintFields(label, paint, new IdentityHashMap<Object, Boolean>());
+ }
+
+ private void recolorPaintFields(Object node, Paint paint,
+ Map<Object, Boolean> visited) {
+ if (node == null || visited.put(node, Boolean.TRUE) != null) return;
+ if (node instanceof Paint || node instanceof String || node instanceof Number
+ || node instanceof Font) {
+ return;
+ }
+ if (node instanceof Iterable) {
+ for (Object child : (Iterable<?>) node) {
+ recolorPaintFields(child, paint, visited);
+ }
+ return;
+ }
+ if (!node.getClass().getName().startsWith("org.jfree.chart.text")) return;
+ Class<?> type = node.getClass();
+ while (type != null && type != Object.class) {
+ for (Field field : type.getDeclaredFields()) {
+ if (Modifier.isStatic(field.getModifiers())) continue;
+ try {
+ field.setAccessible(true);
+ Object value = field.get(node);
+ if (value instanceof Paint && node instanceof TextFragment) {
+ field.set(node, paint);
+ } else if (value instanceof Iterable
+ || value != null && value.getClass().getName()
+ .startsWith("org.jfree.chart.text")) {
+ recolorPaintFields(value, paint, visited);
+ }
+ } catch (IllegalAccessException ignored) {
+ // Keep the default label paint if a JFreeChart field cannot be read.
+ }
+ }
+ type = type.getSuperclass();
+ }
+ }
+ }
+
+ private Shape seriesShape(int seriesIndex) {
+ if (seriesIndex % 3 == 1) {
+ return new Rectangle2D.Double(-6.0, -6.0, 12.0, 12.0);
+ }
+ if (seriesIndex % 3 == 2) {
+ Path2D.Double diamond = new Path2D.Double();
+ diamond.moveTo(0.0, -7.0);
+ diamond.lineTo(7.0, 0.0);
+ diamond.lineTo(0.0, 7.0);
+ diamond.lineTo(-7.0, 0.0);
+ diamond.closePath();
+ return diamond;
+ }
+ return new Ellipse2D.Double(-6.0, -6.0, 12.0, 12.0);
+ }
+
+ private void stylePlot(XYPlot plot) {
+ plot.setBackgroundPaint(Color.WHITE);
+ plot.setOutlineVisible(false);
+ plot.setDomainGridlinesVisible(false);
+ plot.setRangeGridlinesVisible(false);
+ }
+
+ private void styleDomainAxis(org.jfree.chart.axis.CategoryAxis axis) {
+ axis.setTickLabelFont(TICK_FONT);
+ axis.setLabelFont(LABEL_FONT);
+ axis.setTickLabelPaint(MUTED_TEXT_COLOR);
+ axis.setLabelPaint(AXIS_COLOR);
+ axis.setAxisLinePaint(AXIS_COLOR);
+ axis.setTickMarkPaint(AXIS_COLOR);
+ axis.setCategoryMargin(0.05);
+ }
+
+ private void styleDomainAxis(org.jfree.chart.axis.ValueAxis axis) {
+ axis.setTickLabelFont(TICK_FONT);
+ axis.setLabelFont(LABEL_FONT);
+ axis.setTickLabelPaint(MUTED_TEXT_COLOR);
+ axis.setLabelPaint(AXIS_COLOR);
+ axis.setAxisLinePaint(AXIS_COLOR);
+ axis.setTickMarkPaint(AXIS_COLOR);
+ }
+
+ private void applyTheme(JFreeChart chart) {
+ StandardChartTheme theme = new StandardChartTheme("CN");
+ theme.setExtraLargeFont(BASE_FONT.deriveFont(Font.BOLD, 22f));
+ theme.setLargeFont(BASE_FONT.deriveFont(Font.BOLD, 18f));
+ theme.setRegularFont(BASE_FONT);
+ theme.setSmallFont(TICK_FONT);
+ theme.setChartBackgroundPaint(Color.WHITE);
+ theme.setPlotBackgroundPaint(Color.WHITE);
+ theme.setAxisLabelPaint(AXIS_COLOR);
+ theme.setTickLabelPaint(MUTED_TEXT_COLOR);
+ theme.setLegendItemPaint(MUTED_TEXT_COLOR);
+ theme.setLegendBackgroundPaint(Color.WHITE);
+ theme.apply(chart);
+ chart.setBackgroundPaint(Color.WHITE);
+ chart.setBorderVisible(false);
+ }
+
+ private Color seriesColor(int index) {
+ return SERIES_COLORS[index % SERIES_COLORS.length];
+ }
+
+ private byte[] render(JFreeChart chart, int width, int height, String unitLabel,
+ boolean axisChart, boolean percentItemLabels) {
+ try {
+ reserveUnitLabelBand(chart, axisChart, unitLabel);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ org.jfree.chart.ChartRenderingInfo info = new org.jfree.chart.ChartRenderingInfo();
+ java.awt.image.BufferedImage image =
+ chart.createBufferedImage(width, height, info);
+ Graphics2D graphics = image.createGraphics();
+ try {
+ graphics.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING,
+ java.awt.RenderingHints.VALUE_ANTIALIAS_ON);
+ if (axisChart && isHorizontalStackedChart(chart)) {
+ drawUnitLabelTopRight(graphics, unitLabel, info.getPlotInfo().getDataArea());
+ drawStackedValueAxis(graphics, info.getPlotInfo().getDataArea());
+ } else if (axisChart) {
+ drawAxisArrows(graphics, info.getPlotInfo().getDataArea());
+ drawUnitLabel(graphics, unitLabel, info.getPlotInfo().getDataArea());
+ drawDateLabel(graphics, info.getPlotInfo().getDataArea());
+ }
+ drawXYItemLabels(graphics, image, chart,
+ info.getPlotInfo().getDataArea(), percentItemLabels, unitLabel);
+ drawStackedBarLabels(graphics, image, chart,
+ info.getPlotInfo().getDataArea(), percentItemLabels);
+ } finally {
+ graphics.dispose();
+ }
+ javax.imageio.ImageIO.write(image, "png", out);
+ return out.toByteArray();
+ } catch (Exception e) {
+ throw new IllegalArgumentException("璧板娍鍥惧浘鐗囩敓鎴愬け璐�: " + e.getMessage(), e);
+ }
+ }
+
+ private void drawAxisArrows(Graphics2D graphics, Rectangle2D dataArea) {
+ if (dataArea == null || dataArea.getWidth() <= 0 || dataArea.getHeight() <= 0) return;
+ graphics.setColor(AXIS_COLOR);
+ graphics.setStroke(new BasicStroke(2.2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
+ int x = (int) Math.round(dataArea.getX());
+ int y = (int) Math.round(dataArea.getY());
+ int right = (int) Math.round(dataArea.getMaxX());
+ int bottom = (int) Math.round(dataArea.getMaxY());
+ graphics.drawLine(x, y, x, bottom);
+ graphics.drawLine(x, bottom, right, bottom);
+ drawArrowHead(graphics, right, bottom, 1, 0);
+ drawArrowHead(graphics, x, y, 0, -1);
+ }
+
+ private void drawArrowHead(Graphics2D graphics, int x, int y, int dx, int dy) {
+ int length = 11;
+ int width = 5;
+ if (dx != 0) {
+ graphics.drawLine(x, y, x - length, y - width);
+ graphics.drawLine(x, y, x - length, y + width);
+ } else {
+ graphics.drawLine(x, y, x - width, y + length);
+ graphics.drawLine(x, y, x + width, y + length);
+ }
+ }
+
+ private void drawUnitLabel(Graphics2D graphics, String unitLabel, Rectangle2D dataArea) {
+ if (unitLabel == null || unitLabel.isEmpty()) return;
+ graphics.setFont(LABEL_FONT);
+ graphics.setColor(MUTED_TEXT_COLOR);
+ java.awt.FontMetrics metrics = graphics.getFontMetrics();
+ int centerX = (int) Math.round(dataArea.getX());
+ int x = Math.max(4, centerX - metrics.stringWidth(unitLabel) / 2);
+ int y = (int) Math.round(Math.max(metrics.getAscent() + 2.0, dataArea.getY() - 22.0));
+ graphics.drawString(unitLabel, x, y);
+ }
+
+ private void drawUnitLabelTopRight(Graphics2D graphics, String unitLabel,
+ Rectangle2D dataArea) {
+ if (unitLabel == null || unitLabel.isEmpty() || dataArea == null) return;
+ graphics.setFont(LABEL_FONT);
+ graphics.setColor(MUTED_TEXT_COLOR);
+ FontMetrics metrics = graphics.getFontMetrics();
+ int x = (int) Math.round(dataArea.getMaxX() - metrics.stringWidth(unitLabel));
+ int y = (int) Math.round(Math.max(metrics.getAscent() + 2.0, dataArea.getY() - 22.0));
+ graphics.drawString(unitLabel, x, y);
+ }
+
+ private void reserveUnitLabelBand(JFreeChart chart, boolean axisChart, String unitLabel) {
+ if (!axisChart || unitLabel == null || unitLabel.isEmpty()) return;
+ Plot plot = chart.getPlot();
+ RectangleInsets insets = plot.getInsets();
+ plot.setInsets(new RectangleInsets(Math.max(insets.getTop(), UNIT_LABEL_BAND),
+ insets.getLeft(), insets.getBottom(), insets.getRight()));
+ }
+
+ private boolean isHorizontalStackedChart(JFreeChart chart) {
+ return chart.getPlot() instanceof CategoryPlot
+ && PlotOrientation.HORIZONTAL.equals(
+ ((CategoryPlot) chart.getPlot()).getOrientation());
+ }
+
+ private void drawDateLabel(Graphics2D graphics, Rectangle2D dataArea) {
+ if (dataArea == null || dataArea.getWidth() <= 0) return;
+ graphics.setFont(LABEL_FONT);
+ graphics.setColor(MUTED_TEXT_COLOR);
+ int right = (int) Math.round(dataArea.getMaxX());
+ int bottom = (int) Math.round(dataArea.getMaxY());
+ graphics.drawString("鏃ユ湡", right - 50, bottom - 28);
+ }
+
+ private void drawStackedValueAxis(Graphics2D graphics, Rectangle2D dataArea) {
+ if (dataArea == null || dataArea.getWidth() <= 0 || dataArea.getHeight() <= 0) return;
+ graphics.setColor(AXIS_COLOR);
+ graphics.setStroke(new BasicStroke(2.2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
+ int left = (int) Math.round(dataArea.getX());
+ int right = (int) Math.round(dataArea.getMaxX());
+ int bottom = (int) Math.round(dataArea.getMaxY());
+ graphics.drawLine(left, bottom, right, bottom);
+ }
+
+ private void drawStackedBarLabels(Graphics2D graphics, BufferedImage image, JFreeChart chart,
+ Rectangle2D dataArea, boolean percentAxis) {
+ if (!(chart.getPlot() instanceof CategoryPlot) || dataArea == null
+ || dataArea.getWidth() <= 0 || dataArea.getHeight() <= 0) {
+ return;
+ }
+ CategoryPlot plot = (CategoryPlot) chart.getPlot();
+ CategoryDataset dataset = plot.getDataset();
+ CategoryAxis domainAxis = plot.getDomainAxis();
+ ValueAxis rangeAxis = plot.getRangeAxis();
+ if (dataset == null || domainAxis == null || rangeAxis == null) return;
+
+ graphics.setFont(STACK_LABEL_FONT);
+ FontMetrics metrics = graphics.getFontMetrics();
+ int columns = dataset.getColumnCount();
+ int rows = dataset.getRowCount();
+ boolean horizontal = PlotOrientation.HORIZONTAL.equals(plot.getOrientation());
+ List<List<StackCallout>> callouts = new ArrayList<>();
+ for (int column = 0; column < columns; column++) callouts.add(new ArrayList<>());
+ List<HorizontalStackCallout> horizontalCallouts = new ArrayList<>();
+ List<Rectangle2D> occupied = new ArrayList<>();
+ for (int column = 0; column < columns; column++) {
+ double cumulative = 0.0;
+ for (int row = 0; row < rows; row++) {
+ Number number = dataset.getValue(row, column);
+ if (number == null) continue;
+ double value = number.doubleValue();
+ if (value == 0.0) continue;
+ String text = formatStackLabel(value, percentAxis);
+ if (horizontal) {
+ double startX = rangeAxis.valueToJava2D(cumulative, dataArea,
+ plot.getRangeAxisEdge());
+ double endX = rangeAxis.valueToJava2D(cumulative + value, dataArea,
+ plot.getRangeAxisEdge());
+ double segmentWidth = Math.abs(endX - startX);
+ double centerY = domainAxis.getCategoryMiddle(column, columns, dataArea,
+ plot.getDomainAxisEdge());
+ int width = metrics.stringWidth(text);
+ if (segmentWidth >= width + 4.0) {
+ int textX = (int) Math.round((startX + endX - width) / 2.0);
+ int baseline = (int) Math.round(centerY
+ + (metrics.getAscent() - metrics.getDescent()) / 2.0);
+ graphics.setColor(Color.WHITE);
+ graphics.drawString(text, textX, baseline);
+ occupied.add(new Rectangle2D.Double(textX, baseline - metrics.getAscent(),
+ width, metrics.getHeight()));
+ } else {
+ horizontalCallouts.add(new HorizontalStackCallout(Math.min(startX, endX),
+ Math.max(startX, endX), centerY, text, seriesColor(row)));
+ }
+ } else {
+ double top = rangeAxis.valueToJava2D(cumulative + value, dataArea,
+ plot.getRangeAxisEdge());
+ double bottom = rangeAxis.valueToJava2D(cumulative, dataArea,
+ plot.getRangeAxisEdge());
+ double segmentHeight = Math.abs(bottom - top);
+ double x = domainAxis.getCategoryMiddle(column, columns, dataArea,
+ plot.getDomainAxisEdge());
+ if (segmentHeight >= metrics.getHeight() + 4.0) {
+ int width = metrics.stringWidth(text);
+ int y = (int) Math.round(Math.min(top, bottom)
+ + (segmentHeight - metrics.getHeight()) / 2.0
+ + metrics.getAscent());
+ graphics.setColor(Color.WHITE);
+ graphics.drawString(text, (int) Math.round(x - width / 2.0), y);
+ occupied.add(new Rectangle2D.Double(x - width / 2.0,
+ y - metrics.getAscent(), width, metrics.getHeight()));
+ } else if (estimatePercentShare(value, percentAxis) <= 0.03
+ || !percentAxis) {
+ callouts.get(column).add(new StackCallout(x,
+ (top + bottom) / 2.0, text, seriesColor(row)));
+ }
+ }
+ cumulative += value;
+ }
+ }
+ if (horizontal) {
+ drawHorizontalStackCallouts(graphics, horizontalCallouts, dataArea, metrics, occupied);
+ } else {
+ for (int column = 0; column < columns; column++) {
+ drawStackCalloutColumn(graphics, image, callouts.get(column), column, columns,
+ dataArea, metrics, occupied);
+ }
+ }
+ }
+
+ private void drawHorizontalStackCallouts(Graphics2D graphics,
+ List<HorizontalStackCallout> items,
+ Rectangle2D dataArea, FontMetrics metrics,
+ List<Rectangle2D> occupied) {
+ if (items.isEmpty()) return;
+ Map<Long, List<HorizontalStackCallout>> rows = new LinkedHashMap<>();
+ for (HorizontalStackCallout item : items) {
+ Long key = Long.valueOf(Math.round(item.getY()));
+ List<HorizontalStackCallout> row = rows.get(key);
+ if (row == null) {
+ row = new ArrayList<>();
+ rows.put(key, row);
+ }
+ row.add(item);
+ }
+ for (List<HorizontalStackCallout> row : rows.values()) {
+ Collections.sort(row, new Comparator<HorizontalStackCallout>() {
+ @Override
+ public int compare(HorizontalStackCallout first, HorizontalStackCallout second) {
+ return Double.compare(first.getStartX(), second.getStartX());
+ }
+ });
+ int widest = 0;
+ double columnX = 0.0;
+ for (HorizontalStackCallout item : row) {
+ widest = Math.max(widest, metrics.stringWidth(item.getText()));
+ columnX = Math.max(columnX, item.getEndX() + 8.0);
+ }
+ // Keep the label column inside the reserved right margin.
+ columnX = Math.min(columnX, dataArea.getMaxX() + RIGHT_LABEL_BAND - widest);
+ int height = metrics.getHeight();
+ double step = height + 3.0;
+ double centerY = row.get(0).getY();
+ double top = centerY - (step * row.size()) / 2.0 + (step - height) / 2.0;
+ for (int i = 0; i < row.size(); i++) {
+ HorizontalStackCallout item = row.get(i);
+ int width = metrics.stringWidth(item.getText());
+ double x = columnX;
+ double y = top + i * step;
+ graphics.setColor(item.getColor());
+ graphics.setStroke(new BasicStroke(1.4f, BasicStroke.CAP_ROUND,
+ BasicStroke.JOIN_ROUND));
+ graphics.drawLine((int) Math.round(item.getEndX()), (int) Math.round(item.getY()),
+ (int) Math.round(x - 3.0), (int) Math.round(y + height / 2.0));
+ drawLabelWithHalo(graphics, item.getText(),
+ new Rectangle2D.Double(x, y, width, height), metrics, item.getColor());
+ occupied.add(new Rectangle2D.Double(x - 2.0, y - 2.0,
+ width + 4.0, height + 4.0));
+ }
+ }
+ }
+
+ private void drawStackCalloutColumn(Graphics2D graphics, BufferedImage image,
+ List<StackCallout> items, int column, int columnCount,
+ Rectangle2D dataArea, FontMetrics metrics,
+ List<Rectangle2D> occupied) {
+ if (items.isEmpty()) return;
+ Collections.sort(items, new Comparator<StackCallout>() {
+ @Override
+ public int compare(StackCallout first, StackCallout second) {
+ return Double.compare(first.getY(), second.getY());
+ }
+ });
+ double categoryWidth = dataArea.getWidth() / Math.max(1, columnCount);
+ double barHalf = Math.min(80.0, Math.max(18.0, categoryWidth * 0.30));
+ double spacing = metrics.getHeight() + 4.0;
+ double totalHeight = (items.size() - 1) * spacing;
+ double averageY = 0.0;
+ for (StackCallout item : items) averageY += item.getY();
+ averageY /= items.size();
+ double startY = averageY - totalHeight / 2.0 - metrics.getHeight() / 2.0;
+ double maxStartY = dataArea.getMaxY() - metrics.getHeight() - 3.0 - totalHeight;
+ startY = Math.max(dataArea.getMinY() + 3.0, Math.min(startY, maxStartY));
+
+ double centerX = items.get(0).getX();
+ double maxWidth = 0.0;
+ for (StackCallout item : items) {
+ maxWidth = Math.max(maxWidth, metrics.stringWidth(item.getText()));
+ }
+ double rightLeft = centerX + barHalf + 14.0;
+ double leftLeft = centerX - barHalf - 14.0 - maxWidth;
+ double rightPenalty = stackLanePenalty(rightLeft, startY, items, metrics,
+ occupied, dataArea);
+ double leftPenalty = stackLanePenalty(leftLeft, startY, items, metrics,
+ occupied, dataArea);
+ boolean rightSide = rightPenalty <= leftPenalty;
+ double laneLeft = rightSide ? rightLeft : leftLeft;
+
+ for (int i = 0; i < items.size(); i++) {
+ StackCallout item = items.get(i);
+ int width = metrics.stringWidth(item.getText());
+ int height = metrics.getHeight();
+ int y = (int) Math.round(startY + i * spacing);
+ int textX = (int) Math.round(rightSide ? laneLeft : laneLeft + maxWidth - width);
+ int textY = y + metrics.getAscent();
+ double lineStartX = centerX + (rightSide ? barHalf : -barHalf);
+ double lineEndX = rightSide ? laneLeft : laneLeft + maxWidth;
+ graphics.setColor(item.getColor());
+ graphics.setStroke(new BasicStroke(1.8f, BasicStroke.CAP_ROUND,
+ BasicStroke.JOIN_ROUND));
+ graphics.drawLine((int) Math.round(lineStartX), (int) Math.round(item.getY()),
+ (int) Math.round(lineEndX), (int) Math.round(y + height / 2.0));
+ drawLabelWithHalo(graphics, item.getText(),
+ new Rectangle2D.Double(textX, y, width, height), metrics, item.getColor());
+ occupied.add(new Rectangle2D.Double(textX - 2.0, y - 2.0,
+ width + 4.0, height + 4.0));
+ }
+ }
+
+ private double stackLanePenalty(double laneLeft, double startY, List<StackCallout> items,
+ FontMetrics metrics, List<Rectangle2D> occupied,
+ Rectangle2D dataArea) {
+ double penalty = 0.0;
+ for (int i = 0; i < items.size(); i++) {
+ double width = metrics.stringWidth(items.get(i).getText());
+ double height = metrics.getHeight();
+ Rectangle2D rectangle = new Rectangle2D.Double(laneLeft, startY + i * (height + 4.0),
+ width, height);
+ if (rectangle.getMinX() < dataArea.getMinX()
+ || rectangle.getMaxX() > dataArea.getMaxX()
+ || rectangle.getMaxY() > dataArea.getMaxY()) {
+ penalty += 10000.0;
+ }
+ penalty += overlapArea(rectangle, occupied) * 100.0;
+ }
+ return penalty;
+ }
+
+ private double estimatePercentShare(double value, boolean percentAxis) {
+ return percentAxis ? value : value / 1.0;
+ }
+
+ private String formatStackLabel(double value, boolean percentAxis) {
+ if (percentAxis) {
+ NumberFormat formatter = NumberFormat.getPercentInstance();
+ formatter.setMaximumFractionDigits(1);
+ return formatter.format(value);
+ }
+ return new DecimalFormat("#,##0").format(value);
+ }
+
+ private static class HorizontalStackCallout {
+ private final double startX;
+ private final double endX;
+ private final double y;
+ private final String text;
+ private final Color color;
+
+ private HorizontalStackCallout(double startX, double endX, double y,
+ String text, Color color) {
+ this.startX = startX;
+ this.endX = endX;
+ this.y = y;
+ this.text = text;
+ this.color = color;
+ }
+
+ private double getStartX() { return startX; }
+ private double getEndX() { return endX; }
+ private double getY() { return y; }
+ private String getText() { return text; }
+ private Color getColor() { return color; }
+ }
+
+ private static class StackCallout {
+ private final double x;
+ private final double y;
+ private final String text;
+ private final Color color;
+
+ private StackCallout(double x, double y, String text, Color color) {
+ this.x = x;
+ this.y = y;
+ this.text = text;
+ this.color = color;
+ }
+
+ private double getX() { return x; }
+ private double getY() { return y; }
+ private String getText() { return text; }
+ private Color getColor() { return color; }
+ }
+
+ private void drawXYItemLabels(Graphics2D graphics, BufferedImage image, JFreeChart chart,
+ Rectangle2D dataArea, boolean percentItemLabels,
+ String unitLabel) {
+ if (!(chart.getPlot() instanceof XYPlot) || dataArea == null
+ || dataArea.getWidth() <= 0 || dataArea.getHeight() <= 0) {
+ return;
+ }
+ XYPlot plot = (XYPlot) chart.getXYPlot();
+ XYDataset dataset = plot.getDataset();
+ if (dataset == null) return;
+
+ graphics.setFont(ITEM_LABEL_FONT);
+ FontMetrics metrics = graphics.getFontMetrics();
+ List<Rectangle2D> placedLabels = new ArrayList<>();
+ List<Rectangle2D> markers = new ArrayList<>();
+ if (unitLabel != null && !unitLabel.isEmpty()) {
+ placedLabels.add(new Rectangle2D.Double(0.0, 0.0,
+ dataArea.getMinX() + 100.0, dataArea.getMinY() + 30.0));
+ }
+ int itemCount = 0;
+ for (int seriesIndex = 0; seriesIndex < dataset.getSeriesCount(); seriesIndex++) {
+ itemCount = Math.max(itemCount, dataset.getItemCount(seriesIndex));
+ for (int itemIndex = 0; itemIndex < dataset.getItemCount(seriesIndex); itemIndex++) {
+ Point2D point = itemPoint(plot, dataset, seriesIndex, itemIndex, dataArea);
+ if (point != null) {
+ markers.add(new Rectangle2D.Double(point.getX() - 9.0,
+ point.getY() - 9.0, 18.0, 18.0));
+ }
+ }
+ }
+
+ for (int itemIndex = 0; itemIndex < itemCount; itemIndex++) {
+ List<DayLabel> labels = new ArrayList<>();
+ for (int seriesIndex = 0; seriesIndex < dataset.getSeriesCount(); seriesIndex++) {
+ if (itemIndex >= dataset.getItemCount(seriesIndex)) continue;
+ Number yValue = dataset.getY(seriesIndex, itemIndex);
+ Point2D point = itemPoint(plot, dataset, seriesIndex, itemIndex, dataArea);
+ if (yValue == null || point == null) continue;
+ labels.add(new DayLabel(seriesIndex, point,
+ formatLineLabel(yValue, percentItemLabels)));
+ }
+ // A day's labels are stacked in the same top-to-bottom order as its data
+ // points, so a label never looks attached to a neighbouring line.
+ Collections.sort(labels, new Comparator<DayLabel>() {
+ @Override
+ public int compare(DayLabel first, DayLabel second) {
+ return Double.compare(first.getPoint().getY(), second.getPoint().getY());
+ }
+ });
+ double previousBottom = Double.NEGATIVE_INFINITY;
+ for (DayLabel label : labels) {
+ int width = metrics.stringWidth(label.getText());
+ int height = metrics.getHeight();
+ double x = label.getPoint().getX() - width / 2.0;
+ double top = Math.max(label.getPoint().getY() - ITEM_LABEL_GAP - height,
+ previousBottom + 3.0);
+ Rectangle2D chosen = null;
+ double bestPenalty = Double.MAX_VALUE;
+ for (int step = 0; step <= 4; step++) {
+ Rectangle2D candidate = boundToDataArea(new Rectangle2D.Double(
+ x, top + step * (height + 4.0), width, height), dataArea);
+ if (candidate == null) continue;
+ double penalty = overlapArea(candidate, placedLabels) * 1000.0
+ + overlapArea(candidate, markers);
+ if (penalty < bestPenalty) {
+ bestPenalty = penalty;
+ chosen = candidate;
+ }
+ if (penalty <= 0.0) break;
+ }
+ if (chosen == null) continue;
+ drawLabelWithHalo(graphics, label.getText(), chosen, metrics,
+ seriesColor(label.getSeriesIndex()));
+ placedLabels.add(new Rectangle2D.Double(chosen.getX() - 5.0, chosen.getY() - 5.0,
+ chosen.getWidth() + 10.0, chosen.getHeight() + 10.0));
+ previousBottom = chosen.getMaxY();
+ }
+ }
+ }
+
+ private static class DayLabel {
+ private final int seriesIndex;
+ private final Point2D point;
+ private final String text;
+
+ private DayLabel(int seriesIndex, Point2D point, String text) {
+ this.seriesIndex = seriesIndex;
+ this.point = point;
+ this.text = text;
+ }
+
+ private int getSeriesIndex() { return seriesIndex; }
+ private Point2D getPoint() { return point; }
+ private String getText() { return text; }
+ }
+
+ private Point2D itemPoint(XYPlot plot, XYDataset dataset, int seriesIndex,
+ int itemIndex, Rectangle2D dataArea) {
+ Number yValue = dataset.getY(seriesIndex, itemIndex);
+ if (yValue == null || Double.isNaN(yValue.doubleValue())
+ || Double.isInfinite(yValue.doubleValue())) {
+ return null;
+ }
+ double x = plot.getDomainAxis().valueToJava2D(dataset.getXValue(seriesIndex, itemIndex),
+ dataArea, plot.getDomainAxisEdge());
+ double y = plot.getRangeAxis().valueToJava2D(yValue.doubleValue(),
+ dataArea, plot.getRangeAxisEdge());
+ return new Point2D.Double(x, y);
+ }
+
+ private void addLabelCandidate(List<Rectangle2D> result, double x, double y,
+ int width, int height, double dx, double dy) {
+ result.add(new Rectangle2D.Double(x + dx - width / 2.0,
+ y + dy - height / 2.0, width, height));
+ }
+
+ private Rectangle2D boundToDataArea(Rectangle2D rectangle, Rectangle2D dataArea) {
+ double minX = dataArea.getMinX() + 16.0;
+ double minY = dataArea.getMinY() + 2.0;
+ double maxX = dataArea.getMaxX() - rectangle.getWidth() - 2.0;
+ double maxY = dataArea.getMaxY() - rectangle.getHeight() - 2.0;
+ if (maxX < minX || maxY < minY) return null;
+ double x = Math.max(minX, Math.min(rectangle.getX(), maxX));
+ double y = Math.max(minY, Math.min(rectangle.getY(), maxY));
+ return new Rectangle2D.Double(x, y, rectangle.getWidth(), rectangle.getHeight());
+ }
+
+ private double overlapArea(Rectangle2D rectangle, List<Rectangle2D> occupied) {
+ double total = 0.0;
+ for (Rectangle2D other : occupied) {
+ double width = Math.min(rectangle.getMaxX(), other.getMaxX())
+ - Math.max(rectangle.getMinX(), other.getMinX());
+ double height = Math.min(rectangle.getMaxY(), other.getMaxY())
+ - Math.max(rectangle.getMinY(), other.getMinY());
+ if (width > 0.0 && height > 0.0) total += width * height;
+ }
+ return total;
+ }
+
+ private String formatLineLabel(Number value, boolean percentItemLabels) {
+ if (percentItemLabels) {
+ NumberFormat formatter = NumberFormat.getPercentInstance();
+ formatter.setMaximumFractionDigits(1);
+ return formatter.format(value.doubleValue());
+ }
+ return new DecimalFormat("#,##0.##").format(value.doubleValue());
+ }
+
+ private void drawLabelWithHalo(Graphics2D graphics, String text, Rectangle2D rectangle,
+ FontMetrics metrics, Color color) {
+ int x = (int) Math.round(rectangle.getX());
+ int y = (int) Math.round(rectangle.getY() + metrics.getAscent());
+ graphics.setColor(Color.WHITE);
+ for (int dx = -1; dx <= 1; dx++) {
+ for (int dy = -1; dy <= 1; dy++) {
+ if (dx != 0 || dy != 0) graphics.drawString(text, x + dx, y + dy);
+ }
+ }
+ graphics.setColor(color);
+ graphics.drawString(text, x, y);
+ }
+
+ public static final List<String> DAY_LABELS = Arrays.asList(
+ "10鏈�1鏃�", "10鏈�2鏃�", "10鏈�3鏃�", "10鏈�4鏃�",
+ "10鏈�5鏃�", "10鏈�6鏃�", "10鏈�7鏃�", "10鏈�8鏃�");
+
+ public static List<ChartSeries> emptySeries(int size) {
+ List<ChartSeries> result = new ArrayList<>();
+ for (int i = 0; i < size; i++) {
+ result.add(new ChartSeries());
+ }
+ return result;
+ }
+
+ @Data
+ public static class ChartSeries {
+ private String name;
+ private List<BigDecimal> values = new ArrayList<>();
+ }
+
+ @Data
+ public static class ChartCategory {
+ private String name;
+ private BigDecimal value;
+ }
+}
--
Gitblit v1.9.1