From adee580f186d7f1cc5d5405792d6b0c13076f0a2 Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期四, 08 十月 2026 14:29:41 +0800
Subject: [PATCH] fix(holiday): 铁路客运量走势图10月3日标签上移避免遮挡

---
 traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayTrendChartService.java |   98 ++++++++++++++++++++++++++++++++++++++++++++-----
 1 files changed, 88 insertions(+), 10 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
index 3b4357d..a7c8e83 100644
--- 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
@@ -93,21 +93,33 @@
     }
 
     public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis) {
-        return lineChart(yAxisLabel, series, percentAxis, DAY_LABELS, 2, 1100, 590);
+        return lineChart(yAxisLabel, series, percentAxis, 1100, 590);
     }
 
     public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis,
                             int width, int height) {
-        return lineChart(yAxisLabel, series, percentAxis, DAY_LABELS, 2, width, height);
+        return lineChart(yAxisLabel, series, percentAxis, 2, width, height);
     }
 
     public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis,
                             int valueDecimals, int width, int height) {
-        return lineChart(yAxisLabel, series, percentAxis, DAY_LABELS, valueDecimals, width, height);
+        return lineChart(yAxisLabel, series, percentAxis, valueDecimals, DAY_LABELS, width, height);
     }
 
     public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis,
