unit TestChartControl;

interface

uses
    System.SysUtils,
    System.Types,
    System.UITypes,
    System.Classes,
    System.Variants,
    System.Generics.Collections,
    System.Math,
    System.UIConsts,
    System.TimeSpan,
    FMX.Types,
    FMX.Controls,
    FMX.Forms,
    FMX.Graphics,
    FMX.Dialogs,
    FMX.Objects,
    Myc.Signals,
    Myc.Futures,
    Myc.Trade.DataPoint,
    Myc.Signals.FMX;

type
    TPointD = record
    public
        X: Double;
        Y: Double;
        constructor Create(AX, AY: Double);
        procedure Offset(DX, DY: Double);
    end;

    TRectD = record
    private
        function GetHeight: Double;
        function GetWidth: Double;
    public
        Left, Top, Right, Bottom: Double;
        constructor Create(const ALeft, ATop, ARight, ABottom: Double); overload;
        constructor Create(const ATopLeft, ABottomRight: TPointD); overload;
        procedure Offset(DX, DY: Double);
        procedure ExpandBy(const pt: TPointD);
        property Width: Double read GetWidth;
        property Height: Double read GetHeight;
    end;

    TRangeD = record
        A: Double;
        B: Double;
    public
        constructor Create(const AA, AB: Double);
    end;

    TChartZoomMode = (czmXY, czmXOnly);
    TChart = class; // Forward declaration

    TChartSeries = class abstract(TCollectionItem)
    private
        FColor: TAlphaColor;
        FThickness: Single;
        procedure SetColor(const Value: TAlphaColor);
        procedure SetThickness(const Value: Single);
    protected
        function GetChart: TChart;
    public
        constructor Create(Collection: TCollection); override;
        function GetBounds: TFuture<TRectD>; virtual; abstract;
        function GetYBoundsForXRange(const XRange: TRangeD): TFuture<TRangeD>; virtual; abstract;
        procedure Draw(const ACanvas: TCanvas; const ADataRect: TRectD; const ACanvasRect: TRectF); virtual; abstract;
    published
        property Color: TAlphaColor read FColor write SetColor;
        property Thickness: Single read FThickness write SetThickness;
    end;

    TChartSeriesAdapter<T> = class abstract(TChartSeries)
    private
        FDataSeries: TFuture<TDataSeries<T>>;
    protected
        procedure SetDataSeries(const Value: TFuture<TDataSeries<T>>); virtual;
        function DataToPoint(const DataPoint: TDataPoint<T>): TPointD; virtual; abstract;
    public
        constructor Create(Collection: TCollection); override;
        property DataSeries: TFuture<TDataSeries<T>> read FDataSeries write SetDataSeries;
    end;

    // A generic series to plot a TDataSeries<Single>
    TChartLineSeries = class(TChartSeriesAdapter<Double>)
    private
        FBounds: TFuture<TRectD>;
    protected
        procedure SetDataSeries(const Value: TFuture<TDataSeries<Double>>); override;
        function DataToPoint(const DataPoint: TDataPoint<Double>): TPointD; override;
    public
        constructor Create(Collection: TCollection); override;
        function GetBounds: TFuture<TRectD>; override;
        function GetYBoundsForXRange(const XRange: TRangeD): TFuture<TRangeD>; override;
        procedure Draw(const ACanvas: TCanvas; const ADataRect: TRectD; const ACanvasRect: TRectF); override;
    end;

    TChartOhlcSeries = class(TChartSeriesAdapter<TOhlcItem>)
    private
        FUpColor: TAlphaColor;
        FDownColor: TAlphaColor;
        FCandleWidth: Double;
        FTimeInterval: TFuture<Double>;
        FBounds: TFuture<TRectD>;
    protected
        procedure SetDataSeries(const Value: TFuture<TDataSeries<TOhlcItem>>); override;
        function DataToPoint(const DataPoint: TDataPoint<TOhlcItem>): TPointD; override;
    public
        constructor Create(Collection: TCollection); override;
        function GetBounds: TFuture<TRectD>; override;
        function GetYBoundsForXRange(const XRange: TRangeD): TFuture<TRangeD>; override;
        procedure Draw(const ACanvas: TCanvas; const ADataRect: TRectD; const ACanvasRect: TRectF); override;
    published
        property UpColor: TAlphaColor read FUpColor write FUpColor;
        property DownColor: TAlphaColor read FDownColor write FDownColor;
        property CandleWidth: Double read FCandleWidth write FCandleWidth;
    end;

    TChartSeriesCollection = class(TCollection)
    private
        [weak]
        FOwner: TChart;
        function GetItem(Index: Integer): TChartSeries;
        procedure SetItem(Index: Integer; const Value: TChartSeries);
    protected
        function GetOwner: TPersistent; override;
        procedure Update(Item: TCollectionItem); override;
    public
        constructor Create(AOwner: TChart);
        property Items[Index: Integer]: TChartSeries read GetItem write SetItem; default;
    end;

    TChart = class(TControl)
    private
        FSeries: TChartSeriesCollection;
        FAxisColor: TAlphaColor;
        FGridColor: TAlphaColor;
        FPadding: Single;
        FDataBounds: TFuture<TRectD>;
        FYRange: TFuture<TRangeD>;
        FDataView: TRectD;
        FBoundsValid: Boolean;
        FIsPanning: Boolean;
        FPanStartPoint: TPointF;
        FZoomMode: TChartZoomMode;
        procedure SetSeries(const Value: TChartSeriesCollection);
        procedure SetAxisColor(const Value: TAlphaColor);
        procedure SetGridColor(const Value: TAlphaColor);
        procedure SetPadding(const Value: Single);
        procedure RecalcDataBounds;
    protected
        procedure Paint; override;
        procedure DrawAxes(const ACanvas: TCanvas; const ADataRect: TRectD; const ACanvasRect: TRectF);
        procedure DrawGrid(const ACanvas: TCanvas; const ADataRect: TRectD; const ACanvasRect: TRectF);
        procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override;
        procedure MouseMove(Shift: TShiftState; X, Y: Single); override;
        procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override;
        procedure MouseWheel(Shift: TShiftState; WheelDelta: Integer; var Handled: Boolean); override;
        procedure DblClick; override;
        procedure InvalidateDataBounds;
        procedure InvalidateYRange;
    public
        constructor Create(AOwner: TComponent); override;
        destructor Destroy; override;
        procedure BeforeDestruction; override;
        procedure ResetView;
    published
        property Align;
        property Anchors;
        property Series: TChartSeriesCollection read FSeries write SetSeries;
        property AxisColor: TAlphaColor read FAxisColor write SetAxisColor;
        property GridColor: TAlphaColor read FGridColor write SetGridColor;
        property Padding: Single read FPadding write SetPadding;
        property ZoomMode: TChartZoomMode read FZoomMode write FZoomMode;
    end;

    TTestChartForm = class(TForm)
        procedure FormCreate(Sender: TObject);
    private
        FChart: TChart;
    public
    end;

