unit uAnchoredZoomTool;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Math, TAGraph, TAChartUtils, TATools, TASeries;
type
TZoomAnchorMode = (zamCursor, zamCenter, zamRightPinned);
{ -------------------------------------------------------------------
Patched Zoom Tool
-------------------------------------------------------------------
FIX: Override Handled and guard against Toolset=nil so the app
never crashes on: Toolset.FIsHandled := true;
------------------------------------------------------------------- }
TAnchoredZoomMouseWheelTool = class(TZoomMouseWheelTool)
private
FAnchorMode : TZoomAnchorMode;
procedure HandleCalculateNewExtent(ATool: TChartTool;
var ANewExtent: TDoubleRect);
function FirstDataX: Double;
function LastDataX: Double;
procedure RecalcYRange(const AXMin, AXMax: Double;
out AYMin, AYMax: Double);
public
constructor Create(AOwner: TComponent); override;
// ********** CRASH PATCH **********
procedure Handled;
// **********************************
published
property AnchorMode : TZoomAnchorMode
read FAnchorMode write FAnchorMode default zamCursor;
end;
implementation
uses
TACustomSeries;
{ ===================================================================
Constructor
=================================================================== }
constructor TAnchoredZoomMouseWheelTool.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FAnchorMode := zamCursor;
OnCalculateNewExtent := @HandleCalculateNewExtent;
end;
{ ===================================================================
HARD CRASH FIX
=================================================================== }
procedure TAnchoredZoomMouseWheelTool.Handled;
begin
// TAChart calls this even when Toolset=nil → causes NIL dereference
if Toolset <> nil then
inherited Handled;
end;
{ ===================================================================
Data helpers
=================================================================== }
function TAnchoredZoomMouseWheelTool.FirstDataX: Double;
var
S: TChartSeries;
begin
Result := Chart.LogicalExtent.a.x;
if Chart.SeriesCount = 0 then Exit;
S := TChartSeries(Chart.Series[0]);
if S.Count > 0 then
Result := S.XValue[0];
end;
function TAnchoredZoomMouseWheelTool.LastDataX: Double;
var
S: TChartSeries;
begin
Result := Chart.LogicalExtent.b.x;
if Chart.SeriesCount = 0 then Exit;
S := TChartSeries(Chart.Series[0]);
if S.Count > 0 then
Result := S.XValue[S.Count - 1];
end;
{ ===================================================================
Recalculate Y-axis range
=================================================================== }
procedure TAnchoredZoomMouseWheelTool.RecalcYRange(const AXMin, AXMax: Double;
out AYMin, AYMax: Double);
var
i, j: Integer;
S: TChartSeries;
Y: Double;
begin
AYMin := 1E308;
AYMax := -1E308;
for i := 0 to Chart.SeriesCount - 1 do
if Chart.Series[i] is TChartSeries then
begin
S := TChartSeries(Chart.Series[i]);
for j := 0 to S.Count - 1 do
if (S.XValue[j] >= AXMin) and (S.XValue[j] <= AXMax) then
begin
Y := S.YValue[j];
AYMin := Min(AYMin, Y);
AYMax := Max(AYMax, Y);
end;
end;
if AYMin < AYMax then
begin
Y := (AYMax - AYMin) * 0.05;
AYMin -= Y;
AYMax += Y;
end;
end;
{ ===================================================================
MAIN ZOOM LOGIC
=================================================================== }
procedure TAnchoredZoomMouseWheelTool.HandleCalculateNewExtent(
ATool: TChartTool; var ANewExtent: TDoubleRect);
var
OldExt : TDoubleRect;
OldW, NewW: Double;
DataMin, DataMax: Double;
X1, X2: Double;
YMin, YMax: Double;
begin
// Default TAChart behavior → skip custom logic
if AnchorMode = zamCursor then
Exit;
OldExt := Chart.LogicalExtent;
OldW := OldExt.b.x - OldExt.a.x;
NewW := ANewExtent.b.x - ANewExtent.a.x;
if (OldW <= 0) or (NewW <= 0) then
Exit;
DataMin := FirstDataX;
DataMax := LastDataX;
case AnchorMode of
{ ------------------------------------------------------------
CENTER-ANCHORED ZOOM
------------------------------------------------------------ }
zamCenter:
begin
X1 := OldExt.a.x + (OldW - NewW) * 0.5;
X2 := X1 + NewW;
if (X2 - X1) > (DataMax - DataMin) then
begin
X1 := DataMin;
X2 := DataMax;
end
else
begin
if X1 < DataMin then
begin
X1 := DataMin;
X2 := X1 + NewW;
end;
if X2 > DataMax then
begin
X2 := DataMax;
X1 := X2 - NewW;
end;
end;
ANewExtent.a.x := X1;
ANewExtent.b.x := X2;
end;
{ ------------------------------------------------------------
RIGHT-ANCHORED ZOOM
------------------------------------------------------------ }
zamRightPinned:
begin
X2 := DataMax;
X1 := X2 - NewW;
if X1 < DataMin then
begin
X1 := DataMin;
X2 := DataMin + NewW;
if X2 > DataMax then
X2 := DataMax;
end;
ANewExtent.a.x := X1;
ANewExtent.b.x := X2;
end;
else
Exit;
end;
// Recalculate Y-axis after final X-range is known
RecalcYRange(ANewExtent.a.x, ANewExtent.b.x, YMin, YMax);
ANewExtent.a.y := YMin;
ANewExtent.b.y := YMax;
end;
end.