javaapache-poiapache-poi-4

How do you add an XDDF Chart to a specific paragraph run (i.e. a cell in a table)? POI 4.0.1


I want to add a chart to a specific table cell within an XWPFDocument. I want the chart in the table cell so that I can ensure alignment with other elements I will add afterwards. So then,

Already Tried:

  1. Creating a chart mirroring the code found in XWPFDocument.createChart()
  2. Using the XWPFRun.addChart() with the RelationPart.getRelationship.getId() I used from
  3. Using document.createChart() after getting to the specific location of the table
  4. Trying to make the chart -> XDDFDrawing and adding that to the run through run.getCTR.addDrawing... I don't think it goes this way?

/code example

// Create a document with some initial text
XWPFDocument document = new XWPFDocument();
XWPFParagraph tmpParagraph = document.createParagraph();
XWPFRun tmpRun = tmpParagraph.createRun();
tmpRun.setText("text");
tmpRun.setFontSize(18);

// Try making the chart
// the same code as here, https://stackoverflow.com/questions/55192804/how-do-i-add-a-second-line-with-a-second-axis-to-an-xddfchart-in-poi-4-0-1
try{
    String[] categories = new String[]{"1","2","3","4","5","6","7","8","9"};
    Double[] values1 = new Double[]{1d,2d,3d,4d,5d,6d,7d,8d,9d};
    Double[] values2 = new Double[]{200d,300d,400d,500d,600d,700d,800d,900d,1000d};

    // create the chart
    // XWPFChart chart = document.createChart(7* Units.EMU_PER_CENTIMETER, 7*Units.EMU_PER_CENTIMETER);

    // Try to make a chart stand alone
    // using the same code as here, 
    // https://svn.apache.org/viewvc/poi/tags/REL_4_0_1/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFDocument.java?view=markup

    int chartNumber = document.getCharts().size();
    POIXMLDocumentPart.RelationPart rp = document.createRelationship(XWPFRelation.CHART, XWPFFactory.getInstance(), chartNumber, false);
    XWPFChart chart = rp.getDocumentPart();
    chart.setChartIndex(chartNumber);
    chart.setChartBoundingBox(7* Units.EMU_PER_CENTIMETER, 7*Units.EMU_PER_CENTIMETER);
    document.getCharts().add(chart);


    // This stuff to make the chart is not part of the question
        // create data sources
        int numOfPoints = categories.length;
        String categoryDataRange = chart.formatRange(new CellRangeAddress(1, numOfPoints, 0, 0));
        String valuesDataRange1 = chart.formatRange(new CellRangeAddress(1, numOfPoints, 1, 1));
        String valuesDataRange2 = chart.formatRange(new CellRangeAddress(1, numOfPoints, 2, 2));
        XDDFDataSource<String> categoriesData = XDDFDataSourcesFactory.fromArray(categories, categoryDataRange, 0);
        XDDFNumericalDataSource<Double> valuesData1 = XDDFDataSourcesFactory.fromArray(values1, valuesDataRange1, 1);
        XDDFNumericalDataSource<Double> valuesData2 = XDDFDataSourcesFactory.fromArray(values2, valuesDataRange2, 2);

        // first line chart
        XDDFCategoryAxis bottomAxis = chart.createCategoryAxis(AxisPosition.BOTTOM);
        XDDFValueAxis leftAxis = chart.createValueAxis(AxisPosition.LEFT);
        leftAxis.setCrosses(AxisCrosses.AUTO_ZERO);
        XDDFChartData data = chart.createData(ChartTypes.LINE, bottomAxis, leftAxis);
        XDDFChartData.Series series = data.addSeries(categoriesData, valuesData1);
        chart.plot(data);

        solidLineSeries(data, 0, PresetColor.BLUE);

        // second line chart
        // bottom axis must be there but must not be visible
        bottomAxis = chart.createCategoryAxis(AxisPosition.BOTTOM);
        bottomAxis.setVisible(false);

        XDDFValueAxis rightAxis = chart.createValueAxis(AxisPosition.RIGHT);
        rightAxis.setCrosses(AxisCrosses.MAX);

        // set correct cross axis
        bottomAxis.crossAxis(rightAxis);
        rightAxis.crossAxis(bottomAxis);

        data = chart.createData(ChartTypes.LINE, bottomAxis, rightAxis);
        series = data.addSeries(categoriesData, valuesData2);
        chart.plot(data);

        // correct the id and order, must not be 0 again because there is one line series already
        chart.getCTChart().getPlotArea().getLineChartArray(1).getSerArray(0).getIdx().setVal(1);
        chart.getCTChart().getPlotArea().getLineChartArray(1).getSerArray(0).getOrder().setVal(1);

        solidLineSeries(data, 0, PresetColor.RED);


// End of extra stuff
// Back to question

    // Add the chart by relation id
    XWPFParagraph p2 = document.createParagraph();
    XWPFRun r2 = p2.createRun();
    r2.addChart(rp.getRelationship().getId());

    // Add a new run to try to add a new drawing?
    XWPFRun r3 = p2.createRun();
    CTDrawing drawing = r3.getCTR().addNewDrawing();
    ????

}catch(Exception e){}

