lazarus/components/tachart/tatransformations.pas

828 lines
21 KiB
ObjectPascal

{
Axis transformations.
*****************************************************************************
See the file COPYING.modifiedLGPL.txt, included in this distribution,
for details about the license.
*****************************************************************************
Authors: Alexander Klenin
}
unit TATransformations;
{$MODE ObjFPC}{$H+}
interface
uses
Classes, SysUtils,
TAChartUtils;
type
TChartAxisTransformations = class;
{ TAxisTransform }
TAxisTransform = class(TIndexedComponent)
strict private
FEnabled: Boolean;
FTransformations: TChartAxisTransformations;
procedure SetEnabled(AValue: Boolean);
procedure SetTransformations(AValue: TChartAxisTransformations);
protected
procedure ReadState(Reader: TReader); override;
procedure SetParentComponent(AParent: TComponent); override;
protected
procedure Changed;
function GetIndex: Integer; override;
procedure SetIndex(AValue: Integer); override;
protected
FDrawData: TDrawDataItem;
procedure ClearBounds; virtual;
function GetDrawDataClass: TDrawDataItemClass; virtual;
procedure SetChart(AChart: TObject);
procedure UpdateBounds(var AMin, AMax: Double); virtual;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
public
procedure Assign(ASource: TPersistent); override;
function GetParentComponent: TComponent; override;
function HasParent: Boolean; override;
public
function AxisToGraph(AX: Double): Double; virtual;
function GraphToAxis(AX: Double): Double; virtual;
property Transformations: TChartAxisTransformations
read FTransformations write SetTransformations;
published
property Enabled: Boolean read FEnabled write SetEnabled default true;
end;
TAxisTransformClass = class of TAxisTransform;
{$IFNDEF fpdoc} // Workaround for issue #18549.
TAxisTransformEnumerator = specialize TTypedFPListEnumerator<TAxisTransform>;
{$ENDIF}
TAxisTransformList = class(TIndexedComponentList)
public
function GetEnumerator: TAxisTransformEnumerator;
end;
{ TChartAxisTransformations }
TChartAxisTransformations = class(TComponent)
strict private
FBroadcaster: TBroadcaster;
FList: TAxisTransformList;
protected
procedure SetName(const AValue: TComponentName); override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
public
procedure GetChildren(Proc: TGetChildProc; Root: TComponent); override;
procedure SetChildOrder(Child: TComponent; Order: Integer); override;
public
function AxisToGraph(AX: Double): Double;
procedure ClearBounds;
function GraphToAxis(AX: Double): Double;
procedure SetChart(AChart: TObject);
procedure UpdateBounds(var AMin, AMax: Double);
property Broadcaster: TBroadcaster read FBroadcaster;
published
property List: TAxisTransformList read FList;
end;
{ TLinearAxisTransform }
TLinearAxisTransform = class(TAxisTransform)
strict private
FOffset: Double;
FScale: Double;
function OffsetIsStored: Boolean;
function ScaleIsStored: Boolean;
procedure SetOffset(AValue: Double);
procedure SetScale(AValue: Double);
public
constructor Create(AOwner: TComponent); override;
public
procedure Assign(ASource: TPersistent); override;
function AxisToGraph(AX: Double): Double; override;
function GraphToAxis(AX: Double): Double; override;
published
property Offset: Double read FOffset write SetOffset stored OffsetIsStored;
property Scale: Double read FScale write SetScale stored ScaleIsStored;
end;
{ TAutoScaleAxisTransform }
TAutoScaleAxisTransform = class(TAxisTransform)
strict private
FMaxValue: Double;
FMinValue: Double;
function MaxValueIsStored: Boolean;
function MinValueIsStored: Boolean;
procedure SetMaxValue(AValue: Double);
procedure SetMinValue(AValue: Double);
protected
procedure ClearBounds; override;
function GetDrawDataClass: TDrawDataItemClass; override;
procedure UpdateBounds(var AMin, AMax: Double); override;
public
constructor Create(AOwner: TComponent); override;
public
procedure Assign(ASource: TPersistent); override;
function AxisToGraph(AX: Double): Double; override;
function GraphToAxis(AX: Double): Double; override;
published
property MaxValue: Double
read FMaxValue write SetMaxValue stored MaxValueIsStored;
property MinValue: Double
read FMinValue write SetMinValue stored MinValueIsStored;
end;
{ TLogarithmAxisTransform }
TLogarithmAxisTransform = class(TAxisTransform)
strict private
FBase: Double;
procedure SetBase(AValue: Double);
public
constructor Create(AOwner: TComponent); override;
public
procedure Assign(Source: TPersistent); override;
function AxisToGraph(AX: Double): Double; override;
function GraphToAxis(AX: Double): Double; override;
published
property Base: Double read FBase write SetBase;
end;
TCumulNormDistrAxisTransform = class(TAxisTransform)
public
function AxisToGraph(AX: Double): Double; override;
function GraphToAxis(AX: Double): Double; override;
end;
TTransformEvent = procedure (AX: Double; out AT: Double) of object;
{ TUserDefinedAxisTransform }
TUserDefinedAxisTransform = class(TAxisTransform)
private
FOnAxisToGraph: TTransformEvent;
FOnGraphToAxis: TTransformEvent;
procedure SetOnAxisToGraph(AValue: TTransformEvent);
procedure SetOnGraphToAxis(AValue: TTransformEvent);
public
procedure Assign(ASource: TPersistent); override;
function AxisToGraph(AX: Double): Double; override;
function GraphToAxis(AX: Double): Double; override;
published
property OnAxisToGraph: TTransformEvent read FOnAxisToGraph write SetOnAxisToGraph;
property OnGraphToAxis: TTransformEvent read FOnGraphToAxis write SetOnGraphToAxis;
end;
procedure Register;
procedure RegisterAxisTransformClass(AAxisTransformClass: TAxisTransformClass;
const ACaption: String); overload;
procedure RegisterAxisTransformClass(AAxisTransformClass: TAxisTransformClass;
ACaptionPtr: PStr); overload;
implementation
uses
ComponentEditors, Forms, Math, PropEdits,
TAChartStrConsts, TAMath, TASubcomponentsEditor;
type
{ TAxisTransformsComponentEditor }
TAxisTransformsComponentEditor = class(TSubComponentListEditor)
protected
function MakeEditorForm: TForm; override;
public
function GetVerb(Index: Integer): string; override;
end;
{ TAxisTransformsPropertyEditor }
TAxisTransformsPropertyEditor = class(TComponentListPropertyEditor)
protected
function GetChildrenCount: Integer; override;
function MakeEditorForm: TForm; override;
end;
{ TAxisTransformsEditorForm }
TAxisTransformsEditorForm = class(TComponentListEditorForm)
protected
procedure AddSubcomponent(AParent, AChild: TComponent); override;
procedure BuildCaption; override;
function ChildClass: TComponentClass; override;
procedure EnumerateSubcomponentClasses; override;
function GetChildrenList: TFPList; override;
function MakeSubcomponent(
AOwner: TComponent; ATag: Integer): TComponent; override;
end;
TAutoScaleTransformData = class (TDrawDataItem)
private
FMin, FMax, FOffset, FScale: Double;
end;
var
AxisTransformsClassRegistry: TClassRegistry;
procedure Register;
var
i: Integer;
begin
with AxisTransformsClassRegistry do
for i := 0 to Count - 1 do
RegisterNoIcon([TAxisTransformClass(GetClass(i))]);
RegisterComponents(CHART_COMPONENT_IDE_PAGE, [TChartAxisTransformations]);
RegisterPropertyEditor(
TypeInfo(TAxisTransformList), TChartAxisTransformations,
'List', TAxisTransformsPropertyEditor);
RegisterComponentEditor(
TChartAxisTransformations, TAxisTransformsComponentEditor);
end;
procedure RegisterAxisTransformClass(AAxisTransformClass: TAxisTransformClass;
const ACaption: String);
begin
RegisterClass(AAxisTransformClass);
with AxisTransformsClassRegistry do
if IndexOfClass(AAxisTransformClass) < 0 then
Add(TClassRegistryItem.Create(AAxisTransformClass, ACaption));
end;
procedure RegisterAxisTransformClass(AAxisTransformClass: TAxisTransformClass;
ACaptionPtr: PStr);
begin
RegisterClass(AAxisTransformClass);
with AxisTransformsClassRegistry do
if IndexOfClass(AAxisTransformClass) < 0 then
Add(TClassRegistryItem.CreateRes(AAxisTransformClass, ACaptionPtr));
end;
{ TAxisTransformList }
function TAxisTransformList.GetEnumerator: TAxisTransformEnumerator;
begin
Result := TAxisTransformEnumerator.Create(Self);
end;
{ TAxisTransformsComponentEditor }
function TAxisTransformsComponentEditor.GetVerb(Index: Integer): string;
begin
if Index = 0 then
Result := tasAxisTransformsEditorTitle
else
Result := '';
end;
function TAxisTransformsComponentEditor.MakeEditorForm: TForm;
begin
Result := TAxisTransformsEditorForm.Create(Application, GetComponent, Self, nil);
end;
{ TAxisTransformsPropertyEditor }
function TAxisTransformsPropertyEditor.GetChildrenCount: Integer;
begin
Result := (GetObjectValue as TAxisTransformList).Count;
end;
function TAxisTransformsPropertyEditor.MakeEditorForm: TForm;
begin
with TAxisTransformsEditorForm do
Result := Create(Application, GetComponent(0) as TComponent, nil, Self);
end;
{ TAxisTransformsEditorForm }
procedure TAxisTransformsEditorForm.AddSubcomponent(
AParent, AChild: TComponent);
begin
(AChild as TAxisTransform).Transformations :=
AParent as TChartAxisTransformations;
end;
procedure TAxisTransformsEditorForm.BuildCaption;
begin
Caption := tasAxisTransformsEditorTitle + ' - ' + Parent.Name;
end;
function TAxisTransformsEditorForm.ChildClass: TComponentClass;
begin
Result := TAxisTransform;
end;
procedure TAxisTransformsEditorForm.EnumerateSubcomponentClasses;
var
i: Integer;
begin
for i := 0 to AxisTransformsClassRegistry.Count - 1 do
AddSubcomponentClass(AxisTransformsClassRegistry.GetCaption(i), i);
end;
function TAxisTransformsEditorForm.GetChildrenList: TFPList;
begin
Result := (Parent as TChartAxisTransformations).List;
end;
function TAxisTransformsEditorForm.MakeSubcomponent(
AOwner: TComponent; ATag: Integer): TComponent;
begin
with AxisTransformsClassRegistry do
Result := TAxisTransformClass(GetClass(ATag)).Create(AOwner);
end;
{ TAxisTransform }
procedure TAxisTransform.Assign(ASource: TPersistent);
begin
if ASource is TAxisTransform then
with TAxisTransform(ASource) do
Self.FEnabled := Enabled
else
inherited Assign(ASource);
end;
function TAxisTransform.AxisToGraph(AX: Double): Double;
begin
Result := AX;
end;
procedure TAxisTransform.Changed;
begin
if Transformations <> nil then
Transformations.Broadcaster.Broadcast(Self);
end;
procedure TAxisTransform.ClearBounds;
begin
// empty
end;
constructor TAxisTransform.Create(AOwner: TComponent);
begin
FEnabled := true;
inherited Create(AOwner);
end;
destructor TAxisTransform.Destroy;
begin
Transformations := nil;
DrawData.DeleteByOwner(Self);
inherited;
end;
function TAxisTransform.GetDrawDataClass: TDrawDataItemClass;
begin
Result := nil;
end;
function TAxisTransform.GetIndex: Integer;
begin
if Transformations = nil then
Result := -1
else
Result := Transformations.List.IndexOf(Self);
end;
function TAxisTransform.GetParentComponent: TComponent;
begin
Result := Transformations;
end;
function TAxisTransform.GraphToAxis(AX: Double): Double;
begin
Result := AX;
end;
function TAxisTransform.HasParent: Boolean;
begin
Result := true;
end;
procedure TAxisTransform.ReadState(Reader: TReader);
begin
inherited ReadState(Reader);
if Reader.Parent is TChartAxisTransformations then
Transformations := TChartAxisTransformations(Reader.Parent);
end;
procedure TAxisTransform.SetChart(AChart: TObject);
begin
if GetDrawDataClass = nil then exit;
FDrawData := DrawData.Find(AChart, Self);
if FDrawData <> nil then exit;
FDrawData := GetDrawDataClass.Create(AChart, Self);
DrawData.Add(FDrawData);
end;
procedure TAxisTransform.SetEnabled(AValue: Boolean);
begin
if FEnabled = AValue then exit;
FEnabled := AValue;
Changed;
end;
procedure TAxisTransform.SetIndex(AValue: Integer);
begin
with Transformations.List do
Move(Index, EnsureRange(AValue, 0, Count - 1));
end;
procedure TAxisTransform.SetParentComponent(AParent: TComponent);
begin
if not (csLoading in ComponentState) then
Transformations := AParent as TChartAxisTransformations;
end;
procedure TAxisTransform.SetTransformations(AValue: TChartAxisTransformations);
begin
if FTransformations = AValue then exit;
if FTransformations <> nil then
FTransformations.List.Remove(Self);
FTransformations := AValue;
if FTransformations <> nil then
FTransformations.List.Add(Self);
end;
procedure TAxisTransform.UpdateBounds(var AMin, AMax: Double);
begin
if not IsInfinite(AMin) then
AMin := AxisToGraph(AMin);
if not IsInfinite(AMax) then
AMax := AxisToGraph(AMax);
end;
{ TChartAxisTransformations }
function TChartAxisTransformations.AxisToGraph(AX: Double): Double;
var
t: TAxisTransform;
begin
Result := AX;
if IsNan(Result) then exit;
for t in List do
if t.Enabled then
Result := t.AxisToGraph(Result);
end;
procedure TChartAxisTransformations.ClearBounds;
var
t: TAxisTransform;
begin
for t in List do
if t.Enabled then
t.ClearBounds;
end;
constructor TChartAxisTransformations.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FBroadcaster := TBroadcaster.Create;
FList := TAxisTransformList.Create;
end;
destructor TChartAxisTransformations.Destroy;
begin
while List.Count > 0 do
TAxisTransform(List[List.Count - 1]).Free;
FreeAndNil(FList);
FreeAndNil(FBroadcaster);
inherited;
end;
procedure TChartAxisTransformations.GetChildren(
Proc: TGetChildProc; Root: TComponent);
var
t: TAxisTransform;
begin
for t in List do
if t.Owner = Root then
Proc(t);
end;
function TChartAxisTransformations.GraphToAxis(AX: Double): Double;
var
i: Integer;
begin
Result := AX;
for i := List.Count - 1 downto 0 do
with TAxisTransform(List[i]) do
if Enabled then
Result := GraphToAxis(Result);
end;
procedure TChartAxisTransformations.SetChart(AChart: TObject);
var
t: TAxisTransform;
begin
for t in List do
if t.Enabled then
t.SetChart(AChart);
end;
procedure TChartAxisTransformations.SetChildOrder(
Child: TComponent; Order: Integer);
var
i: Integer;
begin
i := List.IndexOf(Child);
if i >= 0 then
List.Move(i, Order);
end;
procedure TChartAxisTransformations.SetName(const AValue: TComponentName);
var
oldName: String;
begin
if Name = AValue then exit;
oldName := Name;
inherited SetName(AValue);
if csDesigning in ComponentState then
List.ChangeNamePrefix(oldName, AValue);
end;
procedure TChartAxisTransformations.UpdateBounds(var AMin, AMax: Double);
var
t: TAxisTransform;
begin
for t in List do
if t.Enabled then
t.UpdateBounds(AMin, AMax);
end;
{ TLinearAxisTransform }
procedure TLinearAxisTransform.Assign(ASource: TPersistent);
begin
if ASource is TLinearAxisTransform then
with TLinearAxisTransform(ASource) do begin
Self.FOffset := Offset;
Self.FScale := Scale;
end;
inherited Assign(ASource);
end;
function TLinearAxisTransform.AxisToGraph(AX: Double): Double;
begin
Result := AX * Scale + Offset;
end;
constructor TLinearAxisTransform.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FScale := 1.0;
end;
function TLinearAxisTransform.GraphToAxis(AX: Double): Double;
begin
Result := (AX - Offset) / Scale;
end;
function TLinearAxisTransform. OffsetIsStored: Boolean;
begin
Result := not SameValue(Offset, 0.0);
end;
function TLinearAxisTransform.ScaleIsStored: Boolean;
begin
Result := not SameValue(Scale, 1.0);
end;
procedure TLinearAxisTransform.SetOffset(AValue: Double);
begin
if SameValue(FOffset, AValue) then exit;
FOffset := AValue;
Changed;
end;
procedure TLinearAxisTransform.SetScale(AValue: Double);
begin
if SameValue(FScale, AValue) then exit;
FScale := AValue;
if SameValue(FScale, 0.0) then FScale := 1.0;
Changed;
end;
{ TLogarithmAxisTransform }
procedure TLogarithmAxisTransform.Assign(Source: TPersistent);
begin
if Source is TLogarithmAxisTransform then
with Source as TLogarithmAxisTransform do
Self.FBase := Base
else
inherited Assign(Source);
end;
function TLogarithmAxisTransform.AxisToGraph(AX: Double): Double;
begin
if AX > 0 then
Result := LogN(Base, AX)
else
Result := NegInfinity;
end;
constructor TLogarithmAxisTransform.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FBase := Exp(1);
end;
function TLogarithmAxisTransform.GraphToAxis(AX: Double): Double;
begin
Result := Power(Base, AX);
end;
procedure TLogarithmAxisTransform.SetBase(AValue: Double);
begin
if FBase = AValue then exit;
if (AValue <= 0) or (AValue = 1.0) then
raise Exception.Create(rsInvalidLogBase);
FBase := AValue;
Changed;
end;
{ TAutoScaleAxisTransform }
procedure TAutoScaleAxisTransform.Assign(ASource: TPersistent);
begin
if ASource is TAutoScaleAxisTransform then
with TAutoScaleAxisTransform(ASource) do begin
Self.FMinValue := FMinValue;
Self.FMaxValue := FMaxValue;
end;
inherited Assign(ASource);
end;
function TAutoScaleAxisTransform.AxisToGraph(AX: Double): Double;
begin
with TAutoScaleTransformData(FDrawData) do
Result := AX * FScale + FOffset;
end;
procedure TAutoScaleAxisTransform.ClearBounds;
begin
inherited ClearBounds;
// Avoid crashing when called too early, e.g. when a TNavPanel is on the form
// https://forum.lazarus.freepascal.org/index.php/topic,47429.0.html
if FDrawData = nil then
exit;
with TAutoScaleTransformData(FDrawData) do begin
FMin := SafeInfinity;
FMax := NegInfinity;
FOffset := 0.0;
FScale := 1.0;
end;
end;
constructor TAutoScaleAxisTransform.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FMaxValue := 1.0;
end;
function TAutoScaleAxisTransform.GetDrawDataClass: TDrawDataItemClass;
begin
Result := TAutoScaleTransformData;
end;
function TAutoScaleAxisTransform.GraphToAxis(AX: Double): Double;
begin
with TAutoScaleTransformData(FDrawData) do
Result := (AX - FOffset) / FScale;
end;
function TAutoScaleAxisTransform.MaxValueIsStored: Boolean;
begin
Result := MaxValue <> 1.0;
end;
function TAutoScaleAxisTransform.MinValueIsStored: Boolean;
begin
Result := MinValue <> 0.0;
end;
procedure TAutoScaleAxisTransform.SetMaxValue(AValue: Double);
begin
if FMaxValue = AValue then exit;
FMaxValue := AValue;
Changed;
end;
procedure TAutoScaleAxisTransform.SetMinValue(AValue: Double);
begin
if FMinValue = AValue then exit;
FMinValue := AValue;
Changed;
end;
procedure TAutoScaleAxisTransform.UpdateBounds(var AMin, AMax: Double);
begin
// Auto-scale is only defined for finite bounds.
if IsInfinite(AMin) or IsInfinite(AMax) then exit;
with TAutoScaleTransformData(FDrawData) do begin
UpdateMinMax(AMin, FMin, FMax);
UpdateMinMax(AMax, FMin, FMax);
if FMax = FMin then
FScale := 1.0
else
FScale := (MaxValue - MinValue) / (FMax - FMin);
FOffset := MinValue - FMin * FScale;
end;
AMin := MinValue;
AMax := MaxValue;
end;
{ TCumulNormDistrAxisTransform }
function TCumulNormDistrAxisTransform.AxisToGraph(AX: Double): Double;
begin
Result := InvCumulNormDistr(AX);
end;
function TCumulNormDistrAxisTransform.GraphToAxis(AX: Double): Double;
begin
Result := CumulNormDistr(AX);
end;
{ TUserDefinedAxisTransform }
procedure TUserDefinedAxisTransform.Assign(ASource: TPersistent);
begin
if ASource is TUserDefinedAxisTransform then
with TUserDefinedAxisTransform(ASource) do begin
Self.FOnAxisToGraph := FOnAxisToGraph;
Self.FOnGraphToAxis := FOnGraphToAxis;
end;
inherited Assign(ASource);
end;
function TUserDefinedAxisTransform.AxisToGraph(AX: Double): Double;
begin
if Assigned(OnAxisToGraph) then
OnAxisToGraph(AX, Result)
else
Result := AX;
end;
function TUserDefinedAxisTransform.GraphToAxis(AX: Double): Double;
begin
if Assigned(OnGraphToAxis) then
OnGraphToAxis(AX, Result)
else
Result := AX;
end;
procedure TUserDefinedAxisTransform.SetOnAxisToGraph(AValue: TTransformEvent);
begin
if TMethod(FOnAxisToGraph) = TMethod(AValue) then exit;
FOnAxisToGraph := AValue;
Changed;
end;
procedure TUserDefinedAxisTransform.SetOnGraphToAxis(AValue: TTransformEvent);
begin
if TMethod(FOnGraphToAxis) = TMethod(AValue) then exit;
FOnGraphToAxis := AValue;
Changed;
end;
initialization
AxisTransformsClassRegistry := TClassRegistry.Create;
RegisterAxisTransformClass(TAutoScaleAxisTransform, @rsAutoScale);
RegisterAxisTransformClass(
TCumulNormDistrAxisTransform, @rsCumulativeNormalDistribution);
RegisterAxisTransformClass(TLinearAxisTransform, @rsLinear);
RegisterAxisTransformClass(TLogarithmAxisTransform, @rsLogarithmic);
RegisterAxisTransformClass(TUserDefinedAxisTransform, @rsUserDefined);
finalization
FreeAndNil(AxisTransformsClassRegistry);
end.