liusheng
2 天以前 d9ac4335686225a20920d7feb7ce46a5c2df1f60
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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 的构建器
 * <p>
 * 按任务名称分组:每个任务写一块数据(行=题目,列=选项,值=数量),并在数据右侧画一张柱状图。
 *
 * @author smartor
 */
public class MydOptionRatioChartBuilder implements ExcelUtil.ChartSheetBuilder {
 
    /**
     * 图表 Sheet 名称
     */
    public static final String SHEET_NAME = "问题详情占比图表";
 
    private final List<ServiceSubtaskDetailRatioExport> rows;
 
    public MydOptionRatioChartBuilder(List<ServiceSubtaskDetailRatioExport> 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<String, List<ServiceSubtaskDetailRatioExport>> entry : groupByTask().entrySet()) {
            // 每写完一块数据立刻建图:SXSSF 流式工作簿里刚写的行还在内存中,图表才能读到单元格
            startRow = writeTaskBlockWithChart(sheet, entry.getKey(), entry.getValue(), startRow) + 2;
        }
    }
 
    /**
     * 按任务名称分组(保持列表原有顺序,即 SQL 的 task_name 排序)
     */
    private Map<String, List<ServiceSubtaskDetailRatioExport>> groupByTask() {
        Map<String, List<ServiceSubtaskDetailRatioExport>> 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<ServiceSubtaskDetailRatioExport> taskRows, int startRow) {
        // 题目 -> (选项 -> 选该选项的人数),选项按出现顺序收集成本任务的系列
        Map<String, Map<String, Double>> questionOptions = new LinkedHashMap<>();
        LinkedHashSet<String> 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<String> optionList = new ArrayList<>(optionSet);
        List<String> 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<String, Double> 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<String> 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<Double> 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;
        }
    }
}