lazarus/components/tachart/tamultiseries.pas

2584 lines
76 KiB
ObjectPascal

{
*****************************************************************************
See the file COPYING.modifiedLGPL.txt, included in this distribution,
for details about the license.
*****************************************************************************
Authors: Alexander Klenin
}
unit TAMultiSeries;
{$MODE ObjFPC}{$H+}
{$WARN 6058 off : Call to subroutine "$1" marked as inline is not inlined}
interface
uses
Classes, Graphics,
TAChartUtils, TATypes, TACustomSource, TACustomSeries, TADrawUtils, TALegend;
const
DEF_BOX_WIDTH = 50;
DEF_WHISKERS_WIDTH = 25;
DEF_OHLC_TICK_WIDTH = 25;
DEF_YINDEX_OPEN = 1;
DEF_YINDEX_HIGH = 3;
DEF_YINDEX_LOW = 0;
DEF_YINDEX_CLOSE = 2;
DEF_YINDEX_WHISKERMIN = 0;
DEF_YINDEX_BOXMIN = 1;
DEF_YINDEX_CENTER = 2;
DEF_YINDEX_BOXMAX = 3;
DEF_YINDEX_WHISKERMAX = 4;
type
// TBubbleRadiusTransform = (brtNone, brtX, brtY); not used
TBubbleOverrideColor = (bocBrush, bocPen);
TBubbleOverrideColors = set of TBubbleOverrideColor;
TBubbleRadiusUnits = (
bruX, // Circle with radius given in x axis units
bruY, // Circle with radius given in y axis units
bruXY, // Ellipse
bruPercentageRadius, // Bubble radius is percentage of the smallest dimension of plot area
bruPercentageArea // dto., but bubble area
);
{ TBubbleSeries }
TBubbleSeries = class(TBasicPointSeries)
private
FBubbleBrush: TBrush;
FBubblePen: TPen;
FOverrideColor: TBubbleOverrideColors;
FBubbleRadiusPercentage: Integer;
FBubbleRadiusUnits: TBubbleRadiusUnits;
FBubbleScalingFactor: Double;
procedure SetBubbleBrush(AValue: TBrush);
procedure SetBubblePen(AValue: TPen);
procedure SetBubbleRadiusPercentage(AValue: Integer);
procedure SetBubbleRadiusUnits(AValue: TBubbleRadiusUnits);
procedure SetOverrideColor(AValue: TBubbleOverrideColors);
protected
function CalcBubbleScalingFactor(const ARect: TRect): Double;
function GetBubbleRect(AItem: PChartDataItem; AFactor: Double; out ARect: TRect): Boolean;
function GetLabelDataPoint(AIndex, AYIndex: Integer): TDoublePoint; override;
procedure GetLegendItems(AItems: TChartLegendItems); override;
function GetSeriesColor: TColor; override;
class procedure GetXYCountNeeded(out AXCount, AYCount: Cardinal); override;
function ToolTargetDistance(const AParams: TNearestPointParams;
AGraphPt: TDoublePoint; APointIdx, AXIdx, AYIdx: Integer): Integer; override;
procedure UpdateLabelDirectionReferenceLevel(AIndex, AYIndex: Integer;
var ALevel: Double); override;
procedure UpdateMargins(ADrawer: IChartDrawer; var AMargins: TRect); override;
public
function AddXY(AX, AY, ARadius: Double; AXLabel: String = '';
AColor: TColor = clTAColor): Integer; overload;
procedure Assign(ASource: TPersistent); override;
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure Draw(ADrawer: IChartDrawer); override;
function Extent: TDoubleRect; override;
function GetNearestPoint(const AParams: TNearestPointParams;
out AResults: TNearestPointResults): Boolean; override;
procedure MovePointEx(var AIndex: Integer; AXIndex, AYIndex: Integer;
const ANewPos: TDoublePoint); override;
published
property AxisIndexX;
property AxisIndexY;
property BubbleBrush: TBrush read FBubbleBrush write SetBubbleBrush;
property BubblePen: TPen read FBubblePen write SetBubblePen;
property BubbleRadiusPercentage: Integer read FBubbleRadiusPercentage
write SetBubbleRadiusPercentage default 20;
property BubbleRadiusUnits: TBubbleRadiusUnits read FBubbleRadiusUnits
write SetBubbleRadiusUnits default bruXY;
property MarkPositions;
property Marks;
property OverrideColor: TBubbleOverrideColors
read FOverrideColor write SetOverrideColor default [];
property Source;
property Styles;
property ToolTargets default [nptPoint, nptYList, nptCustom];
end;
TBoxAndWhiskerSeriesLegendDir = (bwlHorizontal, bwlVertical, bwlAuto);
TBoxAndWhiskerSeriesWidthStyle = (bwsPercent, bwsPercentMin);
TBoxAndWhiskerYDataLayout = (bwlNormal, bwlLegacy, bwlCustom);
TBoxAndWhiskerSeries = class(TBasicPointSeries)
strict private
FBoxBrush: TBrush;
FBoxPen: TPen;
FBoxWidth: Integer;
FLegendDirection: TBoxAndWhiskerSeriesLegendDir;
FMedianPen: TPen;
FWhiskersPen: TPen;
FWhiskersWidth: Integer;
FWidthStyle: TBoxAndWhiskerSeriesWidthStyle;
FYDataLayout: TBoxAndWhiskerYDataLayout;
FYIndexWhiskerMin: Integer;
FYIndexBoxMin: Integer;
FYIndexCenter: Integer;
FYIndexBoxMax: Integer;
FYIndexWhiskerMax: Integer;
procedure SetBoxBrush(AValue: TBrush);
procedure SetBoxPen(AValue: TPen);
procedure SetBoxWidth(AValue: Integer);
procedure SetLegendDirection(AValue: TBoxAndWhiskerSeriesLegendDir);
procedure SetMedianPen(AValue: TPen);
procedure SetWhiskersPen(AValue: TPen);
procedure SetWhiskersWidth(AValue: Integer);
procedure SetYDataLayout(AValue: TBoxAndWhiskerYDataLayout);
procedure SetYIndexBoxMax(AValue: Integer);
procedure SetYIndexBoxMin(AValue: Integer);
procedure SetYIndexCenter(AValue: Integer);
procedure SetYIndexWhiskerMax(AValue: Integer);
procedure SetYIndexWhiskerMin(AValue: Integer);
procedure UpdateYDataLayout;
protected
procedure GetLegendItems(AItems: TChartLegendItems); override;
function GetSeriesColor: TColor; override;
class procedure GetXYCountNeeded(out AXCount, AYCount: Cardinal); override;
function SkipMissingValues(AIndex: Integer): Boolean; override;
function ToolTargetDistance(const AParams: TNearestPointParams;
AGraphPt: TDoublePoint; APointIdx, AXIdx, AYIdx: Integer): Integer; override;
procedure UpdateLabelDirectionReferenceLevel(AIndex, AYIndex: Integer;
var ALevel: Double); override;
public
function AddXY(
AX, AYLoWhisker, AYLoBox, AY, AYHiBox, AYHiWhisker: Double;
AXLabel: String = ''; AColor: TColor = clTAColor): Integer; overload;
procedure Assign(ASource: TPersistent); override;
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure Draw(ADrawer: IChartDrawer); override;
function Extent: TDoubleRect; override;
function GetNearestPoint(const AParams: TNearestPointParams;
out AResults: TNearestPointResults): Boolean; override;
published
property BoxBrush: TBrush read FBoxBrush write SetBoxBrush;
property BoxPen: TPen read FBoxPen write SetBoxPen;
property BoxWidth: Integer
read FBoxWidth write SetBoxWidth default DEF_BOX_WIDTH;
property LegendDirection: TBoxAndWhiskerSeriesLegendDir
read FLegendDirection write SetLegendDirection default bwlHorizontal;
property MedianPen: TPen read FMedianPen write SetMedianPen;
property ToolTargets default [nptPoint, nptYList, nptCustom];
property WidthStyle: TBoxAndWhiskerSeriesWidthStyle
read FWidthStyle write FWidthStyle default bwsPercent;
property WhiskersPen: TPen read FWhiskersPen write SetWhiskersPen;
property WhiskersWidth: Integer
read FWhiskersWidth write SetWhiskersWidth default DEF_WHISKERS_WIDTH;
property YDataLayout: TBoxAndWhiskerYDataLayout
read FYDataLayout write SetYDataLayout default bwlLegacy;
property YIndexBoxMax: Integer
read FYIndexBoxMax write SetYIndexBoxMax default DEF_YINDEX_BOXMAX;
property YIndexBoxMin: Integer
read FYIndexBoxMin write SetYIndexBoxMin default DEF_YINDEX_BOXMIN;
property YIndexCenter: Integer
read FYIndexCenter write SetYIndexCenter default DEF_YINDEX_CENTER;
property YIndexWhiskerMax: Integer
read FYIndexWhiskerMax write SetYIndexWhiskerMax default DEF_YINDEX_WHISKERMAX;
property YIndexWhiskerMin: Integer
read FYIndexWhiskerMin write SetYIndexWhiskerMin default DEF_YINDEX_WHISKERMIN;
published
property AxisIndexX;
property AxisIndexY;
property MarkPositions;
property Marks;
property Source;
end;
TOHLCBrushKind = (obkCandleUp, obkCandleDown);
TOHLCPenKind = (opkCandleUp, opkCandleDown, opkCandleLine, opkLineUp, opkLineDown);
TOHLCBrush = class(TBrush)
private
const
DEFAULT_COLORS: array[TOHLCBrushKind] of TColor = (clLime, clRed);
private
FBrushKind: TOHLCBrushKind;
function IsColorStored: Boolean;
procedure SetBrushKind(AValue: TOHLCBrushKind);
public
property BrushKind: TOHLCBrushKind read FBrushKind write SetBrushKind;
published
property Color stored IsColorStored;
end;
TOHLCPen = class(TPen)
private
const
DEFAULT_COLORS: array[TOHLCPenKind] of TColor = (clGreen, clMaroon, clDefault, clLime, clRed);
private
FPenKind: TOHLCPenKind;
function IsColorStored: Boolean;
procedure SetPenKind(AValue: TOHLCPenKind);
public
property PenKind: TOHLCPenKind read FPenKind write SetPenKind;
published
property Color stored IsColorStored;
end;
TOHLCMode = (mOHLC, mCandleStick);
TTickWidthStyle = (twsPercent, twsPercentMin);
TOpenHighLowCloseSeries = class(TBasicPointSeries)
private
FPen: array[TOHLCPenKind] of TOHLCPen;
FBrush: array[TOHLCBrushKind] of TOHLCBrush;
FTickWidth: Integer;
FTickWidthStyle: TTickWidthStyle;
FYIndexClose: Integer;
FYIndexHigh: Integer;
FYIndexLow: Integer;
FYIndexOpen: Integer;
FMode: TOHLCMode;
function GetBrush(AIndex: TOHLCBrushKind): TOHLCBrush;
function GetPen(AIndex: TOHLCPenKind): TOHLCPen;
procedure SetBrush(AIndex: TOHLCBrushKind; AValue: TOHLCBrush);
procedure SetPen(AIndex: TOHLCPenKind; AValue: TOHLCPen);
procedure SetOHLCMode(AValue: TOHLCMode);
procedure SetTickWidth(AValue: Integer);
procedure SetTickWidthStyle(AValue: TTickWidthStyle);
procedure SetYIndexClose(AValue: Integer);
procedure SetYIndexHigh(AValue: Integer);
procedure SetYIndexLow(AValue: Integer);
procedure SetYIndexOpen(AValue: Integer);
protected
function CalcTickWidth(AX: Double; AIndex: Integer): Double;
procedure GetLegendItems(AItems: TChartLegendItems); override;
function GetSeriesColor: TColor; override;
class procedure GetXYCountNeeded(out AXCount, AYCount: Cardinal); override;
function SkipMissingValues(AIndex: Integer): Boolean; override;
function ToolTargetDistance(const AParams: TNearestPointParams;
AGraphPt: TDoublePoint; APointIdx, AXIdx, AYIdx: Integer): Integer; override;
procedure UpdateLabelDirectionReferenceLevel(AIndex, AYIndex: Integer;
var ALevel: Double); override;
public
procedure Assign(ASource: TPersistent); override;
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
public
function AddXOHLC(
AX, AOpen, AHigh, ALow, AClose: Double;
ALabel: String = ''; AColor: TColor = clTAColor): Integer; inline;
procedure Draw(ADrawer: IChartDrawer); override;
function Extent: TDoubleRect; override;
function GetNearestPoint(const AParams: TNearestPointParams;
out AResults: TNearestPointResults): Boolean; override;
published
property CandlestickDownBrush: TOHLCBrush index obkCandleDown read GetBrush write SetBrush;
property CandlestickDownPen: TOHLCPen index opkCandleDown read GetPen write SetPen;
property CandlestickLinePen: TOHLCPen index opkCandleLine read GetPen write SetPen;
property CandlestickUpBrush: TOHLCBrush index obkCandleUp read GetBrush write SetBrush;
property CandlestickUpPen: TOHLCPen index opkCandleUp read GetPen write Setpen;
property DownLinePen: TOHLCPen index opkLineDown read GetPen write SetPen;
property LinePen: TOHLCPen index opkLineUp read GetPen write SetPen;
property Mode: TOHLCMode read FMode write SetOHLCMode default mOHLC;
property TickWidth: integer
read FTickWidth write SetTickWidth default DEF_OHLC_TICK_WIDTH;
property TickWidthStyle: TTickWidthStyle
read FTickWidthStyle write SetTickWidthStyle default twsPercent;
property ToolTargets default [nptPoint, nptYList, nptCustom];
property YIndexClose: integer
read FYIndexClose write SetYIndexClose default DEF_YINDEX_CLOSE;
property YIndexHigh: Integer
read FYIndexHigh write SetYIndexHigh default DEF_YINDEX_HIGH;
property YIndexLow: Integer
read FYIndexLow write SetYIndexLow default DEF_YINDEX_LOW;
property YIndexOpen: Integer
read FYIndexOpen write SetYIndexOpen default DEF_YINDEX_OPEN;
published
property AxisIndexX;
property AxisIndexY;
property MarkPositions;
property Marks;
property Source;
end;
TVectorCoordKind = (vckCenterDir, vckStartEnd);
TFieldSeries = class(TBasicPointSeries)
private
FArrow: TChartArrow;
FPen: TPen;
FCoordKind: TVectorCoordKind;
procedure SetArrow(AValue: TChartArrow);
procedure SetCoordKind(AValue: TVectorCoordKind);
procedure SetPen(AValue: TPen);
protected
procedure AfterAdd; override;
procedure DrawVector(ADrawer: IChartDrawer; AStartPt, AEndPt: TDoublePoint;
APen: TPen);
function GetColor(AIndex: Integer): TColor; inline;
function GetVectorPoints(AIndex: Integer;
out AStartPt, AEndPt: TDoublePoint): Boolean; inline;
class procedure GetXYCountNeeded(out AXCount, AYCount: Cardinal); override;
public
procedure Assign(ASource: TPersistent); override;
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
function AddVector(AX, AY, AVectorX, AVectorY: Double; AXLabel: String = '';
AColor: TColor = clTAColor): Integer; //inline;
function GetVector(AIndex: Integer): TDoublePoint; inline;
procedure SetVector(AIndex: Integer; const AValue: TDoublePoint); inline;
procedure Draw(ADrawer: IChartDrawer); override;
function Extent: TDoubleRect; override;
procedure GetLegendItems(AItems: TChartLegendItems); override;
function GetNearestPoint(const AParams: TNearestPointParams;
out AResults: TNearestPointResults): Boolean; override;
procedure MovePointEx(var AIndex: Integer; AXIndex, AYIndex: Integer;
const ANewPos: TDoublePoint); override;
procedure NormalizeVectors(ALength: Double);
published
property Arrow: TChartArrow read FArrow write SetArrow;
property AxisIndexX;
property AxisIndexY;
property MarkPositions;
property Marks;
property Pen: TPen read FPen write SetPen;
property Source;
property ToolTargets default [nptPoint, nptXList, nptYList, nptCustom];
property VectorCoordKind: TVectorCoordKind read FCoordKind write SetCoordKind default vckCenterDir;
end;
implementation
uses
FPCanvas, Math, SysUtils, Types,
TAChartStrConsts, TAGeometry, TAGraph, TAMath;
type
TLegendItemOHLCLine = class(TLegendItemLine)
strict private
FMode: TOHLCMode;
FCandleStickUpColor: TColor;
FCandleStickDownColor: TColor;
public
constructor Create(ASeries: TOpenHighLowCloseSeries; const AText: String);
procedure Draw(ADrawer: IChartDrawer; const ARect: TRect); override;
end;
TLegendItemBoxAndWhiskers = class(TLegendItem)
strict private
FBoxBrush: TBrush;
FBoxPen: TPen;
FBoxWidth: Integer;
FIsVertical: Boolean;
FMedianPen: TPen;
FWhiskersPen: TPen;
FWhiskersWidth: Integer;
public
constructor Create(ASeries: TBoxAndWhiskerSeries; const AText: String);
procedure Draw(ADrawer: IChartDrawer; const ARect: TRect); override;
end;
TLegendItemField = class(TLegendItemLine)
strict private
FArrow: TChartArrow;
public
constructor Create(APen: TPen; AArrow: TChartArrow; const AText: String);
procedure Draw(ADrawer: IChartDrawer; const ARect: TRect); override;
end;
{ TLegendItemOHLCLine }
constructor TLegendItemOHLCLine.Create(ASeries: TOpenHighLowCloseSeries; const AText: String);
var
pen: TFPCustomPen;
begin
case ASeries.Mode of
mOHLC : pen := ASeries.LinePen;
mCandleStick : pen := ASeries.CandleStickLinePen;
end;
inherited Create(pen, AText);
FMode := ASeries.Mode;
FCandlestickUpColor := ASeries.CandlestickUpBrush.Color;
FCandlestickDownColor := ASeries.CandlestickDownBrush.Color;
end;
procedure TLegendItemOHLCLine.Draw(ADrawer: IChartDrawer; const ARect: TRect);
const
TICK_LENGTH = 3;
var
dx, dy, x, y: Integer;
pts: array[0..3] of TPoint;
begin
inherited Draw(ADrawer, ARect);
y := (ARect.Top + ARect.Bottom) div 2;
dx := (ARect.Right - ARect.Left) div 3;
x := ARect.Left + dx;
case FMode of
mOHLC:
begin
dy := ADrawer.Scale(TICK_LENGTH);
ADrawer.Line(x, y, x, y + dy);
x += dx;
ADrawer.Line(x, y, x, y - dy);
end;
mCandlestick:
begin
dy := (ARect.Bottom - ARect.Top) div 4;
pts[0] := Point(x, y-dy);
pts[1] := Point(x, y+dy);
pts[2] := Point(x+dx, y+dy);
pts[3] := pts[0];
ADrawer.SetBrushParams(bsSolid, FCandlestickUpColor);
ADrawer.Polygon(pts, 0, 4);
pts[0] := Point(x+dx, y+dy);
pts[1] := Point(x+dx, y-dy);
pts[2] := Point(x, y-dy);
pts[3] := pts[0];
ADrawer.SetBrushParams(bsSolid, FCandlestickDownColor);
ADrawer.Polygon(pts, 0, 4);
end;
end;
end;
{ TLegendItemBoxAndWhiskers }
constructor TLegendItemBoxAndWhiskers.Create(
ASeries: TBoxAndWhiskerSeries; const AText: String);
begin
inherited Create(AText);
with ASeries do begin
FBoxBrush := BoxBrush;
FBoxPen := BoxPen;
FBoxWidth := BoxWidth;
FIsVertical :=
(LegendDirection = bwlVertical) or
(LegendDirection = bwlAuto) and IsRotated;
FMedianPen := MedianPen;
FWhiskersPen := WhiskersPen;
FWhiskersWidth := WhiskersWidth;
end;
end;
procedure TLegendItemBoxAndWhiskers.Draw(
ADrawer: IChartDrawer; const ARect: TRect);
function FlipRect(const AR: TRect): TRect;
begin
Result := Rect(AR.Top, AR.Left, AR.Bottom, AR.Right);
end;
var
symbol: array [1..5] of TRect;
var
center: TPoint;
i, m, ww, bw: Integer;
r: TRect;
begin
inherited Draw(ADrawer, ARect);
r := ARect;
r.BottomRight -= Point(1, 1);
if FIsVertical then
r := FlipRect(r);
center := (r.TopLeft + r.BottomRight) div 2;
m := MaxValue([FWhiskersWidth, FBoxWidth, 1]) * 2;
ww := (r.Bottom - r.Top) * FWhiskersWidth div m;
symbol[1] := Rect(r.Left, center.y, r.Right, center.y);
symbol[2] := Rect(r.Left, center.y - ww, r.Left, center.y + ww + 1);
symbol[3] := Rect(r.Right, center.y - ww, r.Right, center.y + ww + 1);
bw := (r.Bottom - r.Top) * FBoxWidth div m;
symbol[4] := Rect(
(r.Left * 2 + r.Right) div 3, center.y - bw,
(r.Left + r.Right * 2) div 3, center.y + bw);
bw -= Math.IfThen(FBoxPen.Style = psClear, 0, (FBoxPen.Width + 1) div 2);
symbol[5] := Rect(center.x, center.y - bw, center.x, center.y + bw);
if FIsVertical then
for i := 1 to High(symbol) do
symbol[i] := FlipRect(symbol[i]);
// Whisker
ADrawer.Pen := FWhiskersPen;
ADrawer.SetPenColor(FWhiskersPen.Color);
ADrawer.SetBrushParams(bsClear, clTAColor);
for i := 1 to 3 do
ADrawer.Line(symbol[i].TopLeft, symbol[i].BottomRight);
// Box
ADrawer.Pen := FBoxPen;
ADrawer.SetPenColor(FBoxPen.Color);
ADrawer.Brush:= FBoxBrush;
ADrawer.SetBrushColor(FBoxBrush.Color);
ADrawer.Rectangle(symbol[4]);
// Median line
ADrawer.Pen := FMedianPen;
ADrawer.SetPenColor(FMedianPen.Color);
ADrawer.Line(symbol[5].TopLeft, symbol[5].BottomRight);
end;
{ TLegendItemField }
constructor TLegendItemField.Create(APen: TPen; AArrow: TChartArrow;
const AText: String);
begin
inherited Create(APen, AText);
FArrow := AArrow;
end;
procedure TLegendItemField.Draw(ADrawer: IChartDrawer; const ARect: TRect);
var
y: Integer;
len: Double;
arr: TChartArrow;
begin
inherited Draw(ADrawer, ARect);
if (FPen = nil) or (FArrow = nil) or not FArrow.Visible then
exit;
len := (ARect.Right - ARect.Left) * 0.01;
arr := TChartArrow.Create(nil);
try
arr.Assign(FArrow);
arr.SetOwner(nil);
arr.BaseLength := round(FArrow.BaseLength * len);
arr.Length := round(FArrow.Length * len);
arr.Width := round(FArrow.Width * len);
y := (ARect.Top + ARect.Bottom) div 2;
arr.Draw(ADrawer, Point(ARect.Right, y), 0, FPen);
finally
arr.Free;
end;
end;
{ TBubbleSeries }
function TBubbleSeries.AddXY(AX, AY, ARadius: Double; AXLabel: String;
AColor: TColor): Integer;
begin
Result := AddXY(AX, AY, [ARadius], AXLabel, AColor);
end;
procedure TBubbleSeries.Assign(ASource: TPersistent);
begin
if ASource is TBubbleSeries then
with TBubbleSeries(ASource) do begin
Self.BubbleBrush := FBubbleBrush;
Self.BubblePen := FBubblePen;
Self.BubbleRadiusUnits := FBubbleRadiusUnits;
Self.BubbleRadiusPercentage := FBubbleRadiusPercentage;
Self.OverrideColor := FOverrideColor;
end;
inherited Assign(ASource);
end;
{ Adjusts a scaling factor such that the largest bubble radius is the
given percentage (FBubbleRadiusPercentage) of the smallest edge of
the chart area (ARect). }
function TBubbleSeries.CalcBubbleScalingFactor(const ARect: TRect): Double;
var
rMin: Double = 0.0;
rMax: Double = 0.0;
begin
if FBubbleRadiusUnits in [bruPercentageRadius, bruPercentageArea] then
begin
Source.YRange(1, rMin, rMax);
if FBubbleRadiusUnits = bruPercentageArea then
rMax := sqrt(abs(rMax));
Result := Min(ARect.Width, ARect.Height) * FBubbleRadiusPercentage * PERCENT / abs(rMax);
end else
Result := 1.0;
end;
constructor TBubbleSeries.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ToolTargets := [nptPoint, nptYList, nptCustom];
FBubblePen := TPen.Create;
FBubblePen.OnChange := @StyleChanged;
FBubbleBrush := TBrush.Create;
FBubbleBrush.OnChange := @StyleChanged;
FBubbleRadiusPercentage := 20;
FBubbleRadiusUnits := bruXY;
FBubbleScalingFactor := 1.0;
end;
destructor TBubbleSeries.Destroy;
begin
FreeAndNil(FBubbleBrush);
FreeAndNil(FBubblePen);
inherited Destroy;
end;
procedure TBubbleSeries.Draw(ADrawer: IChartDrawer);
var
i: Integer;
item: PChartDataItem;
clipR: TRect;
irect: TRect;
dummyR: TRect = (Left:0; Top:0; Right:0; Bottom:0);
ext: TDoubleRect;
nx, ny: Cardinal;
begin
if IsEmpty or (not Active) then exit;
if not RequestValidChartScaling then exit;
ADrawer.Pen := BubblePen;
if BubblePen.Color = clDefault then
ADrawer.SetPenColor(FChart.GetDefaultColor(dctFont))
else
ADrawer.SetPenColor(BubblePen.Color);
ADrawer.Brush := BubbleBrush;
if BubbleBrush.Color = clDefault then
ADrawer.SetBrushColor(FChart.GetDefaultColor(dctBrush))
else
ADrawer.SetBrushColor(BubbleBrush.Color);
ext := ParentChart.CurrentExtent;
clipR.TopLeft := ParentChart.GraphToImage(ext.a);
clipR.BottomRight := ParentChart.GraphToImage(ext.b);
NormalizeRect(clipR);
ADrawer.ClippingStart(clipR);
FBubbleScalingFactor := CalcBubbleScalingFactor(clipR);
for i := 0 to Count - 1 do begin
item := Source[i];
if not GetBubbleRect(item, FBubbleScalingFactor, irect) then
continue;
if not IntersectRect(dummyR, clipR, irect) then
continue;
if bocPen in OverrideColor then
ADrawer.SetPenParams(BubblePen.Style, ColorDef(item^.Color, BubblePen.Color));
if bocBrush in OverrideColor then
ADrawer.SetBrushColor(ColorDef(item^.Color, BubbleBrush.Color));
if Styles <> nil then
Styles.Apply(ADrawer, i);
ADrawer.Ellipse(irect.Left, irect.Top, irect.Right, irect.Bottom);
end;
GetXYCountNeeded(nx, ny);
if Source.YCount >= ny then
for i := 0 to ny - 1 do DrawLabels(ADrawer, i)
else
DrawLabels(ADrawer);
ADrawer.ClippingStop;
end;
{ Calculates the extent of the series such that bubbles are not clipped.
But note that this method is correct only for BubbleRadiusUnits bruXY, it
would crash for bruX and bruY. Adjust Chart.Margins or Chart.ExpandPercentage
in these cases. }
function TBubbleSeries.Extent: TDoubleRect;
var
i: Integer;
r: Double;
sp, gp, gq, rp: TDoublePoint;
item: PChartDataItem;
begin
Result := EmptyExtent;
if IsEmpty then exit;
if not RequestValidChartScaling then exit;
if FBubbleRadiusUnits = bruXY then
begin
for i := 0 to Count - 1 do begin
item := Source[i];
sp := item^.Point;
if TAChartUtils.IsNaN(sp) then
continue;
r := item^.YList[0];
if Math.IsNaN(r) then
continue;
rp := DoublePoint(r, r);
gp := AxisToGraph(sp);
gq := AxisToGraph(sp + rp);
rp := gq - gp;
Result.a.X := Min(Result.a.X, sp.x - rp.x);
Result.b.X := Max(Result.b.X, sp.x + rp.x);
Result.a.Y := Min(Result.a.Y, sp.y - rp.y);
Result.b.Y := Max(Result.b.Y, sp.y + rp.y);
end;
end else
Result := Source.BasicExtent;
end;
function TBubbleSeries.GetBubbleRect(AItem: PChartDataItem;
AFactor: Double; out ARect: TRect): Boolean;
var
sp: TDoublePoint; // source point in axis units
p: TPoint; // bubble center in image units
q: TPoint; // bubble center offset by 1 radius, in image units
r: Double; // radius in axis units
ri: Integer; // radius in image units
begin
Result := false;
sp := AItem^.Point;
if TAChartUtils.IsNaN(sp) then
exit;
r := AItem^.YList[0];
if Math.IsNaN(r) then
exit;
case FBubbleRadiusUnits of
bruX:
begin
p := ParentChart.GraphToImage(AxisToGraph(sp));
q := ParentChart.GraphToImage(AxisToGraph(sp + DoublePoint(r, 0))); // offset along x
if IsRotated then ri := q.y - p.y else ri := q.x - p.x;
ARect := Rect(p.x - ri, p.y - ri, p.x + ri, p.y + ri);
end;
bruY:
begin
p := ParentChart.GraphToImage(AxisToGraph(sp));
q := ParentChart.GraphToImage(AxisToGraph(sp + DoublePoint(0, r))); // offset along y
if IsRotated then ri := q.x - p.x else ri := q.y - p.y;
ARect := Rect(p.x - ri, p.y - ri, p.x + ri, p.y + ri);
end;
bruXY:
begin
ARect.TopLeft := ParentChart.GraphToImage(AxisToGraph(DoublePoint(sp.x - r, sp.y - r)));
ARect.BottomRight := ParentChart.GraphToImage(AxisToGraph(DoublePoint(sp.x + r, sp.y + r)));
end;
bruPercentageRadius:
begin
p := ParentChart.GraphToImage(AxisToGraph(sp));
ri := round(r * AFactor);
ARect := Rect(p.x - ri, p.y - ri, p.x + ri, p.y + ri);
end;
bruPercentageArea:
begin
p := ParentChart.GraphToImage(AxisToGraph(sp));
ri := round(sqrt(abs(r)) * AFactor);
ARect := Rect(p.x - ri, p.y - ri, p.x + ri, p.y + ri);
end;
end;
NormalizeRect(ARect);
Result := true;
end;
function TBubbleSeries.GetLabelDataPoint(AIndex, AYIndex: Integer): TDoublePoint;
const
DIRECTION: array [Boolean, Boolean] of TLabelDirection =
((ldTop, ldBottom), (ldRight, ldLeft));
IS_NEGATIVE: array[TLinearMarkPositions] of boolean =
(true, false, true, false);
//lmpOutside, lmpPositive, lmpNegative, lmpInside
var
R: TRect;
RArray: array[0..3] of Integer absolute R;
isneg: Boolean;
dir: TLabelDirection;
begin
if (AYIndex = 1) and GetBubbleRect(Source.Item[AIndex + FLoBound], FBubbleScalingFactor, R) then begin
isNeg := IS_NEGATIVE[MarkPositions];
if Assigned(GetAxisY) then
if (IsRotated and ParentChart.IsRightToLeft) xor GetAxisY.Inverted then
isNeg := not isNeg;
dir := DIRECTION[IsRotated, isNeg];
if IsRotated then
Result := ParentChart.ImageToGraph(Point(RArray[ord(dir)], (R.Top + R.Bottom) div 2))
else
Result := ParentChart.ImageToGraph(Point((R.Left + R.Right) div 2, RArray[ord(dir)]));
end else
Result := GetGraphPoint(AIndex, 0, 0);
end;
procedure TBubbleSeries.GetLegendItems(AItems: TChartLegendItems);
begin
GetLegendItemsRect(AItems, BubbleBrush, BubblePen);
end;
function TBubbleSeries.GetNearestPoint(const AParams: TNearestPointParams;
out AResults: TNearestPointResults): Boolean;
var
i: Integer;
item: PChartDataItem;
iRect: TRect;
p: TPoint;
dperim: Integer; // Distance of perimeter point from center of bubble
d, dist: Integer;
phi: Double;
rx, ry: Integer;
cosphi, sinphi: Math.float;
begin
Result := inherited;
if IsEmpty or not RequestValidChartScaling then exit;
if Result and (nptPoint in AParams.FTargets) and (nptPoint in ToolTargets) then
if (AResults.FYIndex = 0) then
exit;
if Result and (nptYList in AParams.FTargets) and (nptYList in ToolTargets) then
if (AResults.FYIndex = 1) then begin
item := Source[AResults.FIndex];
GetBubbleRect(item, FBubbleScalingFactor, iRect);
rx := (iRect.Right - iRect.Left) div 2;
ry := (iRect.Bottom - iRect.Top) div 2;
p := ParentChart.GraphToImage(AxisToGraph(item^.Point));
phi := arctan2(AParams.FPoint.Y - p.y, AParams.FPoint.X - p.x);
SinCos(phi, sinphi, cosphi);
AResults.FImg := p + Point(round(rx * cosPhi), round(ry * sinPhi));
exit;
end;
if (nptCustom in AParams.FTargets) and (nptCustom in ToolTargets) then begin
dist := MaxInt;
for i := 0 to Count - 1 do begin
item := Source[i];
if not GetBubbleRect(item, FBubbleScalingFactor, irect) then
continue;
rx := (iRect.Right - iRect.Left) div 2;
ry := (iRect.Bottom - iRect.Top) div 2;
p := ParentChart.GraphToImage(AxisToGraph(item^.Point));
phi := -arctan2(AParams.FPoint.Y - p.y, AParams.FPoint.X - p.x);
SinCos(phi, sinphi, cosphi);
dperim := round(sqrt(sqr(rx * cosPhi) + sqr(ry * sinPhi)));
d := round(sqrt(PointDistSq(p, AParams.FPoint)));
if (d < dist) and (d < dperim + AParams.FRadius) then begin // not quite exact...
dist := d;
AResults.FDist := d;
AResults.FIndex := i;
AResults.FYIndex := -1;
AResults.FValue := item^.Point;
AResults.FImg := AParams.FPoint;
if d = 0 then break;
end;
end;
if AResults.FIndex <> -1 then begin
AResults.FDist := sqr(AResults.FDist); // we need sqr for comparison with other series
Result := true;
end;
end;
end;
function TBubbleSeries.GetSeriesColor: TColor;
begin
Result := FBubbleBrush.Color;
end;
class procedure TBubbleSeries.GetXYCountNeeded(out AXCount, AYCount: Cardinal);
begin
AXCount := 1;
AYCount := 2;
end;
procedure TBubbleSeries.MovePointEx(var AIndex: Integer;
AXIndex, AYIndex: Integer; const ANewPos: TDoublePoint);
var
np: TDoublePoint; // ANewPos, in axis units
sp: TDoublePoint; // Orig data point (source point), in axis units
gp: TDoublePoint; // Orig data point, in graph units
ip: TPoint; // original data point, in image units
r: Double; // radius, in axis units
inp: TPoint; // NewPos in image units
rvec: TDoublePoint; // Rotated radius vector
begin
Unused(AXIndex);
ParentChart.DisableRedrawing;
ListSource.BeginUpdate;
try
case AYIndex of
-1,
0: begin
np := GraphToAxis(ANewPos);
ListSource.SetXValue(AIndex, np.X);
ListSource.SetYValue(AIndex, np.Y);
end;
1: begin
sp := ListSource.Item[AIndex]^.Point;
gp := AxisToGraph(sp);
case FBubbleRadiusUnits of
bruX:
begin
inp := ParentChart.GraphToImage(ANewPos);
ip := ParentChart.GraphToImage(gp);
// Distance data pt to ANewPos, in image units
r := sqrt(sqr(ip.X - inp.X) + sqr(ip.Y - inp.Y));
// Vector from bubble center to right bubble perimeter, in axis units
rvec := GraphToAxis(ParentChart.ImageToGraph(Point(ip.x + round(r), ip.y))) - sp;
// Radius of the circle
r := abs(rvec.x);
end;
bruY:
begin
// like bruX, but with y instead of x
inp := ParentChart.GraphToImage(ANewPos);
ip := ParentChart.GraphToImage(gp);
r := sqrt(sqr(ip.X - inp.X) + sqr(ip.Y - inp.Y));
rvec := GraphToAxis(ParentChart.ImageToGraph(Point(ip.x, ip.y + round(r)))) - sp;
r := abs(rvec.y);
end;
bruXY:
begin
// Blubble radius is the distance between data pt and mouse pt, in axis units
np := GraphToAxis(ANewPos);
rvec := np - sp;
r := sqrt(sqr(rvec.x) + sqr(rvec.y));
end;
end;
ListSource.SetYList(AIndex, [r]);
end;
end;
finally
ListSource.EndUpdate;
ParentChart.EnableRedrawing;
UpdateParentChart;
end;
end;
procedure TBubbleSeries.SetBubbleBrush(AValue: TBrush);
begin
if FBubbleBrush = AValue then exit;
FBubbleBrush.Assign(AValue);
UpdateParentChart;
end;
procedure TBubbleSeries.SetBubblePen(AValue: TPen);
begin
if FBubblePen = AValue then exit;
FBubblePen.Assign(AValue);
UpdateParentChart;
end;
procedure TBubbleSeries.SetBubbleRadiusPercentage(AValue: Integer);
begin
if FBubbleRadiusPercentage = AValue then exit;
FBubbleRadiusPercentage := AValue;
UpdateParentChart;
end;
procedure TBubbleSeries.SetBubbleRadiusUnits(AValue: TBubbleRadiusUnits);
begin
if FBubbleRadiusUnits = AValue then exit;
FBubbleRadiusUnits := AValue;
UpdateParentChart;
end;
procedure TBubbleSeries.SetOverrideColor(AValue: TBubbleOverrideColors);
begin
if FOverrideColor = AValue then exit;
FOverrideColor := AValue;
UpdateParentChart;
end;
function TBubbleSeries.ToolTargetDistance(const AParams: TNearestPointParams;
AGraphPt: TDoublePoint; APointIdx, AXIdx, AYIdx: Integer): Integer;
var
item: PChartDataItem;
iRect: TRect;
rx, ry: Integer;
d, dPerim: Integer;
p: TPoint;
phi, sinPhi, cosPhi: Math.float;
begin
if AYIdx = 0 then begin
Result := inherited;
exit;
end;
item := Source[APointIdx];
GetBubbleRect(item, FBubbleScalingFactor, iRect);
rx := (iRect.Right - iRect.Left) div 2;
ry := (iRect.Bottom - iRect.Top) div 2;
p := ParentChart.GraphToImage(AxisToGraph(item^.Point));
d := round(sqrt(PointDistSq(p, AParams.FPoint))); // dist between data pt and clicked pt
phi := -arctan2(AParams.FPoint.Y - p.y, AParams.FPoint.X - p.x);
SinCos(phi, sinphi, cosphi);
dperim := round(sqrt((sqr(rx * cosPhi) + sqr(ry * sinPhi))));
if AYIdx = 1 then
Result := sqr(abs(d - dperim))
else begin
Result := PointDistSq(p, AParams.FPoint);
if sqrt(Result) > dperim then
Result := MaxInt;
end;
end;
procedure TBubbleSeries.UpdateLabelDirectionReferenceLevel(AIndex, AYIndex: Integer;
var ALevel: Double);
begin
Unused(AIndex);
case AYIndex of
0: ALevel := -Infinity;
1: ALevel := +Infinity;
end;
end;
procedure TBubbleSeries.UpdateMargins(ADrawer: IChartDrawer;
var AMargins: TRect);
var
i, dist, j: Integer;
labelText: String;
dir: TLabelDirection;
m: array [TLabelDirection] of Integer absolute AMargins;
gp: TDoublePoint;
scMarksDistance: Integer;
center: Double;
begin
if not Marks.IsMarkLabelsVisible or not Marks.AutoMargins then exit;
if IsEmpty then exit;
if not RequestValidChartScaling then exit;
FindExtentInterval(ParentChart.CurrentExtent, Source.IsSortedByXAsc);
with Extent do
center := AxisToGraphY((a.y + b.y) * 0.5);
UpdateLabelDirectionReferenceLevel(0, 0, center);
scMarksDistance := ADrawer.Scale(Marks.Distance);
for i := FLoBound to FUpBound do begin
for j := 0 to Min(1, Source.YCount-1) do begin
gp := GetLabelDataPoint(i, j);
if not ParentChart.IsPointInViewPort(gp) then break;
labelText := FormattedMark(i, '', j);
if labelText = '' then break;
UpdateLabelDirectionReferenceLevel(i, j, center);
dir := GetLabelDirection(TDoublePointBoolArr(gp)[not IsRotated], center);
with Marks.MeasureLabel(ADrawer, labelText) do
dist := Math.IfThen(dir in [ldLeft, ldRight], cx, cy);
if Marks.DistanceToCenter then
dist := dist div 2;
m[dir] := Max(m[dir], dist + scMarksDistance);
end;
end;
end;
{ TBoxAndWhiskerSeries }
function TBoxAndWhiskerSeries.AddXY(
AX, AYLoWhisker, AYLoBox, AY, AYHiBox, AYHiWhisker: Double; AXLabel: String;
AColor: TColor): Integer;
var
y: Double;
begin
if FYIndexWhiskerMin = 0 then
y := AYLoWhisker
else if FYIndexBoxMin = 0 then
y := AYLoBox
else if FYIndexCenter = 0 then
y := AY
else if FYIndexBoxMax = 0 then
y := AYHiBox
else if FYIndexWhiskerMax = 0 then
y := AYHiWhisker
else
raise Exception.Create('[TBoxAndWhiskerSeries.AddXY] Ordinary y value missing');
Result := ListSource.Add(AX, y, AXLabel, AColor);
with ListSource.Item[Result]^ do begin
SetY(FYIndexWhiskerMin, AYLoWhisker);
SetY(FYIndexBoxMin, AYLoBox);
SetY(FYIndexCenter, AY);
SetY(FYIndexBoxMax, AYHiBox);
SetY(FYIndexWhiskerMax, AYHiWhisker);
end;
end;
procedure TBoxAndWhiskerSeries.Assign(ASource: TPersistent);
begin
if ASource is TBoxAndWhiskerSeries then
with TBoxAndWhiskerSeries(ASource) do begin
Self.BoxBrush.Assign(FBoxBrush);
Self.BoxPen.Assign(FBoxPen);
Self.FBoxWidth := FBoxWidth;
Self.MedianPen.Assign(FMedianPen);
Self.WhiskersPen.Assign(FWhiskersPen);
Self.FWhiskersWidth := FWhiskersWidth;
Self.FYDataLayout := FYDataLayout;
Self.FYIndexWhiskerMin := FYIndexWhiskerMin;
Self.FYIndexBoxMin := FYIndexBoxMin;
Self.FYIndexCenter := FYIndexCenter;
Self.FYIndexBoxMax := FYIndexBoxMax;
Self.FYIndexWhiskerMax := FYIndexWhiskerMax;
end;
inherited Assign(ASource);
end;
constructor TBoxAndWhiskerSeries.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ToolTargets := [nptPoint, nptYList, nptCustom];
FOptimizeX := false;
FBoxBrush := TBrush.Create;
FBoxBrush.OnChange := @StyleChanged;
FBoxPen := TPen.Create;
FBoxPen.OnChange := @StyleChanged;
FBoxWidth := DEF_BOX_WIDTH;
FMedianPen := TPen.Create;
FMedianPen.OnChange := @StyleChanged;
FWhiskersPen := TPen.Create;
FWhiskersPen.OnChange := @StyleChanged;
FWhiskersWidth := DEF_WHISKERS_WIDTH;
FYDataLayout := bwlLegacy;
FYIndexWhiskerMin := DEF_YINDEX_WHISKERMIN;
FYIndexBoxMin := DEF_YINDEX_BOXMIN;
FYIndexCenter := DEF_YINDEX_CENTER;
FYIndexBoxMax := DEF_YINDEX_BOXMAX;
FYIndexWhiskerMax := DEF_YINDEX_WHISKERMAX;
end;
destructor TBoxAndWhiskerSeries.Destroy;
begin
FreeAndNil(FBoxBrush);
FreeAndNil(FBoxPen);
FreeAndNil(FMedianPen);
FreeAndNil(FWhiskersPen);
inherited Destroy;
end;
procedure TBoxAndWhiskerSeries.Draw(ADrawer: IChartDrawer);
procedure GraphToImage_Whisker(X, XW, Y1, Y2: Double;
out iX1, iX, iX2, iY1, iY2: Integer);
begin
if IsRotated then
begin
iX1 := ParentChart.YGraphToImage(X - XW);
iX2 := ParentChart.YGraphToImage(X + XW);
if iX1 <> iX2 then
iX := ParentChart.YGraphToImage(X)
else
iX := iX1;
iY1 := ParentChart.XGraphToImage(Y1);
iY2 := ParentChart.XGraphToImage(Y2);
end else
begin
iX1 := ParentChart.XGraphToImage(X - XW);
iX2 := ParentChart.XGraphToImage(X + XW);
if iX1 <> iX2 then
iX := ParentChart.XGraphToImage(X)
else
iX := iX1;
iY1 := ParentChart.YGraphToImage(Y1);
iY2 := ParentChart.YGraphToImage(Y2);
end;
end;
procedure GraphToImage_Bar(X1, X2, Y1, Y2: Double;
out iX1, iX2, iY1, iY2: Integer);
begin
if IsRotated then
begin
iX1 := ParentChart.YGraphToImage(X1);
iX2 := ParentChart.YGraphToImage(X2);
iY1 := ParentChart.XGraphToImage(Y1);
iY2 := ParentChart.XGraphToImage(Y2);
end else
begin
iX1 := ParentChart.XGraphToImage(X1);
iX2 := ParentChart.XGraphToImage(X2);
iY1 := ParentChart.YGraphToImage(Y1);
iY2 := ParentChart.YGraphToImage(Y2);
end;
end;
procedure GraphToImage_Median(Y: Double; out iY: Integer);
begin
if IsRotated then
iY := ParentChart.XGraphToImage(Y)
else
iY := ParentChart.YGraphToImage(Y);
end;
procedure PrepareLine(i: Integer; APen: TPen);
begin
ADrawer.Pen := APen;
if (Source[i]^.Color <> clTAColor) and (APen.Color = clTAColor) then
ADrawer.SetPenColor(Source[i]^.Color);
ADrawer.SetBrushParams(bsClear, clTAColor);
end;
procedure PrepareBox(i: Integer);
begin
ADrawer.Pen := BoxPen;
if Source[i]^.Color <> clTAColor then
begin
if BoxPen.Color = clTAColor then
ADrawer.SetPenColor(Source[i]^.Color);
ADrawer.SetBrushParams(bsSolid, Source[i]^.Color);
end
else
ADrawer.Brush := BoxBrush;
end;
{ X1 X X2
|----+----| Y
|
+-----+-----+ YBox
| |
| |
}
procedure DrawWhisker(X1, X, X2, Y, YBox: Integer);
begin
if IsRotated then
begin
if X1 <> X2 then
ADrawer.Line(Y, X1, Y, X2);
ADrawer.Line(Y, X, YBox, X);
end else
begin
if X1 <> X2 then
ADrawer.Line(X1, Y, X2, Y);
ADrawer.Line(X, Y, X, YBox);
end;
end;
procedure DrawBox(XBox1, XBox2, YBox1, YBox2: Integer);
begin
if (XBox1 = XBox2) or (YBox1 = YBox2) then
begin
if IsRotated then
ADrawer.Line(YBox1, XBox1, YBox2, XBox2)
else
ADrawer.Line(XBox1, YBox1, XBox2, YBox2);
end else
begin
if IsRotated then
ADrawer.Rectangle(YBox1, XBox1, YBox2, XBox2)
else
ADrawer.Rectangle(XBox1, YBox1, XBox2, YBox2);
end;
end;
procedure DrawMedian(X1, X2, Y: Integer);
begin
if IsRotated then
ADrawer.Line(Y, X1, Y, X2)
else
ADrawer.Line(X1, Y, X2, Y);
end;
function GetY(AItem: PChartDataItem; AYIndex: Integer; out AY: Double): Boolean;
var
y: Double;
begin
Result := false;
if AYIndex = 0 then
y := AItem^.Y
else
y := AItem^.YList[AYIndex-1];
if IsNaN(y) then
exit;
AY := AxisToGraphY(y);
Result := true;
end;
var
ext2: TDoubleRect;
x, ymin, yqmin, ymed, yqmax, ymax, wb, ww, w: Double;
i: Integer;
ix, ixb1, ixb2, ixw1, ixw2: Integer;
iymin, iyqmin, iymed, iyqmax, iymax: Integer;
nx, ny: Cardinal;
item: PChartDataItem;
begin
if IsEmpty or (not Active) then exit;
if FWidthStyle = bwsPercentMin then
UpdateMinXRange;
ext2 := ParentChart.CurrentExtent;
ExpandRange(ext2.a.X, ext2.b.X, 1.0);
ExpandRange(ext2.a.Y, ext2.b.Y, 1.0);
PrepareGraphPoints(ext2, true);
for i := FLoBound to FUpBound do begin
// Get values from source and convert to graph units.
item := Source[i];
x := GetGraphPointX(i);
if IsNaN(x) then continue;
if not GetY(item, FYIndexWhiskerMin, ymin) then continue;
if not GetY(item, FYIndexBoxMin, yqmin) then continue;
if not GetY(item, FYIndexCenter, ymed) then continue;
if not GetY(item, FYIndexBoxMax, yqmax) then continue;
if not GetY(item, FYIndexWhiskerMax, ymax) then continue;
case FWidthStyle of
bwsPercent: w := GetXRange(x, i) * PERCENT / 2;
bwsPercentMin: w := FMinXRange * PERCENT / 2;
end;
wb := w * BoxWidth;
ww := w * WhiskersWidth;
// Calculate image coordiantes
GraphToImage_Whisker(x, ww, ymin, ymax, ixw1, ix, ixw2, iymin, iymax);
GraphToImage_Bar(x-wb, x+wb, yqmin, yqmax, ixb1, ixb2, iyqmin, iyqmax);
GraphToImage_Median(ymed, iymed);
// Draw whisker
PrepareLine(i, WhiskersPen);
DrawWhisker(ixw1, ix, ixw2, iymin, iyqmin);
DrawWhisker(ixw1, ix, ixw2, iymax, iyqmax);
// Draw box
PrepareBox(i);
DrawBox(ixb1, ixb2, iyqmin, iyqmax);
ADrawer.Pen := WhiskersPen;
ADrawer.SetPenColor(WhiskersPen.Color);
// Draw median line
PrepareLine(i, MedianPen);
DrawMedian(ixb1, ixb2, iymed);
end;
GetXYCountNeeded(nx, ny);
if Source.YCount > ny then
for i := 0 to ny-1 do DrawLabels(ADrawer, i)
else
DrawLabels(ADrawer);
end;
function TBoxAndWhiskerSeries.Extent: TDoubleRect;
var
x: Double;
j: Integer;
function ExtraWidth(AIndex: Integer): Double;
begin
Result := GetXRange(x, AIndex) * Max(BoxWidth, WhiskersWidth) * PERCENT / 2;
end;
begin
Result := Source.ExtentList;
if Source.Count = 0 then
exit;
// Show first and last boxes fully.
j := -1;
x := NaN;
while IsNaN(x) and (j < Source.Count-1) do begin
inc(j);
x := GetGraphPointX(j);
end;
Result.a.X := Min(Result.a.X, GraphToAxisX(x - ExtraWidth(j)));
j := Count;
x := NaN;
while IsNaN(x) and (j > 0) do begin
dec(j);
x := GetGraphPointX(j);
end;
Result.b.X := Max(Result.b.X, GraphToAxisX(x + ExtraWidth(j)));
end;
procedure TBoxAndWhiskerSeries.GetLegendItems(AItems: TChartLegendItems);
begin
AItems.Add(TLegendItemBoxAndWhiskers.Create(Self, LegendTextSingle));
end;
function TBoxAndWhiskerSeries.GetNearestPoint(const AParams: TNearestPointParams;
out AResults: TNearestPointResults): Boolean;
var
i, j: Integer;
graphClickPt, pt: TDoublePoint;
x, w, wb: Double;
y: Array[0..4] of Double;
pImg: TPoint;
R: TDoubleRect;
xImg, dist: Integer;
begin
Result := inherited;
if Result then begin
if (nptPoint in AParams.FTargets) and (nptPoint in ToolTargets) then
exit;
if (nptYList in AParams.FTargets) and (nptYList in ToolTargets) then
exit;
end;
if not ((nptCustom in AParams.FTargets) and (nptCustom in ToolTargets))
then
exit;
pImg := AParams.FPoint;
graphClickPt := ParentChart.ImageToGraph(AParams.FPoint);
if IsRotated then begin
Exchange(graphclickpt.X, graphclickpt.Y);
pImg := ParentChart.GraphToImage(graphclickPt);
end;
// Iterate through all points of the series
for i := 0 to Count - 1 do begin
x := GetGraphPointX(i);
for j := 0 to High(y) do
y[j] := GetGraphPointY(i, j);
case FWidthStyle of
bwsPercent : w := GetXRange(x, i) * PERCENT / 2;
bwsPercentMin : w := FMinXRange * PERCENT / 2;
end;
wb := w * BoxWidth;
dist := MaxInt;
// click inside box
R.a := DoublePoint(x - wb, y[FYIndexBoxMin]);
R.b := DoublePoint(x + wb, y[FYIndexBoxMax]);
if InRange(graphClickPt.X, R.a.x, R.b.x) and InRange(graphClickPt.Y, R.a.Y, R.b.Y) then
begin
dist := 0;
AResults.FYIndex := -1;
end;
// click on whisker line
xImg := ParentChart.XGraphToImage(x);
if InRange(graphClickPt.Y, y[FYIndexWhiskerMin], y[FYIndexBoxMin]) or
InRange(graphClickPt.Y, y[FYIndexWhiskerMax], y[FYIndexBoxMax])
then begin
dist := sqr(pImg.X - xImg);
AResults.FYIndex := -1;
end;
// Sufficiently close?
if dist < AResults.FDist then begin
AResults.FDist := dist;
AResults.FIndex := i;
pt := DoublePoint(x, y[2]); // Median
AResults.FValue := pt;
if IsRotated then Exchange(pt.X, pt.Y);
AResults.FImg := ParentChart.GraphToImage(pt);
if dist = 0 then break;
end;
end;
Result := AResults.FIndex > -1;
end;
function TBoxAndWhiskerSeries.GetSeriesColor: TColor;
begin
Result := BoxBrush.Color;
end;
class procedure TBoxAndWhiskerSeries.GetXYCountNeeded(out AXCount, AYCount: Cardinal);
begin
AXCount := 0;
AYCount := 5;
end;
procedure TBoxAndWhiskerSeries.SetBoxBrush(AValue: TBrush);
begin
if FBoxBrush = AValue then exit;
FBoxBrush.Assign(AValue);
UpdateParentChart;
end;
procedure TBoxAndWhiskerSeries.SetBoxPen(AValue: TPen);
begin
if FBoxPen = AValue then exit;
FBoxPen.Assign(AValue);
UpdateParentChart;
end;
procedure TBoxAndWhiskerSeries.SetBoxWidth(AValue: Integer);
begin
if FBoxWidth = AValue then exit;
FBoxWidth := AValue;
UpdateParentChart;
end;
procedure TBoxAndWhiskerSeries.SetLegendDirection(
AValue: TBoxAndWhiskerSeriesLegendDir);
begin
if FLegendDirection = AValue then exit;
FLegendDirection := AValue;
UpdateParentChart;
end;
procedure TBoxAndWhiskerSeries.SetMedianPen(AValue: TPen);
begin
if FMedianPen = AValue then exit;
FMedianPen.Assign(AValue);
UpdateParentChart;
end;
procedure TBoxAndWhiskerSeries.SetWhiskersPen(AValue: TPen);
begin
if FWhiskersPen = AValue then exit;
FWhiskersPen.Assign(AValue);
UpdateParentChart;
end;
procedure TBoxAndWhiskerSeries.SetWhiskersWidth(AValue: Integer);
begin
if FWhiskersWidth = AValue then exit;
FWhiskersWidth := AValue;
UpdateParentChart;
end;
procedure TBoxAndWhiskerSeries.SetYIndexBoxMax(AValue: Integer);
begin
if FYIndexBoxMax = AValue then exit;
FYIndexBoxMax := AValue;
UpdateYDataLayout;
UpdateParentChart;
end;
procedure TBoxAndWhiskerSeries.SetYIndexBoxMin(AValue: Integer);
begin
if FYIndexBoxMin = AValue then exit;
FYIndexBoxMin := AValue;
UpdateYDataLayout;
UpdateParentChart;
end;
procedure TBoxAndWhiskerSeries.SetYIndexCenter(AValue: Integer);
begin
if FYIndexCenter = AValue then exit;
FYIndexCenter := AValue;
UpdateYDataLayout;
UpdateParentChart;
end;
procedure TBoxAndWhiskerSeries.SetYIndexWhiskerMax(AValue: Integer);
begin
if FYIndexWhiskerMax = AValue then exit;
FYIndexWhiskerMax := AValue;
UpdateYDataLayout;
UpdateParentChart;
end;
procedure TBoxAndWhiskerSeries.SetYIndexWhiskerMin(AValue: Integer);
begin
if FYIndexWhiskerMin = AValue then exit;
FYIndexWhiskerMin := AValue;
UpdateYDataLayout;
UpdateParentChart;
end;
procedure TBoxAndWhiskerSeries.SetYDataLayout(AValue: TBoxAndWhiskerYDataLayout);
procedure SetYIndices(AWhiskerMin, ABoxMin, ACenter, ABoxMax, AWhiskerMax: Integer);
begin
FYIndexWhiskerMin := AWhiskerMin;
FYIndexBoxMin := ABoxMin;
FYIndexCenter := ACenter;
FYIndexBoxMax := ABoxMax;
FYIndexWhiskerMax := AWhiskerMax;
end;
begin
if FYDataLayout = AValue then exit;
FYDataLayout := AValue;
case FYDataLayout of
bwlNormal: SetYIndices(1, 2, 0, 3, 4);
bwlLegacy: SetYIndices(0, 1, 2, 3, 4);
bwlCustom: ;
end;
UpdateParentChart;
end;
function TBoxAndWhiskerSeries.SkipMissingValues(AIndex: Integer): Boolean;
begin
Result := IsNaN(Source[AIndex]^.Point);
if not Result then
Result := HasMissingYValue(AIndex, 5);
end;
function TBoxAndWhiskerSeries.ToolTargetDistance(
const AParams: TNearestPointParams; AGraphPt: TDoublePoint;
APointIdx, AXIdx, AYIdx: Integer): Integer;
// All in image coordinates transformed to have a horizontal x axis
function DistanceToLine(Pt: TPoint; x1, x2, y: Integer): Integer;
begin
if InRange(Pt.X, x1, x2) then
Result := sqr(Pt.Y - y) // FDistFunc does not calc sqrt
else
Result := Min(
AParams.FDistFunc(Pt, Point(x1, y)),
AParams.FDistFunc(Pt, Point(x2, y))
);
end;
var
xw1, xw2, xb1, xb2, y: Integer;
w, wb, ww: Double;
clickPt: TPoint;
gp: TDoublePoint;
begin
Unused(AXIdx);
if IsRotated then begin
gp := ParentChart.ImageToGraph(AParams.FPoint);
Exchange(gp.X, gp.Y);
clickPt := ParentChart.GraphToImage(gp);
Exchange(AGraphPt.X, AGraphPt.Y);
end else
clickPt := AParams.FPoint;
case FWidthStyle of
bwsPercent : w := GetXRange(AGraphPt.X, APointIdx) * PERCENT / 2;
bwsPercentMin : w := FMinXRange * PERCENT / 2;
end;
wb := w * BoxWidth;
ww := w * WhiskersWidth;
xw1 := ParentChart.XGraphToImage(AGraphPt.X - ww);
xw2 := ParentChart.XGraphToImage(AGraphPt.X + ww);
xb1 := ParentChart.XGraphToImage(AGraphPt.X - wb);
xb2 := ParentChart.XGraphToImage(AGraphPt.X + wb);
y := ParentChart.YGraphToImage(AGraphPt.Y);
if AYIdx in [FYIndexWhiskerMax, FYIndexWhiskerMin] then
Result := DistanceToLine(clickPt, xw1, xw2, y)
else if AYIdx in [FYIndexBoxMax, FYIndexCenter, FYIndexBoxMin] then
Result := DistanceToLine(clickPt, xb1, xb2, y)
else
raise Exception.Create('[TBoxAndWhiskerSeries.ToolTargetDistance] Unknown y index.');
end;
procedure TBoxAndWhiskerSeries.UpdateLabelDirectionReferenceLevel(
AIndex, AYIndex: Integer; var ALevel: Double);
var
item: PChartDataItem;
y0, y3: Double;
begin
{ ToDo: The version before introducing FYIndex* variables had used the values
0 and 3 here. 3 means: FYIndexBoxMax. Interpreted this as a typo, but there
is some chance that it would be correct --> needs to be checked. }
if (AYIndex = FYIndexWhiskerMin) then
ALevel := +Infinity
else if (AYIndex = FYIndexWhiskerMax) then
ALevel := -Infinity
else
begin
item := Source.Item[AIndex];
y0 := AxisToGraphY(item^.GetY(FYIndexWhiskerMin));
y3 := AxisToGraphY(item^.GetY(FYIndexWhiskerMax));
ALevel := (y0 + y3) * 0.5;
end;
end;
procedure TBoxAndWhiskerSeries.UpdateYDataLayout;
begin
if (FYIndexWhiskerMin = 0) and (FYIndexBoxMin = 1) and (FYIndexCenter = 2) and
(FYIndexBoxMax = 3) and (FYIndexWhiskerMax = 4)
then
FYDataLayout := bwlLegacy
else
if (FYIndexCenter = 0) and (FYIndexWhiskerMin = 1) and (FYIndexBoxMin = 2) and
(FYIndexBoxMax = 3) and (FYIndexWhiskerMax = 4)
then
FYDataLayout := bwlNormal
else
FYDataLayout := bwlCustom;
end;
{ TOHLCBrush }
function TOHLCBrush.IsColorStored: Boolean;
begin
Result := (Color = DEFAULT_COLORS[FBrushKind]);
end;
procedure TOHLCBrush.SetBrushKind(AValue: TOHLCBrushKind);
begin
FBrushKind := AValue;
Color := DEFAULT_COLORS[FBrushKind];
end;
{ TOHLCPen }
function TOHLCPen.IsColorStored: Boolean;
begin
Result := (Color = DEFAULT_COLORS[FPenKind]);
end;
procedure TOHLCPen.SetPenKind(AValue: TOHLCPenKind);
begin
FPenKind := AValue;
Color := DEFAULT_COLORS[FPenKind];
end;
{ TOpenHighLowCloseSeries }
function TOpenHighLowCloseSeries.AddXOHLC(
AX, AOpen, AHigh, ALow, AClose: Double;
ALabel: String; AColor: TColor): Integer;
var
y: Double;
begin
if YIndexOpen = 0 then
y := AOpen
else if YIndexHigh = 0 then
y := AHigh
else if YIndexLow = 0 then
y := ALow
else if YIndexClose = 0 then
y := AClose
else
raise Exception.Create('TOpenHighLowCloseSeries: Ordinary y value missing');
Result := ListSource.Add(AX, y, ALabel, AColor);
with ListSource.Item[Result]^ do begin
SetY(YIndexOpen, AOpen);
SetY(YIndexHigh, AHigh);
SetY(YIndexLow, ALow);
SetY(YIndexClose, AClose);
end;
end;
procedure TOpenHighLowCloseSeries.Assign(ASource: TPersistent);
var
bk: TOHLCBrushKind;
pk: TOHLCPenKind;
begin
if ASource is TOpenHighLowCloseSeries then
with TOpenHighLowCloseSeries(ASource) do begin
for bk in TOHLCBrushKind do
Self.FBrush[bk] := FBrush[bk];
for pk in TOHLCPenKind do
Self.FPen[pk] := FPen[pk];
Self.FMode := FMode;
Self.FTickWidth := FTickWidth;
Self.FYIndexClose := FYIndexClose;
Self.FYIndexHigh := FYIndexHigh;
Self.FYIndexLow := FYIndexLow;
Self.FYIndexOpen := FYIndexOpen;
end;
inherited Assign(ASource);
end;
constructor TOpenHighLowCloseSeries.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ToolTargets := [nptPoint, nptYList, nptCustom];
FOptimizeX := false;
FStacked := false;
FTickWidth := DEF_OHLC_TICK_WIDTH;
FYIndexClose := DEF_YINDEX_CLOSE;
FYIndexHigh := DEF_YINDEX_HIGH;
FYIndexLow := DEF_YINDEX_LOW;
FYIndexOpen := DEF_YINDEX_OPEN;
// Candlestick up brush
FBrush[obkCandleUp] := TOHLCBrush.Create;
FBrush[obkCandleUp].BrushKind := obkCandleUp;
FBrush[obkCandleUp].OnChange := @StyleChanged;
// Candlestick down brush
FBrush[obkCandleDown] := TOHLCBrush.Create;
FBrush[obkCandleDown].BrushKind := obkCandleDown;
FBrush[obkCandleDown].OnChange := @StyleChanged;
// Candlestick up border pen
FPen[opkCandleUp] := TOHLCPen.Create;
FPen[opkCandleUp].PenKind := opkCandleUp;
FPen[opkCandleUp].OnChange := @StyleChanged;
// Candlestick down border pen
FPen[opkCandleDown] := TOHLCPen.Create;
FPen[opkCandleDown].PenKind := opkCandleDown;
FPen[opkCandleDown].OnChange := @StyleChanged;
// Candlestick range pen
FPen[opkCandleLine] := TOHLCPen.Create;
FPen[opkCandleLine].PenKind := opkCandleLine;
FPen[opkCandleLine].OnChange := @StyleChanged;
// OHLC up pen
FPen[opkLineUp] := TOHLCPen.Create;
FPen[opkLineUp].PenKind := opkLineUp;
FPen[opkLineUp].OnChange := @StyleChanged;
// OHLC down pen
FPen[opkLineDown] := TOHLCPen.Create;
FPen[opkLineDown].PenKind := opkLineDown;
FPen[opkLineDown].OnChange := @StyleChanged;
end;
destructor TOpenHighLowCloseSeries.Destroy;
var
bk: TOHLCBrushKind;
pk: TOHLCPenKind;
begin
for bk in TOHLCBrushKind do
FreeAndNil(FBrush[bk]);
for pk in TOHLCPenKind do
FreeAndNil(FPen[pk]);
inherited;
end;
function TOpenHighLowCloseSeries.CalcTickWidth(AX: Double; AIndex: Integer): Double;
begin
case FTickWidthStyle of
twsPercent:
Result := GetXRange(AX, AIndex) * PERCENT * TickWidth;
twsPercentMin:
begin
if FMinXRange = 0 then
UpdateMinXRange;
Result := FMinXRange * PERCENT * TickWidth;
end;
end;
end;
procedure TOpenHighLowCloseSeries.Draw(ADrawer: IChartDrawer);
function MaybeRotate(AX, AY: Double): TPoint;
begin
if IsRotated then
Exchange(AX, AY);
Result := ParentChart.GraphToImage(DoublePoint(AX, AY));
end;
procedure DoLine(AX1, AY1, AX2, AY2: Double);
begin
ADrawer.Line(MaybeRotate(AX1, AY1), MaybeRotate(AX2, AY2));
end;
procedure NoZeroRect(var R: TRect);
begin
if IsRotated then
begin
if R.Left = R.Right then inc(R.Right);
end else
begin
if R.Top = R.Bottom then inc(R.Bottom);
end;
end;
procedure DoRect(AX1, AY1, AX2, AY2: Double);
var
r: TRect;
begin
r.TopLeft := MaybeRotate(AX1, AY1);
r.BottomRight := MaybeRotate(AX2, AY2);
NoZeroRect(r);
ADrawer.FillRect(r.Left, r.Top, r.Right, r.Bottom);
ADrawer.Rectangle(r);
end;
procedure DrawOHLC(x, yopen, yhigh, ylow, yclose, tw: Double);
begin
DoLine(x, yhigh, x, ylow);
DoLine(x, yclose, x + tw, yclose);
if not IsNaN(yopen) then
DoLine(x - tw, yopen, x, yopen);
end;
procedure DrawCandleStick(x, yopen, yhigh, ylow, yclose, tw: Double; APenIdx: Integer);
begin
if CandleStickLinePen.Color = clDefault then
// use linepen and linedown pen for range line
ADrawer.Pen := FPen[TOHLCPenKind(APenIdx + 3)]
else
ADrawer.Pen := CandleStickLinePen;
DoLine(x, yhigh, x, ylow);
ADrawer.Pen := FPen[TOHLCPenKind(APenIdx)];
DoRect(x - tw, yopen, x + tw, yclose);
end;
const
UP_INDEX = 0;
DOWN_INDEX = 1;
var
my: Cardinal;
ext2: TDoubleRect;
i: Integer;
x, tw, yopen, yhigh, ylow, yclose, prevclose: Double;
idx: Integer;
nx, ny: Cardinal;
begin
if IsEmpty or (not Active) then exit;
my := MaxIntValue([YIndexOpen, YIndexHigh, YIndexLow, YIndexClose]);
if my >= Source.YCount then exit;
ext2 := ParentChart.CurrentExtent;
ExpandRange(ext2.a.X, ext2.b.X, 1.0);
ExpandRange(ext2.a.Y, ext2.b.Y, 1.0);
PrepareGraphPoints(ext2, true);
prevclose := -Infinity;
for i := FLoBound to FUpBound do begin
x := GetGraphPointX(i);
if IsNaN(x) then Continue;
yopen := GetGraphPointY(i, YIndexOpen);
if IsNaN(yopen) and (FMode = mCandleStick) then Continue;
yhigh := GetGraphPointY(i, YIndexHigh);
if IsNaN(yhigh) then Continue;
ylow := GetGraphPointY(i, YIndexLow);
if IsNaN(ylow) then Continue;
yclose := GetGraphPointY(i, YIndexClose);
if IsNaN(yclose) then Continue;
tw := CalcTickWidth(x, i);
if IsNaN(yopen) then
begin
// HLC chart: compare with close value of previous data point
if prevclose < yclose then
idx := UP_INDEX
else
idx := DOWN_INDEX;
end else
if (yopen <= yclose) then
idx := UP_INDEX
else
idx := DOWN_INDEX;
ADrawer.Brush := FBrush[TOHLCBrushKind(idx)];
case FMode of
mOHLC: ADrawer.Pen := FPen[TOHLCPenKind(idx + 3)];
mCandlestick: ADrawer.Pen := FPen[TOHLCPenKind(idx)];
end;
if Source[i]^.Color <> clTAColor then
begin
ADrawer.SetPenParams(FPen[TOHLCPenKind(idx)].Style, Source[i]^.Color, FPen[TOHLCPenKind(idx)].Width);
ADrawer.SetBrushParams(FBrush[TOHLCBrushKind(idx)].Style, Source[i]^.Color);
end;
case FMode of
mOHLC: DrawOHLC(x, yopen, yhigh, ylow, yclose, tw);
mCandleStick: DrawCandleStick(x, yopen, yhigh, ylow, yclose, tw, idx);
end;
prevclose := yclose;
end;
GetXYCountNeeded(nx, ny);
if Source.YCount > ny then
for i := 0 to ny-1 do DrawLabels(ADrawer, i)
else
DrawLabels(ADrawer);
end;
function TOpenHighLowCloseSeries.Extent: TDoubleRect;
var
x: Double;
tw: Double;
j: Integer;
begin
Result := Source.ExtentList; // axis units
// Enforce recalculation of tick/candlebox width
FMinXRange := 0;
// Show first and last open/close ticks and candle boxes fully.
j := -1;
x := NaN;
while IsNaN(x) and (j < Source.Count-1) do begin
inc(j);
x := GetGraphPointX(j); // graph units
end;
tw := CalcTickWidth(x, j);
Result.a.X := Min(Result.a.X, GraphToAxisX(x - tw)); // axis units
// Result.a.X := Min(Result.a.X, x - tw);
j := Count;
x := NaN;
While IsNaN(x) and (j > 0) do begin
dec(j);
x := GetGraphPointX(j);
end;
tw := CalcTickWidth(x, j);
Result.b.X := Max(Result.b.X, AxisToGraphX(x + tw));
// Result.b.X := Max(Result.b.X, x + tw);
end;
function TOpenHighLowCloseSeries.GetBrush(AIndex: TOHLCBrushKind): TOHLCBrush;
begin
Result := FBrush[AIndex];
end;
procedure TOpenHighLowCloseSeries.GetLegendItems(AItems: TChartLegendItems);
begin
AItems.Add(TLegendItemOHLCLine.Create(Self, LegendTextSingle));
end;
function TOpenHighLowCloseSeries.GetNearestPoint(const AParams: TNearestPointParams;
out AResults: TNearestPointResults): Boolean;
var
i: Integer;
graphClickPt, p: TDoublePoint;
pImg: TPoint;
x, yopen, yhigh, ylow, yclose, tw: Double;
xImg, dist: Integer;
R: TDoubleRect;
begin
Result := inherited;
if Result then begin
if (nptPoint in AParams.FTargets) and (nptPoint in ToolTargets) then
exit;
if (nptYList in AParams.FTargets) and (nptYList in ToolTargets) then
exit;
end;
if not ((nptCustom in AParams.FTargets) and (nptCustom in ToolTargets))
then
exit;
graphClickPt := ParentChart.ImageToGraph(AParams.FPoint);
pImg := AParams.FPoint;
if IsRotated then begin
// Exchange(pImg.X, pImg.Y);
Exchange(graphclickpt.X, graphclickpt.Y);
pImg := ParentChart.GraphToImage(graphClickPt);
end;
// Iterate through all points of the series
for i := 0 to Count - 1 do begin
x := GetGraphPointX(i);
yopen := GetGraphPointY(i, YIndexOpen);
yhigh := GetGraphPointY(i, YIndexHigh);
ylow := GetGraphPointY(i, YIndexLow);
yclose := GetGraphPointY(i, YIndexClose);
tw := CalcTickWidth(x, i);
dist := MaxInt;
// click on vertical line
if InRange(graphClickPt.Y, ylow, yhigh) then begin
xImg := ParentChart.XGraphToImage(x);
dist := sqr(pImg.X - xImg);
AResults.FYIndex := -1;
end;
// click on candle box
if FMode = mCandlestick then begin
R.a := DoublePoint(x - tw, Min(yopen, yclose));
R.b := DoublePoint(x + tw, Max(yopen, yclose));
if InRange(graphClickPt.X, R.a.x, R.b.x) and InRange(graphClickPt.Y, R.a.Y, R.b.Y) then
begin
dist := 0;
AResults.FYIndex := -1;
end;
end;
// Sufficiently close?
if dist < AResults.FDist then begin
AResults.FDist := dist;
AResults.FIndex := i;
p := DoublePoint(x, yclose); // "Close" value
AResults.FValue := p;
if IsRotated then Exchange(p.X, p.Y);
AResults.FImg := ParentChart.GraphToImage(p);
if dist = 0 then break;
end;
end;
Result := AResults.FIndex > -1;
end;
function TOpenHighLowCloseSeries.GetPen(AIndex: TOHLCPenKind): TOHLCPen;
begin
Result := FPen[AIndex];
end;
function TOpenHighLowCloseSeries.GetSeriesColor: TColor;
begin
Result := LinePen.Color;
end;
class procedure TOpenHighLowCloseSeries.GetXYCountNeeded(out AXCount, AYCount: Cardinal);
begin
AXCount := 0;
AYCount := 4;
end;
procedure TOpenHighLowCloseSeries.SetBrush(AIndex: TOHLCBrushKind; AValue: TOHLCBrush);
begin
if GetBrush(AIndex) = AValue then exit;
FBrush[AIndex].Assign(AValue);
UpdateParentChart;
end;
procedure TOpenHighLowCloseSeries.SetPen(AIndex: TOHLCPenKind; AValue: TOHLCPen);
begin
if GetPen(AIndex) = AValue then exit;
FPen[AIndex].Assign(AValue);
UpdateParentChart;
end;
procedure TOpenHighLowCloseSeries.SetOHLCMode(AValue: TOHLCMode);
begin
if FMode = AValue then exit;
FMode := AValue;
UpdateParentChart;
end;
procedure TOpenHighLowCloseSeries.SetTickWidth(AValue: Integer);
begin
if FTickWidth = AValue then exit;
FTickWidth := AValue;
UpdateParentChart;
end;
procedure TOpenHighLowCloseSeries.SetTickWidthStyle(AValue: TTickWidthStyle);
begin
if FTickWidthStyle = AValue then exit;
FTickWidthStyle := AValue;
UpdateParentChart;
end;
procedure TOpenHighLowCloseSeries.SetYIndexClose(AValue: Integer);
begin
if FYIndexClose = AValue then exit;
FYIndexClose := AValue;
UpdateParentChart;
end;
procedure TOpenHighLowCloseSeries.SetYIndexHigh(AValue: Integer);
begin
if FYIndexHigh = AValue then exit;
FYIndexHigh := AValue;
UpdateParentChart;
end;
procedure TOpenHighLowCloseSeries.SetYIndexLow(AValue: Integer);
begin
if FYIndexLow = AValue then exit;
FYIndexLow := AValue;
UpdateParentChart;
end;
procedure TOpenHighLowCloseSeries.SetYIndexOpen(AValue: Integer);
begin
if FYIndexOpen = AValue then exit;
FYIndexOpen := AValue;
UpdateParentChart;
end;
function TOpenHighLowCloseSeries.SkipMissingValues(AIndex: Integer): Boolean;
begin
Result := IsNaN(Source[AIndex]^.Point);
if not Result then
Result := HasMissingYValue(AIndex, 4);
end;
function TOpenHighLowCloseSeries.ToolTargetDistance(
const AParams: TNearestPointParams; AGraphPt: TDoublePoint;
APointIdx, AXIdx, AYIdx: Integer): Integer;
// All in image coordinates transformed to have a horizontal x axis
function DistanceToLine(Pt: TPoint; x1, x2, y: Integer): Integer;
begin
if InRange(Pt.X, x1, x2) then // FDistFunc does not calculate sqrt
Result := sqr(Pt.Y - y)
else
Result := Min(
AParams.FDistFunc(Pt, Point(x1, y)),
AParams.FDistFunc(Pt, Point(x2, y))
);
end;
var
x1, x2: Integer;
w: Double;
p, clickPt: TPoint;
gp: TDoublePoint;
begin
Unused(AXIdx);
// Convert the "clicked" and "test" point to non-rotated axes
if IsRotated then begin
gp := ParentChart.ImageToGraph(AParams.FPoint);
Exchange(gp.X, gp.Y);
clickPt := ParentChart.GraphToImage(gp);
Exchange(AGraphPt.X, AGraphPt.Y);
end else
clickPt := AParams.FPoint;
w := CalcTickWidth(AGraphPt.X, APointIdx);
x1 := ParentChart.XGraphToImage(AGraphPt.X - w);
x2 := ParentChart.XGraphToImage(AGraphPt.X + w);
p := ParentChart.GraphToImage(AGraphPt);
case FMode of
mOHLC:
with ParentChart do
if (AYIdx = YIndexOpen) then
Result := DistanceToLine(clickPt, x1, p.x, p.y)
else if (AYIdx = YIndexClose) then
Result := DistanceToLine(clickPt, p.x, x2, p.y)
else if (AYIdx = YIndexHigh) or (AYIdx = YIndexLow) then
Result := AParams.FDistFunc(clickPt, p)
else
raise Exception.Create('TOpenHighLowCloseSeries.ToolTargetDistance: Illegal YIndex.');
mCandleStick:
with ParentChart do
if (AYIdx = YIndexOpen) or (AYIdx = YIndexClose) then
Result := DistanceToLine(clickPt, x1, x2, p.y)
else if (AYIdx = YIndexHigh) or (AYIdx = YIndexLow) then
Result := AParams.FDistFunc(clickPt, p)
else
raise Exception.Create('TOpenHighLowCloseSeries.ToolTargetDistance: Illegal YIndex.');
end;
end;
procedure TOpenHighLowCloseSeries.UpdateLabelDirectionReferenceLevel(
AIndex, AYIndex: Integer; var ALevel: Double);
var
item: PChartDataItem;
begin
if AYIndex = FYIndexLow then
ALevel := +Infinity
else if AYIndex = FYIndexHigh then
ALevel := -Infinity
else begin
item := Source.Item[AIndex];
ALevel := (AxisToGraphY(item^.GetY(FYIndexLow)) + AxisToGraphY(item^.GetY(FYIndexHigh)))*0.5;
end;
end;
{ TFieldSeries }
constructor TFieldSeries.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ToolTargets := [nptPoint, nptXList, nptYList, nptCustom];
FArrow := TChartArrow.Create(ParentChart);
FArrow.Length := 20;
FArrow.Width := 10;
FArrow.Visible := true;
FPen := TPen.Create;
FPen.OnChange := @StyleChanged;
end;
destructor TFieldSeries.Destroy;
begin
FreeAndNil(FArrow);
FreeAndNil(FPen);
inherited;
end;
function TFieldSeries.AddVector(AX, AY, AVectorX, AVectorY: Double;
AXLabel: String = ''; AColor: TColor = clTAColor): Integer;
begin
Result := AddXY(AX, AY, AXLabel, AColor);
SetVector(Result, DoublePoint(AVectorX, AVectorY));
end;
procedure TFieldSeries.AfterAdd;
begin
inherited;
FArrow.SetOwner(ParentChart);
end;
procedure TFieldSeries.Assign(ASource: TPersistent);
begin
if ASource is TFieldSeries then
with TFieldSeries(ASource) do begin
Self.FArrow.Assign(FArrow);
Self.FPen := FPen;
end;
inherited Assign(ASource);
end;
procedure TFieldSeries.Draw(ADrawer: IChartDrawer);
var
ext: TDoubleRect;
i: Integer;
p1, p2: TDoublePoint;
lPen: TPen;
begin
if IsEmpty or (not Active) then exit;
with Extent do begin
ext.a := AxisToGraph(a);
ext.b := AxisToGraph(b);
end;
NormalizeRect(ext);
// Do not draw anything if the series extent does not intersect CurrentExtent.
if not RectIntersectsRect(ext, ParentChart.CurrentExtent) then exit;
lPen := TPen.Create;
try
lPen.Assign(FPen);
if (AxisIndexX < 0) and (AxisIndexY < 0) then begin
// Optimization: bypass transformations in the default case
for i := 0 to Count - 1 do
if GetVectorPoints(i, p1, p2) then begin
lPen.Color := GetColor(i);
DrawVector(ADrawer, p1, p2, lPen);
end;
end else begin
for i := 0 to Count - 1 do
if GetVectorPoints(i, p1, p2) then begin
p1 := AxisToGraph(p1);
p2 := AxisToGraph(p2);
lPen.Color := GetColor(i);
DrawVector(ADrawer, p1, p2, lPen);
end;
end;
DrawLabels(ADrawer, 0);
finally
lPen.Free;
end;
end;
procedure TFieldSeries.DrawVector(ADrawer: IChartDrawer;
AStartPt, AEndPt: TDoublePoint; APen: TPen);
var
p1, p2: TPoint;
arr: TChartArrow;
len: Double;
begin
p1 := ParentChart.GraphToImage(AStartPt);
p2 := ParentChart.GraphToImage(AEndPt);
ADrawer.Pen := APen;
if APen.Color = clDefault then
ADrawer.SetPenColor(FChart.GetDefaultColor(dctFont))
else
ADrawer.SetPenColor(APen.Color);
ADrawer.Line(p1.x, p1.y, p2.x, p2.y);
if FArrow.Visible then begin
len := sqrt(sqr(p2.x - p1.x) + sqr(p2.y - p1.y)) * 0.01 / ADrawer.Scale(1);
// Be aware that the drawer scales pixels. But the arrow length here is
// already at the correct size!
arr := TChartArrow.Create(nil);
arr.Assign(FArrow);
arr.SetOwner(nil); // avoid repainting due to next commands
arr.BaseLength := round(FArrow.BaseLength * len);
arr.Length := round(FArrow.Length * len);
arr.Width := round(FArrow.Width * len);
arr.Draw(ADrawer, p2, arctan2(p2.y-p1.y, p2.x-p1.x), APen);
arr.Free;
end;
end;
function TFieldSeries.Extent: TDoubleRect;
var
p1, p2: TDoublePoint;
i: Integer;
begin
Result := Source.Extent;
for i := 0 to Source.Count - 1 do
if GetVectorPoints(i, p1, p2) then begin
UpdateMinMax(p1.X, Result.a.X, Result.b.X);
UpdateMinMax(p2.X, Result.a.X, Result.b.X);
UpdateMinMax(p1.Y, Result.a.Y, Result.b.Y);
UpdateMinMax(p2.Y, Result.a.Y, Result.b.Y);
end;
end;
function TFieldSeries.GetColor(AIndex: Integer): TColor;
begin
with Source.Item[AIndex]^ do
Result := TColor(Math.IfThen(Color = clTAColor, FPen.Color, Color));
end;
procedure TFieldSeries.GetLegendItems(AItems: TChartLegendItems);
begin
AItems.Add(TLegendItemField.Create(FPen, FArrow, LegendTextSingle));
end;
function TFieldSeries.GetNearestPoint(const AParams: TNearestPointParams;
out AResults: TNearestPointResults): Boolean;
var
dist, d, i, xidx, yidx: Integer;
pt1, pt2: TPoint;
sp1, sp2: TDoublePoint;
R: TRect;
img: TPoint;
begin
AResults.FDist := Sqr(AParams.FRadius) + 1;
AResults.FIndex := -1;
AResults.FXIndex := 0;
AResults.FYIndex := 0;
if IsEmpty then exit(false);
for i := 0 to Count - 1 do begin
if not GetVectorPoints(i, sp1, sp2) then
Continue;
// End points of the vector arrow
pt1 := ParentChart.GraphToImage(AxisToGraph(sp1));
pt2 := ParentChart.GraphToImage(AxisToGraph(sp2));
// At first we check if the point is in the rect spanned by the vector.
R := Rect(pt1.x, pt1.y, pt2.x, pt2.y);
NormalizeRect(R);
R.TopLeft := R.TopLeft - Point(AParams.FRadius, AParams.FRadius);
R.BottomRight := R.BottomRight + Point(AParams.FRadius, AParams.FRadius);
if not IsPointInRect(AParams.FPoint, R) then continue;
dist := MaxInt;
xidx := -1;
yidx := -1;
if (nptPoint in AParams.FTargets) and (nptPoint in ToolTargets) then begin
dist := AParams.FDistFunc(AParams.FPoint, pt1);
xidx := 0;
yidx := 0;
img := pt1;
end;
if (AParams.FTargets * [nptXList, nptYList] <> []) and
(ToolTargets * [nptXList, nptYList] <> [])
then begin
d := AParams.FDistFunc(AParams.FPoint, pt2);
if d < dist then begin
dist := d;
xidx := 1;
yidx := 1;
img := pt2;
end;
end;
// give priority to end points
if (dist > AResults.FDist) and
(nptCustom in AParams.FTargets) and
(nptCustom in ToolTargets)
then begin
d := PointLineDistSq(AParams.FPoint, pt1, pt2); // distance of point from line
if d < dist then begin
dist := d;
xidx := -1;
yidx := -1;
img := ProjToLine(AParams.FPoint, pt1, pt2);
end;
end;
if dist >= AResults.FDist then continue;
AResults.FDist := dist;
AResults.FIndex := i;
AResults.FXIndex := xidx;
AResults.FYIndex := yidx;
AResults.FImg := img;
AResults.FValue := Source[i]^.Point;
break;
end;
Result := AResults.FIndex >= 0;
end;
function TFieldSeries.GetVector(AIndex: Integer): TDoublePoint;
begin
with Source.Item[AIndex]^ do
case FCoordKind of
vckCenterDir: Result := DoublePoint(XList[0], YList[0]);
vckStartEnd: Result := DoublePoint(XList[0]-X, YList[0]-Y);
end;
end;
function TFieldSeries.GetVectorPoints(AIndex: Integer;
out AStartPt, AEndPt: TDoublePoint): Boolean;
var
dx, dy: Double;
begin
with Source.Item[AIndex]^ do begin
if isNaN(X) or IsNaN(Y) or IsNaN(XList[0]) or IsNaN(YList[0]) then
exit(false)
else begin
case FCoordKind of
vckCenterDir:
begin
dx := XList[0] * 0.5;
dy := YList[0] * 0.5;
AStartPt := DoublePoint(X - dx, Y - dy);
AEndPt := DoublePoint(X + dx, Y + dy);
end;
vckStartEnd:
begin
AStartPt := DoublePoint(X, Y);
AEndPt := DoublePoint(XList[0], YList[0]);
end;
end;
Result := true;
end;
end;
end;
class procedure TFieldSeries.GetXYCountNeeded(out AXCount, AYCount: Cardinal);
begin
AXCount := 2;
AYCount := 2;
end;
procedure TFieldSeries.MovePointEx(var AIndex: Integer;
AXIndex, AYIndex: Integer; const ANewPos: TDoublePoint);
var
np, p: TDoublePoint;
begin
Unused(AXIndex);
if not InRange(AIndex, 0, Count - 1) then
exit;
p := DoublePoint(XValue[AIndex], YValue[AIndex]);
np := GraphToAxis(ANewPos);
ParentChart.DisableRedrawing;
try
case AYIndex of
-1: begin
ListSource.SetXValue(AIndex, np.X);
ListSource.SetYValue(AIndex, np.Y);
end;
0: SetVector(AIndex, (p - np) * 2);
1: SetVector(AIndex, (np - p) * 2);
end;
finally
ParentChart.EnableRedrawing;
UpdateParentChart;
end;
end;
procedure TFieldSeries.NormalizeVectors(ALength: Double);
var
factor, maxlen, len: Double;
i: Integer;
v: TDoublePoint;
begin
maxLen := 0;
for i := 0 to Count - 1 do begin
v := GetVector(i);
len := v.x * v.x + v.y * v.y;
len := sqrt(v.x*v.x + v.y*v.y);
// len := sqrt(sqr(v.x) + sqr(v.y));
maxLen := Max(len, maxlen);
end;
if maxLen = 0 then
exit;
factor := ALength / maxLen;
for i := 0 to Count - 1 do begin
v := GetVector(i);
SetVector(i, v*factor);
end;
end;
procedure TFieldSeries.SetArrow(AValue: TChartArrow);
begin
FArrow.Assign(AValue);
UpdateParentChart;
end;
procedure TFieldSeries.SetCoordKind(AValue: TVectorCoordkind);
begin
if AValue <> FCoordKind then
begin
FCoordKind := AValue;
UpdateParentChart;
end;
end;
procedure TFieldSeries.SetPen(AValue: TPen);
begin
FPen.Assign(AValue);
end;
procedure TFieldSeries.SetVector(AIndex: Integer; const AValue: TDoublePoint);
begin
with ListSource.Item[AIndex]^ do begin
case FCoordKind of
vckCenterDir:
begin
XList[0] := AValue.X;
YList[0] := AValue.Y;
end;
vckStartEnd:
begin
XList[0] := X + AValue.X;
YList[0] := Y + AValue.Y;
end;
end;
end;
end;
initialization
RegisterSeriesClass(TBubbleSeries, @rsBubbleSeries);
RegisterSeriesClass(TBoxAndWhiskerSeries, @rsBoxAndWhiskerSeries);
RegisterSeriesClass(TOpenHighLowCloseSeries, @rsOpenHighLowCloseSeries);
RegisterSeriesClass(TFieldSeries, @rsFieldSeries);
end.