var
    TestChartForm: TTestChartForm;

implementation

{$R *.fmx}

{ TPointD }

constructor TPointD.Create(AX, AY: Double);
begin
    X := AX;
    Y := AY;
end;

procedure TPointD.Offset(DX, DY: Double);
begin
    X := X + DX;
    Y := Y + DY;
end;

{ TRectD }

constructor TRectD.Create(const ALeft, ATop, ARight, ABottom: Double);
begin
    Left := ALeft;
    Top := ATop;
    Right := ARight;
    Bottom := ABottom;
end;

constructor TRectD.Create(const ATopLeft, ABottomRight: TPointD);
begin
    Left := ATopLeft.X;
    Top := ATopLeft.Y;
    Right := ABottomRight.X;
    Bottom := ABottomRight.Y;
end;

procedure TRectD.ExpandBy(const pt: TPointD);
begin
    if pt.X < Left then
        Left := pt.X;
    if pt.X > Right then
        Right := pt.X;
    if pt.Y < Top then
        Top := pt.Y;
    if pt.Y > Bottom then
        Bottom := pt.Y;
end;

procedure TRectD.Offset(DX, DY: Double);
begin
    Left := Left + DX;
    Right := Right + DX;
    Top := Top + DY;
    Bottom := Bottom + DY;
end;

function TRectD.GetHeight: Double;
begin
    Result := Bottom - Top;
end;

function TRectD.GetWidth: Double;
begin
    Result := Right - Left;
end;

{ TChartSeries }

constructor TChartSeries.Create(Collection: TCollection);
begin
    inherited Create(Collection);
    FColor := TAlphaColors.Black;
    FThickness := 1;
end;

function TChartSeries.GetChart: TChart;
begin
    Result := (Collection as TChartSeriesCollection).GetOwner as TChart;
end;

