package com.smartor.common; import com.ruoyi.common.utils.poi.ExcelUtil; import com.smartor.domain.ServiceSubtaskDetailRatioExport; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.xddf.usermodel.chart.AxisCrosses; import org.apache.poi.xddf.usermodel.chart.AxisPosition; import org.apache.poi.xddf.usermodel.chart.BarDirection; import org.apache.poi.xddf.usermodel.chart.ChartTypes; import org.apache.poi.xddf.usermodel.chart.LegendPosition; import org.apache.poi.xddf.usermodel.chart.XDDFBarChartData; import org.apache.poi.xddf.usermodel.chart.XDDFCategoryAxis; import org.apache.poi.xddf.usermodel.chart.XDDFChartData; import org.apache.poi.xddf.usermodel.chart.XDDFChartLegend; import org.apache.poi.xddf.usermodel.chart.XDDFDataSource; import org.apache.poi.xddf.usermodel.chart.XDDFDataSourcesFactory; import org.apache.poi.xddf.usermodel.chart.XDDFNumericalDataSource; import org.apache.poi.xddf.usermodel.chart.XDDFValueAxis; import org.apache.poi.xssf.usermodel.XSSFChart; import org.apache.poi.xssf.usermodel.XSSFClientAnchor; import org.apache.poi.xssf.usermodel.XSSFDrawing; import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; /** * 导出「问题详情占比图表」Sheet 的构建器 *

* 按任务名称分组:每个任务写一块数据(行=题目,列=选项,值=数量),并在数据右侧画一张柱状图。 * * @author smartor */ public class MydOptionRatioChartBuilder implements ExcelUtil.ChartSheetBuilder { /** * 图表 Sheet 名称 */ public static final String SHEET_NAME = "问题详情占比图表"; private final List rows; public MydOptionRatioChartBuilder(List rows) { this.rows = rows == null ? new ArrayList<>() : rows; } @Override public void build(XSSFWorkbook workbook) { if (rows.isEmpty()) { return; } XSSFSheet sheet = workbook.createSheet(SHEET_NAME); int startRow = 0; for (Map.Entry> entry : groupByTask().entrySet()) { // 每写完一块数据立刻建图:SXSSF 流式工作簿里刚写的行还在内存中,图表才能读到单元格 startRow = writeTaskBlockWithChart(sheet, entry.getKey(), entry.getValue(), startRow) + 2; } } /** * 按任务名称分组(保持列表原有顺序,即 SQL 的 task_name 排序) */ private Map> groupByTask() { Map> grouped = new LinkedHashMap<>(); for (ServiceSubtaskDetailRatioExport row : rows) { String taskName = row.getTaskname() == null ? "" : row.getTaskname(); grouped.computeIfAbsent(taskName, k -> new ArrayList<>()).add(row); } return grouped; } /** * 写一个任务的数据块并画柱状图,返回下一个块可用的起始行 */ private int writeTaskBlockWithChart(XSSFSheet sheet, String taskName, List taskRows, int startRow) { // 题目 -> (选项 -> 选该选项的人数),选项按出现顺序收集成本任务的系列 Map> questionOptions = new LinkedHashMap<>(); LinkedHashSet optionSet = new LinkedHashSet<>(); String taskid = ""; for (ServiceSubtaskDetailRatioExport row : taskRows) { String question = row.getQuestiontext() == null ? "" : row.getQuestiontext(); String option = row.getOptionresult() == null ? "" : row.getOptionresult(); questionOptions.computeIfAbsent(question, k -> new LinkedHashMap<>()).put(option, parseCount(row.getCount())); optionSet.add(option); if (taskid.isEmpty() && row.getTaskid() != null) { taskid = row.getTaskid(); } } List optionList = new ArrayList<>(optionSet); List questionList = new ArrayList<>(questionOptions.keySet()); int rowIdx = startRow; // 标题行 Row titleRow = sheet.createRow(rowIdx++); titleRow.createCell(0).setCellValue("任务名称:" + taskName + "(任务编号 " + taskid + ")"); // 表头行:题目 + 各选项(图表系列的标题就是这一行) Row headerRow = sheet.createRow(rowIdx++); headerRow.createCell(0).setCellValue("题目"); for (int i = 0; i < optionList.size(); i++) { headerRow.createCell(i + 1).setCellValue(optionList.get(i)); } // 数据行:值 = 选该选项的人数 int firstDataRow = rowIdx; for (String question : questionList) { Row dataRow = sheet.createRow(rowIdx++); dataRow.createCell(0).setCellValue(question); Map optionMap = questionOptions.get(question); for (int i = 0; i < optionList.size(); i++) { Double count = optionMap.get(optionList.get(i)); dataRow.createCell(i + 1).setCellValue(count == null ? 0D : count); } } int lastDataRow = rowIdx - 1; // 柱状图:分类=题目,系列=选项,数值=选该选项的人数 int chartCol = optionList.size() + 3; int chartHeight = Math.max(16, questionList.size() + 4); XSSFDrawing drawing = sheet.createDrawingPatriarch(); XSSFClientAnchor anchor = drawing.createAnchor(0, 0, 0, 0, chartCol, firstDataRow - 1, chartCol + 10, firstDataRow - 1 + chartHeight); XSSFChart chart = drawing.createChart(anchor); chart.setTitleText(taskName); chart.setTitleOverlay(false); XDDFChartLegend legend = chart.getOrAddLegend(); legend.setPosition(LegendPosition.BOTTOM); XDDFCategoryAxis bottomAxis = chart.createCategoryAxis(AxisPosition.BOTTOM); bottomAxis.setTitle("题目"); XDDFValueAxis leftAxis = chart.createValueAxis(AxisPosition.LEFT); leftAxis.setTitle("数量"); leftAxis.setCrosses(AxisCrosses.AUTO_ZERO); XDDFDataSource categories = XDDFDataSourcesFactory.fromStringCellRange(sheet, new CellRangeAddress(firstDataRow, lastDataRow, 0, 0)); XDDFBarChartData barChart = (XDDFBarChartData) chart.createData(ChartTypes.BAR, bottomAxis, leftAxis); for (int i = 0; i < optionList.size(); i++) { XDDFNumericalDataSource values = XDDFDataSourcesFactory.fromNumericCellRange(sheet, new CellRangeAddress(firstDataRow, lastDataRow, i + 1, i + 1)); XDDFChartData.Series series = barChart.addSeries(categories, values); series.setTitle(optionList.get(i), null); } barChart.setBarDirection(BarDirection.COL); // 关键:不调用 plot 图表不会显示 chart.plot(barChart); return rowIdx; } /** * 数量字符串 -> 数字,异常值按 0 处理 */ private double parseCount(String count) { if (count == null || count.isEmpty()) { return 0D; } try { return Double.parseDouble(count.trim()); } catch (NumberFormatException e) { return 0D; } } }