feat: 增加了word文档导出工具方法
This commit is contained in:
parent
750445cbdd
commit
1c80c33bd6
@ -38,6 +38,7 @@
|
|||||||
<jsoup.version>1.11.3</jsoup.version>
|
<jsoup.version>1.11.3</jsoup.version>
|
||||||
<jasypt.version>1.16</jasypt.version>
|
<jasypt.version>1.16</jasypt.version>
|
||||||
<ip2region.version>1.7.2</ip2region.version>
|
<ip2region.version>1.7.2</ip2region.version>
|
||||||
|
<jfreechart.version>1.5.4</jfreechart.version>
|
||||||
<easy.captcha.version>1.6.2</easy.captcha.version>
|
<easy.captcha.version>1.6.2</easy.captcha.version>
|
||||||
<useragentutils.version>1.21</useragentutils.version>
|
<useragentutils.version>1.21</useragentutils.version>
|
||||||
<gson.version>2.10.1</gson.version>
|
<gson.version>2.10.1</gson.version>
|
||||||
@ -256,6 +257,13 @@
|
|||||||
<version>${poi.ooxml.version}</version>
|
<version>${poi.ooxml.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JFreeChart 图表渲染(Word 图表导出) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jfree</groupId>
|
||||||
|
<artifactId>jfreechart</artifactId>
|
||||||
|
<version>${jfreechart.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- fastjson -->
|
<!-- fastjson -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.alibaba</groupId>
|
<groupId>com.alibaba</groupId>
|
||||||
|
|||||||
@ -0,0 +1,40 @@
|
|||||||
|
package com.yfd.platform.qgc_export.controller;
|
||||||
|
|
||||||
|
import com.yfd.platform.qgc_export.domain.word.WordChartExportRequest;
|
||||||
|
import com.yfd.platform.qgc_export.service.IWordChartExportService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Word 图表导出控制器
|
||||||
|
* <p>
|
||||||
|
* 支持饼图、柱状图、折线图、折柱混合图、堆叠柱状图、堆叠柱状图归一化(百分比)等图形导出到 Word。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/qgcExport/word")
|
||||||
|
@Tag(name = "Word 图表导出")
|
||||||
|
@Validated
|
||||||
|
public class WordChartExportController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IWordChartExportService wordChartExportService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出 Word 文档(含多种图表)
|
||||||
|
*
|
||||||
|
* @param request 文档标题 + 图表列表
|
||||||
|
* @param response HttpServletResponse
|
||||||
|
*/
|
||||||
|
@PostMapping("/export")
|
||||||
|
@Operation(summary = "导出 Word 文档(饼图/柱状图/折线图/折柱混合/堆叠/归一化堆叠)")
|
||||||
|
public void exportWord(@RequestBody WordChartExportRequest request, HttpServletResponse response) {
|
||||||
|
wordChartExportService.exportWord(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
package com.yfd.platform.qgc_export.domain.word;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Word 导出支持的图表类型
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum ChartType {
|
||||||
|
|
||||||
|
/** 饼图 */
|
||||||
|
PIE("饼图"),
|
||||||
|
/** 柱状图 */
|
||||||
|
BAR("柱状图"),
|
||||||
|
/** 折线图 */
|
||||||
|
LINE("折线图"),
|
||||||
|
/** 折柱混合图 */
|
||||||
|
BAR_LINE("折柱混合图"),
|
||||||
|
/** 堆叠柱状图 */
|
||||||
|
STACKED_BAR("堆叠柱状图"),
|
||||||
|
/** 堆叠柱状图(归一化/百分比) */
|
||||||
|
PERCENT_STACKED_BAR("堆叠柱状图归一化");
|
||||||
|
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
ChartType(String desc) {
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 忽略大小写解析图表类型
|
||||||
|
*/
|
||||||
|
public static ChartType from(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String normalized = value.trim().replace("-", "_").toUpperCase();
|
||||||
|
for (ChartType type : values()) {
|
||||||
|
if (type.name().equals(normalized)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 兼容别名
|
||||||
|
return switch (normalized) {
|
||||||
|
case "BARLINE", "BAR_LINE", "LINECOLUMN", "MIXED" -> BAR_LINE;
|
||||||
|
case "STACKED", "STACKEDBAR" -> STACKED_BAR;
|
||||||
|
case "PERCENT_STACKED", "PERCENTSTACKEDBAR", "NORMALIZED" -> PERCENT_STACKED_BAR;
|
||||||
|
case "COLUMN" -> BAR;
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.yfd.platform.qgc_export.domain.word;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Word 图表导出请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "Word 图表导出请求")
|
||||||
|
public class WordChartExportRequest {
|
||||||
|
|
||||||
|
@Schema(description = "文档标题")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Schema(description = "文档副标题/说明(可选)")
|
||||||
|
private String subtitle;
|
||||||
|
|
||||||
|
@Schema(description = "图表列表,按顺序依次插入文档")
|
||||||
|
private List<WordChartVo> charts = new ArrayList<>();
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
package com.yfd.platform.qgc_export.domain.word;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Word 文档中的单个图表描述
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "Word 图表")
|
||||||
|
public class WordChartVo {
|
||||||
|
|
||||||
|
@Schema(description = "图表类型:pie/bar/line/barLine/stackedBar/percentStackedBar")
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
@Schema(description = "图表标题")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Schema(description = "X 轴名称")
|
||||||
|
private String xAxisName;
|
||||||
|
|
||||||
|
@Schema(description = "Y 轴(左轴)名称")
|
||||||
|
private String yAxisName;
|
||||||
|
|
||||||
|
@Schema(description = "Y 轴(右轴)名称,折柱混合图用")
|
||||||
|
private String yAxisNameRight;
|
||||||
|
|
||||||
|
@Schema(description = "X 轴分类(如月份、站点名等)")
|
||||||
|
private List<String> categories = new ArrayList<>();
|
||||||
|
|
||||||
|
@Schema(description = "系列数据列表")
|
||||||
|
private List<WordSeriesVo> series = new ArrayList<>();
|
||||||
|
|
||||||
|
@Schema(description = "是否显示数值标签")
|
||||||
|
private boolean showValues = true;
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.yfd.platform.qgc_export.domain.word;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图表中的单个系列(折柱混合图中可指定主轴/副轴)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "图表系列")
|
||||||
|
public class WordSeriesVo {
|
||||||
|
|
||||||
|
@Schema(description = "系列名称(图例)")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "系列数值,与 categories 一一对应")
|
||||||
|
private List<Double> values = new ArrayList<>();
|
||||||
|
|
||||||
|
@Schema(description = "Y 轴索引:0=主轴(左), 1=副轴(右),默认 0。折柱混合图用")
|
||||||
|
private Integer yAxisIndex = 0;
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package com.yfd.platform.qgc_export.service;
|
||||||
|
|
||||||
|
import com.yfd.platform.qgc_export.domain.word.WordChartExportRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Word 图表导出服务
|
||||||
|
*/
|
||||||
|
public interface IWordChartExportService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图表配置生成 Word 文档并写入响应
|
||||||
|
*
|
||||||
|
* @param request 文档标题 + 图表列表
|
||||||
|
* @param response HttpServletResponse
|
||||||
|
*/
|
||||||
|
void exportWord(WordChartExportRequest request, HttpServletResponse response);
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
package com.yfd.platform.qgc_export.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.yfd.platform.qgc_export.domain.word.WordChartExportRequest;
|
||||||
|
import com.yfd.platform.qgc_export.service.IWordChartExportService;
|
||||||
|
import com.yfd.platform.qgc_export.util.WordChartExportUtil;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Word 图表导出服务实现
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class WordChartExportServiceImpl implements IWordChartExportService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void exportWord(WordChartExportRequest request, HttpServletResponse response) {
|
||||||
|
byte[] data;
|
||||||
|
try {
|
||||||
|
data = WordChartExportUtil.generateWord(request);
|
||||||
|
} catch (IOException e) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String fileName = resolveFileName(request);
|
||||||
|
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||||
|
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||||
|
response.setContentLength(data.length);
|
||||||
|
response.setHeader("Content-Disposition",
|
||||||
|
"attachment; filename*=UTF-8''" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
try (OutputStream os = response.getOutputStream()) {
|
||||||
|
os.write(data);
|
||||||
|
os.flush();
|
||||||
|
} catch (IOException e) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveFileName(WordChartExportRequest request) {
|
||||||
|
if (request != null && StrUtil.isNotBlank(request.getTitle())) {
|
||||||
|
return request.getTitle() + ".docx";
|
||||||
|
}
|
||||||
|
return "图表报告.docx";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,308 @@
|
|||||||
|
package com.yfd.platform.qgc_export.util;
|
||||||
|
|
||||||
|
import com.yfd.platform.qgc_export.domain.word.ChartType;
|
||||||
|
import com.yfd.platform.qgc_export.domain.word.WordChartVo;
|
||||||
|
import com.yfd.platform.qgc_export.domain.word.WordSeriesVo;
|
||||||
|
import org.jfree.chart.ChartFactory;
|
||||||
|
import org.jfree.chart.JFreeChart;
|
||||||
|
import org.jfree.chart.StandardChartTheme;
|
||||||
|
import org.jfree.chart.axis.CategoryAxis;
|
||||||
|
import org.jfree.chart.axis.NumberAxis;
|
||||||
|
import org.jfree.chart.labels.StandardCategoryItemLabelGenerator;
|
||||||
|
import org.jfree.chart.labels.StandardPieSectionLabelGenerator;
|
||||||
|
import org.jfree.chart.plot.CategoryPlot;
|
||||||
|
import org.jfree.chart.plot.DatasetRenderingOrder;
|
||||||
|
import org.jfree.chart.plot.PiePlot;
|
||||||
|
import org.jfree.chart.plot.PlotOrientation;
|
||||||
|
import org.jfree.chart.renderer.category.BarRenderer;
|
||||||
|
import org.jfree.chart.renderer.category.LineAndShapeRenderer;
|
||||||
|
import org.jfree.chart.renderer.category.StackedBarRenderer;
|
||||||
|
import org.jfree.chart.ui.RectangleInsets;
|
||||||
|
import org.jfree.data.category.DefaultCategoryDataset;
|
||||||
|
import org.jfree.data.general.DefaultPieDataset;
|
||||||
|
|
||||||
|
import java.awt.BasicStroke;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Font;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图表渲染工具:将结构化图表数据渲染为 BufferedImage,供 Word 导出插入。
|
||||||
|
* <p>
|
||||||
|
* 支持:饼图、柱状图、折线图、折柱混合图、堆叠柱状图、堆叠柱状图归一化(百分比)。
|
||||||
|
*/
|
||||||
|
public final class ChartRenderUtil {
|
||||||
|
|
||||||
|
/** 默认渲染宽度 */
|
||||||
|
private static final int DEFAULT_WIDTH = 900;
|
||||||
|
/** 默认渲染高度 */
|
||||||
|
private static final int DEFAULT_HEIGHT = 520;
|
||||||
|
/** 中文字体 */
|
||||||
|
private static final String FONT_NAME = "SimSun";
|
||||||
|
|
||||||
|
static {
|
||||||
|
// 统一主题,避免中文乱码,去除阴影
|
||||||
|
StandardChartTheme theme = (StandardChartTheme) StandardChartTheme.createJFreeTheme();
|
||||||
|
Font extraLarge = new Font(FONT_NAME, Font.BOLD, 20);
|
||||||
|
Font large = new Font(FONT_NAME, Font.BOLD, 14);
|
||||||
|
Font regular = new Font(FONT_NAME, Font.PLAIN, 12);
|
||||||
|
Font small = new Font(FONT_NAME, Font.PLAIN, 10);
|
||||||
|
theme.setExtraLargeFont(extraLarge);
|
||||||
|
theme.setLargeFont(large);
|
||||||
|
theme.setRegularFont(regular);
|
||||||
|
theme.setSmallFont(small);
|
||||||
|
theme.setShadowVisible(false);
|
||||||
|
ChartFactory.setChartTheme(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChartRenderUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染图表为图片
|
||||||
|
*/
|
||||||
|
public static BufferedImage render(WordChartVo chart) {
|
||||||
|
ChartType chartType = ChartType.from(chart.getType());
|
||||||
|
if (chartType == null) {
|
||||||
|
throw new IllegalArgumentException("不支持的图表类型: " + chart.getType());
|
||||||
|
}
|
||||||
|
JFreeChart jfreeChart = switch (chartType) {
|
||||||
|
case PIE -> buildPieChart(chart);
|
||||||
|
case BAR -> buildBarChart(chart);
|
||||||
|
case LINE -> buildLineChart(chart);
|
||||||
|
case BAR_LINE -> buildBarLineChart(chart);
|
||||||
|
case STACKED_BAR -> buildStackedBarChart(chart, false);
|
||||||
|
case PERCENT_STACKED_BAR -> buildStackedBarChart(chart, true);
|
||||||
|
};
|
||||||
|
if (jfreeChart == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
jfreeChart.setBackgroundPaint(Color.WHITE);
|
||||||
|
return jfreeChart.createBufferedImage(DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JFreeChart buildPieChart(WordChartVo chart) {
|
||||||
|
DefaultPieDataset<String> dataset = new DefaultPieDataset<>();
|
||||||
|
List<String> categories = chart.getCategories();
|
||||||
|
WordSeriesVo series = chart.getSeries().isEmpty() ? null : chart.getSeries().get(0);
|
||||||
|
if (categories == null || categories.isEmpty() || series == null || series.getValues() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<Double> values = series.getValues();
|
||||||
|
for (int i = 0; i < categories.size(); i++) {
|
||||||
|
Double value = i < values.size() ? values.get(i) : 0.0;
|
||||||
|
dataset.setValue(categories.get(i), value == null ? 0.0 : value);
|
||||||
|
}
|
||||||
|
|
||||||
|
JFreeChart jfreeChart = ChartFactory.createPieChart(
|
||||||
|
chart.getTitle(), dataset, true, true, false);
|
||||||
|
PiePlot<String> plot = (PiePlot<String>) jfreeChart.getPlot();
|
||||||
|
plot.setLabelGenerator(new StandardPieSectionLabelGenerator(
|
||||||
|
"{0}: {1} ({2})", new DecimalFormat("0.##"), new DecimalFormat("0.0%")));
|
||||||
|
plot.setLabelFont(new Font(FONT_NAME, Font.PLAIN, 12));
|
||||||
|
plot.setLegendLabelGenerator(new StandardPieSectionLabelGenerator("{0}"));
|
||||||
|
plot.setBackgroundPaint(Color.WHITE);
|
||||||
|
plot.setOutlineVisible(false);
|
||||||
|
plot.setLabelBackgroundPaint(new Color(255, 255, 255, 200));
|
||||||
|
plot.setLabelOutlinePaint(null);
|
||||||
|
plot.setLabelShadowPaint(null);
|
||||||
|
return jfreeChart;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JFreeChart buildBarChart(WordChartVo chart) {
|
||||||
|
DefaultCategoryDataset dataset = buildDataset(chart);
|
||||||
|
JFreeChart jfreeChart = ChartFactory.createBarChart(
|
||||||
|
chart.getTitle(),
|
||||||
|
chart.getXAxisName(),
|
||||||
|
chart.getYAxisName(),
|
||||||
|
dataset,
|
||||||
|
PlotOrientation.VERTICAL,
|
||||||
|
true, true, false);
|
||||||
|
CategoryPlot plot = jfreeChart.getCategoryPlot();
|
||||||
|
stylePlot(plot);
|
||||||
|
BarRenderer renderer = (BarRenderer) plot.getRenderer();
|
||||||
|
renderer.setDefaultItemLabelsVisible(chart.isShowValues());
|
||||||
|
renderer.setDefaultItemLabelGenerator(
|
||||||
|
new StandardCategoryItemLabelGenerator("{2}", new DecimalFormat("0.##")));
|
||||||
|
renderer.setDefaultItemLabelFont(new Font(FONT_NAME, Font.PLAIN, 10));
|
||||||
|
renderer.setItemMargin(0.1);
|
||||||
|
return jfreeChart;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JFreeChart buildLineChart(WordChartVo chart) {
|
||||||
|
DefaultCategoryDataset dataset = buildDataset(chart);
|
||||||
|
JFreeChart jfreeChart = ChartFactory.createLineChart(
|
||||||
|
chart.getTitle(),
|
||||||
|
chart.getXAxisName(),
|
||||||
|
chart.getYAxisName(),
|
||||||
|
dataset,
|
||||||
|
PlotOrientation.VERTICAL,
|
||||||
|
true, true, false);
|
||||||
|
CategoryPlot plot = jfreeChart.getCategoryPlot();
|
||||||
|
stylePlot(plot);
|
||||||
|
LineAndShapeRenderer renderer = (LineAndShapeRenderer) plot.getRenderer();
|
||||||
|
renderer.setDefaultShapesVisible(true);
|
||||||
|
renderer.setDefaultShapesFilled(true);
|
||||||
|
renderer.setDefaultStroke(new BasicStroke(2.0f));
|
||||||
|
renderer.setDefaultItemLabelsVisible(chart.isShowValues());
|
||||||
|
renderer.setDefaultItemLabelGenerator(
|
||||||
|
new StandardCategoryItemLabelGenerator("{2}", new DecimalFormat("0.##")));
|
||||||
|
renderer.setDefaultItemLabelFont(new Font(FONT_NAME, Font.PLAIN, 10));
|
||||||
|
return jfreeChart;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JFreeChart buildStackedBarChart(WordChartVo chart, boolean percent) {
|
||||||
|
DefaultCategoryDataset dataset = buildDataset(chart);
|
||||||
|
if (percent) {
|
||||||
|
dataset = normalizePercent(dataset);
|
||||||
|
}
|
||||||
|
JFreeChart jfreeChart = ChartFactory.createStackedBarChart(
|
||||||
|
chart.getTitle(),
|
||||||
|
chart.getXAxisName(),
|
||||||
|
percent ? (chart.getYAxisName() == null ? "占比(%)" : chart.getYAxisName()) : chart.getYAxisName(),
|
||||||
|
dataset,
|
||||||
|
PlotOrientation.VERTICAL,
|
||||||
|
true, true, false);
|
||||||
|
CategoryPlot plot = jfreeChart.getCategoryPlot();
|
||||||
|
stylePlot(plot);
|
||||||
|
StackedBarRenderer renderer = (StackedBarRenderer) plot.getRenderer();
|
||||||
|
renderer.setDefaultItemLabelsVisible(chart.isShowValues());
|
||||||
|
if (percent) {
|
||||||
|
renderer.setDefaultItemLabelGenerator(
|
||||||
|
new StandardCategoryItemLabelGenerator("{2}%", new DecimalFormat("0.##")));
|
||||||
|
NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
|
||||||
|
rangeAxis.setRange(0.0, 100.0);
|
||||||
|
rangeAxis.setNumberFormatOverride(new DecimalFormat("0'%'"));
|
||||||
|
} else {
|
||||||
|
renderer.setDefaultItemLabelGenerator(
|
||||||
|
new StandardCategoryItemLabelGenerator("{2}", new DecimalFormat("0.##")));
|
||||||
|
}
|
||||||
|
renderer.setDefaultItemLabelFont(new Font(FONT_NAME, Font.PLAIN, 10));
|
||||||
|
return jfreeChart;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JFreeChart buildBarLineChart(WordChartVo chart) {
|
||||||
|
DefaultCategoryDataset barDataset = new DefaultCategoryDataset();
|
||||||
|
DefaultCategoryDataset lineDataset = new DefaultCategoryDataset();
|
||||||
|
boolean hasBar = false;
|
||||||
|
boolean hasLine = false;
|
||||||
|
for (WordSeriesVo series : chart.getSeries()) {
|
||||||
|
if (series == null || series.getName() == null || series.getValues() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
boolean onRight = series.getYAxisIndex() != null && series.getYAxisIndex() == 1;
|
||||||
|
DefaultCategoryDataset target = onRight ? lineDataset : barDataset;
|
||||||
|
if (onRight) {
|
||||||
|
hasLine = true;
|
||||||
|
} else {
|
||||||
|
hasBar = true;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < chart.getCategories().size(); i++) {
|
||||||
|
Double value = i < series.getValues().size() ? series.getValues().get(i) : null;
|
||||||
|
target.addValue(value == null ? 0.0 : value, series.getName(), chart.getCategories().get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 若全部被归到一个轴,至少保证主轴有数据
|
||||||
|
if (!hasBar && hasLine) {
|
||||||
|
return buildLineChart(chart);
|
||||||
|
}
|
||||||
|
|
||||||
|
CategoryAxis domainAxis = new CategoryAxis(chart.getXAxisName());
|
||||||
|
domainAxis.setTickLabelFont(new Font(FONT_NAME, Font.PLAIN, 12));
|
||||||
|
NumberAxis leftAxis = new NumberAxis(chart.getYAxisName());
|
||||||
|
leftAxis.setTickLabelFont(new Font(FONT_NAME, Font.PLAIN, 12));
|
||||||
|
NumberAxis rightAxis = new NumberAxis(chart.getYAxisNameRight());
|
||||||
|
rightAxis.setTickLabelFont(new Font(FONT_NAME, Font.PLAIN, 12));
|
||||||
|
|
||||||
|
BarRenderer barRenderer = new BarRenderer();
|
||||||
|
barRenderer.setDefaultItemLabelsVisible(chart.isShowValues());
|
||||||
|
barRenderer.setDefaultItemLabelGenerator(
|
||||||
|
new StandardCategoryItemLabelGenerator("{2}", new DecimalFormat("0.##")));
|
||||||
|
barRenderer.setDefaultItemLabelFont(new Font(FONT_NAME, Font.PLAIN, 10));
|
||||||
|
|
||||||
|
LineAndShapeRenderer lineRenderer = new LineAndShapeRenderer();
|
||||||
|
lineRenderer.setDefaultShapesVisible(true);
|
||||||
|
lineRenderer.setDefaultShapesFilled(true);
|
||||||
|
lineRenderer.setDefaultStroke(new BasicStroke(2.0f));
|
||||||
|
lineRenderer.setDefaultItemLabelsVisible(chart.isShowValues());
|
||||||
|
lineRenderer.setDefaultItemLabelGenerator(
|
||||||
|
new StandardCategoryItemLabelGenerator("{2}", new DecimalFormat("0.##")));
|
||||||
|
lineRenderer.setDefaultItemLabelFont(new Font(FONT_NAME, Font.PLAIN, 10));
|
||||||
|
|
||||||
|
CategoryPlot plot = new CategoryPlot(barDataset, domainAxis, leftAxis, barRenderer);
|
||||||
|
plot.setDataset(1, lineDataset);
|
||||||
|
plot.setRenderer(1, lineRenderer);
|
||||||
|
plot.setRangeAxis(1, rightAxis);
|
||||||
|
plot.mapDatasetToRangeAxis(0, 0);
|
||||||
|
plot.mapDatasetToRangeAxis(1, 1);
|
||||||
|
plot.setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD);
|
||||||
|
plot.setBackgroundPaint(Color.WHITE);
|
||||||
|
plot.setDomainGridlinesVisible(true);
|
||||||
|
plot.setRangeGridlinesVisible(true);
|
||||||
|
plot.setAxisOffset(new RectangleInsets(5.0, 5.0, 5.0, 5.0));
|
||||||
|
plot.setDomainGridlinePaint(Color.LIGHT_GRAY);
|
||||||
|
plot.setRangeGridlinePaint(Color.LIGHT_GRAY);
|
||||||
|
|
||||||
|
return new JFreeChart(chart.getTitle(), JFreeChart.DEFAULT_TITLE_FONT, plot, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DefaultCategoryDataset buildDataset(WordChartVo chart) {
|
||||||
|
DefaultCategoryDataset dataset = new DefaultCategoryDataset();
|
||||||
|
List<String> categories = chart.getCategories();
|
||||||
|
if (categories == null || categories.isEmpty()) {
|
||||||
|
return dataset;
|
||||||
|
}
|
||||||
|
for (WordSeriesVo series : chart.getSeries()) {
|
||||||
|
if (series == null || series.getName() == null || series.getValues() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < categories.size(); i++) {
|
||||||
|
Double value = i < series.getValues().size() ? series.getValues().get(i) : null;
|
||||||
|
dataset.addValue(value == null ? 0.0 : value, series.getName(), categories.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dataset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将数据按列(分类)归一化为百分比,用于归一化堆叠柱状图
|
||||||
|
*/
|
||||||
|
private static DefaultCategoryDataset normalizePercent(DefaultCategoryDataset source) {
|
||||||
|
DefaultCategoryDataset result = new DefaultCategoryDataset();
|
||||||
|
int rowCount = source.getRowCount();
|
||||||
|
int columnCount = source.getColumnCount();
|
||||||
|
if (rowCount == 0 || columnCount == 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
for (int col = 0; col < columnCount; col++) {
|
||||||
|
double sum = 0.0;
|
||||||
|
for (int row = 0; row < rowCount; row++) {
|
||||||
|
Number value = source.getValue(row, col);
|
||||||
|
sum += value == null ? 0.0 : value.doubleValue();
|
||||||
|
}
|
||||||
|
for (int row = 0; row < rowCount; row++) {
|
||||||
|
Number value = source.getValue(row, col);
|
||||||
|
double v = value == null ? 0.0 : value.doubleValue();
|
||||||
|
double percent = sum == 0.0 ? 0.0 : v / sum * 100.0;
|
||||||
|
result.addValue(percent, source.getRowKey(row), source.getColumnKey(col));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void stylePlot(CategoryPlot plot) {
|
||||||
|
plot.setBackgroundPaint(Color.WHITE);
|
||||||
|
plot.setDomainGridlinesVisible(true);
|
||||||
|
plot.setRangeGridlinesVisible(true);
|
||||||
|
plot.setDomainGridlinePaint(Color.LIGHT_GRAY);
|
||||||
|
plot.setRangeGridlinePaint(Color.LIGHT_GRAY);
|
||||||
|
plot.setAxisOffset(new RectangleInsets(5.0, 5.0, 5.0, 5.0));
|
||||||
|
CategoryAxis domainAxis = plot.getDomainAxis();
|
||||||
|
domainAxis.setTickLabelFont(new Font(FONT_NAME, Font.PLAIN, 12));
|
||||||
|
NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
|
||||||
|
rangeAxis.setTickLabelFont(new Font(FONT_NAME, Font.PLAIN, 12));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
package com.yfd.platform.qgc_export.util;
|
||||||
|
|
||||||
|
import com.yfd.platform.qgc_export.domain.word.WordChartExportRequest;
|
||||||
|
import com.yfd.platform.qgc_export.domain.word.WordChartVo;
|
||||||
|
import org.apache.poi.util.Units;
|
||||||
|
import org.apache.poi.xwpf.usermodel.Document;
|
||||||
|
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
|
||||||
|
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
||||||
|
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
|
||||||
|
import org.apache.poi.xwpf.usermodel.XWPFRun;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Word 图表导出工具:将多个图表渲染为图片并按顺序写入 .docx 文档。
|
||||||
|
*/
|
||||||
|
public final class WordChartExportUtil {
|
||||||
|
|
||||||
|
/** 插入图片的目标宽度(单位:磅,1 磅 = 1/72 英寸) */
|
||||||
|
private static final double IMAGE_WIDTH_PT = 460.0;
|
||||||
|
|
||||||
|
private WordChartExportUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 Word 文档字节数组
|
||||||
|
*
|
||||||
|
* @param request 文档标题 + 图表列表
|
||||||
|
* @return .docx 字节数组
|
||||||
|
*/
|
||||||
|
public static byte[] generateWord(WordChartExportRequest request) throws IOException {
|
||||||
|
List<WordChartVo> charts = request == null ? List.of() : request.getCharts();
|
||||||
|
try (XWPFDocument document = new XWPFDocument();
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||||
|
|
||||||
|
appendTitle(document, request);
|
||||||
|
for (WordChartVo chart : charts) {
|
||||||
|
appendChart(document, chart);
|
||||||
|
}
|
||||||
|
document.write(out);
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void appendTitle(XWPFDocument document, WordChartExportRequest request) {
|
||||||
|
String title = request == null ? null : request.getTitle();
|
||||||
|
if (title != null && !title.isBlank()) {
|
||||||
|
XWPFParagraph paragraph = document.createParagraph();
|
||||||
|
paragraph.setAlignment(ParagraphAlignment.CENTER);
|
||||||
|
XWPFRun run = paragraph.createRun();
|
||||||
|
run.setText(title);
|
||||||
|
run.setBold(true);
|
||||||
|
run.setFontSize(22);
|
||||||
|
run.setFontFamily("SimSun");
|
||||||
|
}
|
||||||
|
String subtitle = request == null ? null : request.getSubtitle();
|
||||||
|
if (subtitle != null && !subtitle.isBlank()) {
|
||||||
|
XWPFParagraph paragraph = document.createParagraph();
|
||||||
|
paragraph.setAlignment(ParagraphAlignment.CENTER);
|
||||||
|
XWPFRun run = paragraph.createRun();
|
||||||
|
run.setText(subtitle);
|
||||||
|
run.setFontSize(12);
|
||||||
|
run.setColor("808080");
|
||||||
|
run.setFontFamily("SimSun");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void appendChart(XWPFDocument document, WordChartVo chart) throws IOException {
|
||||||
|
BufferedImage image = ChartRenderUtil.render(chart);
|
||||||
|
if (image == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
byte[] png = toPngBytes(image);
|
||||||
|
double imageHeightPt = IMAGE_WIDTH_PT * image.getHeight() / image.getWidth();
|
||||||
|
|
||||||
|
XWPFParagraph paragraph = document.createParagraph();
|
||||||
|
paragraph.setAlignment(ParagraphAlignment.CENTER);
|
||||||
|
XWPFRun run = paragraph.createRun();
|
||||||
|
try (ByteArrayInputStream in = new ByteArrayInputStream(png)) {
|
||||||
|
try {
|
||||||
|
run.addPicture(in, Document.PICTURE_TYPE_PNG, "chart.png",
|
||||||
|
Units.toEMU(IMAGE_WIDTH_PT), Units.toEMU(imageHeightPt));
|
||||||
|
} catch (org.apache.poi.openxml4j.exceptions.InvalidFormatException e) {
|
||||||
|
throw new IOException("插入图片失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 每个图表后空一行,避免图片粘连
|
||||||
|
XWPFParagraph spacer = document.createParagraph();
|
||||||
|
spacer.setSpacingAfter(120);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] toPngBytes(BufferedImage image) throws IOException {
|
||||||
|
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||||
|
ImageIO.write(image, "png", out);
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user