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 -
This commit is contained in:
ask 2010-07-22 06:40:43 +00:00
parent e0a5a6d15b
commit d5a2df6249
3 changed files with 113 additions and 105 deletions

View File

@ -361,20 +361,19 @@ procedure TChartAxis.Draw(
const ATransf: ICoordTransformer; var ARect: TRect); const ATransf: ICoordTransformer; var ARect: TRect);
var 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 begin
if Marks.IsLabelHiddenDueToOverlap(prevLabelRect, ALabelRect) then exit;
PrepareSimplePen(ACanvas, TickColor); PrepareSimplePen(ACanvas, TickColor);
ACanvas.Line(ATickRect); ACanvas.Line(ATickRect);
Marks.DrawLabel(ACanvas, ALabelRect, AText); Marks.DrawLabel(ACanvas, ALabelCenter, ALabelCenter, AText, prevLabelPoly);
end; end;
procedure DrawXMark(AY: Integer; AMark: Double; const AText: String); procedure DrawXMark(AY: Integer; AMark: Double; const AText: String);
var var
x, t: Integer; x, d: Integer;
sz: TSize;
begin begin
x := ATransf.XGraphToImage(AMark); x := ATransf.XGraphToImage(AMark);
@ -386,18 +385,17 @@ var
x, ATransf.YGraphToImage(AExtent.b.Y)); x, ATransf.YGraphToImage(AExtent.b.Y));
end; end;
sz := Marks.MeasureLabel(ACanvas, AText); d :=
t := TickLength + Marks.Distance; TickLength + Marks.Distance + Marks.MeasureLabel(ACanvas, AText).cy div 2;
t := IfThen(Alignment = calTop, - t - sz.cy, t); if Alignment = calTop then
d := -d;
DrawLabelAndTick( DrawLabelAndTick(
BoundsSize(x - sz.cx div 2, AY + t, sz), Point(x, AY + d), Rect(x, AY - TickLength, x, AY + TickLength), AText);
Rect(x, AY - TickLength, x, AY + TickLength), AText);
end; end;
procedure DrawYMark(AX: Integer; AMark: Double; const AText: String); procedure DrawYMark(AX: Integer; AMark: Double; const AText: String);
var var
y, t: Integer; y, d: Integer;
sz: TSize;
begin begin
y := ATransf.YGraphToImage(AMark); y := ATransf.YGraphToImage(AMark);
@ -409,12 +407,12 @@ var
ATransf.XGraphToImage(AExtent.b.X), y); ATransf.XGraphToImage(AExtent.b.X), y);
end; end;
sz := Marks.MeasureLabel(ACanvas, AText); d :=
t := TickLength + Marks.Distance; TickLength + Marks.Distance + Marks.MeasureLabel(ACanvas, AText).cx div 2;
t := IfThen(Alignment = calLeft, - t - sz.cx, t); if Alignment = calLeft then
d := -d;
DrawLabelAndTick( DrawLabelAndTick(
BoundsSize(AX + t, y - sz.cy div 2, sz), Point(AX + d, y), Rect(AX - TickLength, y, AX + TickLength, y), AText);
Rect(AX - TickLength, y, AX + TickLength, y), AText);
end; end;
var var
@ -515,33 +513,30 @@ procedure TChartAxis.Measure(
ACanvas: TCanvas; const AExtent: TDoubleRect; AFirstPass: Boolean; ACanvas: TCanvas; const AExtent: TDoubleRect; AFirstPass: Boolean;
var AMargins: TChartAxisMargins); var AMargins: TChartAxisMargins);
function CalcMarksSize(AMin, AMax: Double): TSize;
const const
SOME_DIGIT = '0'; SOME_DIGIT = '0';
procedure CalcVertSize;
var var
i: Integer; i: Integer;
t: String;
sz: TSize;
begin begin
if AExtent.a.Y = AExtent.b.Y then exit; Result := Size(0, 0);
GetMarkValues(AExtent.a.Y, AExtent.b.Y); if AMin = AMax then exit;
FSize := 0; GetMarkValues(AMin, AMax);
for i := 0 to High(FMarkTexts) do for i := 0 to High(FMarkTexts) do begin
with Marks.MeasureLabel(ACanvas, FMarkTexts[i]) do
FSize := Max(cx, FSize);
// CalculateTransformationCoeffs changes axis interval, so it is possibile // CalculateTransformationCoeffs changes axis interval, so it is possibile
// that a new mark longer then existing ones is introduced. // that a new mark longer then existing ones is introduced.
// That will change marks width and reduce view area, // That will change marks width and reduce view area,
// requiring another call to CalculateTransformationCoeffs... // requiring another call to CalculateTransformationCoeffs...
// So punt for now and just reserve space for extra digit unconditionally. // So punt for now and just reserve space for extra digit unconditionally.
t := FMarkTexts[i];
if AFirstPass then if AFirstPass then
FSize += ACanvas.TextWidth(SOME_DIGIT); 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 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;
end; end;
procedure CalcTitleSize; procedure CalcTitleSize;
@ -570,9 +565,9 @@ begin
FTitleSize := 0; FTitleSize := 0;
if not Visible then exit; if not Visible then exit;
if IsVertical then if IsVertical then
CalcVertSize FSize := CalcMarksSize(AExtent.a.Y, AExtent.b.Y).cx
else else
CalcHorSize; FSize := CalcMarksSize(AExtent.a.X, AExtent.b.X).cy;
if FSize > 0 then if FSize > 0 then
FSize += TickLength + Marks.Distance; FSize += TickLength + Marks.Distance;
CalcTitleSize; CalcTitleSize;

