0

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,

  • How do I create an XWPF/XDDF chart without adding it the document (e.g. without using document.createChart())?

  • How do I take that chart and add it to a specific paragraph/run (e.g. the one that is created in the table cell)?

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

Olaf Kock
  • 46,930
  • 8
  • 59
  • 90

2 Answers2

2

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);
 }
}
Axel Richter
  • 56,077
  • 6
  • 60
  • 87
  • Wonderful! This works perfectly, thank you! java.lang.reflect.Method is a nice trick! – user220790b Oct 14 '19 at 15:42
  • In case it is helpful for others, I also tried creating, duplicating, and then removing the paragraph, but I got stuck with the duplication part. I tried the clone methods here, https://stackoverflow.com/questions/23112924/make-an-exact-copy-of-a-paragraph-including-all-contents-and-properties but the drawing was not duplicated. I even tried cloning every single element in the Run (inline, graphic, , graphicData, etc), but no luck there either. I'd duplicate Axel's pattern if I needed to do something like that in the future – user220790b Oct 14 '19 at 15:46
1

Here is a simple function to add an XWPFChart chart in an XWPFRun run. we can also extend the XWPFDocument with a custom java class and create a function with the access of protected variables which can be easy.

protected XWPFDocument addChartToRun(XWPFDocument document, XWPFChart oldChart, XWPFRun run) {
        try {
            //find number of charts available
            int chartNumber = 0;
            for (int i = 0; i < document.getRelations().size(); i++) {
                if (document.getRelations().get(i) instanceof XWPFChart) {
                    chartNumber++;
                }
            }

            //creating new chart from oldChart
            POIXMLDocumentPart.RelationPart rp = document.createRelationship(
                    XWPFRelation.CHART, XWPFFactory.getInstance(), chartNumber + 1, false);
            XWPFChart chart = rp.getDocumentPart();
            chart.setChartIndex(chartNumber);
            Method attach = XWPFChart.class.getDeclaredMethod("attach", String.class, XWPFRun.class);
            attach.setAccessible(true);
            attach.invoke(chart, rp.getRelationship().getId(), run);
            chart.setChartBoundingBox(200, 200);


            // copy old chart style
            if (oldChart != null) {
                chart.getCTChartSpace().setSpPr(oldChart.getCTChartSpace().getSpPr());
                chart.getCTChartSpace().setChart(oldChart.getCTChartSpace().getChart());
                chart.getChartSeries().addAll(oldChart.getChartSeries());
            } else {
                //oldChart not found
                throw new NullPointerException();
            }

            return document;
        } catch (Exception e) {
            log.error("Error while copying chart...");
        }
        return null;
    }
Bathri Nathan
  • 1,101
  • 2
  • 13
  • 17