{********************************************}
{ TeeChart - TChartLayout                    }
{ Copyright (c) 2000-2020 by Steema Software }
{********************************************}
unit TeeChartLayout;
{$I TeeDefs.inc}

interface

uses
  {$IFDEF MSWINDOWS}
  Windows,
  {$ENDIF}
  Classes, 
  {$IFDEF D9}
  Types,
  {$ENDIF}
  {$IFDEF FMX}
  FMX.Layouts, FMX.Types,
  {$IFDEF D19}
  FMX.Graphics,
  {$ENDIF}
  FMXTee.Canvas, FMXTee.Engine, FMXTee.Chart, FMXTee.Tools.SubChart
  {$ELSE}
  Controls, Forms, Graphics, TeCanvas, TeEngine, Chart
  {$ENDIF}
  ;

type
  TChartLayout=class;

  TChartItem=class(TCollectionItem)
  private
    FChart : TChart;

    function GetChart:TChart;
    procedure ReadChart(Stream: TStream);
    procedure SetChart(const Value: TChart);
    procedure WriteChart(Stream: TStream);
  protected
    procedure DefineProperties(Filer: TFiler); override;
  public
    Constructor Create(Collection:TCollection); overload; override;
    Destructor Destroy; override;

    procedure Assign(Source:TPersistent); override;
  published
    property Chart:TChart read GetChart write SetChart stored False;
  end;

  TChartCollection=class(TOwnedCollection)
  private
    IOwner : TChartLayout;

    Function Get(Index:Integer):TChartItem;
    Procedure Put(Index:Integer; Const Value:TChartItem);
  protected
    {$IFDEF D6}
    procedure Notify(Item: TCollectionItem; Action: TCollectionNotification); override;
    {$ELSE}
    procedure Update(Item: TCollectionItem); override;
    {$ENDIF}
  public
    function AddChart(const ATitle:String=''):TChart;
    function Clone(const AChart:TChart):TChartItem;

    property Items[Index:Integer]:TChartItem read Get write Put; default;
  end;

  {$IFDEF D16}
  [ComponentPlatformsAttribute(TeeAllComponentPlatformIDs)]
  {$ENDIF}
  TChartLayout=class(TScrollBox)
  private
    FChartHeight: Integer;
    FCharts  : TChartCollection;
    FChartWidth: Integer;
    FColumns: Integer;

    function CalcColumns(out W,H:Integer):Integer;
    function GetView3D: Boolean;
    function IsVisible(const Item:TChartItem):Boolean;
    procedure SetChartHeight(const Value: Integer);
    procedure SetCharts(const Value:TChartCollection);
    procedure SetChartWidth(const Value: Integer);
    procedure SetColumns(const Value: Integer);
    procedure SetView3D(const Value: Boolean);
  protected
    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
  public
    Constructor Create(AOwner:TComponent); override;
    Destructor Destroy; override;

    function Add(const Title:String):TChart; overload;
    procedure Add(const Detail:TSeriesDetail); overload;

    function Bitmap:TBitmap;
    procedure Clear;
    function Count:Integer;
    procedure Relayout;

    property View3D:Boolean read GetView3D write SetView3D;
  published
    {$IFNDEF FMX}
    property BorderStyle default bsNone;
    {$ENDIF}

    property Charts:TChartCollection read FCharts write SetCharts;

    property ChartHeight:Integer read FChartHeight write SetChartHeight default 250;
    property ChartWidth:Integer read FChartWidth write SetChartWidth default 400;

    property Columns:Integer read FColumns write SetColumns default 0;
  end;

implementation

uses
  {$IFDEF FMX}
  FMXTee.Procs, FMXTee.Series
  {$ELSE}

  {$IFDEF LCL}
  Types,
  {$ENDIF}

  {$IFDEF MSWINDOWS}
  TeeGDIPlus,
  {$ENDIF}

  TeeProcs, Series
  {$ENDIF}
  ;

{ TChartLayout }

Constructor TChartLayout.Create(AOwner: TComponent);
begin
  inherited;

  {$IFDEF FMX}
  {$ELSE}

  {$IFNDEF LCL}
  Ctl3D:=False;
  {$ENDIF}
  
  BorderStyle:=bsNone;

  HorzScrollBar.Smooth:=True;
  VertScrollBar.Smooth:=True;

  HorzScrollBar.Tracking:=True;
  VertScrollBar.Tracking:=True;
  {$ENDIF}

  FCharts:=TChartCollection.Create(Self,TChartItem);

  FChartWidth:=400;
  FChartHeight:=250;

  // Default size at design-time for first-time:

  if csDesigning in ComponentState then
     if not (csLoading in ComponentState) then
     begin
       Width:=400;
       Height:=300;
     end;
