mirror of
https://gitlab.com/freepascal.org/lazarus/lazarus.git
synced 2025-08-17 13:59:23 +02:00
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:
parent
e0a5a6d15b
commit
d5a2df6249
@ -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);
|
||||||
|
|
||||||
const
|
function CalcMarksSize(AMin, AMax: Double): TSize;
|
||||||
SOME_DIGIT = '0';
|
const
|
||||||
|
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
|
// CalculateTransformationCoeffs changes axis interval, so it is possibile
|
||||||
FSize := Max(cx, FSize);
|
// that a new mark longer then existing ones is introduced.
|
||||||
// CalculateTransformationCoeffs changes axis interval, so it is possibile
|
// That will change marks width and reduce view area,
|
||||||
// that a new mark longer then existing ones is introduced.
|
// requiring another call to CalculateTransformationCoeffs...
|
||||||
// That will change marks width and reduce view area,
|
// So punt for now and just reserve space for extra digit unconditionally.
|
||||||
// requiring another call to CalculateTransformationCoeffs...
|
t := FMarkTexts[i];
|
||||||
// So punt for now and just reserve space for extra digit unconditionally.
|
if AFirstPass then
|
||||||
if AFirstPass then
|
t += SOME_DIGIT;
|
||||||
FSize += ACanvas.TextWidth(SOME_DIGIT);
|
sz := Marks.MeasureLabel(ACanvas, t);
|
||||||
end;
|
Result.cx := Max(sz.cx, Result.cx);
|
||||||
|
Result.cy := Max(sz.cy, Result.cy);
|
||||||
procedure CalcHorSize;
|
end;
|
||||||
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;
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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,41 +451,56 @@ 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;
|
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
|
if wasClipping then
|
||||||
ACanvas.Clipping := true;
|
ACanvas.Clipping := true;
|
||||||
end;
|
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;
|
function TGenericChartMarks.IsMarginRequired: Boolean;
|
||||||
begin
|
begin
|
||||||
Result :=
|
Result :=
|
||||||
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user