Java 导出word 实现表格内插入图表(柱状图、折线图、饼状图)--可编辑数据

发布于:2025-08-15 ⋅ 阅读:(15) ⋅ 点赞:(0)

表格内插入图表导出效果

在这里插入图片描述

表格内图表生成流程分析

核心问题与解决方案

问题

  • Word 图表作为独立对象,容易与文本分离
  • 位置难以精确控制,编辑时容易偏移
  • 缺乏与表格数据的关联性

解决方案

  • 直接嵌入:将图表嵌入表格单元格,确保数据关联
  • 精确控制:使用 CTInline 控制位置和大小
  • 格式兼容:利用 DrawingML 格式实现精确嵌入

核心流程(5个关键步骤)

1. 准备阶段

// 验证参数,获取文档对象
XWPFDocument document = cellParagraph.getDocument();
XWPFRun run = cellParagraph.createRun();
CTInline inline = run.getCTR().addNewDrawing().addNewInline();

2. 图表创建

// 创建图表对象并渲染数据
XWPFChart chart = createChartInCell(document, chartConfig, widthEMU, heightEMU);
String chartRelId = document.getRelationId(chart);

3. 图表嵌入

// 构建 DrawingML XML 并嵌入
String chartXml = "<a:graphic xmlns:a=\"...\">...</a:graphic>";
XmlToken xmlToken = XmlToken.Factory.parse(chartXml);
inline.set(xmlToken);

4. 位置控制

// 设置边距和尺寸,确保图表完全限制在单元格内
inline.setDistT(0); inline.setDistB(0); inline.setDistL(0); inline.setDistR(0);
extent.setCx(widthEMU); extent.setCy(heightEMU);

5. 清理优化

// 权限保护和重复内容清理
TableChartCleanupUtil.cleanupTableAfterCharts(document);

关键技术要点

1. EMU 单位转换

int widthEMU = (int) (width * Units.EMU_PER_PIXEL);
  • Word 文档使用 EMU (English Metric Units) 作为标准度量单位
  • 确保图表尺寸的精确控制

2. DrawingML XML 嵌入

  • 使用 DrawingML 格式定义图表结构
  • 通过 r:id 属性建立图表对象与嵌入内容的关联
  • 实现图表在单元格中的精确定位

3. 数据源管理

  • 使用嵌入的 Excel 数据作为图表数据源
  • 数据与文档结构分离,便于维护
  • 支持复杂的数据计算和格式化

图表类型特殊处理

柱状图/折线图

  • 支持多系列数据
  • 需要 X 轴和 Y 轴配置
  • 支持网格线和数据标签

饼图

  • 单系列数据
  • 不需要坐标轴
  • 数据验证:不能有负值或0值

设计优势

1. 精确控制

  • 图表完全限制在单元格内
  • 边距和尺寸精确控制
  • 位置固定,不会因编辑而偏移

2. 数据关联

  • 图表与表格数据紧密关联
  • 便于数据展示和分析
  • 支持复杂的数据结构

3. 格式兼容

  • 与 Word 2007+ 格式完全兼容
  • 支持 DrawingML 的所有功能
  • 与 Excel 图表格式兼容

4. 易于扩展

  • 模块化设计
  • 支持新图表类型快速集成
  • 配置驱动的样式管理

测试代码

package com.gemantic.gpt.util;

import java.io.FileOutputStream;
import java.util.List;

import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.poi.xwpf.usermodel.IBodyElement;
import org.apache.poi.xwpf.usermodel.XWPFRun;

/**
 * 测试表格内图表的修复效果
 */
public class TableChartTest {

