📊 支持圖表導出功能!
支持將 柱狀圖、折線圖 圖表以 Word 文檔格式導出,并保留圖例、坐標軸、顏色、數據標簽等完整信息。
如需使用該功能,請私聊我,備注 “導出柱狀圖 / 折線圖”。
生成的效果圖如下:
示例調用方式
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;// 將十六進制顏色轉換為RGBint 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());}}
}