From c21fbdd3687824f79c543e20114b40c27e294008 Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期三, 07 十月 2026 11:26:51 +0800
Subject: [PATCH] fix(holiday): 统一每日客流构成外置标签布局

---
 HANDOFF.md                                                                                        |   10 +++++
 traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayTrendChartService.java |   97 +++++++++++++++++++++++++++++++++++++-----------
 2 files changed, 84 insertions(+), 23 deletions(-)

diff --git a/HANDOFF.md b/HANDOFF.md
index ffe14b4..bda268b 100644
--- a/HANDOFF.md
+++ b/HANDOFF.md
@@ -6,6 +6,16 @@
 
 ## 鏈�鏂帮紙2026-10-07 涓婂崍锛屾柊寮�浼氳瘽浜ゆ帴锛�
 
+### 10-07锛氭瘡鏃ュ娴佹瀯鎴愬浘鍙充晶鏍囩甯冨眬淇锛堥獙鏀堕�氳繃锛�
+
+- **闂**锛歚image13`銆屾瘡鏃ュ娴佹瀯鎴愬彉鍖栥�嶅彸渚у缃櫨鍒嗘瘮鏍囩鍦ㄧ浉閭绘棩鏈熶箣闂撮噸鍙� 4 澶勶紱鏀逛负涓ゅ垪鍚庡紩绾夸氦閿欙紝涓埆寮曠嚎鍘嬩綇缁胯壊鍧楀唴鏁板�笺��
+- **鏈�缁堣鍒�**锛氬彧鏈夎壊鍧楀唴鏀句笉涓嬬殑鏍囩杩涘叆鍙充晶澶栫疆鍖猴紱澶栫疆鏍囩鎸夊師濮嬪浘渚嬪簭鍙锋帓搴忥紝鍗抽亾璺啋閾佽矾鈫掓皯鑸啋姘磋矾锛涙渶澶� 2 鍒楋紝鎸夆�滃厛琛屽悗鍒椼�佹瘡琛屼粠宸﹀埌鍙斥�濆~鍏咃紱1 涓爣绛炬暣浣撳眳涓紝2 涓爣绛句袱鍒楀悇灞呬腑锛�3 涓爣绛句笅鎺掑崟鏍囩璺ㄤ袱鍒楀眳涓紝4 涓爣绛句负 2脳2 鏍煎眳涓紱涓ゅ垪甯冨眬涓嶇敾寮曠嚎锛岄鑹插搴旇壊鍧椼��
+- **瀹炵幇**锛歚HolidayTrendChartService` 灏嗗彸渚ф爣绛惧甫浠� 74 鍔犲鍒� 140锛沗HorizontalStackCallout` 璁板綍鍘熷 `seriesIndex` 骞舵寜鍏舵帓搴忥紱浠呭姣忔棩瀹㈡祦鏋勬垚鍥惧惎鐢ㄤ袱鍒楀眳涓竷灞�锛涘叕璺嚭琛岀粨鏋勪粛璧板師鍗曞垪甯﹀紩绾块�昏緫銆�
+- **楠岃瘉**锛氱湡瀹炲簱鍙鐢熸垚 2026-10-07 璧板娍鍥撅紱涓庡師鐗堢浉姣� 15 寮犲浘涓粎 `image13` 鍙樺寲锛屽叾浣� 14 寮� SHA-256 瀹屽叏涓�鑷达紱鍙充晶鏍囩鍖� OCR 閲嶅彔浠� 4 闄嶄负 0锛涙棤寮曠嚎銆傚悗绔叏閲� `mvn -q test -f traffic-audit-server\pom.xml` 閫氳繃銆�
+- **楠屾敹**锛氱敤鎴� 2026-10-07 楠屾敹 `(12).docx` 閫氳繃銆�
+- **鍚庣画**锛�10-07 鏁版嵁榻愬叏鍚庯紝鎸夌郴缁熼厤缃殑鍋囨湡澶╂暟锛�7 澶╋級鐢熸垚鏈�缁堢増璧板娍鍥撅紝鐢ㄤ簬鍒嗘瀽鎶ュ憡锛涙湰杞笉璋冩暣鍋囨棩澶╂暟鏉ユ簮銆�
+- **鏁版嵁杈圭晫**锛氭湭瀵煎叆銆佽鐩栨垨閲嶅浠讳綍涓氬姟鏁版嵁銆�
+
 ### 10-07锛氭櫘閫氬浗鐪侀亾 2025-10-05 璋冩暣鍊兼洿姝o紙宸叉牳瀵癸級
 
 - **鑳屾櫙**锛�10-05 鍓旈櫎 3 涓幆姣旈檷骞呰秴杩� 50% 鐨勭珯鐐癸紙閮戝簵銆佹▕瀹跺簞銆佷笂娲ョ珯锛夊拰 3 涓� 10-05 缂哄け绔欑偣锛堥緳娉夈�佸崕椴佷氦璋冪珯銆佸ぇ鍏告渤锛夈��
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 47bd7ca..0f06317 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
@@ -74,7 +74,7 @@
     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 RIGHT_LABEL_BAND = 140.0;
     private static final double ITEM_LABEL_GAP = 10.0;
     private static final float HALO_OUTLINE_WIDTH = 4.0f;
     private static final int PERCENT_ITEM_LABEL_DECIMALS = 1;
@@ -615,7 +615,7 @@
                         occupied.add(new Rectangle2D.Double(textX, baseline - metrics.getAscent(),
                                 width, metrics.getHeight()));
                     } else {
-                        horizontalCallouts.add(new HorizontalStackCallout(Math.min(startX, endX),
+                        horizontalCallouts.add(new HorizontalStackCallout(row, Math.min(startX, endX),
                                 Math.max(startX, endX), centerY, text, seriesColor(row)));
                     }
                 } else {
@@ -645,7 +645,8 @@
             }
         }
         if (horizontal) {
-            drawHorizontalStackCallouts(graphics, horizontalCallouts, dataArea, metrics, occupied);
+            drawHorizontalStackCallouts(graphics, horizontalCallouts, dataArea, metrics, occupied,
+                    rows >= 3);
         } else {
             for (int column = 0; column < columns; column++) {
                 drawStackCalloutColumn(graphics, image, callouts.get(column), column, columns,
@@ -657,7 +658,8 @@
     private void drawHorizontalStackCallouts(Graphics2D graphics,
                                               List<HorizontalStackCallout> items,
                                               Rectangle2D dataArea, FontMetrics metrics,
-                                              List<Rectangle2D> occupied) {
+                                              List<Rectangle2D> occupied,
+                                              boolean packIntoColumns) {
         if (items.isEmpty()) return;
         Map<Long, List<HorizontalStackCallout>> rows = new LinkedHashMap<>();
         for (HorizontalStackCallout item : items) {
@@ -673,36 +675,82 @@
             Collections.sort(row, new Comparator<HorizontalStackCallout>() {
                 @Override
                 public int compare(HorizontalStackCallout first, HorizontalStackCallout second) {
-                    return Double.compare(first.getStartX(), second.getStartX());
+                    return Integer.compare(first.getSeriesIndex(), second.getSeriesIndex());
                 }
             });
-            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);
+            if (!packIntoColumns) {
+                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));
+                }
+                continue;
             }
-            // Keep the label column inside the reserved right margin.
-            columnX = Math.min(columnX, dataArea.getMaxX() + RIGHT_LABEL_BAND - widest);
+            // Small slices are labelled outside the bar. Packing those labels
+            // into two columns keeps each day's callouts inside its own row.
+            int columnCount = row.size() <= 1 ? 1 : 2;
+            int rowCount = (int) Math.ceil(row.size() / (double) columnCount);
+            double[] columnWidths = new double[columnCount];
+            for (int i = 0; i < row.size(); i++) {
+                int column = columnCount == 1 ? 0 : i % columnCount;
+                columnWidths[column] = Math.max(columnWidths[column],
+                        metrics.stringWidth(row.get(i).getText()));
+            }
+            double columnGap = 10.0;
+            double blockWidth = columnGap * (columnCount - 1);
+            for (double columnWidth : columnWidths) blockWidth += columnWidth;
+            double blockLeft = dataArea.getMaxX() + 8.0;
+            double blockRight = dataArea.getMaxX() + RIGHT_LABEL_BAND;
+            blockLeft += Math.max(0.0, (blockRight - blockLeft - blockWidth) / 2.0);
+            double[] columnCenters = new double[columnCount];
+            double cursor = blockLeft;
+            for (int column = 0; column < columnCount; column++) {
+                columnCenters[column] = cursor + columnWidths[column] / 2.0;
+                cursor += columnWidths[column] + columnGap;
+            }
             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;
+            double rowStep = height + 3.0;
+            double totalHeight = rowCount * height + (rowCount - 1) * (rowStep - height);
+            double top = row.get(0).getY() - totalHeight / 2.0;
             for (int i = 0; i < row.size(); i++) {
                 HorizontalStackCallout item = row.get(i);
+                int column = columnCount == 1 ? 0 : i % columnCount;
+                int rowIndex = columnCount == 1 ? i : i / columnCount;
+                int itemsInRow = Math.min(columnCount, row.size() - rowIndex * columnCount);
                 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));
+                double centerX = itemsInRow == 1 && columnCount > 1
+                        ? blockLeft + blockWidth / 2.0
+                        : columnCenters[column];
+                double x = centerX - width / 2.0;
+                double y = top + rowIndex * rowStep;
                 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));
             }
+
         }
     }
 
@@ -796,14 +844,16 @@
     }
 
     private static class HorizontalStackCallout {
+        private final int seriesIndex;
         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,
+        private HorizontalStackCallout(int seriesIndex, double startX, double endX, double y,
                                        String text, Color color) {
+            this.seriesIndex = seriesIndex;
             this.startX = startX;
             this.endX = endX;
             this.y = y;
@@ -811,6 +861,7 @@
             this.color = color;
         }
 
+        private int getSeriesIndex() { return seriesIndex; }
         private double getStartX() { return startX; }
         private double getEndX() { return endX; }
         private double getY() { return y; }

--
Gitblit v1.9.1