    public static void main(String[] args) throws Exception {
        // 1. 创建文档
        XWPFDocument document = new XWPFDocument();

        ObjectMapper mapper = new ObjectMapper();

        // 2. 测试柱状图
        System.out.println("=== 测试表格内的柱状图 ===");
        XWPFTable barTable = document.createTable(2, 1);
        barTable.setWidth("100%");
        barTable.setCellMargins(10, 10, 10, 10);
        barTable.getRow(0).getCell(0).setText("柱状图示例");
        barTable.getRow(1).getCell(0).setText("");

        XWPFTableCell barChartCell = barTable.getRow(1).getCell(0);
        barChartCell.removeParagraph(0);
        XWPFParagraph barChartParagraph = barChartCell.addParagraph();
        barChartParagraph.setAlignment(org.apache.poi.xwpf.usermodel.ParagraphAlignment.CENTER);

        String barChartConfigJson = "{\n" +
                "    \"type\": \"chart_bar\",\n" +
                "    \"title\": \"销售数据柱状图\",\n" +
                "    \"xAxisTitle\": \"产品类别\",\n" +
                "    \"yAxisTitle\": \"销售额(万元)\",\n" +
                "    \"showTitle\": true,\n" +
                "    \"showGrid\": true,\n" +
                "    \"showLegend\": true,\n" +
                "    \"showDataLabel\": true,\n" +
                "    \"showAxisLabel\": true,\n" +
                "    \"showAxis\": true,\n" +
                "    \"legend\": [\"2023年\", \"2024年\"],\n" +
                "    \"colors\": [\"#5470c6\", \"#91cc75\"],\n" +
                "    \"valueList\": [\n" +
                "        {\"name\": \"电子产品\", \"value\": [150, 180]},\n" +
                "        {\"name\": \"服装鞋帽\", \"value\": [120, 140]},\n" +
                "        {\"name\": \"家居用品\", \"value\": [80, 95]},\n" +
                "        {\"name\": \"食品饮料\", \"value\": [200, 220]},\n" +
                "        {\"name\": \"图书文具\", \"value\": [60, 75]}\n" +
                "    ]\n" +
                "}";

        JsonNode barChartConfig = mapper.readTree(barChartConfigJson);
        String barChartValueNodeJson = "{\n" +
                "    \"type\": \"image\",\n" +
                "    \"imageType\": \"chart\",\n" +
                "    \"width\": 400,\n" +
                "    \"height\": 300,\n" +
                "    \"chartConfig\": " + barChartConfigJson + "\n" +
                "}";
        JsonNode barChartValueNode = mapper.readTree(barChartValueNodeJson);

        TableChartUtil.handleChart(barChartParagraph, barChartValueNode, barChartConfig, 96, false, 400, 300);

        // 添加柱状图说明
        addChartDescription(barChartCell, "柱状图说明:",
                "1. 本图表展示了2023年和2024年各产品类别的销售对比数据",
                "2. 从数据可以看出,所有产品类别在2024年都有不同程度的增长",
                "3. 食品饮料类别的销售额最高,图书文具类别的增长幅度最大");

        // 3. 测试饼图
        System.out.println("\n=== 测试表格内的饼图 ===");
        XWPFParagraph spacer1 = document.createParagraph();
        spacer1.createRun().setText("");

        XWPFTable pieTable = document.createTable(2, 1);
        pieTable.setWidth("100%");
        pieTable.setCellMargins(10, 10, 10, 10);
        pieTable.getRow(0).getCell(0).setText("饼图示例");
        pieTable.getRow(1).getCell(0).setText("");

        XWPFTableCell pieChartCell = pieTable.getRow(1).getCell(0);
        pieChartCell.removeParagraph(0);
        XWPFParagraph pieChartParagraph = pieChartCell.addParagraph();
        pieChartParagraph.setAlignment(org.apache.poi.xwpf.usermodel.ParagraphAlignment.CENTER);

        String pieChartConfigJson = "{\n" +
                "    \"type\": \"chart_pie\",\n" +
                "    \"title\": \"市场份额饼图\",\n" +
                "    \"showTitle\": true,\n" +
                "    \"showLegend\": true,\n" +
                "    \"showDataLabel\": true,\n" +
                "    \"colors\": [\"#5470c6\", \"#91cc75\", \"#fac858\", \"#ee6666\", \"#73c0de\"],\n" +
                "    \"valueList\": [\n" +
                "        {\"name\": \"苹果\", \"value\": [35]},\n" +
                "        {\"name\": \"三星\", \"value\": [25]},\n" +
                "        {\"name\": \"华为\", \"value\": [20]},\n" +
                "        {\"name\": \"小米\", \"value\": [15]},\n" +
                "        {\"name\": \"其他\", \"value\": [5]}\n" +
                "    ]\n" +
                "}";

        JsonNode pieChartConfig = mapper.readTree(pieChartConfigJson);
        String pieChartValueNodeJson = "{\n" +
                "    \"type\": \"image\",\n" +
                "    \"imageType\": \"chart\",\n" +
                "    \"width\": 400,\n" +
                "    \"height\": 300,\n" +
                "    \"chartConfig\": " + pieChartConfigJson + "\n" +
                "}";
        JsonNode pieChartValueNode = mapper.readTree(pieChartValueNodeJson);

        TableChartUtil.handlePieChart(pieChartParagraph, pieChartValueNode, pieChartConfig, 96, false, 400, 300);

        // 添加饼图说明
        addChartDescription(pieChartCell, "饼图说明:",
                "1. 本图表展示了智能手机市场的品牌份额分布",
                "2. 苹果以35%的市场份额位居第一",
                "3. 前四大品牌占据了95%的市场份额");

        // 4. 测试折线图
        System.out.println("\n=== 测试表格内的折线图 ===");
        XWPFParagraph spacer2 = document.createParagraph();
        spacer2.createRun().setText("");

        XWPFTable lineTable = document.createTable(2, 1);
        lineTable.setWidth("100%");
        lineTable.setCellMargins(10, 10, 10, 10);
        lineTable.getRow(0).getCell(0).setText("折线图示例");
        lineTable.getRow(1).getCell(0).setText("");

        XWPFTableCell lineChartCell = lineTable.getRow(1).getCell(0);
        lineChartCell.removeParagraph(0);
        XWPFParagraph lineChartParagraph = lineChartCell.addParagraph();
        lineChartParagraph.setAlignment(org.apache.poi.xwpf.usermodel.ParagraphAlignment.CENTER);

        String lineChartConfigJson = "{\n" +
                "    \"type\": \"chart_line\",\n" +
                "    \"title\": \"销售趋势折线图\",\n" +
                "    \"xAxisTitle\": \"月份\",\n" +
                "    \"yAxisTitle\": \"销售额(万元)\",\n" +
                "    \"colors\": [\n" +
                "        \"#5470c6\",\n" +
                "        \"#91cc75\",\n" +
                "        \"#fac858\"\n" +
                "    ],\n" +
                "    \"showTitle\": true,\n" +
                "    \"showGrid\": true,\n" +
                "    \"showLegend\": true,\n" +
                "    \"showDataLabel\": true,\n" +
                "    \"showAxisLabel\": true,\n" +
                "    \"showAxis\": true,\n" +
                "    \"legend\": [\n" +
                "        \"产品A\",\n" +
                "        \"产品B\",\n" +
                "        \"产品C\"\n" +
                "    ],\n" +
                "    \"valueList\": [\n" +
                "        {\n" +
                "            \"name\": \"1月\",\n" +
                "            \"value\": [120, 85, 95]\n" +
                "        },\n" +
                "        {\n" +
                "            \"name\": \"2月\",\n" +
                "            \"value\": [150, 120, 110]\n" +
                "        },\n" +
                "        {\n" +
                "            \"name\": \"3月\",\n" +
                "            \"value\": [180, 160, 140]\n" +
                "        },\n" +
                "        {\n" +
                "            \"name\": \"4月\",\n" +
                "            \"value\": [220, 200, 180]\n" +
                "        },\n" +
                "        {\n" +
                "            \"name\": \"5月\",\n" +
                "            \"value\": [250, 230, 210]\n" +
                "        },\n" +
                "        {\n" +
                "            \"name\": \"6月\",\n" +
                "            \"value\": [280, 260, 240]\n" +
                "        }\n" +
                "    ]\n" +
                "}";

        JsonNode lineChartConfig = mapper.readTree(lineChartConfigJson);
        String lineChartValueNodeJson = "{\n" +
                "    \"type\": \"image\",\n" +
                "    \"imageType\": \"chart\",\n" +
                "    \"width\": 400,\n" +
                "    \"height\": 300,\n" +
                "    \"chartConfig\": " + lineChartConfigJson + "\n" +
                "}";
        JsonNode lineChartValueNode = mapper.readTree(lineChartValueNodeJson);

        TableChartUtil.handleChart(lineChartParagraph, lineChartValueNode, lineChartConfig, 96, false, 400, 300);

        // 添加折线图说明
        addChartDescription(lineChartCell, "折线图说明:",
                "1. 本图表展示了三个产品在2024年上半年的销售趋势",
                "2. 所有产品都呈现上升趋势,其中产品A增长最快",
                "3. 6月份所有产品的销售额都达到了年度新高");

        // 5. 输出文档元素信息
        System.out.println("\n=== 文档元素信息 ===");
        System.out.println("最终文档元素数量: " + document.getBodyElements().size());
        System.out.println("文档元素类型:");
        for (int i = 0; i < document.getBodyElements().size(); i++) {
            IBodyElement element = document.getBodyElements().get(i);
            System.out.println("  元素 " + i + ": " + element.getClass().getSimpleName());
        }

        // 6. 保存文档
        String outputPath = "/Users/wtm/Desktop/output/three_charts_test_" + System.currentTimeMillis() + ".docx";
        try (FileOutputStream out = new FileOutputStream(outputPath)) {
            document.write(out);
        }

        System.out.println("\n✅ 三种图表测试完成:文档已保存到 " + outputPath);
        System.out.println("请检查文档中是否包含:");
        System.out.println("1. 柱状图表格(销售数据对比)");
        System.out.println("2. 饼图表格(市场份额分布)");
        System.out.println("3. 折线图表格(销售趋势分析)");
    }