procedure TChartSeries.SetColor(const Value: TAlphaColor);
begin
    if (FColor <> Value) then
    begin
        FColor := Value;
        if Assigned(Collection) then
            Changed(False);
    end;
end;

procedure TChartSeries.SetThickness(const Value: Single);
begin
    if (FThickness <> Value) then
    begin
        FThickness := Value;
        if Assigned(Collection) then
            Changed(False);
    end;
end;

{ TChartSeriesAdapter<T> }

constructor TChartSeriesAdapter<T>.Create(Collection: TCollection);
begin
    inherited Create(Collection);
end;

procedure TChartSeriesAdapter<T>.SetDataSeries(const Value: TFuture<TDataSeries<T>>);
begin
    FDataSeries := Value;
    if Assigned(Collection) then
        Changed(False);
end;

{ TChartLineSeries }

constructor TChartLineSeries.Create(Collection: TCollection);
begin
    inherited Create(Collection);
    Self.Color := TAlphaColors.Green;
    Self.Thickness := 1;
end;

procedure TChartLineSeries.SetDataSeries(const Value: TFuture<TDataSeries<Double>>);
begin
    // Future<Array of Points> -> Future<Bounds Rectangle>
    FBounds :=
        Value.Chain<TRectD>(
            function(const Data: TDataSeries<Double>): TRectD
            begin
                if Data.Count = 0 then
                    Exit(TRectD.Create(0, 0, 0, 0));

                Result := TRectD.Create(Data[0].Time, Data[0].Data, Data[0].Time, Data[0].Data);
                for var i := 1 to Data.Count - 1 do
                    Result.ExpandBy(TPointD.Create(Data[i].Time, Data[i].Data));
            end
        );

    inherited SetDataSeries(Value);
end;

function TChartLineSeries.DataToPoint(const DataPoint: TDataPoint<Double>): TPointD;
begin
    Result.X := DataPoint.Time;
    Result.Y := DataPoint.Data;
end;

function TChartLineSeries.GetBounds: TFuture<TRectD>;
begin
    Result := FBounds;
end;

function TChartLineSeries.GetYBoundsForXRange(const XRange: TRangeD): TFuture<TRangeD>;
begin
    Result :=
        DataSeries.Chain<TRangeD>(
            function(const Data: TDataSeries<Double>): TRangeD
            begin
                Result.A := Infinity;
                Result.B := NegInfinity;

                var n := Data.IndexOf(XRange.A);
                for var i := n to Data.Count - 1 do
                begin
                    var dp := Data[i];
                    var pt := TPointD.Create(dp.Time, dp.Data);
                    //                    if (pt.X >= XRange.A) and (pt.X <= XRange.B) then
                    //                    begin
                    //                        Result.A := System.Math.Min(Result.A, pt.Y);
                    //                        Result.B := System.Math.Max(Result.B, pt.Y);
                    //                    end;
                    if pt.X > XRange.B then
                        break;
                    Result.A := System.Math.Min(Result.A, pt.Y);
                    Result.B := System.Math.Max(Result.B, pt.Y);
                end;
                if IsInfinite(Result.A) then
                begin
                    Result.A := 0;
                    Result.B := -1;
                end;
            end
        );
end;

procedure TChartLineSeries.Draw(const ACanvas: TCanvas; const ADataRect: TRectD; const ACanvasRect: TRectF);
var
    scaleX, scaleY: Double;
    transformedPt1, transformedPt2: TPointF;
    j: Integer;
begin
    if not DataSeries.Done.IsSet then
        exit;
    var points := DataSeries.Value;

    if points.Count < 2 then
        Exit;

    if (ADataRect.Width <= 0) or (ADataRect.Height <= 0) then
        Exit;

    scaleX := ACanvasRect.Width / ADataRect.Width;
    scaleY := ACanvasRect.Height / ADataRect.Height;

    ACanvas.Stroke.Kind := TBrushKind.Solid;
    ACanvas.Stroke.Color := Self.Color;
    ACanvas.Stroke.Thickness := Self.Thickness;

    var dp1 := points[0];
    var dp2 := dp1;
    for j := 1 to points.Count - 1 do
    begin
        dp2 := points[j];

        transformedPt1.X := ACanvasRect.Left + (dp1.Time - ADataRect.Left) * scaleX;
        transformedPt1.Y := ACanvasRect.Bottom - (dp1.Data - ADataRect.Top) * scaleY;
        transformedPt2.X := ACanvasRect.Left + (dp2.Time - ADataRect.Left) * scaleX;
        transformedPt2.Y := ACanvasRect.Bottom - (dp2.Data - ADataRect.Top) * scaleY;

        ACanvas.DrawLine(transformedPt1, transformedPt2, 1);
        dp1 := dp2;
    end;