-                            List<String> dayLabels, int valueDecimals, int width, int height) {
+                            int valueDecimals, List<String> dayLabels, int width, int height) {
+        return lineChart(yAxisLabel, series, percentAxis, valueDecimals, dayLabels, width, height,
+                Collections.emptyList());
+    }
+
+    /**
+     * Renders a line chart and pins the listed labels to a fixed anchor. The overrides are
+     * opt-in: a chart that passes an empty list keeps the automatic placement unchanged.
+     */
+    public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis,
+                            int valueDecimals, List<String> dayLabels, int width, int height,
+                            List<LineLabelOverride> labelOverrides) {
+        List<String> axisLabels = (dayLabels == null || dayLabels.isEmpty()) ? DAY_LABELS : dayLabels;
         XYSeriesCollection dataset = new XYSeriesCollection();
         for (int seriesIndex = 0; seriesIndex < series.size(); seriesIndex++) {
             ChartSeries item = series.get(seriesIndex);
@@ -137,7 +149,6 @@
         plot.setRangeGridlinesVisible(false);
         plot.setDomainTickBandPaint(null);
         plot.setRangeTickBandPaint(null);
-        List<String> axisLabels = dayLabels == null || dayLabels.isEmpty() ? DAY_LABELS : dayLabels;
         SymbolAxis domainAxis = new SymbolAxis(null, axisLabels.toArray(new String[0]));
         domainAxis.setGridBandPaint(new Color(0, 0, 0, 0));
         domainAxis.setGridBandAlternatePaint(new Color(0, 0, 0, 0));
@@ -181,7 +192,7 @@
         }
         plot.setRenderer(renderer);
         return render(chart, width, scaleLineChartHeight(height),
-                yAxisLabel, true, percentAxis, valueDecimals);
+                yAxisLabel, true, percentAxis, valueDecimals, labelOverrides);
     }
 
     public byte[] pieChart(String valueSuffix, List<ChartCategory> categories,
@@ -461,6 +472,13 @@
 
     private byte[] render(JFreeChart chart, int width, int height, String unitLabel,
                          boolean axisChart, boolean percentItemLabels, int valueDecimals) {
+        return render(chart, width, height, unitLabel, axisChart, percentItemLabels,
+                valueDecimals, Collections.emptyList());
+    }
+
+    private byte[] render(JFreeChart chart, int width, int height, String unitLabel,
+                         boolean axisChart, boolean percentItemLabels, int valueDecimals,
+                         List<LineLabelOverride> labelOverrides) {
         try {
             reserveUnitLabelBand(chart, axisChart, unitLabel);
             ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -480,7 +498,8 @@
                     drawDateLabel(graphics, info.getPlotInfo().getDataArea());
                 }
                 drawXYItemLabels(graphics, image, chart,
-                        info.getPlotInfo().getDataArea(), percentItemLabels, unitLabel, valueDecimals);
+                        info.getPlotInfo().getDataArea(), percentItemLabels, unitLabel, valueDecimals,
+                        labelOverrides);
                 drawStackedBarLabels(graphics, image, chart,
                         info.getPlotInfo().getDataArea(), percentItemLabels);
             } finally {
@@ -896,7 +915,8 @@
 
     private void drawXYItemLabels(Graphics2D graphics, BufferedImage image, JFreeChart chart,
                                   Rectangle2D dataArea, boolean percentItemLabels,
-                                  String unitLabel, int valueDecimals) {
+                                  String unitLabel, int valueDecimals,
+                                  List<LineLabelOverride> labelOverrides) {
         if (!(chart.getPlot() instanceof XYPlot) || dataArea == null
                 || dataArea.getWidth() <= 0 || dataArea.getHeight() <= 0) {
             return;
@@ -938,7 +958,8 @@
             List<List<LineLabelCandidate>> candidateSets = new ArrayList<>();
             for (DayLabel label : labels) {
                 List<LineLabelCandidate> candidates = lineLabelCandidates(
-                        label, metrics, dataArea);
+                        label, metrics, dataArea,
+                        forcedAnchor(labelOverrides, label.getSeriesIndex(), itemIndex));
                 if (candidates.isEmpty()) {
                     candidateSets.clear();
                     break;
@@ -1057,7 +1078,8 @@
     }
 
     private List<LineLabelCandidate> lineLabelCandidates(DayLabel label, FontMetrics metrics,
-                                                         Rectangle2D dataArea) {
+                                                         Rectangle2D dataArea,
+                                                         LineLabelAnchor forcedAnchor) {
         List<LineLabelCandidate> result = new ArrayList<>();
         int width = metrics.stringWidth(label.getText());
         int height = metrics.getHeight();
@@ -1067,6 +1089,25 @@
         double farHorizontalOffset = horizontalOffset + 18.0;
         double verticalOffset = ITEM_LABEL_GAP + height / 2.0;
         double farVerticalOffset = verticalOffset + height + 6.0;
+        if (forcedAnchor != null) {
+            switch (forcedAnchor) {
+                case ABOVE:
+                    addLabelCandidate(result, x, y, width, height, 0.0, -verticalOffset, dataArea);
+                    break;
+                case BELOW:
+                    addLabelCandidate(result, x, y, width, height, 0.0, verticalOffset, dataArea);
+                    break;
+                case LEFT:
+                    addLabelCandidate(result, x, y, width, height, -horizontalOffset, 0.0, dataArea);
+                    break;
+                case RIGHT:
+                    addLabelCandidate(result, x, y, width, height, horizontalOffset, 0.0, dataArea);
+                    break;
+                default:
+                    break;
+            }
+            return result;
+        }
         addLabelCandidate(result, x, y, width, height, 0.0, -verticalOffset, dataArea);
         addLabelCandidate(result, x, y, width, height, 0.0, verticalOffset, dataArea);
         addLabelCandidate(result, x, y, width, height, horizontalOffset, 0.0, dataArea);
@@ -1110,6 +1151,43 @@
         return result;
     }
 
+    /**
+     * Fixed positions a data label can be pinned to, relative to its own point.
+     */
+    public enum LineLabelAnchor {
+        ABOVE, BELOW, LEFT, RIGHT
+    }
+
+    /**
+     * Opt-in request that pins one data label (series + item) to a fixed anchor.
+     */
+    public static class LineLabelOverride {
+        private final int seriesIndex;
+        private final int itemIndex;
+        private final LineLabelAnchor anchor;
+
+        public LineLabelOverride(int seriesIndex, int itemIndex, LineLabelAnchor anchor) {
+            this.seriesIndex = seriesIndex;
+            this.itemIndex = itemIndex;
+            this.anchor = anchor;
+        }
+
+        public int getSeriesIndex() { return seriesIndex; }
+        public int getItemIndex() { return itemIndex; }
+        public LineLabelAnchor getAnchor() { return anchor; }
+    }
+
+    private static LineLabelAnchor forcedAnchor(List<LineLabelOverride> labelOverrides,
+                                               int seriesIndex, int itemIndex) {
+        if (labelOverrides == null) return null;
+        for (LineLabelOverride override : labelOverrides) {
+            if (override.getSeriesIndex() == seriesIndex && override.getItemIndex() == itemIndex) {
+                return override.getAnchor();
+            }
+        }
+        return null;
+    }
+
     private void addLabelCandidate(List<LineLabelCandidate> result, double x, double y,
                                    int width, int height, double dx, double dy,
                                    Rectangle2D dataArea) {

--
Gitblit v1.9.1