{ Axises for TAChart series. ***************************************************************************** * * * See the file COPYING.modifiedLGPL.txt, included in this distribution, * * for details about the copyright. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * * * ***************************************************************************** Authors: Alexander Klenin } unit TAChartAxis; {$H+} interface uses Classes, Graphics, SysUtils, Types, TAChartUtils, TACustomSource, TADrawUtils, TAStyles, TATransformations, TATypes; const DEF_TICK_LENGTH = 4; DEF_TITLE_DISTANCE = 4; type TChartAxisBrush = class(TBrush) published property Style default bsClear; end; TChartAxisFramePen = class(TChartPen) published property Style default psClear; end; {$IFNDEF fpdoc} // Workaround for issue #18549. TCustomChartAxisTitle = specialize TGenericChartMarks; {$ENDIF} { TChartAxisTitle } TChartAxisTitle = class(TCustomChartAxisTitle) private FCaption: String; function GetFont: TFont; procedure SetCaption(AValue: String); procedure SetFont(AValue: TFont); public constructor Create(AOwner: TCustomChart); public procedure Assign(Source: TPersistent); override; published property Caption: String read FCaption write SetCaption; property Distance default DEF_TITLE_DISTANCE; // Use LabelFont instead. property Font: TFont read GetFont write SetFont stored false; deprecated; property Frame; property LabelBrush; property Visible default false; end; ICoordTransformer = interface ['{6EDA0F9F-ED59-4CA6-BA68-E247EB88AE3D}'] function XGraphToImage(AX: Double): Integer; function YGraphToImage(AY: Double): Integer; end; TChartAxisAlignment = (calLeft, calTop, calRight, calBottom); TChartAxisMargins = array [TChartAxisAlignment] of Integer; TChartAxisMarkToTextEvent = procedure (var AText: String; AMark: Double) of object; TChartAxisPen = class(TChartPen) published property Style default psDot; end; {$IFNDEF fpdoc} // Workaround for issue #18549. TCustomChartAxisMarks = specialize TGenericChartMarks; {$ENDIF} { TChartAxisMarks } TChartAxisMarks = class(TCustomChartAxisMarks) private FAtDataOnly: Boolean; FDefaultSource: TCustomChartSource; FListener: TListener; FSource: TCustomChartSource; FStripes: TChartStyles; function IsFormatStored: Boolean; procedure SetAtDataOnly(AValue: Boolean); procedure SetSource(AValue: TCustomChartSource); procedure SetStripes(AValue: TChartStyles); public constructor Create(AOwner: TCustomChart); destructor Destroy; override; function SourceDef: TCustomChartSource; published property AtDataOnly: Boolean read FAtDataOnly write SetAtDataOnly default false; property Distance default 1; property Format stored IsFormatStored; property Frame; property LabelBrush; property OverlapPolicy; property Source: TCustomChartSource read FSource write SetSource; property Stripes: TChartStyles read FStripes write SetStripes; property Style default smsValue; end; TChartAxisGroup = record FCount: Integer; FSize: Integer; FTitleSize: Integer; end; { TChartAxis } TChartAxis = class(TCollectionItem) private FListener: TListener; FMarkTexts: TStringDynArray; FMarkValues: TDoubleDynArray; procedure GetMarkValues(AMin, AMax: Double); procedure VisitSource(ASource: TCustomChartSource; var AData); private FAxisRect: TRect; FGroupIndex: Integer; FTitleRect: TRect; private FAlignment: TChartAxisAlignment; FGrid: TChartAxisPen; FGroup: Integer; FInverted: Boolean; FMarks: TChartAxisMarks; FOnMarkToText: TChartAxisMarkToTextEvent; FTickColor: TColor; FTickLength: Integer; FTitle: TChartAxisTitle; FTransformations: TChartAxisTransformations; FVisible: Boolean; FZPosition: TChartDistance; function GetTransform: TChartAxisTransformations; procedure SetAlignment(AValue: TChartAxisAlignment); procedure SetGrid(AValue: TChartAxisPen); procedure SetGroup(AValue: Integer); procedure SetInverted(AValue: Boolean); procedure SetMarks(const AValue: TChartAxisMarks); procedure SetOnMarkToText(const AValue: TChartAxisMarkToTextEvent); procedure SetTickColor(AValue: TColor); procedure SetTickLength(AValue: Integer); procedure SetTitle(AValue: TChartAxisTitle); procedure SetTransformations(AValue: TChartAxisTransformations); procedure SetVisible(const AValue: Boolean); procedure SetZPosition(const AValue: TChartDistance); procedure StyleChanged(ASender: TObject); function TryApplyStripes( ADrawer: IChartDrawer; var AIndex: Cardinal): Boolean; protected function GetDisplayName: string; override; public constructor Create(ACollection: TCollection); override; destructor Destroy; override; public procedure Assign(Source: TPersistent); override; procedure Draw( ADrawer: IChartDrawer; const AClipRect: TRect; const ATransf: ICoordTransformer; const AZOffset: TPoint); procedure DrawTitle( ADrawer: IChartDrawer; const ACenter, AZOffset: TPoint; ASize: Integer); function IsVertical: Boolean; inline; procedure Measure( ADrawer: IChartDrawer; const AExtent: TDoubleRect; AFirstPass: Boolean; var AMeasureData: TChartAxisGroup); published property Alignment: TChartAxisAlignment read FAlignment write SetAlignment default calLeft; property Grid: TChartAxisPen read FGrid write SetGrid; property Group: Integer read FGroup write SetGroup default 0; // Inverts the axis scale from increasing to decreasing. property Inverted: boolean read FInverted write SetInverted default false; property Marks: TChartAxisMarks read FMarks write SetMarks; property TickColor: TColor read FTickColor write SetTickColor default clBlack; property TickLength: Integer read FTickLength write SetTickLength default DEF_TICK_LENGTH; property Title: TChartAxisTitle read FTitle write SetTitle; property Transformations: TChartAxisTransformations read FTransformations write SetTransformations; property Visible: Boolean read FVisible write SetVisible default true; property ZPosition: TChartDistance read FZPosition write SetZPosition default 0; published property OnMarkToText: TChartAxisMarkToTextEvent read FOnMarkToText write SetOnMarkToText; end; TChartOnSourceVisitor = procedure (ASource: TCustomChartSource; var AData) of object; TChartOnVisitSources = procedure ( AVisitor: TChartOnSourceVisitor; AAxis: TChartAxis; var AData) of object; { TChartAxisList } TChartAxisList = class(TCollection) private FChart: TCustomChart; FOnVisitSources: TChartOnVisitSources; function GetAxes(AIndex: Integer): TChartAxis; private FCenterPoint: TPoint; FGroupOrder: TFPList; FGroups: array of TChartAxisGroup; FZOrder: TFPList; procedure InitAndSort(AList: TFPList; ACompare: TListSortCompare); protected function GetOwner: TPersistent; override; procedure Update(AItem: TCollectionItem); override; public constructor Create(AOwner: TCustomChart); destructor Destroy; override; public function Add: TChartAxis; inline; procedure Draw( ADrawer: IChartDrawer; const AClipRect: TRect; const ATransf: ICoordTransformer; ACurrentZ, AMaxZ: Integer; var AIndex: Integer); function GetAxis(AIndex: Integer): TChartAxis; procedure Measure( ADrawer: IChartDrawer; const AExtent: TDoubleRect; AFirstPass: Boolean; var AMargins: TChartAxisMargins); procedure Prepare(ARect: TRect); procedure PrepareGroups; procedure SetAxis(AIndex: Integer; AValue: TChartAxis); property Axes[AIndex: Integer]: TChartAxis read GetAxes; default; property BottomAxis: TChartAxis index 1 read GetAxis write SetAxis; property LeftAxis: TChartAxis index 2 read GetAxis write SetAxis; property OnVisitSources: TChartOnVisitSources read FOnVisitSources write FOnVisitSources; end; TAxisConvFunc = function (AX: Integer): Double of object; { TAxisCoeffHelper } TAxisCoeffHelper = object FAxis: TChartAxis; FImageLo, FImageHi, FMarginLo, FMarginHi: Integer; FLo, FHi: Integer; FMin, FMax: PDouble; function CalcOffset(AScale: Double): Double; function CalcScale(ASign: Integer): Double; constructor Init( AAxis: TChartAxis; AImageLo, AImageHi, AMarginLo, AMarginHi: Integer; AMin, AMax: PDouble); procedure UpdateMinMax(AConv: TAxisConvFunc); end; procedure SideByAlignment( var ARect: TRect; AAlignment: TChartAxisAlignment; ADelta: Integer); function TransformByAxis( AAxisList: TChartAxisList; AIndex: Integer): TChartAxisTransformations; implementation uses LResources, Math, PropEdits, TAGeometry, TASources; type TAxisDataExtent = record FMin, FMax: Double; end; var VIdentityTransform: TChartAxisTransformations; function AxisGroupCompare(Item1, Item2: Pointer): Integer; begin Result := TChartAxis(Item1).Group - TChartAxis(Item2).Group; end; function AxisZCompare(Item1, Item2: Pointer): Integer; begin Result := TChartAxis(Item1).ZPosition - TChartAxis(Item2).ZPosition; end; procedure SideByAlignment( var ARect: TRect; AAlignment: TChartAxisAlignment; ADelta: Integer); var a: TChartAxisMargins absolute ARect; begin if AAlignment in [calLeft, calTop] then ADelta := -ADelta; a[AAlignment] += ADelta; end; function TransformByAxis( AAxisList: TChartAxisList; AIndex: Integer): TChartAxisTransformations; begin Result := nil; if InRange(AIndex, 0, AAxisList.Count - 1) then Result := AAxisList[AIndex].Transformations; if Result = nil then Result := VIdentityTransform; end; { TChartAxisTitle } procedure TChartAxisTitle.Assign(Source: TPersistent); begin if Source is TChartAxisTitle then with TChartAxisTitle(Source) do begin Self.FLabelBrush.Assign(FLabelBrush); Self.FLabelFont.Assign(FLabelFont); Self.FLinkPen.Assign(FLinkPen); Self.FCaption := FCaption; end; inherited Assign(Source); end; constructor TChartAxisTitle.Create(AOwner: TCustomChart); begin inherited Create(AOwner); FDistance := DEF_TITLE_DISTANCE; FFrame.Style := psClear; FLabelBrush.Style := bsClear; FVisible := false; end; function TChartAxisTitle.GetFont: TFont; begin Result := LabelFont; end; procedure TChartAxisTitle.SetCaption(AValue: String); begin if FCaption = AValue then exit; FCaption := AValue; StyleChanged(Self); end; procedure TChartAxisTitle.SetFont(AValue: TFont); begin LabelFont := AValue; end; { TChartAxisMarks } constructor TChartAxisMarks.Create(AOwner: TCustomChart); begin inherited Create(AOwner); FDefaultSource := TIntervalChartSource.Create(AOwner); FDistance := 1; FFrame.Style := psClear; FLabelBrush.Style := bsClear; FListener := TListener.Create(@FSource, @StyleChanged); FStyle := smsValue; FFormat := SERIES_MARK_FORMATS[FStyle]; end; destructor TChartAxisMarks.Destroy; begin FreeAndNil(FListener); FreeAndNil(FDefaultSource); inherited; end; function TChartAxisMarks.IsFormatStored: Boolean; begin Result := FStyle <> smsValue; end; procedure TChartAxisMarks.SetAtDataOnly(AValue: Boolean); begin if FAtDataOnly = AValue then exit; FAtDataOnly := AValue; StyleChanged(Self); end; procedure TChartAxisMarks.SetSource(AValue: TCustomChartSource); begin if FSource = AValue then exit; if FListener.IsListening then FSource.Broadcaster.Unsubscribe(FListener); FSource := AValue; if FSource <> nil then FSource.Broadcaster.Subscribe(FListener); StyleChanged(Self); end; procedure TChartAxisMarks.SetStripes(AValue: TChartStyles); begin if FStripes = AValue then exit; FStripes := AValue; StyleChanged(Self); end; function TChartAxisMarks.SourceDef: TCustomChartSource; begin Result := FSource; if Result = nil then Result := FDefaultSource; end; { TChartAxis } procedure TChartAxis.Assign(Source: TPersistent); begin if Source is TChartAxis then with TChartAxis(Source) do begin FGrid.Assign(Grid); FInverted := Inverted; FTitle.Assign(Title); end else inherited Assign(Source); end; constructor TChartAxis.Create(ACollection: TCollection); begin inherited Create(ACollection); FListener := TListener.Create(@FTransformations, @StyleChanged); FGrid := TChartAxisPen.Create; FGrid.OnChange := @StyleChanged; FGrid.Style := psDot; FMarks := TChartAxisMarks.Create(ACollection.Owner as TCustomChart); FTickColor := clBlack; FTickLength := DEF_TICK_LENGTH; FTitle := TChartAxisTitle.Create(ACollection.Owner as TCustomChart); FVisible := true; end; destructor TChartAxis.Destroy; begin FreeAndNil(FTitle); FreeAndNil(FMarks); FreeAndNil(FListener); FreeAndNil(FGrid); inherited; end; procedure TChartAxis.Draw( ADrawer: IChartDrawer; const AClipRect: TRect; const ATransf: ICoordTransformer; const AZOffset: TPoint); var prevLabelPoly: TPointArray = nil; stripeIndex: Cardinal = 0; prevCoord, scaledTickLength: Integer; procedure BarZ(AX1, AY1, AX2, AY2: Integer); begin with AZOffset do ADrawer.FillRect(AX1 + X, AY1 + Y, AX2 + X, AY2 + Y); end; procedure LineZ(AP1, AP2: TPoint); begin ADrawer.Line(AP1 + AZOffset, AP2 + AZOffset); end; procedure DrawLabelAndTick( ALabelCenter: TPoint; const ATickRect: TRect; const AText: String); begin ADrawer.PrepareSimplePen(TickColor); LineZ(ATickRect.TopLeft, ATickRect.BottomRight); ALabelCenter += AZOffset; Marks.DrawLabel(ADrawer, ALabelCenter, ALabelCenter, AText, prevLabelPoly); end; procedure DrawXMark(AY: Integer; AMark: Double; const AText: String); var x, d: Integer; begin x := ATransf.XGraphToImage(AMark); if Grid.Visible then begin ADrawer.Pen := Grid; ADrawer.SetBrushParams(bsClear, clTAColor); if TryApplyStripes(ADrawer, stripeIndex) then BarZ(prevCoord + 1, AClipRect.Top + 1, x, AClipRect.Bottom); LineZ(Point(x, AClipRect.Top), Point(x, AClipRect.Bottom)); prevCoord := x; end; if Marks.Visible then begin d := scaledTickLength + Marks.CenterOffset(ADrawer, AText).cy; if Alignment = calTop then d := -d; DrawLabelAndTick( Point(x, AY + d), Rect(x, AY - scaledTickLength, x, AY + scaledTickLength), AText); end; end; procedure DrawYMark(AX: Integer; AMark: Double; const AText: String); var y, d: Integer; begin y := ATransf.YGraphToImage(AMark); if Grid.Visible then begin ADrawer.Pen := Grid; ADrawer.SetBrushParams(bsClear, clTAColor); if TryApplyStripes(ADrawer, stripeIndex) then BarZ(AClipRect.Left + 1, prevCoord, AClipRect.Right, y); LineZ(Point(AClipRect.Left, y), Point(AClipRect.Right, y)); prevCoord := y; end; if Marks.Visible then begin d := scaledTickLength + Marks.CenterOffset(ADrawer, AText).cx; if Alignment = calLeft then d := -d; DrawLabelAndTick( Point(AX + d, y), Rect(AX - scaledTickLength, y, AX + scaledTickLength, y), AText); end; end; var i, coord: Integer; v: Double; begin if not Visible then exit; scaledTickLength := ADrawer.Scale(TickLength); if Marks.Visible then ADrawer.Font := Marks.LabelFont; coord := TChartAxisMargins(FAxisRect)[Alignment]; prevCoord := IfThen(IsVertical, AClipRect.Bottom, AClipRect.Left); for i := 0 to High(FMarkValues) do begin v := GetTransform.AxisToGraph(FMarkValues[i]); if IsVertical then DrawYMark(coord, v, FMarkTexts[i]) else DrawXMark(coord, v, FMarkTexts[i]); end; if Grid.Visible and TryApplyStripes(ADrawer, stripeIndex) then if IsVertical then BarZ(AClipRect.Left + 1, AClipRect.Top + 1, AClipRect.Right, prevCoord) else BarZ(prevCoord + 1, AClipRect.Top + 1, AClipRect.Right, AClipRect.Bottom); end; procedure TChartAxis.DrawTitle( ADrawer: IChartDrawer; const ACenter, AZOffset: TPoint; ASize: Integer); var p: TPoint; dummy: TPointArray = nil; d: Integer; begin if not Visible or (ASize = 0) then exit; p := ACenter; d := (ASize + Title.Distance) div 2; case Alignment of calLeft: p.X := FTitleRect.Left - d; calTop: p.Y := FTitleRect.Top - d; calRight: p.X := FTitleRect.Right + d; calBottom: p.Y := FTitleRect.Bottom + d; end; p += AZOffset; Title.DrawLabel(ADrawer, p, p, Title.Caption, dummy); end; function TChartAxis.GetDisplayName: string; const SIDE_NAME: array [TChartAxisAlignment] of String = ('Left', 'Top', 'Right', 'Bottom'); VISIBLE_NAME: array [Boolean] of String = (' Hidden', ''); INVERTED_NAME: array [Boolean] of String = ('', ' Inverted'); CAPTION_FMT = ' (%s)'; begin Result := SIDE_NAME[Alignment] + VISIBLE_NAME[Visible] + INVERTED_NAME[Inverted]; if Title.Caption <> '' then Result += Format(CAPTION_FMT, [Title.Caption]); end; procedure TChartAxis.GetMarkValues(AMin, AMax: Double); var i: Integer; d: TAxisDataExtent; vis: TChartOnVisitSources; begin AMin := GetTransform.GraphToAxis(AMin); AMax := GetTransform.GraphToAxis(AMax); EnsureOrder(AMin, AMax); SetLength(FMarkValues, 0); SetLength(FMarkTexts, 0); vis := TChartAxisList(Collection).OnVisitSources; if Marks.AtDataOnly and Assigned(vis) then begin d.FMin := AMin; d.FMax := AMax; vis(@VisitSource, Self, d); end else Marks.SourceDef.ValuesInRange( AMin, AMax, Marks.Format, IsVertical, FMarkValues, FMarkTexts); if Inverted then for i := 0 to High(FMarkValues) div 2 do begin Exchange(FMarkValues[i], FMarkValues[High(FMarkValues) - i]); Exchange(FMarkTexts[i], FMarkTexts[High(FMarkValues) - i]); end; if Assigned(FOnMarkToText) then for i := 0 to High(FMarkTexts) do FOnMarkToText(FMarkTexts[i], FMarkValues[i]); end; function TChartAxis.GetTransform: TChartAxisTransformations; begin Result := Transformations; if Result = nil then Result := VIdentityTransform; end; function TChartAxis.IsVertical: Boolean; inline; begin Result := Alignment in [calLeft, calRight]; end; procedure TChartAxis.Measure( ADrawer: IChartDrawer; const AExtent: TDoubleRect; AFirstPass: Boolean; var AMeasureData: TChartAxisGroup); function CalcMarksSize(AMin, AMax: Double): TSize; const SOME_DIGIT = '0'; var i, d: Integer; t: String; begin Result := Size(0, 0); if AMin = AMax then exit; GetMarkValues(AMin, AMax); if not Marks.Visible then exit; for i := 0 to High(FMarkTexts) do begin // CalculateTransformationCoeffs changes axis interval, so it is possibile // that a new mark longer then existing ones is introduced. // That will change marks width and reduce view area, // requiring another call to CalculateTransformationCoeffs... // So punt for now and just reserve space for extra digit unconditionally. t := FMarkTexts[i]; if AFirstPass then t += SOME_DIGIT; d := IfThen(Marks.DistanceToCenter, 2, 1); with Marks.MeasureLabel(ADrawer, t) do begin Result.cx := Max(cx div d, Result.cx); Result.cy := Max(cy div d, Result.cy); end; end; end; function CalcTitleSize: Integer; var sz: TSize; begin if not Title.Visible or (Title.Caption = '') then exit(0); sz := Title.MeasureLabel(ADrawer, Title.Caption); Result := IfThen(IsVertical, sz.cx, sz.cy) + Title.Distance; end; var sz: Integer; begin if not Visible then exit; if IsVertical then sz := CalcMarksSize(AExtent.a.Y, AExtent.b.Y).cx else sz := CalcMarksSize(AExtent.a.X, AExtent.b.X).cy; if sz > 0 then sz += ADrawer.Scale(TickLength) + ADrawer.Scale(Marks.Distance); with AMeasureData do begin FSize := Max(sz, FSize); FTitleSize := Max(CalcTitleSize, FTitleSize); end; end; procedure TChartAxis.SetAlignment(AValue: TChartAxisAlignment); begin if FAlignment = AValue then exit; FAlignment := AValue; StyleChanged(Self); end; procedure TChartAxis.SetGrid(AValue: TChartAxisPen); begin FGrid.Assign(AValue); StyleChanged(Self); end; procedure TChartAxis.SetGroup(AValue: Integer); begin if FGroup = AValue then exit; FGroup := AValue; StyleChanged(Self); end; procedure TChartAxis.SetInverted(AValue: Boolean); begin if FInverted = AValue then exit; FInverted := AValue; StyleChanged(Self); end; procedure TChartAxis.SetMarks(const AValue: TChartAxisMarks); begin if FMarks = AValue then exit; FMarks := AValue; StyleChanged(Self); end; procedure TChartAxis.SetOnMarkToText(const AValue: TChartAxisMarkToTextEvent); begin if TMethod(FOnMarkToText) = TMethod(AValue) then exit; FOnMarkToText := AValue; StyleChanged(Self); end; procedure TChartAxis.SetTickColor(AValue: TColor); begin if FTickColor = AValue then exit; FTickColor := AValue; StyleChanged(Self); end; procedure TChartAxis.SetTickLength(AValue: Integer); begin if FTickLength = AValue then exit; FTickLength := AValue; StyleChanged(Self); end; procedure TChartAxis.SetTitle(AValue: TChartAxisTitle); begin FTitle.Assign(AValue); StyleChanged(Self); end; procedure TChartAxis.SetTransformations(AValue: TChartAxisTransformations); begin if FTransformations = AValue then exit; if FListener.IsListening then Transformations.Broadcaster.Unsubscribe(FListener); FTransformations := AValue; if FTransformations <> nil then Transformations.Broadcaster.Subscribe(FListener); StyleChanged(Self); end; procedure TChartAxis.SetVisible(const AValue: Boolean); begin if FVisible = AValue then exit; FVisible := AValue; StyleChanged(Self); end; procedure TChartAxis.SetZPosition(const AValue: TChartDistance); begin if FZPosition = AValue then exit; FZPosition := AValue; StyleChanged(Self); end; procedure TChartAxis.StyleChanged(ASender: TObject); begin with Collection.Owner as TCustomChart do begin // Transformation change could have invalidated the current extent, // so revert to full extent for now. if ASender is TAxisTransform then ZoomFull; Invalidate; end; end; function TChartAxis.TryApplyStripes( ADrawer: IChartDrawer; var AIndex: Cardinal): Boolean; begin Result := Marks.Stripes <> nil; if not Result then exit; Marks.Stripes.Apply(ADrawer, AIndex); AIndex += 1; end; procedure TChartAxis.VisitSource(ASource: TCustomChartSource; var AData); var lmin, lmax: Double; ext: TDoubleRect; begin ext := ASource.Extent; with TAxisDataExtent(AData) do begin if IsVertical then begin lmin := Max(ext.a.Y, FMin); lmax := Min(ext.b.Y, FMax); end else begin lmin := Max(ext.a.X, FMin); lmax := Min(ext.b.X, FMax); end; Marks.SourceDef.ValuesInRange( lmin, lmax, Marks.Format, IsVertical, FMarkValues, FMarkTexts); end; end; const AXIS_INDEX: array [1..2] of TChartAxisAlignment = (calBottom, calLeft); { TChartAxisList } function TChartAxisList.Add: TChartAxis; inline; begin Result := TChartAxis(inherited Add); end; constructor TChartAxisList.Create(AOwner: TCustomChart); begin inherited Create(TChartAxis); FChart := AOwner; FGroupOrder := TFPList.Create; FZOrder := TFPList.Create; end; destructor TChartAxisList.Destroy; begin FreeAndNil(FGroupOrder); FreeAndNil(FZOrder); inherited Destroy; end; procedure TChartAxisList.Draw( ADrawer: IChartDrawer; const AClipRect: TRect; const ATransf: ICoordTransformer; ACurrentZ, AMaxZ: Integer; var AIndex: Integer); var zoffset: TPoint; begin while AIndex < FZOrder.Count do with TChartAxis(FZOrder[AIndex]) do begin if ACurrentZ < ZPosition then break; zoffset.Y := Min(ZPosition, AMaxZ); zoffset.X := - zoffset.Y; Draw(ADrawer, AClipRect, ATransf, zoffset); DrawTitle(ADrawer, FCenterPoint, zoffset, FGroups[FGroupIndex].FTitleSize); AIndex += 1; end; end; function TChartAxisList.GetAxes(AIndex: Integer): TChartAxis; begin Result := TChartAxis(Items[AIndex]); end; function TChartAxisList.GetAxis(AIndex: Integer): TChartAxis; var i: Integer; begin for i := 0 to Count - 1 do if Axes[i].Alignment = AXIS_INDEX[AIndex] then exit(Axes[i]); Result := nil; end; function TChartAxisList.GetOwner: TPersistent; begin Result := FChart; end; procedure TChartAxisList.InitAndSort( AList: TFPList; ACompare: TListSortCompare); var i: Integer; begin AList.Clear; for i := 0 to Count - 1 do AList.Add(Pointer(Axes[i])); AList.Sort(ACompare); end; procedure TChartAxisList.Measure( ADrawer: IChartDrawer; const AExtent: TDoubleRect; AFirstPass: Boolean; var AMargins: TChartAxisMargins); var i, j, ai: Integer; axis: TChartAxis; g: ^TChartAxisGroup; begin ai := 0; for i := 0 to High(FGroups) do begin g := @FGroups[i]; g^.FSize := 0; g^.FTitleSize := 0; for j := 0 to g^.FCount - 1 do begin axis := TChartAxis(FGroupOrder[ai]); axis.Measure(ADrawer, AExtent, AFirstPass, g^); ai += 1; end; if AFirstPass then AMargins[axis.Alignment] += g^.FSize + g^.FTitleSize; end; end; procedure TChartAxisList.Prepare(ARect: TRect); var i, j, ai: Integer; axis: TChartAxis; g: ^TChartAxisGroup; begin FCenterPoint := CenterPoint(ARect); ai := 0; for i := 0 to High(FGroups) do begin g := @FGroups[i]; for j := 0 to g^.FCount - 1 do begin axis := TChartAxis(FGroupOrder[ai + j]); axis.FAxisRect := ARect; end; SideByAlignment(ARect, axis.Alignment, g^.FSize); for j := 0 to g^.FCount - 1 do begin axis := TChartAxis(FGroupOrder[ai]); axis.FTitleRect := ARect; ai += 1; end; SideByAlignment(ARect, axis.Alignment, g^.FTitleSize); end; InitAndSort(FZOrder, @AxisZCompare); end; procedure TChartAxisList.PrepareGroups; var i, prevGroup, groupCount: Integer; begin InitAndSort(FGroupOrder, @AxisGroupCompare); SetLength(FGroups, Count); groupCount := 0; prevGroup := 0; for i := 0 to FGroupOrder.Count - 1 do with TChartAxis(FGroupOrder[i]) do begin if (Group = 0) or (Group <> prevGroup) then begin FGroups[groupCount].FCount := 1; groupCount += 1; prevGroup := Group; end else FGroups[groupCount - 1].FCount += 1; FGroupIndex := groupCount - 1; end; SetLength(FGroups, groupCount); end; procedure TChartAxisList.SetAxis(AIndex: Integer; AValue: TChartAxis); var a: TChartAxis; begin a := GetAxis(AIndex); if a = nil then a := Add; a.Assign(AValue); a.FAlignment := AXIS_INDEX[AIndex]; end; procedure TChartAxisList.Update(AItem: TCollectionItem); begin Unused(AItem); FChart.Invalidate; end; { TAxisCoeffHelper } constructor TAxisCoeffHelper.Init( AAxis: TChartAxis; AImageLo, AImageHi, AMarginLo, AMarginHi: Integer; AMin, AMax: PDouble); begin FAxis := AAxis; FImageLo := AImageLo; FImageHi := AImageHi; FMarginLo := AMarginLo; FMarginHi := AMarginHi; FMin := AMin; FMax := AMax; FLo := FImageLo + FMarginLo; FHi := FImageHi + FMarginHi; end; function TAxisCoeffHelper.CalcScale(ASign: Integer): Double; begin if (FMax^ = FMin^) or (Sign(FHi - FLo) <> ASign) then exit(1.0); if (FAxis <> nil) and FAxis.Inverted then Exchange(FLo, FHi); Result := (FHi - FLo) / (FMax^ - FMin^); end; function TAxisCoeffHelper.CalcOffset(AScale: Double): Double; begin Result := (FLo + FHi) / 2 - AScale * (FMin^ + FMax^) / 2; end; procedure TAxisCoeffHelper.UpdateMinMax(AConv: TAxisConvFunc); begin FMin^ := AConv(FImageLo); FMax^ := AConv(FImageHi); if (FAxis <> nil) and FAxis.Inverted then Exchange(FMin^, FMax^); end; procedure SkipObsoleteAxisProperties; const TRANSFORM_NOTE = 'Obsolete, use Transformations instead'; begin RegisterPropertyToSkip(TChartAxis, 'Offset', TRANSFORM_NOTE, ''); RegisterPropertyToSkip(TChartAxis, 'Scale', TRANSFORM_NOTE, ''); RegisterPropertyToSkip(TChartAxis, 'Transformation', TRANSFORM_NOTE, ''); RegisterPropertyEditor( TypeInfo(TFont), TChartAxisTitle, 'Font', THiddenPropertyEditor); end; initialization VIdentityTransform := TChartAxisTransformations.Create(nil); SkipObsoleteAxisProperties; finalization FreeAndNil(VIdentityTransform); end.