From f2056aa286179f5d29f3fb6fce1479035cca856a Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期一, 12 十月 2026 17:14:15 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/main'

---
 HANDOFF.md                                                                                            |  110 +++++++++++++++
 traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayTrendChartService.java     |  193 +++++++++++++++++++++++----
 traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayTrendService.java          |   33 +++-
 traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayTrendChartServiceTest.java |   82 +++++++++++
 4 files changed, 380 insertions(+), 38 deletions(-)

diff --git a/HANDOFF.md b/HANDOFF.md
index 5b161de..e9726de 100644
--- a/HANDOFF.md
+++ b/HANDOFF.md
@@ -238,6 +238,106 @@
 - **鍗$偣鏈彉**锛歚resolveSummaryMother`锛坄ReportExportService.java:4576`锛夊彧绮剧‘鍖归厤銆妠year}骞磠month}鏈堥亾璺繍杈撻噺姹囨�昏〃.xlsx銆嬶紝鐩爣鏈堟棤姣嶇増鍗虫姤閿欙紱9 鏈堟瘝鐗堜粛涓虹敤鎴� 09-29 鎵嬪伐鎵╁垪浜х墿锛堟湰鏈烘湭鍏ュ簱锛夈��
 - **寰呯‘璁�**锛氣憼 鎵╁垪钃濇湰鐢� 9 鏈堟墜宸ョ増杩樻槸 8 鏈堢増锛涒憽 鏄惁涓�娆″缓鎴� 12 鏈堟弧鐗堝紡锛堟柟妗� v1 寤鸿婊$増寮忥級銆�
 - **鐘舵��**锛氭湰娈典笌 M0 瑙勬牸鍧囦负銆愬緟楠屾敹銆戯紱鏈疆鏈敼鍔ㄤ换浣曚唬鐮併�傚伐浣滃尯鏈窡韪殑 `docs/鐢熸垚姹囨�诲ぇ琛�/2026骞�9鏈堥亾璺繍杈撻噺姹囨�昏〃*.xlsx` 涓� `docs/纭.docx` 鎸夌害瀹氫繚鐣欍��
