From d5a2df6249c63a206832d3df1f6b57e1e69f2ad6 Mon Sep 17 00:00:00 2001 From: ask Date: Thu, 22 Jul 2010 06:40:43 +0000 Subject: [PATCH] TAChart: Correctly support arbitrary label orientation. As a bonus: * Fix axis ticks disappearing when the corresponding label is hidden. * Support Marks.OverlapPolicy in pie series. git-svn-id: trunk@26771 - --- components/tachart/tachartaxis.pas | 83 ++++++++++++++-------------- components/tachart/taseries.pas | 48 ++++++----------- components/tachart/tatypes.pas | 87 ++++++++++++++++++++---------- 3 files changed, 113 insertions(+), 105 deletions(-) diff --git a/components/tachart/tachartaxis.pas b/components/tachart/tachartaxis.pas index ba29f1a19e..faf9c65680 100644 --- a/components/tachart/tachartaxis.pas +++ b/components/tachart/tachartaxis.pas @@ -361,20 +361,19 @@ procedure TChartAxis.Draw( const ATransf: ICoordTransformer; var ARect: TRect); var - prevLabelRect: TRect = (Left: 0; Top: 0; Right: 0; Bottom: 0); + prevLabelPoly: TPointArray = nil; - procedure DrawLabelAndTick(const ALabelRect, ATickRect: TRect; const AText: String); + procedure DrawLabelAndTick( + const ALabelCenter: TPoint; const ATickRect: TRect; const AText: String); begin - if Marks.IsLabelHiddenDueToOverlap(prevLabelRect, ALabelRect) then exit; PrepareSimplePen(ACanvas, TickColor); ACanvas.Line(ATickRect); - Marks.DrawLabel(ACanvas, ALabelRect, AText); + Marks.DrawLabel(ACanvas, ALabelCenter, ALabelCenter, AText, prevLabelPoly); end; procedure DrawXMark(AY: Integer; AMark: Double; const AText: String); var - x, t: Integer; - sz: TSize; + x, d: Integer; begin x := ATransf.XGraphToImage(AMark); @@ -386,18 +385,17 @@ var x, ATransf.YGraphToImage(AExtent.b.Y)); end; - sz := Marks.MeasureLabel(ACanvas, AText); - t := TickLength + Marks.Distance; - t := IfThen(Alignment = calTop, - t - sz.cy, t); + d := + TickLength + Marks.Distance + Marks.MeasureLabel(ACanvas, AText).cy div 2; + if Alignment = calTop then + d := -d; DrawLabelAndTick( - BoundsSize(x - sz.cx div 2, AY + t, sz), - Rect(x, AY - TickLength, x, AY + TickLength), AText); + Point(x, AY + d), Rect(x, AY - TickLength, x, AY + TickLength), AText); end; procedure DrawYMark(AX: Integer; AMark: Double; const AText: String); var - y, t: Integer; - sz: TSize; + y, d: Integer; begin y := ATransf.YGraphToImage(AMark); @@ -409,12 +407,12 @@ var ATransf.XGraphToImage(AExtent.b.X), y); end; - sz := Marks.MeasureLabel(ACanvas, AText); - t := TickLength + Marks.Distance; - t := IfThen(Alignment = calLeft, - t - sz.cx, t); + d := + TickLength + Marks.Distance + Marks.MeasureLabel(ACanvas, AText).cx div 2; + if Alignment = calLeft then + d := -d; DrawLabelAndTick( - BoundsSize(AX + t, y - sz.cy div 2, sz), - Rect(AX - TickLength, y, AX + TickLength, y), AText); + Point(AX + d, y), Rect(AX - TickLength, y, AX + TickLength, y), AText); end; var @@ -515,33 +513,30 @@ procedure TChartAxis.Measure( ACanvas: TCanvas; const AExtent: TDoubleRect; AFirstPass: Boolean; var AMargins: TChartAxisMargins); -const - SOME_DIGIT = '0'; - - procedure CalcVertSize; + function CalcMarksSize(AMin, AMax: Double): TSize; + const + SOME_DIGIT = '0'; var i: Integer; + t: String; + sz: TSize; begin - if AExtent.a.Y = AExtent.b.Y then exit; - GetMarkValues(AExtent.a.Y, AExtent.b.Y); - FSize := 0; - for i := 0 to High(FMarkTexts) do - with Marks.MeasureLabel(ACanvas, FMarkTexts[i]) do - FSize := Max(cx, FSize); - // 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. - if AFirstPass then - FSize += ACanvas.TextWidth(SOME_DIGIT); - end; - - procedure CalcHorSize; - begin - if AExtent.a.X = AExtent.b.X then exit; - GetMarkValues(AExtent.a.X, AExtent.b.X); - FSize := Marks.MeasureLabel(ACanvas, SOME_DIGIT).cy; + Result := Size(0, 0); + if AMin = AMax then exit; + GetMarkValues(AMin, AMax); + 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; + sz := Marks.MeasureLabel(ACanvas, t); + Result.cx := Max(sz.cx, Result.cx); + Result.cy := Max(sz.cy, Result.cy); + end; end; procedure CalcTitleSize; @@ -570,9 +565,9 @@ begin FTitleSize := 0; if not Visible then exit; if IsVertical then - CalcVertSize + FSize := CalcMarksSize(AExtent.a.Y, AExtent.b.Y).cx else - CalcHorSize; + FSize := CalcMarksSize(AExtent.a.X, AExtent.b.X).cy; if FSize > 0 then FSize += TickLength + Marks.Distance; CalcTitleSize; diff --git a/components/tachart/taseries.pas b/components/tachart/taseries.pas index f0e558a21f..f32137cf80 100644 --- a/components/tachart/taseries.pas +++ b/components/tachart/taseries.pas @@ -44,7 +44,6 @@ type TBasicPointSeries = class(TChartSeries) private - FPrevLabelRect: TRect; procedure SetUseReticule(AValue: Boolean); protected @@ -679,6 +678,8 @@ end; { TBasicPointSeries } procedure TBasicPointSeries.DrawLabels(ACanvas: TCanvas); +var + prevLabelPoly: TPointArray; procedure DrawLabel( const AText: String; const ADataPoint: TPoint; ADir: TLabelDirection); @@ -686,7 +687,6 @@ procedure TBasicPointSeries.DrawLabels(ACanvas: TCanvas); OFFSETS: array [TLabelDirection] of TPoint = ((X: -1; Y: 0), (X: 0; Y: -1), (X: 1; Y: 0), (X: 0; Y: 1)); var - labelRect: TRect; center: TPoint; sz: TSize; begin @@ -696,15 +696,7 @@ procedure TBasicPointSeries.DrawLabels(ACanvas: TCanvas); center := ADataPoint; center.X += OFFSETS[ADir].X * (Marks.Distance + sz.cx div 2); center.Y += OFFSETS[ADir].Y * (Marks.Distance + sz.cy div 2); - with center do - labelRect := BoundsSize(X - sz.cx div 2, Y - sz.cy div 2, sz); - if Marks.IsLabelHiddenDueToOverlap(FPrevLabelRect, labelRect) then exit; - - // Link between the label and the bar. - ACanvas.Pen.Assign(Marks.LinkPen); - ACanvas.Line(ADataPoint, center); - - Marks.DrawLabel(ACanvas, labelRect, AText); + Marks.DrawLabel(ACanvas, ADataPoint, center, AText, prevLabelPoly); end; var @@ -791,8 +783,6 @@ begin d := IfThen(dir in [ldLeft, ldRight], cx, cy); m[dir] := Max(m[dir], d + Marks.Distance + LABEL_TO_BORDER); end; - - FPrevLabelRect := Rect(0, 0, 0, 0); end; { TBarSeries } @@ -969,7 +959,7 @@ var SetLength(labelTexts, Count); for i := 0 to Count - 1 do begin labelTexts[i] := FormattedMark(i); - with ACanvas.TextExtent(labelTexts[i]) do begin + with Marks.MeasureLabel(ACanvas, labelTexts[i]) do begin labelWidths[i] := cx; labelHeights[i] := cy; end; @@ -992,9 +982,10 @@ var var i, radius: Integer; prevAngle: Double = 0; - angleStep, sliceCenterAngle: Double; - a, b, c, center: TPoint; - r: TRect; + d, angleStep, sliceCenterAngle: Double; + c, center: TPoint; + sa, ca: Extended; + prevLabelPoly: TPointArray = nil; const RAD_TO_DEG16 = 360 * 16; begin @@ -1023,21 +1014,14 @@ begin if not Marks.IsMarkLabelsVisible then continue; - a := LineEndPoint(c, sliceCenterAngle, radius); - b := LineEndPoint(c, sliceCenterAngle, radius + Marks.Distance); - - // line from mark to pie - ACanvas.Pen.Assign(Marks.LinkPen); - ACanvas.Line(a, b); - - if b.x < center.x then - b.x -= labelWidths[i]; - if b.y < center.y then - b.y -= labelHeights[i]; - - r := Bounds(b.x, b.y, labelWidths[i], labelHeights[i]); - InflateRect(r, MARKS_MARGIN_X, MARKS_MARGIN_Y); - Marks.DrawLabel(ACanvas, r, labelTexts[i]); + // This is a crude approximation of label "radius", it may be improved. + SinCos(DegToRad(sliceCenterAngle / 16), sa, ca); + d := Max(Abs(labelWidths[i] * ca), Abs(labelHeights[i] * sa)) / 2; + Marks.DrawLabel( + ACanvas, + LineEndPoint(c, sliceCenterAngle, radius), + LineEndPoint(c, sliceCenterAngle, radius + Marks.Distance + d), + labelTexts[i], prevLabelPoly); end; end; diff --git a/components/tachart/tatypes.pas b/components/tachart/tatypes.pas index 57ae37c91c..4feadc5b1a 100644 --- a/components/tachart/tatypes.pas +++ b/components/tachart/tatypes.pas @@ -116,6 +116,8 @@ type generic TGenericChartMarks<_TLabelBrush, _TLinkPen, _TFramePen> = class(TChartElement) + private + function LabelAngle: Double; inline; protected FClipped: Boolean; FDistance: TChartDistance; @@ -145,9 +147,8 @@ type public procedure Assign(Source: TPersistent); override; procedure DrawLabel( - ACanvas: TCanvas; const ALabelRect: TRect; const AText: String); - function IsLabelHiddenDueToOverlap( - var APrevLabelRect: TRect; const ALabelRect: TRect): Boolean; + ACanvas: TCanvas; const ADataPoint, ALabelCenter: TPoint; + const AText: String; var APrevLabelPoly: TPointArray); function IsMarkLabelsVisible: Boolean; function MeasureLabel(ACanvas: TCanvas; const AText: String): TSize; @@ -277,7 +278,7 @@ type implementation uses - TASources; + Math, TASources; { TChartPen } @@ -450,41 +451,56 @@ begin end; procedure TGenericChartMarks.DrawLabel( - ACanvas: TCanvas; const ALabelRect: TRect; const AText: String); + ACanvas: TCanvas; const ADataPoint, ALabelCenter: TPoint; + const AText: String; var APrevLabelPoly: TPointArray); var wasClipping: Boolean = false; - pt: TPoint; + labelPoly: TPointArray; + ptSize, ptText: TPoint; + a: Double; + i: Integer; begin + ACanvas.Font.Assign(LabelFont); + ptText := ACanvas.TextExtent(AText); + ptSize := ptText; + if IsMarginRequired then + ptSize += Point(MARKS_MARGIN_X, MARKS_MARGIN_Y) * 2; + + SetLength(labelPoly, 4); + labelPoly[0] := -ptSize div 2; + labelPoly[2] := labelPoly[0] + ptSize; + labelPoly[1] := Point(labelPoly[2].X, labelPoly[0].Y); + labelPoly[3] := Point(labelPoly[0].X, labelPoly[2].Y); + a := LabelAngle; + for i := 0 to High(labelPoly) do + labelPoly[i] := RotatePoint(labelPoly[i], a) + ALabelCenter; + + if + (OverlapPolicy = opHideNeighbour) and + IsPolygonIntersectsPolygon(APrevLabelPoly, labelPoly) + then + exit; + APrevLabelPoly := labelPoly; + if not Clipped and ACanvas.Clipping then begin ACanvas.Clipping := false; wasClipping := true; end; - pt := ALabelRect.TopLeft; - ACanvas.Font.Assign(LabelFont); + + ACanvas.Pen.Assign(LinkPen); + ACanvas.Line(ADataPoint, ALabelCenter); ACanvas.Brush.Assign(LabelBrush); if IsMarginRequired then begin ACanvas.Pen.Assign(Frame); - ACanvas.Rectangle(ALabelRect); - pt += Point(MARKS_MARGIN_X, MARKS_MARGIN_Y); + ACanvas.Polygon(labelPoly); end; - ACanvas.TextOut(pt.X, pt.Y, AText); + + ptText := RotatePoint(-ptText div 2, a) + ALabelCenter; + ACanvas.TextOut(ptText.X, ptText.Y, AText); if wasClipping then ACanvas.Clipping := true; end; -function TGenericChartMarks.IsLabelHiddenDueToOverlap( - var APrevLabelRect: TRect; const ALabelRect: TRect): Boolean; -var - dummy: TRect = (Left: 0; Top: 0; Right: 0; Bottom: 0); -begin - Result := - (OverlapPolicy = opHideNeighbour) and - not IsRectEmpty(APrevLabelRect) and - IntersectRect(dummy, ALabelRect, APrevLabelRect); - if not Result then - APrevLabelRect := ALabelRect; -end; - function TGenericChartMarks.IsMarginRequired: Boolean; begin Result := @@ -497,15 +513,28 @@ begin Result := Visible and (Style <> smsNone) and (Format <> ''); end; +function TGenericChartMarks.LabelAngle: Double; +begin + // Negate to take into account top-down Y axis. + Result := -DegToRad(LabelFont.Orientation / 10); +end; + function TGenericChartMarks.MeasureLabel( ACanvas: TCanvas; const AText: String): TSize; +var + pt1, pt2: TPoint; + a: Double; begin ACanvas.Font.Assign(LabelFont); - Result := ACanvas.TextExtent(AText); - if IsMarginRequired then begin - Result.cx += 2 * MARKS_MARGIN_X; - Result.cy += 2 * MARKS_MARGIN_Y; - end; + pt1 := ACanvas.TextExtent(AText) div 2; + if IsMarginRequired then + pt1 += Point(MARKS_MARGIN_X, MARKS_MARGIN_Y); + pt2 := Point(pt1.X, -pt1.Y); + a := LabelAngle; + pt1 := RotatePoint(pt1, a); + pt2 := RotatePoint(pt2, a); + Result.cx := Max(Abs(pt1.X), Abs(pt2.X)) * 2; + Result.cy := Max(Abs(pt1.Y), Abs(pt2.Y)) * 2; end; procedure TGenericChartMarks.SetClipped(const AValue: Boolean);