View File

@ -44,7 +44,6 @@ type
TBasicPointSeries = class(TChartSeries) TBasicPointSeries = class(TChartSeries)
private private
FPrevLabelRect: TRect;
procedure SetUseReticule(AValue: Boolean); procedure SetUseReticule(AValue: Boolean);
protected protected
@ -679,6 +678,8 @@ end;
{ TBasicPointSeries } { TBasicPointSeries }
procedure TBasicPointSeries.DrawLabels(ACanvas: TCanvas); procedure TBasicPointSeries.DrawLabels(ACanvas: TCanvas);
var
prevLabelPoly: TPointArray;
procedure DrawLabel( procedure DrawLabel(
const AText: String; const ADataPoint: TPoint; ADir: TLabelDirection); const AText: String; const ADataPoint: TPoint; ADir: TLabelDirection);
@ -686,7 +687,6 @@ procedure TBasicPointSeries.DrawLabels(ACanvas: TCanvas);
OFFSETS: array [TLabelDirection] of TPoint = OFFSETS: array [TLabelDirection] of TPoint =
((X: -1; Y: 0), (X: 0; Y: -1), (X: 1; Y: 0), (X: 0; Y: 1)); ((X: -1; Y: 0), (X: 0; Y: -1), (X: 1; Y: 0), (X: 0; Y: 1));
var var
labelRect: TRect;
center: TPoint; center: TPoint;
sz: TSize; sz: TSize;
begin begin
@ -696,15 +696,7 @@ procedure TBasicPointSeries.DrawLabels(ACanvas: TCanvas);
center := ADataPoint; center := ADataPoint;
center.X += OFFSETS[ADir].X * (Marks.Distance + sz.cx div 2); center.X += OFFSETS[ADir].X * (Marks.Distance + sz.cx div 2);
center.Y += OFFSETS[ADir].Y * (Marks.Distance + sz.cy div 2); center.Y += OFFSETS[ADir].Y * (Marks.Distance + sz.cy div 2);
with center do Marks.DrawLabel(ACanvas, ADataPoint, center, AText, prevLabelPoly);
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);
end; end;
var var
@ -791,8 +783,6 @@ begin
d := IfThen(dir in [ldLeft, ldRight], cx, cy); d := IfThen(dir in [ldLeft, ldRight], cx, cy);
m[dir] := Max(m[dir], d + Marks.Distance + LABEL_TO_BORDER); m[dir] := Max(m[dir], d + Marks.Distance + LABEL_TO_BORDER);
end; end;
FPrevLabelRect := Rect(0, 0, 0, 0);
end; end;
{ TBarSeries } { TBarSeries }
@ -969,7 +959,7 @@ var
SetLength(labelTexts, Count); SetLength(labelTexts, Count);
for i := 0 to Count - 1 do begin for i := 0 to Count - 1 do begin
labelTexts[i] := FormattedMark(i); labelTexts[i] := FormattedMark(i);
with ACanvas.TextExtent(labelTexts[i]) do begin with Marks.MeasureLabel(ACanvas, labelTexts[i]) do begin
labelWidths[i] := cx; labelWidths[i] := cx;
labelHeights[i] := cy; labelHeights[i] := cy;
end; end;
@ -992,9 +982,10 @@ var
var var
i, radius: Integer; i, radius: Integer;
prevAngle: Double = 0; prevAngle: Double = 0;
angleStep, sliceCenterAngle: Double; d, angleStep, sliceCenterAngle: Double;
a, b, c, center: TPoint; c, center: TPoint;
r: TRect; sa, ca: Extended;
prevLabelPoly: TPointArray = nil;
const const
RAD_TO_DEG16 = 360 * 16; RAD_TO_DEG16 = 360 * 16;
begin begin
@ -1023,21 +1014,14 @@ begin
if not Marks.IsMarkLabelsVisible then continue; if not Marks.IsMarkLabelsVisible then continue;
a := LineEndPoint(c, sliceCenterAngle, radius); // This is a crude approximation of label "radius", it may be improved.
b := LineEndPoint(c, sliceCenterAngle, radius + Marks.Distance); SinCos(DegToRad(sliceCenterAngle / 16), sa, ca);
d := Max(Abs(labelWidths[i] * ca), Abs(labelHeights[i] * sa)) / 2;
// line from mark to pie Marks.DrawLabel(
ACanvas.Pen.Assign(Marks.LinkPen); ACanvas,
ACanvas.Line(a, b); LineEndPoint(c, sliceCenterAngle, radius),
LineEndPoint(c, sliceCenterAngle, radius + Marks.Distance + d),
if b.x < center.x then labelTexts[i], prevLabelPoly);
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]);
end; end;
end; end;

