diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index b7b6e6c42..18ae27fb9 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 07db07133..029b7e041 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 ac823b2b1..dcb722b82 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 f4e1f2f65..1bb41ddc7 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 9192d97d7..b1a26f798 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 5e2a34f1e..54d248319 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 000000000..d7fbf13b8 Binary files /dev/null and b/test-data/spreadsheet/49581.xls differ