    /**
     * 添加图表说明文字
     */
    private static void addChartDescription(XWPFTableCell cell, String title, String... descriptions) {
        // 添加标题
        XWPFParagraph titleParagraph = cell.addParagraph();
        titleParagraph.setAlignment(org.apache.poi.xwpf.usermodel.ParagraphAlignment.LEFT);
        XWPFRun titleRun = titleParagraph.createRun();
        titleRun.setText(title);
        titleRun.setBold(true);
        titleRun.setFontSize(12);

        // 添加说明文字
        for (String description : descriptions) {
            XWPFParagraph descParagraph = cell.addParagraph();
            descParagraph.setAlignment(org.apache.poi.xwpf.usermodel.ParagraphAlignment.LEFT);
            XWPFRun descRun = descParagraph.createRun();
            descRun.setText(description);
            descRun.setFontSize(10);
        }
    }
}

工具类

TableChartUtil 表格图表工具类:用于在表格单元格中插入图表

package com.gemantic.gpt.util;

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.XDDFLineProperties;
import org.apache.poi.xddf.usermodel.XDDFShapeProperties;
import org.apache.poi.xddf.usermodel.XDDFSolidFillProperties;
import org.apache.poi.xddf.usermodel.chart.AxisCrossBetween;
import org.apache.poi.xddf.usermodel.chart.AxisCrosses;
import org.apache.poi.xddf.usermodel.chart.AxisPosition;
import org.apache.poi.xddf.usermodel.chart.AxisTickLabelPosition;
import org.apache.poi.xddf.usermodel.chart.AxisTickMark;
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.XDDFCategoryDataSource;
import org.apache.poi.xddf.usermodel.chart.XDDFChartData;
import org.apache.poi.xddf.usermodel.chart.XDDFChartLegend;
import org.apache.poi.xddf.usermodel.chart.XDDFDataSourcesFactory;
import org.apache.poi.xddf.usermodel.chart.XDDFLineChartData;
import org.apache.poi.xddf.usermodel.chart.XDDFNumericalDataSource;
import org.apache.poi.xddf.usermodel.chart.XDDFValueAxis;
import org.apache.poi.xddf.usermodel.chart.XDDFPieChartData;
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.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.xmlbeans.XmlToken;
import org.openxmlformats.schemas.drawingml.x2006.chart.CTBarSer;
import org.openxmlformats.schemas.drawingml.x2006.chart.CTDLbls;
import org.openxmlformats.schemas.drawingml.x2006.chart.CTLineSer;
import org.openxmlformats.schemas.drawingml.x2006.main.CTNonVisualDrawingProps;
import org.openxmlformats.schemas.drawingml.x2006.main.CTPositiveSize2D;
import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.CTInline;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.JsonNode;
import org.apache.poi.xwpf.usermodel.IBodyElement;

/**
 * 表格图表工具类:用于在表格单元格中插入图表
 * 按照ChartInTableExample的正确顺序实现
 */
public class TableChartUtil {
    
    private static final Logger LOG = LoggerFactory.getLogger(TableChartUtil.class);
    