+## 鏈�鏂帮紙2026-10-08 閾佽矾瀹㈣繍閲忚蛋鍔垮浘10鏈�3鏃ユ爣绛鹃伩璁╋級
+
+### 10-08锛氶搧璺杩愰噺璧板娍鍥� 10鏈�3鏃� `72.4` 鏍囩涓婄Щ锛岄伩鍏嶈 `70.6` 閬尅
+
+- **闇�姹傦紙鐢ㄦ埛锛�**锛氳蛋鍔垮浘鍏朵綑閮ㄥ垎涓�寰嬩笉鍔紝鍙妸 `鈥滃崄涓�鈥濆亣鏈熼搧璺杩愰噺鎯呭喌` 鍥句腑 10鏈�3鏃� `72.4` 鐨勬爣绛句粠鐐圭殑鍙宠竟绉诲埌鐐规涓婃柟锛涜浣嶇疆鍘熸湰绌虹櫧锛屾斁鍙宠竟浼氳 2024 骞寸豢鑹� `70.6` 鏍囩鍘嬩綇涓�閮ㄥ垎瀵艰嚧鐪嬩笉鍏ㄣ��
+- **鏁版嵁鑳屾櫙锛�2026 鍗佷竴閾佽矾瀹㈣繍閲忥紝涓囦汉娆★級**锛�2026 骞� 10鏈�3鏃� = 72.4锛�2025 骞村悓鏃� = 72.3锛�2024 骞村悓鏃� = 70.6銆備笁鏉$嚎鍚屾棩鏁板�奸潪甯告帴杩戯紝鑷姩甯冨眬鎶� `72.4` 鏀惧埌浜嗙偣鍙充晶銆�
+- **鏍瑰洜**锛氭爣绛捐嚜鍔ㄥ竷灞�锛坄HolidayTrendChartService.drawXYItemLabels` 鍔ㄦ�佽鍒掞級鍙鈥滄寜鏁板�兼帓搴忓悗鐩搁偦鐨勪袱涓爣绛锯�濆仛閲嶅彔鎯╃綒锛沗72.4` 涓� `70.6` 涔嬮棿闅旂潃 `72.3`锛屼簩鑰呬笉鐩搁偦锛岄噸鍙犳湭琚儵缃氾紝鍥犳鍏佽浜掔浉瑕嗙洊銆�
+- **鏀瑰姩**锛歚HolidayTrendChartService.lineChart` 鏂板鍙�� `List<LineLabelOverride> labelOverrides` 鍏ュ弬锛堥厤鍚� `LineLabelAnchor{ABOVE,BELOW,LEFT,RIGHT}`锛夛紝鍙妸鎸囧畾 `(seriesIndex,itemIndex)` 鐨勬爣绛惧浐瀹氬埌鏌愪釜鏂逛綅锛涗笉浼犲弬鏃跺畬鍏ㄨ蛋鍘熻嚜鍔ㄥ竷灞�锛屾覆鏌撻�愬瓧鑺備笉鍙樸�俙HolidayTrendService` 浠呭閾佽矾鍥句紶涓�鏉� override锛歚seriesIndex=0`锛�2026 骞达級銆乣itemIndex=2`锛�10鏈�3鏃ワ級銆乣ABOVE`銆�
+- **楠岃瘉**锛歚mvn -o test` 17/17 閫氳繃锛堟柊澧炴爣绛鹃敋鐐瑰崟娴嬶細绌�/涓嶅尮閰� override 涓嶆敼鍙樻覆鏌撱�丄BOVE 楂樹簬 BELOW锛夛紱鐪熷疄搴撶鍒扮閲嶆柊鐢熸垚璧板娍鍥撅紝15 寮犲浘涓粎 `image5.png`锛堥搧璺浘锛塖HA-256 鍙樺寲锛屽叾浣� 14 寮犻�愬瓧鑺備竴鑷达紱閾佽矾鍥惧墠鍚庡儚绱犲樊鍖呭洿鐩� `x 313-436, y 395-438`锛屾鏄鏍囩鍖哄煙锛涙湰鍦� OCR 纭 `72.4` 瀹屾暣鍙銆佷綅浜庣偣锛坹鈮�427锛変笂鏂癸紝涓嶅啀涓� `70.6`锛坹431-450锛夐噸鍙犮��
+- **杈圭晫**锛氬彧鏀逛簡閾佽矾杩欎竴寮犲浘鐨勮繖涓�澶勬爣绛撅紱蹇姤銆佹暟鎹鍏ャ�佸叾瀹冪粺璁¢�昏緫鍧囨湭瑙︾锛涙湭瀵煎叆/瑕嗙洊/閲嶅浠讳綍涓氬姟鏁版嵁銆�
+- **閲嶅惎**锛氫负璁╂寜閽笅杞界敤涓婃柊浠g爜锛屽凡鐢� `start-dev.ps1` 閲嶅惎鍚庣锛堝墠绔湭鍔級锛涘彲鐩存帴鍦ㄧ郴缁熼噷閲嶆柊涓嬭浇璧板娍鍥鹃獙鏀躲��
+- **寰呭姙/椋庨櫓**锛氳繖鏉� override 缁戝畾 `seriesIndex=0`锛�2026 骞达級涓� `itemIndex=2`锛�10鏈�3鏃ワ級锛涜嫢鍚庣画閾佽矾鍥剧郴鍒楅『搴忔垨澶╂暟鍙e緞璋冩暣锛岄渶瑕佸悓姝ユ牳瀵广�傚叾浣欏勾浠�/鏃ユ湡濡傚啀鍑虹幇鏍囩閬尅锛屽彲澶嶇敤璇� override 鏈哄埗锛屼絾闇�閫愬浘浜哄伐纭鍚庡啀鍔犮��
+- **楠屾敹**锛氱敤鎴� 2026-10-08 宸茬湅鍥剧‘璁ゆ晥鏋滃彲浠ャ��
+
+## 鍘嗗彶锛�2026-10-08 璧板娍鍥鹃檮褰曚簩瑙傛祴绔欏浘澶╂暟淇锛�
+
+
+
+### 10-08锛氳娴嬬珯閫熷害/鎷ユ尋搴﹁蛋鍔垮浘鏀圭敤绯荤粺閰嶇疆鍋囨湡澶╂暟锛堝凡楠屾敹锛宑ommit e958585锛�
+
+- **闇�姹傦紙鐢ㄦ埛锛�**锛氳蛋鍔垮浘鏈�鍚庝袱寮犲浘鈥滄櫘閫氬浗鐪侀亾瑙傛祴绔欏钩鍧囨満鍔ㄨ溅閫熷害鈥濃�溾�﹀钩鍧囨嫢鎸ゅ害鈥濆彧鍚粖骞存暟鎹紝妯酱澶╂暟涓嶅簲鏄浐瀹� 8 澶╋紝鑰屽簲涓虹郴缁熼厤缃殑鍋囨湡澶╂暟銆�
+- **鏀瑰姩**锛歚HolidayTrendChartService.lineChart` 鏂板鍙紶 `dayLabels` 鐨勯噸杞斤紙榛樿浠嶄负 `DAY_LABELS` 8 澶╋紝鍏朵綑 13 寮犲浘闆舵敼鍔級锛沗HolidayTrendService.observationData` 鏀逛负鎸� `configService.findHolidayDays(year, holidayType)`锛坄day_index>=1`锛夌殑澶╂暟涓庢棩鏈熺敓鎴愭暟鎹偣鍜屾í杞存爣绛撅紝鏈�鍚庝袱寮犲浘浼犲叆璇ユ爣绛俱��
+- **閰嶇疆渚濇嵁**锛歚holiday_calendar` 2026 鍗佷竴 `day_index 1..7`锛�7 澶╋紝`day_index 0` = 09-30 鐜瘮鍩烘湡锛夛紱2025 = 8 澶┿��2024 = 7 澶┿��
+- **楠岃瘉**锛歚mvn -o test` 16/16 閫氳繃锛堟柊澧炶嚜瀹氫箟妯酱鏍囩鍗曞厓娴嬭瘯锛夛紱鐪熷疄浜戝彧璇荤敓鎴愯蛋鍔垮浘锛屼笌鐢ㄦ埛宸蹭笅杞界増閫愬浘瀵规瘮锛歩mage1~13 鐨� SHA-256 瀹屽叏涓�鑷达紝浠� image14锛堥�熷害锛夈�乮mage15锛堟嫢鎸ゅ害锛夊彉鍖栵紱OCR 纭鏂板浘妯酱涓� 10鏈�1鏃10鏈�7鏃ャ�佹棤 10鏈�8鏃ャ��
+- **杈圭晫**锛氬叾浣欏浘浠嶅浐瀹� 8 澶╁彛寰勶紝鏈敼锛涙湭瀵煎叆/瑕嗙洊/閲嶅浠讳綍涓氬姟鏁版嵁銆�
+- **閲嶅惎**锛氫负璁╂寜閽笅杞界敤涓婃柊浠g爜锛屽凡鐢� `start-dev.ps1` 閲嶅惎鍚庣锛堝墠绔湭鍔級锛岄噸鍚椂闂� 2026-10-08 10:38:53锛涘彲鐩存帴鍦ㄧ郴缁熼噷閲嶆柊涓嬭浇璧板娍鍥鹃獙鏀躲��
+- **楠屾敹**锛氱敤鎴� 2026-10-08 鍦ㄧ郴缁熼噷閲嶆柊涓嬭浇璧板娍鍥惧悗瀹℃牳锛岄獙鏀堕�氳繃銆�
+
+## 鍘嗗彶锛�2026-10-08 涓婄彮鍓嶅叧鏈哄揩鐓э級
+
+### 10-08锛氳蛋鍔垮浘 `(14)` 涓庡凡楠屾敹 `(12)` 姣斿锛堝唴瀹逛竴鑷达級
+
+- **鐢ㄦ埛浠诲姟**锛氭瘮瀵圭郴缁熼噸鏂颁笅杞界殑 `C:\Users\jcxiong\Downloads\2026骞村崄涓�鍋囨湡鍏ㄧ渷缁煎悎杩愯緭瀹㈡祦閲忚蛋鍔垮浘 (14).docx` 涓庢鍓嶉獙鏀堕�氳繃鐨� `(12)`銆�
+- **姣斿鏂规硶**锛氭寜 DOCX 鍖呴�愰」姣斿銆俙(12)`銆乣(13)`銆乣(14)` 鐨� 31 涓唴閮ㄦ枃浠讹紙鍚鏂� XML銆佹牱寮忋�佸叧绯绘枃浠跺拰鍏ㄩ儴鍥捐〃鍥剧墖锛夐�愪釜 SHA-256 瀹屽叏鐩稿悓銆�
+- **姣斿缁撴灉**锛氫笁涓枃浠舵暣浣� SHA-256 涓嶅悓锛屼粎鍥� ZIP 瀹瑰櫒鏃堕棿鎴崇瓑鎵撳寘鍏冩暟鎹笉鍚岋紱鏂囦欢闀垮害鐩稿悓锛屽樊寮備负 234 涓瓧鑺傦紝闆嗕腑鍦� 3 瀛楄妭鏃堕棿鎴虫锛屾枃妗e唴瀹规病鏈変换浣曞樊寮傘��
+- **缁撹**锛歚(14)` 灏辨槸宸查獙鏀剁殑 `(12)` 鍚屼竴鐗堟湰锛屽浘琛ㄦ暟鍊笺�佹爣绛俱�佺増寮忓潎鏈彉鍖栵紝涓嶉渶瑕侀噸鏂伴獙鏀躲��
+- **杈圭晫**锛氬彧璇绘瘮瀵癸紝鏈慨鏀逛笅杞芥枃浠躲�佷唬鐮併�佺郴缁熸暟鎹垨涓氬姟搴撱��
+- **褰撳墠杩愯**锛�2026-10-08 閲嶅惎鍚庯紝宸茬敤 `start-dev.ps1` 鎭㈠ dev 鐜锛涘墠绔� `8080`銆佸悗绔� `8090` 鍧囧湪鐩戝惉骞惰闂甯搞�侴it 鍦ㄦ湰娆′氦鎺ユ彁浜ゅ墠涓� `HEAD = origin/main = c7b6553`锛涘伐浣滃尯鏃犲凡璺熻釜鏀瑰姩锛屽彧鍓╃害瀹氫笉鍏ュ簱鐨� `_tmp_*`銆乣_tmp_pack_smoke_trend.docx` 鍜岃娴嬬珯鍘熷 XLS銆�
+- **鏁版嵁涓庝笅涓�姝�**锛氬綋鍓嶄笟鍔″簱浠嶅彧瑕嗙洊鍒� `2026-10-06`锛宍2026-10-07` 鏈鍏ャ�傛柊瀵硅瘽鍏堣 `AGENTS.md` + `HANDOFF.md`锛涚瓑 10-07 鏁版嵁榻愬叏鍚庯紝鍏堝彧璇绘牳瀵规簮 Excel 鍜屽紓甯搁」锛屽緱鍒扮敤鎴锋槑纭寚绀哄悗鍐嶅鍏ユ垨鏀瑰姩锛屽啀鐢熸垚鏈�缁堣蛋鍔垮浘鍜屽垎鏋愭姤鍛娿�傛姌绾垮浘妯酱浠嶆寜宸茬‘璁ょ殑鍥哄畾 8 澶╁彛寰勶紝涓嶈兘鏀规垚鎸夊勾搴︽埅鏂��
+
+### 10-08锛氬悓浜嬪垎鏋愭姤鍛婅秼鍔挎牳瀵癸紙鍙锛屾湭鏀瑰姩鏂囨。锛�
+
+- **浠诲姟**锛氭牳瀵� `D:\03_鍏徃\02_椤圭洰\27_婀栧寳鐪侀亾璺繍杈撲簨涓氬彂灞曚腑蹇僜10_涓氬姟\99_涓撻」缁熻\01_閲嶈鑺傚亣鏃2026\鍗佷竴\2026骞村崄涓�.docx` 鐨勮蛋鍔跨粨璁烘槸鍚︽湁鏁版嵁鏀拺銆傜敤鎴锋槑纭厛鍙湅瓒嬪娍锛屼笉绾犵粨鍋囨湡鏈粨鏉熸椂鐨勫叿浣撴暟鍊硷紱鏂囦腑鍘诲勾鐨勫浘涓嶄綔涓哄垽鏂緷鎹��
+- **褰撳墠鏁版嵁杈圭晫**锛氬彧璇绘煡璇㈢‘璁� `holiday_passenger_flow`銆乣holiday_vehicle_flow`銆乣observation_station_flow` 鍧囧彧瑕嗙洊鍒� `2026-10-06`锛沗2026-10-07` 灏氭湭瀵煎叆銆傛湰杞湭鎵ц浠讳綍瀵煎叆銆佽鐩栨垨閲嶅銆�
+- **鎬讳綋缁撹**锛氭姤鍛婁富绾垮熀鏈垚绔嬨��10 鏈� 1 鏃ヨ嚦 6 鏃ヨ法鍖哄煙鎬婚噺涓洪鏃ラ珮寮�銆�2 鏃ヨ嚦 3 鏃ヤ笅闄嶃��4 鏃ヨ嚦 5 鏃ュ洖鍒伴珮浣嶃��6 鏃ュ皬骞呭洖钀斤紱閬撹矾瀹㈣繍绋冲畾銆侀搧璺寜鍚屾棩鏈熸瘮杈冨敮涓�澧為暱涓旀疆姹愭�ф槑鏄俱�佹按璺悓姣斾笅闄嶄笖涓夊勾宄板�煎潎鍦� 10 鏈� 5 鏃ュ墠鍚庛�佽嚜椹句富浣撳湴浣嶅拰鍥界渷閬撴棩娴侀噺楂樹簬 300 涓囪締娆★紝鍧囨湁鏁版嵁鏀拺銆�
+- **闇�瑕佹敹绱х殑缁撹**锛氶槾闆ㄥ鑷翠笅闄嶇洰鍓嶅彧鑳借鏄庢椂闂村惢鍚堬紝涓嶈兘鍗曠嫭璇佹槑鍥犳灉锛涒�滃墠鏈熺Н鍘嬮渶姹傞噴鏀锯�濊瘉鎹笉瓒筹紱閬撹矾杞︽祦鎬婚噺鍙湁 10 鏈� 4 鏃ユ槑鏄惧洖鍗囷紝5 鏃ュ熀鏈寔骞炽��6 鏃ュ張鍥炶惤锛屼笉鑳藉啓鎴� 4 鏃ヨ嚦 7 鏃ユ寔缁珮浣嶏紱鈥滅煭閫旀父銆佸懆杈规父銆佷埂鏉戝井搴﹀亣鈥濆彧鑳借瘉鏄庣煭閫旈�氳娲昏穬锛岄渶鏂囨梾銆佽矾绾挎垨 OD 鏁版嵁锛涒�滃幙鍩熷強鍩庝埂鐭�斿杩愰煣鎬�/鍏滃簳鈥濋渶缁撴瀯鏁版嵁锛�2025 涓� 2026 鍙兘璇存�讳綋褰㈡�佺浉杩戯紝涓嶆槸鏃ラ棿鑺傚涓�鑷淬��
+- **寰� 10-07 鏁版嵁榻愬叏鍚庡畾绋�**锛�7 澶╂�婚噺銆佹棩鍧囥�佸悇鏂瑰紡鍗犳瘮銆佸嘲鍊艰胺鍊笺�佽繑绋嬮珮浜庡嚭琛屻�侀搧璺拰姘戣埅灏炬棩銆佸叏鍛ㄦ湡鍚屾瘮銆傞噸瑕佸彛寰勶細2025 骞翠负 8 澶┿��2026 骞寸洰鍓嶄负 7 澶╋紝鍚屾瘮蹇呴』鎸夌浉鍚屾棩鏈熻寖鍥存瘮杈冿紝涓嶈兘鐩存帴鐢� 7 澶╁ 8 澶┿��
+- **杈圭晫**锛氭湭淇敼鍚屼簨鐨� Word锛屾湭鏀瑰姩绯荤粺鏁版嵁锛屾湭鐢熸垚鏂版姤鍛婃垨璧板娍鍥俱�傚彂鐜伴棶棰樺厛鎶ュ憡锛屽緱鍒扮敤鎴锋槑纭寚绀哄悗鍐嶆敼銆�
+- **Git/杩愯**锛歚HEAD = origin/main = ca0bbe1`锛涙棤宸茶窡韪枃浠舵湭鎻愪氦鏀瑰姩銆俤ev 鍚庣 8090銆佸墠绔� 8080 鍦ㄥ叧鏈哄墠淇濇寔杩愯锛岄殢鍏虫満鑷姩鍋滄锛涢噸鍚悗鎸� `AGENTS.md` 鐢� `start-dev.ps1` 鍚姩銆�
+
+## 鍘嗗彶锛�2026-10-07 涓婂崍锛屾柊寮�浼氳瘽浜ゆ帴锛�
+
+### 10-07 鍏虫満蹇収锛堝綋鍓嶄氦鎺ワ級
+
+- **Git 鐘舵��**锛歚HEAD` 涓� `origin/main` 鍧囦负 `179544a`锛涘凡璺熻釜鏂囦欢鏃犳湭鎻愪氦鏀瑰姩銆傚伐浣滃尯浠呬繚鐣欐寜绾﹀畾涓嶅叆搴撶殑 `_tmp_*` 涓存椂鐩綍銆乣_tmp_pack_smoke_trend.docx` 鍜屼竴浠借娴嬬珯鍘熷 XLS銆�
+- **鏈疆鏇存**锛氭垜璇皢鎶樼嚎鍥惧浐瀹� 8 澶╁垽鏂负缂洪櫡锛屾彁浜� `8ad75d5` 鎿呰嚜鏀规垚浜嗘寜骞村害閰嶇疆鎴彇锛涚敤鎴锋槑纭籂姝e悗锛屽凡鐢� `179544a` 鏁翠綋鍥為��骞舵帹閫併�傚綋鍓嶄唬鐮佺姸鎬佺瓑鍚岄敊璇慨鏀瑰墠鐨� `c21fbdd`銆�
+- **宸茬‘璁ゅ彛寰勶紙涓嶅緱鍐嶆敼锛�**锛氭姌绾垮浘妯酱鍥哄畾 8 澶╋紱2025 骞存湁 10 鏈� 8 鏃ョ殑鏁版嵁鐐瑰拰妯酱浣嶇疆锛�2024銆�2026 骞磋鏃ユ棤鐐逛絾妯酱淇濈暀 10 鏈� 8 鏃ワ紱绱鏇茬嚎鍙繛鎺ュ疄闄呮湁鏁版嵁鐨勬棩鏈燂紝缂烘棩涓嶅欢缁紱瑙傛祴绔欓檮褰曟煡璇㈣鐩� 10 鏈� 1 鏃ヨ嚦 10 鏈� 8 鏃ャ��
+- **閿欒浜х墿鎻愰啋**锛歚C:\Users\jcxiong\Downloads\2026骞村崄涓�鍋囨湡鍏ㄧ渷缁煎悎杩愯緭瀹㈡祦閲忚蛋鍔垮浘锛�7澶╃増-寰呴獙鏀讹級.docx` 鏄敊璇敼鍔ㄦ椂鐢熸垚鐨勬枃浠讹紝涓嶅緱浣跨敤锛涙湭缁忕敤鎴锋寚绀轰笉鍒犻櫎銆�
+- **杩愯鐘舵��**锛氬叧鏈哄墠鍚庣 `8090`銆佸墠绔� `8080` 浠嶇洃鍚紝闅忓叧鏈鸿嚜鍔ㄥ仠姝紱涓嬫満鍚庢寜 `AGENTS.md` 鐢� `start-dev.ps1` 鍚姩銆�
+- **鏁版嵁杈圭晫**锛氭湰杞湭瀵煎叆銆佽鐩栨垨閲嶅浠讳綍涓氬姟鏁版嵁銆�
+- **涓嬫鎺ユ墜**锛氬厛璇绘牴鐩綍 `AGENTS.md` + 鏈枃浠讹紱鑻ョ敤鎴锋彁渚涗簡 10 鏈� 7 鏃ユ暟鎹紝鍏堝彧璇绘牳楠岋紝鍙戠幇寮傚父鍏堟姤鍛婏紝寰楀埌鏄庣‘鎸囩ず鍚庡啀鎵ц浠讳綍淇敼銆佸鍏ユ垨鎻愪氦銆�
+
+### 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 缂哄け绔欑偣锛堥緳娉夈�佸崕椴佷氦璋冪珯銆佸ぇ鍏告渤锛夈��
+- **绔欑偣缂栧彿**锛氶儜搴� `S101L110420115`銆佹▕瀹跺簞 `S120L105420702`銆佷笂娲ョ珯 `G242L104420322`銆侀緳娉� `S216L103420506`銆佸崕椴佷氦璋冪珯 `S223L117421024`銆佸ぇ鍏告渤 `S464L105422828`銆�
+- **鍚勬棩鍓旈櫎鍚堣锛堜腑灏忓娴侀噺锛岃締娆★級**锛�2026-10-04 `16,484`锛�2026-10-05 `4,398`锛�2025-10-05 `14,239`锛�2024-10-05 `11,907`銆傝皟鏁存椂缂哄け绔欑偣鎸� 0 澶勭悊銆�
+- **绯荤粺鏇存**锛歚holiday_vehicle_adjustment` 鐨� 2025-10-05 璋冩暣鍊煎凡鐢� `322.7801` 鏀逛负 `332.7801`锛堟洿鏂版椂闂� 2026-10-07 08:45:52锛夛紱2024-10-05 `289.5557`銆�2025-10-04 `337.5009`銆�2024-10-04 `322.5277` 涓嶅彉銆�
+- **宸ヤ綔绨挎洿姝�**锛歚C:\Users\jcxiong\Downloads\2026骞村崄涓�鏁版嵁 (4).xlsx`锛堜慨鏀规椂闂� 08:46:52锛変腑 `鏅�氬浗鐪侀亾!F7=332.7801`銆傝仈鍔ㄧ粨鏋滃凡姝g‘閲嶇畻锛歚鏅�氬浗鐪侀亾!G7=-4.8%`锛沗浜哄憳娴佸姩閲忔眹鎬籤 鐨� `E7=1460.97`銆乣F7=-0.3%`銆乣K7=1372.85`銆乣Q7=1290.04`銆乣R7=-1.0%`銆乣V7=-4.28%`銆�
+- **鍏紡鏇存宸叉牳瀹�**锛歚鏅�氬浗鐪侀亾!E8=(D8-D7)/D7`銆乣G8=D8/F8-1`銆乣I8=D8/H8-1`銆乣E9=(D9-D8)/D8`銆�
+- **10-06 杈圭晫锛堥噸瑕侊級**锛氬伐浣滅翱鐩墠鍙湁閾佽矾 `C9=96.2`銆佹按璺� `C8=5.4932`銆侀珮閫熷叕璺� `C8=206.1206`锛涙皯鑸� `C9`銆侀亾璺� `C8`銆佹櫘閫氬浗鐪侀亾 `D8` 浠嶄负绌恒�傚洜姝� `浜哄憳娴佸姩閲忔眹鎬�!C8=596.38264` 鍙槸閮ㄥ垎鍚堣锛宍D8=-59.06%` 浠ュ強鍚� `-100%/#DIV/0!` 閮戒笉鍙敤浜庡揩鎶ワ紱琛ラ綈涓夐」鍚庨噸绠椼��
+- **鏁版嵁杈圭晫**锛氭湰杞彧璇绘牳瀵癸紝鏈鍏ャ�佽鐩栨垨閲嶅浠讳綍涓氬姟鏁版嵁銆�
+
+### 10-07锛欳odex 杈撳叆鍗¢】鎺掓煡涓庝氦鎺ュ缓璁�
+
+- 褰撳墠绾跨▼浠� 10-05 杩炵画浣跨敤鑷充粖锛宺ollout 绾� `32.17 MB`锛屽惈 `3,751` 鏉� `response_item`銆乣3,469` 鏉� `event_msg`銆乣10` 娆� compaction锛涚敤鎴峰凡鍐冲畾鏂板紑瀵硅瘽缁х画銆�
+- 宸叉帓闄� `codex-plus-plus-manager --show-update`锛堟湭杩愯锛夛紱閲嶈瘯椋庢毚 guard 宸插畨瑁咃紝`dropped=859` 涓旇繛缁� 10 绉掍笉澧為暱锛屽綋鍓嶆病鏈夋寔缁噸璇曢鏆淬��
+- 瑙傛祴鍒� Codex 涓昏繘绋嬬疮璁$害 `4.34 h` CPU銆佹覆鏌撹繘绋嬬害 `1.28 h`锛涚煭鏃跺嘲鍊间富杩涚▼绾� `116%` 鍗曟牳銆佹覆鏌撶害 `44%`锛屽睘浜庨棿姝囬珮璐熻浇銆傚缓璁柊瀵硅瘽缁х画锛涜嫢浠嶅崱锛屽畬鍏ㄩ��鍑哄苟閲嶅惎 Codex銆�
+- **鏂板璇濇帴鎵�**锛氬厛璇绘牴鐩綍 `AGENTS.md` + 鏈枃浠躲�傚綋鍓� git `HEAD` 涓� `origin/main` 鍧囦负 `9b56330`锛屽凡璺熻釜鏂囦欢鏃犳敼鍔紱dev 鍓嶇 `8080`銆佸悗绔� `8090` 姝e湪杩愯銆�
+
+## 鍘嗗彶锛�2026-10-07 鏃╅棿鍏虫満蹇収锛�
+
+### 10-07 鍏虫満蹇収
+
+- **褰撳墠鐘舵��**锛氭湰杞粎鍋氬叧鏈轰氦鎺ワ紝鏃犱唬鐮併�佹祴璇曞彛寰勬垨涓氬姟鏁版嵁鍙樻洿銆備笂涓�浠诲姟鈥滄姌绾垮浘绾靛悜闂磋窛鍔犻珮 20%鈥濆凡浜� 10-06 楠屾敹锛屾彁浜や负 `54e10ae fix(holiday): 鍔犲ぇ鎶樼嚎鍥剧旱鍚戦棿璺濆苟鍚屾 Word 灏哄`銆�
+- **Git**锛歚main` 鐨� `HEAD` 涓� `origin/main` 鍧囦负 `54e10aee11aa4a10926df02782ca07d41eeb9b98`锛涙棤鏈彁浜ょ殑宸茶窡韪枃浠讹紝宸ヤ綔鍖轰粎鏈夋寜绾﹀畾涓嶅叆搴撶殑 `_tmp_*` 涓存椂鐩綍/鏂囦欢銆乣_tmp_pack_smoke_trend.docx` 鍜屼竴浠借娴嬬珯鍘熷 XLS銆�
+- **杩愯鐘舵��**锛氬叧鏈哄墠鏈満 dev 鍓嶅悗绔粛鍙闂紙鍓嶇 8080銆佸悗绔� 8090 鍧囪繑鍥� 200锛夛紱闅忚绠楁満鍏虫満浼氳嚜鍔ㄥ仠姝紝閲嶅惎鍚庢寜 `AGENTS.md` 鐢� `start-dev.ps1` 鍚姩銆�
+- **鏈湴鐣欏瓨**锛氭姌绾垮浘绾靛悜闂磋窛鐨勬祴璇曟姤鍛婂拰宸ヤ綔鏃ョ粨宸插啓鍏� `docs/鍔熻兘娴嬭瘯鎶ュ憡/`銆乣docs/宸ヤ綔鏃ョ粨/`锛屽潎鎸夌害瀹氫笉鍏ュ簱銆�
+- **涓嬫鎺ユ墜**锛氬厛璇绘牴鐩綍 `AGENTS.md` + 鏈枃浠讹紱浠撳簱绾у緟鍔炰粛瑙佺涓冭妭锛屾湰杞棤鏂板寰呭姙銆�
+- **鏁版嵁杈圭晫**锛氭湭瀵煎叆銆佽鐩栨垨閲嶅浠讳綍涓氬姟鏁版嵁銆�
 
 ## 鏈�鏂帮紙2026-10-06锛�
 
@@ -1045,3 +1145,13 @@
 
 
 - 2026-08-31锛堜笅鐝墠锛夛細鏁版嵁瀵煎叆椤甸噸鏋勯獙鏀堕�氳繃锛坈ommit 791e70f 宸叉帹閫侊紝璇﹁绗� 7 鑺傦級銆傛敼閫犲叏鐢� Python 鏇挎崲鍐欐枃浠讹紙apply_patch 鏈満蹇呰触锛夛紱鍏抽敭鍧戯細Vue 鏂版柟娉曡鏀� methods 澶� 鈫� render TypeError銆乪l-table 灞曞紑琛屼笉娓叉煋锛孾Vue warn] 鍙湪 console 涓嶅湪 pageerror锛涢獙璇佺敤 puppeteer 鎷︽埅 /api/import/batches 杩斿洖 mock 澶辫触鏁版嵁鐩存帴楠屾槑缁嗚〃锛岀湡瀹炴棤澶辫触鏁版嵁楠屻�屾殏鏃犲鍏ュけ璐ヨ褰曘�嶇┖鎬併��
+
+- 2026-10-09锛氭寜鏄ヨ妭鐗堛�屼汉鍛樻祦鍔ㄩ噺鎯呭喌銆嶈〃鍒朵綔 **2026 鍗佷竴鍋囨湡**鐗堬紝杈撳嚭 `D:\03_鍏徃\02_椤圭洰\27_婀栧寳鐪侀亾璺繍杈撲簨涓氬彂灞曚腑蹇僜10_涓氬姟\99_涓撻」缁熻\01_閲嶈鑺傚亣鏃2026\鍗佷竴\2026骞村崄涓�鍋囨湡浜哄憳娴佸姩閲忔儏鍐�.xlsx`锛堝師鏄ヨ妭妯℃澘澶囦唤鍦� `_tmp_flow_table\template_backup.xlsx`锛屾湭鍒犵敤鎴锋枃浠讹級銆傜粨鏋勶細鏍囬琛� + 涓夌骇鍚堝苟琛ㄥご + `鍚堣` 琛� + 10鏈�1鏃10鏈�7鏃ラ�愭棩琛岋紱7 涓寚鏍囷紙鍏ㄧぞ浼氳法鍖哄煙 / 閾佽矾 / 鍏矾浜哄憳娴佸姩閲忋�斾笅鍒嗘祦鍔ㄩ噺鍚堣銆侀珮閫熷叕璺強鏅�氬浗鐪侀亾闈炶惀涓氭�у皬瀹㈣溅浜哄憳鍑鸿閲忋�佸叕璺惀涓氭�у杩愰噺銆�/ 姘磋矾 / 姘戣埅锛夋瘡涓兘缁欍�屾暟閲� / 鍚屾瘮2025骞� / 鍚屾瘮2024骞淬�嶏紝鍗虫妸鍘熻〃鐨勩�屽悓姣�2019骞淬�嶆敼涓恒�屽悓姣�2024骞淬�嶅苟缁欐瘡涓寚鏍囪ˉ榻愮浜屼釜鍚屾瘮鍒楋紙A~V 鍏� 22 鍒楋級銆傛暟鎹彇鑷郴缁� `GET /api/holiday/overview?holidayType=NATIONAL_DAY`锛�2024/2025/2026锛夛紝骞堕�愰」瀵归綈 10鏈�7鏃ユ簮鏂囦欢 `2026骞村崄涓�鏁版嵁10鏈�7鏃�.xlsx` 鐨� `浜哄憳娴佸姩閲忔眹鎬籤 涓� 閾佽矾/姘磋矾/姘戣埅/閬撹矾 鍚勯〉锛屽叏閮ㄤ竴鑷达紙7 澶╁悎璁℃�婚噺 10115.56 涓囦汉娆°�佸悓姣�2025 -4.46%銆佸悓姣�2024 +4.25%锛夈�傚彛寰勬亽绛夊紡鏍¢獙閫氳繃锛氬叕璺汉鍛樻祦鍔ㄩ噺 = 闈炶惀涓氭�у皬瀹㈣溅 + 鍏矾钀ヤ笟鎬э紱鍏ㄧぞ浼� = 鍏矾 + 閾佽矾 + 姘磋矾 + 姘戣埅銆傜敓鎴愯剼鏈� `_tmp_flow_table/nodework/build_xlsx.mjs`锛坅rtifact-tool锛屼复鏃剁洰褰曚笉鍏ュ簱锛夈��
+
+- 2026-10-09锛堣ˉ锛夛細涓婅〃鎸� A4 鎵撳嵃鍋氶�傞厤鈥斺�斿垪瀹芥敹绐勶紙鏃ユ湡鍒� 7銆佹暟閲忓垪 7.5銆佸悓姣斿垪 6.8锛�+ 宸ヤ綔琛ㄦ墦鍗拌缃紙A4銆佹í鍚戙�佺缉鏀俱��1 椤靛脳1 椤甸珮銆嶃�佹按骞冲眳涓�侀〉杈硅窛宸﹀彸 0.35鈥�/涓婁笅 0.4鈥筹級銆傜敤 Excel 瀹為檯瀵煎嚭 PDF 瀹炴祴 **姝eソ 1 椤�**锛圓4 妯悜 841.92脳595.32 pt锛夛紝鏁板��/鐧惧垎姣旀棤 `####`銆佹棤鎴柇銆傛敞锛歛rtifact-tool 鏃犳墦鍗拌缃� API锛宲ageSetup/printOptions/pageMargins 鏄鍑哄悗娉ㄥ叆 XLSX 鐨� `xl/worksheets/sheet1.xml`锛堣剼鏈� `_tmp_flow_table/print_setup.py`锛屼复鏃朵笉鍏ュ簱锛夈��
+
+- 2026-10-09锛堢画锛夛細鎸夊弽棣堟敼鍑哄崄涓�浜哄憳娴佸姩閲忚〃绗� 2 鐗堬紝杈撳嚭 `D:\03_鍏徃\02_椤圭洰\27_婀栧寳鐪侀亾璺繍杈撲簨涓氬彂灞曚腑蹇僜10_涓氬姟\99_涓撻」缁熻\01_閲嶈鑺傚亣鏃2026\鍗佷竴\2026骞村崄涓�鍋囨湡浜哄憳娴佸姩閲忔儏鍐�2.xlsx`锛堝崟琛� A1:S13锛夈�傛敼鍔細鈶犺〃澶撮『搴忚皟鏁翠负 鍏ㄧぞ浼� / 鍏矾 / 閾佽矾 / 姘磋矾 / 姘戣埅锛堝叕璺彁鍓嶏級锛涒憽鍘绘帀銆屽叕璺汉鍛樻祦鍔ㄩ噺路娴佸姩閲忓悎璁°�嶅瓙鍒楋紝鍙暀 楂橀�熷叕璺強鏅�氬浗鐪侀亾闈炶惀涓氭�у皬瀹㈣溅浜哄憳鍑鸿閲� 涓� 鍏矾钀ヤ笟鎬у杩愰噺锛涒憿鏂板銆屾棩鍧囥�嶈锛堣搴忥細鍚堣銆佹棩鍧囥��10鏈�1鏃7鏃ワ級锛涒懀銆屽悎璁°�嶈鍚屾瘮2025 鏀逛负 2026骞�7澶╁悎璁� 梅 2025骞�8澶╁悎璁★紙2025骞村浗搴嗗亣鏈熶负10鏈�1-8鏃ュ叡8澶╋級锛屽悓姣�2024 涓嶅彉锛涙棩鍧囩殑鍚屾瘮鎸夊悇骞村ぉ鏁版姌绠楋紙2026銆�2024涓�7澶╋紝2025涓�8澶╋級銆傛暟鍊硷細鍚堣鍚屾瘮2025 鍏ㄧぞ浼� -14.37%銆侀潪钀ヤ笟鎬у皬瀹㈣溅 -14.82%銆佸叕璺惀涓氭�� -17.45%銆侀搧璺� -3.33%銆佹按璺� -23.76%銆佹皯鑸� -17.12%锛涙棩鍧囧悓姣�2025 鍏ㄧぞ浼� -2.13%銆傛爣棰樻部鐢ㄧ敤鎴风増鏈��2026骞粹�滃崄涓�鈥濆亣鏈熷叏绀句細璺ㄥ尯鍩熶汉鍛樻祦鍔ㄩ噺鎯呭喌銆嶃�備粛涓� A4 妯悜 1 椤碉紙瀵煎嚭 PDF 瀹炴祴锛夈�傛暟鎹簮鍚屽墠锛堢郴缁� overview + 10鏈�7鏃ユ簮鏂囦欢鏍稿锛�2025 绗�8鏃ュ彇绯荤粺 dayIndex=8锛夈��
+
+- 2026-10-12锛堜氦鎺ュ揩鐓э紝鐢ㄦ埛鍑嗗鏂板紑瀵硅瘽锛夛細鐢佃剳閲嶅惎鍚庡凡鐢� `start-dev.ps1` 鎷夎捣鍓嶅悗绔紝鑴氭湰杈撳嚭鍚庣 OK銆佸墠绔� OK锛涘墠绔闂� http://localhost:8080锛屽悗绔� 8090锛屾棩蹇楀垎鍒 `backend.log` / `traffic-audit-web/frontend.log`銆傚綋鍓� git 鍒嗘敮 `main`锛屾棤宸茶窡韪枃浠舵敼鍔紱宸ヤ綔鍖烘湁 10-09 鐢熸垚鐨勬湭鍏ュ簱涓存椂鐩綍/鏂囦欢锛坄_tmp_doc_compare/`銆乣_tmp_flow_table/`銆乣_tmp_holiday_verify/`銆乣_tmp_rail_label/`銆乣_tmp_pack_smoke_trend.docx`锛夊強鐢ㄦ埛鏁版嵁 `docs/涓撻」缁熻/閲嶈鑺傚亣鏃�/瑙傛祴绔欏垎鏃ヨ皟鏌ユ暟鎹� (2026骞�10鏈�1鏃�).xls`锛屼笉瑕佸垹闄ゆ垨璇彁浜ゃ�傛湰杞湭鏀逛唬鐮併�佹湭璺戞祴璇曘�佹湭鎻愪氦銆�
+
+- 2026-10-12锛堜笟鍔$枒鐐规牳鏌ワ紝鍙锛夛細浼佷笟涓婃姤鐨勩�屽帤鍧�-杩滃畨銆嶃�屽悗鍧埌杩滃畨銆嶄袱涓湴鍚嶅凡鍒濇牳銆傚彲纭銆屽悗鍧�嶅湪鍏村北鍘挎按鏈堝闀囧悗鍧竴甯︼紙鍚庡潽鏋楀満銆佸悗鍧7鐭块檮杩戯級锛屽悗鍧7鐭垮叕寮�璧勬枡杩樻秹鍙婃瀛愪埂鐭虫煴鏉戯紱銆岃繙瀹夈�嶆寚瀹滄槍甯傝繙瀹夊幙锛涖�屽帤鍧�嶅湪瀹滄槍/鍏村北鍏紑鍦板悕涓湭鏌ュ埌鏈夋晥鍦扮偣锛屽垽鏂ぇ姒傜巼鏄�屽悗鍧�嶇殑閿欏瓧鎴栫畝绉帮紙鍙︽湁銆屽叴灞卞帤鍧墿娴佹湁闄愬叕鍙搞�嶆敞鍐屽湪宄″彛闀囨硹婀樻邯鏉戯紝浣嗗叕鍙稿悕涓嶈兘绛夊悓鍦板悕锛夈�傛寜姘存湀瀵洪晣鍚庡潽鍒拌繙瀹夊幙鍩庣殑鍏紑閬撹矾娴嬬畻锛屽崟绋嬬害 106.7 km锛涗紒涓氬潎濉� 201 km锛屼笉鑳界洿鎺ヨ瀹氶敊璇紝闇�浼佷笟琛ュ厖鍏蜂綋瑁呰揣鐐广�佸嵏璐х偣銆佸崟绋�/寰�杩斿強缁曡璺嚎鍚庡啀鏍搞�傜郴缁� `h2032_enterprise_monthly` 鐨勫鏍歌鏄庝腑鏈暀涓嬭繖涓ゅ鍦扮偣鍘熸枃銆�
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..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
@@ -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;
@@ -103,6 +103,23 @@
 
     public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis,
                             int valueDecimals, int width, int height) {
+        return lineChart(yAxisLabel, series, percentAxis, valueDecimals, DAY_LABELS, width, height);
+    }
+
+    public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis,
+                            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);
@@ -132,7 +149,7 @@
         plot.setRangeGridlinesVisible(false);
         plot.setDomainTickBandPaint(null);
         plot.setRangeTickBandPaint(null);
