{ Function series for TAChart. ***************************************************************************** See the file COPYING.modifiedLGPL.txt, included in this distribution, for details about the license. ***************************************************************************** Authors: Alexander Klenin } unit TAFuncSeries; {$H+} interface uses Classes, Graphics, typ, Types, TAChartUtils, TACustomFuncSeries, TACustomSeries, TACustomSource, TADrawUtils, TAFitUtils, TALegend, TATypes; const DEF_FUNC_STEP = 2; DEF_SPLINE_DEGREE = 3; DEF_SPLINE_STEP = 4; DEF_FIT_STEP = 4; DEF_FIT_PARAM_COUNT = 3; DEF_COLORMAP_STEP = 4; DEF_COLORMAP_LEGENDFORMAT = 'z ≤ %1:g|%g < z ≤ %g|%g < z'; type TFuncCalculateEvent = procedure (const AX: Double; out AY: Double) of object; TFuncSeriesStep = 1..MaxInt; TCustomFuncSeries = class(TBasicFuncSeries) strict private FDomainExclusions: TIntervalList; FExtentAutoY: Boolean; FPen: TChartPen; FStep: TFuncSeriesStep; procedure SetExtentAutoY(AValue: Boolean); procedure SetPen(AValue: TChartPen); procedure SetStep(AValue: TFuncSeriesStep); protected function DoCalculate(AX: Double): Double; virtual; abstract; procedure GetBounds(var ABounds: TDoubleRect); override; procedure GetLegendItems(AItems: TChartLegendItems); override; public procedure Assign(ASource: TPersistent); override; constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure Draw(ADrawer: IChartDrawer); override; function GetNearestPoint( const AParams: TNearestPointParams; out AResults: TNearestPointResults): Boolean; override; public property DomainExclusions: TIntervalList read FDomainExclusions; published property AxisIndexX; property AxisIndexY; property ExtentAutoY: Boolean read FExtentAutoY write SetExtentAutoY default false; property Pen: TChartPen read FPen write SetPen; property Step: TFuncSeriesStep read FStep write SetStep default DEF_FUNC_STEP; end; TFuncSeries = class(TCustomFuncSeries) strict private FOnCalculate: TFuncCalculateEvent; procedure SetOnCalculate(AValue: TFuncCalculateEvent); protected function DoCalcIdentity(AX: Double): Double; function DoCalculate(AX: Double): Double; override; procedure GetBounds(var ABounds: TDoubleRect); override; public procedure Assign(ASource: TPersistent); override; procedure Draw(ADrawer: IChartDrawer); override; function GetNearestPoint( const AParams: TNearestPointParams; out AResults: TNearestPointResults): Boolean; override; function IsEmpty: Boolean; override; published property OnCalculate: TFuncCalculateEvent read FOnCalculate write SetOnCalculate; end; TParametricCurveCalculateEvent = procedure ( const AT: Double; out AX, AY: Double) of object; TParametricCurveSeries = class(TBasicFuncSeries) strict private FOnCalculate: TParametricCurveCalculateEvent; FParamMax: Double; FParamMaxStep: Double; FParamMin: Double; FPen: TChartPen; FStep: TFuncSeriesStep; function DoCalcIdentity(AT: Double): TDoublePoint; function DoCalculate(AT: Double): TDoublePoint; function ParamMaxIsStored: Boolean; function ParamMaxStepIsStored: Boolean; function ParamMinIsStored: Boolean; procedure SetOnCalculate(AValue: TParametricCurveCalculateEvent); procedure SetParamMax(AValue: Double); procedure SetParamMaxStep(AValue: Double); procedure SetParamMin(AValue: Double); procedure SetPen(AValue: TChartPen); procedure SetStep(AValue: TFuncSeriesStep); protected procedure GetLegendItems(AItems: TChartLegendItems); override; public procedure Assign(ASource: TPersistent); override; constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure Draw(ADrawer: IChartDrawer); override; function IsEmpty: Boolean; override; published property AxisIndexX; property AxisIndexY; property OnCalculate: TParametricCurveCalculateEvent read FOnCalculate write SetOnCalculate; property ParamMax: Double read FParamMax write SetParamMax stored ParamMaxIsStored; property ParamMaxStep: Double read FParamMaxStep write SetParamMaxStep stored ParamMaxStepIsStored; property ParamMin: Double read FParamMin write SetParamMin stored ParamMinIsStored; property Pen: TChartPen read FPen write SetPen; property Step: TFuncSeriesStep read FStep write SetStep default DEF_FUNC_STEP; end; TSplineDegree = 1..100; { TBSplineSeries } TBSplineSeries = class(TBasicPointSeries) strict private FDegree: TSplineDegree; FPen: TChartPen; FStep: TFuncSeriesStep; procedure InternalPrepareGraphPoints; procedure SetDegree(AValue: TSplineDegree); procedure SetPen(AValue: TChartPen); procedure SetStep(AValue: TFuncSeriesStep); protected procedure GetLegendItems(AItems: TChartLegendItems); override; public procedure Assign(ASource: TPersistent); override; constructor Create(AOwner: TComponent); override; destructor Destroy; override; function Calculate(AX: Double): Double; procedure Draw(ADrawer: IChartDrawer); override; function GetNearestPoint(const AParams: TNearestPointParams; out AResults: TNearestPointResults): Boolean; override; published property Active default true; property AxisIndexX; property AxisIndexY; property ShowInLegend; property Source; property Title; property ZPosition; published property Degree: TSplineDegree read FDegree write SetDegree default DEF_SPLINE_DEGREE; property Pen: TChartPen read FPen write SetPen; property Pointer; property Step: TFuncSeriesStep read FStep write SetStep default DEF_SPLINE_STEP; property ToolTargets default [nptPoint, nptCustom]; property OnCustomDrawPointer; property OnGetPointerStyle; end; TBadDataChartPen = class(TChartPen) published property Color default clRed; end; TCubicSplineOptions = set of ( csoDrawFewPoints, csoDrawUnorderedX, csoExtrapolateLeft, csoExtrapolateRight); TCubicSplineSeries = class(TBasicPointSeries) strict private FBadDataPen: TBadDataChartPen; FOptions: TCubicSplineOptions; FPen: TChartPen; FStep: TFuncSeriesStep; procedure SetPen(AValue: TChartPen); procedure SetStep(AValue: TFuncSeriesStep); strict private type TSpline = class public FCoeff, FX, FY: array of ArbFloat; FIntervals: TIntervalList; FIsUnorderedX: Boolean; FStartIndex: Integer; destructor Destroy; override; function Calculate(AX: Double): Double; function IsFewPoints: Boolean; inline; function PrepareCoeffs( ASource: TCustomChartSource; var AIndex: Integer): Boolean; procedure PrepareIntervals(AOptions: TCubicSplineOptions); end; var FSplines: array of TSpline; procedure FreeSplines; function IsUnorderedVisible: Boolean; inline; function IsFewPointsVisible: Boolean; inline; procedure PrepareCoeffs; procedure SetBadDataPen(AValue: TBadDataChartPen); procedure SetOptions(AValue: TCubicSplineOptions); protected procedure GetLegendItems(AItems: TChartLegendItems); override; procedure SourceChanged(ASender: TObject); override; public procedure Assign(ASource: TPersistent); override; function Calculate(AX: Double): Double; 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 Active default true; property AxisIndexX; property AxisIndexY; property Pointer; property ShowInLegend; property Source; property Title; property ToolTargets default [nptPoint, nptCustom]; property ZPosition; property OnCustomDrawPointer; property OnGetPointerStyle; published // Used when data is not suitable for drawing cubic spline -- // e.g. points are too few or not ordered by X value. property BadDataPen: TBadDataChartPen read FBadDataPen write SetBadDataPen; property Options: TCubicSplineOptions read FOptions write SetOptions default []; property Pen: TChartPen read FPen write SetPen; property Step: TFuncSeriesStep read FStep write SetStep default DEF_SPLINE_STEP; end; TFitParamsState = (fpsUnknown, fpsInvalid, fpsValid); TCalcGoodnessOfFitEvent = procedure (Sender: TObject; var x,y: ArbFloat; n: Integer; out AResult: Double) of object; TFitSeries = class(TBasicPointSeries) strict private FDrawFitRangeOnly: Boolean; FFitEquation: TFitEquation; FFitParams: TDoubleDynArray; FFitRange: TChartRange; FOnFitComplete: TNotifyEvent; FPen: TChartPen; FState: TFitParamsState; FStep: TFuncSeriesStep; FGoodnessOfFit: Double; FOnCalcGoodnessOfFit: TCalcGoodnessOfFitEvent; function GetParam(AIndex: Integer): Double; function GetParamCount: Integer; function PrepareIntervals: TIntervalList; procedure SetDrawFitRangeOnly(AValue: Boolean); procedure SetFitEquation(AValue: TFitEquation); procedure SetFitRange(AValue: TChartRange); procedure SetParam(AIndex: Integer; AValue: Double); procedure SetParamCount(AValue: Integer); procedure SetPen(AValue: TChartPen); procedure SetStep(AValue: TFuncSeriesStep); strict protected procedure CalcXRange(out AXMin, AXMax: Double); procedure Transform(AX, AY: Double; out ANewX, ANewY: Extended); protected procedure AfterAdd; override; function CalcGoodnessOfFit(var x,y: ArbFloat; n: Integer): Double; virtual; procedure GetLegendItems(AItems: TChartLegendItems); override; procedure SourceChanged(ASender: TObject); override; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; public function Calculate(AX: Double): Double; virtual; procedure Draw(ADrawer: IChartDrawer); override; procedure ExecFit; virtual; function EquationText: IFitEquationText; function GetFitEquationString( ANumFormat: String; AXText: String = 'x'; AYText: String = 'y'): String; deprecated 'Use EquationText'; function GetNearestPoint( const AParams: TNearestPointParams; out AResults: TNearestPointResults): Boolean; override; property Param[AIndex: Integer]: Double read GetParam write SetParam; property GoodnessOfFit: Double read FGoodnessOfFit; property State: TFitParamsState read FState; published property AxisIndexX; property AxisIndexY; property DrawFitRangeOnly: Boolean read FDrawFitRangeOnly write SetDrawFitRangeOnly default true; property FitEquation: TFitEquation read FFitEquation write SetFitEquation default fePolynomial; property FitRange: TChartRange read FFitRange write SetFitRange; property ParamCount: Integer read GetParamCount write SetParamCount default DEF_FIT_PARAM_COUNT; property Pen: TChartPen read FPen write SetPen; property Pointer; property Source; property ToolTargets default [nptPoint, nptCustom]; property Step: TFuncSeriesStep read FStep write SetStep default DEF_FIT_STEP; property OnCalcGoodnessOfFit: TCalcGoodnessOfFitEvent read FOnCalcGoodnessOfFit write FOnCalcGoodnessOfFit; property OnCustomDrawPointer; property OnFitComplete: TNotifyEvent read FOnFitComplete write FOnFitComplete; property OnGetPointerStyle; end; TColorMapPalette = (cmpHot, cmpCold, cmpRainbow, cmpMonochrome); TFuncCalculate3DEvent = procedure (const AX, AY: Double; out AZ: Double) of object; TCustomColorMapSeries = class(TBasicFuncSeries) public type TUseImage = (cmuiAuto, cmuiAlways, cmuiNever); strict private FBrush: TBrush; FColorSource: TCustomChartSource; FColorSourceListener: TListener; FInterpolate: Boolean; FStepX: TFuncSeriesStep; FStepY: TFuncSeriesStep; FUseImage: TUseImage; FAutoMapColors: Boolean; FColorExtentMin, FColorExtentMax: Double; FBuiltinColorSource: TCustomChartSource; FBuiltinPalette: TColormapPalette; function GetColorSource: TCustomChartSource; function IsColorSourceStored: boolean; procedure SetAutoMapColors(AValue: Boolean); procedure SetBrush(AValue: TBrush); procedure SetBuiltinPalette(AValue: TColorMapPalette); procedure SetColorSource(AValue: TCustomChartSource); procedure SetInterpolate(AValue: Boolean); procedure SetStepX(AValue: TFuncSeriesStep); procedure SetStepY(AValue: TFuncSeriesStep); procedure SetUseImage(AValue: TUseImage); protected FMinZ, FMaxZ: Double; procedure BuildPalette(APalette: TColorMapPalette); procedure GetLegendItems(AItems: TChartLegendItems); override; procedure GetZRange(ARect: TRect; dx, dy: Integer); public procedure Assign(ASource: TPersistent); override; constructor Create(AOwner: TComponent); override; destructor Destroy; override; public function ColorByValue(AValue: Double): TColor; function FunctionValue(AX, AY: Double): Double; virtual; procedure Draw(ADrawer: IChartDrawer); override; function IsEmpty: Boolean; override; published property AutoMapColors: Boolean read FAutoMapColors write SetAutoMapColors default false; property AxisIndexX; property AxisIndexY; property Brush: TBrush read FBrush write SetBrush; property BuiltInPalette: TColorMapPalette read FBuiltinPalette write SetBuiltinPalette default cmpHot; property ColorSource: TCustomChartSource read GetColorSource write SetColorSource stored IsColorSourceStored; property Interpolate: Boolean read FInterpolate write SetInterpolate default false; property StepX: TFuncSeriesStep read FStepX write SetStepX default DEF_COLORMAP_STEP; property StepY: TFuncSeriesStep read FStepY write SetStepY default DEF_COLORMAP_STEP; property UseImage: TUseImage read FUseImage write SetUseImage default cmuiAuto; end; TColorMapSeries = class(TCustomColorMapSeries) private FOnCalculate: TFuncCalculate3DEvent; procedure SetOnCalculate(AValue: TFuncCalculate3DEvent); public procedure Assign(ASource: TPersistent); override; function FunctionValue(AX, AY: Double): Double; override; function IsEmpty: Boolean; override; published property OnCalculate: TFuncCalculate3DEvent read FOnCalculate write SetOnCalculate; end; // Builds an equation string based on the parameters and the type of equation. // AXText and AYText are placeholders for the x and y variables, respectively. // Parameters are formatted by passing ANumFormat to the "Format" function. function ParamsToEquation( AEquation: TFitEquation; const AParams: array of Double; ANumFormat: String; AXText: String = 'x'; AYText: String = 'y'): String; deprecated 'Use IFitEquationText'; implementation uses ipf, GraphType, GraphUtil, IntfGraphics, Math, StrUtils, SysUtils, TAChartStrConsts, TAGeometry, TAGraph, TAMath, TASources; const DEF_PARAM_MIN = 0.0; DEF_PARAM_MAX = 1.0; type TFitSeriesRange = class(TChartRange) strict private FSeries: TFitSeries; strict protected procedure StyleChanged(ASender: TObject); override; public constructor Create(ASeries: TFitSeries); end; TLegendItemColorMap = class(TLegendItem) strict private FColor2: TColor; FFramePen: TChartPen; public constructor Create( AColor1, AColor2: TColor; AFramePen: TChartPen; const AText: String); procedure Draw(ADrawer: IChartDrawer; const ARect: TRect); override; end; TParametricFunc = function (A: Double): TDoublePoint of object; function ParamsToEquation( AEquation: TFitEquation; const AParams: array of Double; ANumFormat, AXText, AYText: String): String; begin Result := TFitEquationText.Create.Equation(AEquation). X(AXText).Y(AYText).NumFormat(ANumFormat).Params(AParams); end; { TColorMapLegendItem } constructor TLegendItemColorMap.Create( AColor1, AColor2: TColor; AFramePen: TChartPen; const AText: String); begin inherited Create(AText); Color := AColor1; FColor2 := AColor2; FFramePen := AFramePen; end; procedure TLegendItemColorMap.Draw(ADrawer: IChartDrawer; const ARect: TRect); var x, w, pw: Integer; c: TColor; begin inherited Draw(ADrawer, ARect); with FFramePen do pw := IfThen(EffVisible, (Width + 1) div 2, 0); w := ARect.Right - ARect.Left - 2 * pw; if w <= 0 then exit; for x := ARect.Left + pw to ARect.Right - pw do begin c := InterpolateRGB(Color, FColor2, (x - ARect.Left - pw) / w); ADrawer.SetPenParams(psSolid, c); ADrawer.Line(x, ARect.Top, x, ARect.Bottom - 1); end; if pw > 0 then begin ADrawer.Pen := FFramePen; ADrawer.SetBrushParams(bsClear, clTAColor); ADrawer.Rectangle(ARect); end; end; { TFitSeriesRange } constructor TFitSeriesRange.Create(ASeries: TFitSeries); begin inherited Create(ASeries.ParentChart); FSeries := ASeries; end; procedure TFitSeriesRange.StyleChanged(ASender: TObject); begin FSeries.SourceChanged(nil); // reset FState to fpsUnknown FSeries.ExecFit; inherited; end; { TCustomFuncSeries } procedure TCustomFuncSeries.Assign(ASource: TPersistent); begin if ASource is TCustomFuncSeries then with TFuncSeries(ASource) do begin Self.FDomainExclusions.Assign(FDomainExclusions); Self.FExtentAutoY := FExtentAutoY; Self.Pen := FPen; Self.FStep := FStep; end; inherited Assign(ASource); end; constructor TCustomFuncSeries.Create(AOwner: TComponent); begin inherited Create(AOwner); FDomainExclusions := TIntervalList.Create; FDomainExclusions.OnChange := @StyleChanged; FPen := TChartPen.Create; FPen.OnChange := @StyleChanged; FStep := DEF_FUNC_STEP; end; destructor TCustomFuncSeries.Destroy; begin FreeAndNil(FDomainExclusions); FreeAndNil(FPen); inherited; end; procedure TCustomFuncSeries.Draw(ADrawer: IChartDrawer); begin ADrawer.SetBrushParams(bsClear, clTAColor); ADrawer.Pen := Pen; with TDrawFuncHelper.Create(Self, DomainExclusions, @DoCalculate, Step) do try DrawFunction(ADrawer); finally Free; end; end; procedure TCustomFuncSeries.GetBounds(var ABounds: TDoubleRect); var ymin, ymax: Double; begin inherited GetBounds(ABounds); if not Extent.UseXMin or not Extent.UseXMax or not ExtentAutoY then exit; ymin := SafeInfinity; ymax := NegInfinity; with TDrawFuncHelper.Create(Self, DomainExclusions, @DoCalculate, Step) do try CalcAxisExtentY(ABounds.a.X, ABounds.b.X, ymin, ymax); if not Extent.UseYMin or (ymin > Extent.YMin) then ABounds.a.Y := ymin; if not Extent.UseYMax or (ymax < Extent.YMax) then ABounds.b.Y := ymax; finally Free; end; end; procedure TCustomFuncSeries.GetLegendItems(AItems: TChartLegendItems); begin AItems.Add(TLegendItemLine.Create(Pen, LegendTextSingle)); end; function TCustomFuncSeries.GetNearestPoint( const AParams: TNearestPointParams; out AResults: TNearestPointResults): Boolean; begin with TDrawFuncHelper.Create(Self, DomainExclusions, @DoCalculate, Step) do try Result := GetNearestPoint(AParams, AResults); finally Free; end; end; procedure TCustomFuncSeries.SetExtentAutoY(AValue: Boolean); begin if FExtentAutoY = AValue then exit; FExtentAutoY := AValue; UpdateParentChart; end; procedure TCustomFuncSeries.SetPen(AValue: TChartPen); begin if FPen = AValue then exit; FPen.Assign(AValue); UpdateParentChart; end; procedure TCustomFuncSeries.SetStep(AValue: TFuncSeriesStep); begin if FStep = AValue then exit; FStep := AValue; UpdateParentChart; end; { TFuncSeries } procedure TFuncSeries.Assign(ASource: TPersistent); begin if ASource is TFuncSeries then with TFuncSeries(ASource) do begin Self.FOnCalculate := FOnCalculate; end; inherited Assign(ASource); end; function TFuncSeries.DoCalcIdentity(AX: Double): Double; begin Result := AX; end; function TFuncSeries.DoCalculate(AX: Double): Double; begin OnCalculate(AX, Result) end; procedure TFuncSeries.Draw(ADrawer: IChartDrawer); var calc: TTransformFunc; begin if Assigned(OnCalculate) then calc := @DoCalculate else if csDesigning in ComponentState then calc := @DoCalcIdentity else exit; ADrawer.SetBrushParams(bsClear, clTAColor); ADrawer.Pen := Pen; with TDrawFuncHelper.Create(Self, DomainExclusions, calc, Step) do try DrawFunction(ADrawer); finally Free; end; end; procedure TFuncSeries.GetBounds(var ABounds: TDoubleRect); begin if (csDesigning in ComponentState) then exit; inherited GetBounds(ABounds); end; function TFuncSeries.GetNearestPoint( const AParams: TNearestPointParams; out AResults: TNearestPointResults): Boolean; begin AResults.FIndex := -1; if not Assigned(OnCalculate) then exit(false); Result := inherited; end; function TFuncSeries.IsEmpty: Boolean; begin Result := not Assigned(OnCalculate); end; procedure TFuncSeries.SetOnCalculate(AValue: TFuncCalculateEvent); begin if TMethod(FOnCalculate) = TMethod(AValue) then exit; FOnCalculate := AValue; UpdateParentChart; end; { TParametricCurveSeries } procedure TParametricCurveSeries.Assign(ASource: TPersistent); begin if ASource is TFuncSeries then with TFuncSeries(ASource) do begin Self.FOnCalculate := FOnCalculate; Self.FParamMax := FParamMax; Self.FParamMin := FParamMin; Self.Pen := FPen; Self.FStep := FStep; end; inherited Assign(ASource); end; constructor TParametricCurveSeries.Create(AOwner: TComponent); begin inherited Create(AOwner); FParamMin := DEF_PARAM_MIN; FParamMax := DEF_PARAM_MAX; FPen := TChartPen.Create; FPen.OnChange := @StyleChanged; FStep := DEF_FUNC_STEP; end; destructor TParametricCurveSeries.Destroy; begin FreeAndNil(FPen); inherited; end; function TParametricCurveSeries.DoCalcIdentity(AT: Double): TDoublePoint; begin Result := DoublePoint(AT, AT); end; function TParametricCurveSeries.DoCalculate(AT: Double): TDoublePoint; begin OnCalculate(AT, Result.X, Result.Y); end; procedure TParametricCurveSeries.Draw(ADrawer: IChartDrawer); var calc: TParametricFunc; function PointAt(AT: Double): TPoint; begin Result := ParentChart.GraphToImage(AxisToGraph(calc(AT))) end; var t, ts, ms: Double; p, pp: TPoint; begin if Assigned(OnCalculate) then calc := @DoCalculate else if csDesigning in ComponentState then calc := @DoCalcIdentity else exit; ADrawer.SetBrushParams(bsClear, clTAColor); ADrawer.Pen := Pen; t := ParamMin; pp := PointAt(ParamMin); ADrawer.MoveTo(pp); ms := IfThen(ParamMaxStep > 0, ParamMaxStep, (ParamMax - ParamMin) / 4); ts := ms; while t < ParamMax do begin p := PointAt(t + ts); if PointDist(p, pp) > Sqr(Step) then ts /= 2 else begin ADrawer.LineTo(p); pp := p; t += ts; ts := MinValue([ts * 2, ms, ParamMax - t]); end; end; end; procedure TParametricCurveSeries.GetLegendItems(AItems: TChartLegendItems); begin AItems.Add(TLegendItemLine.Create(Pen, LegendTextSingle)); end; function TParametricCurveSeries.IsEmpty: Boolean; begin Result := not Assigned(OnCalculate); end; function TParametricCurveSeries.ParamMaxIsStored: Boolean; begin Result := not SameValue(ParamMax, DEF_PARAM_MAX); end; function TParametricCurveSeries.ParamMaxStepIsStored: Boolean; begin Result := not SameValue(ParamMaxStep, 0.0) and (ParamMaxStep > 0); end; function TParametricCurveSeries.ParamMinIsStored: Boolean; begin Result := not SameValue(ParamMin, DEF_PARAM_MIN); end; procedure TParametricCurveSeries.SetOnCalculate( AValue: TParametricCurveCalculateEvent); begin if TMethod(FOnCalculate) = TMethod(AValue) then exit; FOnCalculate := AValue; UpdateParentChart; end; procedure TParametricCurveSeries.SetParamMax(AValue: Double); begin if SameValue(FParamMax, AValue) then exit; FParamMax := AValue; UpdateParentChart; end; procedure TParametricCurveSeries.SetParamMaxStep(AValue: Double); begin if SameValue(FParamMaxStep, AValue) then exit; FParamMaxStep := AValue; UpdateParentChart; end; procedure TParametricCurveSeries.SetParamMin(AValue: Double); begin if SameValue(FParamMin, AValue) then exit; FParamMin := AValue; UpdateParentChart; end; procedure TParametricCurveSeries.SetPen(AValue: TChartPen); begin if FPen = AValue then exit; FPen.Assign(AValue); UpdateParentChart; end; procedure TParametricCurveSeries.SetStep(AValue: TFuncSeriesStep); begin if FStep = AValue then exit; FStep := AValue; UpdateParentChart; end; { TBSplineSeries } procedure TBSplineSeries.Assign(ASource: TPersistent); begin if ASource is TBSplineSeries then with TBSplineSeries(ASource) do begin Self.FDegree := FDegree; Self.Pen := FPen; Self.FStep := FStep; end; inherited Assign(ASource); end; function TBSplineSeries.Calculate(AX: Double): Double; var p: array of TDoublePoint; startIndex: Integer; splineStart: Integer = 0; splineEnd: Integer = -2; //level: Integer = 0; pStart, pEnd: TDoublePoint; function CalcSpline(APos: Double): TDoublePoint; var i, d: Integer; w, denom: Double; begin // Duplicate end points Degree times to fix spline to them. for i := 0 to Degree do p[i] := FGraphPoints[ EnsureRange(startIndex - Degree + i, splineStart, splineEnd)]; // De Boor's algorithm, source points used as control points. // Parametric coordinate is equal to point index. for d := 1 to Degree do begin denom := 1 / (Degree + 1 - d); for i := Degree downto d do begin w := (APos + Degree - i) * denom; p[i].X := WeightedAverage(p[i - 1].X, p[i].X, w); p[i].Y := WeightedAverage(p[i - 1].Y, p[i].Y, w); end; end; Result := p[Degree]; end; function Interpolate(ATest: Double): TDoublePoint; // calculates the B-Spline at n pivot points of the parametric coordinate t=0..1 // and seeks the t for the requested x value (ATest) by means of // interpolating a cubic spline var i,n: Integer; pp: TDoublePoint; xval, yval: array of ArbFloat; coeff: array of ArbFloat; ok: Integer; t: ArbFloat; begin n := 10; SetLength(xval, n+1); SetLength(yval, n+1); SetLength(coeff, n+1); // calculate pivots for i:=0 to n do begin pp := CalcSpline(i/n); xval[i] := pp.X; yval[i] := i/n; end; // calc interpolation spline coefficients ok := 0; ipfisn(N, xval[0], yval[0], coeff[0], ok); // calc interpolation spline value at ATest t := ipfspn(High(coeff), xval[0], yval[0], coeff[0], ATest, ok); // calc B-Spline value at t Result := CalcSpline(t); end; begin Result := NaN; if IsEmpty then exit; if Length(FGraphPoints) = 0 then InternalPrepareGraphPoints; SetLength(p, Degree + 1); while NextNumberSeq(FGraphPoints, splineStart, splineEnd) do begin startIndex := splineStart; pStart := CalcSpline(0.0); while startIndex <= splineEnd + Degree - 1 do begin pEnd := CalcSpline(1.0); // find interval if (AX = pStart.X) and (pStart.X = pEnd.X) then Result := pStart.Y else if InRange(AX, pStart.X, pEnd.X) and (pStart.X <> pEnd.X) then begin // calculate B-spline y value by interpolation if SameValue(AX, 15.88, 0.01) then Result := 1; Result := Interpolate(AX).Y; exit; end; pStart := pEnd; inc(startIndex); end; end; end; constructor TBSplineSeries.Create(AOwner: TComponent); begin inherited Create(AOwner); ToolTargets := [nptPoint, nptCustom]; FDegree := DEF_SPLINE_DEGREE; FPen := TChartPen.Create; FPen.OnChange := @StyleChanged; FPointer := TSeriesPointer.Create(ParentChart); FStep := DEF_SPLINE_STEP; end; destructor TBSplineSeries.Destroy; begin FreeAndNil(FPen); inherited; end; procedure TBSplineSeries.Draw(ADrawer: IChartDrawer); var p: array of TDoublePoint; startIndex: Integer; splineStart: Integer = 0; splineEnd: Integer = -2; function SplinePoint(APos: Double): TPoint; var i, d: Integer; w, denom: Double; begin // Duplicate end points Degree times to fix spline to them. for i := 0 to Degree do p[i] := FGraphPoints[ EnsureRange(startIndex - Degree + i, splineStart, splineEnd)]; // De Boor's algorithm, source points used as control points. // Parametric coordinate is equal to point index. for d := 1 to Degree do begin denom := 1 / (Degree + 1 - d); for i := Degree downto d do begin w := (APos + Degree - i) * denom; p[i].X := WeightedAverage(p[i - 1].X, p[i].X, w); p[i].Y := WeightedAverage(p[i - 1].Y, p[i].Y, w); end; end; Result := ParentChart.GraphToImage(p[Degree]); end; var level: Integer = 0; // Pass screen coordinates down to calculate them only once for each point. procedure SplineSegment(AL, AR: Double; const APL, APR: TPoint); const INF_SENTINEL = 15; // Arbitrary guard against infinite recursion. var m: Double; pm: TPoint; begin if (level > INF_SENTINEL) or (PointDist(APL, APR) <= Sqr(Step)) then // Left-then-right recursive call order guarantees that // the last drawn segment is the immediately preceding one. ADrawer.LineTo(APR) else begin m := (AL + AR) / 2; pm := SplinePoint(m); level += 1; SplineSegment(AL, m, APL, pm); SplineSegment(m, AR, pm, APR); level -= 1; end; end; begin if IsEmpty then exit; InternalPrepareGraphPoints; SetLength(p, Degree + 1); ADrawer.SetBrushParams(bsClear, clTAColor); ADrawer.Pen := Pen; while NextNumberSeq(FGraphPoints, splineStart, splineEnd) do begin ADrawer.MoveTo(ParentChart.GraphToImage(FGraphPoints[splineStart])); for startIndex := splineStart to splineEnd + Degree - 1 do SplineSegment(0.0, 1.0, SplinePoint(0.0), SplinePoint(1.0)); end; DrawLabels(ADrawer); DrawPointers(ADrawer); end; procedure TBSplineSeries.GetLegendItems(AItems: TChartLegendItems); var cp: TChartPen; p: TSeriesPointer; begin if FPen.Visible and (FPen.Style <> psClear) then cp := FPen else cp := nil; if FPointer.Visible then p := FPointer else p := nil; AItems.Add(TLegendItemLinePointer.Create(cp, p, LegendTextSingle)); end; function TBSplineSeries.GetNearestPoint( const AParams: TNearestPointParams; out AResults: TNearestPointResults): Boolean; var x, y: Double; begin Result := inherited GetNearestPoint(AParams, AResults); if (not Result) and (nptCustom in ToolTargets) and (nptCustom in AParams.FTargets) then begin x := GraphToAxisX(ParentChart.XImageToGraph(AParams.FPoint.X)); y := Calculate(x); AResults.FValue := DoublePoint(x, y); AResults.FImg := AParams.FPoint; AResults.FIndex := -1; AResults.FXIndex := -1; AResults.FYIndex := -1; AResults.FDist := 0; Result := not IsNaN(y); end; end; procedure TBSplineSeries.InternalPrepareGraphPoints; var ext: TDoubleRect; begin with Extent do begin ext.a := AxisToGraph(a); ext.b := AxisToGraph(b); end; NormalizeRect(ext); ExpandRange(ext.a.X, ext.b.X, 1.0); ExpandRange(ext.a.Y, ext.b.Y, 1.0); PrepareGraphPoints(ext, true); end; procedure TBSplineSeries.SetDegree(AValue: TSplineDegree); begin if FDegree = AValue then exit; FDegree := AValue; UpdateParentChart; end; procedure TBSplineSeries.SetPen(AValue: TChartPen); begin if FPen = AValue then exit; FPen.Assign(AValue); UpdateParentChart; end; procedure TBSplineSeries.SetStep(AValue: TFuncSeriesStep); begin if FStep = AValue then exit; FStep := AValue; UpdateParentChart; end; { TCubicSplineSeries.TSpline } function TCubicSplineSeries.TSpline.Calculate(AX: Double): Double; var ok: Integer = 0; begin if IsFewPoints then exit(SafeNaN); Result := ipfspn(High(FCoeff), FX[0], FY[0], FCoeff[0], AX, ok); if ok > 1 then Result := SafeNaN; end; destructor TCubicSplineSeries.TSpline.Destroy; begin FreeAndNil(FIntervals); inherited; end; function TCubicSplineSeries.TSpline.IsFewPoints: Boolean; begin Result := Length(FX) < 4; end; function TCubicSplineSeries.TSpline.PrepareCoeffs( ASource: TCustomChartSource; var AIndex: Integer): Boolean; var n, ok: Integer; begin n := ASource.Count - AIndex; SetLength(FX, n); SetLength(FY, n); SetLength(FCoeff, n); FIsUnorderedX := false; while (AIndex < ASource.Count) and IsNan(ASource[AIndex]^.Point) do AIndex += 1; FStartIndex := AIndex; n := 0; while (AIndex < ASource.Count) and not IsNan(ASource[AIndex]^.Point) do begin with ASource[AIndex]^ do if (n > 0) and (FX[n - 1] >= X) then FIsUnorderedX := true else begin FX[n] := X; FY[n] := Y; n += 1; end; AIndex += 1; end; SetLength(FX, n); SetLength(FY, n); SetLength(FCoeff, n); if n = 0 then exit(false); if IsFewPoints then exit(true); ok := 0; ipfisn(n - 1, FX[0], FY[0], FCoeff[0], ok); Result := ok = 1; end; procedure TCubicSplineSeries.TSpline.PrepareIntervals( AOptions: TCubicSplineOptions); begin FIntervals := TIntervalList.Create; try if not (csoExtrapolateLeft in AOptions) then FIntervals.AddRange(NegInfinity, FX[0]); if not (csoExtrapolateRight in AOptions) then FIntervals.AddRange(FX[High(FX)], SafeInfinity); except FreeAndNil(FIntervals); raise; end; end; { TCubicSplineSeries } procedure TCubicSplineSeries.Assign(ASource: TPersistent); begin if ASource is TCubicSplineSeries then with TCubicSplineSeries(ASource) do begin Self.Pen := FPen; Self.FStep := FStep; end; inherited Assign(ASource); end; function TCubicSplineSeries.Calculate(AX: Double): Double; var hint: Integer; s: TSpline; x: Double; begin for s in FSplines do begin hint := 0; x := AX; if not s.FIntervals.Intersect(x, x, hint) then exit(s.Calculate(AX)); end; Result := SafeNaN; end; constructor TCubicSplineSeries.Create(AOwner: TComponent); begin inherited Create(AOwner); ToolTargets := [nptPoint, nptCustom]; FBadDataPen := TBadDataChartPen.Create; FBadDataPen.OnChange := @StyleChanged; FPen := TChartPen.Create; FPen.OnChange := @StyleChanged; FPointer := TSeriesPointer.Create(ParentChart); FStep := DEF_SPLINE_STEP; FUseReticule := true; end; destructor TCubicSplineSeries.Destroy; begin FreeSplines; FreeAndNil(FBadDataPen); FreeAndNil(FPen); inherited; end; procedure TCubicSplineSeries.Draw(ADrawer: IChartDrawer); function DrawFewPoints(ASpline: TSpline): Boolean; var pts: TPointArray; i, lb, ub: Integer; begin Result := ASpline.IsFewPoints; if not Result or not IsFewPointsVisible then exit; lb := Max(ASpline.FStartIndex, FLoBound); ub := Min(ASpline.FStartIndex + High(ASpline.FX), FUpBound); if lb > ub then exit; SetLength(pts, ub - lb + 1); for i := lb to ub do pts[i - lb] := ParentChart.GraphToImage(FGraphPoints[i - FLoBound]); ADrawer.Pen := BadDataPen; ADrawer.Polyline(pts, 0, Length(pts)); end; procedure DrawSpline(ASpline: TSpline); begin ADrawer.SetBrushParams(bsClear, clTAColor); if ASpline.FIsUnorderedX then begin if not IsUnorderedVisible then exit; ADrawer.Pen := BadDataPen; end else begin if not Pen.EffVisible then exit; ADrawer.Pen := Pen; end; with TDrawFuncHelper.Create(Self, ASpline.FIntervals, @ASpline.Calculate, Step) do try DrawFunction(ADrawer); finally Free; end; end; var s: TSpline; begin if IsEmpty then exit; if FSplines = nil then PrepareCoeffs; PrepareGraphPoints(FChart.CurrentExtent, true); for s in FSplines do if not DrawFewPoints(s) then DrawSpline(s); DrawLabels(ADrawer); DrawPointers(ADrawer); end; function TCubicSplineSeries.Extent: TDoubleRect; var r: Integer = 0; minv, maxv: ArbFloat; extY: TDoubleInterval = (FStart: Infinity; FEnd: NegInfinity); s: TSpline; begin Result := inherited Extent; if FSplines = nil then PrepareCoeffs; if FSplines = nil then exit; for s in FSplines do begin if s.IsFewPoints then continue; minv := Result.a.Y; maxv := Result.b.Y; ipfsmm(High(s.FCoeff), s.FX[0], s.FY[0], s.FCoeff[0], minv, maxv, r); extY.FStart := Min(minv, extY.FStart); extY.FEnd := Max(maxv, extY.FEnd); end; Result.a.Y := extY.FStart; Result.b.Y := extY.FEnd; end; procedure TCubicSplineSeries.FreeSplines; var s: TSpline; begin for s in FSplines do s.Free; FSplines := nil; end; procedure TCubicSplineSeries.GetLegendItems(AItems: TChartLegendItems); var cp: TChartPen; p: TSeriesPointer; begin if FPen.Visible and (FPen.Style <> psClear) then cp := FPen else cp := nil; if FPointer.Visible then p := FPointer else p := nil; AItems.Add(TLegendItemLinePointer.Create(cp, p, LegendTextSingle)); end; function TCubicSplineSeries.GetNearestPoint( const AParams: TNearestPointParams; out AResults: TNearestPointResults): Boolean; var s: TSpline; r: TNearestPointResults; begin Result := inherited GetNearestPoint(AParams, AResults); if (not Result) and (nptCustom in ToolTargets) and (nptCustom in AParams.FTargets) then for s in FSplines do begin if s.IsFewPoints or (s.FIsUnorderedX and not IsUnorderedVisible) then continue; with TDrawFuncHelper.Create(Self, s.FIntervals, @s.Calculate, Step) do try if not GetNearestPoint(AParams, r) or Result and (AResults.FDist <= r.FDist) then continue; AResults := r; AResults.FYIndex := -1; Result := true; finally Free; end; end; end; function TCubicSplineSeries.IsFewPointsVisible: Boolean; begin Result := (csoDrawFewPoints in Options) and BadDataPen.EffVisible; end; function TCubicSplineSeries.IsUnorderedVisible: Boolean; begin Result := (csoDrawUnorderedX in Options) and BadDataPen.EffVisible; end; procedure TCubicSplineSeries.PrepareCoeffs; var i: Integer = 0; s: TSpline; begin FreeSplines; while i < Source.Count do begin s := TSpline.Create; if s.PrepareCoeffs(Source, i) then begin s.PrepareIntervals(Options); SetLength(FSplines, Length(FSplines) + 1); FSplines[High(FSplines)] := s; end else s.Free; end; end; procedure TCubicSplineSeries.SetBadDataPen(AValue: TBadDataChartPen); begin if FBadDataPen = AValue then exit; FBadDataPen.Assign(AValue); UpdateParentChart; end; procedure TCubicSplineSeries.SetOptions(AValue: TCubicSplineOptions); begin if FOptions = AValue then exit; FOptions := AValue; FreeSplines; UpdateParentChart; end; procedure TCubicSplineSeries.SetPen(AValue: TChartPen); begin if FPen = AValue then exit; FPen.Assign(AValue); UpdateParentChart; end; procedure TCubicSplineSeries.SetStep(AValue: TFuncSeriesStep); begin if FStep = AValue then exit; FStep := AValue; UpdateParentChart; end; procedure TCubicSplineSeries.SourceChanged(ASender: TObject); begin inherited SourceChanged(ASender); FreeSplines; end; { TFitSeries } procedure TFitSeries.AfterAdd; begin inherited AfterAdd; FFitRange.SetOwner(ParentChart); end; { Calculates the R-squared parameter as a simple measure for the goodness-of-fit. More advanced calculations require the standard deviation of the y values which are not available. Method can be overridden for more advanced calculations. x and y are the first values of arrays containing the transformed values used during fitting. n indicates the number of these value pairs. } function TFitSeries.CalcGoodnessOfFit(var x,y: ArbFloat; n: Integer): Double; type TArbFloatArray = array[0..0] of Arbfloat; var yave, ycalc, SStot, SSres: Double; i, j: Integer; begin {$PUSH} {$R-} yave := 0; for i:=0 to n-1 do yave := yave + TArbFloatArray(y)[i]; yave := yave / n; SStot := 0.0; SSres := 0.0; for i:=0 to n-1 do begin SStot := SStot + sqr(TArbFloatArray(y)[i] - yave); ycalc := 0.0; for j:=High(FFitParams) downto 0 do ycalc := ycalc * TArbFloatArray(x)[i] + FFitParams[j]; SSres := SSres + sqr(TArbFloatArray(y)[i] - ycalc); end; if SStot = 0 then Result := 0.0 else Result := 1.0 - SSres / SStot; {$POP} end; function TFitSeries.Calculate(AX: Double): Double; var i: Integer; begin if IsInfinite(AX) then exit(AX); Result := SafeNaN; if IsNaN(AX) or (State <> fpsValid) then exit; case FFitEquation of fePolynomial, feLinear: begin Result := 0; for i := High(FFitParams) downto 0 do Result := Result * AX + FFitParams[i]; end; feExp: Result := FFitParams[0] * Exp(FFitParams[1] * AX); fePower: if AX < 0 then Result := SafeNaN else Result := FFitParams[0] * Power(AX, FFitParams[1]); end; end; procedure TFitSeries.CalcXRange(out AXMin, AXMax: Double); var ext: TDoubleRect; begin with Extent do begin ext.a := AxisToGraph(a); ext.b := AxisToGraph(b); end; NormalizeRect(ext); AXMin := GraphToAxisX(ext.a.X); AXMax := GraphToAxisX(ext.b.X); EnsureOrder(AXMin, AXMax); FFitRange.Intersect(AXMin, AXMax); end; constructor TFitSeries.Create(AOwner: TComponent); begin inherited Create(AOwner); ToolTargets := [nptPoint, nptCustom]; FFitEquation := fePolynomial; FFitRange := TFitSeriesRange.Create(Self); FDrawFitRangeOnly := true; FPointer := TSeriesPointer.Create(ParentChart); FPointer.Visible := false; FPen := TChartPen.Create; FPen.OnChange := @StyleChanged; FStep := DEF_FIT_STEP; ParamCount := DEF_FIT_PARAM_COUNT; // Parabolic fit as default. FGoodnessOfFit := NaN; end; destructor TFitSeries.Destroy; begin FreeAndNil(FPen); FreeAndNil(FFitRange); inherited; end; procedure TFitSeries.Draw(ADrawer: IChartDrawer); var de : TIntervalList; begin if IsEmpty then exit; ExecFit; if State <> fpsValid then exit; ADrawer.SetBrushParams(bsClear, clTAColor); ADrawer.Pen := Pen; de := PrepareIntervals; try PrepareGraphPoints(FChart.CurrentExtent, true); with TDrawFuncHelper.Create(Self, de, @Calculate, Step) do try DrawFunction(ADrawer); finally Free; end; DrawLabels(ADrawer); DrawPointers(ADrawer); finally de.Free; end; end; function TFitSeries.EquationText: IFitEquationText; begin if State = fpsValid then Result := TFitEquationText.Create else Result := TFitEmptyEquationText.Create; Result.TextFormat(Marks.TextFormat).Equation(FitEquation).Params(FFitParams); end; procedure TFitSeries.ExecFit; var xmin, xmax: Double; function IsValidPoint(AX, AY: Double): Boolean; inline; begin Result := not IsNaN(AX) and not IsNaN(AY) and InRange(AX, xmin, xmax); end; procedure TryFit; var i, j, term, ns, np, n: Integer; xv, yv, fp: array of ArbFloat; begin np := ParamCount; ns := Source.Count; if (np <= 0) or (ns = 0) or (ns < np) then exit; CalcXRange(xmin, xmax); if xmin = xmax then exit; n := 0; for i := 0 to ns - 1 do with Source.Item[i]^ do n += Ord(IsValidPoint(X, Y)); if n < np then exit; // Copy data in fit range to temporary arrays. SetLength(xv, n); SetLength(yv, n); j := 0; for i := 0 to ns - 1 do with Source.Item[i]^ do if IsValidPoint(X, Y) then begin Transform(X, Y, xv[j], yv[j]); j += 1; end; // Execute the polynomial fit; the degree of the polynomial is np - 1. SetLength(fp, np); term := 0; ipfpol(n, np - 1, xv[0], yv[0], fp[0], term); if term <> 1 then exit; for i := 0 to High(FFitParams) do FFitParams[i] := fp[i]; // Calculate goodness-of-fit parameter if Assigned(FOnCalcGoodnessOfFit) then FOnCalcGoodnessOfFit(Self, xv[0], yv[0], Length(yv), FGoodnessOfFit) else FGoodnessOfFit := CalcGoodnessOfFit(xv[0], yv[0], Length(yv)); // See comment for "Transform": for exponential and power fit equations, the // first fitted parameter is the logarithm of the "real" parameter. It needs // to be transformed back to real units by exp function. if FFitEquation in [feExp, fePower] then FFitParams[0] := Exp(FFitParams[0]); FState := fpsValid; end; begin if State <> fpsUnknown then exit; FState := fpsInvalid; try TryFit; finally if Assigned(FOnFitComplete) then FOnFitComplete(Self); UpdateParentChart; end; end; function TFitSeries.GetFitEquationString(ANumFormat: String; AXText: String; AYText: String): String; begin Result := EquationText.NumFormat(ANumFormat).X(AXText).Y(AYText); end; procedure TFitSeries.GetLegendItems(AItems: TChartLegendItems); var cp: TChartPen; p: TSeriesPointer; t: String; begin if FPen.Visible and (FPen.Style <> psClear) then cp := FPen else cp := nil; if FPointer.Visible then p := FPointer else p := nil; if Legend.Format = '' then t := Title else t := Format(Legend.Format, [Title, Index, EquationText.NumFormat('%f').Get]); AItems.Add(TLegendItemLinePointer.Create(cp, p, t)); end; function TFitSeries.GetNearestPoint( const AParams: TNearestPointParams; out AResults: TNearestPointResults): Boolean; var de : TIntervalList; begin Result := inherited GetNearestPoint(AParams, AResults); if (not Result) and (nptCustom in ToolTargets) and (nptCustom in AParams.FTargets) then begin ExecFit; if State <> fpsValid then exit(false); de := PrepareIntervals; try with TDrawFuncHelper.Create(Self, de, @Calculate, Step) do try Result := GetNearestPoint(AParams, AResults); if Result then AResults.FYIndex := -1; finally Free; end; finally de.Free; end; end; end; function TFitSeries.GetParam(AIndex: Integer): Double; begin if not InRange(AIndex, 0, ParamCount - 1) then raise EChartError.Create('TFitSeries.GetParam index out of range'); Result := FFitParams[AIndex] end; function TFitSeries.GetParamCount: Integer; begin Result := Length(FFitParams); end; function TFitSeries.PrepareIntervals: TIntervalList; var xmin, xmax: Double; begin Result := TIntervalList.Create; try CalcXRange(xmin, xmax); if DrawFitRangeOnly then begin Result.AddRange(NegInfinity, xmin); Result.AddRange(xmax, SafeInfinity); end; except Result.Free; raise; end; end; procedure TFitSeries.SetDrawFitRangeOnly(AValue: Boolean); begin if FDrawFitRangeOnly = AValue then exit; FDrawFitRangeOnly := AValue; UpdateParentChart; end; procedure TFitSeries.SetFitEquation(AValue: TFitEquation); begin if FFitEquation = AValue then exit; FFitEquation := AValue; SetLength( FFitParams, IfThen(FFitEquation = fePolynomial, DEF_FIT_PARAM_COUNT, 2)); FState := fpsUnknown; UpdateParentChart; end; procedure TFitSeries.SetFitRange(AValue: TChartRange); begin if FFitRange = AValue then exit; FFitRange := AValue; FState := fpsUnknown; UpdateParentChart; end; procedure TFitSeries.SetParam(AIndex: Integer; AValue: Double); begin if not InRange(AIndex, 0, ParamCount - 1) then raise EChartError.Create('TFitSeries.SetParam index out of range'); FFitParams[AIndex] := AValue; UpdateParentChart; end; procedure TFitSeries.SetParamCount(AValue: Integer); begin if (AValue = ParamCount) or (FFitEquation <> fePolynomial) then exit; SetLength(FFitParams, AValue); FState := fpsUnknown; UpdateParentChart; end; procedure TFitSeries.SetPen(AValue: TChartPen); begin if FPen = AValue then exit; FPen.Assign(AValue); UpdateParentChart; end; procedure TFitSeries.SetStep(AValue: TFuncSeriesStep); begin if FStep = AValue then exit; FStep := AValue; UpdateParentChart; end; procedure TFitSeries.SourceChanged(ASender: TObject); begin inherited; FState := fpsUnknown; end; procedure TFitSeries.Transform(AX, AY: Double; out ANewX, ANewY: Extended); begin // The exponential and power fitting equations can be transformed to a // polynomial by taking the logarithm: // feExp: y = a exp(b*x) ==> ln(y) = ln(a) + b*x // fePower: y = a*x^b ==> ln(y) = ln(a) + b*ln(x) // In each case, the first parameter (a) needs to be transformed back // after the fitting -- see "ExecFit". if FitEquation in [fePower] then ANewX := Ln(AX) else ANewX := AX; if FitEquation in [feExp, fePower] then ANewY := Ln(AY) else ANewY := AY; end; { TCustomColorMapSeries } procedure TCustomColorMapSeries.Assign(ASource: TPersistent); begin if ASource is TCustomColorMapSeries then with TCustomColorMapSeries(ASource) do begin Self.AutoMapColors := FAutoMapColors; Self.Brush := FBrush; Self.BuiltinPalette := FBuiltinPalette; Self.ColorSource := FColorSource; Self.FInterpolate := FInterpolate; Self.FStepX := FStepX; Self.FStepY := FStepY; end; inherited Assign(ASource); end; procedure TCustomColorMapSeries.BuildPalette(APalette: TColorMapPalette); var i: Integer; h,s,l: Byte; begin with FBuiltinColorSource as TListChartSource do begin BeginUpdate; try Clear; case APalette of cmpHot: begin Add(0, 0, '', clBlack); Add(1/3, 0, '', clRed); Add(2/3, 0, '', clYellow); Add(1, 0, '', clWhite); end; cmpCold: begin ColorToHLS(clBlue, h, l, s); i := 0; while i <= 255 do begin Add(i, 0, '', HLSToColor(h, i, s)); inc(i, 32); end; Add(255, 0, '', clWhite); end; cmpRainbow: begin i := 0; while i <= 255 do begin // i is hue Add(i, 0, '', HLSToColor(i, 128, 255)); inc(i, 32); end; Add(255, 0, '', HLSToColor(255, 128, 255)); end; cmpMonochrome: begin i := 0; while i <= 255 do begin Add(i, 0, '', RgbToColor(i, i, i)); inc(i, 32); end; Add(255, 0, '', clWhite); end; else raise Exception.Create('Palette not supported'); end; finally EndUpdate; end; end; end; function TCustomColorMapSeries.ColorByValue(AValue: Double): TColor; var lb, ub: Integer; c1, c2: TColor; v1, v2: Double; begin if (ColorSource = nil) or (ColorSource.Count = 0) then exit(clTAColor); if FAutoMapColors then begin // Transform data value to the values assigned to the colorsource if FMinZ <> FMaxZ then begin AValue := (AValue - FMinZ) / (FMaxZ - FMinZ); AValue := AValue * (FColorExtentMax - FColorExtentMin) + FColorExtentMin; end else AValue := FColorExtentMin; end; ColorSource.FindBounds(AValue, SafeInfinity, lb, ub); if Interpolate and InRange(lb, 1, ColorSource.Count - 1) then begin with ColorSource[lb - 1]^ do begin v1 := X; c1 := Color; end; with ColorSource[lb]^ do begin v2 := X; c2 := Color; end; if v2 <= v1 then Result := c1 else Result := InterpolateRGB(c1, c2, (AValue - v1) / (v2 - v1)); end else Result := ColorSource[EnsureRange(lb, 0, ColorSource.Count - 1)]^.Color; end; constructor TCustomColorMapSeries.Create(AOwner: TComponent); const BUILTIN_SOURCE_NAME = 'BuiltinColors'; begin inherited Create(AOwner); FColorSourceListener := TListener.Create(@FColorSource, @StyleChanged); FBuiltinColorSource := TListChartSource.Create(self); FBuiltinColorSource.Name := BUILTIN_SOURCE_NAME; FBuiltinColorSource.Broadcaster.Subscribe(FColorSourceListener); FBrush := TBrush.Create; FBrush.OnChange := @StyleChanged; FStepX := DEF_COLORMAP_STEP; FStepY := DEF_COLORMAP_STEP; SetBuiltinPalette(cmpHot); end; destructor TCustomColorMapSeries.Destroy; begin FreeAndNil(FBrush); FreeAndNil(FBuiltinColorSource); FreeAndNil(FColorSourceListener); inherited Destroy; end; procedure TCustomColorMapSeries.Draw(ADrawer: IChartDrawer); var ext: TDoubleRect; bounds: TDoubleRect; r, cell: TRect; pt, next, offset: TPoint; gp: TDoublePoint; v: Double; img: TLazIntfImage = nil; rawImage: TRawImage; optimize: Boolean; x, y: Integer; cellColor: TChartColor; scaled_stepX: Integer; scaled_stepY: Integer; begin if not (csDesigning in ComponentState) and IsEmpty then exit; ext := ParentChart.CurrentExtent; bounds := EmptyExtent; GetBounds(bounds); bounds.a := AxisToGraph(bounds.a); bounds.b := AxisToGraph(bounds.b); if not RectIntersectsRect(ext, bounds) then exit; r.TopLeft := ParentChart.GraphToImage(ext.a); r.BottomRight := ParentChart.GraphToImage(ext.b); NormalizeRect(r); offset := ParentChart.GraphToImage(ZeroDoublePoint); case UseImage of cmuiAuto: optimize := (StepX <= 2) and (StepY <= 2); cmuiAlways: optimize := true; cmuiNever: optimize := false; end; if optimize then img := CreateLazIntfImage(rawImage, r.BottomRight - r.TopLeft) else begin ADrawer.Brush := Brush; ADrawer.SetPenParams(psClear, clTAColor); end; scaled_stepX := IfThen(StepX > 1, Max(1, ADrawer.Scale(StepX)), 1); scaled_stepY := IfThen(StepY > 1, Max(1, ADrawer.Scale(StepY)), 1); GetZRange(r, scaled_stepX, scaled_stepY); try pt.Y := (r.Top div scaled_stepY - 1) * scaled_stepY + offset.Y mod scaled_stepY; while pt.Y <= r.Bottom do begin next.Y := pt.Y + scaled_stepY; if next.Y <= r.Top then begin pt.Y := next.Y; continue; end; pt.X := (r.Left div scaled_stepX - 1) * scaled_stepX + offset.X mod scaled_stepX; while pt.X <= r.Right do begin next.X := pt.X + scaled_stepX; if next.X <= r.Left then begin pt.X := next.X; continue; end; gp := GraphToAxis(ParentChart.ImageToGraph((pt + next) div 2)); // if not (csDesigning in ComponentState) then v := FunctionValue(gp.X, gp.Y); cell := Rect( Max(pt.X, r.Left), Max(pt.Y, r.Top), Min(next.X, r.Right) + 1, Min(next.Y, r.Bottom) + 1); if optimize then begin if ColorSource = nil then cellColor := Brush.Color else cellColor := ColorByValue(v); for y := cell.Top - r.Top to cell.Bottom - r.Top - 2 do for x := cell.Left - r.Left to cell.Right - r.Left - 2 do img.TColors[x, y] := cellColor; end else begin if ColorSource <> nil then ADrawer.BrushColor := ColorByValue(v); ADrawer.Rectangle(cell); end; pt.X := next.X; end; pt.Y := next.Y; end; if optimize then ADrawer.PutImage(r.Left, r.Top, img); finally FreeAndNil(img); end; end; function TCustomColorMapSeries.FunctionValue(AX, AY: Double): Double; begin Unused(AX, AY); Result := 0.0; end; function TCustomColorMapSeries.GetColorSource: TCustomChartSource; begin if Assigned(FColorSource) then Result := FColorSource else Result := FBuiltinColorSource; end; procedure TCustomColorMapSeries.GetLegendItems(AItems: TChartLegendItems); function PrepareFormats: TStrings; begin Result := Split(IfThen(Legend.Format = '', DEF_COLORMAP_LEGENDFORMAT, Legend.Format)); with Result do try while Count < 3 do Add(Strings[Count - 1]); except Result.Free; raise; end; end; var prev: Double; formats: TStrings; function ItemTitle(AIndex: Integer; const AText: String; AX: Double): String; var idx: Integer; begin if AText <> '' then exit(AText); if ColorSource.Count = 1 then exit(''); if AIndex = 0 then idx := 0 else if AIndex = ColorSource.Count - 1 then idx := 2 else idx := 1; Result := Format(formats[idx], [prev, AX]); end; procedure MakePointItems; var t: String; prevColor: TColor; li: TLegendItem; i: Integer; begin prev := ColorSource[0]^.X; prevColor := clTAColor; formats := PrepareFormats; try for i := 0 to ColorSource.Count - 1 do with ColorSource[i]^ do begin t := ItemTitle(i, Text, X); if Interpolate then li := TLegendItemColorMap.Create( ColorDef(prevColor, Color), Color, ParentChart.Legend.SymbolFrame, t) else begin li := TLegendItemBrushRect.Create(Brush, t); li.Color := Color; end; AItems.Add(li); prev := X; prevColor := Color; end; finally formats.Free; end; end; begin case Legend.Multiplicity of lmSingle: AItems.Add(TLegendItemBrushRect.Create(Brush, LegendTextSingle)); lmPoint: if (ColorSource <> nil) and (ColorSource.Count > 0) then MakePointItems; end; end; procedure TCustomColorMapSeries.GetZRange(ARect: TRect; dx, dy: Integer); var gp: TDoublePoint; ix, iy: Integer; z: Double; dx2, dy2: Double; begin if IsEmpty then begin FMinZ := 0.0; FMaxZ := 0.0; exit; end; dx2 := dx div 2; dy2 := dy div 2; FMinZ := 1E308; FMaxZ := -FMinZ; iy := ARect.Top; try while iy <= ARect.Bottom - dy2 do begin ix := ARect.Left; while ix < ARect.Right - dx2 do begin gp := ParentChart.ImageToGraph(Point(ix, iy)); z := FunctionValue(gp.X + dx2, gp.Y + dy2); FMinZ := Min(FMinZ, z); FMaxZ := Max(FMaxZ, z); inc(ix, dx); end; inc(iy, dy); end; except FActive := false; raise; end; end; function TCustomColorMapSeries.IsEmpty: Boolean; begin Result := true; end; function TCustomColorMapSeries.IsColorSourceStored: boolean; begin Result := FColorSource <> nil; end; procedure TCustomColorMapSeries.SetAutoMapColors(AValue: Boolean); begin if FAutoMapColors = AValue then exit; FAutoMapColors := AValue; UpdateParentChart; end; procedure TCustomColorMapSeries.SetBrush(AValue: TBrush); begin if FBrush = AValue then exit; FBrush := AValue; UpdateParentChart; end; procedure TCustomColorMapSeries.SetBuiltinPalette(AValue: TColorMapPalette); var ex: TDoubleRect; begin // if FBuiltinPalette = AValue then exit; FBuiltinPalette := AValue; BuildPalette(FBuiltinPalette); if FColorSource = nil then begin ex := FBuiltinColorSource.Extent; FColorExtentMin := ex.a.x; FColorExtentMax := ex.b.x; UpdateParentChart; end; end; procedure TCustomColorMapSeries.SetColorSource(AValue: TCustomChartSource); var ex: TDoubleRect; begin if FColorSource = AValue then exit; if FColorSourceListener.IsListening then ColorSource.Broadcaster.Unsubscribe(FColorSourceListener); FColorSource := AValue; ColorSource.Broadcaster.Subscribe(FColorSourceListener); ex := ColorSource.Extent; FColorExtentMin := ex.a.x; FColorExtentMax := ex.b.x; UpdateParentChart; end; procedure TCustomColorMapSeries.SetInterpolate(AValue: Boolean); begin if FInterpolate = AValue then exit; FInterpolate := AValue; UpdateParentChart; end; procedure TCustomColorMapSeries.SetStepX(AValue: TFuncSeriesStep); begin if FStepX = AValue then exit; FStepX := AValue; UpdateParentChart; end; procedure TCustomColorMapSeries.SetStepY(AValue: TFuncSeriesStep); begin if FStepY = AValue then exit; FStepY := AValue; UpdateParentChart; end; procedure TCustomColorMapSeries.SetUseImage(AValue: TUseImage); begin if FUseImage = AValue then exit; FUseImage := AValue; UpdateParentChart; end; { TColorMapSeries } procedure TColorMapSeries.Assign(ASource: TPersistent); begin if ASource is TColorMapSeries then with TCustomColorMapSeries(ASource) do begin Self.FOnCalculate := FOnCalculate; end; inherited; end; function TColorMapSeries.FunctionValue(AX, AY: Double): Double; begin if (csDesigning in ComponentState) or not Assigned(FOnCalculate) then Result := 0 else OnCalculate(AX, AY, Result); end; function TColorMapSeries.IsEmpty: Boolean; begin Result := not Assigned(OnCalculate); end; procedure TColorMapSeries.SetOnCalculate(AValue: TFuncCalculate3DEvent); begin if TMethod(FOnCalculate) = TMethod(AValue) then exit; FOnCalculate := AValue; UpdateParentChart; end; initialization RegisterSeriesClass(TParametricCurveSeries, @rsParametricCurveSeries); RegisterSeriesClass(TBSplineSeries, @rsBSplineSeries); RegisterSeriesClass(TCubicSplineSeries, @rsCubicSplineSeries); RegisterSeriesClass(TFitSeries, @rsLeastSquaresFitSeries); RegisterSeriesClass(TFuncSeries, @rsFunctionSeries); RegisterSeriesClass(TColorMapSeries, @rsColorMapSeries); end.