From 956c47ba36bb1882f071dda5d1af66872ee4cd5c Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Sat, 12 Jul 2025 22:20:23 +0200 Subject: [PATCH] Fix chart left data range fail --- AuraTrader/MainForm.pas | 6 +- AuraTrader/Myc.Fmx.Chart.pas | 260 +++++++++++------------------------ 2 files changed, 85 insertions(+), 181 deletions(-) diff --git a/AuraTrader/MainForm.pas b/AuraTrader/MainForm.pas index e20327c..b1815fb 100644 --- a/AuraTrader/MainForm.pas +++ b/AuraTrader/MainForm.pas @@ -176,11 +176,11 @@ begin Ohlc.Sender.Link(Closes); - for var i := 0 to 200 do + for var i := 0 to 3 do begin - var Hull: IMycConverter := THullMovingAverage.Create(20 + (20 * i)); + var Hull: IMycConverter := THullMovingAverage.Create(50 + (50 * i)); Closes.Sender.Link(Hull); - chart.AddDoubleSeries(Hull.Sender, Random(Cardinal.MaxValue)); + chart.AddDoubleSeries(Hull.Sender, TAlphaColor(Random(Integer.MaxValue - 1))); end; // var Hull: IMycConverter := THullMovingAverage.Create(250); diff --git a/AuraTrader/Myc.Fmx.Chart.pas b/AuraTrader/Myc.Fmx.Chart.pas index eb56980..046f36f 100644 --- a/AuraTrader/Myc.Fmx.Chart.pas +++ b/AuraTrader/Myc.Fmx.Chart.pas @@ -20,8 +20,6 @@ uses Myc.Lazy; type - TCandleStyle = (csCandleStick, csHiLoBar); - TMycChart = class(TStyledControl) public type @@ -33,13 +31,9 @@ type function GetCount: Int64; virtual; abstract; function GetTotalCount: Int64; virtual; abstract; function GetValueRange(StartIndex, Count: Int64; out Min, Max: Double): Boolean; virtual; abstract; - function Update: Boolean; virtual; abstract; + procedure Update; virtual; abstract; // The signature is changed to support viewport painting with an index offset - procedure Paint( - const ACanvas: TCanvas; - StartIndex, Count: Int64; - const AXForm, AYForm: TFunc - ); virtual; abstract; + procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); virtual; abstract; property MainSeries: TSeries read GetMainSeries; public constructor Create(AOwner: TMycChart); @@ -74,8 +68,7 @@ type function AddOhlcSeries( const DataProvider: IMycDataProvider>; const AUpColor: TAlphaColor = TAlphaColors.Green; - const ADownColor: TAlphaColor = TAlphaColors.Red; - const AStyle: TCandleStyle = csCandleStick + const ADownColor: TAlphaColor = TAlphaColors.Red ): TSeries; // Creates a simple line series for double values and returns it @@ -107,7 +100,6 @@ type function ProcessData(const Value: T): Boolean; override; public constructor Create(const ALookback: TWriteable); - function GetData(var Data: TMycDataArray): Boolean; property Data: TWriteable> read FData; end; @@ -123,7 +115,7 @@ type protected function GetCount: Int64; override; function GetTotalCount: Int64; override; - function Update: Boolean; override; + procedure Update; override; public constructor Create(AOwner: TMycChart; const ADataProvider: IMycDataProvider); @@ -136,16 +128,14 @@ type private FUpColor: TAlphaColor; FDownColor: TAlphaColor; - FStyle: TCandleStyle; protected - function GetValueRange(StartIndex, Count: Int64; out MinValue, MaxValue: Double): Boolean; override; - procedure Paint(const ACanvas: TCanvas; StartIndex, Count: Int64; const AXForm, AYForm: TFunc); override; + function GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; override; + procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); override; public constructor Create( AOwner: TMycChart; const ADataProvider: IMycDataProvider>; - const AUpColor, ADownColor: TAlphaColor; - AStyle: TCandleStyle + const AUpColor, ADownColor: TAlphaColor ); end; @@ -155,8 +145,8 @@ type FLineColor: TAlphaColor; FLineWidth: Single; protected - function GetValueRange(StartIndex, Count: Int64; out MinValue, MaxValue: Double): Boolean; override; - procedure Paint(const ACanvas: TCanvas; StartIndex, Count: Int64; const AXForm, AYForm: TFunc); override; + function GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; override; + procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); override; public constructor Create( AOwner: TMycChart; @@ -220,55 +210,53 @@ end; function TMycChart.AddOhlcSeries( const DataProvider: IMycDataProvider>; const AUpColor: TAlphaColor = TAlphaColors.Green; - const ADownColor: TAlphaColor = TAlphaColors.Red; - const AStyle: TCandleStyle = csCandleStick + const ADownColor: TAlphaColor = TAlphaColors.Red ): TSeries; begin - Result := TChartOhlcSeries.Create(Self, DataProvider, AUpColor, ADownColor, AStyle); + Result := TChartOhlcSeries.Create(Self, DataProvider, AUpColor, ADownColor); FSeriesList.Add(Result); end; procedure TMycChart.DoIdle; -var - doRepaint: Boolean; - mainSeries: TSeries; - isLiveView: Boolean; - prevTotalCount, newTotalCount, countDelta: Int64; begin if (FSeriesList.Count = 0) then exit; - mainSeries := FSeriesList[0]; - if (mainSeries = nil) then - exit; + var prevTotalCount := FSeriesList[0].TotalCount; - // Remember state before update - isLiveView := (FViewStartIndex = 0); - prevTotalCount := mainSeries.TotalCount; - - // Check all series for updates - doRepaint := false; + var seriesRepaint := false; + var seriesChanged := false; for var series in FSeriesList do begin - if series.Update then - begin - doRepaint := true; - end; + var seriesCount := series.TotalCount; + + // Check if this series may need a repaint + if FViewStartIndex <= prevTotalCount - seriesCount then + seriesRepaint := true; + + // Update the series + series.Update; + + if seriesCount <> series.TotalCount then + seriesChanged := true; end; - if doRepaint and (not isLiveView) then + // Don't move with live data, if the last bar isn't visible + if FViewStartIndex > 0 then begin - newTotalCount := mainSeries.TotalCount; - countDelta := newTotalCount - prevTotalCount; + var newTotalCount := FSeriesList[0].TotalCount; + var countDelta := newTotalCount - prevTotalCount; - if (countDelta > 0) then + if countDelta > 0 then begin // Adjust the start index based on the absolute change in total data points FViewStartIndex := FViewStartIndex + countDelta; + if FViewStartIndex + FViewCount > newTotalCount then + FViewStartIndex := newTotalCount - FViewCount; end; end; - if doRepaint then + if seriesRepaint and seriesChanged then Repaint; end; @@ -342,6 +330,7 @@ begin // Panning logic inherited MouseMove(Shift, X, Y); + if FIsDragging then begin dx := X - FDragStartPoint.X; @@ -364,24 +353,18 @@ begin // Clamp values mainSeries := FSeriesList[0]; - maxIndex := 0; - if (mainSeries <> nil) then - begin - maxIndex := mainSeries.Count - FViewCount; - end; + maxIndex := mainSeries.Count - FViewCount; if (maxIndex < 0) then - begin maxIndex := 0; - end; if (FViewStartIndex < 0) then FViewStartIndex := 0; if (FViewStartIndex > maxIndex) then FViewStartIndex := maxIndex; - // Optimized point update FDragStartPoint.X := X; FDragStartPoint.Y := Y; + Repaint; end; end; @@ -398,18 +381,14 @@ begin Handled := true; if (FSeriesList.Count = 0) then - begin exit; - end; mainSeries := FSeriesList[0]; - if (mainSeries = nil) or (mainSeries.Count = 0) or (Self.Width <= 0) then - begin + if (mainSeries.Count = 0) or (Width <= 0) then exit; - end; - mousePos := Self.ScreenToLocal(Screen.MousePos); + mousePos := ScreenToLocal(Screen.MousePos); - // Correctly calculate the anchor index for a right-to-left axis + // Calculate the anchor index for a right-to-left axis anchorIndex := FViewStartIndex + ((Self.Width - mousePos.X) / Self.Width) * FViewCount; if (WheelDelta > 0) then @@ -449,21 +428,16 @@ end; procedure TMycChart.Paint; - function CalcView(Series: TSeries; MasterCount: Int64; out First: Int64; out Count: Int64): Boolean; + function CalcView(Series: TSeries; MasterCount: Int64; out First: Int64; out Last: Int64): Boolean; begin var seriesTotalCount := series.TotalCount; var offset := MasterCount - seriesTotalCount; var startIdx := FViewStartIndex - offset; - var effectiveStart := Max(0, startIdx); - var effectiveEnd := Min(series.Count, startIdx + FViewCount) - 1; + First := Max(0, startIdx); + Last := Min(series.Count, startIdx + FViewCount) - 1; - Result := effectiveStart <= effectiveEnd - 1; - if Result then - begin - First := effectiveStart; - Count := effectiveEnd - effectiveStart + 1; - end; + Result := First <= Last - 1; end; var @@ -477,7 +451,7 @@ var buttonColor: TAlphaColor; path: TPathData; mainSeries: TSeries; - masterTotalCount, seriesStart, seriesCount: Int64; + masterTotalCount, seriesFirst, seriesLast: Int64; begin inherited; rect := Self.LocalRect; @@ -495,9 +469,9 @@ begin masterTotalCount := mainSeries.TotalCount; for series in FSeriesList do begin - if CalcView(series, masterTotalCount, seriesStart, seriesCount) then + if CalcView(series, masterTotalCount, seriesFirst, seriesLast) then begin - if series.GetValueRange(seriesStart, seriesCount, seriesMin, seriesMax) then + if series.GetValueRange(seriesFirst, seriesLast, seriesMin, seriesMax) then begin if not rangeInitialized then begin @@ -532,7 +506,7 @@ begin // The masterTotalCount is already calculated from the range finding loop for series in FSeriesList do begin - if CalcView(series, masterTotalCount, seriesStart, seriesCount) then + if CalcView(series, masterTotalCount, seriesFirst, seriesLast) then begin var offset := masterTotalCount - series.TotalCount; xTransform := @@ -541,7 +515,7 @@ begin Result := rect.Right - (((index + offset - FViewStartIndex) / (FViewCount - 1)) * rect.Width); end; - series.Paint(Self.Canvas, seriesStart, seriesCount, xTransform, yTransform); + series.Paint(Self.Canvas, seriesFirst, seriesLast, xTransform, yTransform); end; end; @@ -602,13 +576,6 @@ begin FData := TWriteable>.CreateWriteable(FCurrData).Protect; end; -function TChartSeriesReceiver.GetData(var Data: TMycDataArray): Boolean; -begin - var prevTotalCount := Data.TotalCount; - Data := FData.Value; - Result := prevTotalCount <> Data.TotalCount; -end; - function TChartSeriesReceiver.ProcessData(const Value: T): Boolean; begin Result := true; @@ -645,9 +612,9 @@ begin Result := FData.TotalCount; end; -function TChartSeriesProcessor.Update: Boolean; +procedure TChartSeriesProcessor.Update; begin - Result := FReceiver.GetData(FData); + FData := FReceiver.Data.Value; end; { TChartOhlcSeries } @@ -655,43 +622,22 @@ end; constructor TChartOhlcSeries.Create( AOwner: TMycChart; const ADataProvider: IMycDataProvider>; - const AUpColor, ADownColor: TAlphaColor; - AStyle: TCandleStyle + const AUpColor, ADownColor: TAlphaColor ); begin inherited Create(AOwner, ADataProvider); FUpColor := AUpColor; FDownColor := ADownColor; - FStyle := AStyle; end; -function TChartOhlcSeries.GetValueRange(StartIndex, Count: Int64; out MinValue, MaxValue: Double): Boolean; +function TChartOhlcSeries.GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; var i: Int64; - seriesCount: Int64; - effectiveStart, effectiveEnd: Int64; begin - seriesCount := GetCount; - // Ensure the request is valid and there's data to check - Result := (seriesCount > 0) and (Count > 0); - if not Result then - Exit; - - // Calculate the effective range that is actually available within this series's data - effectiveStart := Max(0, StartIndex); - effectiveEnd := Min(seriesCount - 1, StartIndex + Count - 1); - - // If the entire requested range is outside our available data, there's nothing to do. - if (effectiveStart > effectiveEnd) then - begin - Result := false; - Exit; - end; - MinValue := MaxDouble; MaxValue := -MaxDouble; - for i := effectiveStart to effectiveEnd do + for i := First to Last do begin var dp := Data.Items[i]; MinValue := Min(MinValue, dp.Data.Low); @@ -700,50 +646,39 @@ begin Result := (MinValue <> MaxDouble); end; -procedure TChartOhlcSeries.Paint(const ACanvas: TCanvas; StartIndex, Count: Int64; const AXForm, AYForm: TFunc); +procedure TChartOhlcSeries.Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); var i: Int64; - lastIndex: Int64; x, candleWidth: Single; yOpen, yHigh, yLow, yClose: Single; item: TDataPoint; isUp: boolean; begin - if (Count <= 0) then - Exit; + candleWidth := Max(2, 0.8 * Abs((XForm(First + 1) - XForm(First)))); - if (Count > 1) then - candleWidth := Max(2, 0.8 * Abs((AXForm(StartIndex + 1) - AXForm(StartIndex)))) - else - candleWidth := 10; - - lastIndex := Min(Data.Count - 1, StartIndex + Count - 1); - for i := StartIndex to lastIndex do + for i := First to Last do begin item := Data.Items[i]; - x := AXForm(i); - yOpen := AYForm(item.Data.Open); - yClose := AYForm(item.Data.Close); - yHigh := AYForm(item.Data.High); - yLow := AYForm(item.Data.Low); + x := XForm(i); + yOpen := YForm(item.Data.Open); + yClose := YForm(item.Data.Close); + yHigh := YForm(item.Data.High); + yLow := YForm(item.Data.Low); isUp := item.Data.Close >= item.Data.Open; if isUp then - ACanvas.Stroke.Color := FUpColor + Canvas.Stroke.Color := FUpColor else - ACanvas.Stroke.Color := FDownColor; - ACanvas.Stroke.Thickness := 1.0; + Canvas.Stroke.Color := FDownColor; + Canvas.Stroke.Thickness := 1.0; - ACanvas.DrawLine(TPointF.Create(x, yHigh), TPointF.Create(x, yLow), 1); + Canvas.DrawLine(TPointF.Create(x, yHigh), TPointF.Create(x, yLow), 1); - if (FStyle = csCandleStick) then - begin - ACanvas.Fill.Color := ACanvas.Stroke.Color; - if isUp then - ACanvas.FillRect(TRectF.Create(x - candleWidth / 2, yClose, x + candleWidth / 2, yOpen), 0, 0, AllCorners, 1) - else - ACanvas.FillRect(TRectF.Create(x - candleWidth / 2, yOpen, x + candleWidth / 2, yClose), 0, 0, AllCorners, 1); - end; + Canvas.Fill.Color := Canvas.Stroke.Color; + if isUp then + Canvas.FillRect(TRectF.Create(x - candleWidth / 2, yClose, x + candleWidth / 2, yOpen), 0, 0, AllCorners, 1) + else + Canvas.FillRect(TRectF.Create(x - candleWidth / 2, yOpen, x + candleWidth / 2, yClose), 0, 0, AllCorners, 1); end; end; @@ -761,32 +696,13 @@ begin FLineWidth := ALineWidth; end; -function TChartLineSeries.GetValueRange(StartIndex, Count: Int64; out MinValue, MaxValue: Double): Boolean; +function TChartLineSeries.GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; var i: Int64; - seriesCount: Int64; - effectiveStart, effectiveEnd: Int64; begin - seriesCount := GetCount; - // Ensure the request is valid and there's data to check - Result := (seriesCount > 0) and (Count > 0); - if not Result then - Exit; - - // Calculate the effective range that is actually available within this series's data - effectiveStart := Max(0, StartIndex); - effectiveEnd := Min(seriesCount - 1, StartIndex + Count - 1); - - // If the entire requested range is outside our available data, there's nothing to do. - if (effectiveStart > effectiveEnd) then - begin - Result := false; - Exit; - end; - MinValue := MaxDouble; MaxValue := -MaxDouble; - for i := effectiveStart to effectiveEnd do + for i := First to Last do begin var v := Data.Items[i]; if not IsNaN(v) then @@ -798,40 +714,28 @@ begin Result := (MinValue <> MaxDouble); end; -procedure TChartLineSeries.Paint(const ACanvas: TCanvas; StartIndex, Count: Int64; const AXForm, AYForm: TFunc); +procedure TChartLineSeries.Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); var points: TPathData; i, n: Int64; - lastIndex: Int64; begin - if (Data.Count = 0) or (Count < 2) then - Exit; - - lastIndex := Min(Data.Count - 1, StartIndex + Count - 1); points := TPathData.Create; try // Skip warmup data - n := StartIndex; - while (n <= lastIndex) and (IsNaN(Data[n])) do - begin + n := First; + while (n <= Last) and (IsNaN(Data[n])) do inc(n); - end; - - if (n > lastIndex) then + if (n > Last) then exit; - points.MoveTo(TPointF.Create(AXForm(n), AYForm(Data[n]))); - for i := n + 1 to lastIndex do - begin + points.MoveTo(TPointF.Create(XForm(n), YForm(Data[n]))); + for i := n + 1 to Last do if not IsNaN(Data[i]) then - begin - points.LineTo(TPointF.Create(AXForm(i), AYForm(Data[i]))); - end; - end; + points.LineTo(TPointF.Create(XForm(i), YForm(Data[i]))); - ACanvas.Stroke.Color := FLineColor; - ACanvas.Stroke.Thickness := FLineWidth; - ACanvas.DrawPath(points, 1); + Canvas.Stroke.Color := FLineColor; + Canvas.Stroke.Thickness := FLineWidth; + Canvas.DrawPath(points, 1); finally points.Free; end;