end;

Destructor TChartLayout.Destroy;
begin
  FCharts.Free;
  inherited;
end;

function TChartLayout.Add(const Title: String): TChart;
begin
  result:=FCharts.AddChart(TeeGetUniqueName(Self,'Chart'));
  result.Parent:=Self;

  result.Title.Caption:=Title;
  result.Title.Show;
end;

procedure TChartLayout.Add(const Detail:TSeriesDetail);
var t : Integer;
begin
  for t:=0 to Detail.Count-1 do
      Add(Detail[t].Title).AddSeries(Detail[t]);
end;

procedure TChartLayout.Clear;
begin
  FCharts.Clear;
end;

function TChartLayout.Count:Integer;
begin
  result:=FCharts.Count;
end;

function TChartLayout.Bitmap: TBitmap;
var n,
    c,
    cols,
    l,t,w,h : Integer;
    tmpR : TRect;
begin
  result:=TBitmap.Create{$IFNDEF D19}{$IFDEF FMX}(0,0){$ENDIF}{$ENDIF};

  // Obtain default width and height:

  cols:=CalcColumns(W,H);

  // Calculate total bounds rectangle of all visible charts:

  tmpR:=TeeZeroRect;

  n:=0;

  for c:=0 to FCharts.Count-1 do
  if FCharts[c].Chart.Visible then
  begin
    l:=(n mod cols)*w;
    t:=(n div cols)*h;

    UnionRect(tmpR,tmpR,TeeRect(l,t,l+w,t+h));

    Inc(n);
  end;

  // Set bitmap size to total bounds:

  TeeSetBitmapSize(result,{$IFDEF FMX}Round{$ENDIF}(tmpR.Right-tmpR.Left),
                          {$IFDEF FMX}Round{$ENDIF}(tmpR.Bottom-tmpR.Top));

  // Draw all visible charts to bitmap:

  n:=0;

  for c:=0 to FCharts.Count-1 do
  if FCharts[c].Chart.Visible then
  begin
    l:=(n mod cols)*w;
    t:=(n div cols)*h;

    tmpR:=TeeRect(l,t,l+w,t+h);

    FCharts[c].Chart.Draw(result.Canvas,tmpR);

    Inc(n);
  end;
end;

function TChartLayout.GetView3D: Boolean;
begin
  if Charts.Count=0 then
     result:=True
  else
     result:=Charts[0].Chart.View3D;
end;

procedure TChartLayout.Notification(AComponent: TComponent; Operation: TOperation);
begin
  inherited;

  if AComponent is TCustomChart then
     Relayout;
end;

function TChartLayout.IsVisible(const Item:TChartItem):Boolean;
begin
  result:=Assigned(Item.FChart) and
          (not (csDestroying in Item.FChart.ComponentState)) and
          Item.FChart.Visible;
end;

function TChartLayout.CalcColumns(out W,H:Integer):Integer;
var c,
    tmpVisible : Integer;
begin
  W:=ChartWidth;

  if W<=0 then
     W:=400;

  H:=ChartHeight;

  if H<=0 then
     H:=250;

  result:=Columns;

  if result<=0 then
  begin
    // Calculate the best number of columns for the quantity of visible charts:

    tmpVisible:=0;

    for c:=0 to FCharts.Count-1 do
        if IsVisible(FCharts[c]) then
           Inc(tmpVisible);

    result:=Round(Sqrt(tmpVisible));

    if result<=0 then
       result:=1;
  end;
end;

procedure TChartLayout.Relayout;
var c,
    n : Integer;

    cols,
    l,t,
    w,h : Integer;
begin
  cols:=CalcColumns(w,h);

  // Reposition all visible charts:

  n:=0;

  for c:=0 to FCharts.Count-1 do
  if IsVisible(FCharts[c]) then
  begin
    l:=(n mod cols)*w;
    t:=(n div cols)*h;

    FCharts[c].Chart.BoundsRect:=TeeRect(l,t,l+w,t+h);

    Inc(n);
  end;
