diff --git a/components/fpspreadsheet/examples/other/chart/write_chart_demo.lpr b/components/fpspreadsheet/examples/other/chart/write_chart_demo.lpr index 14a0fae72..b14fae548 100644 --- a/components/fpspreadsheet/examples/other/chart/write_chart_demo.lpr +++ b/components/fpspreadsheet/examples/other/chart/write_chart_demo.lpr @@ -83,7 +83,7 @@ begin ch.Background.Style := fsSolidFill; ch.Border.Style := clsSolid; ch.PlotArea.Background.Style := fsSolidFill; - ch.RotatedAxes := true; + //ch.RotatedAxes := true; ch.StackMode := csmStackedPercentage; //ch.Interpolation := ciCubicSpline; diff --git a/components/fpspreadsheet/laz_fpspreadsheet.lpk b/components/fpspreadsheet/laz_fpspreadsheet.lpk index 51533001b..073db68c6 100644 --- a/components/fpspreadsheet/laz_fpspreadsheet.lpk +++ b/components/fpspreadsheet/laz_fpspreadsheet.lpk @@ -33,7 +33,7 @@ This package is all you need if you don't want graphical components (such as grids and charts)."/> - + @@ -305,6 +305,10 @@ This package is all you need if you don't want graphical components (such a + + + + diff --git a/components/fpspreadsheet/source/common/fpsopendocument.pas b/components/fpspreadsheet/source/common/fpsopendocument.pas index 20f8fbf6e..d9edd41be 100644 --- a/components/fpspreadsheet/source/common/fpsopendocument.pas +++ b/components/fpspreadsheet/source/common/fpsopendocument.pas @@ -236,6 +236,8 @@ type FHasColFormats: Boolean; FHasRowFormats: Boolean; + FChartWriter: TsBasicSpreadChartWriter; + // Routines to write parts of files procedure WriteAutomaticStyles(AStream: TStream); procedure WriteCellRow(AStream: TStream; ASheet: TsBasicWorksheet; @@ -275,7 +277,7 @@ type function WriteDefaultGraphicStyleXMLAsString: String; overload; function WriteDocumentProtectionXMLAsString: String; function WriteFontStyleXMLAsString(const AFormat: TsCellFormat): String; overload; - function WriteFontStyleXMLAsString(AFont: TsFont): String; overload; +// function WriteFontStyleXMLAsString(AFont: TsFont): String; overload; function WriteHeaderFooterFontXMLAsString(AFont: TsHeaderFooterFont): String; function WriteHorAlignmentStyleXMLAsString(const AFormat: TsCellFormat): String; function WriteNumFormatStyleXMLAsString(const AFormat: TsCellFormat): String; @@ -288,50 +290,14 @@ type function WriteVertAlignmentStyleXMLAsString(const AFormat: TsCellFormat): String; function WriteWordwrapStyleXMLAsString(const AFormat: TsCellFormat): String; - { Chart support } - function GetChartAxisStyleAsXML(Axis: TsChartAxis; AIndent, AStyleID: Integer): String; - function GetChartBackgroundStyleAsXML(AChart: TsChart; AFill: TsChartFill; - ABorder: TsChartLine; AIndent: Integer; AStyleID: Integer): String; - function GetChartCaptionStyleAsXML(AChart: TsChart; ACaptionKind, AIndent, AStyleID: Integer): String; - function GetChartLegendStyleAsXML(AChart: TsChart; AIndent, AStyleID: Integer): String; - function GetChartLineStyleAsXML(AChart: TsChart; ALine: TsChartLine; AIndent, AStyleID: Integer): String; - function GetChartPlotAreaStyleAsXML(AChart: TsChart; AIndent, AStyleID: Integer): String; - function GetChartSeriesStyleAsXML(AChart: TsChart; ASeriesIndex, AIndent, AStyleID: integer): String; -// function GetChartTitleStyleAsXML(AChart: TsChart; AStyleIndex, AIndent: Integer): String; - - function GetChartFillStyleGraphicPropsAsXML(AChart: TsChart; AFill: TsChartFill): String; - function GetChartLineStyleGraphicPropsAsXML(AChart: TsChart; ALine: TsChartLine): String; - - procedure PrepareChartTable(AChart: TsChart; AWorksheet: TsBasicWorksheet); - - procedure WriteChart(AStream: TStream; AChart: TsChart); - procedure WriteChartAxis(AChartStream, AStyleStream: TStream; - AChartIndent, AStyleIndent: Integer; Axis: TsChartAxis; var AStyleID: Integer); - procedure WriteChartBackground(AChartStream, AStyleStream: TStream; - AChartIndent, AStyleIndent: Integer; AChart: TsChart; var AStyleID: Integer); - procedure WriteChartLegend(AChartStream, AStyleStream: TStream; - AChartIndent, AStyleIndent: Integer; AChart: TsChart; var AStyleID: Integer); - procedure WriteChartNumberStyles(AStream: TStream; AIndent: Integer; AChart: TsChart); - procedure WriteChartPlotArea(AChartStream, AStyleStream: TStream; - AChartIndent, AStyleIndent: Integer; AChart: TsChart; var AStyleID: Integer); - procedure WriteChartSeries(AChartStream, AStyleStream: TStream; - AChartIndent, AStyleIndent: Integer; AChart: TsChart; ASeriesIndex: Integer; var AStyleID: Integer); -// procedure WriteChartStyles(AStream: TStream; AChart: TsChart; AIndent: Integer); - procedure WriteChartTable(AStream: TStream; AChart: TsChart; AIndent: Integer); - procedure WriteChartTitle(AChartStream, AStyleStream: TStream; - AChartIndent, AStyleIndent: Integer; AChart: TsChart; IsSubtitle: Boolean; var AStyleID: Integer); -// procedure WriteChartTitle(AStream: TStream; AChart: TsChart; IsSubtitle: Boolean; AIndent: Integer); - - procedure WriteObjectStyles(AStream: TStream; AChart: TsChart); - protected FPointSeparatorSettings: TFormatSettings; // Streams with the contents of files FSMeta, FSSettings, FSStyles, FSContent: TStream; FSMimeType, FSMetaInfManifest: TStream; - FSCharts: array of TStream; - FSObjectStyles: array of TStream; +// FSCharts: array of TStream; +// FSObjectStyles: array of TStream; { Helpers } procedure AddBuiltinNumFormats; override; @@ -355,12 +321,10 @@ type procedure ResetStreams; { Routines to write those files } - procedure WriteCharts; procedure WriteContent; procedure WriteMetaInfManifest; procedure WriteMeta; procedure WriteMimetype; - procedure WriteObjectStyles; procedure WriteSettings; procedure WriteStyles; procedure WriteWorksheet(AStream: TStream; ASheetIndex: Integer); @@ -389,6 +353,9 @@ type { General writing methods } procedure WriteStringToFile(AString, AFileName: string); procedure WriteToStream(AStream: TStream; AParams: TsStreamParams = []); override; + + // Needed by the chart writer + function WriteFontStyleXMLAsString(AFont: TsFont): String; end; procedure InitOpenDocLimitations(out ALimitations: TsSpreadsheetFormatLimitations); @@ -404,14 +371,13 @@ uses {$ENDIF} StrUtils, Variants, LazFileUtils, URIParser, LazUTF8, {%H-}fpsPatches, - fpsStrings, fpsStreams, fpsCrypto, fpsClasses, fpspreadsheet, - fpsExprParser, fpsImages, fpsConditionalFormat; + fpsStrings, fpsStreams, fpsCrypto, fpsClasses, fpSpreadsheet, + fpsExprParser, fpsImages, fpsConditionalFormat, fpsOpenDocumentChart; const LE = LineEnding; { OpenDocument general XML constants } - XML_HEADER = ''; { OpenDocument Directory structure constants } OPENDOC_PATH_CONTENT = 'content.xml'; @@ -537,16 +503,6 @@ const '5Rating', '5Quarters', '5Boxes' // is5Rating, is5Quarters, is5Boxes ); - CHART_TYPE_NAMES: array[TsChartType] of string = ( - '', 'bar', 'line', 'area', 'barLine', 'scatter', 'bubble' - ); - - CHART_SYMBOL_NAMES: array[TsChartSeriesSymbol] of String = ( - 'square', 'diamond', 'arrow-up', 'arrow-down', 'arrow-left', - 'arrow-right', 'circle', 'star', 'x', 'plus', 'asterisk' - ); // unsupported: bow-tie, hourglass, horizontal-bar, vertical-bar - - function CFOperandToStr(v: variant; AWorksheet: TsWorksheet; const AFormatSettings: TFormatSettings): String; var @@ -590,18 +546,6 @@ begin Result := vkNone; end; -function ASCIIName(AName: String): String; -var - i: Integer; -begin - Result := ''; - for i := 1 to Length(AName) do - if AName[i] in ['a'..'z', 'A'..'Z', '0'..'9'] then - Result := Result + AName[i] - else - Result := Result + Format('_%.2x_', [ord(AName[i])]); -end; - type { Table style items stored in TableStyleList of the reader } @@ -5748,8 +5692,6 @@ end; { Creates the streams for the individual data files. Will be zipped into a single xlsx file. } procedure TsSpreadOpenDocWriter.CreateStreams; -var - i, n: Integer; begin FSMeta := CreateTempStream(FWorkbook, 'fpsM'); FSSettings := CreateTempStream(FWorkbook, 'fpsS'); @@ -5757,37 +5699,20 @@ begin FSContent := CreateTempStream(FWorkbook, 'fpsC'); FSMimeType := CreateTempStream(FWorkbook, 'fpsMT'); FSMetaInfManifest := CreateTempStream(FWorkbook, 'fpsMIM'); - - n := TsWorkbook(FWorkbook).GetChartCount; - SetLength(FSCharts, n); - SetLength(FSObjectStyles, n); - for i := 0 to n - 1 do - begin - FSCharts[i] := CreateTempStream(FWorkbook, 'fpsCh'); - FSObjectStyles[i] := CreateTempStream(FWorkbook, 'fpsOS'); - end; - + FChartWriter.CreateStreams; // FSSheets will be created when needed. end; { Destroys the temporary streams that were created by the writer } procedure TsSpreadOpenDocWriter.DestroyStreams; -var - i: Integer; begin + FChartWriter.DestroyStreams; DestroyTempStream(FSMeta); DestroyTempStream(FSSettings); DestroyTempStream(FSStyles); DestroyTempStream(FSContent); DestroyTempStream(FSMimeType); DestroyTempStream(FSMetaInfManifest); - for i := 0 to High(FSCharts) do - begin - DestroyTempStream(FSCharts[i]); - DestroyTempStream(FSObjectStyles[i]); - end; - Setlength(FSCharts, 0); - SetLength(FSObjectStyles, 0); end; procedure TsSpreadOpenDocWriter.GetHeaderFooterImageName( @@ -5887,8 +5812,7 @@ begin WriteSettings(); WriteStyles(); WriteContent; - WriteObjectStyles; - WriteCharts; + FChartWriter.WriteCharts; { Now compress the files } FZip := TZipper.Create; @@ -5901,13 +5825,7 @@ begin FZip.Entries.AddFileEntry(FSMimetype, OPENDOC_PATH_MIMETYPE); FZip.Entries.AddFileEntry(FSMetaInfManifest, OPENDOC_PATH_METAINF_MANIFEST); ZipPictures(FZip); - for i := 0 to TsWorkbook(FWorkbook).GetChartCount-1 do - begin - FZip.Entries.AddFileEntry( - FSCharts[i], Format(OPENDOC_PATH_CHART_CONTENT, [i+1])); - FZip.Entries.AddFileEntry( - FSObjectStyles[i], Format(OPENDOC_PATH_CHART_STYLES, [i+1])); - end; + TsSpreadOpenDocChartWriter(FChartWriter).AddChartsToZip(FZip); ResetStreams; @@ -6097,8 +6015,6 @@ end; { Is called before zipping the individual file parts. Rewinds the streams. } procedure TsSpreadOpenDocWriter.ResetStreams; -var - i: Integer; begin FSMeta.Position := 0; FSSettings.Position := 0; @@ -6106,11 +6022,7 @@ begin FSContent.Position := 0; FSMimeType.Position := 0; FSMetaInfManifest.Position := 0; - for i := 0 to High(FSCharts) do - begin - FSCharts[i].Position := 0; - FSObjectStyles[i].Position := 0; - end; + FChartWriter.ResetStreams; end; { Writes the node "office:automatic-styles". Although this node occurs in both @@ -6501,32 +6413,6 @@ begin ''); end; -procedure TsSpreadOpenDocWriter.WriteCharts; -var - i: Integer; - chart: TsChart; -begin - for i := 0 to TsWorkbook(FWorkbook).GetChartCount - 1 do - begin - chart := TsWorkbook(FWorkbook).GetChartByIndex(i); - WriteChart(FSCharts[i], chart); - end; -end; - -{ Writes the Object X/styles.xml stream which is needed for chart support - (sometimes) and contains the dash pattern definitions. } -procedure TsSpreadOpenDocWriter.WriteObjectStyles; -var - i: Integer; - chart: TsChart; -begin - for i := 0 to TsWorkbook(FWorkbook).GetChartCount - 1 do - begin - chart := TsWorkbook(FWorkbook).GetChartByIndex(i); - WriteObjectStyles(FSObjectStyles[i], chart); - end; -end; - procedure TsSpreadOpenDocWriter.WriteContent; var i: Integer; @@ -6754,1283 +6640,6 @@ begin end; end; -{ Writes the chart to the specified stream. - All chart elements are formatted by means of styles. To simplify assignment - of styles to elements we first write the elements and create the style of - the currently written chart element on the fly. Since styles must be written - to the steam first, we write the chart elements to a separate stream which - is appended to the main stream afterwards. } -procedure TsSpreadOpenDocWriter.WriteChart(AStream: TStream; AChart: TsChart); -var - chartStream: TMemoryStream; - styleStream: TMemoryStream; - styleID: Integer; -begin -// FChartStyleList.Clear; - - chartStream := TMemoryStream.Create; - styleStream := TMemoryStream.Create; - try - WriteChartNumberStyles(styleStream, 4, AChart); - - styleID := 1; - WriteChartBackground(chartStream, styleStream, 6, 4, AChart, styleID); - WriteChartTitle(chartStream, styleStream, 6, 4, AChart, false, styleID); // Title - WriteChartTitle(chartStream, styleStream, 6, 4, AChart, true, styleID); // Subtitle - WriteChartLegend(chartStream, styleStream, 6, 4, AChart, styleID); // Legend - WriteChartPlotArea(chartStream, styleStream, 6, 4, AChart, styleID); // Wall, axes, series - - // Here begins the main stream - AppendToStream(AStream, - XML_HEADER + LE); - - AppendToStream(AStream, - '' + LE - ); - - // The file begins with the chart styles - AppendToStream(AStream, - ' ' + LE - ); - - // Copy the styles from the temporary style stream. - styleStream.Position := 0; - AStream.CopyFrom(styleStream, stylestream.Size); - - // Now the chart part follows, after closing the styles part - AppendToStream(AStream, - ' ' + LE + - ' ' + LE + - ' ' + LE - ); - - // Copy the chart elements from the temporary chart stream - chartStream.Position := 0; - AStream.CopyFrom(chartStream, chartStream.Size); - - // After the chart elements we have the data to be plotted - WriteChartTable(AStream, AChart, 8); - - // Finally the footer. - AppendToStream(AStream, - ' ' + LE + - ' ' + LE + - ' ' + LE + - ''); - finally - chartStream.Free; - styleStream.Free; - end; -end; - -procedure TsSpreadOpenDocWriter.WriteChartAxis(AChartStream, AStyleStream: TStream; - AChartIndent, AStyleIndent: Integer; Axis: TsChartAxis; - var AStyleID: Integer); -type - TAxisKind = 3..6; -const - AXIS_ID: array[TAxisKind] of string = ('x', 'y', 'x', 'y'); - AXIS_LEVEL: array[TAxisKind] of string = ('primary', 'primary', 'secondary', 'secondary'); -var - indent: String; - captionKind: Integer; - chart: TsChart; - series: TsChartSeries; - sheet: TsWorksheet; - refStr: String; - r1, c1, r2, c2: Cardinal; -begin - if not Axis.Visible then - exit; - - chart := Axis.Chart; - - if Axis = chart.XAxis then - captionKind := 3 - else if Axis = chart.YAxis then - captionKind := 4 - else if Axis = chart.X2Axis then - captionKind := 5 - else if Axis = chart.Y2Axis then - captionKind := 6 - else - raise Exception.Create('[WriteChartAxis] Unknown axis'); - - // Write axis - indent := DupeString(' ', AChartIndent); - AppendToStream(AChartStream, Format( - indent + '' + LE, - [ AStyleID, AXIS_ID[captionKind], AXIS_LEVEL[captionKind], AXIS_ID[captionKind] ] - )); - - if (Axis = chart.XAxis) and (not chart.IsScatterChart) and (chart.Series.Count > 0) then - begin - series := chart.Series[0]; - sheet := TsWorkbook(FWorkbook).GetWorksheetByIndex(chart.SheetIndex); - r1 := series.LabelRange.Row1; - c1 := series.LabelRange.Col1; - r2 := series.LabelRange.Row2; - c2 := series.LabelRange.Col2; - refStr := GetSheetCellRangeString_ODS(sheet.Name, sheet.Name, r1, c1, r2, c2, rfAllRel, false); - AppendToStream(AChartStream, Format( - indent + ' ' + LE, - [ refStr ] - )); - end; - - // Write axis style - AppendToStream(AStyleStream, - GetChartAxisStyleAsXML(Axis, AStyleIndent, AStyleID) - ); - - // Next style - inc(AStyleID); - - // Axis title - if Axis.ShowCaption and (Axis.Caption <> '') then - begin - AppendToStream(AChartStream, Format( - indent + ' ' + LE + - indent + ' %s' + LE + - indent + ' ' + LE, - [ AStyleID, Axis.Caption ] - )); - - // Axis title style - AppendToStream(AStyleStream, - GetChartCaptionStyleAsXML(chart, captionKind, AStyleIndent, AStyleID) - ); - - // Next style - inc(AStyleID); - end; - - // Major grid lines - if Axis.MajorGridLines.Style <> clsNoLine then - begin - AppendToStream(AChartStream, Format( - indent + ' ' + LE, - [ AStyleID ] - )); - - // Major grid lines style - AppendToStream(AStyleStream, - GetChartLineStyleAsXML(chart, Axis.MajorGridLines, AStyleIndent, AStyleID) - ); - - // Next style - inc(AStyleID); - end; - - // Minor grid lines - if Axis.MinorGridLines.Style <> clsNoLine then - begin - AppendToStream(AChartStream, Format( - indent + ' ' + LE, - [ AStyleID ] - )); - - // Minor grid lines style - AppendToStream(AStyleStream, - GetChartLineStyleAsXML(chart, Axis.MinorGridLines, AStyleIndent, AStyleID) - ); - - // Next style - inc(AStyleID); - end; - - // Close the xml node - AppendToStream(AChartStream, - indent + '' + LE - ); -end; - -{ Writes the chart's background to the xml stream } -procedure TsSpreadOpenDocWriter.WriteChartBackground(AChartStream, AStyleStream: TStream; - AChartIndent, AStyleIndent: Integer; AChart: TsChart; var AStyleID: Integer); -var - indent: String; - chartClass: String; -begin - chartClass := CHART_TYPE_NAMES[AChart.GetChartType]; - if chartClass <> '' then - chartClass := 'chart:class="chart:' + chartClass + '"'; - - indent := DupeString(' ', AChartIndent); - AppendToStream(AChartStream, Format( - indent + '' + LE, [ - AStyleID, - chartClass, - AChart.Width, AChart.Height // Width, Height are in mm - ], FPointSeparatorSettings - )); - - AppendToStream(AStyleStream, - GetChartBackgroundStyleAsXML(AChart, AChart.Background, AChart.Border, AStyleIndent, AStyleID) - ); - - inc(AStyleID); -end; - -{ Writes the chart's legend to the xml stream } -procedure TsSpreadOpenDocWriter.WriteChartLegend(AChartStream, AStyleStream: TStream; - AChartIndent, AStyleIndent: Integer; AChart: TsChart; var AStyleID: Integer); -const - LEGEND_POSITION: array[TsChartLegendPosition] of string = ( - 'end', 'top', 'bottom', 'start' - ); -var - indent: String; - canOverlap: String = ''; -begin - if (not AChart.Legend.Visible) then - exit; - - if AChart.Legend.CanOverlapPlotArea then - canOverlap := 'loext:overlay="true" '; - - // Write legend properties - indent := DupeString(' ', AChartIndent); - AppendToStream(AChartStream, Format( - indent + '' + LE, - [ AStyleID, LEGEND_POSITION[AChart.Legend.Position], canOverlap ] - )); - - // Write legend style - AppendToStream(AStyleStream, - GetChartLegendStyleAsXML(AChart, AStyleIndent, AStyleID) - ); - - // Next style - inc(AStyleID); -end; - -procedure TsSpreadOpenDocWriter.WriteChartNumberStyles(AStream: TStream; - AIndent: Integer; AChart: TsChart); -var - indent: String; -begin - indent := DupeString(' ', AIndent); - - AppendToStream(AStream, - indent + '' + LE + - indent + ' ' + LE + - indent + '' + LE - ); - - if AChart.StackMode = csmStackedPercentage then - AppendToStream(AStream, - indent + '' + LE + - indent + ' ' + LE + - indent + ' %' + LE + - indent + '' + LE - ); -end; - -procedure TsSpreadOpenDocWriter.WriteChartPlotArea(AChartStream, AStyleStream: TStream; - AChartIndent, AStyleIndent: Integer; AChart: TsChart; var AStyleID: Integer); -var - indent: String; - i: Integer; -begin - indent := DupeString(' ', AChartIndent); - - // Plot area properties - // (ods has a table:cell-range-address here but it is reconstructed by Calc) - AppendToStream(AChartStream, Format( - indent + '' + LE, - [ AStyleID ] - )); - // Plot area style - AppendToStream(AStyleStream, - GetChartPlotAreaStyleAsXML(AChart, AStyleIndent, AStyleID) - ); - - // Next style - inc(AStyleID); - - // Wall properties - AppendToStream(AChartStream, Format( - indent + ' ' + LE, - [ AStyleID ] - )); - // Wall style - AppendToStream(AStyleStream, - GetChartBackgroundStyleAsXML(AChart, AChart.PlotArea.Background, AChart.PlotArea.Border, AStyleIndent, AStyleID) - ); - - // Next style - inc(AStyleID); - - // Floor properties - AppendToStream(AChartStream, Format( - indent + ' ' + LE, - [ AStyleID ] - )); - // Floor style - AppendToStream(AStyleStream, - GetChartBackgroundStyleAsXML(AChart, AChart.Floor.Background, AChart.Floor.Border, AStyleIndent, AStyleID) - ); - - // Next style - inc(AStyleID); - - // primary x axis - WriteChartAxis(AChartStream, AStyleStream, AChartIndent+2, AStyleIndent, AChart.XAxis, AStyleID); - - // primary y axis - WriteChartAxis(AChartStream, AStyleStream, AChartIndent+2, AStyleIndent, AChart.YAxis, AStyleID); - - // secondary x axis - WriteChartAxis(AChartStream, AStyleStream, AChartIndent+2, AStyleIndent, AChart.X2Axis, AStyleID); - - // secondary y axis - WriteChartAxis(AChartStream, AStyleStream, AChartIndent+2, AStyleIndent, AChart.Y2Axis, AStyleID); - - // series - for i := 0 to AChart.Series.Count-1 do - WriteChartSeries(AChartStream, AStyleStream, AChartIndent+2, AStyleIndent, AChart, i, AStyleID); - - // close xml node - AppendToStream(AChartStream, - indent + '' + LE - ); -end; - -procedure TsSpreadOpenDocWriter.WriteChartSeries(AChartStream, AStyleStream: TStream; - AChartIndent, AStyleIndent: Integer; AChart: TsChart; ASeriesIndex: Integer; - var AStyleID: Integer); -var - indent: String; - sheet: TsWorksheet; - series: TsChartSeries; - valuesRange: String; - domainRangeX: String = ''; - domainRangeY: String = ''; - rangeStr: String = ''; - titleAddr: String; - count: Integer; -begin - indent := DupeString(' ', AChartIndent); - - series := AChart.Series[ASeriesIndex]; - sheet := TsWorkbook(FWorkbook).GetWorksheetByIndex(AChart.sheetIndex); - - // These are the x values of a scatter or bubble plot. - if (series is TsScatterSeries) or (series is TsBubbleSeries) then - begin - domainRangeX := GetSheetCellRangeString_ODS( - sheet.Name, sheet.Name, - series.XRange.Row1, series.XRange.Col1, - series.XRange.Row2, series.XRange.Col2, - rfAllRel, false - ); - end; - - if series is TsBubbleSeries then - begin - domainRangeY := GetSheetCellRangeString_ODS( - sheet.Name, sheet.Name, - series.YRange.Row1, series.YRange.Col1, - series.YRange.Row2, series.YRange.Col2, - rfAllRel, false - ); - // These are the bubble radii - valuesRange := GetSheetCellRangeString_ODS( - sheet.Name, sheet.Name, - TsBubbleSeries(series).BubbleRange.Row1, TsBubbleSeries(series).BubbleRange.Col1, - TsBubbleSeries(series).BubbleRange.Row2, TsBubbleSeries(series).BubbleRange.Col2, - rfAllRel, false - ); - end else - // These are the y values of the non-bubble series - valuesRange := GetSheetCellRangeString_ODS( - sheet.Name, sheet.Name, - series.YRange.Row1, series.YRange.Col1, - series.YRange.Row2, series.YRange.Col2, - rfAllRel, false - ); - - // And these are the data point labels. - titleAddr := GetSheetCellRangeString_ODS( - sheet.Name, sheet.Name, - series.TitleAddr.Row, series.TitleAddr.Col, - series.TitleAddr.Row, series.TitleAddr.Col, - rfAllRel, false - ); - count := series.YRange.Row2 - series.YRange.Row1 + 1; - - // Store the series properties - AppendToStream(AChartStream, Format( - indent + '' + LE, - [ AStyleID, valuesRange, titleAddr, CHART_TYPE_NAMES[series.ChartType] ] - )); - if domainRangeY <> '' then - AppendToStream(AChartStream, Format( - indent + '' + LE, - [ domainRangeY ] - )); - if domainRangeX <> '' then - AppendToStream(AChartStream, Format( - indent + '' + LE, - [ domainRangeX ] - )); - - AppendToStream(AChartStream, Format( - indent + ' ' + LE, - [ count ] - )); - AppendToStream(AChartStream, - indent + '' + LE - ); - - // Series style - AppendToStream(AStyleStream, - GetChartSeriesStyleAsXML(AChart, ASeriesIndex, AStyleIndent, AStyleID) - ); - - // Next style - inc(AStyleID); -end; - -function TsSpreadOpenDocWriter.GetChartAxisStyleAsXML( - Axis: TsChartAxis; AIndent, AStyleID: Integer): String; -var - chart: TsChart; - indent: String; - angle: Integer; - textProps: String = ''; - graphProps: String = ''; - chartProps: String = ''; - numStyle: String = 'N0'; -begin - Result := ''; - if not Axis.Visible then - exit; - - chart := Axis.Chart; - - if (Axis = chart.YAxis) and (chart.StackMode = csmStackedPercentage) then - numStyle := 'N10010'; - - if Axis.ShowLabels then - chartProps := chartProps + 'chart:display-label="true" '; - - if Axis.Logarithmic then - chartProps := chartProps + 'chart:logarithmic="true" '; - - if Axis.Inverted then - chartProps := chartProps + 'chart:reverse-direction="true" '; - - angle := Axis.LabelRotation; - chartProps := chartProps + Format('style:rotation-angle="%d" ', [angle]); - - graphProps := 'svg:stroke-color="' + ColorToHTMLColorStr(Axis.AxisLine.Color) + '" '; - - textProps := WriteFontStyleXMLAsString(Axis.LabelFont); - - indent := DupeString(' ', AIndent); - Result := Format( - indent + '' + LE + - indent + ' ' + LE + - indent + ' ' + LE + - indent + ' ' + LE + - indent + '' + LE, - [ AStyleID, numStyle, chartProps, graphProps, textProps ] - ); -end; - -function TsSpreadOpenDocWriter.GetChartBackgroundStyleAsXML( - AChart: TsChart; AFill: TsChartFill; ABorder: TsChartLine; - AIndent, AStyleID: Integer): String; -var - indent: String; - fillStr: String = ''; - borderStr: String = ''; -begin - fillStr := GetChartFillStyleGraphicPropsAsXML(AChart, AFill); - borderStr := GetChartLineStyleGraphicPropsAsXML(AChart, ABorder); - indent := DupeString(' ', AIndent); - Result := Format( - indent + '' + LE + - indent + ' ' + LE + - indent + '' + LE, - [ AStyleID, fillStr, borderStr ] - ); -end; - -{ - - - - - ACaptionKind = 1 ---> Title - ACaptionKind = 2 ---> SubTitle - ACaptionKind = 3 ---> x xis - ACaptionKind = 4 ---> y axis - ACaptionKind = 5 ---> x2 axis - ACaptionKind = 6 ---> y2 axis } -function TsSpreadOpenDocWriter.GetChartCaptionStyleAsXML(AChart: TsChart; - ACaptionKind, AIndent, AStyleID: Integer): String; -var - title: TsChartText; - axis: TsChartAxis; - font: TsFont; - indent: String; - rotAngle: Integer; - chartProps: String = ''; - textProps: String = ''; -begin - Result := ''; - - case ACaptionKind of - 1, 2: - begin - if ACaptionKind = 1 then title := AChart.Title else title := AChart.Subtitle; - font := title.Font; - rotAngle := title.RotationAngle; - end; - 3, 4, 5, 6: - begin - case ACaptionKind of - 3: axis := AChart.XAxis; - 4: axis := AChart.YAxis; - 5: axis := AChart.X2Axis; - 6: axis := AChart.Y2Axis; - end; - font := axis.CaptionFont; - rotAngle := axis.CaptionRotation; - if AChart.RotatedAxes then - begin - if rotAngle = 0 then rotAngle := 90 else if rotAngle = 90 then rotAngle := 0; - end; - end; - else - raise Exception.Create('[GetChartCaptionStyleAsXML] Unknown caption.'); - end; - - chartProps := 'chart:auto-position="true" '; - chartProps := chartProps + Format('style:rotation-angle="%d" ', [rotAngle]); - - textProps := WriteFontStyleXMLAsString(font); - - indent := DupeString(' ', AIndent); - Result := Format( - indent + '' + LE + - indent + ' ' + LE + - indent + ' ' + LE + - indent + '' + LE, - [ AStyleID, chartProps, textProps ] - ); -end; - -function TsSpreadOpenDocWriter.GetChartFillStyleGraphicPropsAsXML(AChart: TsChart; - AFill: TsChartFill): String; -var - fillStr: String; - fillColorStr: String; -begin - if AFill.Style = fsNoFill then - begin - Result := 'draw:fill="none" '; - exit; - end; - - // To do: extend with hatched and gradient fills - fillStr := 'draw:fill="solid" '; - fillColorStr := 'draw:fill-color="' + ColorToHTMLColorStr(AFill.FgColor) + '" '; - - Result := fillStr + fillColorStr; -end; - -{ - - - - - -} -function TsSpreadOpenDocWriter.GetChartLegendStyleAsXML(AChart: TsChart; - AIndent, AStyleID: Integer): String; -var - indent: String; - textProps: String = ''; - graphProps: String = ''; -begin - Result := ''; - - if not AChart.Legend.Visible then - exit; - - graphProps := GetChartLineStyleGraphicPropsAsXML(AChart, AChart.Legend.Border) + - GetChartFillStyleGraphicPropsAsXML(AChart, AChart.Legend.Background); - - textProps := WriteFontStyleXMLAsString(AChart.Legend.Font); - - indent := DupeString(' ', AIndent); - Result := Format( - indent + '' + LE + - indent + ' ' + LE + - indent + ' ' + LE + - indent + ' ' + LE + - indent + '' + LE, - [ AStyleID, graphProps, textProps ] - ); -end; - -{ - - } -function TsSpreadOpenDocWriter.GetChartLineStyleAsXML(AChart: TsChart; - ALine: TsChartLine; AIndent, AStyleID: Integer): String; -var - ind: String; - graphProps: String = ''; -begin - ind := DupeString(' ', AIndent); - graphProps := GetChartLineStyleGraphicPropsAsXML(AChart, ALine); - Result := Format( - ind + '' + LE + - ind + ' ' + LE + - ind + '' + LE, - [ AStyleID, graphProps ] - ); -end; - -{ Constructs the xml for a line style to be used in the } -function TsSpreadOpenDocWriter.GetChartLineStyleGraphicPropsAsXML(AChart: TsChart; - ALine: TsChartLine): String; -var - strokeStr: String = ''; - widthStr: String = ''; - colorStr: String = ''; - s: String; - linestyle: TsChartLineStyle; -begin - if ALine.Style = clsNoLine then - begin - Result := 'draw:stroke="none" '; - exit; - end; - - strokeStr := 'draw:stroke="solid" '; - if (ALine.Style <> clsSolid) then - begin - linestyle := AChart.GetLineStyle(ALine.Style); - if linestyle <> nil then - strokeStr := 'draw:stroke="dash" draw:stroke-dash="' + ASCIIName(linestyle.Name) + '" '; - end; - - if ALine.Width > 0 then - widthStr := Format('svg:stroke-width="%.1fmm" ', [ALine.Width], FPointSeparatorSettings); - colorStr := Format('svg:stroke-color="%s" ', [ColorToHTMLColorStr(ALine.Color)]); - - Result := strokeStr + widthStr + colorStr; -end; - -function TsSpreadOpenDocWriter.GetChartPlotAreaStyleAsXML(AChart: TsChart; - AIndent, AStyleID: Integer): String; -var - indent: String; - interpolationStr: String = ''; - verticalStr: String = ''; - stackModeStr: String = ''; -begin - indent := DupeString(' ', AIndent); - - if AChart.RotatedAxes then - verticalStr := 'chart:vertical="true" '; - - case AChart.StackMode of - csmSideBySide: ; - csmStacked: stackModeStr := 'chart:stacked="true" '; - csmStackedPercentage: stackModeStr := 'chart:percentage="true" '; - end; - - case AChart.Interpolation of - ciLinear: ; - ciCubicSpline: interpolationStr := 'chart:interpolation="cubic-spline" '; - ciBSpline: interpolationStr := 'chart:interpolation="b-spline" '; - ciStepStart: interpolationStr := 'chart:interpolation="step-start" '; - ciStepEnd: interpolationStr := 'chart:interpolation="step-end" '; - ciStepCenterX: interpolationStr := 'chart:interpolation="step-center-x" '; - ciStepCenterY: interpolationStr := 'chart:interpolation="step-center-y" '; - end; - - Result := Format( - indent + ' ', [ AStyleID ]) + LE + - indent + ' ' + LE + - indent + ' ' + LE; -end; - -{ - - - - } -function TsSpreadOpenDocWriter.GetChartSeriesStyleAsXML(AChart: TsChart; - ASeriesIndex, AIndent, AStyleID: Integer): String; -var - series: TsChartSeries; - lineser: TsLineSeries = nil; - indent: String; - chartProps: String = ''; - graphProps: String = ''; - textProps: String = ''; - lineProps: String = ''; - fillProps: String = ''; -begin - Result := ''; - - indent := DupeString(' ', AIndent); - series := AChart.Series[ASeriesIndex]; - - // Chart properties - chartProps := 'chart:symbol-type="none" '; - if (series is TsLineSeries) then - begin - lineser := TsLineSeries(series); - if lineser.ShowSymbols then - chartProps := Format( - 'chart:symbol-type="named-symbol" chart:symbol-name="%s" chart:symbol-width="%.1fmm" chart.symbol-height="%.1fmm" ', - [CHART_SYMBOL_NAMES[lineSer.Symbol], lineSer.SymbolWidth, lineSer.SymbolHeight ], - FPointSeparatorSettings - ); - end; - chartProps := chartProps + 'chart:link-data-style-to-source="true" '; - - // Graphic properties - lineProps := GetChartLineStyleGraphicPropsAsXML(AChart, series.Line); - fillProps := GetChartFillStyleGraphicPropsAsXML(AChart, series.Fill); - if (series is TsLineSeries) then - begin - if lineSer.ShowSymbols then - graphProps := graphProps + fillProps; - if lineSer.ShowLines and (lineser.Line.Style <> clsNoLine) then - graphProps := graphProps + lineProps; - end else - graphProps := fillProps + lineProps; - - // Text properties - textProps := 'fo:font-size="10pt" style:font-size-asian="10pt" style:font-size-complex="10pt" '; -// textProps := WriteFontStyleXMLAsString(font); // <--- to be completed. this is for the series labels. - - Result := Format( - indent + '' + LE + - indent + ' ' + LE + - indent + ' ' + LE + - indent + ' ' + LE + - indent + '' + LE, - [ AStyleID, chartProps, graphProps, textProps ] - ); -end; - -{ Extracts the cells needed by the given chart from the chart's worksheet and - copies their values into a temporary worksheet, AWorksheet, so that these - data can be written to the xml immediately. - Independently of the layout in the original worksheet, data are arranged in - columns of AWorksheet, starting at cell A1. - - First column: Categories (or index in case of scatter chart) - - Second column: - in case of category charts: y values of the first series, - in case of scatter series: x values of the first series - - Third column: - in case of category charts: y values of the second series - in case of scatter series. y values of the first series - - etc. - The first row contains - - nothing in case of the first column - - cell range reference in ODS syntax for the cells in the original worksheet. - The aux worksheet should be contained in a separate workbook to avoid - interfering with the writing process. - } -procedure TsSpreadOpenDocWriter.PrepareChartTable(AChart: TsChart; - AWorksheet: TsBasicWorksheet); -var - isScatterChart: Boolean; - series: TsChartSeries; - seriesSheet: TsWorksheet; - auxSheet: TsWorksheet; - refStr, txt: String; - i, j: Integer; - srcCell, destCell: PCell; - destCol: Cardinal; - r1, c1, r2, c2: Cardinal; - nRows: Integer; -begin - if AChart.Series.Count = 0 then - exit; - - auxSheet := TsWorksheet(AWorksheet); - seriesSheet := TsWorkbook(FWorkbook).GetWorksheetByIndex(AChart.SheetIndex); - isScatterChart := AChart.IsScatterChart; - - // Determine the number of rows in auxiliary output worksheet. - nRows := 0; - for i := 0 to AChart.Series.Count-1 do - begin - series := AChart.Series[i]; - j := series.GetXCount; - if j > nRows then nRows := j; - j := series.GetYCount; - if j > nRows then nRows := j; - end; - - // Write label column. If missing, write consecutive numbers 1, 2, 3, ... - destCol := 0; - for i := 0 to AChart.Series.Count-1 do - begin - series := AChart.Series[i]; - - // Write the label column. Use consecutive numbers 1, 2, 3, ... if there - // are no labels. - if series.HasLabels then - begin - r1 := series.LabelRange.Row1; - c1 := series.LabelRange.Col1; - r2 := series.LabelRange.Row2; - c2 := series.LabelRange.Col2; - refStr := GetSheetCellRangeString_ODS(seriesSheet.Name, seriesSheet.Name, r1, c1, r2, c2, rfAllRel, false); - end else - refStr := ''; - - auxSheet.WriteText(0, destCol, ''); - - for j := 1 to nRows do - begin - if series.HasLabels then - begin - if series.LabelsInCol then - srcCell := seriesSheet.FindCell(r1 + j - 1, c1) - else - srcCell := seriesSheet.FindCell(r1, c1 + j - 1); - end else - srcCell := nil; - if srcCell <> nil then - begin - destCell := auxsheet.GetCell(j, destCol); - seriesSheet.CopyValue(srcCell, destCell); - end else - destCell := auxSheet.WriteNumber(j, destCol, j); - end; - if (refStr <> '') then - auxsheet.WriteComment(1, destCol, refStr); - - // In case of scatter plot write the x column. Use consecutive numbers 1, 2, 3, ... - // if there are no x values. - if isScatterChart then - begin - inc(destCol); - - if series.HasXValues then - begin - r1 := series.XRange.Row1; - c1 := series.XRange.Col1; - r2 := series.XRange.Row2; - c2 := series.XRange.Col2; - refStr := GetSheetCellRangeString_ODS(seriesSheet.Name, seriesSheet.Name, r1, c1, r2, c2, rfAllRel, false); - if series.XValuesInCol then - txt := 'Col ' + GetColString(c1) - else - txt := 'Row ' + GetRowString(r1); - end else - begin - refStr := ''; - txt := ''; - end; - - auxSheet.WriteText(0, destCol, txt); - - for j := 1 to nRows do - begin - if series.HasXValues then - begin - if series.XValuesInCol then - srcCell := seriesSheet.FindCell(r1 + j - 1, c1) - else - srcCell := seriesSheet.FindCell(r1, c1 + j - 1); - end else - srcCell := nil; - if srcCell <> nil then - begin - destCell := auxsheet.GetCell(j, destCol); - seriesSheet.CopyValue(srcCell, destCell); - end else - destCell := auxSheet.WriteNumber(j, destCol, j); - end; - if (refStr <> '') then - auxsheet.WriteComment(1, destCol, refStr); - end; - - // Write the y column - if not series.HasYValues then - Continue; - - inc(destCol); - - r1 := series.TitleAddr.Row; // Series title - c1 := series.TitleAddr.Col; - txt := seriesSheet.ReadAsText(r1, c1); - auxsheet.WriteText(0, destCol, txt); - refStr := GetSheetCellRangeString_ODS(seriesSheet.Name, seriesSheet.Name, r1, c1, r1, c1, rfAllRel, false); - if (refStr <> '') then - auxSheet.WriteComment(0, destCol, refStr); // Store title reference as comment for svg node - - r1 := series.YRange.Row1; - c1 := series.YRange.Col1; - r2 := series.YRange.Row2; - c2 := series.YRange.Col2; - refStr := GetSheetCellRangeString_ODS(seriesSheet.Name, seriesSheet.Name, r1, c1, r2, c2, rfAllRel, false); - for j := 1 to series.GetYCount do - begin - if series.YValuesInCol then - srcCell := seriesSheet.FindCell(r1 + j - 1, c1) - else - srcCell := seriesSheet.FindCell(r1, c1 + j - 1); - if srcCell <> nil then - begin - destCell := auxSheet.GetCell(j, destCol); - seriesSheet.CopyValue(srcCell, destCell); - end else - destCell := auxSheet.WriteNumber(j, destCol, j); - end; - - if (refStr <> '') then - auxSheet.WriteComment(1, destCol, refStr); // Store y range reference as comment for svg node - end; -end; - -{ Writes the chart's data table. NOTE: The chart gets its data from this table - rather than from the worksheet! } -procedure TsSpreadOpenDocWriter.WriteChartTable(AStream: TStream; AChart: TsChart; - AIndent: Integer); -var - auxBook: TsWorkbook; - auxSheet: TsWorksheet; - - procedure WriteAuxCell(AIndent: Integer; ACell: PCell); - var - ind: String; - valueType: String; - value: String; - officeValue: String; - draw: String; - begin - ind := DupeString(' ', AIndent); - if (ACell = nil) or (ACell^.ContentType = cctEmpty) then - begin - AppendToStream(AStream, - ind + '' + LE + - ind + ' ' + LE + - ind + '' + LE - ); - exit; - end; - - case ACell^.ContentType of - cctUTF8String: - begin - valueType := 'string'; - value := auxSheet.ReadAsText(ACell); - officeValue := ''; - end; - cctNumber, cctDateTime, cctBool: - begin - valueType := 'float'; - value := Format('%g', [auxSheet.ReadAsNumber(ACell)], FPointSeparatorSettings); - end; - cctError: - begin - valueType := 'float'; - value := 'NaN'; - end; - end; - - if ACell^.ContentType = cctUTF8String then - officeValue := '' - else - officeValue := ' office:value="' + value + '"'; - - if auxSheet.HasComment(ACell) then - begin - draw := auxSheet.ReadComment(ACell); - if draw <> '' then - draw := Format( - ind + ' ' + LE + - ind + ' %s' + LE + - ind + ' ' + LE, [draw]); - end else - draw := ''; - AppendToStream(AStream, Format( - ind + '' + LE + - ind + ' %s' + LE + - draw + - ind + '' + LE, - [ valueType, officevalue, value ] - )); - end; - -var - ind: String; - colCountStr: String; - n: Integer; - r, c: Cardinal; -begin - ind := DupeString(' ', AIndent); - n := AChart.Series.Count; - if n > 0 then - begin - if AChart.IsScatterChart then - n := n * 2; - colCountStr := Format('table:number-columns-repeated="%d"', [n]); - end else - colCountStr := ''; - - AppendToStream(AStream, Format( - ind + '' + LE + - ind + ' ' + LE + - ind + ' ' + LE + - ind + ' ' + LE + - ind + ' ' + LE + - ind + ' ' + LE + - ind + ' ' + LE, [ colCountStr ] - )); - - auxBook := TsWorkbook.Create; - try - auxSheet := auxBook.AddWorksheet('chart'); - PrepareChartTable(AChart, auxSheet); - - // Header rows (containing the series names) - AppendToStream(AStream, - ind + ' ' + LE + - ind + ' ' + LE ); - for c := 0 to auxSheet.GetLastColIndex do - WriteAuxCell(AIndent + 6, auxSheet.FindCell(0, c)); - AppendToStream(AStream, - ind + ' ' + LE + - ind + ' ' + LE - ); - - // Write data rows - AppendToStream(AStream, - ind + ' ' + LE - ); - for r := 1 to auxSheet.GetLastRowIndex do - begin - AppendToStream(AStream, - ind + ' ' + LE - ); - for c := 0 to auxSheet.GetlastColIndex do - WriteAuxCell(AIndent + 6, auxSheet.FindCell(r, c)); - AppendToStream(AStream, - ind + ' ' + LE - ); - end; - AppendToStream(AStream, - ind + ' ' + LE + - ind + '' + LE - ); - - //auxBook.WriteToFile('table.ods', true); - finally - auxBook.Free; - end; -end; - -{ Writes the chart's title (or subtitle, depending on the value of IsSubTitle) - to the xml stream (chart stream) and the corresponding style to the stylestream. } -procedure TsSpreadOpenDocWriter.WriteChartTitle(AChartStream, AStyleStream: TStream; - AChartIndent, AStyleIndent: Integer; AChart: TsChart; IsSubtitle: Boolean; - var AStyleID: Integer); -var - title: TsChartText; - captionKind: Integer; - elementName: String; - indent: String; -begin - if IsSubTitle then - begin - title := AChart.SubTitle; - elementName := 'subtitle'; - captionKind := 2; - end else - begin - title := AChart.Title; - elementName := 'title'; - captionKind := 1; - end; - - if (not title.Visible) or (title.Caption = '') then - exit; - - // Write title properties - indent := DupeString(' ', AChartIndent); - AppendToStream(AChartStream, Format( - indent + '' + LE + - indent + ' %s' + LE + - indent + '' + LE, - [ elementName, AStyleID, title.Caption, elementName ], FPointSeparatorSettings - )); - - // Write title style - AppendToStream(AStyleStream, - GetChartCaptionStyleAsXML(AChart, captionKind, AStyleIndent, AStyleID) - ); - - // Next style - inc(AStyleID); -end; - -{ Writes the file "Object N/styles.xml" (N = 1, 2, ...) which is needed by the - charts since it defines the line dash patterns. } -procedure TsSpreadOpenDocWriter.WriteObjectStyles(AStream: TStream; AChart: TsChart); -const - LENGTH_UNIT: array[boolean] of string = ('mm', '%'); // relative to line width - DECS: array[boolean] of Integer = (1, 0); // relative to line width -var - i: Integer; - linestyle: TsChartLineStyle; - seg1, seg2: String; -begin - AppendToStream(AStream, - XML_HEADER + LE); - - AppendToStream(AStream, - '' + LE - ); - - AppendToStream(AStream, - ' ' + LE - ); - - for i := 0 to AChart.NumLineStyles-1 do - begin - lineStyle := AChart.GetLineStyle(i); - if linestyle.Segment1.Count > 0 then - seg1 := Format('draw:dots1="%d" draw:dots1-length="%.*f%s" ', [ - lineStyle.Segment1.Count, - DECS[linestyle.RelativeToLineWidth], linestyle.Segment1.Length, LENGTH_UNIT[linestyle.RelativeToLineWidth] - ], FPointSeparatorSettings - ) - else - seg1 := ''; - - if linestyle.Segment2.Count > 0 then - seg2 := Format('draw:dots2="%d" draw:dots2-length="%.*f%s" ', [ - lineStyle.Segment2.Count, - DECS[linestyle.RelativeToLineWidth], linestyle.Segment2.Length, LENGTH_UNIT[linestyle.RelativeToLineWidth] - ], FPointSeparatorSettings - ) - else - seg2 := ''; - - if (seg1 <> '') or (seg2 <> '') then - AppendToStream(AStream, Format( - ' ' + LE, [ - ASCIIName(linestyle.Name), linestyle.Name, - DECS[linestyle.RelativeToLineWidth], linestyle.Distance, LENGTH_UNIT[linestyle.RelativeToLineWidth], - seg1, seg2 - ])); - end; - - AppendToStream(AStream, - ' ' + LE + - '' - ); -end; - { Writes the "office:automatic" > "style:style" node for table-column style family in content.xml for all column records. } procedure TsSpreadOpenDocWriter.WriteColStyles(AStream: TStream); @@ -9492,7 +8101,8 @@ constructor TsSpreadOpenDocWriter.Create(AWorkbook: TsBasicWorkbook); begin inherited Create(AWorkbook); -// FChartStyleList := TsChartStyleList.Create; + FChartWriter := TsSpreadOpenDocChartWriter.Create(self); + FColumnStyleList := TFPList.Create; FRowStyleList := TFPList.Create; FRichTextFontList := TStringList.Create; @@ -9518,6 +8128,8 @@ begin FRichTextFontList.Free; // Do not destroy fonts, they are owned by Workbook FHeaderFooterFontList.Free; + FChartWriter.Free; + inherited Destroy; end; diff --git a/components/fpspreadsheet/source/common/fpsopendocumentchart.pas b/components/fpspreadsheet/source/common/fpsopendocumentchart.pas new file mode 100644 index 000000000..41cad6025 --- /dev/null +++ b/components/fpspreadsheet/source/common/fpsopendocumentchart.pas @@ -0,0 +1,1455 @@ +unit fpsOpenDocumentChart; + +{$mode objfpc}{$H+} + +interface + +uses + Classes, SysUtils, StrUtils, + {$IF FPC_FULLVERSION >= 20701} + zipper, + {$ELSE} + fpszipper, + {$ENDIF} + fpsTypes, fpSpreadsheet, fpsChart, fpsUtils, fpsReaderWriter, fpsXMLCommon; + +type + TsSpreadOpenDocChartWriter = class(TsBasicSpreadChartWriter) + private + FSCharts: array of TStream; + FSObjectStyles: array of TStream; + FPointSeparatorSettings: TFormatSettings; + function GetChartAxisStyleAsXML(Axis: TsChartAxis; AIndent, AStyleID: Integer): String; + function GetChartBackgroundStyleAsXML(AChart: TsChart; AFill: TsChartFill; + ABorder: TsChartLine; AIndent: Integer; AStyleID: Integer): String; + function GetChartCaptionStyleAsXML(AChart: TsChart; ACaptionKind, AIndent, AStyleID: Integer): String; + function GetChartFillStyleGraphicPropsAsXML(AChart: TsChart; AFill: TsChartFill): String; + function GetChartLegendStyleAsXML(AChart: TsChart; AIndent, AStyleID: Integer): String; + function GetChartLineStyleAsXML(AChart: TsChart; ALine: TsChartLine; AIndent, AStyleID: Integer): String; + function GetChartLineStyleGraphicPropsAsXML(AChart: TsChart; ALine: TsChartLine): String; + function GetChartPlotAreaStyleAsXML(AChart: TsChart; AIndent, AStyleID: Integer): String; + function GetChartSeriesStyleAsXML(AChart: TsChart; ASeriesIndex, AIndent, AStyleID: integer): String; +// function GetChartTitleStyleAsXML(AChart: TsChart; AStyleIndex, AIndent: Integer): String; + procedure PrepareChartTable(AChart: TsChart; AWorksheet: TsBasicWorksheet); + + protected + procedure WriteChart(AStream: TStream; AChart: TsChart); + procedure WriteChartAxis(AChartStream, AStyleStream: TStream; + AChartIndent, AStyleIndent: Integer; Axis: TsChartAxis; var AStyleID: Integer); + procedure WriteChartBackground(AChartStream, AStyleStream: TStream; + AChartIndent, AStyleIndent: Integer; AChart: TsChart; var AStyleID: Integer); + procedure WriteChartLegend(AChartStream, AStyleStream: TStream; + AChartIndent, AStyleIndent: Integer; AChart: TsChart; var AStyleID: Integer); + procedure WriteChartNumberStyles(AStream: TStream; + AIndent: Integer; AChart: TsChart); + procedure WriteObjectStyles(AStream: TStream; AChart: TsChart); + procedure WriteChartPlotArea(AChartStream, AStyleStream: TStream; + AChartIndent, AStyleIndent: Integer; AChart: TsChart; var AStyleID: Integer); + procedure WriteChartSeries(AChartStream, AStyleStream: TStream; + AChartIndent, AStyleIndent: Integer; AChart: TsChart; ASeriesIndex: Integer; + var AStyleID: Integer); + procedure WriteChartTable(AStream: TStream; AChart: TsChart; AIndent: Integer); + procedure WriteChartTitle(AChartStream, AStyleStream: TStream; + AChartIndent, AStyleIndent: Integer; AChart: TsChart; IsSubtitle: Boolean; + var AStyleID: Integer); + + public + constructor Create(AWriter: TsBasicSpreadWriter); + procedure AddChartsToZip(AZip: TZipper); + procedure CreateStreams; override; + procedure DestroyStreams; override; + procedure ResetStreams; override; + procedure WriteCharts; override; + end; + +implementation + +uses + fpsOpenDocument; + +const + OPENDOC_PATH_CHART_CONTENT = 'Object %d/content.xml'; + OPENDOC_PATH_CHART_STYLES = 'Object %d/styles.xml'; + + CHART_TYPE_NAMES: array[TsChartType] of string = ( + '', 'bar', 'line', 'area', 'barLine', 'scatter', 'bubble' + ); + + CHART_SYMBOL_NAMES: array[TsChartSeriesSymbol] of String = ( + 'square', 'diamond', 'arrow-up', 'arrow-down', 'arrow-left', + 'arrow-right', 'circle', 'star', 'x', 'plus', 'asterisk' + ); // unsupported: bow-tie, hourglass, horizontal-bar, vertical-bar + + LE = LineEnding; + +function ASCIIName(AName: String): String; +var + i: Integer; +begin + Result := ''; + for i := 1 to Length(AName) do + if AName[i] in ['a'..'z', 'A'..'Z', '0'..'9'] then + Result := Result + AName[i] + else + Result := Result + Format('_%.2x_', [ord(AName[i])]); +end; + + +{------------------------------------------------------------------------------} +{ TsSpreadOpenDocChartWriter } +{------------------------------------------------------------------------------} + +constructor TsSpreadOpenDocChartWriter.Create(AWriter: TsBasicSpreadWriter); +begin + inherited Create(AWriter); + FPointSeparatorSettings := SysUtils.DefaultFormatSettings; + FPointSeparatorSettings.DecimalSeparator:='.'; +end; + +procedure TsSpreadOpenDocChartWriter.AddChartsToZip(AZip: TZipper); +var + i: Integer; +begin + for i := 0 to TsWorkbook(Writer.Workbook).GetChartCount-1 do + begin + AZip.Entries.AddFileEntry( + FSCharts[i], Format(OPENDOC_PATH_CHART_CONTENT, [i+1])); + AZip.Entries.AddFileEntry( + FSObjectStyles[i], Format(OPENDOC_PATH_CHART_STYLES, [i+1])); + end; +end; + +procedure TsSpreadOpenDocChartWriter.CreateStreams; +var + i, n: Integer; +begin + n := TsWorkbook(Writer.Workbook).GetChartCount; + SetLength(FSCharts, n); + SetLength(FSObjectStyles, n); + for i := 0 to n - 1 do + begin + FSCharts[i] := CreateTempStream(Writer.Workbook, 'fpsCh'); + FSObjectStyles[i] := CreateTempStream(Writer.Workbook, 'fpsOS'); + end; +end; + +procedure TsSpreadOpenDocChartWriter.DestroyStreams; +var + i: Integer; +begin + for i := 0 to High(FSCharts) do + begin + DestroyTempStream(FSCharts[i]); + DestroyTempStream(FSObjectStyles[i]); + end; + Setlength(FSCharts, 0); + SetLength(FSObjectStyles, 0); +end; + +function TsSpreadOpenDocChartWriter.GetChartAxisStyleAsXML( + Axis: TsChartAxis; AIndent, AStyleID: Integer): String; +var + chart: TsChart; + indent: String; + angle: Integer; + textProps: String = ''; + graphProps: String = ''; + chartProps: String = ''; + numStyle: String = 'N0'; +begin + Result := ''; + if not Axis.Visible then + exit; + + chart := Axis.Chart; + + if (Axis = chart.YAxis) and (chart.StackMode = csmStackedPercentage) then + numStyle := 'N10010'; + + if Axis.ShowLabels then + chartProps := chartProps + 'chart:display-label="true" '; + + if Axis.Logarithmic then + chartProps := chartProps + 'chart:logarithmic="true" '; + + if Axis.Inverted then + chartProps := chartProps + 'chart:reverse-direction="true" '; + + angle := Axis.LabelRotation; + chartProps := chartProps + Format('style:rotation-angle="%d" ', [angle]); + + graphProps := 'svg:stroke-color="' + ColorToHTMLColorStr(Axis.AxisLine.Color) + '" '; + + textProps := TsSpreadOpenDocWriter(Writer).WriteFontStyleXMLAsString(Axis.LabelFont); + + indent := DupeString(' ', AIndent); + Result := Format( + indent + '' + LE + + indent + ' ' + LE + + indent + ' ' + LE + + indent + ' ' + LE + + indent + '' + LE, + [ AStyleID, numStyle, chartProps, graphProps, textProps ] + ); +end; + +function TsSpreadOpenDocChartWriter.GetChartBackgroundStyleAsXML( + AChart: TsChart; AFill: TsChartFill; ABorder: TsChartLine; + AIndent, AStyleID: Integer): String; +var + indent: String; + fillStr: String = ''; + borderStr: String = ''; +begin + fillStr := GetChartFillStyleGraphicPropsAsXML(AChart, AFill); + borderStr := GetChartLineStyleGraphicPropsAsXML(AChart, ABorder); + indent := DupeString(' ', AIndent); + Result := Format( + indent + '' + LE + + indent + ' ' + LE + + indent + '' + LE, + [ AStyleID, fillStr, borderStr ] + ); +end; + +{ + + + + + ACaptionKind = 1 ---> Title + ACaptionKind = 2 ---> SubTitle + ACaptionKind = 3 ---> x xis + ACaptionKind = 4 ---> y axis + ACaptionKind = 5 ---> x2 axis + ACaptionKind = 6 ---> y2 axis } +function TsSpreadOpenDocChartWriter.GetChartCaptionStyleAsXML(AChart: TsChart; + ACaptionKind, AIndent, AStyleID: Integer): String; +var + title: TsChartText; + axis: TsChartAxis; + font: TsFont; + indent: String; + rotAngle: Integer; + chartProps: String = ''; + textProps: String = ''; +begin + Result := ''; + + case ACaptionKind of + 1, 2: + begin + if ACaptionKind = 1 then title := AChart.Title else title := AChart.Subtitle; + font := title.Font; + rotAngle := title.RotationAngle; + end; + 3, 4, 5, 6: + begin + case ACaptionKind of + 3: axis := AChart.XAxis; + 4: axis := AChart.YAxis; + 5: axis := AChart.X2Axis; + 6: axis := AChart.Y2Axis; + end; + font := axis.CaptionFont; + rotAngle := axis.CaptionRotation; + if AChart.RotatedAxes then + begin + if rotAngle = 0 then rotAngle := 90 else if rotAngle = 90 then rotAngle := 0; + end; + end; + else + raise Exception.Create('[GetChartCaptionStyleAsXML] Unknown caption.'); + end; + + chartProps := 'chart:auto-position="true" '; + chartProps := chartProps + Format('style:rotation-angle="%d" ', [rotAngle]); + + textProps := TsSpreadOpenDocWriter(Writer).WriteFontStyleXMLAsString(font); + + indent := DupeString(' ', AIndent); + Result := Format( + indent + '' + LE + + indent + ' ' + LE + + indent + ' ' + LE + + indent + '' + LE, + [ AStyleID, chartProps, textProps ] + ); +end; + +function TsSpreadOpenDocChartWriter.GetChartFillStyleGraphicPropsAsXML(AChart: TsChart; + AFill: TsChartFill): String; +var + fillStr: String; + fillColorStr: String; +begin + if AFill.Style = fsNoFill then + begin + Result := 'draw:fill="none" '; + exit; + end; + + // To do: extend with hatched and gradient fills + fillStr := 'draw:fill="solid" '; + fillColorStr := 'draw:fill-color="' + ColorToHTMLColorStr(AFill.FgColor) + '" '; + + Result := fillStr + fillColorStr; +end; + +{ + + + + + +} +function TsSpreadOpenDocChartWriter.GetChartLegendStyleAsXML(AChart: TsChart; + AIndent, AStyleID: Integer): String; +var + indent: String; + textProps: String = ''; + graphProps: String = ''; +begin + Result := ''; + + if not AChart.Legend.Visible then + exit; + + graphProps := GetChartLineStyleGraphicPropsAsXML(AChart, AChart.Legend.Border) + + GetChartFillStyleGraphicPropsAsXML(AChart, AChart.Legend.Background); + + textProps := TsSpreadOpenDocWriter(Writer).WriteFontStyleXMLAsString(AChart.Legend.Font); + + indent := DupeString(' ', AIndent); + Result := Format( + indent + '' + LE + + indent + ' ' + LE + + indent + ' ' + LE + + indent + ' ' + LE + + indent + '' + LE, + [ AStyleID, graphProps, textProps ] + ); +end; + +{ + + } +function TsSpreadOpenDocChartWriter.GetChartLineStyleAsXML(AChart: TsChart; + ALine: TsChartLine; AIndent, AStyleID: Integer): String; +var + ind: String; + graphProps: String = ''; +begin + ind := DupeString(' ', AIndent); + graphProps := GetChartLineStyleGraphicPropsAsXML(AChart, ALine); + Result := Format( + ind + '' + LE + + ind + ' ' + LE + + ind + '' + LE, + [ AStyleID, graphProps ] + ); +end; + +{ Constructs the xml for a line style to be used in the } +function TsSpreadOpenDocChartWriter.GetChartLineStyleGraphicPropsAsXML( + AChart: TsChart; ALine: TsChartLine): String; +var + strokeStr: String = ''; + widthStr: String = ''; + colorStr: String = ''; + s: String; + linestyle: TsChartLineStyle; +begin + if ALine.Style = clsNoLine then + begin + Result := 'draw:stroke="none" '; + exit; + end; + + strokeStr := 'draw:stroke="solid" '; + if (ALine.Style <> clsSolid) then + begin + linestyle := AChart.GetLineStyle(ALine.Style); + if linestyle <> nil then + strokeStr := 'draw:stroke="dash" draw:stroke-dash="' + ASCIIName(linestyle.Name) + '" '; + end; + + if ALine.Width > 0 then + widthStr := Format('svg:stroke-width="%.1fmm" ', [ALine.Width], FPointSeparatorSettings); + colorStr := Format('svg:stroke-color="%s" ', [ColorToHTMLColorStr(ALine.Color)]); + + Result := strokeStr + widthStr + colorStr; +end; + +function TsSpreadOpenDocChartWriter.GetChartPlotAreaStyleAsXML(AChart: TsChart; + AIndent, AStyleID: Integer): String; +var + indent: String; + interpolationStr: String = ''; + verticalStr: String = ''; + stackModeStr: String = ''; +begin + indent := DupeString(' ', AIndent); + + if AChart.RotatedAxes then + verticalStr := 'chart:vertical="true" '; + + case AChart.StackMode of + csmSideBySide: ; + csmStacked: stackModeStr := 'chart:stacked="true" '; + csmStackedPercentage: stackModeStr := 'chart:percentage="true" '; + end; + + case AChart.Interpolation of + ciLinear: ; + ciCubicSpline: interpolationStr := 'chart:interpolation="cubic-spline" '; + ciBSpline: interpolationStr := 'chart:interpolation="b-spline" '; + ciStepStart: interpolationStr := 'chart:interpolation="step-start" '; + ciStepEnd: interpolationStr := 'chart:interpolation="step-end" '; + ciStepCenterX: interpolationStr := 'chart:interpolation="step-center-x" '; + ciStepCenterY: interpolationStr := 'chart:interpolation="step-center-y" '; + end; + + Result := Format( + indent + ' ', [ AStyleID ]) + LE + + indent + ' ' + LE + + indent + ' ' + LE; +end; + +{ + + + + } +function TsSpreadOpenDocChartWriter.GetChartSeriesStyleAsXML(AChart: TsChart; + ASeriesIndex, AIndent, AStyleID: Integer): String; +var + series: TsChartSeries; + lineser: TsLineSeries = nil; + indent: String; + chartProps: String = ''; + graphProps: String = ''; + textProps: String = ''; + lineProps: String = ''; + fillProps: String = ''; +begin + Result := ''; + + indent := DupeString(' ', AIndent); + series := AChart.Series[ASeriesIndex]; + + // Chart properties + chartProps := 'chart:symbol-type="none" '; + if (series is TsLineSeries) then + begin + lineser := TsLineSeries(series); + if lineser.ShowSymbols then + chartProps := Format( + 'chart:symbol-type="named-symbol" chart:symbol-name="%s" chart:symbol-width="%.1fmm" chart.symbol-height="%.1fmm" ', + [CHART_SYMBOL_NAMES[lineSer.Symbol], lineSer.SymbolWidth, lineSer.SymbolHeight ], + FPointSeparatorSettings + ); + end; + chartProps := chartProps + 'chart:link-data-style-to-source="true" '; + + // Graphic properties + lineProps := GetChartLineStyleGraphicPropsAsXML(AChart, series.Line); + fillProps := GetChartFillStyleGraphicPropsAsXML(AChart, series.Fill); + if (series is TsLineSeries) then + begin + if lineSer.ShowSymbols then + graphProps := graphProps + fillProps; + if lineSer.ShowLines and (lineser.Line.Style <> clsNoLine) then + graphProps := graphProps + lineProps; + end else + graphProps := fillProps + lineProps; + + // Text properties + textProps := 'fo:font-size="10pt" style:font-size-asian="10pt" style:font-size-complex="10pt" '; +// textProps := WriteFontStyleXMLAsString(font); // <--- to be completed. this is for the series labels. + + Result := Format( + indent + '' + LE + + indent + ' ' + LE + + indent + ' ' + LE + + indent + ' ' + LE + + indent + '' + LE, + [ AStyleID, chartProps, graphProps, textProps ] + ); +end; + +{ Extracts the cells needed by the given chart from the chart's worksheet and + copies their values into a temporary worksheet, AWorksheet, so that these + data can be written to the xml immediately. + Independently of the layout in the original worksheet, data are arranged in + columns of AWorksheet, starting at cell A1. + - First column: Categories (or index in case of scatter chart) + - Second column: + in case of category charts: y values of the first series, + in case of scatter series: x values of the first series + - Third column: + in case of category charts: y values of the second series + in case of scatter series. y values of the first series + - etc. + The first row contains + - nothing in case of the first column + - cell range reference in ODS syntax for the cells in the original worksheet. + The aux worksheet should be contained in a separate workbook to avoid + interfering with the writing process. + } +procedure TsSpreadOpenDocChartWriter.PrepareChartTable(AChart: TsChart; + AWorksheet: TsBasicWorksheet); +var + isScatterChart: Boolean; + series: TsChartSeries; + seriesSheet: TsWorksheet; + auxSheet: TsWorksheet; + refStr, txt: String; + i, j: Integer; + srcCell, destCell: PCell; + destCol: Cardinal; + r1, c1, r2, c2: Cardinal; + nRows: Integer; +begin + if AChart.Series.Count = 0 then + exit; + + auxSheet := TsWorksheet(AWorksheet); + seriesSheet := TsWorkbook(Writer.Workbook).GetWorksheetByIndex(AChart.SheetIndex); + isScatterChart := AChart.IsScatterChart; + + // Determine the number of rows in auxiliary output worksheet. + nRows := 0; + for i := 0 to AChart.Series.Count-1 do + begin + series := AChart.Series[i]; + j := series.GetXCount; + if j > nRows then nRows := j; + j := series.GetYCount; + if j > nRows then nRows := j; + end; + + // Write label column. If missing, write consecutive numbers 1, 2, 3, ... + destCol := 0; + for i := 0 to AChart.Series.Count-1 do + begin + series := AChart.Series[i]; + + // Write the label column. Use consecutive numbers 1, 2, 3, ... if there + // are no labels. + if series.HasLabels then + begin + r1 := series.LabelRange.Row1; + c1 := series.LabelRange.Col1; + r2 := series.LabelRange.Row2; + c2 := series.LabelRange.Col2; + refStr := GetSheetCellRangeString_ODS(seriesSheet.Name, seriesSheet.Name, r1, c1, r2, c2, rfAllRel, false); + end else + refStr := ''; + + auxSheet.WriteText(0, destCol, ''); + + for j := 1 to nRows do + begin + if series.HasLabels then + begin + if series.LabelsInCol then + srcCell := seriesSheet.FindCell(r1 + j - 1, c1) + else + srcCell := seriesSheet.FindCell(r1, c1 + j - 1); + end else + srcCell := nil; + if srcCell <> nil then + begin + destCell := auxsheet.GetCell(j, destCol); + seriesSheet.CopyValue(srcCell, destCell); + end else + destCell := auxSheet.WriteNumber(j, destCol, j); + end; + if (refStr <> '') then + auxsheet.WriteComment(1, destCol, refStr); + + // In case of scatter plot write the x column. Use consecutive numbers 1, 2, 3, ... + // if there are no x values. + if isScatterChart then + begin + inc(destCol); + + if series.HasXValues then + begin + r1 := series.XRange.Row1; + c1 := series.XRange.Col1; + r2 := series.XRange.Row2; + c2 := series.XRange.Col2; + refStr := GetSheetCellRangeString_ODS(seriesSheet.Name, seriesSheet.Name, r1, c1, r2, c2, rfAllRel, false); + if series.XValuesInCol then + txt := 'Col ' + GetColString(c1) + else + txt := 'Row ' + GetRowString(r1); + end else + begin + refStr := ''; + txt := ''; + end; + + auxSheet.WriteText(0, destCol, txt); + + for j := 1 to nRows do + begin + if series.HasXValues then + begin + if series.XValuesInCol then + srcCell := seriesSheet.FindCell(r1 + j - 1, c1) + else + srcCell := seriesSheet.FindCell(r1, c1 + j - 1); + end else + srcCell := nil; + if srcCell <> nil then + begin + destCell := auxsheet.GetCell(j, destCol); + seriesSheet.CopyValue(srcCell, destCell); + end else + destCell := auxSheet.WriteNumber(j, destCol, j); + end; + if (refStr <> '') then + auxsheet.WriteComment(1, destCol, refStr); + end; + + // Write the y column + if not series.HasYValues then + Continue; + + inc(destCol); + + r1 := series.TitleAddr.Row; // Series title + c1 := series.TitleAddr.Col; + txt := seriesSheet.ReadAsText(r1, c1); + auxsheet.WriteText(0, destCol, txt); + refStr := GetSheetCellRangeString_ODS(seriesSheet.Name, seriesSheet.Name, r1, c1, r1, c1, rfAllRel, false); + if (refStr <> '') then + auxSheet.WriteComment(0, destCol, refStr); // Store title reference as comment for svg node + + r1 := series.YRange.Row1; + c1 := series.YRange.Col1; + r2 := series.YRange.Row2; + c2 := series.YRange.Col2; + refStr := GetSheetCellRangeString_ODS(seriesSheet.Name, seriesSheet.Name, r1, c1, r2, c2, rfAllRel, false); + for j := 1 to series.GetYCount do + begin + if series.YValuesInCol then + srcCell := seriesSheet.FindCell(r1 + j - 1, c1) + else + srcCell := seriesSheet.FindCell(r1, c1 + j - 1); + if srcCell <> nil then + begin + destCell := auxSheet.GetCell(j, destCol); + seriesSheet.CopyValue(srcCell, destCell); + end else + destCell := auxSheet.WriteNumber(j, destCol, j); + end; + + if (refStr <> '') then + auxSheet.WriteComment(1, destCol, refStr); // Store y range reference as comment for svg node + end; +end; + +procedure TsSpreadOpenDocChartWriter.ResetStreams; +var + i: Integer; +begin + for i := 0 to High(FSCharts) do + begin + FSCharts[i].Position := 0; + FSObjectStyles[i].Position := 0; + end; +end; + +{ Writes the chart to the specified stream. + All chart elements are formatted by means of styles. To simplify assignment + of styles to elements we first write the elements and create the style of + the currently written chart element on the fly. Since styles must be written + to the steam first, we write the chart elements to a separate stream which + is appended to the main stream afterwards. } +procedure TsSpreadOpenDocChartWriter.WriteChart(AStream: TStream; AChart: TsChart); +var + chartStream: TMemoryStream; + styleStream: TMemoryStream; + styleID: Integer; +begin +// FChartStyleList.Clear; + + chartStream := TMemoryStream.Create; + styleStream := TMemoryStream.Create; + try + WriteChartNumberStyles(styleStream, 4, AChart); + + styleID := 1; + WriteChartBackground(chartStream, styleStream, 6, 4, AChart, styleID); + WriteChartTitle(chartStream, styleStream, 6, 4, AChart, false, styleID); // Title + WriteChartTitle(chartStream, styleStream, 6, 4, AChart, true, styleID); // Subtitle + WriteChartLegend(chartStream, styleStream, 6, 4, AChart, styleID); // Legend + WriteChartPlotArea(chartStream, styleStream, 6, 4, AChart, styleID); // Wall, axes, series + + // Here begins the main stream + AppendToStream(AStream, + XML_HEADER + LE); + + AppendToStream(AStream, + '' + LE + ); + + // The file begins with the chart styles + AppendToStream(AStream, + ' ' + LE + ); + + // Copy the styles from the temporary style stream. + styleStream.Position := 0; + AStream.CopyFrom(styleStream, stylestream.Size); + + // Now the chart part follows, after closing the styles part + AppendToStream(AStream, + ' ' + LE + + ' ' + LE + + ' ' + LE + ); + + // Copy the chart elements from the temporary chart stream + chartStream.Position := 0; + AStream.CopyFrom(chartStream, chartStream.Size); + + // After the chart elements we have the data to be plotted + WriteChartTable(AStream, AChart, 8); + + // Finally the footer. + AppendToStream(AStream, + ' ' + LE + + ' ' + LE + + ' ' + LE + + ''); + finally + chartStream.Free; + styleStream.Free; + end; +end; + +procedure TsSpreadOpenDocChartWriter.WriteChartAxis( + AChartStream, AStyleStream: TStream; + AChartIndent, AStyleIndent: Integer; Axis: TsChartAxis; + var AStyleID: Integer); +type + TAxisKind = 3..6; +const + AXIS_ID: array[TAxisKind] of string = ('x', 'y', 'x', 'y'); + AXIS_LEVEL: array[TAxisKind] of string = ('primary', 'primary', 'secondary', 'secondary'); +var + indent: String; + captionKind: Integer; + chart: TsChart; + series: TsChartSeries; + sheet: TsWorksheet; + refStr: String; + r1, c1, r2, c2: Cardinal; +begin + if not Axis.Visible then + exit; + + chart := Axis.Chart; + + if Axis = chart.XAxis then + captionKind := 3 + else if Axis = chart.YAxis then + captionKind := 4 + else if Axis = chart.X2Axis then + captionKind := 5 + else if Axis = chart.Y2Axis then + captionKind := 6 + else + raise Exception.Create('[WriteChartAxis] Unknown axis'); + + // Write axis + indent := DupeString(' ', AChartIndent); + AppendToStream(AChartStream, Format( + indent + '' + LE, + [ AStyleID, AXIS_ID[captionKind], AXIS_LEVEL[captionKind], AXIS_ID[captionKind] ] + )); + + if (Axis = chart.XAxis) and (not chart.IsScatterChart) and (chart.Series.Count > 0) then + begin + series := chart.Series[0]; + sheet := TsWorkbook(Writer.Workbook).GetWorksheetByIndex(chart.SheetIndex); + r1 := series.LabelRange.Row1; + c1 := series.LabelRange.Col1; + r2 := series.LabelRange.Row2; + c2 := series.LabelRange.Col2; + refStr := GetSheetCellRangeString_ODS(sheet.Name, sheet.Name, r1, c1, r2, c2, rfAllRel, false); + AppendToStream(AChartStream, Format( + indent + ' ' + LE, + [ refStr ] + )); + end; + + // Write axis style + AppendToStream(AStyleStream, + GetChartAxisStyleAsXML(Axis, AStyleIndent, AStyleID) + ); + + // Next style + inc(AStyleID); + + // Axis title + if Axis.ShowCaption and (Axis.Caption <> '') then + begin + AppendToStream(AChartStream, Format( + indent + ' ' + LE + + indent + ' %s' + LE + + indent + ' ' + LE, + [ AStyleID, Axis.Caption ] + )); + + // Axis title style + AppendToStream(AStyleStream, + GetChartCaptionStyleAsXML(chart, captionKind, AStyleIndent, AStyleID) + ); + + // Next style + inc(AStyleID); + end; + + // Major grid lines + if Axis.MajorGridLines.Style <> clsNoLine then + begin + AppendToStream(AChartStream, Format( + indent + ' ' + LE, + [ AStyleID ] + )); + + // Major grid lines style + AppendToStream(AStyleStream, + GetChartLineStyleAsXML(chart, Axis.MajorGridLines, AStyleIndent, AStyleID) + ); + + // Next style + inc(AStyleID); + end; + + // Minor grid lines + if Axis.MinorGridLines.Style <> clsNoLine then + begin + AppendToStream(AChartStream, Format( + indent + ' ' + LE, + [ AStyleID ] + )); + + // Minor grid lines style + AppendToStream(AStyleStream, + GetChartLineStyleAsXML(chart, Axis.MinorGridLines, AStyleIndent, AStyleID) + ); + + // Next style + inc(AStyleID); + end; + + // Close the xml node + AppendToStream(AChartStream, + indent + '' + LE + ); +end; + +{ Writes the chart's background to the xml stream } +procedure TsSpreadOpenDocChartWriter.WriteChartBackground( + AChartStream, AStyleStream: TStream; + AChartIndent, AStyleIndent: Integer; AChart: TsChart; var AStyleID: Integer); +var + indent: String; + chartClass: String; +begin + chartClass := CHART_TYPE_NAMES[AChart.GetChartType]; + if chartClass <> '' then + chartClass := 'chart:class="chart:' + chartClass + '"'; + + indent := DupeString(' ', AChartIndent); + AppendToStream(AChartStream, Format( + indent + '' + LE, [ + AStyleID, + chartClass, + AChart.Width, AChart.Height // Width, Height are in mm + ], FPointSeparatorSettings + )); + + AppendToStream(AStyleStream, + GetChartBackgroundStyleAsXML(AChart, AChart.Background, AChart.Border, AStyleIndent, AStyleID) + ); + + inc(AStyleID); +end; + +{ Writes the chart's legend to the xml stream } +procedure TsSpreadOpenDocChartWriter.WriteChartLegend(AChartStream, AStyleStream: TStream; + AChartIndent, AStyleIndent: Integer; AChart: TsChart; var AStyleID: Integer); +const + LEGEND_POSITION: array[TsChartLegendPosition] of string = ( + 'end', 'top', 'bottom', 'start' + ); +var + indent: String; + canOverlap: String = ''; +begin + if (not AChart.Legend.Visible) then + exit; + + if AChart.Legend.CanOverlapPlotArea then + canOverlap := 'loext:overlay="true" '; + + // Write legend properties + indent := DupeString(' ', AChartIndent); + AppendToStream(AChartStream, Format( + indent + '' + LE, + [ AStyleID, LEGEND_POSITION[AChart.Legend.Position], canOverlap ] + )); + + // Write legend style + AppendToStream(AStyleStream, + GetChartLegendStyleAsXML(AChart, AStyleIndent, AStyleID) + ); + + // Next style + inc(AStyleID); +end; + +procedure TsSpreadOpenDocChartWriter.WriteChartNumberStyles(AStream: TStream; + AIndent: Integer; AChart: TsChart); +var + indent: String; +begin + indent := DupeString(' ', AIndent); + + AppendToStream(AStream, + indent + '' + LE + + indent + ' ' + LE + + indent + '' + LE + ); + + if AChart.StackMode = csmStackedPercentage then + AppendToStream(AStream, + indent + '' + LE + + indent + ' ' + LE + + indent + ' %' + LE + + indent + '' + LE + ); +end; + +{ Writes the file "Object N/styles.xml" (N = 1, 2, ...) which is needed by the + charts since it defines the line dash patterns. } +procedure TsSpreadOpenDocChartWriter.WriteObjectStyles(AStream: TStream; + AChart: TsChart); +const + LENGTH_UNIT: array[boolean] of string = ('mm', '%'); // relative to line width + DECS: array[boolean] of Integer = (1, 0); // relative to line width +var + i: Integer; + linestyle: TsChartLineStyle; + seg1, seg2: String; +begin + AppendToStream(AStream, + XML_HEADER + LE); + + AppendToStream(AStream, + '' + LE + ); + + AppendToStream(AStream, + ' ' + LE + ); + + for i := 0 to AChart.NumLineStyles-1 do + begin + lineStyle := AChart.GetLineStyle(i); + if linestyle.Segment1.Count > 0 then + seg1 := Format('draw:dots1="%d" draw:dots1-length="%.*f%s" ', [ + lineStyle.Segment1.Count, + DECS[linestyle.RelativeToLineWidth], linestyle.Segment1.Length, LENGTH_UNIT[linestyle.RelativeToLineWidth] + ], FPointSeparatorSettings + ) + else + seg1 := ''; + + if linestyle.Segment2.Count > 0 then + seg2 := Format('draw:dots2="%d" draw:dots2-length="%.*f%s" ', [ + lineStyle.Segment2.Count, + DECS[linestyle.RelativeToLineWidth], linestyle.Segment2.Length, LENGTH_UNIT[linestyle.RelativeToLineWidth] + ], FPointSeparatorSettings + ) + else + seg2 := ''; + + if (seg1 <> '') or (seg2 <> '') then + AppendToStream(AStream, Format( + ' ' + LE, [ + ASCIIName(linestyle.Name), linestyle.Name, + DECS[linestyle.RelativeToLineWidth], linestyle.Distance, LENGTH_UNIT[linestyle.RelativeToLineWidth], + seg1, seg2 + ])); + end; + + AppendToStream(AStream, + ' ' + LE + + '' + ); +end; + +procedure TsSpreadOpenDocChartWriter.WriteChartPlotArea(AChartStream, AStyleStream: TStream; + AChartIndent, AStyleIndent: Integer; AChart: TsChart; var AStyleID: Integer); +var + indent: String; + i: Integer; +begin + indent := DupeString(' ', AChartIndent); + + // Plot area properties + // (ods has a table:cell-range-address here but it is reconstructed by Calc) + AppendToStream(AChartStream, Format( + indent + '' + LE, + [ AStyleID ] + )); + // Plot area style + AppendToStream(AStyleStream, + GetChartPlotAreaStyleAsXML(AChart, AStyleIndent, AStyleID) + ); + + // Next style + inc(AStyleID); + + // Wall properties + AppendToStream(AChartStream, Format( + indent + ' ' + LE, + [ AStyleID ] + )); + // Wall style + AppendToStream(AStyleStream, + GetChartBackgroundStyleAsXML(AChart, AChart.PlotArea.Background, AChart.PlotArea.Border, AStyleIndent, AStyleID) + ); + + // Next style + inc(AStyleID); + + // Floor properties + AppendToStream(AChartStream, Format( + indent + ' ' + LE, + [ AStyleID ] + )); + // Floor style + AppendToStream(AStyleStream, + GetChartBackgroundStyleAsXML(AChart, AChart.Floor.Background, AChart.Floor.Border, AStyleIndent, AStyleID) + ); + + // Next style + inc(AStyleID); + + // primary x axis + WriteChartAxis(AChartStream, AStyleStream, AChartIndent+2, AStyleIndent, AChart.XAxis, AStyleID); + + // primary y axis + WriteChartAxis(AChartStream, AStyleStream, AChartIndent+2, AStyleIndent, AChart.YAxis, AStyleID); + + // secondary x axis + WriteChartAxis(AChartStream, AStyleStream, AChartIndent+2, AStyleIndent, AChart.X2Axis, AStyleID); + + // secondary y axis + WriteChartAxis(AChartStream, AStyleStream, AChartIndent+2, AStyleIndent, AChart.Y2Axis, AStyleID); + + // series + for i := 0 to AChart.Series.Count-1 do + WriteChartSeries(AChartStream, AStyleStream, AChartIndent+2, AStyleIndent, AChart, i, AStyleID); + + // close xml node + AppendToStream(AChartStream, + indent + '' + LE + ); +end; + +procedure TsSpreadOpenDocChartWriter.WriteChartSeries( + AChartStream, AStyleStream: TStream; + AChartIndent, AStyleIndent: Integer; AChart: TsChart; ASeriesIndex: Integer; + var AStyleID: Integer); +var + indent: String; + sheet: TsWorksheet; + series: TsChartSeries; + valuesRange: String; + domainRangeX: String = ''; + domainRangeY: String = ''; + rangeStr: String = ''; + titleAddr: String; + count: Integer; +begin + indent := DupeString(' ', AChartIndent); + + series := AChart.Series[ASeriesIndex]; + sheet := TsWorkbook(Writer.Workbook).GetWorksheetByIndex(AChart.sheetIndex); + + // These are the x values of a scatter or bubble plot. + if (series is TsScatterSeries) or (series is TsBubbleSeries) then + begin + domainRangeX := GetSheetCellRangeString_ODS( + sheet.Name, sheet.Name, + series.XRange.Row1, series.XRange.Col1, + series.XRange.Row2, series.XRange.Col2, + rfAllRel, false + ); + end; + + if series is TsBubbleSeries then + begin + domainRangeY := GetSheetCellRangeString_ODS( + sheet.Name, sheet.Name, + series.YRange.Row1, series.YRange.Col1, + series.YRange.Row2, series.YRange.Col2, + rfAllRel, false + ); + // These are the bubble radii + valuesRange := GetSheetCellRangeString_ODS( + sheet.Name, sheet.Name, + TsBubbleSeries(series).BubbleRange.Row1, TsBubbleSeries(series).BubbleRange.Col1, + TsBubbleSeries(series).BubbleRange.Row2, TsBubbleSeries(series).BubbleRange.Col2, + rfAllRel, false + ); + end else + // These are the y values of the non-bubble series + valuesRange := GetSheetCellRangeString_ODS( + sheet.Name, sheet.Name, + series.YRange.Row1, series.YRange.Col1, + series.YRange.Row2, series.YRange.Col2, + rfAllRel, false + ); + + // And these are the data point labels. + titleAddr := GetSheetCellRangeString_ODS( + sheet.Name, sheet.Name, + series.TitleAddr.Row, series.TitleAddr.Col, + series.TitleAddr.Row, series.TitleAddr.Col, + rfAllRel, false + ); + count := series.YRange.Row2 - series.YRange.Row1 + 1; + + // Store the series properties + AppendToStream(AChartStream, Format( + indent + '' + LE, + [ AStyleID, valuesRange, titleAddr, CHART_TYPE_NAMES[series.ChartType] ] + )); + if domainRangeY <> '' then + AppendToStream(AChartStream, Format( + indent + '' + LE, + [ domainRangeY ] + )); + if domainRangeX <> '' then + AppendToStream(AChartStream, Format( + indent + '' + LE, + [ domainRangeX ] + )); + + AppendToStream(AChartStream, Format( + indent + ' ' + LE, + [ count ] + )); + AppendToStream(AChartStream, + indent + '' + LE + ); + + // Series style + AppendToStream(AStyleStream, + GetChartSeriesStyleAsXML(AChart, ASeriesIndex, AStyleIndent, AStyleID) + ); + + // Next style + inc(AStyleID); +end; + +procedure TsSpreadOpenDocChartWriter.WriteCharts; +var + i: Integer; + chart: TsChart; +begin + for i := 0 to TsWorkbook(Writer.Workbook).GetChartCount - 1 do + begin + chart := TsWorkbook(Writer.Workbook).GetChartByIndex(i); + WriteChart(FSCharts[i], chart); + end; +end; + +{ Writes the chart's data table. NOTE: The chart gets its data from this table + rather than from the worksheet! } +procedure TsSpreadOpenDocChartWriter.WriteChartTable(AStream: TStream; + AChart: TsChart; AIndent: Integer); +var + auxBook: TsWorkbook; + auxSheet: TsWorksheet; + + procedure WriteAuxCell(AIndent: Integer; ACell: PCell); + var + ind: String; + valueType: String; + value: String; + officeValue: String; + draw: String; + begin + ind := DupeString(' ', AIndent); + if (ACell = nil) or (ACell^.ContentType = cctEmpty) then + begin + AppendToStream(AStream, + ind + '' + LE + + ind + ' ' + LE + + ind + '' + LE + ); + exit; + end; + + case ACell^.ContentType of + cctUTF8String: + begin + valueType := 'string'; + value := auxSheet.ReadAsText(ACell); + officeValue := ''; + end; + cctNumber, cctDateTime, cctBool: + begin + valueType := 'float'; + value := Format('%g', [auxSheet.ReadAsNumber(ACell)], FPointSeparatorSettings); + end; + cctError: + begin + valueType := 'float'; + value := 'NaN'; + end; + end; + + if ACell^.ContentType = cctUTF8String then + officeValue := '' + else + officeValue := ' office:value="' + value + '"'; + + if auxSheet.HasComment(ACell) then + begin + draw := auxSheet.ReadComment(ACell); + if draw <> '' then + draw := Format( + ind + ' ' + LE + + ind + ' %s' + LE + + ind + ' ' + LE, [draw]); + end else + draw := ''; + AppendToStream(AStream, Format( + ind + '' + LE + + ind + ' %s' + LE + + draw + + ind + '' + LE, + [ valueType, officevalue, value ] + )); + end; + +var + ind: String; + colCountStr: String; + n: Integer; + r, c: Cardinal; +begin + ind := DupeString(' ', AIndent); + n := AChart.Series.Count; + if n > 0 then + begin + if AChart.IsScatterChart then + n := n * 2; + colCountStr := Format('table:number-columns-repeated="%d"', [n]); + end else + colCountStr := ''; + + AppendToStream(AStream, Format( + ind + '' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE + + ind + ' ' + LE, [ colCountStr ] + )); + + auxBook := TsWorkbook.Create; + try + auxSheet := auxBook.AddWorksheet('chart'); + PrepareChartTable(AChart, auxSheet); + + // Header rows (containing the series names) + AppendToStream(AStream, + ind + ' ' + LE + + ind + ' ' + LE ); + for c := 0 to auxSheet.GetLastColIndex do + WriteAuxCell(AIndent + 6, auxSheet.FindCell(0, c)); + AppendToStream(AStream, + ind + ' ' + LE + + ind + ' ' + LE + ); + + // Write data rows + AppendToStream(AStream, + ind + ' ' + LE + ); + for r := 1 to auxSheet.GetLastRowIndex do + begin + AppendToStream(AStream, + ind + ' ' + LE + ); + for c := 0 to auxSheet.GetlastColIndex do + WriteAuxCell(AIndent + 6, auxSheet.FindCell(r, c)); + AppendToStream(AStream, + ind + ' ' + LE + ); + end; + AppendToStream(AStream, + ind + ' ' + LE + + ind + '' + LE + ); + + //auxBook.WriteToFile('table.ods', true); + finally + auxBook.Free; + end; +end; + +{ Writes the chart's title (or subtitle, depending on the value of IsSubTitle) + to the xml stream (chart stream) and the corresponding style to the stylestream. } +procedure TsSpreadOpenDocChartWriter.WriteChartTitle(AChartStream, AStyleStream: TStream; + AChartIndent, AStyleIndent: Integer; AChart: TsChart; IsSubtitle: Boolean; + var AStyleID: Integer); +var + title: TsChartText; + captionKind: Integer; + elementName: String; + indent: String; +begin + if IsSubTitle then + begin + title := AChart.SubTitle; + elementName := 'subtitle'; + captionKind := 2; + end else + begin + title := AChart.Title; + elementName := 'title'; + captionKind := 1; + end; + + if (not title.Visible) or (title.Caption = '') then + exit; + + // Write title properties + indent := DupeString(' ', AChartIndent); + AppendToStream(AChartStream, Format( + indent + '' + LE + + indent + ' %s' + LE + + indent + '' + LE, + [ elementName, AStyleID, title.Caption, elementName ], FPointSeparatorSettings + )); + + // Write title style + AppendToStream(AStyleStream, + GetChartCaptionStyleAsXML(AChart, captionKind, AStyleIndent, AStyleID) + ); + + // Next style + inc(AStyleID); +end; + + +end. + diff --git a/components/fpspreadsheet/source/common/fpsreaderwriter.pas b/components/fpspreadsheet/source/common/fpsreaderwriter.pas index 493bcf042..e195d290e 100644 --- a/components/fpspreadsheet/source/common/fpsreaderwriter.pas +++ b/components/fpspreadsheet/source/common/fpsreaderwriter.pas @@ -197,6 +197,19 @@ type property NumFormatList: TStringList read FNumFormatList; end; + {@@ Helper class for the spreadsheet writer to keep processing of charts + out of the main writer unit. } + TsBasicSpreadChartWriter = class + protected + FWriter: TsBasicSpreadWriter; + public + constructor Create(AWriter: TsBasicSpreadWriter); virtual; + procedure CreateStreams; virtual; abstract; + procedure DestroyStreams; virtual; abstract; + procedure ResetStreams; virtual; abstract; + procedure WriteCharts; virtual; abstract; + property Writer: TsBasicSpreadWriter read FWriter; + end; type TsSpreadFileAccess = (faRead, faWrite); @@ -912,6 +925,21 @@ begin raise Exception.Create(rsUnsupportedWriteFormat); end; + +{------------------------------------------------------------------------------} +{ TsBasicSpreadChartWriter } +{------------------------------------------------------------------------------} + +{@@ Constructor of the ChartWriter. It gets the main writer as parameter. } +constructor TsBasicSpreadChartWriter.Create(AWriter: TsBasicSpreadWriter); +begin + FWriter := AWriter; +end; + + +{------------------------------------------------------------------------------} +{ TsSpreadFormatRegistry } +{------------------------------------------------------------------------------} type TsSpreadFormatData = class private diff --git a/components/fpspreadsheet/source/common/fpsxmlcommon.pas b/components/fpspreadsheet/source/common/fpsxmlcommon.pas index 40adb6cd7..e461c65fb 100644 --- a/components/fpspreadsheet/source/common/fpsxmlcommon.pas +++ b/components/fpspreadsheet/source/common/fpsxmlcommon.pas @@ -18,6 +18,9 @@ uses {$ENDIF} fpstypes, fpsreaderwriter; +const + XML_HEADER = ''; + type TsSpreadXMLReader = class(TsCustomSpreadReader) protected