-        SymbolAxis domainAxis = new SymbolAxis(null, DAY_LABELS.toArray(new String[0]));
+        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));
         domainAxis.setLowerMargin(0.04);
@@ -175,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,
@@ -455,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();
@@ -474,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 {
@@ -615,7 +640,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 +670,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 +683,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 +700,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 +869,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 +886,7 @@
             this.color = color;
         }
 
+        private int getSeriesIndex() { return seriesIndex; }
         private double getStartX() { return startX; }
         private double getEndX() { return endX; }
         private double getY() { return y; }
@@ -839,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;
@@ -881,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;
@@ -1000,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();
@@ -1010,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);
@@ -1053,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) {
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayTrendService.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayTrendService.java
index 9960464..08ef648 100644
--- a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayTrendService.java
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayTrendService.java
@@ -15,6 +15,7 @@
 import java.io.InputStream;
 import java.math.BigDecimal;
 import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -84,7 +85,13 @@
         blocks.add(new HolidayTrendWordService.ChartBlock(
                 "鈥�" + holidayLabel + "鈥濆亣鏈熼搧璺杩愰噺鎯呭喌",
                 chartService.lineChart("涓囦汉娆�",
-                        series(years, yearData, item -> item.rail), false, 1100, 445),
+                        // Pin the 2026 rail label for day 3 above its point so the
+                        // 2024 label (70.6) no longer covers it; other charts keep
+                        // automatic placement.
+                        series(years, yearData, item -> item.rail), false, 2,
+                        HolidayTrendChartService.DAY_LABELS, 1100, 445,
+                        Collections.singletonList(new HolidayTrendChartService.LineLabelOverride(
+                                0, 2, HolidayTrendChartService.LineLabelAnchor.ABOVE))),
                 lineImageSize(5264785L, 2129155L)));
         blocks.add(new HolidayTrendWordService.ChartBlock(
                 "鈥�" + holidayLabel + "鈥濆亣鏈熸按璺杩愰噺鎯呭喌",
@@ -138,12 +145,14 @@
         blocks.add(new HolidayTrendWordService.ChartBlock(
                 prefix + "鏅�氬浗鐪侀亾瑙傛祴绔欏钩鍧囨満鍔ㄨ溅閫熷害",
                 chartService.lineChart("鍏噷/灏忔椂",
-                        Collections.singletonList(series("骞冲潎閫熷害", observation.speed)), false, 1100, 590),
+                        Collections.singletonList(series("骞冲潎閫熷害", observation.speed)), false, 2,
+                        observation.labels, 1100, 590),
                 lineImageSize(5268595L, 2821305L)));
         blocks.add(new HolidayTrendWordService.ChartBlock(
                 prefix + "鏅�氬浗鐪侀亾瑙傛祴绔欏钩鍧囨嫢鎸ゅ害",
                 chartService.lineChart("鎷ユ尋搴�",
-                        Collections.singletonList(series("骞冲潎鎷ユ尋搴�", observation.congestion)), false, 1100, 590),
+                        Collections.singletonList(series("骞冲潎鎷ユ尋搴�", observation.congestion)), false, 2,
+                        observation.labels, 1100, 590),
                 lineImageSize(5268595L, 2821305L)));
 
         File template = resolveTemplateFile();
