unit TAChartExtentLink;

{$H+}

interface

uses
  Classes, TAChartUtils, TAGraph;

type
  TLinkedChart = class(TCollectionItem)
  strict private
    FChart: TChart;
    FListener: TListener;
    procedure OnExtentChanged(ASender: TObject);
    procedure SetChart(AValue: TChart);
  protected
    function GetDisplayName: String; override;
  public
    constructor Create(ACollection: TCollection); override;
    destructor Destroy; override;
  published
    property Chart: TChart read FChart write SetChart;
  end;

  TLinkedCharts = class(TCollection)
  strict private
    FOwner: TComponent;
  protected
    function GetOwner: TPersistent; override;
  public
    constructor Create(AOwner: TComponent);
    function Add: TLinkedChart; // Should be inline, but FPC 2.6 miscompiles it.
  end;

  TChartExtendLinkMode = (elmXY, elmOnlyX, elmOnlyY);

  TChartExtentLink = class(TComponent)
  strict private
    FEnabled: Boolean;
    FLinkedCharts: TLinkedCharts;
    FMode: TChartExtendLinkMode;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

    procedure AddChart(AChart: TChart);
    procedure SyncWith(AChart: TChart);
  published
    property Enabled: Boolean read FEnabled write FEnabled default true;
    property LinkedCharts: TLinkedCharts read FLinkedCharts write FLinkedCharts;
    property Mode: TChartExtendLinkMode read FMode write FMode default elmXY;
  end;

procedure Register;

implementation

uses
  SysUtils, TAGeometry;

procedure Register;
begin
  RegisterComponents(CHART_COMPONENT_IDE_PAGE, [TChartExtentLink]);
end;

{ TLinkedCharts }

function TLinkedCharts.Add: TLinkedChart;
begin
  Result := TLinkedChart(inherited Add);
end;

constructor TLinkedCharts.Create(AOwner: TComponent);
begin
  inherited Create(TLinkedChart);
  FOwner := AOwner;
end;

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

{ TLinkedChart }

constructor TLinkedChart.Create(ACollection: TCollection);
begin
  inherited Create(ACollection);
  FListener := TListener.Create(@FChart, @OnExtentChanged);
end;

destructor TLinkedChart.Destroy;
begin
  FreeAndNil(FListener);
  inherited;
end;

function TLinkedChart.GetDisplayName: String;
begin
  Result := inherited GetDisplayName;
  if Chart <> nil then
    Result += ' -> ' + Chart.Name;
end;

procedure TLinkedChart.OnExtentChanged(ASender: TObject);
begin
  Unused(ASender);
  (Collection.Owner as TChartExtentLink).SyncWith(Chart);
end;

procedure TLinkedChart.SetChart(AValue: TChart);
begin
  if FChart = AValue then exit;
  if Chart <> nil then
    Chart.ExtentBroadcaster.Unsubscribe(FListener);
  FChart := AValue;
  if Chart <> nil then
    Chart.ExtentBroadcaster.Subscribe(FListener);
end;

{ TChartExtentLink }

procedure TChartExtentLink.AddChart(AChart: TChart);
var
  i: TCollectionItem;
begin
  for i in LinkedCharts do
    if TLinkedChart(i).Chart = AChart then exit;
  LinkedCharts.Add.Chart := AChart;
end;

constructor TChartExtentLink.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FEnabled := true;
  FLinkedCharts := TLinkedCharts.Create(Self);
end;

destructor TChartExtentLink.Destroy;
begin
  FreeAndNil(FLinkedCharts);
  inherited;
end;

procedure TChartExtentLink.SyncWith(AChart: TChart);

  function CombineXY(const AX, AY: TDoubleRect): TDoubleRect;
  begin
    Result.a := DoublePoint(AX.a.X, AY.a.Y);
    Result.b := DoublePoint(AX.b.X, AY.b.Y);
  end;

var
  c: TCollectionItem;
  ch: TChart;
begin
  if not FEnabled or (AChart = nil) then exit;
  for c in LinkedCharts do begin
    ch := TLinkedChart(c).Chart;
    // Do not sync if the chart was never drawn yet.
    if (ch = nil) or (ch.LogicalExtent = EmptyExtent) then continue;
    // ZoomFull is lazy by default, so full extent may be not recalculated yet.
    if not ch.IsZoomed and (ch <> AChart) then
      ch.LogicalExtent := ch.GetFullExtent;
    // An event loop will be broken since setting LogicalExtent to
    // the same value does not initiale the extent broadcast.
    case Mode of
      elmXY:
        ch.LogicalExtent := AChart.LogicalExtent;
      elmOnlyX:
        ch.LogicalExtent := CombineXY(AChart.LogicalExtent, ch.LogicalExtent);
      elmOnlyY:
        ch.LogicalExtent := CombineXY(ch.LogicalExtent, AChart.LogicalExtent);
    end;
  end;
end;

end.