end;

{ TChartOhlcSeries }

constructor TChartOhlcSeries.Create(Collection: TCollection);
begin
    inherited Create(Collection);
    FUpColor := TAlphaColors.Green;
    FDownColor := TAlphaColors.Red;
    FCandleWidth := 0.8;
    FTimeInterval := TFuture<Double>.Construct(1.0);
end;

procedure TChartOhlcSeries.SetDataSeries(const Value: TFuture<TDataSeries<TOhlcItem>>);
begin
    FTimeInterval :=
        Value.Chain<Double>(
            function(const DataSeries: TDataSeries<TOhlcItem>): Double
            var
                i: Int64;
                diff: Double;
            begin
                Result := 1.0;
                if DataSeries.Count < 2 then
                    Exit;

                Result := MaxDouble;
                for i := 0 to DataSeries.Count - 2 do
                begin
                    diff := Abs(DataSeries[i].Time - DataSeries[i + 1].Time);
                    if (diff > 1E-9) and (diff < Result) then
                        Result := diff;
                end;

                if (Result = MaxDouble) then
                    Result := 1.0;
            end
        );

    FBounds :=
        Value.Chain<TRectD>(
            function(const DataSeries: TDataSeries<TOhlcItem>): TRectD
            var
                i: Int64;
            begin
                if (DataSeries.Count = 0) then
                    Exit(TRectD.Create(0, 0, 0, 0));

                Result := TRectD.Create(DataSeries[0].Time, DataSeries[0].Data.High, DataSeries[0].Time, DataSeries[0].Data.Low);
                for i := 0 to DataSeries.Count - 1 do
                begin
                    Result.ExpandBy(TPointD.Create(DataSeries[i].Time, DataSeries[i].Data.High));
                    Result.ExpandBy(TPointD.Create(DataSeries[i].Time, DataSeries[i].Data.Low));
                end;
            end
        );

    inherited SetDataSeries(Value);
end;

function TChartOhlcSeries.DataToPoint(const DataPoint: TDataPoint<TOhlcItem>): TPointD;
begin
    Result.X := DataPoint.Time;
    Result.Y := DataPoint.Data.Close;
end;

function TChartOhlcSeries.GetBounds: TFuture<TRectD>;
begin
    Result := FBounds;
end;

function TChartOhlcSeries.GetYBoundsForXRange(const XRange: TRangeD): TFuture<TRangeD>;
begin
    Result :=
        DataSeries.Chain<TRangeD>(
            function(const Data: TDataSeries<TOhlcItem>): TRangeD
            var
                i: Int64;
                dataPoint: TDataPoint<TOhlcItem>;
            begin
                Result.A := Infinity;
                Result.B := NegInfinity;

                var n := Data.IndexOf(XRange.A);

                if n >= 0 then
                    for i := 1 to Data.Count - 2 do
                    begin
                        dataPoint := Data[i];
                        var t := Double(dataPoint.Time);
                        var t2 := Double(Data[i + 1].Time);

                        if ((t >= XRange.A) and (t <= XRange.B)) or ((t2 >= XRange.A) and (t2 <= XRange.B)) then
                        begin
                            Result.A := System.Math.Min(Result.A, dataPoint.Data.Low);
                            Result.B := System.Math.Max(Result.B, dataPoint.Data.High);
                        end;
                    end;
                if IsInfinite(Result.A) then
                begin
                    Result.A := 0;
                    Result.B := -1;
                end;
            end
        );
end;

procedure TChartOhlcSeries.Draw(const ACanvas: TCanvas; const ADataRect: TRectD; const ACanvasRect: TRectF);
var
    scaleX, scaleY: Double;
    i: Int64;
    dataPoint: TDataPoint<TOhlcItem>;
    x_center, pixelCandleWidth, pixelCandleIntervalWidth: Single;
    y_high, y_low, y_open, y_close: Single;
    body: TRectF;
    data: TDataSeries<TOhlcItem>;
    timeInterval: Double;
