From 40a14f81d3c84b1dc107a2065aa29dd86f352d33 Mon Sep 17 00:00:00 2001 From: Nick Burch Date: Fri, 16 Jul 2010 16:46:42 +0000 Subject: [PATCH] Patch from Jens Gotze from bug #49581 - Ability to add, modify and remove series from HSSF Charts git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@964855 13f79535-47bb-0310-9956-ffa450edef68 --- src/documentation/content/xdocs/status.xml | 1 + .../apache/poi/hssf/record/RecordFactory.java | 1 + .../record/chart/ChartEndBlockRecord.java | 15 + .../record/chart/ChartStartBlockRecord.java | 17 + .../apache/poi/hssf/usermodel/HSSFChart.java | 382 +++++++++++++++++- .../poi/hssf/usermodel/TestHSSFChart.java | 99 +++++ test-data/spreadsheet/49581.xls | Bin 0 -> 49664 bytes 7 files changed, 494 insertions(+), 21 deletions(-) create mode 100644 test-data/spreadsheet/49581.xls diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index b7b6e6c423..18ae27fb98 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -34,6 +34,7 @@ + 49581 - Ability to add, modify and remove series from HSSF Charts 49185 - Support for HSSFNames where the comment is stored in a NameCommentRecord 49599 - Correct writing of NoteRecord author text when switching between ASCII and Unicode HWPF: Improve reading of auto-saved ("complex") documents diff --git a/src/java/org/apache/poi/hssf/record/RecordFactory.java b/src/java/org/apache/poi/hssf/record/RecordFactory.java index 07db07133e..029b7e0413 100644 --- a/src/java/org/apache/poi/hssf/record/RecordFactory.java +++ b/src/java/org/apache/poi/hssf/record/RecordFactory.java @@ -232,6 +232,7 @@ public final class RecordFactory { ChartStartObjectRecord.class, ChartEndObjectRecord.class, CatLabRecord.class, + DataFormatRecord.class, EndRecord.class, LinkedDataRecord.class, SeriesToChartGroupRecord.class, diff --git a/src/java/org/apache/poi/hssf/record/chart/ChartEndBlockRecord.java b/src/java/org/apache/poi/hssf/record/chart/ChartEndBlockRecord.java index ac823b2b12..dcb722b826 100644 --- a/src/java/org/apache/poi/hssf/record/chart/ChartEndBlockRecord.java +++ b/src/java/org/apache/poi/hssf/record/chart/ChartEndBlockRecord.java @@ -35,6 +35,9 @@ public final class ChartEndBlockRecord extends StandardRecord { private short iObjectKind; private byte[] unused; + public ChartEndBlockRecord() { + } + public ChartEndBlockRecord(RecordInputStream in) { rt = in.readShort(); grbitFrt = in.readShort(); @@ -80,4 +83,16 @@ public final class ChartEndBlockRecord extends StandardRecord { buffer.append("[/ENDBLOCK]\n"); return buffer.toString(); } + + @Override + public ChartEndBlockRecord clone() { + ChartEndBlockRecord record = new ChartEndBlockRecord(); + + record.rt = rt ; + record.grbitFrt = grbitFrt ; + record.iObjectKind = iObjectKind ; + record.unused = unused.clone() ; + + return record; + } } diff --git a/src/java/org/apache/poi/hssf/record/chart/ChartStartBlockRecord.java b/src/java/org/apache/poi/hssf/record/chart/ChartStartBlockRecord.java index f4e1f2f655..1bb41ddc7b 100644 --- a/src/java/org/apache/poi/hssf/record/chart/ChartStartBlockRecord.java +++ b/src/java/org/apache/poi/hssf/record/chart/ChartStartBlockRecord.java @@ -37,6 +37,9 @@ public final class ChartStartBlockRecord extends StandardRecord { private short iObjectInstance1; private short iObjectInstance2; + public ChartStartBlockRecord() { + } + public ChartStartBlockRecord(RecordInputStream in) { rt = in.readShort(); grbitFrt = in.readShort(); @@ -80,4 +83,18 @@ public final class ChartStartBlockRecord extends StandardRecord { buffer.append("[/STARTBLOCK]\n"); return buffer.toString(); } + + @Override + public ChartStartBlockRecord clone() { + ChartStartBlockRecord record = new ChartStartBlockRecord(); + + record.rt = rt; + record.grbitFrt = grbitFrt; + record.iObjectKind = iObjectKind; + record.iObjectContext = iObjectContext; + record.iObjectInstance1 = iObjectInstance1; + record.iObjectInstance2 = iObjectInstance2; + + return record; + } } diff --git a/src/scratchpad/src/org/apache/poi/hssf/usermodel/HSSFChart.java b/src/scratchpad/src/org/apache/poi/hssf/usermodel/HSSFChart.java index 9192d97d78..b1a26f798b 100644 --- a/src/scratchpad/src/org/apache/poi/hssf/usermodel/HSSFChart.java +++ b/src/scratchpad/src/org/apache/poi/hssf/usermodel/HSSFChart.java @@ -18,6 +18,7 @@ package org.apache.poi.hssf.usermodel; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import org.apache.poi.hssf.record.chart.*; @@ -35,8 +36,10 @@ import org.apache.poi.hssf.record.SCLRecord; import org.apache.poi.hssf.record.UnknownRecord; import org.apache.poi.hssf.record.VCenterRecord; import org.apache.poi.hssf.record.formula.Area3DPtg; +import org.apache.poi.hssf.record.formula.AreaPtgBase; import org.apache.poi.hssf.record.formula.Ptg; -import org.apache.poi.hssf.record.chart.LinkedDataRecord; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.ss.util.CellRangeAddressBase; /** * Has methods for construction of a chart object. @@ -44,6 +47,7 @@ import org.apache.poi.hssf.record.chart.LinkedDataRecord; * @author Glen Stampoultzis (glens at apache.org) */ public final class HSSFChart { + private HSSFSheet sheet; private ChartRecord chartRecord; private LegendRecord legendRecord; @@ -51,10 +55,54 @@ public final class HSSFChart { private SeriesTextRecord chartTitleText; private List valueRanges = new ArrayList(); + private HSSFChartType type = HSSFChartType.Unknown; + private List series = new ArrayList(); - private HSSFChart(ChartRecord chartRecord) { + public enum HSSFChartType { + Area { + @Override + public short getSid() { + return 0x101A; + } + }, + Bar { + @Override + public short getSid() { + return 0x1017; + } + }, + Line { + @Override + public short getSid() { + return 0x1018; + } + }, + Pie { + @Override + public short getSid() { + return 0x1019; + } + }, + Scatter { + @Override + public short getSid() { + return 0x101B; + } + }, + Unknown { + @Override + public short getSid() { + return 0; + } + }; + + public abstract short getSid(); + } + + private HSSFChart(HSSFSheet sheet, ChartRecord chartRecord) { this.chartRecord = chartRecord; + this.sheet = sheet; } /** @@ -146,22 +194,20 @@ public final class HSSFChart { for(RecordBase r : records) { if(r instanceof ChartRecord) { - lastChart = new HSSFChart((ChartRecord)r); + lastSeries = null; + + lastChart = new HSSFChart(sheet,(ChartRecord)r); charts.add(lastChart); - } - if(r instanceof LegendRecord) { + } else if(r instanceof LegendRecord) { lastChart.legendRecord = (LegendRecord)r; - } - if(r instanceof SeriesRecord) { + } else if(r instanceof SeriesRecord) { HSSFSeries series = lastChart.new HSSFSeries( (SeriesRecord)r ); lastChart.series.add(series); lastSeries = series; - } - if(r instanceof ChartTitleFormatRecord) { + } else if(r instanceof ChartTitleFormatRecord) { lastChart.chartTitleFormat = (ChartTitleFormatRecord)r; - } - if(r instanceof SeriesTextRecord) { + } else if(r instanceof SeriesTextRecord) { // Applies to a series, unless we've seen // a legend already SeriesTextRecord str = (SeriesTextRecord)r; @@ -173,13 +219,28 @@ public final class HSSFChart { } else { lastChart.chartTitleText = str; } - } - if(r instanceof LinkedDataRecord) { - LinkedDataRecord data = (LinkedDataRecord)r; - lastSeries.insertData( data ); - } - if(r instanceof ValueRangeRecord){ + } else if (r instanceof LinkedDataRecord) { + LinkedDataRecord linkedDataRecord = (LinkedDataRecord) r; + if (lastSeries != null) { + lastSeries.insertData(linkedDataRecord); + } + } else if(r instanceof ValueRangeRecord){ lastChart.valueRanges.add((ValueRangeRecord)r); + } else if (r instanceof Record) { + if (lastChart != null) + { + Record record = (Record) r; + for (HSSFChartType type : HSSFChartType.values()) { + if (type == HSSFChartType.Unknown) + { + continue; + } + if (record.getSid() == type.getSid()) { + lastChart.type = type ; + break; + } + } + } } } @@ -908,14 +969,13 @@ public final class HSSFChart { private LinkedDataRecord dataValues; private LinkedDataRecord dataCategoryLabels; private LinkedDataRecord dataSecondaryCategoryLabels; - private int dataReaded = 0; /* package */ HSSFSeries(SeriesRecord series) { this.series = series; } - public void insertData(LinkedDataRecord data){ - switch(dataReaded){ + /* package */ void insertData(LinkedDataRecord data){ + switch(data.getLinkType()){ case 0: dataName = data; break; case 1: dataValues = data; @@ -925,7 +985,11 @@ public final class HSSFChart { case 3: dataSecondaryCategoryLabels = data; break; } - dataReaded++; + } + + /* package */ void setSeriesTitleText(SeriesTextRecord seriesTitleText) + { + this.seriesTitleText = seriesTitleText; } public short getNumValues() { @@ -996,5 +1060,281 @@ public final class HSSFChart { public SeriesRecord getSeries() { return series; } + + private CellRangeAddressBase getCellRange(LinkedDataRecord linkedDataRecord) { + if (linkedDataRecord == null) + { + return null ; + } + + int firstRow = 0; + int lastRow = 0; + int firstCol = 0; + int lastCol = 0; + + for (Ptg ptg : linkedDataRecord.getFormulaOfLink()) { + if (ptg instanceof AreaPtgBase) { + AreaPtgBase areaPtg = (AreaPtgBase) ptg; + + firstRow = areaPtg.getFirstRow(); + lastRow = areaPtg.getLastRow(); + + firstCol = areaPtg.getFirstColumn(); + lastCol = areaPtg.getLastColumn(); + } + } + + return new CellRangeAddress(firstRow, lastRow, firstCol, lastCol); + } + + public CellRangeAddressBase getValuesCellRange() { + return getCellRange(dataValues); + } + + public CellRangeAddressBase getCategoryLabelsCellRange() { + return getCellRange(dataCategoryLabels); + } + + private Integer setVerticalCellRange(LinkedDataRecord linkedDataRecord, + CellRangeAddressBase range) { + if (linkedDataRecord == null) + { + return null; + } + + List ptgList = new ArrayList(); + + int rowCount = (range.getLastRow() - range.getFirstRow()) + 1; + int colCount = (range.getLastColumn() - range.getFirstColumn()) + 1; + + for (Ptg ptg : linkedDataRecord.getFormulaOfLink()) { + if (ptg instanceof AreaPtgBase) { + AreaPtgBase areaPtg = (AreaPtgBase) ptg; + + areaPtg.setFirstRow(range.getFirstRow()); + areaPtg.setLastRow(range.getLastRow()); + + areaPtg.setFirstColumn(range.getFirstColumn()); + areaPtg.setLastColumn(range.getLastColumn()); + ptgList.add(areaPtg); + } + } + + linkedDataRecord.setFormulaOfLink(ptgList.toArray(new Ptg[ptgList.size()])); + + return rowCount * colCount; + } + + public void setValuesCellRange(CellRangeAddressBase range) { + Integer count = setVerticalCellRange(dataValues, range); + if (count == null) + { + return; + } + + series.setNumValues((short)(int)count); + } + + public void setCategoryLabelsCellRange(CellRangeAddressBase range) { + Integer count = setVerticalCellRange(dataCategoryLabels, range); + if (count == null) + { + return; + } + + series.setNumCategories((short)(int)count); + } + } + + public HSSFSeries createSeries() throws Exception { + ArrayList seriesTemplate = new ArrayList(); + boolean seriesTemplateFilled = false; + + int idx = 0; + int deep = 0; + int chartRecordIdx = -1; + int chartDeep = -1; + int lastSeriesDeep = -1; + int endSeriesRecordIdx = -1; + int seriesIdx = 0; + final List records = sheet.getSheet().getRecords(); + + /* store first series as template and find last series index */ + for(final RecordBase record : records) { + + idx++; + + if (record instanceof BeginRecord) { + deep++; + } else if (record instanceof EndRecord) { + deep--; + + if (lastSeriesDeep == deep) { + lastSeriesDeep = -1; + endSeriesRecordIdx = idx; + if (!seriesTemplateFilled) { + seriesTemplate.add(record); + seriesTemplateFilled = true; + } + } + + if (chartDeep == deep) { + break; + } + } + + if (record instanceof ChartRecord) { + if (record == chartRecord) { + chartRecordIdx = idx; + chartDeep = deep; + } + } else if (record instanceof SeriesRecord) { + if (chartRecordIdx != -1) { + seriesIdx++; + lastSeriesDeep = deep; + } + } + + if (lastSeriesDeep != -1 && !seriesTemplateFilled) { + seriesTemplate.add(record) ; + } + } + + /* check if a series was found */ + if (endSeriesRecordIdx == -1) { + return null; + } + + /* next index in the records list where the new series can be inserted */ + idx = endSeriesRecordIdx + 1; + + HSSFSeries newSeries = null; + + /* duplicate record of the template series */ + ArrayList clonedRecords = new ArrayList(); + for(final RecordBase record : seriesTemplate) { + + Record newRecord = null; + + if (record instanceof BeginRecord) { + newRecord = new BeginRecord(); + } else if (record instanceof EndRecord) { + newRecord = new EndRecord(); + } else if (record instanceof SeriesRecord) { + SeriesRecord seriesRecord = (SeriesRecord) ((SeriesRecord)record).clone(); + newSeries = new HSSFSeries(seriesRecord); + newRecord = seriesRecord; + } else if (record instanceof LinkedDataRecord) { + LinkedDataRecord linkedDataRecord = (LinkedDataRecord) ((LinkedDataRecord)record).clone(); + if (newSeries != null) { + newSeries.insertData(linkedDataRecord); + } + newRecord = linkedDataRecord; + } else if (record instanceof DataFormatRecord) { + DataFormatRecord dataFormatRecord = (DataFormatRecord) ((DataFormatRecord)record).clone(); + + dataFormatRecord.setSeriesIndex((short)seriesIdx) ; + dataFormatRecord.setSeriesNumber((short)seriesIdx) ; + + newRecord = dataFormatRecord; + } else if (record instanceof SeriesTextRecord) { + SeriesTextRecord seriesTextRecord = (SeriesTextRecord) ((SeriesTextRecord)record).clone(); + if (newSeries != null) { + newSeries.setSeriesTitleText(seriesTextRecord); + } + newRecord = seriesTextRecord; + } else if (record instanceof Record) { + newRecord = (Record) ((Record)record).clone(); + } + + if (newRecord != null) + { + clonedRecords.add(newRecord); + } + } + + /* check if a user model series object was created */ + if (newSeries == null) + { + return null; + } + + /* transfer series to record list */ + for(final RecordBase record : clonedRecords) { + records.add(idx++, record); + } + + return newSeries; + } + + public boolean removeSeries(HSSFSeries series) { + int idx = 0; + int deep = 0; + int chartDeep = -1; + int lastSeriesDeep = -1; + int seriesIdx = -1; + boolean removeSeries = false; + boolean chartEntered = false; + boolean result = false; + final List records = sheet.getSheet().getRecords(); + + /* store first series as template and find last series index */ + Iterator iter = records.iterator(); + while (iter.hasNext()) { + RecordBase record = iter.next(); + idx++; + + if (record instanceof BeginRecord) { + deep++; + } else if (record instanceof EndRecord) { + deep--; + + if (lastSeriesDeep == deep) { + lastSeriesDeep = -1; + + if (removeSeries) { + removeSeries = false; + result = true; + iter.remove(); + } + } + + if (chartDeep == deep) { + break; + } + } + + if (record instanceof ChartRecord) { + if (record == chartRecord) { + chartDeep = deep; + chartEntered = true; + } + } else if (record instanceof SeriesRecord) { + if (chartEntered) { + if (series.series == record) { + lastSeriesDeep = deep; + removeSeries = true; + } else { + seriesIdx++; + } + } + } else if (record instanceof DataFormatRecord) { + if (chartEntered && !removeSeries) { + DataFormatRecord dataFormatRecord = (DataFormatRecord) record; + dataFormatRecord.setSeriesIndex((short) seriesIdx); + dataFormatRecord.setSeriesNumber((short) seriesIdx); + } + } + + if (removeSeries) { + iter.remove(); + } + } + + return result; + } + + public HSSFChartType getType() { + return type; } } diff --git a/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestHSSFChart.java b/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestHSSFChart.java index 5e2a34f1ed..54d248319a 100644 --- a/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestHSSFChart.java +++ b/src/scratchpad/testcases/org/apache/poi/hssf/usermodel/TestHSSFChart.java @@ -19,8 +19,12 @@ package org.apache.poi.hssf.usermodel; import junit.framework.TestCase; +import org.apache.poi.hssf.HSSFITestDataProvider; import org.apache.poi.hssf.HSSFTestDataSamples; import org.apache.poi.hssf.record.chart.SeriesRecord; +import org.apache.poi.hssf.usermodel.HSSFChart.HSSFSeries; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.ss.util.CellRangeAddressBase; /** * Tests for {@link HSSFChart} @@ -127,4 +131,99 @@ public final class TestHSSFChart extends TestCase { assertEquals("Base Numbers", charts[0].getSeries()[1].getSeriesTitle()); assertEquals("Sheet 3 Chart with Title", charts[0].getChartTitle()); } + + public void testExistingSheet3() throws Exception { + HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook("49581.xls"); + + HSSFSheet sheet = wb.getSheetAt( 2 ) ; + HSSFChart[] charts = HSSFChart.getSheetCharts( sheet ) ; + assertEquals(1, charts.length); + + for ( HSSFChart chart : charts ) { + for ( HSSFSeries series : chart.getSeries() ) { + chart.removeSeries( series ) ; + } + } + + // Save and re-check + wb = HSSFITestDataProvider.instance.writeOutAndReadBack(wb); + sheet = wb.getSheetAt( 2 ) ; + assertEquals(1, HSSFChart.getSheetCharts(sheet).length); + + HSSFChart c = HSSFChart.getSheetCharts(sheet)[0]; + assertEquals(0, c.getSeries().length); + } + + public void testExistingSheet2() throws Exception { + HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook("49581.xls"); + HSSFSheet sheet = wb.getSheetAt( 1 ) ; + HSSFChart[] charts = HSSFChart.getSheetCharts( sheet ) ; + + assertEquals(1, charts.length); + for ( HSSFChart chart : charts ) { + HSSFSeries series ; + + // Starts with one + assertEquals(1, chart.getSeries().length); + + // Add two more + series = chart.createSeries() ; + series.setCategoryLabelsCellRange( new CellRangeAddress( 3, 4, 0, 0 ) ) ; + series.setValuesCellRange( new CellRangeAddress( 3, 4, 1, 1 ) ) ; + + series = chart.createSeries() ; + series.setCategoryLabelsCellRange( new CellRangeAddress( 6, 7, 0, 0 ) ) ; + series.setValuesCellRange( new CellRangeAddress( 6, 7, 1, 1 ) ) ; + } + + // Save and re-check + wb = HSSFITestDataProvider.instance.writeOutAndReadBack(wb); + sheet = wb.getSheetAt( 1 ) ; + assertEquals(1, HSSFChart.getSheetCharts(sheet).length); + + HSSFChart c = HSSFChart.getSheetCharts(sheet)[0]; + assertEquals(3, c.getSeries().length); + } + + public void testExistingSheet1() throws Exception { + HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook("49581.xls"); + HSSFSheet sheet = wb.getSheetAt( 0 ) ; + HSSFChart[] charts = HSSFChart.getSheetCharts( sheet ) ; + + for ( HSSFChart chart : charts ) { + //System.out.println( chart.getType() ) ; + HSSFSeries[] seriesArray = chart.getSeries() ; + //System.out.println( "seriesArray.length=" + seriesArray.length ) ; + for ( HSSFSeries series : seriesArray ) + { + //System.out.println( "serie.getNumValues()=" + series.getNumValues() ) ; + CellRangeAddressBase range ; + + range = series.getValuesCellRange() ; + //System.out.println( range.toString() ) ; + range.setLastRow( range.getLastRow() + 1 ) ; + series.setValuesCellRange( range ) ; + + range = series.getCategoryLabelsCellRange() ; + //System.out.println( range.toString() ) ; + range.setLastRow( range.getLastRow() + 1 ) ; + series.setCategoryLabelsCellRange( range ) ; + } + + for ( int id = 0 ; id < 2 ; id++ ) + { + HSSFSeries newSeries = chart.createSeries() ; + newSeries.setValuesCellRange( new CellRangeAddress( 1 + id, 4, 3, 3 ) ) ; + String oldSeriesTitle = newSeries.getSeriesTitle() ; + if ( oldSeriesTitle != null ) + { + //System.out.println( "old series title: " + oldSeriesTitle ) ; + newSeries.setSeriesTitle( "new series" ) ; + } + } + } + + HSSFChart chart = charts[ 2 ] ; + chart.removeSeries( chart.getSeries()[ 0 ] ) ; + } } diff --git a/test-data/spreadsheet/49581.xls b/test-data/spreadsheet/49581.xls new file mode 100644 index 0000000000000000000000000000000000000000..d7fbf13b8b7e15cd4887de38ccbc7b7ef91c6a84 GIT binary patch literal 49664 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 literal 0 HcmV?d00001 -- 2.39.5