📊 支持图表导出功能!
支持将 柱状图 、折线图 图表以 Word 文档格式导出,并保留图例、坐标轴、颜色、数据标签等完整信息。
如需使用该功能,请私聊我,备注 "导出柱状图 / 折线图"。
生成的效果图如下:

示例调用方式
java
package com.gemantic.qflow.word.utils;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.util.Units;
import org.apache.poi.xddf.usermodel.XDDFColor;
import org.apache.poi.xddf.usermodel.XDDFShapeProperties;
import org.apache.poi.xddf.usermodel.XDDFSolidFillProperties;
import org.apache.poi.xddf.usermodel.chart.ChartTypes;
import org.apache.poi.xddf.usermodel.chart.LegendPosition;
import org.apache.poi.xddf.usermodel.chart.XDDFCategoryDataSource;
import org.apache.poi.xddf.usermodel.chart.XDDFChartLegend;
import org.apache.poi.xddf.usermodel.chart.XDDFDataSourcesFactory;
import org.apache.poi.xddf.usermodel.chart.XDDFNumericalDataSource;
import org.apache.poi.xddf.usermodel.chart.XDDFPieChartData;
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
import org.apache.poi.xwpf.usermodel.XWPFChart;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.openxmlformats.schemas.drawingml.x2006.chart.CTDLbls;
import org.openxmlformats.schemas.drawingml.x2006.chart.CTPieSer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* 饼图渲染工具类:用于在Word文档中生成饼图。
* 支持自定义颜色、图例位置、数据标签显示等参数。
* 当单个数据点包含多个值时,自动取第一个值作为饼图数据。
*/
public class PieChartRenderer {
/**
* 测试主方法,直接运行可生成示例Word文档并导出到本地。
* @param args 命令行参数
* @throws IOException 文件写入异常
* @throws InvalidFormatException 图表格式异常
*/
public static void main(String[] args) throws IOException, InvalidFormatException {
// 使用用户提供的JSON数据结构进行测试
String jsonData = "{\n" +
" \"type\": \"chart_pie\",\n" +
" \"chartType\": \"pie\",\n" +
" \"title\": \"股价对比图表\",\n" +
" \"xAxisTitle\": \"日期\",\n" +
" \"yAxisTitle\": \"价格\",\n" +
" \"legend\": [],\n" +
" \"value\": [\n" +
" { \"name\": \"2022/11/12\", \"value\": [120.99,2000] },\n" +
" { \"name\": \"2022/11/13\", \"value\": [null] },\n" +
" { \"name\": \"2022/11/14\", \"value\": [150] },\n" +
" { \"name\": \"2022/11/15\", \"value\": [160] },\n" +
" { \"name\": \"2022/11/16\", \"value\": [180] }\n" +
" ],\n" +
" \"colors\": [\n" +
" \"#4E79A7\",\n" +
" \"#29A0CA\"\n" +
" ],\n" +
" \"showTitle\": true,\n" +
" \"showGrid\": true,\n" +
" \"showLegend\": true,\n" +
" \"showAxisLabel\": true,\n" +
" \"showDataLabel\": true,\n" +
" \"showAxis\": true,\n" +
" \"width\": 600,\n" +
" \"height\": 400\n" +
"}";
new PieChartRenderer().renderFromJson(new XWPFDocument(), jsonData);
}
/**
* 基于JSON字符串渲染饼图到Word文档
* @param doc 目标XWPFDocument文档对象
* @param jsonData JSON字符串,包含饼图配置和数据
* @throws IOException 文件写入异常
* @throws InvalidFormatException 图表格式异常
*/
public void renderFromJson(XWPFDocument doc, String jsonData) throws IOException, InvalidFormatException {
ObjectMapper mapper = new ObjectMapper();
JsonNode rootNode = mapper.readTree(jsonData);
// 解析基本配置
String chartType = rootNode.get("chartType").asText(); // 应该是 "pie"
String title = rootNode.get("title").asText();
// 【图表标题】是否显示图表主标题。true:显示。false:不显示。
boolean showTitle = rootNode.get("showTitle").asBoolean();
// 【图例】是否显示图例。
boolean showLegend = rootNode.get("showLegend").asBoolean();
// 【数据标签】是否在图表上直接显示每个数据点的数值标签。true:显示。false:不显示。
boolean showDataLabel = rootNode.get("showDataLabel").asBoolean();
// 饼图的尺寸配置
int width = rootNode.has("width") ? rootNode.get("width").asInt() : 600;
int height = rootNode.has("height") ? rootNode.get("height").asInt() : 400;
// 解析颜色配置
List<String> colors = new ArrayList<>();
JsonNode colorsNode = rootNode.get("colors");
if (colorsNode != null) {
for (JsonNode color : colorsNode) {
colors.add(color.asText());
}
}
// 解析饼图数据
JsonNode valueNode = rootNode.get("value");
List<String> categories = new ArrayList<>();
List<Double> values = new ArrayList<>();
// 解析每个数据点,如果有多个值则取第一个,过滤掉null或无效值
for (JsonNode dataPoint : valueNode) {
String name = dataPoint.get("name").asText();
JsonNode valueArray = dataPoint.get("value");
Double validValue = null;
if (valueArray.isArray() && valueArray.size() > 0) {
// 遍历值数组,找到第一个有效的非null数值
for (int i = 0; i < valueArray.size(); i++) {
JsonNode valueNode2 = valueArray.get(i);
if (!valueNode2.isNull() && valueNode2.isNumber()) {
double val = valueNode2.asDouble();
// 只接受大于0的有效值(饼图不能有负值或0值)
if (val > 0) {
validValue = val;
break;
}
}
}
}
// 只添加有有效值的数据点到饼图中
if (validValue != null) {
categories.add(name);
values.add(validValue);
System.out.println("✅ 添加饼图数据点:" + name + " = " + validValue);
} else {
System.out.println("⚠️ 跳过无效数据点:" + name + "(值为null、0或负数)");
}
}
// 转换为数组
String[] categoryArray = categories.toArray(new String[0]);
Double[] valueArray = values.toArray(new Double[0]);
// 创建饼图
createPieChart(doc, title, categoryArray, valueArray, colors,
showDataLabel, showTitle, showLegend, width, height);
// 保存文件
String outputPath = "/Users/wtm/Desktop/output/pie_chart_" + System.currentTimeMillis() + ".docx";
try (FileOutputStream out = new FileOutputStream(outputPath)) {
doc.write(out);
}
System.out.println("✅ 饼图导出完成,路径:" + outputPath);
}
/**
* 创建饼图的核心方法
* @param doc 目标XWPFDocument文档对象
* @param chartTitle 图表标题
* @param categories 饼图分类标签数组
* @param values 饼图数值数组
* @param colors 颜色列表
* @param showDataLabels 是否显示数据标签
* @param showTitle 是否显示图表标题
* @param showLegend 是否显示图例
* @param width 图表宽度(像素)
* @param height 图表高度(像素)
* @throws IOException 文件写入异常
* @throws InvalidFormatException 图表格式异常
*/
private void createPieChart(XWPFDocument doc,
String chartTitle,
String[] categories,
Double[] values,
List<String> colors,
boolean showDataLabels,
boolean showTitle,
boolean showLegend,
int width,
int height) throws IOException, InvalidFormatException {
// 创建段落标题
XWPFParagraph p = doc.createParagraph();
p.setAlignment(ParagraphAlignment.CENTER);
XWPFRun r = p.createRun();
r.setText(chartTitle);
r.setBold(true);
r.setFontSize(16);
// 创建图表对象 - 使用JSON提供的尺寸,转换为EMU单位
int widthEMU = (int) (width * Units.EMU_PER_PIXEL);
int heightEMU = (int) (height * Units.EMU_PER_PIXEL);
XWPFChart chart = doc.createChart(widthEMU, heightEMU);
// 首先填充嵌入的Excel数据,确保数据源正确建立
populateEmbeddedExcelDataForPie(chart, categories, values);
// 设置图表标题显示/隐藏
if (showTitle) {
chart.setTitleText(chartTitle);
chart.setTitleOverlay(false);
} else {
// 隐藏图表标题
chart.setTitleText("");
chart.setTitleOverlay(true);
}
// 设置图例显示/隐藏
if (showLegend) {
XDDFChartLegend legend = chart.getOrAddLegend();
legend.setPosition(LegendPosition.BOTTOM); // 【修改】图例位置设置为底部
} else {
// 隐藏图例
if (chart.getCTChart().isSetLegend()) {
chart.getCTChart().unsetLegend();
}
}
// 使用Excel工作表数据作为数据源
XDDFCategoryDataSource categoryDataSource = createCategoryDataSourceFromExcelForPie(chart, categories.length);
XDDFNumericalDataSource<Double> valuesDataSource = createNumericalDataSourceFromExcelForPie(chart, categories.length);
// 创建饼图数据
XDDFPieChartData data = (XDDFPieChartData) chart.createData(ChartTypes.PIE, null, null);
// 添加饼图系列
XDDFPieChartData.Series series = (XDDFPieChartData.Series) data.addSeries(categoryDataSource, valuesDataSource);
series.setTitle("饼图数据", null);
// 设置数据标签
setPieDataLabels(series, showDataLabels, values);
// 设置饼图扇形颜色
setPieSeriesColors(series, colors, categories.length);
// 绘制图表
chart.plot(data);
System.out.println("✅ 饼图创建完成,包含 " + categories.length + " 个扇形");
}
/**
* 填充嵌入的Excel数据,专门为饼图设计
* @param chart XWPFChart对象
* @param categories 饼图分类标签
* @param values 饼图数值
*/
private void populateEmbeddedExcelDataForPie(XWPFChart chart, String[] categories, Double[] values) {
try {
// 获取嵌入的Excel工作簿
if (chart.getWorkbook() != null) {
org.apache.poi.ss.usermodel.Workbook workbook = chart.getWorkbook();
// 获取第一个工作表,如果不存在则创建
org.apache.poi.ss.usermodel.Sheet sheet = workbook.getNumberOfSheets() > 0 ?
workbook.getSheetAt(0) : workbook.createSheet("PieChartData");
// 设置工作表名称
if (workbook.getNumberOfSheets() > 0) {
workbook.setSheetName(0, "PieChartData");
}
// 清空现有数据
for (int i = sheet.getLastRowNum(); i >= 0; i--) {
org.apache.poi.ss.usermodel.Row row = sheet.getRow(i);
if (row != null) {
sheet.removeRow(row);
}
}
// 创建表头行
org.apache.poi.ss.usermodel.Row headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("分类"); // 第一列为分类标题
headerRow.createCell(1).setCellValue("数值"); // 第二列为数值标题
// 填充数据行
for (int i = 0; i < categories.length && i < values.length; i++) {
org.apache.poi.ss.usermodel.Row dataRow = sheet.createRow(i + 1);
dataRow.createCell(0).setCellValue(categories[i]);
dataRow.createCell(1).setCellValue(values[i] != null ? values[i] : 0.0);
}
// 自动调整列宽
sheet.autoSizeColumn(0);
sheet.autoSizeColumn(1);
// 设置数据区域名称,便于图表引用
org.apache.poi.ss.usermodel.Name dataRange = workbook.createName();
dataRange.setNameName("PieChartDataRange");
String rangeFormula = "PieChartData!$A$1:$B$" + (categories.length + 1);
dataRange.setRefersToFormula(rangeFormula);
System.out.println("✅ 已填充饼图嵌入Excel数据,包含 " + (categories.length + 1) + " 行 2 列");
System.out.println("✅ 饼图数据范围设置为:" + rangeFormula);
}
} catch (Exception e) {
System.err.println("警告:填充饼图嵌入Excel数据时出错:" + e.getMessage());
e.printStackTrace();
}
}
/**
* 从Excel工作表创建分类数据源(专门为饼图设计)
* @param chart XWPFChart对象
* @param categoryCount 分类数量
* @return 分类数据源
*/
private XDDFCategoryDataSource createCategoryDataSourceFromExcelForPie(XWPFChart chart, int categoryCount) {
try {
// 创建引用Excel第一列的数据源(A2:A[n],跳过标题行)
return XDDFDataSourcesFactory.fromStringCellRange(chart.getWorkbook().getSheetAt(0),
new org.apache.poi.ss.util.CellRangeAddress(1, categoryCount, 0, 0));
} catch (Exception e) {
System.err.println("警告:无法创建饼图Excel分类数据源,使用默认数据源:" + e.getMessage());
// 如果失败,返回默认的字符串数组数据源
String[] defaultCategories = new String[categoryCount];
for (int i = 0; i < categoryCount; i++) {
defaultCategories[i] = "分类" + (i + 1);
}
return XDDFDataSourcesFactory.fromArray(defaultCategories);
}
}
/**
* 从Excel工作表创建数值数据源(专门为饼图设计)
* @param chart XWPFChart对象
* @param dataCount 数据行数
* @return 数值数据源
*/
private XDDFNumericalDataSource<Double> createNumericalDataSourceFromExcelForPie(XWPFChart chart, int dataCount) {
try {
// 创建引用Excel第二列的数据源(B2:B[n],跳过标题行)
return XDDFDataSourcesFactory.fromNumericCellRange(chart.getWorkbook().getSheetAt(0),
new org.apache.poi.ss.util.CellRangeAddress(1, dataCount, 1, 1));
} catch (Exception e) {
System.err.println("警告:无法创建饼图Excel数值数据源,使用默认数据源:" + e.getMessage());
// 如果失败,返回默认的数值数组数据源
Double[] defaultData = new Double[dataCount];
for (int i = 0; i < dataCount; i++) {
defaultData[i] = (double) (i + 1) * 10; // 简单的递增数据
}
return XDDFDataSourcesFactory.fromArray(defaultData);
}
}
/**
* 设置饼图数据标签
* @param series 饼图系列
* @param showDataLabels 是否显示数据标签
* @param values 数值数组(用于确定哪些点需要标签)
*/
private void setPieDataLabels(XDDFPieChartData.Series series, boolean showDataLabels, Double[] values) {
if (!showDataLabels) {
// 关闭所有标签
CTPieSer ctSer = series.getCTPieSer();
if (ctSer.isSetDLbls()) {
ctSer.unsetDLbls();
}
return;
}
try {
// 为饼图显示数据标签
CTPieSer ctSer = series.getCTPieSer();
CTDLbls dLbls = ctSer.isSetDLbls() ? ctSer.getDLbls() : ctSer.addNewDLbls();
// 清空原有标签
dLbls.setDLblArray(null);
// 全局标签设置:显示数值
dLbls.addNewShowVal().setVal(true);
dLbls.addNewShowLegendKey().setVal(false);
dLbls.addNewShowCatName().setVal(false);
dLbls.addNewShowSerName().setVal(false);
dLbls.addNewShowPercent().setVal(false);
dLbls.addNewShowLeaderLines().setVal(true); // 饼图特有:显示引导线
// 为每个有值的数据点设置标签
for (int i = 0; i < values.length; i++) {
if (values[i] != null && values[i] > 0) {
org.openxmlformats.schemas.drawingml.x2006.chart.CTDLbl lbl = dLbls.addNewDLbl();
lbl.addNewIdx().setVal(i);
lbl.addNewShowVal().setVal(true);
lbl.addNewShowLegendKey().setVal(false);
lbl.addNewShowCatName().setVal(false);
lbl.addNewShowSerName().setVal(false);
lbl.addNewShowPercent().setVal(false);
}
}
System.out.println("✅ 已设置饼图数据标签,显示 " + values.length + " 个数据点的标签");
} catch (Exception e) {
System.err.println("警告:设置饼图数据标签时出错:" + e.getMessage());
}
}
/**
* 设置饼图扇形颜色
* @param series 饼图系列
* @param colors 颜色列表
* @param pointCount 数据点数量
*/
private void setPieSeriesColors(XDDFPieChartData.Series series, List<String> colors, int pointCount) {
if (colors == null || colors.isEmpty()) {
System.out.println("⚠️ 未提供颜色配置,将使用默认颜色");
return;
}
try {
// 为每个饼图扇形设置颜色
for (int i = 0; i < pointCount; i++) {
// 使用模运算实现颜色循环:当颜色数量少于数据点时循环使用
String colorHex = colors.get(i % colors.size());
setPieSliceColor(series, i, colorHex);
}
System.out.println("✅ 已设置饼图扇形颜色,使用 " + colors.size() + " 种颜色为 " + pointCount + " 个扇形着色");
} catch (Exception e) {
System.err.println("警告:设置饼图颜色时出错:" + e.getMessage());
}
}
/**
* 设置单个饼图扇形的颜色
* @param series 饼图系列
* @param pointIndex 数据点索引
* @param colorHex 十六进制颜色值(如 #4E79A7)
*/
private void setPieSliceColor(XDDFPieChartData.Series series, int pointIndex, String colorHex) {
try {
// 移除颜色字符串前的#号
String hex = colorHex.startsWith("#") ? colorHex.substring(1) : colorHex;
// 将十六进制颜色转换为RGB
int r = Integer.parseInt(hex.substring(0, 2), 16);
int g = Integer.parseInt(hex.substring(2, 4), 16);
int b = Integer.parseInt(hex.substring(4, 6), 16);
// 创建颜色对象
XDDFColor xddfColor = XDDFColor.from(new byte[]{(byte)r, (byte)g, (byte)b});
XDDFSolidFillProperties fillProperties = new XDDFSolidFillProperties(xddfColor);
// 设置饼图扇形颜色
XDDFShapeProperties shapeProperties = new XDDFShapeProperties();
shapeProperties.setFillProperties(fillProperties);
// 通过底层CT对象设置特定数据点的颜色
CTPieSer ctSer = series.getCTPieSer();
if (ctSer.getDPtArray().length <= pointIndex) {
// 如果数据点不存在,创建新的数据点
while (ctSer.getDPtArray().length <= pointIndex) {
org.openxmlformats.schemas.drawingml.x2006.chart.CTDPt dPt = ctSer.addNewDPt();
dPt.addNewIdx().setVal(ctSer.getDPtArray().length - 1);
}
}
org.openxmlformats.schemas.drawingml.x2006.chart.CTDPt dPt = ctSer.getDPtArray(pointIndex);
if (dPt == null) {
dPt = ctSer.addNewDPt();
dPt.addNewIdx().setVal(pointIndex);
}
// 设置数据点的填充属性
if (!dPt.isSetSpPr()) {
dPt.addNewSpPr();
}
if (!dPt.getSpPr().isSetSolidFill()) {
dPt.getSpPr().addNewSolidFill();
}
if (!dPt.getSpPr().getSolidFill().isSetSrgbClr()) {
dPt.getSpPr().getSolidFill().addNewSrgbClr();
}
// 设置RGB颜色值
dPt.getSpPr().getSolidFill().getSrgbClr().setVal(new byte[]{(byte)r, (byte)g, (byte)b});
} catch (Exception e) {
// 如果颜色格式错误,记录错误但不中断流程
System.err.println("警告:无法解析颜色 " + colorHex + " 用于数据点 " + pointIndex + ",将使用默认颜色。错误:" + e.getMessage());
}
}
}