begin
    if (not DataSeries.Done.IsSet) or (not FTimeInterval.Done.IsSet) then
        exit;
    data := DataSeries.Value;
    timeInterval := FTimeInterval.Value;

    if (data.Count = 0) or (ADataRect.Width <= 0) or (ADataRect.Height <= 0) then
        Exit;

    scaleX := ACanvasRect.Width / ADataRect.Width;
    scaleY := ACanvasRect.Height / ADataRect.Height;

    pixelCandleIntervalWidth := timeInterval * scaleX;
    pixelCandleWidth := System.Math.Max(1.0, FCandleWidth * pixelCandleIntervalWidth);

    for i := data.Count - 1 downto 0 do
    begin
        dataPoint := data[i];

        if ((dataPoint.Time + timeInterval) < ADataRect.Left) or ((dataPoint.Time - timeInterval) > ADataRect.Right) then
            continue;

        x_center := ACanvasRect.Left + (dataPoint.Time - ADataRect.Left) * scaleX;
        y_high := ACanvasRect.Bottom - (dataPoint.Data.High - ADataRect.Top) * scaleY;
        y_low := ACanvasRect.Bottom - (dataPoint.Data.Low - ADataRect.Top) * scaleY;
        y_open := ACanvasRect.Bottom - (dataPoint.Data.Open - ADataRect.Top) * scaleY;
        y_close := ACanvasRect.Bottom - (dataPoint.Data.Close - ADataRect.Top) * scaleY;

        ACanvas.Stroke.Color := Self.Color;
        ACanvas.Stroke.Thickness := Self.Thickness;
        ACanvas.DrawLine(TPointF.Create(x_center, y_high), TPointF.Create(x_center, y_low), 1.0);

        if (dataPoint.Data.Close >= dataPoint.Data.Open) then
            ACanvas.Fill.Color := FUpColor
        else
            ACanvas.Fill.Color := FDownColor;

        body := TRectF.Create(x_center - pixelCandleWidth / 2, y_open, x_center + pixelCandleWidth / 2, y_close);
        NormalizeRect(body);
        ACanvas.FillRect(body, 0, 0, [], 1.0);
    end;
end;

{ TChartSeriesCollection }

constructor TChartSeriesCollection.Create(AOwner: TChart);
begin
    inherited Create(TChartSeries);
    FOwner := AOwner;
end;

function TChartSeriesCollection.GetItem(Index: Integer): TChartSeries;
begin
    Result := TChartSeries(inherited GetItem(Index));
end;

function TChartSeriesCollection.GetOwner: TPersistent;
begin
    Result := FOwner;
end;

procedure TChartSeriesCollection.SetItem(Index: Integer; const Value: TChartSeries);
begin
    inherited SetItem(Index, Value);
end;

procedure TChartSeriesCollection.Update(Item: TCollectionItem);
begin
    inherited;
    if Assigned(FOwner) then
        FOwner.InvalidateDataBounds;
end;

{ TChart }

constructor TChart.Create(AOwner: TComponent);
begin
    inherited Create(AOwner);
    FSeries := TChartSeriesCollection.Create(Self);
    FAxisColor := TAlphaColors.Black;
    FGridColor := TAlphaColors.Lightgray;
    FPadding := 30;
    FIsPanning := False;
    FBoundsValid := False;
    FZoomMode := TChartZoomMode.czmXOnly;
    // Initialize futures with default empty values
    FDataBounds := TFuture<TRectD>.Construct(TRectD.Create(0, 0, 0, 0));
    FDataView := TRectD.Create(0, 0, 0, 0);
end;

destructor TChart.Destroy;
begin
    FSeries.Free;
    inherited Destroy;
end;

procedure TChart.BeforeDestruction;
begin
    FYRange.WaitFor;
    FDataBounds.WaitFor;
    inherited;
end;

procedure TChart.InvalidateDataBounds;
begin
    if FBoundsValid then
        InvalidateYRange;
    FBoundsValid := False;

    Repaint;
end;

procedure TChart.ResetView;
begin
    if not FBoundsValid then
        RecalcDataBounds;

    // The view is reset to the full data bounds.
    FDataView := FDataBounds.WaitFor;
    InvalidateYRange;

    Repaint;
end;

procedure TChart.DblClick;
begin
    inherited;
    ResetView;
end;

procedure TChart.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin
    inherited;
    if Button = TMouseButton.mbLeft then
    begin
        FIsPanning := True;
        FPanStartPoint := TPointF.Create(X, Y);
        Cursor := crHandPoint;
    end;
end;

procedure TChart.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin
    inherited;
    if FIsPanning and (Button = TMouseButton.mbLeft) then
    begin
        FIsPanning := False;
        Cursor := crDefault;
    end;
end;