end;

procedure TChartLayout.SetChartHeight(const Value: Integer);
begin
  if FChartHeight<>Value then
  begin
    FChartHeight:=Value;
    Relayout;
  end;
end;

procedure TChartLayout.SetCharts(const Value: TChartCollection);
begin
  FCharts.Assign(Value);
end;

procedure TChartLayout.SetChartWidth(const Value: Integer);
begin
  if FChartWidth<>Value then
  begin
    FChartWidth:=Value;
    Relayout;
  end;
end;

procedure TChartLayout.SetColumns(const Value: Integer);
begin
  if FColumns<>Value then
  begin
    FColumns:=Value;
    Relayout;
  end;
end;

procedure TChartLayout.SetView3D(const Value: Boolean);
var t : Integer;
begin
  for t:=0 to Charts.Count-1 do
      Charts[t].Chart.View3D:=Value;
end;

{ TChartItem }

type
  TOwnedCollectionAccess=class(TOwnedCollection);

constructor TChartItem.Create(Collection: TCollection);
begin
  inherited;
  GetChart;
end;

Destructor TChartItem.Destroy;
begin
  FChart.Free;
  inherited;
end;

procedure TChartItem.Assign(Source: TPersistent);
begin
  if Source is TChartItem then
     Chart.Assign(TChartItem(Source).Chart)
  else
  if Source is TCustomChart then
     Chart.Assign(TCustomChart(Source))
  else
     inherited;
end;

procedure TChartItem.ReadChart(Stream: TStream);
begin
  if Assigned(FChart) then
     FChart:=Stream.ReadComponent(FChart) as TChart
  else
     FChart:=Stream.ReadComponent(nil) as TChart;

//  FChart.BoundsRect:=ITempRect;
//  TInnerChart(FChart).IParent:=Self;
end;

procedure TChartItem.WriteChart(Stream: TStream);
begin
  Stream.WriteComponent(Chart);
end;

procedure TChartItem.DefineProperties(Filer: TFiler);

  function SaveInnerChart:Boolean;
  begin
    result:=(not Assigned(Filer.Ancestor)) //or
            //(not EqualChart(FChart,TSubChart(Filer.Ancestor).FChart));
  end;

begin
  inherited;
  Filer.DefineBinaryProperty('InnerChart',ReadChart,WriteChart,SaveInnerChart);
end;

type
  TComponentAccess=class({$IFDEF FPC}TChart{$ELSE}TComponent{$ENDIF});

function TChartItem.GetChart: TChart;
var tmp : TChartLayout;
begin
  if not Assigned(FChart) then
  begin
    FChart:=TChart.Create(nil);
    FChart.BevelOuter:=bvNone;

    if Assigned(Collection) then
    begin
      tmp:=TChartLayout(TOwnedCollectionAccess(Collection).GetOwner);

      if Assigned(tmp) then
      begin
        FChart.Parent:=tmp;

        if csDesigning in tmp.ComponentState then
           TComponentAccess(FChart).SetDesigning(True);

       tmp.Relayout;
      end;
    end;
  end;

  result:=FChart;
end;

procedure TChartItem.SetChart(const Value: TChart);
begin
  Chart.Assign(Value);
end;

{ TChartCollection }

function TChartCollection.AddChart(const ATitle: String): TChart;
begin
  result:=(Add as TChartItem).Chart;
  result.Title.Caption:=ATitle;
end;

function TChartCollection.Clone(const AChart: TChart): TChartItem;
var tmp : TChart;
begin
  result:=Add as TChartItem;
  tmp:=result.Chart;
  CloneChart(tmp,AChart,tmp.Owner,False);
end;

function TChartCollection.Get(Index: Integer): TChartItem;
begin
  result:=TChartItem(inherited Items[Index]);
end;

procedure TChartCollection.Put(Index: Integer; const Value: TChartItem);
begin
  inherited Items[Index]:=Value;
end;

{$IFDEF D6}
procedure TChartCollection.Notify(Item: TCollectionItem; Action: TCollectionNotification);
{$ELSE}
procedure TChartCollection.Update(Item: TCollectionItem);
{$ENDIF}
begin
  inherited;

  if Assigned(Item) then
     if Assigned(IOwner) then
        IOwner.ReLayout;
end;

initialization
  RegisterTeeStandardSeries;
end.