@@ -263,23 +272,27 @@
     private ObservationData observationData(int year, String holidayType) {
         List<HolidayCalendar> days = configService.findHolidayDays(year, holidayType);
         if (days.isEmpty()) {
-            return new ObservationData(nulls(), nulls());
+            return new ObservationData(nulls(), nulls(), HolidayTrendChartService.DAY_LABELS);
         }
         LocalDate start = days.get(0).getHolidayDate();
-        LocalDate end = start.plusDays(HolidayTrendChartService.DAY_LABELS.size() - 1L);
+        LocalDate end = days.get(days.size() - 1).getHolidayDate();
         List<ObservationDailyOperation> rows = observationStationFlowMapper.selectDailyOperations(start, end);
         Map<LocalDate, ObservationDailyOperation> byDate = new LinkedHashMap<>();
         for (ObservationDailyOperation row : rows) {
             byDate.put(row.getObsDate(), row);
         }
+        DateTimeFormatter labelFormat = DateTimeFormatter.ofPattern("M鏈坉鏃�");
         List<BigDecimal> speed = new ArrayList<>();
         List<BigDecimal> congestion = new ArrayList<>();
-        for (int i = 0; i < HolidayTrendChartService.DAY_LABELS.size(); i++) {
-            ObservationDailyOperation row = byDate.get(start.plusDays(i));
+        List<String> labels = new ArrayList<>();
+        for (HolidayCalendar day : days) {
+            LocalDate date = day.getHolidayDate();
+            ObservationDailyOperation row = byDate.get(date);
             speed.add(row == null ? null : row.getAverageSpeed());
             congestion.add(row == null ? null : row.getAverageCongestion());
+            labels.add(date.format(labelFormat));
         }
-        return new ObservationData(speed, congestion);
+        return new ObservationData(speed, congestion, labels);
     }
 
     private HolidayTrendChartService.ChartSeries series(String name, List<BigDecimal> values) {
@@ -441,10 +454,12 @@
     private static class ObservationData {
         private final List<BigDecimal> speed;
         private final List<BigDecimal> congestion;
+        private final List<String> labels;
 
-        private ObservationData(List<BigDecimal> speed, List<BigDecimal> congestion) {
+        private ObservationData(List<BigDecimal> speed, List<BigDecimal> congestion, List<String> labels) {
             this.speed = speed;
             this.congestion = congestion;
+            this.labels = labels;
         }
     }
 }
diff --git a/traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayTrendChartServiceTest.java b/traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayTrendChartServiceTest.java
index fff727d..90db24d 100644
--- a/traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayTrendChartServiceTest.java
+++ b/traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayTrendChartServiceTest.java
@@ -114,6 +114,88 @@
         assertFalse(java.util.Arrays.equals(twoDecimals, threeDecimals));
     }
 
+    @Test
+    void rendersLineChartWithConfiguredDayLabels() {
+        HolidayTrendChartService service = new HolidayTrendChartService();
+        HolidayTrendChartService.ChartSeries congestion = new HolidayTrendChartService.ChartSeries();
+        congestion.setName("\u5e73\u5747\u62e5\u6324\u5ea6");
+        congestion.setValues(java.util.Arrays.asList(
+                new BigDecimal("0.50"), new BigDecimal("0.48"), new BigDecimal("0.47"),
+                new BigDecimal("0.49"), new BigDecimal("0.49"), new BigDecimal("0.49"),
+                new BigDecimal("0.47")));
+        java.util.List<String> sevenDays = java.util.Arrays.asList(
+                "10\u67081\u65e5", "10\u67082\u65e5", "10\u67083\u65e5", "10\u67084\u65e5",
+                "10\u67085\u65e5", "10\u67086\u65e5", "10\u67087\u65e5");
+
+        byte[] custom = service.lineChart("\u62e5\u6324\u5ea6",
+                java.util.Collections.singletonList(congestion), false, 2, sevenDays, 1100, 590);
+        byte[] defaults = service.lineChart("\u62e5\u6324\u5ea6",
+                java.util.Collections.singletonList(congestion), false, 2, 1100, 590);
+
+        assertPng(custom);
+        assertPng(defaults);
+        assertFalse(java.util.Arrays.equals(custom, defaults),
+                "custom day labels should change the rendered axis");
+    }
+
+    @Test
+    void pinsRequestedLineLabelToFixedAnchor() throws Exception {
+        HolidayTrendChartService service = new HolidayTrendChartService();
+        HolidayTrendChartService.ChartSeries series = new HolidayTrendChartService.ChartSeries();
+        series.setName("2026\u5e74");
+        series.setValues(java.util.Collections.singletonList(new BigDecimal("72.4")));
+        java.util.List<HolidayTrendChartService.ChartSeries> single =
+                java.util.Collections.singletonList(series);
+
+        byte[] plain = service.lineChart("\u4e07\u4eba\u6b21", single, false, 2, 1100, 590);
+        byte[] emptyOverrides = service.lineChart("\u4e07\u4eba\u6b21", single, false, 2,
+                HolidayTrendChartService.DAY_LABELS, 1100, 590,
+                java.util.Collections.emptyList());
+        assertArrayEquals(plain, emptyOverrides,
+                "an empty override list must keep the automatic placement");
+
+        byte[] unmatched = service.lineChart("\u4e07\u4eba\u6b21", single, false, 2,
+                HolidayTrendChartService.DAY_LABELS, 1100, 590,
+                java.util.Collections.singletonList(new HolidayTrendChartService.LineLabelOverride(
+                        0, 7, HolidayTrendChartService.LineLabelAnchor.ABOVE)));
+        assertArrayEquals(plain, unmatched,
+                "an override for a label that does not exist must not change rendering");
+
+        byte[] above = service.lineChart("\u4e07\u4eba\u6b21", single, false, 2,
+                HolidayTrendChartService.DAY_LABELS, 1100, 590,
+                java.util.Collections.singletonList(new HolidayTrendChartService.LineLabelOverride(
+                        0, 0, HolidayTrendChartService.LineLabelAnchor.ABOVE)));
+        byte[] below = service.lineChart("\u4e07\u4eba\u6b21", single, false, 2,
+                HolidayTrendChartService.DAY_LABELS, 1100, 590,
+                java.util.Collections.singletonList(new HolidayTrendChartService.LineLabelOverride(
+                        0, 0, HolidayTrendChartService.LineLabelAnchor.BELOW)));
+
+        assertPng(above);
+        assertPng(below);
+        assertFalse(java.util.Arrays.equals(above, below),
+                "ABOVE and BELOW anchors must render differently");
+        assertTrue(meanSeriesColorY(above) < meanSeriesColorY(below),
+                "the ABOVE anchor must place the label higher than the BELOW anchor");
+    }
+
+    private double meanSeriesColorY(byte[] data) throws Exception {
+        BufferedImage image = ImageIO.read(new ByteArrayInputStream(data));
+        assertNotNull(image);
+        int target = 0xC0504D;
+        long count = 0;
+        long sum = 0;
+        for (int y = 0; y < image.getHeight() - 60; y++) {
+            for (int x = 0; x < image.getWidth(); x++) {
+                if ((image.getRGB(x, y) & 0xFFFFFF) == target) {
+                    count++;
+                    sum += y;
+                }
+            }
+        }
+        assertTrue(count > 0, "expected series-coloured pixels for the item label");
+        return (double) sum / count;
+    }
+
     private void assertNoLargeLightGrayBackground(byte[] data) throws Exception {
         BufferedImage image = ImageIO.read(new ByteArrayInputStream(data));
         int matches = 0;

--
Gitblit v1.9.1