procedure TChart.MouseMove(Shift: TShiftState; X, Y: Single);
var
    canvasRect: TRectF;
    currentPoint, delta: TPointF;
    dataPerPixelX, dataPerPixelY: Double;
    dataShift: TPointD;
begin
    inherited;
    if not FIsPanning then
        Exit;

    canvasRect := Self.LocalRect;
    canvasRect.Inflate(-FPadding, -FPadding);

    if (canvasRect.Width < 1) or (canvasRect.Height < 1) or (FDataView.Width <= 0) or (FDataView.Height <= 0) then
        Exit;

    currentPoint := TPointF.Create(X, Y);
    delta := TPointF.Create(currentPoint.X - FPanStartPoint.X, currentPoint.Y - FPanStartPoint.Y);

    if (delta.X = 0) and (delta.Y = 0) then
        Exit;

    dataPerPixelX := FDataView.Width / canvasRect.Width;
    dataPerPixelY := FDataView.Height / canvasRect.Height;
    dataShift.X := delta.X * dataPerPixelX;
    dataShift.Y := -delta.Y * dataPerPixelY;

    FDataView.Offset(-dataShift.X, -dataShift.Y);
    FPanStartPoint := currentPoint;

    InvalidateYRange;
    Repaint;
end;

procedure TChart.MouseWheel(Shift: TShiftState; WheelDelta: Integer; var Handled: Boolean);
var
    zoomFactor: Double;
    canvasRect: TRectF;
    mouseScreenPos: TPointF;
    focalPoint: TPointD;
    newWidth, newHeight: Double;
    leftRatio, topRatio: Double;
begin
    inherited;
    if WheelDelta = 0 then
        Exit;

    canvasRect := Self.LocalRect;
    canvasRect.Inflate(-FPadding, -FPadding);
    if (canvasRect.Width < 1) or (canvasRect.Height < 1) or (FDataView.Width <= 0) then
        Exit;

    if WheelDelta > 0 then
        zoomFactor := 1 / 1.25
    else
        zoomFactor := 1.25;

    mouseScreenPos := Self.ScreenToLocal(Screen.MousePos);
    if not PtInRect(canvasRect, mouseScreenPos) then
        Exit;

    leftRatio := (mouseScreenPos.X - canvasRect.Left) / canvasRect.Width;
    focalPoint.X := FDataView.Left + FDataView.Width * leftRatio;
    newWidth := FDataView.Width * zoomFactor;
    FDataView.Left := focalPoint.X - newWidth * leftRatio;
    FDataView.Right := FDataView.Left + newWidth;

    case FZoomMode of
        czmXY:
        begin
            if (FDataView.Height <= 0) then
                Exit;
            topRatio := (canvasRect.Bottom - mouseScreenPos.Y) / canvasRect.Height;
            focalPoint.Y := FDataView.Top + FDataView.Height * topRatio;
            newHeight := FDataView.Height * zoomFactor;
            FDataView.Top := focalPoint.Y - newHeight * topRatio;
            FDataView.Bottom := FDataView.Top + newHeight;
        end;
        czmXOnly:
        begin
            // The scaling is done, now we chain the auto-scaling for the Y-axis.
            // We create a new future for the view and then call AutoScaleY.
            if FYRange.Done.IsSet then
                if FYRange.Value.A <= FYRange.Value.B then
                begin
                    FDataView.Top := FYRange.Value.A;
                    FDataView.Bottom := FYRange.Value.B;
                end;
            InvalidateYRange;
            Repaint;
            Handled := True;
            Exit; // Exit because AutoScaleY will trigger its own repaint.
        end;
    end;

    // For czmXY, we update the view future directly.
    Repaint;
    Handled := True;
end;

procedure TChart.Paint;
var
    canvasRect: TRectF;
    series: TCollectionItem;
begin
    inherited;
    Canvas.Fill.Color := TAlphaColors.White;
    Canvas.FillRect(LocalRect, 1);

    if not FBoundsValid then
    begin
        RecalcDataBounds;
        ResetView;
    end;

    if FSeries.Count = 0 then
        Exit;

    if (FDataView.Width <= 0) or (FDataView.Height <= 0) then
        Exit;

    canvasRect := Self.LocalRect;
    canvasRect.Inflate(-FPadding, -FPadding);
    if (canvasRect.Width < 1) or (canvasRect.Height < 1) then
        Exit;

    DrawGrid(Canvas, FDataView, canvasRect);
    DrawAxes(Canvas, FDataView, canvasRect);

    for series in FSeries do
    begin
        (series as TChartSeries).Draw(Canvas, FDataView, canvasRect);
    end;