When I add the chart via r2.addChart(), nothing shows up? So maybe I didn't create the chart correctly? Or I didn't add it to the run correctly?

Is it possible that the chart can be transformed to a drawing?

This shows the XML I am trying to mimic (copied from the other image)

  1. Paragraph
  2. Run
  3. Drawing?
  4. Chart?

This is the expected output


Solution

  • As often apache poi makes it really hard to extend their code because of weird decisions about what methods are protected or private. In this case it lacks a method public XWPFChart createChart(int width, int height, XWPFRun run) in XWPFDocument since the existing methods always puts the chart into a new created run in a new created paragraph in document body. But simply extending XWPFDocument is nearly impossible because needed methods are protected or private.

    Simplest approach I have found is to first put the chart in first paragraph of the document using document.createChart(). Then remove that first paragraph. The chart part remains (at least using apache poi 4.1.0). Then attach the chart part new at wanted text run. But even this is not as easy as it could be since XWPFChart.attach also is protected. So using java.lang.reflect is needed.

    Complete example:

    import java.io.*;
    
    import org.apache.poi.xwpf.usermodel.*;
    
    import org.apache.poi.ss.util.CellRangeAddress;
    import org.apache.poi.util.Units;
    
    import org.apache.poi.xddf.usermodel.*;
    import org.apache.poi.xddf.usermodel.chart.*;
    
    public class CreateWordXDDFChartTwoLinesInTable {
    
     public static void main(String[] args) throws Exception {
      try (XWPFDocument document = new XWPFDocument()) {
    
       // create the data
       String[] categories = new String[]{"1","2","3","4","5","6","7","8","9"};
       Double[] values1 = new Double[]{1d,2d,3d,4d,5d,6d,7d,8d,9d};
       Double[] values2 = new Double[]{200d,300d,400d,500d,600d,700d,800d,900d,1000d};
    
       // create the chart
       // this also puts the chart into a run in a new created paragraph
       XWPFChart chart = createChart(document, categories, values1, values2);
       // remove the first paragraph since we need the chart being elsewhere
       document.removeBodyElement(0);
    
       XWPFParagraph paragraph = document.createParagraph();
       XWPFRun run = paragraph.createRun();
       run.setText("First paragraph having first text run.");
    
       // create the table
       XWPFTable table = document.createTable(1,2);
       table.setWidth("100%");
       // create first run in first table cell
       paragraph = table.getRow(0).getCell(0).getParagraphArray(0);
       run = paragraph.createRun();
       // attach the chart here
       java.lang.reflect.Method attach = XWPFChart.class.getDeclaredMethod("attach", String.class, XWPFRun.class);
       attach.setAccessible(true);
       attach.invoke(chart, document.getRelationId(chart), run);
       chart.setChartBoundingBox(7*Units.EMU_PER_CENTIMETER, 7*Units.EMU_PER_CENTIMETER);
    
       // set text in second table cell
       paragraph = table.getRow(0).getCell(1).getParagraphArray(0);
       run = paragraph.createRun();
       run.setText("Other text goes in the 2");
       run = paragraph.createRun();
       run.setSubscript(VerticalAlign.SUPERSCRIPT);
       run.setText("nd");
       run = paragraph.createRun();
       run.setText(" cell.");
    
       paragraph = document.createParagraph();
       run = paragraph.createRun();
       run.setText("Lorem ipsum...");
    
       // Write the output to a file
       try (FileOutputStream fileOut = new FileOutputStream("CreateWordXDDFChartTwoLinesInTable.docx")) {
        document.write(fileOut);
       }
      }
     }
    
     private static XWPFChart createChart(XWPFDocument document, 
       String[] categories, Double[] values1, Double[] values2) throws Exception {
    
       // create the chart
       XWPFChart chart = document.createChart();
    
       // create data sources
       int numOfPoints = categories.length;
       String categoryDataRange = chart.formatRange(new CellRangeAddress(1, numOfPoints, 0, 0));
       String valuesDataRange1 = chart.formatRange(new CellRangeAddress(1, numOfPoints, 1, 1));
       String valuesDataRange2 = chart.formatRange(new CellRangeAddress(1, numOfPoints, 2, 2));
       XDDFDataSource<String> categoriesData = XDDFDataSourcesFactory.fromArray(categories, categoryDataRange, 0);
       XDDFNumericalDataSource<Double> valuesData1 = XDDFDataSourcesFactory.fromArray(values1, valuesDataRange1, 1);
       XDDFNumericalDataSource<Double> valuesData2 = XDDFDataSourcesFactory.fromArray(values2, valuesDataRange2, 2);
    
       // first line chart
       XDDFCategoryAxis bottomAxis = chart.createCategoryAxis(AxisPosition.BOTTOM);
       XDDFValueAxis leftAxis = chart.createValueAxis(AxisPosition.LEFT);
       leftAxis.setCrosses(AxisCrosses.AUTO_ZERO);
       XDDFChartData data = chart.createData(ChartTypes.LINE, bottomAxis, leftAxis);
       XDDFChartData.Series series = data.addSeries(categoriesData, valuesData1);
       chart.plot(data);
    
       solidLineSeries(data, 0, PresetColor.BLUE);
    
       // second line chart
       // bottom axis must be there but must not be visible
       bottomAxis = chart.createCategoryAxis(AxisPosition.BOTTOM);
       bottomAxis.setVisible(false);
    
       XDDFValueAxis rightAxis = chart.createValueAxis(AxisPosition.RIGHT);
       rightAxis.setCrosses(AxisCrosses.MAX);
    
       // set correct cross axis
       bottomAxis.crossAxis(rightAxis);
       rightAxis.crossAxis(bottomAxis);
    
       data = chart.createData(ChartTypes.LINE, bottomAxis, rightAxis);
       series = data.addSeries(categoriesData, valuesData2);
       chart.plot(data);
    
       // correct the id and order, must not be 0 again because there is one line series already
       chart.getCTChart().getPlotArea().getLineChartArray(1).getSerArray(0).getIdx().setVal(1);
       chart.getCTChart().getPlotArea().getLineChartArray(1).getSerArray(0).getOrder().setVal(1);
    
       solidLineSeries(data, 0, PresetColor.RED);
    
       return chart;
     }
    
     private static void solidLineSeries(XDDFChartData data, int index, PresetColor color) {
      XDDFSolidFillProperties fill = new XDDFSolidFillProperties(XDDFColor.from(color));
      XDDFLineProperties line = new XDDFLineProperties();
      line.setFillProperties(fill);
      XDDFChartData.Series series = data.getSeries().get(index);
      XDDFShapeProperties properties = series.getShapeProperties();
      if (properties == null) {
       properties = new XDDFShapeProperties();
      }
      properties.setLineProperties(line);
      series.setShapeProperties(properties);
     }
    }