How do I add a second line with a SECOND AXIS to a

2019-07-29 23:49发布

问题:

I am unable to add a line on a second axis (right axis) on an existing chart. Is there a way to do this with the new implementation of Charts in POI 4.0.0/1?

Desired output will look like this (A simple excel chart with 2 axes): . The associated data to that chart as an example:

Series 1/Axis1 = [1, 2, 3, 4, 5, 6, 7, 8, 9]

Series 2/Axis2 = [200,300,400,500,600,700,800,900,1000]

Here is the code that I am trying so far in Java, it is mostly replicated from the LineChart.java example

//Initial code instantiates a document
XWPFDocument doc = new XWPFDocument();
...


//  Generate Chart
//  This was taken from the example https://svn.apache.org/repos/asf/poi/trunk/src/examples/src/org/apache/poi/xssf/usermodel/examples/LineChart.java
XWPFChart prChart = doc.createChart();

//Values 1 on the Left Axis
//Values 2 on the Right Axis
String[] categories = dates.toArray(new String[dates.size()]);
BigDecimal[] values1 = prices1.toArray(new BigDecimal[prices1.size()]);
BigDecimal[] values2 = prices2.toArray(new BigDecimal[prices2.size()]);

XDDFChartAxis bottomAxis = prChart.createCategoryAxis(AxisPosition.BOTTOM);
bottomAxis.setMajorTickMark(AxisTickMark.NONE);

XDDFValueAxis leftAxis = prChart.createValueAxis(AxisPosition.LEFT);
leftAxis.setCrosses(AxisCrosses.AUTO_ZERO);
leftAxis.setMajorTickMark(AxisTickMark.OUT);

/*
 * Is this made correctly?  
 */
XDDFValueAxis rightAxis = prChart.createValueAxis(AxisPosition.RIGHT);
rightAxis.setCrosses(AxisCrosses.MAX);
rightAxis.setMajorTickMark(AxisTickMark.IN);


final int numOfPoints = categories.length;
final String categoryDataRange = prChart.formatRange(new CellRangeAddress(1, numOfPoints, 0, 0));
final String valuesDataRange = prChart.formatRange(new CellRangeAddress(1, numOfPoints, 1, 1));
final String valuesDataRange2 = prChart.formatRange(new CellRangeAddress(1, numOfPoints, 2, 2));

final XDDFDataSource<?> categoriesData = XDDFDataSourcesFactory.fromArray(categories, categoryDataRange, 0);
final XDDFNumericalDataSource<? extends Number> valuesData = XDDFDataSourcesFactory.fromArray(values1, valuesDataRange, 1);
final XDDFNumericalDataSource<? extends Number> valuesData2 = XDDFDataSourcesFactory.fromArray(values2, valuesDataRange2, 2);

XDDFLineChartData line = (XDDFLineChartData) prChart.createData(ChartTypes.LINE, bottomAxis, leftAxis);
XDDFLineChartData.Series series1 = (XDDFLineChartData.Series) line.addSeries(categoriesData, valuesData);            
series1.setTitle("Price", null);
series1.setSmooth(true);
series1.setMarkerStyle(MarkerStyle.NONE);
solidLineSeries(series1, PresetColor.BLUE_VIOLET);

// Am I adding the rightAxis correctly here?
XDDFLineChartData line2 = (XDDFLineChartData) prChart.createData(ChartTypes.LINE, bottomAxis, rightAxis);
XDDFLineChartData.Series series2 = (XDDFLineChartData.Series) line2.addSeries(categoriesData, valuesData2);
series2.setTitle("Index", null);
series2.setSmooth(true);
series2.setMarkerStyle(MarkerStyle.NONE);
solidLineSeries(series2, PresetColor.BLACK);


prChart.plot(line);
prChart.plot(line2);        ///     <- Does this add to the same plot correctly?
prChart.displayBlanksAs(DisplayBlanks.GAP);

Running this code doesn't produce any compile errors. But I do get errors when opening the document "Problem with its' contents."

I suppose I am not adding the 2nd line and 2nd axes correctly.

Is there a way to accomplish this?

Update w. Solution Axel's solution below works perfectly. The additional info to know is exactly what was the issue.

I would also like to recognize the order in which you add to the plot, this will hopefully help others

  1. Create first set of axis
  2. Create first Line
  3. Plot first Line
  4. Create new Axis
  5. Create 2nd line
  6. Plot 2nd line
  7. Update the axis ids!

回答1:

When it comes to multiple different value axes in one chart, this is not fully implemented in XDDF until now. So we need correcting something using the low level ooxml-schemas-1.4 classes.

Needed knowledge:

In principle the series which shall be shown on second value axis are in a separate chart in the same plot area. So the series which shall be shown on second value axis needs it's own bottom axis too. But this bottom axis must be invisible.

Both the axes, the second bottom and the new right axis, must cross each other properly. This crossing apache poi does not properly until now. So we must correct here.

Because while adding to the chart, the apache poi code which adds the second line chart does not knows something about the already present line chart, it's IDs starts with 0 again. But this is wrong for an combined chart. So we need correct the id and order. It must not start with 0 again because there is a line series already in same plot area.

Complete example to be reproducible for others too:

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 CreateWordXDDFChart {

 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
   XWPFChart chart = document.createChart(15*Units.EMU_PER_CENTIMETER, 10*Units.EMU_PER_CENTIMETER);

   // 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);

   // Write the output to a file
   try (FileOutputStream fileOut = new FileOutputStream("CreateWordXDDFChart.docx")) {
    document.write(fileOut);
   }
  }
 }

 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);
 }
}