end;

procedure TChart.RecalcDataBounds;
var
    series: TCollectionItem;
    boundFutures: TList<TFuture<TRectD>>;
begin
    boundFutures := TList<TFuture<TRectD>>.Create;
    try
        for series in FSeries do
            boundFutures.Add((series as TChartSeries).GetBounds);

        FDataBounds :=
            TFuture
                .FromArray<TRectD>(boundFutures.ToArray)
                .Chain<TRectD>(
                    function(const Rects: TArray<TRectD>): TRectD
                    var
                        minPt, maxPt: TPointD;
                    begin
                        if Length(Rects) = 0 then
                            Exit(TRectD.Create(0, 0, 0, 0));

                        minPt := TPointD.Create(Infinity, Infinity);
                        maxPt := TPointD.Create(NegInfinity, NegInfinity);

                        for var rect in Rects do
                        begin
                            minPt.X := System.Math.Min(minPt.X, rect.Left);
                            minPt.Y := System.Math.Min(minPt.Y, rect.Top);
                            maxPt.X := System.Math.Max(maxPt.X, rect.Right);
                            maxPt.Y := System.Math.Max(maxPt.Y, rect.Bottom);
                        end;

                        if IsInfinite(minPt.X) then
                            Exit(TRectD.Create(0, 0, 0, 0));

                        if (minPt.X = maxPt.X) then
                        begin
                            maxPt.X := minPt.X + 1;
                        end;
                        if (minPt.Y = maxPt.Y) then
                        begin
                            maxPt.Y := minPt.Y + 1;
                            minPt.Y := minPt.Y - 1;
                        end;
                        Result := TRectD.Create(minPt.X, minPt.Y, maxPt.X, maxPt.Y);
                    end);
    finally
        boundFutures.Free;
    end;

    FBoundsValid := True;
end;

procedure TChart.DrawAxes(const ACanvas: TCanvas; const ADataRect: TRectD; const ACanvasRect: TRectF);
begin
    ACanvas.Stroke.Kind := TBrushKind.Solid;
    ACanvas.Stroke.Color := FAxisColor;
    ACanvas.Stroke.Thickness := 1;
    ACanvas.DrawLine(TPointF.Create(ACanvasRect.Left, ACanvasRect.Bottom), TPointF.Create(ACanvasRect.Right, ACanvasRect.Bottom), 1);
    ACanvas.DrawLine(TPointF.Create(ACanvasRect.Left, ACanvasRect.Top), TPointF.Create(ACanvasRect.Left, ACanvasRect.Bottom), 1);
end;

procedure TChart.DrawGrid(const ACanvas: TCanvas; const ADataRect: TRectD; const ACanvasRect: TRectF);
var
    i: Integer;
    x, y: Single;
const
    GridLines = 5;
begin
    ACanvas.Stroke.Kind := TBrushKind.Solid;
    ACanvas.Stroke.Color := FGridColor;
    ACanvas.Stroke.Thickness := 1;
    ACanvas.Stroke.Dash := TStrokeDash.Dot;
    for i := 1 to GridLines do
    begin
        x := ACanvasRect.Left + i * ACanvasRect.Width / GridLines;
        ACanvas.DrawLine(TPointF.Create(x, ACanvasRect.Top), TPointF.Create(x, ACanvasRect.Bottom), 1);
    end;
    for i := 0 to GridLines - 1 do
    begin
        y := ACanvasRect.Bottom - i * ACanvasRect.Height / GridLines;
        ACanvas.DrawLine(TPointF.Create(ACanvasRect.Left, y), TPointF.Create(ACanvasRect.Right, y), 1);
    end;
    ACanvas.Stroke.Dash := TStrokeDash.Solid;
end;

procedure TChart.InvalidateYRange;
var
    Arr: TArray<TFuture<TRangeD>>;