View File

@ -116,6 +116,8 @@ type
generic TGenericChartMarks<_TLabelBrush, _TLinkPen, _TFramePen> = generic TGenericChartMarks<_TLabelBrush, _TLinkPen, _TFramePen> =
class(TChartElement) class(TChartElement)
private
function LabelAngle: Double; inline;
protected protected
FClipped: Boolean; FClipped: Boolean;
FDistance: TChartDistance; FDistance: TChartDistance;
@ -145,9 +147,8 @@ type
public public
procedure Assign(Source: TPersistent); override; procedure Assign(Source: TPersistent); override;
procedure DrawLabel( procedure DrawLabel(
ACanvas: TCanvas; const ALabelRect: TRect; const AText: String); ACanvas: TCanvas; const ADataPoint, ALabelCenter: TPoint;
function IsLabelHiddenDueToOverlap( const AText: String; var APrevLabelPoly: TPointArray);
var APrevLabelRect: TRect; const ALabelRect: TRect): Boolean;
function IsMarkLabelsVisible: Boolean; function IsMarkLabelsVisible: Boolean;
function MeasureLabel(ACanvas: TCanvas; const AText: String): TSize; function MeasureLabel(ACanvas: TCanvas; const AText: String): TSize;
@ -277,7 +278,7 @@ type
implementation implementation
uses uses
TASources; Math, TASources;
{ TChartPen } { TChartPen }
@ -450,39 +451,54 @@ begin
end; end;
procedure TGenericChartMarks.DrawLabel( procedure TGenericChartMarks.DrawLabel(
ACanvas: TCanvas; const ALabelRect: TRect; const AText: String); ACanvas: TCanvas; const ADataPoint, ALabelCenter: TPoint;
const AText: String; var APrevLabelPoly: TPointArray);
var var
wasClipping: Boolean = false; wasClipping: Boolean = false;
pt: TPoint; labelPoly: TPointArray;
ptSize, ptText: TPoint;
a: Double;
i: Integer;
begin 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 if not Clipped and ACanvas.Clipping then begin
ACanvas.Clipping := false; ACanvas.Clipping := false;
wasClipping := true; wasClipping := true;
end; end;
pt := ALabelRect.TopLeft;
ACanvas.Font.Assign(LabelFont); ACanvas.Pen.Assign(LinkPen);
ACanvas.Line(ADataPoint, ALabelCenter);
ACanvas.Brush.Assign(LabelBrush); ACanvas.Brush.Assign(LabelBrush);
if IsMarginRequired then begin if IsMarginRequired then begin
ACanvas.Pen.Assign(Frame); ACanvas.Pen.Assign(Frame);
ACanvas.Rectangle(ALabelRect); ACanvas.Polygon(labelPoly);
pt += Point(MARKS_MARGIN_X, MARKS_MARGIN_Y);
end;
ACanvas.TextOut(pt.X, pt.Y, AText);
if wasClipping then
ACanvas.Clipping := true;
end; end;
function TGenericChartMarks.IsLabelHiddenDueToOverlap( ptText := RotatePoint(-ptText div 2, a) + ALabelCenter;
var APrevLabelRect: TRect; const ALabelRect: TRect): Boolean; ACanvas.TextOut(ptText.X, ptText.Y, AText);
var if wasClipping then
dummy: TRect = (Left: 0; Top: 0; Right: 0; Bottom: 0); ACanvas.Clipping := true;
begin
Result :=
(OverlapPolicy = opHideNeighbour) and
not IsRectEmpty(APrevLabelRect) and
IntersectRect(dummy, ALabelRect, APrevLabelRect);
if not Result then
APrevLabelRect := ALabelRect;
end; end;
function TGenericChartMarks.IsMarginRequired: Boolean; function TGenericChartMarks.IsMarginRequired: Boolean;
@ -497,15 +513,28 @@ begin
Result := Visible and (Style <> smsNone) and (Format <> ''); Result := Visible and (Style <> smsNone) and (Format <> '');
end; end;
function TGenericChartMarks.LabelAngle: Double;
begin
// Negate to take into account top-down Y axis.
Result := -DegToRad(LabelFont.Orientation / 10);
end;
function TGenericChartMarks.MeasureLabel( function TGenericChartMarks.MeasureLabel(
ACanvas: TCanvas; const AText: String): TSize; ACanvas: TCanvas; const AText: String): TSize;
var
pt1, pt2: TPoint;
a: Double;
begin begin
ACanvas.Font.Assign(LabelFont); ACanvas.Font.Assign(LabelFont);
Result := ACanvas.TextExtent(AText); pt1 := ACanvas.TextExtent(AText) div 2;
if IsMarginRequired then begin if IsMarginRequired then
Result.cx += 2 * MARKS_MARGIN_X; pt1 += Point(MARKS_MARGIN_X, MARKS_MARGIN_Y);
Result.cy += 2 * MARKS_MARGIN_Y; pt2 := Point(pt1.X, -pt1.Y);
end; 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; end;
procedure TGenericChartMarks.SetClipped(const AValue: Boolean); procedure TGenericChartMarks.SetClipped(const AValue: Boolean);