Java 导出word 实现饼状图导出--可编辑数据

📊 支持图表导出功能!

支持将 柱状图折线图 图表以 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());
        }
    }
}
相关推荐
胚芽鞘6814 小时前
关于java项目中maven的理解
java·数据库·maven
岁忧5 小时前
(LeetCode 面试经典 150 题 ) 11. 盛最多水的容器 (贪心+双指针)
java·c++·算法·leetcode·面试·go
CJi0NG5 小时前
【自用】JavaSE--算法、正则表达式、异常
java
Hellyc5 小时前
用户查询优惠券之缓存击穿
java·redis·缓存
今天又在摸鱼6 小时前
Maven
java·maven
老马啸西风6 小时前
maven 发布到中央仓库常用脚本-02
java·maven
代码的余温6 小时前
MyBatis集成Logback日志全攻略
java·tomcat·mybatis·logback
一只叫煤球的猫7 小时前
【🤣离谱整活】我写了一篇程序员掉进 Java 异世界的短篇小说
java·后端·程序员
斐波娜娜7 小时前
Maven详解
java·开发语言·maven
Bug退退退1237 小时前
RabbitMQ 高级特性之事务
java·分布式·spring·rabbitmq