    /**
     * 处理柱状图和折线图
     */
    public static void handleChart(XWPFParagraph cellParagraph, JsonNode valueNode, JsonNode chartConfig, 
                                   int dpi, boolean showLock, int width, int height) {
        try {
            // 检查是否已经在表格单元格中
            if (cellParagraph == null) {
                LOG.warn("单元格段落为空,跳过图表创建");
                return;
            }
            
            XWPFDocument document = cellParagraph.getDocument();
            
            // 记录创建图表前的状态
            int chartsBefore = document.getCharts().size();
            LOG.info("创建图表前的图表数量: {}", chartsBefore);
            
            XWPFRun run = cellParagraph.createRun();
            CTInline inline = run.getCTR().addNewDrawing().addNewInline();
            
            int widthEMU = (int) (width * Units.EMU_PER_PIXEL);
            int heightEMU = (int) (height * Units.EMU_PER_PIXEL);
            
            // 1. 先创建图表并渲染数据
            XWPFChart chart = createChartInCell(document, chartConfig, widthEMU, heightEMU);
            
            // 记录创建图表后的状态
            int chartsAfter = document.getCharts().size();
            LOG.info("创建图表后的图表数量: {}", chartsAfter);
            
            // 2. 获取图表关系ID并嵌入inline
            String chartRelId = document.getRelationId(chart);
            String chartXml =
                    "<a:graphic xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\">" +
                            "<a:graphicData uri=\"http://schemas.openxmlformats.org/drawingml/2006/chart\">" +
                            "<c:chart xmlns:c=\"http://schemas.openxmlformats.org/drawingml/2006/chart\" " +
                            "xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\" r:id=\"" + chartRelId + "\"/>" +
                            "</a:graphicData>" +
                            "</a:graphic>";
            XmlToken xmlToken = XmlToken.Factory.parse(chartXml);
            inline.set(xmlToken);

            // 3. 最后设置inline属性,确保图表完全限制在单元格内
            inline.setDistT(0);
            inline.setDistB(0);
            inline.setDistL(0);
            inline.setDistR(0);
            CTPositiveSize2D extent = inline.addNewExtent();
            extent.setCx(widthEMU);
            extent.setCy(heightEMU);
            CTNonVisualDrawingProps docPr = inline.addNewDocPr();
            docPr.setId(1);
            docPr.setName("ChartInTable");
            
            // 4. 处理图表的权限标记
            if (showLock && valueNode.has("unlock") && valueNode.get("unlock").asBoolean()) {
                // 为图表添加权限保护
                XWPFParagraph lastParagraph = cellParagraph;
                if (lastParagraph != null && !lastParagraph.getRuns().isEmpty()) {
                    XWPFRun lastRun = run;
                    SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1);
                    String uniqueId = generator.nextId();
                    WordUtils.insertPermissionNodes(lastParagraph, lastRun, uniqueId);
                    LOG.info("为表格中的饼图添加权限保护,ID: {}", uniqueId);
                }
            }

            // 5. 轻量级清理:只清理表格后的重复空段落,不删除表格前的内容
            TableChartCleanupUtil.cleanupTableAfterCharts(document);
            
        } catch (Exception e) {
            LOG.error("处理图表时发生错误: " + e.getMessage(), e);
        }
    }
    
    /**
     * 处理饼图
     */
    public static void handlePieChart(XWPFParagraph cellParagraph, JsonNode valueNode, JsonNode chartConfig, 
                                      int dpi, boolean showLock, int width, int height) {
        try {
            // 检查是否已经在表格单元格中
            if (cellParagraph == null) {
                LOG.warn("单元格段落为空,跳过饼图创建");
                return;
            }
            
            XWPFDocument document = cellParagraph.getDocument();
            XWPFRun run = cellParagraph.createRun();
            CTInline inline = run.getCTR().addNewDrawing().addNewInline();
            
            int widthEMU = (int) (width * Units.EMU_PER_PIXEL);
            int heightEMU = (int) (height * Units.EMU_PER_PIXEL);
            
            // 1. 先创建饼图并渲染数据
            XWPFChart chart = createPieChartInCell(document, chartConfig, widthEMU, heightEMU);
            
            // 2. 获取图表关系ID并嵌入inline
            String chartRelId = document.getRelationId(chart);
            String chartXml =
                    "<a:graphic xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\">" +
                            "<a:graphicData uri=\"http://schemas.openxmlformats.org/drawingml/2006/chart\">" +
                            "<c:chart xmlns:c=\"http://schemas.openxmlformats.org/drawingml/2006/chart\" " +
                            "xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\" r:id=\"" + chartRelId + "\"/>" +
                            "</a:graphicData>" +
                            "</a:graphic>";
            XmlToken xmlToken = XmlToken.Factory.parse(chartXml);
            inline.set(xmlToken);

            // 3. 最后设置inline属性,确保图表完全限制在单元格内
            inline.setDistT(0);
            inline.setDistB(0);
            inline.setDistL(0);
            inline.setDistR(0);
            CTPositiveSize2D extent = inline.addNewExtent();
            extent.setCx(widthEMU);
            extent.setCy(heightEMU);
            CTNonVisualDrawingProps docPr = inline.addNewDocPr();
            docPr.setId(1);
            docPr.setName("PieChartInTable");
            
            // 4. 处理图表的权限标记
            if (showLock && valueNode.has("unlock") && valueNode.get("unlock").asBoolean()) {
                // 为图表添加权限保护
                XWPFParagraph lastParagraph = cellParagraph;
                if (lastParagraph != null && !lastParagraph.getRuns().isEmpty()) {
                    XWPFRun lastRun = run;
                    SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1);
                    String uniqueId = generator.nextId();
                    WordUtils.insertPermissionNodes(lastParagraph, lastRun, uniqueId);
                    LOG.info("为表格中的饼图添加权限保护,ID: {}", uniqueId);
                }
            }
            
            // 5. 轻量级清理:只清理表格后的重复空段落,不删除表格前的内容
            TableChartCleanupUtil.cleanupTableAfterCharts(document);
            
        } catch (Exception e) {
            LOG.error("处理饼图时发生错误: " + e.getMessage(), e);
        }
    }
    
    /**
     * 在单元格中创建并渲染柱状图/折线图
     */
    private static XWPFChart createChartInCell(XWPFDocument document, JsonNode chartConfig, int widthEMU, int heightEMU) 
            throws IOException, InvalidFormatException {
        
        // 创建图表
        XWPFChart chart = document.createChart(widthEMU, heightEMU);
        
        // 解析基本配置
        String chartType = chartConfig.get("type").asText();
        String title = chartConfig.get("title").asText();
        String xAxisTitle = chartConfig.has("xAxisTitle") ? chartConfig.get("xAxisTitle").asText() : "";
        String yAxisTitle = chartConfig.has("yAxisTitle") ? chartConfig.get("yAxisTitle").asText() : "";
        
        boolean showTitle = chartConfig.has("showTitle") ? chartConfig.get("showTitle").asBoolean() : true;
        boolean showGrid = chartConfig.has("showGrid") ? chartConfig.get("showGrid").asBoolean() : true;
        boolean showLegend = chartConfig.has("showLegend") ? chartConfig.get("showLegend").asBoolean() : true;
        boolean showDataLabel = chartConfig.has("showDataLabel") ? chartConfig.get("showDataLabel").asBoolean() : false;
        boolean showAxisLabel = chartConfig.has("showAxisLabel") ? chartConfig.get("showAxisLabel").asBoolean() : true;
        boolean showAxis = chartConfig.has("showAxis") ? chartConfig.get("showAxis").asBoolean() : true;
        
        // 解析图例
        JsonNode legendNode = chartConfig.get("legend");
        List<String> legends = new ArrayList<>();
        for (JsonNode legend : legendNode) {
            legends.add(legend.asText());
        }
        
        // 解析颜色
        List<String> colors = new ArrayList<>();
        JsonNode colorsNode = chartConfig.get("colors");
        if (colorsNode != null) {
            for (JsonNode color : colorsNode) {
                colors.add(color.asText());
            }
        }
        
        // 解析数据
        JsonNode valueNode = chartConfig.get("valueList");
        List<String> categories = new ArrayList<>();
        List<List<Double>> seriesData = new ArrayList<>();
        
        // 初始化系列数据列表
        for (int i = 0; i < legends.size(); i++) {
            seriesData.add(new ArrayList<>());
        }
        
        // 解析每个数据点
        for (JsonNode dataPoint : valueNode) {
            String name = dataPoint.get("name").asText();
            categories.add(name);
            
            JsonNode values = dataPoint.get("value");
            for (int i = 0; i < values.size() && i < legends.size(); i++) {
                seriesData.get(i).add(values.get(i).asDouble());
            }
        }
        
        // 转换为数组
        String[] categoryArray = categories.toArray(new String[0]);
        List<Double[]> seriesArrays = new ArrayList<>();
        for (List<Double> series : seriesData) {
            seriesArrays.add(series.toArray(new Double[0]));
        }
        
        // 创建图表
        ChartTypes poiChartType = "chart_bar".equals(chartType) ? ChartTypes.BAR : ChartTypes.LINE;
        renderChartData(chart, title, poiChartType, categoryArray, seriesArrays, legends, colors,
                xAxisTitle, yAxisTitle, "chart_bar".equals(chartType), showGrid, showDataLabel,
                showTitle, showLegend, showAxis, showAxisLabel);
        
        return chart;
    }
    
    /**
     * 在单元格中创建并渲染饼图
     */
    private static XWPFChart createPieChartInCell(XWPFDocument document, JsonNode chartConfig, int widthEMU, int heightEMU) 
            throws IOException, InvalidFormatException {
        
        // 创建图表
        XWPFChart chart = document.createChart(widthEMU, heightEMU);
        
        // 解析基本配置
        String title = chartConfig.get("title").asText();
        boolean showTitle = chartConfig.has("showTitle") ? chartConfig.get("showTitle").asBoolean() : true;
        boolean showLegend = chartConfig.has("showLegend") ? chartConfig.get("showLegend").asBoolean() : true;
        boolean showDataLabel = chartConfig.has("showDataLabel") ? chartConfig.get("showDataLabel").asBoolean() : false;
        
        // 解析颜色配置
        List<String> colors = new ArrayList<>();
        JsonNode colorsNode = chartConfig.get("colors");
        if (colorsNode != null) {
            for (JsonNode color : colorsNode) {
                colors.add(color.asText());
            }
        }
        
        // 解析饼图数据
        JsonNode valueNode = chartConfig.has("valueList") ? chartConfig.get("valueList") : chartConfig.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);
            }
        }
        
        // 转换为数组
        String[] categoryArray = categories.toArray(new String[0]);
        Double[] valueArray = values.toArray(new Double[0]);
        
        // 渲染饼图数据
        renderPieChartData(chart, title, categoryArray, valueArray, colors,
                showDataLabel, showTitle, showLegend);
        
        return chart;
    }
    
    /**
     * 渲染柱状图/折线图数据
     */
    private static void renderChartData(XWPFChart chart,
                                        String chartTitle,
                                        ChartTypes chartType,
                                        String[] categories,
                                        List<Double[]> seriesDataList,
                                        List<String> legends,
                                        List<String> colors,
                                        String xAxisTitle,
                                        String yAxisTitle,
                                        boolean isBarChart,
                                        boolean showGridlines,
                                        boolean showDataLabels,
                                        boolean showTitle,
                                        boolean showLegend,
                                        boolean showAxis,
                                        boolean showAxisLabel) throws IOException, InvalidFormatException {
        
        // 填充嵌入的Excel数据
        populateEmbeddedExcelData(chart, categories, seriesDataList, legends);
        
        // 设置图表标题
        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();
            }
        }
        
        // 设置X轴
        XDDFCategoryAxis bottomAxis = chart.createCategoryAxis(AxisPosition.BOTTOM);
        if (showAxisLabel) {
            bottomAxis.setTitle(xAxisTitle);
        }
        if (showAxis) {
            bottomAxis.setMajorTickMark(AxisTickMark.OUT);
        } else {
            bottomAxis.setMajorTickMark(AxisTickMark.NONE);
            bottomAxis.setVisible(false);
        }
        
        bottomAxis.setTickLabelPosition(AxisTickLabelPosition.LOW);
        bottomAxis.setMajorUnit(1.0);
        
        // 设置Y轴
        XDDFValueAxis leftAxis = chart.createValueAxis(AxisPosition.LEFT);
        if (showAxisLabel) {
            leftAxis.setTitle(yAxisTitle);
        }
        if (!showAxis) {
            leftAxis.setVisible(false);
        }
        
        leftAxis.setCrosses(AxisCrosses.AUTO_ZERO);
        leftAxis.setMinimum(0.0);
        
        try {
            leftAxis.setCrossBetween(AxisCrossBetween.BETWEEN);
        } catch (Exception e) {
            LOG.warn("警告:无法设置Y轴交叉位置:{}", e.getMessage());
        }
        
        // 设置网格线
        XDDFShapeProperties major = leftAxis.getOrAddMajorGridProperties();
        XDDFShapeProperties minor = leftAxis.getOrAddMinorGridProperties();
        if (showGridlines) {
            major.setLineProperties(
                    new XDDFLineProperties(
                            new XDDFSolidFillProperties(XDDFColor.from(new byte[]{(byte) 200, (byte) 200, (byte) 200}))
                    )
            );
            minor.setLineProperties(
                    new XDDFLineProperties(
                            new XDDFSolidFillProperties(XDDFColor.from(new byte[]{(byte) 240, (byte) 240, (byte) 240}))
                    )
            );
        } else {
            major.setLineProperties(
                    new XDDFLineProperties(
                            new XDDFSolidFillProperties(XDDFColor.from(new byte[]{(byte) 255, (byte) 255, (byte) 255}))
                    )
            );
            minor.setLineProperties(
                    new XDDFLineProperties(
                            new XDDFSolidFillProperties(XDDFColor.from(new byte[]{(byte) 255, (byte) 255, (byte) 255}))
                    )
            );
        }
        
        // 使用Excel工作表数据作为数据源
        XDDFCategoryDataSource categoryDataSource = createCategoryDataSourceFromExcel(chart, categories.length);
        
        // 构建图表数据
        XDDFChartData data = chart.createData(chartType, bottomAxis, leftAxis);
        
        // 设置柱子方向和间隙(只对柱状图生效)
        if (isBarChart) {
            XDDFBarChartData barData = (XDDFBarChartData) data;
            barData.setBarDirection(BarDirection.COL);
            barData.setGapWidth(150);
            barData.setOverlap((byte) -10);
        }
        
        // 动态添加所有系列
        for (int i = 0; i < seriesDataList.size() && i < legends.size(); i++) {
            XDDFNumericalDataSource<Double> seriesDataSource = createNumericalDataSourceFromExcel(chart, i + 1, categories.length);
            XDDFChartData.Series series = data.addSeries(categoryDataSource, seriesDataSource);
            series.setTitle(legends.get(i), null);
            setGenericDataLabels(series, chartType, showDataLabels, seriesDataList.get(i));
            
            // 设置系列颜色
            if (colors != null && !colors.isEmpty()) {
                String color = colors.get(i % colors.size());
                setSeriesColor(series, chartType, color);
            }
        }
        
        // 绘制图表
        chart.plot(data);
    }
    
    /**
     * 渲染饼图数据
     */
    private static void renderPieChartData(XWPFChart chart,
                                           String chartTitle,
                                           String[] categories,
                                           Double[] values,
                                           List<String> colors,
                                           boolean showDataLabels,
                                           boolean showTitle,
                                           boolean showLegend) throws IOException, InvalidFormatException {
        
        // 填充嵌入的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);
    }
    
    // 以下是复用MixedChartRendererUtil和PieChartRendererUtil中的辅助方法
    private static void populateEmbeddedExcelData(XWPFChart chart, String[] categories, List<Double[]> seriesDataList, List<String> legends) {
        try {
            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("ChartData");
                
                if (workbook.getNumberOfSheets() > 0) {
                    workbook.setSheetName(0, "ChartData");
                }
                
                // 清空现有数据
                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("");
                
                for (int i = 0; i < legends.size() && i < seriesDataList.size(); i++) {
                    headerRow.createCell(i + 1).setCellValue(legends.get(i));
                }
                
                // 填充数据行
                for (int rowIndex = 0; rowIndex < categories.length; rowIndex++) {
                    org.apache.poi.ss.usermodel.Row dataRow = sheet.createRow(rowIndex + 1);
                    dataRow.createCell(0).setCellValue(categories[rowIndex]);
                    
                    for (int seriesIndex = 0; seriesIndex < seriesDataList.size() && seriesIndex < legends.size(); seriesIndex++) {
                        Double[] seriesData = seriesDataList.get(seriesIndex);
                        if (rowIndex < seriesData.length && seriesData[rowIndex] != null) {
                            dataRow.createCell(seriesIndex + 1).setCellValue(seriesData[rowIndex]);
                        } else {
                            dataRow.createCell(seriesIndex + 1).setCellValue(0.0);
                        }
                    }
                }
                
                // 自动调整列宽
                for (int i = 0; i <= legends.size(); i++) {
                    sheet.autoSizeColumn(i);
                }
                
                org.apache.poi.ss.usermodel.Name dataRange = workbook.createName();
                dataRange.setNameName("ChartDataRange");
                String rangeFormula = "ChartData!$A$1:$" +
                        (char) ('A' + legends.size()) + "$" + (categories.length + 1);
                dataRange.setRefersToFormula(rangeFormula);
                
            }
        } catch (Exception e) {
            LOG.warn("警告:填充嵌入Excel数据时出错:{}", e.getMessage());
        }
    }
    
    private static void populateEmbeddedExcelDataForPie(XWPFChart chart, String[] categories, Double[] values) {
        try {
            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);
            }
        } catch (Exception e) {
            LOG.warn("警告:填充饼图嵌入Excel数据时出错:{}", e.getMessage());
        }
    }
    
    private static XDDFCategoryDataSource createCategoryDataSourceFromExcel(XWPFChart chart, int categoryCount) {
        try {
            return XDDFDataSourcesFactory.fromStringCellRange(chart.getWorkbook().getSheetAt(0),
                    new org.apache.poi.ss.util.CellRangeAddress(1, categoryCount, 0, 0));
        } catch (Exception e) {
            LOG.warn("警告:无法创建Excel分类数据源,使用默认数据源:{}", e.getMessage());
            String[] defaultCategories = new String[categoryCount];
            for (int i = 0; i < categoryCount; i++) {
                defaultCategories[i] = "类别" + (i + 1);
            }
            return XDDFDataSourcesFactory.fromArray(defaultCategories);
        }
    }
    
    private static XDDFNumericalDataSource<Double> createNumericalDataSourceFromExcel(XWPFChart chart, int columnIndex, int dataCount) {
        try {
            return XDDFDataSourcesFactory.fromNumericCellRange(chart.getWorkbook().getSheetAt(0),
                    new org.apache.poi.ss.util.CellRangeAddress(1, dataCount, columnIndex, columnIndex));
        } catch (Exception e) {
            LOG.warn("警告:无法创建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);
        }
    }
    
    private static XDDFCategoryDataSource createCategoryDataSourceFromExcelForPie(XWPFChart chart, int categoryCount) {
        try {
            return XDDFDataSourcesFactory.fromStringCellRange(chart.getWorkbook().getSheetAt(0),
                    new org.apache.poi.ss.util.CellRangeAddress(1, categoryCount, 0, 0));
        } catch (Exception e) {
            LOG.warn("警告:无法创建饼图Excel分类数据源,使用默认数据源:{}", e.getMessage());
            String[] defaultCategories = new String[categoryCount];
            for (int i = 0; i < categoryCount; i++) {
                defaultCategories[i] = "分类" + (i + 1);
            }
            return XDDFDataSourcesFactory.fromArray(defaultCategories);
        }
    }
    
    private static XDDFNumericalDataSource<Double> createNumericalDataSourceFromExcelForPie(XWPFChart chart, int dataCount) {
        try {
            return XDDFDataSourcesFactory.fromNumericCellRange(chart.getWorkbook().getSheetAt(0),
                    new org.apache.poi.ss.util.CellRangeAddress(1, dataCount, 1, 1));
        } catch (Exception e) {
            LOG.warn("警告:无法创建饼图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);
        }
    }
    
    private static void setGenericDataLabels(XDDFChartData.Series series, ChartTypes chartType, boolean showDataLabels, Double[] data) {
        if (!showDataLabels) {
            if (chartType == ChartTypes.BAR) {
                CTBarSer ctSer = ((XDDFBarChartData.Series) series).getCTBarSer();
                if (ctSer.isSetDLbls()) ctSer.unsetDLbls();
            } else if (chartType == ChartTypes.LINE) {
                CTLineSer ctSer = ((XDDFLineChartData.Series) series).getCTLineSer();
                if (ctSer.isSetDLbls()) ctSer.unsetDLbls();
            }
            return;
        }
        
        if (chartType == ChartTypes.BAR) {
            CTBarSer ctSer = ((XDDFBarChartData.Series) series).getCTBarSer();
            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.addNewDLblPos().setVal(org.openxmlformats.schemas.drawingml.x2006.chart.STDLblPos.OUT_END);
            for (int i = 0; i < data.length; i++) {
                if (data[i] != null) {
                    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);
                    lbl.addNewDLblPos().setVal(org.openxmlformats.schemas.drawingml.x2006.chart.STDLblPos.OUT_END);
                }
            }
        } else if (chartType == ChartTypes.LINE) {
            CTLineSer ctSer = ((XDDFLineChartData.Series) series).getCTLineSer();
            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.addNewDLblPos().setVal(org.openxmlformats.schemas.drawingml.x2006.chart.STDLblPos.T);
            
            for (int i = 0; i < data.length; i++) {
                if (data[i] != null) {
                    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);
                    lbl.addNewDLblPos().setVal(org.openxmlformats.schemas.drawingml.x2006.chart.STDLblPos.T);
                }
            }
        }
    }
    
    private static void setSeriesColor(XDDFChartData.Series series, ChartTypes chartType, String colorHex) {
        try {
            String hex = colorHex.startsWith("#") ? colorHex.substring(1) : colorHex;
            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);
            
            if (chartType == ChartTypes.BAR && series instanceof XDDFBarChartData.Series) {
                XDDFBarChartData.Series barSeries = (XDDFBarChartData.Series) series;
                XDDFShapeProperties shapeProperties = new XDDFShapeProperties();
                shapeProperties.setFillProperties(fillProperties);
                barSeries.setShapeProperties(shapeProperties);
            } else if (chartType == ChartTypes.LINE && series instanceof XDDFLineChartData.Series) {
                XDDFLineChartData.Series lineSeries = (XDDFLineChartData.Series) series;
                XDDFShapeProperties shapeProperties = new XDDFShapeProperties();
                XDDFLineProperties lineProperties = new XDDFLineProperties();
                lineProperties.setFillProperties(fillProperties);
                shapeProperties.setLineProperties(lineProperties);
                lineSeries.setShapeProperties(shapeProperties);
                shapeProperties.setFillProperties(fillProperties);
            }
        } catch (Exception e) {
            LOG.warn("警告:无法解析颜色 {},将使用默认颜色。错误:{}", colorHex, e.getMessage());
        }
    }
    
    private static void setPieDataLabels(XDDFPieChartData.Series series, boolean showDataLabels, Double[] values) {
        if (!showDataLabels) {
            org.openxmlformats.schemas.drawingml.x2006.chart.CTPieSer ctSer = series.getCTPieSer();
            if (ctSer.isSetDLbls()) {
                ctSer.unsetDLbls();
            }
            return;
        }
        
        try {
            org.openxmlformats.schemas.drawingml.x2006.chart.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);
                }
            }
        } catch (Exception e) {
            LOG.warn("警告:设置饼图数据标签时出错:{}", e.getMessage());
        }
    }
    
    private static void setPieSeriesColors(XDDFPieChartData.Series series, List<String> colors, int pointCount) {
        if (colors == null || colors.isEmpty()) {
            LOG.warn("未提供颜色配置,将使用默认颜色");
            return;
        }
        
        try {
            for (int i = 0; i < pointCount; i++) {
                String colorHex = colors.get(i % colors.size());
                setPieSliceColor(series, i, colorHex);
            }
        } catch (Exception e) {
            LOG.warn("警告:设置饼图颜色时出错:{}", e.getMessage());
        }
    }
    
    private static void setPieSliceColor(XDDFPieChartData.Series series, int pointIndex, String colorHex) {
        try {
            String hex = colorHex.startsWith("#") ? colorHex.substring(1) : colorHex;
            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);
            
            org.openxmlformats.schemas.drawingml.x2006.chart.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();
            }
            
            dPt.getSpPr().getSolidFill().getSrgbClr().setVal(new byte[]{(byte) r, (byte) g, (byte) b});
            
        } catch (Exception e) {
            LOG.warn("警告:无法解析颜色 {} 用于数据点 {},将使用默认颜色。错误:{}", colorHex, pointIndex, e.getMessage());
        }
    }

    /**
     * 轻量级清理:只清理表格后的重复空段落和重复图表,不删除表格前的内容
     */
    private static void cleanupTableAfterEmptyParagraphs(XWPFDocument document) {
        try {
            List<IBodyElement> bodyElements = document.getBodyElements();
            int tableIndex = -1;
            for (int i = 0; i < bodyElements.size(); i++) {
                if (bodyElements.get(i) instanceof XWPFTable) {
                    tableIndex = i;
                    break;
                }
            }

            if (tableIndex != -1) {
                // 从后往前遍历,确保删除表格后的空段落和重复图表
                for (int i = bodyElements.size() - 1; i > tableIndex; i--) {
                    IBodyElement element = bodyElements.get(i);
                    if (element instanceof XWPFParagraph) {
                        XWPFParagraph p = (XWPFParagraph) element;
                        String text = p.getText().trim();
                        
                        // 检查段落是否包含图表
                        boolean hasChart = false;
                        for (XWPFRun run : p.getRuns()) {
                            if (run.getEmbeddedPictures() != null && !run.getEmbeddedPictures().isEmpty()) {
                                hasChart = true;
                                break;
                            }
                            // 检查是否有图表相关的XML内容
                            if (run.getCTR().getDrawingList() != null && !run.getCTR().getDrawingList().isEmpty()) {
                                hasChart = true;
                                break;
                            }
                        }
                        
                        // 删除条件:
                        // 1. 完全空的段落
                        // 2. 包含图表但文本为空的段落(可能是重复的图表)
                        if ((text.isEmpty() && p.getRuns().isEmpty()) || 
                            (hasChart && text.isEmpty())) {
                            LOG.info("删除表格后的段落,索引: {},包含图表: {},文本: '{}'", i, hasChart, text);
                            document.removeBodyElement(i);
                        }
                    }
                }
            }
        } catch (Exception e) {
            LOG.warn("轻量级清理表格后的空段落时发生错误: " + e.getMessage());
        }
    }
}

