diff --git a/src/main/java/com/biutag/supervision/SupervisionApplication.java b/src/main/java/com/biutag/supervision/SupervisionApplication.java index bb76792..15694ba 100644 --- a/src/main/java/com/biutag/supervision/SupervisionApplication.java +++ b/src/main/java/com/biutag/supervision/SupervisionApplication.java @@ -15,6 +15,7 @@ import org.springframework.scheduling.annotation.EnableScheduling; public class SupervisionApplication { public static void main(String[] args) { + System.setProperty("java.awt.headless", "true"); SpringApplication.run(SupervisionApplication.class, args); } diff --git a/src/main/java/com/biutag/supervision/pojo/dto/report/ReportViewModel.java b/src/main/java/com/biutag/supervision/pojo/dto/report/ReportViewModel.java index 6e83e69..c479913 100644 --- a/src/main/java/com/biutag/supervision/pojo/dto/report/ReportViewModel.java +++ b/src/main/java/com/biutag/supervision/pojo/dto/report/ReportViewModel.java @@ -41,7 +41,7 @@ public class ReportViewModel { @Schema(description = "单位查处情况") - private UnitInvestigationOverviewSection unitInvestigationSection; + private UnitInvestigationOverviewSection unitInvestigationOverviewSection; // @Schema(description = "单位查处情况--详情预留") diff --git a/src/main/java/com/biutag/supervision/pojo/dto/report/accountability/AccountabilityDepartmentSection.java b/src/main/java/com/biutag/supervision/pojo/dto/report/accountability/AccountabilityDepartmentSection.java index 81ff73d..5bcaaea 100644 --- a/src/main/java/com/biutag/supervision/pojo/dto/report/accountability/AccountabilityDepartmentSection.java +++ b/src/main/java/com/biutag/supervision/pojo/dto/report/accountability/AccountabilityDepartmentSection.java @@ -1,5 +1,6 @@ package com.biutag.supervision.pojo.dto.report.accountability; +import com.deepoove.poi.data.PictureRenderData; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; @@ -16,4 +17,8 @@ public class AccountabilityDepartmentSection { @Schema(description = "单位问责类型列表") private List typeItems; + + @Schema(description = "单位问责饼状图") + private PictureRenderData departPieChart; + } \ No newline at end of file diff --git a/src/main/java/com/biutag/supervision/pojo/dto/report/accountability/AccountabilityPersonalSection.java b/src/main/java/com/biutag/supervision/pojo/dto/report/accountability/AccountabilityPersonalSection.java index 41ba6f5..1aa4523 100644 --- a/src/main/java/com/biutag/supervision/pojo/dto/report/accountability/AccountabilityPersonalSection.java +++ b/src/main/java/com/biutag/supervision/pojo/dto/report/accountability/AccountabilityPersonalSection.java @@ -1,5 +1,6 @@ package com.biutag.supervision.pojo.dto.report.accountability; +import com.deepoove.poi.data.PictureRenderData; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; @@ -16,4 +17,7 @@ public class AccountabilityPersonalSection { @Schema(description = "个人问责类型列表") private List typeItems; + + @Schema(description = "个人问责饼状图") + private PictureRenderData personPieChart; } \ No newline at end of file diff --git a/src/main/java/com/biutag/supervision/pojo/dto/report/businessLine/BusinessLineOverviewSection.java b/src/main/java/com/biutag/supervision/pojo/dto/report/businessLine/BusinessLineOverviewSection.java index d060e53..a036e25 100644 --- a/src/main/java/com/biutag/supervision/pojo/dto/report/businessLine/BusinessLineOverviewSection.java +++ b/src/main/java/com/biutag/supervision/pojo/dto/report/businessLine/BusinessLineOverviewSection.java @@ -1,5 +1,6 @@ package com.biutag.supervision.pojo.dto.report.businessLine; +import com.deepoove.poi.data.PictureRenderData; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; @@ -22,4 +23,8 @@ public class BusinessLineOverviewSection { @Schema(description = "各业务条线情况-总览文本(已拼接)") private String businessLineOverviewText; + + @Schema(description = "业务总览饼状图") + private PictureRenderData problemPieChart; + } \ No newline at end of file diff --git a/src/main/java/com/biutag/supervision/pojo/dto/report/unitInvestigation/UnitInvestigationOverviewSection.java b/src/main/java/com/biutag/supervision/pojo/dto/report/unitInvestigation/UnitInvestigationOverviewSection.java index 830cf32..49a6a20 100644 --- a/src/main/java/com/biutag/supervision/pojo/dto/report/unitInvestigation/UnitInvestigationOverviewSection.java +++ b/src/main/java/com/biutag/supervision/pojo/dto/report/unitInvestigation/UnitInvestigationOverviewSection.java @@ -1,5 +1,6 @@ package com.biutag.supervision.pojo.dto.report.unitInvestigation; +import com.deepoove.poi.data.PictureRenderData; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; @@ -30,4 +31,8 @@ public class UnitInvestigationOverviewSection { private List topUnits; + + @Schema(description = "单位查处情况柱状图") + private PictureRenderData unitBarChart; + } diff --git a/src/main/java/com/biutag/supervision/service/NegativeQueryService.java b/src/main/java/com/biutag/supervision/service/NegativeQueryService.java index 57cbe4a..eb3561f 100644 --- a/src/main/java/com/biutag/supervision/service/NegativeQueryService.java +++ b/src/main/java/com/biutag/supervision/service/NegativeQueryService.java @@ -212,7 +212,7 @@ public class NegativeQueryService { NegativeQueryVo vo = new NegativeQueryVo(); BeanUtils.copyProperties(item, vo); if (Objects.nonNull(item.getFirstDistributeTime()) && !ProcessingStatusEnum.completed.name().equals(item.getProcessingStatus())) { - vo.setRemainingDuration(TimeUtil.getRemainingDuration(item.getFirstDistributeTime(), item.getMaxSignDuration(), item.getMaxHandleDuration(), item.getExtensionDays(), item.getFlowKey())); +// vo.setRemainingDuration(TimeUtil.getRemainingDuration(item.getFirstDistributeTime(), item.getMaxSignDuration(), item.getMaxHandleDuration(), item.getExtensionDays(), item.getFlowKey())); } return vo; }).toList(); diff --git a/src/main/java/com/biutag/supervision/service/report/ReportDataServiceImpl.java b/src/main/java/com/biutag/supervision/service/report/ReportDataServiceImpl.java index 9bc7edb..cf032a1 100644 --- a/src/main/java/com/biutag/supervision/service/report/ReportDataServiceImpl.java +++ b/src/main/java/com/biutag/supervision/service/report/ReportDataServiceImpl.java @@ -30,11 +30,16 @@ import com.biutag.supervision.repository.negativeBlame.NegativeBlameResourceServ import com.biutag.supervision.repository.supdepart.SupDepartResourceService; import com.biutag.supervision.service.NegativeQueryService; import com.biutag.supervision.service.SupDictProblemSourceService; +import com.biutag.supervision.util.ChartRenderUtil; import com.biutag.supervision.util.DateCompareRangeUtil; import com.biutag.supervision.util.ReportTrendUtil; import com.biutag.supervision.util.TimeUtil; +import com.deepoove.poi.data.PictureRenderData; +import com.deepoove.poi.data.PictureType; +import com.deepoove.poi.data.Pictures; import jakarta.annotation.Resource; import lombok.Data; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.math.BigDecimal; @@ -43,6 +48,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; @Service +@Slf4j public class ReportDataServiceImpl implements ReportDataService { @Resource @@ -70,10 +76,11 @@ public class ReportDataServiceImpl implements ReportDataService { vm.setOverviewSection(buildOverviewSection(request, periodStart, periodEnd)); vm.setBusinessLineOverviewSection(buildBusinessLineOverviewSection(request, periodStart, periodEnd)); vm.setBusinessLineDetailSections(buildBusinessLineDetailSections(request, periodStart, periodEnd)); - vm.setUnitInvestigationSection(buildUnitInvestigationSection(request, periodStart, periodEnd)); + vm.setUnitInvestigationOverviewSection(buildUnitInvestigationOverviewSection(request, periodStart, periodEnd)); vm.setAccountabilityOverviewSection(buildAccountabilityOverviewSection(request, periodStart, periodEnd)); vm.setAccountabilityUnitDetailSection(buildAccountabilityUnitDetailSection(request, periodStart, periodEnd)); vm.setAccountabilityPersonOverviewSection(buildAccountabilityPersonOverviewSection(request, periodStart, periodEnd)); +// System.out.println(1/0); return vm; } @@ -106,6 +113,18 @@ public class ReportDataServiceImpl implements ReportDataService { .sorted(Comparator.comparing(AccountabilityTypeItem::getCount).reversed()) .toList(); accountabilityPersonalSection.setTypeItems(typeItems); + + // 饼图 + Map pieData = new LinkedHashMap<>(); + for (AccountabilityTypeItem typeItem : typeItems) { + pieData.put(typeItem.getTypeName(), typeItem.getCount()); + } + byte[] pieBytes = ChartRenderUtil.piePng("单位问责情况", pieData, 5, 900, 520); + PictureRenderData picture = Pictures.ofBytes(pieBytes, PictureType.PNG) + .size(500, 300) + .create(); + accountabilityPersonalSection.setPersonPieChart(picture); + return accountabilityPersonalSection; } @@ -138,6 +157,17 @@ public class ReportDataServiceImpl implements ReportDataService { .sorted(Comparator.comparing(AccountabilityTypeItem::getCount).reversed()) .toList(); accountabilityDepartmentSection.setTypeItems(typeItems); + + // 饼图 + Map pieData = new LinkedHashMap<>(); + for (AccountabilityTypeItem typeItem : typeItems) { + pieData.put(typeItem.getTypeName(), typeItem.getCount()); + } + byte[] pieBytes = ChartRenderUtil.piePng("单位问责情况", pieData, 5, 900, 520); + PictureRenderData picture = Pictures.ofBytes(pieBytes, PictureType.PNG) + .size(500, 300) + .create(); + accountabilityDepartmentSection.setDepartPieChart(picture); return accountabilityDepartmentSection; } @@ -164,9 +194,9 @@ public class ReportDataServiceImpl implements ReportDataService { return accountabilityOverviewSection; } - private UnitInvestigationOverviewSection buildUnitInvestigationSection(NegativeQueryParam request, - String periodStart, - String periodEnd) { + private UnitInvestigationOverviewSection buildUnitInvestigationOverviewSection(NegativeQueryParam request, + String periodStart, + String periodEnd) { DateCompareRangeUtil.CompareDateRange compareDateRange = DateCompareRangeUtil.buildCompareDateRange(request.getCrtTime().get(0), request.getCrtTime().get(1)); // 总体数据 NegativeQueryParam ztnegativeQueryParam = request.newQueryParam(); @@ -179,7 +209,8 @@ public class ReportDataServiceImpl implements ReportDataService { for (DepartAndSubDepartDto value : departAndSubDepart.values()) { Set allDepartIds = value.getAllDepartIds(); List voList = ztNegativeList.stream().filter(one -> allDepartIds.contains(one.getInvolveDepartId())).toList(); - if (CollectionUtil.isNotEmpty(voList)){ + log.info(value.getParentName() + "的数量=====================" + voList.size()); + if (CollectionUtil.isNotEmpty(voList)) { UnitInvestigationItem unitInvestigationItem = new UnitInvestigationItem(); unitInvestigationItem.setUnitName(value.getParentName()); unitInvestigationItem.setIssuedProblemCount(voList.size()); @@ -193,10 +224,26 @@ public class ReportDataServiceImpl implements ReportDataService { } } topUnits.sort(Comparator.comparing(UnitInvestigationItem::getVerifiedProblemCount).reversed()); + topUnits = topUnits.stream().limit(3).toList(); UnitInvestigationOverviewSection section = new UnitInvestigationOverviewSection(); section.setPeriodStart(periodStart); section.setPeriodEnd(periodEnd); section.setTopUnits(topUnits); + // 柱状图 + Map> seriesData = new LinkedHashMap<>(); + Map issuedMap = new LinkedHashMap<>(); + Map verifiedMap = new LinkedHashMap<>(); + for (UnitInvestigationItem item : topUnits) { + issuedMap.put(item.getUnitName(), item.getIssuedProblemCount()); + verifiedMap.put(item.getUnitName(), item.getVerifiedProblemCount()); + } + seriesData.put("下发问题数", issuedMap); + seriesData.put("查实问题数", verifiedMap); + byte[] barBytes = ChartRenderUtil.groupedBarPng("单位查处情况", seriesData, "单位", "数量", 1000, 520); + PictureRenderData picture = Pictures.ofBytes(barBytes, PictureType.PNG) + .size(500, 300) + .create(); + section.setUnitBarChart(picture); return section; } @@ -372,6 +419,16 @@ public class ReportDataServiceImpl implements ReportDataService { businessLineOverviewSection.setPeriodEnd(periodEnd); businessLineOverviewSection.setBusinessLineTotal(ztNegativeList.size()); businessLineOverviewSection.setBusinessLineOverviewText(businessLineOverviewText.toString()); + + Map pieData = new LinkedHashMap<>(); + for (Map.Entry entry : sorted) { + pieData.put(entry.getKey(), entry.getValue()); + } + byte[] pieBytes = ChartRenderUtil.piePng("占比统计", pieData, 5, 900, 520); + PictureRenderData picture = Pictures.ofBytes(pieBytes, PictureType.PNG) + .size(500, 300) + .create(); + businessLineOverviewSection.setProblemPieChart(picture); return businessLineOverviewSection; } diff --git a/src/main/java/com/biutag/supervision/util/ChartRenderUtil.java b/src/main/java/com/biutag/supervision/util/ChartRenderUtil.java index a4aaded..50cdcc5 100644 --- a/src/main/java/com/biutag/supervision/util/ChartRenderUtil.java +++ b/src/main/java/com/biutag/supervision/util/ChartRenderUtil.java @@ -10,47 +10,123 @@ import org.jfree.chart.plot.PiePlot; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.renderer.category.BarRenderer; import org.jfree.chart.title.LegendTitle; +import org.jfree.chart.ui.HorizontalAlignment; +import org.jfree.chart.ui.RectangleEdge; import org.jfree.data.category.DefaultCategoryDataset; import org.jfree.data.general.DefaultPieDataset; -import java.awt.*; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.GraphicsEnvironment; import java.io.ByteArrayOutputStream; -import java.text.NumberFormat; +import java.text.DecimalFormat; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; +import java.util.Set; /** * 图表渲染工具(PNG) - * - 适配报告输出:白底、抗锯齿、中文字体、标签更清晰、柱状图显示数值 + *

+ * 适用场景: + * 1. JFreeChart 生成图表 + * 2. poi-tl 插入 Word 模板 + * 3. 报表类固定输出场景 */ public class ChartRenderUtil { + private ChartRenderUtil() { + } + + /** + * 饼图配色 + */ + private static final Color[] PIE_COLORS = new Color[]{ + new Color(91, 155, 213), + new Color(237, 125, 49), + new Color(165, 165, 165), + new Color(255, 192, 0), + new Color(68, 114, 196), + new Color(112, 173, 71), + new Color(38, 68, 120), + new Color(158, 72, 14), + new Color(99, 99, 99), + new Color(153, 115, 0) + }; + + /** + * 柱状图系列配色 + */ + private static final Color[] BAR_COLORS = new Color[]{ + new Color(91, 155, 213), + new Color(237, 125, 49), + new Color(112, 173, 71), + new Color(165, 165, 165), + new Color(255, 192, 0), + new Color(68, 114, 196), + new Color(158, 72, 14), + new Color(38, 68, 120) + }; + + private static final String DEFAULT_OTHER_NAME = "其余问题类型"; + private static final String DEFAULT_EMPTY_NAME = "暂无数据"; + // ========================= - // 对外 API:根据数据生成图 + // 对外 API:饼图(唯一入口) // ========================= - public static byte[] piePng(Map data, int w, int h) { + /** + * 生成饼图 PNG + * + * @param title 标题 + * @param data 数据,key=名称,value=数值 + * @param topN 前N项,<=0表示不截断,超出部分合并为“其他” + * @param w 宽 + * @param h 高 + */ + public static byte[] piePng(String title, Map data, int topN, int w, int h) { + Map safeData = normalizePieData(data); + Map chartData = mergeTopNAsOther(safeData, topN, DEFAULT_OTHER_NAME); + DefaultPieDataset dataset = new DefaultPieDataset<>(); - data.forEach(dataset::setValue); + chartData.forEach(dataset::setValue); JFreeChart chart = ChartFactory.createPieChart( - "占比统计", // 你不想要标题可以传 null + title, dataset, true, false, false ); - beautifyPie(chart); + beautifyPie(chart, chartData); return toPng(chart, w, h, "生成饼图失败"); } - public static byte[] barPng(Map data, String xLabel, String yLabel, int w, int h) { + // ========================= + // 对外 API:柱状图(单系列) + // ========================= + + /** + * 单系列柱状图 + * + * @param title 标题 + * @param data 数据,key=类目,value=数值 + * @param xLabel X轴标题 + * @param yLabel Y轴标题 + * @param w 宽 + * @param h 高 + */ + public static byte[] barPng(String title, Map data, String xLabel, String yLabel, int w, int h) { DefaultCategoryDataset dataset = new DefaultCategoryDataset(); - // series 用一个固定值即可(yLabel 也行) - data.forEach((k, v) -> dataset.addValue(v, yLabel, k)); + + Map safeData = normalizeBarData(data); + safeData.forEach((category, value) -> dataset.addValue(value, yLabel, category)); JFreeChart chart = ChartFactory.createBarChart( - "统计分析", // 你不想要标题可以传 null + title, xLabel, yLabel, dataset, @@ -60,75 +136,161 @@ public class ChartRenderUtil { false ); - beautifyBar(chart); + beautifyBar(chart, 1); return toPng(chart, w, h, "生成柱状图失败"); } // ========================= - // Demo:随便生成一个图(可留可删) + // 对外 API:柱状图(多系列) + // ========================= + + /** + * 多系列分组柱状图 + * + * @param title 标题 + * @param seriesData 多系列数据 + * 外层key = 系列名,例如“下发问题数”“查实问题数” + * 内层key = 类目名,例如“单位A”“单位B” + * 内层value = 数值 + * @param xLabel X轴标题 + * @param yLabel Y轴标题 + * @param w 宽 + * @param h 高 + */ + public static byte[] groupedBarPng(String title, + Map> seriesData, + String xLabel, + String yLabel, + int w, + int h) { + DefaultCategoryDataset dataset = new DefaultCategoryDataset(); + + Map> safeSeriesData = normalizeGroupedBarData(seriesData); + + for (Map.Entry> seriesEntry : safeSeriesData.entrySet()) { + String seriesName = seriesEntry.getKey(); + Map categoryData = seriesEntry.getValue(); + for (Map.Entry categoryEntry : categoryData.entrySet()) { + dataset.addValue(categoryEntry.getValue(), seriesName, categoryEntry.getKey()); + } + } + + JFreeChart chart = ChartFactory.createBarChart( + title, + xLabel, + yLabel, + dataset, + PlotOrientation.VERTICAL, + true, + false, + false + ); + + beautifyBar(chart, safeSeriesData.size()); + return toPng(chart, w, h, "生成分组柱状图失败"); + } + + // ========================= + // Demo // ========================= public static byte[] createPieChart() { - return piePng(Map.of("查实", 40, "基本属实", 30, "不属实", 30), 900, 520); + Map data = new LinkedHashMap<>(); + data.put("查实", 40); + data.put("基本属实", 30); + data.put("不属实", 30); + return piePng("查实情况分布", data, 0, 900, 520); } public static byte[] createBarChart() { - return barPng(Map.of("市局", 120, "分县市局", 80, "基层单位", 50), "单位", "数量", 900, 520); + Map> seriesData = new LinkedHashMap<>(); + + Map issued = new LinkedHashMap<>(); + issued.put("单位A", 120); + issued.put("单位B", 80); + issued.put("单位C", 50); + + Map verified = new LinkedHashMap<>(); + verified.put("单位A", 100); + verified.put("单位B", 60); + verified.put("单位C", 30); + + seriesData.put("下发问题数", issued); + seriesData.put("查实问题数", verified); + + return groupedBarPng("单位问题分布", seriesData, "单位", "数量", 1000, 520); } // ========================= // 美化:饼图 // ========================= - private static void beautifyPie(JFreeChart chart) { + private static void beautifyPie(JFreeChart chart, Map data) { chart.setAntiAlias(true); chart.setTextAntiAlias(true); chart.setBackgroundPaint(Color.WHITE); chart.setBorderVisible(false); - Font titleFont = pickFont(Font.BOLD, 16); + Font titleFont = pickFont(Font.BOLD, 18); Font legendFont = pickFont(Font.PLAIN, 12); - Font labelFont = pickFont(Font.PLAIN, 12); + Font labelFont = pickFont(Font.PLAIN, 11); if (chart.getTitle() != null) { chart.getTitle().setFont(titleFont); + chart.getTitle().setPaint(new Color(51, 51, 51)); + chart.getTitle().setHorizontalAlignment(HorizontalAlignment.CENTER); + chart.getTitle().setMargin(0, 0, 12, 0); } LegendTitle legend = chart.getLegend(); if (legend != null) { legend.setItemFont(legendFont); legend.setBackgroundPaint(Color.WHITE); + legend.setBorder(0, 0, 0, 0); + legend.setPosition(RectangleEdge.TOP); } - PiePlot plot = (PiePlot) chart.getPlot(); + PiePlot plot = (PiePlot) chart.getPlot(); plot.setBackgroundPaint(Color.WHITE); plot.setOutlineVisible(false); + plot.setShadowPaint(null); + plot.setInteriorGap(0.08); - // label 样式:白底半透明,边框淡一点,去阴影 plot.setLabelFont(labelFont); + plot.setLabelPaint(new Color(51, 51, 51)); plot.setLabelBackgroundPaint(new Color(255, 255, 255, 235)); plot.setLabelOutlinePaint(new Color(220, 220, 220)); plot.setLabelShadowPaint(null); + plot.setLabelLinkPaint(new Color(160, 160, 160)); + plot.setLabelLinkStroke(new BasicStroke(1.0f)); + plot.setSimpleLabels(false); - // 标签内容:名称 + 百分比(例:查实 40.0%) plot.setLabelGenerator(new StandardPieSectionLabelGenerator( - "{0} {2}", - NumberFormat.getNumberInstance(), - NumberFormat.getPercentInstance() + "{0} {2}({1})", + new DecimalFormat("0"), + new DecimalFormat("0.0%") )); - // 让饼图别贴边 - plot.setInteriorGap(0.03); + plot.setSectionOutlinesVisible(true); + plot.setForegroundAlpha(0.95f); + + // 分类太多时隐藏扇区标签,只保留图例 + if (data.size() > 6) { + plot.setLabelGenerator(null); + } - // 分离效果(可选:更“立体”,如果你觉得花哨可以注释) - // plot.setExplodePercent("查实", 0.04); + int index = 0; + for (String key : data.keySet()) { + plot.setSectionPaint(key, PIE_COLORS[index % PIE_COLORS.length]); + index++; + } } // ========================= // 美化:柱状图 // ========================= - private static void beautifyBar(JFreeChart chart) { + private static void beautifyBar(JFreeChart chart, int seriesCount) { chart.setAntiAlias(true); chart.setTextAntiAlias(true); chart.setBackgroundPaint(Color.WHITE); @@ -137,45 +299,221 @@ public class ChartRenderUtil { Font titleFont = pickFont(Font.BOLD, 16); Font legendFont = pickFont(Font.PLAIN, 12); Font axisFont = pickFont(Font.PLAIN, 12); - Font valueFont = pickFont(Font.PLAIN, 12); + Font valueFont = pickFont(Font.PLAIN, 11); if (chart.getTitle() != null) { chart.getTitle().setFont(titleFont); + chart.getTitle().setPaint(new Color(51, 51, 51)); + chart.getTitle().setHorizontalAlignment(HorizontalAlignment.CENTER); } LegendTitle legend = chart.getLegend(); if (legend != null) { legend.setItemFont(legendFont); legend.setBackgroundPaint(Color.WHITE); + legend.setBorder(0, 0, 0, 0); + legend.setPosition(RectangleEdge.TOP); } CategoryPlot plot = chart.getCategoryPlot(); plot.setBackgroundPaint(Color.WHITE); plot.setOutlineVisible(false); - - // 网格线淡一点(更干净) plot.setRangeGridlinePaint(new Color(230, 230, 230)); plot.setRangeGridlinesVisible(true); - // 轴字体 plot.getDomainAxis().setTickLabelFont(axisFont); plot.getDomainAxis().setLabelFont(axisFont); plot.getRangeAxis().setTickLabelFont(axisFont); plot.getRangeAxis().setLabelFont(axisFont); - // 柱体渲染:更清爽 BarRenderer renderer = (BarRenderer) plot.getRenderer(); renderer.setDrawBarOutline(false); renderer.setShadowVisible(false); - renderer.setMaximumBarWidth(0.12); renderer.setItemMargin(0.10); - // 在柱子上方显示数值 + // 多系列时适当放宽柱宽 + if (seriesCount <= 1) { + renderer.setMaximumBarWidth(0.12); + } else if (seriesCount == 2) { + renderer.setMaximumBarWidth(0.18); + } else { + renderer.setMaximumBarWidth(0.25); + } + + for (int i = 0; i < seriesCount; i++) { + renderer.setSeriesPaint(i, BAR_COLORS[i % BAR_COLORS.length]); + } + renderer.setDefaultItemLabelGenerator(new StandardCategoryItemLabelGenerator()); renderer.setDefaultItemLabelsVisible(true); renderer.setDefaultItemLabelFont(valueFont); } + // ========================= + // 数据预处理:饼图 + // ========================= + + /** + * 过滤无效数据,并按数值倒序 + */ + private static Map normalizePieData(Map data) { + Map filtered = new LinkedHashMap<>(); + if (data != null) { + data.forEach((k, v) -> { + if (k != null && !k.isBlank() && v != null && v.doubleValue() > 0) { + filtered.put(k.trim(), v); + } + }); + } + + if (filtered.isEmpty()) { + filtered.put(DEFAULT_EMPTY_NAME, 1); + return filtered; + } + + LinkedHashMap sorted = new LinkedHashMap<>(); + filtered.entrySet().stream() + .sorted((a, b) -> Double.compare(b.getValue().doubleValue(), a.getValue().doubleValue())) + .forEachOrdered(e -> sorted.put(e.getKey(), e.getValue())); + + return sorted; + } + + /** + * 仅保留前 topN 项,其余合并为“其他” + */ + private static Map mergeTopNAsOther(Map data, int topN, String otherName) { + if (data == null || data.isEmpty()) { + Map result = new LinkedHashMap<>(); + result.put(DEFAULT_EMPTY_NAME, 1); + return result; + } + + if (topN <= 0 || data.size() <= topN) { + return data; + } + + String finalOtherName = (otherName == null || otherName.isBlank()) + ? DEFAULT_OTHER_NAME + : otherName.trim(); + + List> sorted = data.entrySet().stream().toList(); + + Map result = new LinkedHashMap<>(); + double otherTotal = 0D; + + for (int i = 0; i < sorted.size(); i++) { + Map.Entry entry = sorted.get(i); + if (i < topN) { + result.put(entry.getKey(), entry.getValue()); + } else { + otherTotal += entry.getValue().doubleValue(); + } + } + + if (otherTotal > 0D) { + if (isIntegerLike(otherTotal)) { + result.put(finalOtherName, (long) otherTotal); + } else { + result.put(finalOtherName, otherTotal); + } + } + + return result; + } + + // ========================= + // 数据预处理:单系列柱状图 + // ========================= + + private static Map normalizeBarData(Map data) { + Map result = new LinkedHashMap<>(); + if (data != null) { + data.forEach((k, v) -> { + if (k != null && !k.isBlank() && v != null && v.doubleValue() >= 0) { + result.put(k.trim(), v); + } + }); + } + + if (result.isEmpty()) { + result.put(DEFAULT_EMPTY_NAME, 0); + } + + return result; + } + + // ========================= + // 数据预处理:多系列柱状图 + // ========================= + + private static Map> normalizeGroupedBarData(Map> seriesData) { + Map> result = new LinkedHashMap<>(); + + if (seriesData == null || seriesData.isEmpty()) { + Map emptySeries = new LinkedHashMap<>(); + emptySeries.put(DEFAULT_EMPTY_NAME, 0); + result.put("数量", emptySeries); + return result; + } + + // 收集所有类目,保证多系列类目对齐 + Set allCategories = new LinkedHashSet<>(); + for (Map.Entry> seriesEntry : seriesData.entrySet()) { + Map item = seriesEntry.getValue(); + if (item == null) { + continue; + } + item.forEach((k, v) -> { + if (k != null && !k.isBlank()) { + allCategories.add(k.trim()); + } + }); + } + + if (allCategories.isEmpty()) { + Map emptySeries = new LinkedHashMap<>(); + emptySeries.put(DEFAULT_EMPTY_NAME, 0); + result.put("数量", emptySeries); + return result; + } + + for (Map.Entry> seriesEntry : seriesData.entrySet()) { + String seriesName = seriesEntry.getKey(); + if (seriesName == null || seriesName.isBlank()) { + continue; + } + + Map rawCategoryData = seriesEntry.getValue(); + Map normalizedCategoryData = new LinkedHashMap<>(); + + for (String category : allCategories) { + Number value = 0; + if (rawCategoryData != null && rawCategoryData.containsKey(category)) { + Number raw = rawCategoryData.get(category); + if (raw != null && raw.doubleValue() >= 0) { + value = raw; + } + } + normalizedCategoryData.put(category, value); + } + + result.put(seriesName.trim(), normalizedCategoryData); + } + + if (result.isEmpty()) { + Map emptySeries = new LinkedHashMap<>(); + emptySeries.put(DEFAULT_EMPTY_NAME, 0); + result.put("数量", emptySeries); + } + + return result; + } + + private static boolean isIntegerLike(double value) { + return Math.abs(value - Math.rint(value)) < 0.0000001D; + } + // ========================= // 输出 PNG // ========================= @@ -194,25 +532,30 @@ public class ChartRenderUtil { // ========================= private static Font pickFont(int style, int size) { - // 按优先级尝试(Windows 常见:微软雅黑;Linux 常见:Noto/文泉驿/DejaVu) - String[] candidates = new String[]{ - "Microsoft YaHei", "微软雅黑", - "PingFang SC", "苹方-简", - "Noto Sans CJK SC", "Noto Sans SC", - "WenQuanYi Micro Hei", "文泉驿微米黑", - "SimSun", "宋体", - "Arial Unicode MS", - "SansSerif" - }; - - GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); - String[] available = ge.getAvailableFontFamilyNames(); - for (String c : candidates) { - for (String a : available) { - if (a.equalsIgnoreCase(c)) { - return new Font(a, style, size); + try { + // 按优先级尝试(Windows 常见:微软雅黑;Linux 常见:Noto/文泉驿/DejaVu) + String[] candidates = new String[]{ + "Microsoft YaHei", "微软雅黑", + "PingFang SC", "苹方-简", + "Noto Sans CJK SC", "Noto Sans SC", + "WenQuanYi Micro Hei", "文泉驿微米黑", + "SimSun", "宋体", + "Arial Unicode MS", + "SansSerif" + }; + + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + String[] available = ge.getAvailableFontFamilyNames(); + for (String c : candidates) { + for (String a : available) { + if (a.equalsIgnoreCase(c)) { + return new Font(a, style, size); + } } } + } catch (Throwable e) { + // 字体系统初始化失败时兜底 + System.err.println("Font detection failed, fallback to SansSerif: " + e.getMessage()); } return new Font("SansSerif", style, size); } diff --git a/src/main/resources/static/templates/督审一体化平台研判分析报告.docx b/src/main/resources/static/templates/督审一体化平台研判分析报告.docx index db67adc..0badf43 100644 Binary files a/src/main/resources/static/templates/督审一体化平台研判分析报告.docx and b/src/main/resources/static/templates/督审一体化平台研判分析报告.docx differ