fix(holiday): 提高平均拥挤度走势图标签精度
| | |
| | | |
| | | ## 最新(2026-10-06) |
| | | |
| | | ### 10-06:观测站平均拥挤度走势图改为 3 位小数 |
| | | |
| | | - **问题**:用户导入 10-05 观测站数据后反馈,最后一张拥挤度图中 10-04、10-05 标签都显示 `0.49`,但点位高度不同。 |
| | | - **根因**:平均拥挤度原值分别为 `0.490890014472` 和 `0.492414492754`,折线按未取整原值绘制,标签原为最多 2 位小数,导致标签精度与图形精度不一致,不是日汇总或点位计算错误。 |
| | | - **修正**:`HolidayTrendChartService.lineChart` 新增可选 `valueDecimals`,默认仍为 2;仅“普通国省道观测站平均拥挤度”图传 3 位。10-04、10-05 现在分别显示 `0.491`、`0.492`,其余图口径不变。 |
| | | - **验证**:后端全量 `mvn -q test -f traffic-audit-server\pom.xml` 通过;重启后只读生成新 DOCX,OCR 标签为 `0.504/0.476/0.467/0.491/0.492`;与用户提供的文档逐图比对,15 张图中仅拥挤度图 `image15` 变化,其他 14 张 SHA-256 完全一致。测试报告:`docs/功能测试报告/功能测试报告_拥挤度走势图精度_2026-10-06.md`(本地留存,不入库)。 |
| | | - **数据边界**:未导入、覆盖或重导任何业务数据。 |
| | | |
| | | ### 10-06:往年国省道调整列表补充调整前值 |
| | | |
| | | - **改动**:调整列表接口在同一年份、同一日期、普通国省道口径下,从 `holiday_vehicle_flow` 补返回 `originalValue`;没有原始记录时保持空,页面不显示占位符。表格新增“调整前国省道车流量(万辆次)”,原“调整后国省道车流”改为“调整后国省道车流量”。 |
| | |
| | | |
| | | public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis, |
| | | int width, int 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) { |
| | | XYSeriesCollection dataset = new XYSeriesCollection(); |
| | | for (int seriesIndex = 0; seriesIndex < series.size(); seriesIndex++) { |
| | | ChartSeries item = series.get(seriesIndex); |
| | |
| | | } |
| | | NumberFormat valueFormat = percentAxis |
| | | ? NumberFormat.getPercentInstance() : new DecimalFormat("#,##0.##"); |
| | | valueFormat.setMaximumFractionDigits(percentAxis ? 0 : 2); |
| | | valueFormat.setMaximumFractionDigits(percentAxis ? 0 : valueDecimals); |
| | | rangeAxis.setNumberFormatOverride(valueFormat); |
| | | plot.setRangeAxis(rangeAxis); |
| | | plot.setAxisOffset(new RectangleInsets(2.0, 2.0, 2.0, 2.0)); |
| | |
| | | BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); |
| | | } |
| | | plot.setRenderer(renderer); |
| | | return render(chart, width, height, yAxisLabel, true, percentAxis); |
| | | return render(chart, width, height, yAxisLabel, true, percentAxis, valueDecimals); |
| | | } |
| | | |
| | | public byte[] pieChart(String valueSuffix, List<ChartCategory> categories, |
| | |
| | | for (ChartCategory category : categories) { |
| | | plot.setSectionPaint(category.getName(), seriesColor(index++)); |
| | | } |
| | | return render(chart, width, height, valueSuffix, false, false); |
| | | return render(chart, width, height, valueSuffix, false, false, 2); |
| | | } |
| | | |
| | | public byte[] stackedBarChart(String yAxisLabel, List<String> categories, |
| | |
| | | renderer.setSeriesPaint(i, seriesColor(i)); |
| | | } |
| | | plot.setRenderer(renderer); |
| | | return render(chart, width, height, yAxisLabel, true, percentAxis); |
| | | return render(chart, width, height, yAxisLabel, true, percentAxis, 2); |
| | | } |
| | | |
| | | private static class WaterRightPiePlot<K extends Comparable<K>> extends PiePlot<K> { |
| | |
| | | } |
| | | |
| | | private byte[] render(JFreeChart chart, int width, int height, String unitLabel, |
| | | boolean axisChart, boolean percentItemLabels) { |
| | | boolean axisChart, boolean percentItemLabels, int valueDecimals) { |
| | | try { |
| | | reserveUnitLabelBand(chart, axisChart, unitLabel); |
| | | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| | |
| | | drawDateLabel(graphics, info.getPlotInfo().getDataArea()); |
| | | } |
| | | drawXYItemLabels(graphics, image, chart, |
| | | info.getPlotInfo().getDataArea(), percentItemLabels, unitLabel); |
| | | info.getPlotInfo().getDataArea(), percentItemLabels, unitLabel, valueDecimals); |
| | | drawStackedBarLabels(graphics, image, chart, |
| | | info.getPlotInfo().getDataArea(), percentItemLabels); |
| | | } finally { |
| | |
| | | |
| | | private void drawXYItemLabels(Graphics2D graphics, BufferedImage image, JFreeChart chart, |
| | | Rectangle2D dataArea, boolean percentItemLabels, |
| | | String unitLabel) { |
| | | String unitLabel, int valueDecimals) { |
| | | if (!(chart.getPlot() instanceof XYPlot) || dataArea == null |
| | | || dataArea.getWidth() <= 0 || dataArea.getHeight() <= 0) { |
| | | return; |
| | |
| | | Point2D point = itemPoint(plot, dataset, seriesIndex, itemIndex, dataArea); |
| | | if (yValue == null || point == null) continue; |
| | | labels.add(new DayLabel(seriesIndex, point, |
| | | formatLineLabel(yValue, percentItemLabels))); |
| | | formatLineLabel(yValue, percentItemLabels, valueDecimals))); |
| | | } |
| | | // 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. |
| | |
| | | return total; |
| | | } |
| | | |
| | | private String formatLineLabel(Number value, boolean percentItemLabels) { |
| | | private String formatLineLabel(Number value, boolean percentItemLabels, int valueDecimals) { |
| | | if (percentItemLabels) { |
| | | NumberFormat formatter = NumberFormat.getPercentInstance(); |
| | | formatter.setMaximumFractionDigits(1); |
| | | return formatter.format(value.doubleValue()); |
| | | } |
| | | return new DecimalFormat("#,##0.##").format(value.doubleValue()); |
| | | DecimalFormat formatter = new DecimalFormat("#,##0"); |
| | | formatter.setMaximumFractionDigits(valueDecimals); |
| | | return formatter.format(value.doubleValue()); |
| | | } |
| | | |
| | | private void drawLabelWithHalo(Graphics2D graphics, String text, Rectangle2D rectangle, |
| | |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | prefix + "普通国省道观测站平均拥挤度", |
| | | chartService.lineChart("拥挤度", |
| | | Collections.singletonList(series("平均拥挤度", observation.congestion)), false, 1100, 590), |
| | | Collections.singletonList(series("平均拥挤度", observation.congestion)), false, 3, 1100, 590), |
| | | new HolidayTrendWordService.ImageSize(5268595L, 2821305L))); |
| | | |
| | | File template = resolveTemplateFile(); |
| | |
| | | import java.awt.image.BufferedImage; |
| | | import java.io.ByteArrayInputStream; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertFalse; |
| | | import static org.junit.jupiter.api.Assertions.assertNotNull; |
| | | import static org.junit.jupiter.api.Assertions.assertTrue; |
| | | |
| | |
| | | assertNoLargeLightGrayBackground(stacked); |
| | | } |
| | | |
| | | @Test |
| | | void rendersHigherPrecisionLineLabelsWhenRequested() { |
| | | HolidayTrendChartService service = new HolidayTrendChartService(); |
| | | HolidayTrendChartService.ChartSeries congestion = new HolidayTrendChartService.ChartSeries(); |
| | | congestion.setName("平均拥挤度"); |
| | | congestion.setValues(java.util.Arrays.asList( |
| | | new java.math.BigDecimal("0.467029"), |
| | | new java.math.BigDecimal("0.490890"), |
| | | new java.math.BigDecimal("0.492414"))); |
| | | |
| | | byte[] twoDecimals = service.lineChart("拥挤度", |
| | | java.util.Collections.singletonList(congestion), false, 2, 1100, 590); |
| | | byte[] threeDecimals = service.lineChart("拥挤度", |
| | | java.util.Collections.singletonList(congestion), false, 3, 1100, 590); |
| | | |
| | | assertPng(twoDecimals); |
| | | assertPng(threeDecimals); |
| | | assertFalse(java.util.Arrays.equals(twoDecimals, threeDecimals)); |
| | | } |
| | | |
| | | private void assertNoLargeLightGrayBackground(byte[] data) throws Exception { |
| | | BufferedImage image = ImageIO.read(new ByteArrayInputStream(data)); |
| | | int matches = 0; |