TableChartCleanupUtil 表格图表清理工具类:专门用于清理表格后的重复图表,而不影响表格外的正常图表

package com.gemantic.gpt.util;

import java.util.ArrayList;
import java.util.List;

import org.apache.poi.xwpf.usermodel.IBodyElement;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 表格图表清理工具类:专门用于清理表格后的重复图表,而不影响表格外的正常图表
 * 
 * @Description: 解决TableChartUtil中cleanupTableAfterEmptyParagraphs方法过度清理的问题
 * @Auther: Wangtianming
 * @Date: 2024/12/19
 */
public class TableChartCleanupUtil {
    
    private static final Logger LOG = LoggerFactory.getLogger(TableChartCleanupUtil.class);
    
    /**
     * 智能清理表格后的重复图表
     * 只删除紧跟在表格后面的重复图表,不影响表格外的正常图表
     * 
     * @param document Word文档对象
     */
    public static void cleanupTableAfterCharts(XWPFDocument document) {
        try {
            List<IBodyElement> bodyElements = document.getBodyElements();
            
            // 找到所有表格的位置
            List<Integer> tableIndices = new ArrayList<>();
            for (int i = 0; i < bodyElements.size(); i++) {
                if (bodyElements.get(i) instanceof XWPFTable) {
                    tableIndices.add(i);
                }
            }
            
            if (tableIndices.isEmpty()) {
                return; // 没有表格,无需清理
            }
            
            // 从后往前遍历,只清理表格后的重复图表
            for (int i = bodyElements.size() - 1; i >= 0; i--) {
                IBodyElement element = bodyElements.get(i);
                
                // 只处理段落元素
                if (!(element instanceof XWPFParagraph)) {
                    continue;
                }
                
                XWPFParagraph p = (XWPFParagraph) element;
                String text = p.getText().trim();
                
                // 检查段落是否包含图表
                boolean hasChart = isParagraphContainsChart(p);
                
                // 检查这个段落是否紧跟在表格后面
                boolean isAfterTable = isParagraphAfterTable(i, tableIndices);
                
                // 删除条件:
                // 1. 段落紧跟在表格后面
                // 2. 段落包含图表
                // 3. 段落文本为空
                // 4. 段落只包含图表,没有其他内容
                if (isAfterTable && hasChart && text.isEmpty() && isParagraphOnlyContainsChart(p)) {
                    LOG.info("删除表格后的重复图表段落,索引: {},文本: '{}'", i, text);
                    document.removeBodyElement(i);
                }
            }
        } catch (Exception e) {
            LOG.warn("清理表格后的重复图表时发生错误: " + e.getMessage());
        }
    }
    
