poi-tl 源码版本1-12-2
poi-tl github源码地址
https://github.com/Sayi/poi-tl
1、修改package com.deepoove.poi.policy.reference.MultiSeriesChartTemplateRenderPolicy类
java
/*
* Copyright 2014-2026 Sayi
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.deepoove.poi.policy.reference;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.poi.xddf.usermodel.chart.AxisPosition;
import org.apache.poi.xddf.usermodel.chart.XDDFAreaChartData;
import org.apache.poi.xddf.usermodel.chart.XDDFBarChartData;
import org.apache.poi.xddf.usermodel.chart.XDDFChart;
import org.apache.poi.xddf.usermodel.chart.XDDFChartData;
import org.apache.poi.xddf.usermodel.chart.XDDFChartData.Series;
import org.apache.poi.xddf.usermodel.chart.XDDFDataSource;
import org.apache.poi.xddf.usermodel.chart.XDDFLineChartData;
import org.apache.poi.xddf.usermodel.chart.XDDFNumericalDataSource;
import org.apache.poi.xddf.usermodel.chart.XDDFScatterChartData;
import org.apache.poi.xddf.usermodel.chart.XDDFValueAxis;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xwpf.usermodel.XWPFChart;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.data.ChartMultiSeriesRenderData;
import com.deepoove.poi.data.SeriesRenderData;
import com.deepoove.poi.data.SeriesRenderData.ComboType;
import com.deepoove.poi.exception.RenderException;
import com.deepoove.poi.template.ChartTemplate;
import com.deepoove.poi.util.ChartUtils;
import com.deepoove.poi.util.ReflectionUtils;
/**
* multi series chart
*
* @author Sayi
*/
public class MultiSeriesChartTemplateRenderPolicy
extends AbstractChartTemplateRenderPolicy<ChartMultiSeriesRenderData> {
@Override
public void doRender(ChartTemplate eleTemplate, ChartMultiSeriesRenderData data, XWPFTemplate template)
throws Exception {
XWPFChart chart = eleTemplate.getChart();
List<XDDFChartData> chartSeries = chart.getChartSeries();
validate(chartSeries, data);
int totalSeriesCount = ensureSeriesCount(chart, chartSeries);
int valueCol = 1;
List<SeriesRenderData> usedSeriesDatas = new ArrayList<>();
Map<Long, XDDFValueAxis> valueAxes = ChartUtils.getValueAxes(chart);
// Count how many times each XDDFChartData type appears (for dual Y-axis charts)
Map<Class<?>, Integer> typeCounts = new HashMap<>();
for (XDDFChartData cd : chartSeries) {
typeCounts.merge(cd.getClass(), 1, Integer::sum);
}
// Track which occurrence of each type we're processing
Map<Class<?>, Integer> typeIndex = new HashMap<>();
for (XDDFChartData chartData : chartSeries) {
int orignSize = chartData.getSeriesCount();
List<SeriesRenderData> currentSeriesData = null;
if (chartSeries.size() <= 1) {
currentSeriesData = data.getSeriesDatas();
} else {
AxisPosition chartAxisPosition = determineAxisPosition(chartData, valueAxes, typeCounts, typeIndex);
currentSeriesData = obtainSeriesData(chartData.getClass(), data.getSeriesDatas(), chartAxisPosition);
}
usedSeriesDatas.addAll(currentSeriesData);
int currentSeriesSize = currentSeriesData.size();
XDDFDataSource<?> categoriesData = null;
if (chartData instanceof XDDFScatterChartData) {
categoriesData = createNumbericalDataSource(chart, toNumberArray(data.getCategories()), 0);
} else {
categoriesData = createStringDataSource(chart, data.getCategories(), 0);
}
for (int i = 0; i < currentSeriesSize; i++) {
XDDFNumericalDataSource<? extends Number> valuesData = createNumbericalDataSource(chart,
currentSeriesData.get(i).getValues(), valueCol);
XDDFChartData.Series currentSeries = null;
if (i < orignSize) {
currentSeries = chartData.getSeries(i);
valuesData.setFormatCode(currentSeries.getValuesData().getFormatCode());
currentSeries.replaceData(categoriesData, valuesData);
} else {
currentSeries = chartData.addSeries(categoriesData, valuesData);
processNewSeries(chartData, currentSeries);
}
SeriesRenderData seriesRenderData = currentSeriesData.get(i);
// Assign series to specific Y axis if configured
if (seriesRenderData.getYAxisPosition() != null && !valueAxes.isEmpty()) {
assignSeriesToYAxis(currentSeries, seriesRenderData.getYAxisPosition(), valueAxes);
}
String name = seriesRenderData.getName();
currentSeries.setTitle(name, chart.setSheetTitle(name, valueCol));
valueCol++;
}
removeExtraSeries(chartData, orignSize, currentSeriesSize);
}
XSSFSheet sheet = chart.getWorkbook().getSheetAt(0);
updateCTTable(sheet, usedSeriesDatas);
removeExtraSheetCell(sheet, data.getCategories().length, totalSeriesCount, usedSeriesDatas.size());
for (XDDFChartData chartData : chartSeries) {
plot(chart, chartData);
}
setTitle(chart, data.getChartTitle());
setAxisTitle(chart, data.getxAxisTitle(), data.getyAxisTitle());
}
/**
* Determine which Y axis position the given XDDFChartData belongs to.
* First tries to read from existing series, then falls back to counting
* occurrences of each chart data type (first = LEFT, second = RIGHT).
*/
private AxisPosition determineAxisPosition(XDDFChartData chartData, Map<Long, XDDFValueAxis> valueAxes,
Map<Class<?>, Integer> typeCounts, Map<Class<?>, Integer> typeIndex) {
if (valueAxes.isEmpty()) return null;
int totalOfType = typeCounts.getOrDefault(chartData.getClass(), 1);
if (totalOfType <= 1) return null;
// Try to determine from existing series' yAxisId
if (chartData.getSeriesCount() > 0) {
try {
Series firstSeries = chartData.getSeries(0);
Field yAxisIdField = ReflectionUtils.findField(firstSeries.getClass(), "yAxisId");
if (yAxisIdField != null) {
yAxisIdField.setAccessible(true);
Long axisId = (Long) yAxisIdField.get(firstSeries);
if (axisId != null) {
XDDFValueAxis axis = valueAxes.get(axisId);
if (axis != null) return axis.getPosition();
}
}
} catch (Exception ignored) {
}
}
// Fallback: use occurrence count to assign axes (first=LEFT, second=RIGHT)
int idx = typeIndex.merge(chartData.getClass(), 1, Integer::sum) - 1;
if (idx == 0) return AxisPosition.LEFT;
if (idx == 1) return AxisPosition.RIGHT;
return null;
}
/**
* Assign a series to a specific Y axis by setting its yAxisId via reflection.
*/
private void assignSeriesToYAxis(Series series, AxisPosition yAxisPosition, Map<Long, XDDFValueAxis> valueAxes) {
XDDFValueAxis targetAxis = null;
for (XDDFValueAxis axis : valueAxes.values()) {
if (axis.getPosition() == yAxisPosition) {
targetAxis = axis;
break;
}
}
if (targetAxis != null) {
try {
Field yAxisIdField = ReflectionUtils.findField(series.getClass(), "yAxisId");
if (yAxisIdField != null) {
yAxisIdField.setAccessible(true);
yAxisIdField.set(series, targetAxis.getId());
}
} catch (Exception ignored) {
}
}
}
protected void processNewSeries(XDDFChartData chartData, Series addSeries) {
}
private int ensureSeriesCount(XWPFChart chart, List<XDDFChartData> chartSeries) throws IllegalAccessException {
int totalSeriesCount = chartSeries.stream().mapToInt(XDDFChartData::getSeriesCount).sum();
Field field = ReflectionUtils.findField(XDDFChart.class, "seriesCount");
field.setAccessible(true);
field.set(chart, totalSeriesCount);
return totalSeriesCount;
}
private void validate(List<XDDFChartData> chartSeries, ChartMultiSeriesRenderData data) {
if (chartSeries.size() >= 2) {
long nullCount = data.getSeriesDatas().stream().filter(d -> null == d.getComboType()).count();
if (nullCount > 0) throw new RenderException("Combo chart must set comboType field of series!");
}
}
/**
* Obtain series data matching the chart data class AND axis position.
* When a dual Y-axis chart has multiple XDDFChartData of the same type,
* series are filtered by yAxisPosition to avoid duplication.
*/
private List<SeriesRenderData> obtainSeriesData(Class<? extends XDDFChartData> clazz,
List<SeriesRenderData> seriesDatas, AxisPosition chartAxisPosition) {
Predicate<SeriesRenderData> typePredicate;
if (clazz.equals(XDDFBarChartData.class)) {
typePredicate = data -> ComboType.BAR == data.getComboType();
} else if (clazz.equals(XDDFAreaChartData.class)) {
typePredicate = data -> ComboType.AREA == data.getComboType();
} else if (clazz.equals(XDDFLineChartData.class)) {
typePredicate = data -> ComboType.LINE == data.getComboType();
} else {
typePredicate = data -> false;
}
List<SeriesRenderData> typeMatched = seriesDatas.stream().filter(typePredicate).collect(Collectors.toList());
// When axis position is known, filter series by axis
if (chartAxisPosition != null) {
return typeMatched.stream()
.filter(d -> {
AxisPosition seriesAxis = d.getYAxisPosition();
if (seriesAxis == null) {
return chartAxisPosition == AxisPosition.LEFT;
}
return seriesAxis == chartAxisPosition;
})
.collect(Collectors.toList());
}
return typeMatched;
}
}
2、修改package com.deepoove.poi.data.SeriesRenderData类
java
/*
* Copyright 2014-2026 Sayi
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.deepoove.poi.data;
import java.io.Serializable;
import org.apache.poi.xddf.usermodel.chart.AxisPosition;
/**
* One Series data
*
* @author Sayi
*/
public class SeriesRenderData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* series name
*/
private String name;
/**
* value must be mapped one to one with category
*/
private Number[] values;
/**
* Only specify the type of series in the combination chart
*/
private ComboType comboType;
/**
* Y axis position for dual Y-axis charts (LEFT or RIGHT)
*/
private AxisPosition yAxisPosition;
public SeriesRenderData() {
}
public enum ComboType {
BAR, LINE, AREA;
}
public SeriesRenderData(String name, Number[] data) {
this.name = name;
this.values = data;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Number[] getValues() {
return values;
}
public void setValues(Number[] data) {
this.values = data;
}
public ComboType getComboType() {
return comboType;
}
public void setComboType(ComboType comboType) {
this.comboType = comboType;
}
public AxisPosition getYAxisPosition() {
return yAxisPosition;
}
public void setYAxisPosition(AxisPosition yAxisPosition) {
this.yAxisPosition = yAxisPosition;
}
}
3、修改package com.deepoove.poi.data.Charts类
java
/*
* Copyright 2014-2026 Sayi
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.deepoove.poi.data;
import java.util.ArrayList;
import java.util.List;
import org.apache.poi.xddf.usermodel.chart.AxisPosition;
import com.deepoove.poi.data.SeriesRenderData.ComboType;
/**
* Factory method to create chart
*
* @author Sayi
*
*/
public class Charts {
public static ChartMultis ofBar(String chartTitle, String[] categories) {
return ofMultiSeries(chartTitle, categories);
}
public static ChartMultis ofLine(String chartTitle, String[] categories) {
return ofMultiSeries(chartTitle, categories);
}
public static ChartMultis ofArea(String chartTitle, String[] categories) {
return ofMultiSeries(chartTitle, categories);
}
public static ChartMultis ofBar3D(String chartTitle, String[] categories) {
return ofMultiSeries(chartTitle, categories);
}
public static ChartMultis ofArea3D(String chartTitle, String[] categories) {
return ofMultiSeries(chartTitle, categories);
}
public static ChartMultis ofLine3D(String chartTitle, String[] categories) {
return ofMultiSeries(chartTitle, categories);
}
public static ChartMultis ofRadar(String chartTitle, String[] categories) {
return ofMultiSeries(chartTitle, categories);
}
public static ChartSingles ofPie(String chartTitle, String[] categories) {
return ofSingleSeries(chartTitle, categories);
}
public static ChartSingles ofPie3D(String chartTitle, String[] categories) {
return ofSingleSeries(chartTitle, categories);
}
public static ChartSingles ofDoughnut(String chartTitle, String[] categories) {
return ofSingleSeries(chartTitle, categories);
}
public static ChartMultis ofMultiSeries(String chartTitle, String[] categories) {
return new ChartMultis(chartTitle, categories);
}
public static ChartCombos ofComboSeries(String chartTitle, String[] categories) {
return new ChartCombos(chartTitle, categories);
}
public static ChartSingles ofSingleSeries(String chartTitle, String[] categories) {
return new ChartSingles(chartTitle, categories);
}
public static interface ChartSetting<T extends RenderData> {
ChartBuilder<T> setxAsixTitle(String xAxisTitle);
ChartBuilder<T> setyAsixTitle(String yAxisTitle);
}
public static abstract class ChartBuilder<T extends RenderData> implements RenderDataBuilder<T>, ChartSetting<T> {
protected String chartTitle;
protected String xAxisTitle;
protected String yAxisTitle;
protected String[] categories;
protected ChartBuilder(String chartTitle, String[] categories) {
this.chartTitle = chartTitle;
this.categories = categories;
}
protected void checkLengh(int length) {
if (categories.length != length) {
throw new IllegalArgumentException(
"The length of categories and series values in chart must be the same!");
}
}
public ChartBuilder<T> setxAsixTitle(String xAxisTitle) {
this.xAxisTitle = xAxisTitle;
return this;
}
public ChartBuilder<T> setyAsixTitle(String yAxisTitle) {
this.yAxisTitle = yAxisTitle;
return this;
}
}
/**
*
* builder to build multi series chart
*
*/
public static class ChartMultis extends ChartBuilder<ChartMultiSeriesRenderData> {
private List<SeriesRenderData> seriesDatas = new ArrayList<>();
private ChartMultis(String chartTitle, String[] categories) {
super(chartTitle, categories);
}
public ChartMultis addSeries(String name, Number[] value) {
checkLengh(value.length);
seriesDatas.add(new SeriesRenderData(name, value));
return this;
}
public ChartMultis addBarSeries(String name, Number[] value) {
addSeries(ComboType.BAR, name, value, null);
return this;
}
public ChartMultis addBarSeries(String name, Number[] value, AxisPosition yAxisPosition) {
addSeries(ComboType.BAR, name, value, yAxisPosition);
return this;
}
public ChartMultis addLineSeries(String name, Number[] value) {
addSeries(ComboType.LINE, name, value, null);
return this;
}
public ChartMultis addLineSeries(String name, Number[] value, AxisPosition yAxisPosition) {
addSeries(ComboType.LINE, name, value, yAxisPosition);
return this;
}
public ChartMultis addAreaSeries(String name, Number[] value) {
addSeries(ComboType.AREA, name, value, null);
return this;
}
public ChartMultis addAreaSeries(String name, Number[] value, AxisPosition yAxisPosition) {
addSeries(ComboType.AREA, name, value, yAxisPosition);
return this;
}
private void addSeries(ComboType type, String name, Number[] value, AxisPosition yAxisPosition) {
checkLengh(value.length);
SeriesRenderData seriesRenderData = new SeriesRenderData(name, value);
seriesRenderData.setComboType(type);
seriesRenderData.setYAxisPosition(yAxisPosition);
seriesDatas.add(seriesRenderData);
}
@Override
public ChartMultiSeriesRenderData create() {
ChartMultiSeriesRenderData data = new ChartMultiSeriesRenderData();
data.setChartTitle(chartTitle);
data.setxAxisTitle(xAxisTitle);
data.setyAxisTitle(yAxisTitle);
data.setCategories(categories);
data.setSeriesDatas(seriesDatas);
return data;
}
}
/**
* builder to build combo series chart
*
*/
public static class ChartCombos extends ChartBuilder<ChartMultiSeriesRenderData> {
private List<SeriesRenderData> seriesDatas = new ArrayList<>();
private ChartCombos(String chartTitle, String[] categories) {
super(chartTitle, categories);
}
public ChartCombos addBarSeries(String name, Number[] value) {
addSeries(ComboType.BAR, name, value, null);
return this;
}
public ChartCombos addBarSeries(String name, Number[] value, AxisPosition yAxisPosition) {
addSeries(ComboType.BAR, name, value, yAxisPosition);
return this;
}
public ChartCombos addLineSeries(String name, Number[] value) {
addSeries(ComboType.LINE, name, value, null);
return this;
}
public ChartCombos addLineSeries(String name, Number[] value, AxisPosition yAxisPosition) {
addSeries(ComboType.LINE, name, value, yAxisPosition);
return this;
}
public ChartCombos addAreaSeries(String name, Number[] value) {
addSeries(ComboType.AREA, name, value, null);
return this;
}
public ChartCombos addAreaSeries(String name, Number[] value, AxisPosition yAxisPosition) {
addSeries(ComboType.AREA, name, value, yAxisPosition);
return this;
}
private void addSeries(ComboType type, String name, Number[] value, AxisPosition yAxisPosition) {
checkLengh(value.length);
SeriesRenderData seriesRenderData = new SeriesRenderData(name, value);
seriesRenderData.setComboType(type);
seriesRenderData.setYAxisPosition(yAxisPosition);
seriesDatas.add(seriesRenderData);
}
@Override
public ChartMultiSeriesRenderData create() {
ChartMultiSeriesRenderData data = new ChartMultiSeriesRenderData();
data.setChartTitle(chartTitle);
data.setxAxisTitle(xAxisTitle);
data.setyAxisTitle(yAxisTitle);
data.setCategories(categories);
data.setSeriesDatas(seriesDatas);
return data;
}
}
/**
* builder to build single series chart
*
*/
public static class ChartSingles extends ChartBuilder<ChartSingleSeriesRenderData> {
private SeriesRenderData series;
private ChartSingles(String chartTitle, String[] categories) {
super(chartTitle, categories);
}
public ChartSingles series(String name, Number[] value) {
checkLengh(value.length);
series = new SeriesRenderData(name, value);
return this;
}
@Override
public ChartSingleSeriesRenderData create() {
ChartSingleSeriesRenderData data = new ChartSingleSeriesRenderData();
data.setChartTitle(chartTitle);
data.setxAxisTitle(xAxisTitle);
data.setyAxisTitle(yAxisTitle);
data.setCategories(categories);
data.setSeriesData(series);
return data;
}
}
}
4、打包后使用maven反向安装到仓库
bash
mvn install:install-file -DgroupId=com.deepoove -DartifactId=poi-tl -Dversion=poi-tl-xxx -Dpackaging=jar -Dfile=poi-tl-xxx.jar