begin
    SetLength(Arr, FSeries.Count);
    for var i := 0 to High(Arr) do
        Arr[i] := (FSeries[i] as TChartSeries).GetYBoundsForXRange(TRangeD.Create(FDataView.Left, FDataView.Right));

    var yBounds := TFuture.FromArray<TRangeD>(Arr);

    FYRange :=
        yBounds.Chain<TRangeD>(
            function(const Ranges: TArray<TRangeD>): TRangeD
            var
                yRange: Double;
            begin
                Result.A := Infinity;
                Result.B := NegInfinity;
                for var range in Ranges do
                    if range.A <= range.B then
                    begin
                        Result.A := System.Math.Min(Result.A, range.A);
                        Result.B := System.Math.Max(Result.B, range.B);
                    end;

                if IsInfinite(Result.A) then
                begin
                    // No data in view, use previous view's Y-range or default
                    Result.A := 0;
                    Result.B := -1;
                    exit;
                end;

                yRange := Result.B - Result.A;
                if yRange = 0 then
                    yRange := Abs(Result.A * 0.1);

                Result.A := Result.A - yRange * 0.05;
                Result.B := Result.B + yRange * 0.05;
            end
        );
end;

procedure TChart.SetSeries(const Value: TChartSeriesCollection);
begin
    FSeries.Assign(Value);
    InvalidateDataBounds;
end;

procedure TChart.SetAxisColor(const Value: TAlphaColor);
begin
    if (FAxisColor <> Value) then
    begin
        FAxisColor := Value;
        Repaint;
    end;
end;

procedure TChart.SetGridColor(const Value: TAlphaColor);
begin
    if (FGridColor <> Value) then
    begin
        FGridColor := Value;
        Repaint;
    end;
end;

procedure TChart.SetPadding(const Value: Single);
begin
    if (FPadding <> Value) then
    begin
        FPadding := Value;
        Repaint;
    end;
end;

{ TTestChartForm }

procedure TTestChartForm.FormCreate(Sender: TObject);
var
    askSeries, bidSeries: TChartLineSeries;
    ohlc: TChartOhlcSeries;
    askBidSeries: TFuture<TDataSeries<TAskBidItem>>;
    askData, bidData: TFuture<TDataSeries<Double>>;
begin
    FChart := TChart.Create(Self);
    FChart.Parent := Self;
    FChart.Align := TAlignLayout.Client;

    askBidSeries :=
        TFuture<TDataSeries<TAskBidItem>>.Construct(
            function: TDataSeries<TAskBidItem>
            var
                askBidDataPointList: TArray<TDataPoint<TAskBidItem>>;
                lastPrice: Double;
                currentTime: TDateTime;
                i: Integer;
            const
                cnt = 200000;
            begin
                SetLength(askBidDataPointList, cnt);
                lastPrice := 100.0;
                currentTime := Now;
                for i := 0 to cnt - 1 do
                begin
                    lastPrice := lastPrice + (Random - 0.495) * 0.1;
                    currentTime := currentTime + TTimeSpan.FromMilliseconds(500 + (Random(500) - 250));
                    askBidDataPointList[i] :=
                        TDataPoint<TAskBidItem>.Create(currentTime, TAskBidItem.Create(lastPrice, lastPrice * 0.9998));
                end;
                Result := TDataSeries<TAskBidItem>.CreateDataSeries(cnt, askBidDataPointList);
            end
        );

    askData :=
        askBidSeries.Chain<TDataSeries<Double>>(
            function(const AskBid: TDataSeries<TAskBidItem>): TDataSeries<Double>
            begin
                Result := AskBid.Convert<Double>(function(const Val: TDataPoint<TAskBidItem>): Double begin Result := Val.Data.Ask end);
            end
        );

    bidData :=
        askBidSeries.Chain<TDataSeries<Double>>(
            function(const AskBid: TDataSeries<TAskBidItem>): TDataSeries<Double>
            begin
                Result := AskBid.Convert<Double>(function(const Val: TDataPoint<TAskBidItem>): Double begin Result := Val.Data.Bid end);
            end
        );

    askSeries := TChartLineSeries.Create(FChart.Series);
    askSeries.DataSeries := askData;
    askSeries.Color := TAlphaColors.Blue;
    askSeries.Thickness := 1;

    bidSeries := TChartLineSeries.Create(FChart.Series);
    bidSeries.DataSeries := bidData;
    bidSeries.Color := TAlphaColors.Red;
    bidSeries.Thickness := 1;

    ohlc := TChartOhlcSeries.Create(FChart.Series);
    ohlc.DataSeries :=
        askData.Chain<TDataSeries<TOhlcItem>>(
            function(const Data: TDataSeries<Double>): TDataSeries<TOhlcItem> begin Result := Data.ToOhlc(TTimeSpan.FromMinutes(15)); end
        );
    ohlc.Color := TAlphaColors.Black;
    ohlc.Thickness := 1;
end;

constructor TRangeD.Create(const AA, AB: Double);
begin
    A := AA;
    B := AB;
end;

end.