    /**
     * 检查段落是否包含图表
     */
    private static boolean isParagraphContainsChart(XWPFParagraph paragraph) {
        if (paragraph == null || paragraph.getRuns().isEmpty()) {
            return false;
        }
        
        for (XWPFRun run : paragraph.getRuns()) {
            // 检查是否有嵌入的图片
            if (run.getEmbeddedPictures() != null && !run.getEmbeddedPictures().isEmpty()) {
                return true;
            }
            // 检查是否有图表相关的XML内容
            if (run.getCTR().getDrawingList() != null && !run.getCTR().getDrawingList().isEmpty()) {
                return true;
            }
            // 检查是否有图表关系
            if (run.getCTR().getDrawingList() != null) {
                for (org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDrawing drawing : run.getCTR().getDrawingList()) {
                    if (drawing.getInlineList() != null && !drawing.getInlineList().isEmpty()) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
    
    /**
     * 检查段落是否只包含图表,没有其他文本内容
     */
    private static boolean isParagraphOnlyContainsChart(XWPFParagraph paragraph) {
        if (paragraph == null) {
            return false;
        }
        
        String text = paragraph.getText().trim();
        boolean hasChart = isParagraphContainsChart(paragraph);
        
        // 如果段落有图表但没有文本内容,则认为只包含图表
        return hasChart && text.isEmpty();
    }
    
    /**
     * 检查段落是否紧跟在表格后面
     */
    private static boolean isParagraphAfterTable(int paragraphIndex, List<Integer> tableIndices) {
        // 找到段落前面的最近表格
        int nearestTableIndex = -1;
        for (int tableIndex : tableIndices) {
            if (tableIndex < paragraphIndex && tableIndex > nearestTableIndex) {
                nearestTableIndex = tableIndex;
            }
        }
        
        // 如果段落紧跟在表格后面(索引相差1),则认为是表格后的段落
        return nearestTableIndex != -1 && (paragraphIndex - nearestTableIndex) == 1;
    }
}


网站公告

今日签到

点亮在社区的每一天
去签到