From 91474f2d4967967cca33acee11eee35529bb10b3 Mon Sep 17 00:00:00 2001 From: wp_xxyyzz Date: Mon, 4 Dec 2023 00:29:43 +0000 Subject: [PATCH] fpspreadsheet: Initial commit to support secondary y axis in ods reader/writer and chart link. git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@9065 8e941d3f-bd1b-0410-a28a-d453659cc2b4 --- .../other/chart/barchart_2axes_write_demo.lpi | 73 ++++++++++++++++ .../other/chart/barchart_2axes_write_demo.lpr | 84 +++++++++++++++++++ .../visual/fpschart/fpschartlink/main.lfm | 1 + .../source/common/fpsopendocumentchart.pas | 38 +++++++++ .../source/visual/fpspreadsheetchart.pas | 36 +++++++- 5 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 components/fpspreadsheet/examples/other/chart/barchart_2axes_write_demo.lpi create mode 100644 components/fpspreadsheet/examples/other/chart/barchart_2axes_write_demo.lpr diff --git a/components/fpspreadsheet/examples/other/chart/barchart_2axes_write_demo.lpi b/components/fpspreadsheet/examples/other/chart/barchart_2axes_write_demo.lpi new file mode 100644 index 000000000..cfcd8a267 --- /dev/null +++ b/components/fpspreadsheet/examples/other/chart/barchart_2axes_write_demo.lpi @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + <UseAppBundle Value="False"/> + <ResourceType Value="res"/> + </General> + <BuildModes> + <Item Name="Default" Default="True"/> + </BuildModes> + <PublishOptions> + <Version Value="2"/> + <UseFileFilters Value="True"/> + </PublishOptions> + <RunParams> + <FormatVersion Value="2"/> + </RunParams> + <RequiredPackages> + <Item> + <PackageName Value="laz_fpspreadsheet"/> + </Item> + </RequiredPackages> + <Units> + <Unit> + <Filename Value="barchart_2axes_write_demo.lpr"/> + <IsPartOfProject Value="True"/> + <UnitName Value="barchart_write_demo"/> + </Unit> + </Units> + </ProjectOptions> + <CompilerOptions> + <Version Value="11"/> + <PathDelim Value="\"/> + <Target> + <Filename Value="barchart_2axes_write_demo"/> + </Target> + <SearchPaths> + <UnitOutputDirectory Value="lib\$(TargetCPU)-$(TargetOS)"/> + </SearchPaths> + <Linking> + <Debugging> + <DebugInfoType Value="dsDwarf3"/> + </Debugging> + </Linking> + <Other> + <ConfigFile> + <WriteConfigFilePath Value=""/> + </ConfigFile> + </Other> + </CompilerOptions> + <Debugging> + <Exceptions> + <Item> + <Name Value="EAbort"/> + </Item> + <Item> + <Name Value="ECodetoolError"/> + </Item> + <Item> + <Name Value="EFOpenError"/> + </Item> + </Exceptions> + </Debugging> +</CONFIG> diff --git a/components/fpspreadsheet/examples/other/chart/barchart_2axes_write_demo.lpr b/components/fpspreadsheet/examples/other/chart/barchart_2axes_write_demo.lpr new file mode 100644 index 000000000..5ed380f4b --- /dev/null +++ b/components/fpspreadsheet/examples/other/chart/barchart_2axes_write_demo.lpr @@ -0,0 +1,84 @@ +program barchart_write_demo; + +{.$DEFINE DARK_MODE} + +uses + SysUtils, + fpspreadsheet, fpstypes, fpsUtils, fpschart, xlsxooxml, fpsopendocument; + +const + FILE_NAME = 'bars_2axes'; +var + book: TsWorkbook; + sheet: TsWorksheet; + ch: TsChart; + ser: TsChartSeries; +begin + book := TsWorkbook.Create; + try + // worksheet + sheet := book.AddWorksheet('bar_series'); + + // Enter data + sheet.WriteText( 0, 0, 'Test Results'); + sheet.WriteFont( 0, 0, '', 12, [fssBold], scBlack); + sheet.WriteText( 2, 0, ''); sheet.WriteText ( 2, 1, 'Count'); sheet.WriteText ( 2, 2, 'Volume'); + sheet.WriteText( 3, 0, 'Case 1'); sheet.WriteNumber( 3, 1, 12); sheet.WriteNumber( 3, 2, 501); + sheet.WriteText( 4, 0, 'Case 2'); sheet.WriteNumber( 4, 1, 24); sheet.WriteNumber( 4, 2, 1054); + sheet.WriteText( 5, 0, 'Case 3'); sheet.WriteNumber( 5, 1, 21); sheet.WriteNumber( 5, 2, 4432); + sheet.WriteText( 6, 0, 'Case 4'); sheet.WriteNumber( 6, 1, 19); sheet.WriteNumber( 6, 2, 6982); + sheet.WriteText( 7, 0, 'Case 5'); sheet.WriteNumber( 7, 1, 9); sheet.WriteNumber( 7, 2, 304); + sheet.WriteText( 8, 0, 'Case 6'); sheet.WriteNumber( 8, 1, 5); sheet.WriteNumber( 8, 2, 1285); + + // Create chart: left/top in cell D4, 160 mm x 100 mm + ch := book.AddChart(sheet, 2, 3, 120, 100); + + // Chart properties + ch.Border.Style := clsNoLine; + ch.Title.Caption := 'Test Results'; + ch.Title.Font.Style := [fssBold]; + ch.Legend.Border.Style := clsNoLine; + ch.XAxis.Title.Caption := ''; + ch.YAxis.Title.Caption := 'Count'; + ch.YAxis.Title.Font.Color := $0075ea; //597bff; + ch.YAxis.AxisLine.Color := $0075ea; //597bff; + ch.YAxis.LabelFont.Color := $0075ea; //597bff; + ch.YAxis.MajorTicks := []; + ch.Y2Axis.Title.Caption := 'Volume'; + ch.Y2Axis.Title.Font.Color := $b08359; + ch.Y2Axis.AxisLine.Color := $b08359; + ch.Y2Axis.LabelFont.Color := $b08359; + + // Add 1st bar series ("Count") + ser := TsBarSeries.Create(ch); + ser.YAxis := alPrimary; + ser.SetTitleAddr(2, 1); + ser.SetLabelRange(3, 0, 8, 0); + ser.SetYRange(3, 1, 8, 1); + ser.Fill.Style := cfsSolid; + ser.Fill.Color := $0075ea; //597bff; + ser.Fill.Color := scRed; + ser.Line.Style := clsNoLine; + + // Add 2nd bar series ("Volume") + ser := TsBarSeries.Create(ch); + ser.YAxis := alSecondary; + ser.SetTitleAddr(2, 2); + ser.SetLabelRange(3, 0, 8, 0); + ser.SetYRange(3, 2, 8, 2); + ser.Fill.Style := cfsSolid; + ser.Fill.Color := $b08359; + ser.Line.Style := clsNoLine; + + { + book.WriteToFile(FILE_NAME + '.xlsx', true); // Excel fails to open the file + WriteLn('Data saved with chart in ', FILENAME, '.xlsx'); + } + + book.WriteToFile(FILE_NAME + '.ods', true); + WriteLn('Data saved with chart in ', FILE_NAME, '.ods'); + finally + book.Free; + end; +end. + diff --git a/components/fpspreadsheet/examples/visual/fpschart/fpschartlink/main.lfm b/components/fpspreadsheet/examples/visual/fpschart/fpschartlink/main.lfm index 2d89cbc61..2e34f0f37 100644 --- a/components/fpspreadsheet/examples/visual/fpschart/fpschartlink/main.lfm +++ b/components/fpspreadsheet/examples/visual/fpschart/fpschartlink/main.lfm @@ -94,6 +94,7 @@ object Form1: TForm1 Items.Strings = ( '../../../other/chart/area.ods' '../../../other/chart/bars.ods' + '../../../other/chart/bars_2axes.ods' '../../../other/chart/bubble.ods' '../../../other/chart/pie.ods' '../../../other/chart/radar.ods' diff --git a/components/fpspreadsheet/source/common/fpsopendocumentchart.pas b/components/fpspreadsheet/source/common/fpsopendocumentchart.pas index 955492b9f..4bc538134 100644 --- a/components/fpspreadsheet/source/common/fpsopendocumentchart.pas +++ b/components/fpspreadsheet/source/common/fpsopendocumentchart.pas @@ -94,6 +94,7 @@ type function GetChartSeriesDataPointStyleAsXML(AChart: TsChart; ASeriesIndex, APointIndex, AIndent, AStyleID: Integer): String; function GetChartSeriesStyleAsXML(AChart: TsChart; ASeriesIndex, AIndent, AStyleID: integer): String; + procedure CheckAxis(AChart: TsChart; Axis: TsChartAxis); function GetNumberFormatID(ANumFormat: String): String; procedure ListAllNumberFormats(AChart: TsChart); @@ -649,6 +650,7 @@ begin end; // Default values + axis.Visible := true; // The presence of this node makes the axis visible. axis.Title.Caption := ''; axis.MajorGridLines.Style := clsNoLine; axis.MinorGridLines.Style := clsNoLine; @@ -957,6 +959,10 @@ begin ReadChartPlotAreaStyle(styleNode, AChart); // Defaults + AChart.XAxis.Visible := false; + AChart.YAxis.Visible := false; + AChart.X2Axis.Visible := false; + AChart.Y2Axis.Visible := false; AChart.PlotArea.Border.Style := clsNoLine; AChart.Floor.Border.Style := clsNoLine; @@ -1265,6 +1271,12 @@ begin else ReadChartCellRange(ANode, 'chart:values-cell-range-address', series.YRange); + s := GetAttrValue(ANode, 'chart:attached-axis'); + if s = 'primary-y' then + series.YAxis := alPrimary + else if s = 'secondary-y' then + series.YAxis := alSecondary; + xyCounter := 0; subnode := ANode.FirstChild; while subnode <> nil do @@ -2513,6 +2525,21 @@ begin end; end; +{ Switches secondary axes to visible when there are series needing them. } +procedure TsSpreadOpenDocChartWriter.CheckAxis(AChart: TsChart; Axis: TsChartAxis); +var + i: Integer; +begin + if Axis = AChart.Y2Axis then + for i := 0 to AChart.Series.Count - 1 do + if AChart.Series[i].YAxis = alSecondary then + begin + Axis.Visible := true; + break; + end; +end; + + (* DO NOT DELETE THIS! MAYBE NEEDED LATER... { Extracts the cells needed by the given chart from the chart's worksheet and @@ -3260,9 +3287,11 @@ begin WriteChartAxis(AChartStream, AStyleStream, AChartIndent+2, AStyleIndent, AChart.YAxis, AStyleID); // secondary x axis + CheckAxis(AChart, AChart.X2Axis); WriteChartAxis(AChartStream, AStyleStream, AChartIndent+2, AStyleIndent, AChart.X2Axis, AStyleID); // secondary y axis + CheckAxis(AChart, AChart.Y2Axis); WriteChartAxis(AChartStream, AStyleStream, AChartIndent+2, AStyleIndent, AChart.Y2Axis, AStyleID); // series @@ -3288,6 +3317,7 @@ var fillColorRange: String = ''; lineColorRange: String = ''; chartClass: String = ''; + seriesYAxis: String = ''; regressionEquation: String = ''; needRegressionStyle: Boolean = false; needRegressionEquationStyle: Boolean = false; @@ -3360,6 +3390,13 @@ begin rfAllRel, false ); + // Axis of the series + if AChart.Y2Axis.Visible then + case series.YAxis of + alPrimary : seriesYAxis := 'chart:attached-axis="primary-y" '; + alSecondary: seriesYAxis := 'chart:attached-axis="secondary-y" '; + end; + // And this is the title of the series for the legend titleAddr := GetSheetCellRangeString_ODS( series.TitleAddr.GetSheetName, series.TitleAddr.GetSheetName, @@ -3383,6 +3420,7 @@ begin AppendToStream(AChartStream, Format( indent + '<chart:series chart:style-name="ch%d" ' + 'chart:class="chart:%s" ' + // series type + seriesYAxis + // attached y axis 'chart:values-cell-range-address="%s" ' + // y values 'chart:label-cell-address="%s">' + LE, // series title [ AStyleID, chartClass, valuesRange, titleAddr, chartClass ] diff --git a/components/fpspreadsheet/source/visual/fpspreadsheetchart.pas b/components/fpspreadsheet/source/visual/fpspreadsheetchart.pas index 3904e3588..958358020 100644 --- a/components/fpspreadsheet/source/visual/fpspreadsheetchart.pas +++ b/components/fpspreadsheet/source/visual/fpspreadsheetchart.pas @@ -1097,6 +1097,14 @@ begin Result.Title := src.Title; end; + // Assign series to axis for primary and secondary y axes support + case ASeries.YAxis of + alPrimary: + Result.AxisIndexY := FChart.AxisList.GetAxisByAlign(calLeft).Index; + alSecondary: + Result.AxisIndexY := FChart.AxisList.GetAxisByAlign(calRight).Index; + end; + if stackable then begin style := TChartStyle(FChartStyles.Styles.Add); @@ -1199,14 +1207,16 @@ begin // Clear the axes for i := FChart.AxisList.Count-1 downto 0 do begin + if FChart.AxisList[i].Minors <> nil then + for j := FChart.AxisList[i].Minors.Count-1 downto 0 do + FChart.AxisList[i].Minors.Delete(j); + case FChart.AxisList[i].Alignment of calLeft, calBottom: FChart.AxisList[i].Title.Caption := ''; calTop, calRight: FChart.AxisList.Delete(i); end; - for j := FChart.AxisList[i].Minors.Count-1 downto 0 do - FChart.AxisList[i].Minors.Delete(j); end; // Clear the title @@ -1413,8 +1423,30 @@ begin end; function TsWorkbookChartLink.IsStackable(ASeries: TsChartSeries): Boolean; +var + nextSeries: TsChartSeries; + firstSeries: TsChartSeries; + i, numSeries: Integer; begin Result := (ASeries.ChartType in [ctBar, ctLine, ctArea]); + if Result then + begin + numSeries := ASeries.Chart.Series.Count; + firstSeries := ASeries.Chart.Series[0]; + nextSeries := nil; + for i := 0 to numSeries - 1 do + if (ASeries.Chart.Series[i] = ASeries) then + begin + if i < numSeries - 1 then + nextSeries := ASeries.Chart.Series[i+1]; + exit; + end; + Result := (firstSeries.YAxis = ASeries.YAxis) and + ( + ((nextSeries <> nil) and (nextSeries.YAxis = ASeries.YAxis)) or + ((nextSeries = nil) and (firstSeries = ASeries)) + ); + end; end; procedure TsWorkbookChartLink.ListenerNotification(AChangedItems: TsNotificationItems;