{**********************************************}
{   TBubbleSeries (derived from TPointSeries)  }
{   Copyright (c) 1995-2020 by Steema Software }
{**********************************************}
unit BubbleCh;
{$I TeeDefs.inc}

interface

{ TBubbleSeries derives from standard TPointSeries.
  Each point in the series is drawn like a Bubble.
  Each point has a Radius value that's used to draw the Bubble with its
  corresponding screen size.

  Inherits all functionality from TPointSeries and
  its ancestor TCustomSeries.
}
uses {$IFNDEF LINUX}
     {$IFNDEF FMX}
     Windows, Messages,
     {$ENDIF}
     {$ENDIF}

     {$IFDEF FMX}
     FMX.Types, System.UITypes, System.Types,
     {$ELSE}

     {$IFDEF D16}
     System.UITypes, System.Types,
     {$ENDIF}

     Graphics,
     {$ENDIF}

     Classes,

     {$IFDEF FMX}
     FMXTee.Canvas, FMXTee.Procs, FMXTee.Engine, FMXTee.Series
     {$ELSE}
     Series, TeEngine, TeeProcs
     {$ENDIF}
     ;

type
  TPointerSizes=record
    X,
    Y : Integer;
    Size : Single;
  end;

  TCustomBubbleSeries=class(TPointSeries)
  private
    FRadiusValues : TChartValueList; { <-- Bubble's radius storage }

    procedure RestorePointer(const Value:TPointerSizes);
    function SavePointer:TPointerSizes;
    Procedure SetRadiusValues(Value:TChartValueList);
  protected
    function CalcPointerSize(const ValueIndex:Integer):Single; virtual;
    procedure DrawValue(ValueIndex:Integer); override; { <-- main draw method }
    Procedure PreparePointer(ValueIndex:Integer); override;
  public
    Constructor Create(AOwner: TComponent); override;

    Function AddBubble(Const AX,AY,ARadius:Double; Const AXLabel:String='';
                       AColor:TColor=clTeeColor):Integer;

    Function ClickedPointer( ValueIndex,tmpX,tmpY:Integer;
                             x,y:Integer):Boolean; override;

    Function IsValidSourceOf(Value:TChartSeries):Boolean; override;
  published
    property RadiusValues:TChartValueList read FRadiusValues write SetRadiusValues;
  end;

  TBubbleSeries=class(TCustomBubbleSeries)
  private
    FSquared      : Boolean;

    Function ApplyRadius( const Value:Double;
                          const AList:TChartValueList;
                          Increment:Boolean):Double;
    Procedure SetSquared(Value:Boolean);
  protected
    Procedure AddSampleValues(NumValues:Integer; OnlyMandatory:Boolean=False); override; { <-- to add random radius values }
    Procedure AssignFormatting(Source:TChartSeries); override;
    Procedure DrawLegendShape(ValueIndex:Integer; Const Rect:TRect); override;
    class function GetEditorClass:String; override;
    Function GetGradient:TChartGradient; override;
    Function IsPointInChartRect(ValueIndex:Integer):Boolean; override;
    Procedure PrepareForGallery; override;
    Procedure PreparePointer(ValueIndex:Integer); override;
    procedure RecalcAxisMargins(const Axis:TChartAxis; var R:TRect); override;
  public
    Constructor Create(AOwner: TComponent); override;

    Function NumSampleValues:Integer; override;

    Function MaxYValue:TChartValue; override;  { <-- adds radius }
    Function MinYValue:TChartValue; override;  { <-- substracts radius }
    Function MaxZValue:TChartValue; override;
    Function MinZValue:TChartValue; override;
  published
    property ColorEachPoint default True;
    property Squared:Boolean read FSquared write SetSquared default True;
  end;

implementation

uses
  Math, SysUtils,
  {$IFDEF FMX}
  FMXTee.Chart, FMXTee.Constants
  {$ELSE}
  TeCanvas, Chart, TeeConst
  {$ENDIF}
  ;

type
  TMarksAccess=class(TSeriesMarks);

{ TCustomBubbleSeries }

Constructor TCustomBubbleSeries.Create(AOwner: TComponent);
Begin
  inherited;

  FRadiusValues:=TChartValueList.Create(Self,TeeMsg_ValuesBubbleRadius); { <-- radius storage }

  Pointer.Style:=psCircle; { <-- a Bubble is a circle (by default) }
  Marks.Frame.Hide;
  TMarksAccess(Marks).InitTransparent(True);
end;

Procedure TCustomBubbleSeries.SetRadiusValues(Value:TChartValueList);
Begin
  SetChartValueList(FRadiusValues,Value); { standard method }
end;

{ Helper method, special to Bubble series }
Function TCustomBubbleSeries.AddBubble( Const AX,AY,ARadius:Double;
                                  Const AXLabel:String; AColor:TColor):Integer;
Begin
  RadiusValues.TempValue:=ARadius;
  result:=AddXY(AX,AY,AXLabel,AColor);
end;

Function TCustomBubbleSeries.IsValidSourceOf(Value:TChartSeries):Boolean;
begin
  result:=Value is TCustomBubbleSeries; // Only Bubbles can be assigned to Bubbles
end;

function TCustomBubbleSeries.CalcPointerSize(const ValueIndex:Integer):Single;
begin
  result:=GetVertAxis.CalcSizeValue(RadiusValues.Value[ValueIndex]);
end;

function TCustomBubbleSeries.SavePointer:TPointerSizes;
begin
  result.X:=Pointer.HorizSize;
  result.Y:=Pointer.VertSize;
  result.Size:=Pointer.SizeFloat;
end;

type
  TPointerAccess=class(TSeriesPointer);

procedure TCustomBubbleSeries.RestorePointer(const Value:TPointerSizes);
begin
  TPointerAccess(Pointer).ChangeHorizSize(Value.X);
  TPointerAccess(Pointer).ChangeVertSize(Value.Y);
  Pointer.SizeFloat:=Value.Size;
end;

function TCustomBubbleSeries.ClickedPointer(ValueIndex,tmpX,tmpY,x,y:Integer):Boolean;
var Old : TPointerSizes;
begin
  Old:=SavePointer;

  PreparePointer(ValueIndex);
  result:=Pointer.Clicked(DoGetPointerStyle(ValueIndex),TeePoint(X,Y),tmpX,tmpY);

  RestorePointer(Old);
end;

procedure TCustomBubbleSeries.PreparePointer(ValueIndex: Integer);
var tmp : Single;
    tmpI : Integer;
begin
  tmp:=CalcPointerSize(ValueIndex);
  TPointerAccess(Pointer).SizeFloat:=tmp;

  tmpI:=Round(tmp);
  TPointerAccess(Pointer).ChangeHorizSize(tmpI);
  TPointerAccess(Pointer).ChangeVertSize(tmpI);
end;

function TBubbleSeries.IsPointInChartRect(ValueIndex: Integer): Boolean;
begin
  PreparePointer(ValueIndex);
  result:=inherited IsPointInChartRect(ValueIndex);
end;

procedure TCustomBubbleSeries.DrawValue(ValueIndex:Integer);
Begin
// This overrided method is the main paint for bubble points.
// The bubble effect is achieved by changing the Pointer.Size based
// on the corresponding Radius value for each point in the series.
// We dont use Pointer.Size:=... because that will force a repaint
// while we are painting !! giving recursive endlessly repaints !!!

  PreparePointer(ValueIndex);
  DrawPointer(CalcXPos(ValueIndex),CalcYPos(ValueIndex),ValueColor[ValueIndex],ValueIndex);
// dont call inherited here to avoid drawing the "pointer"
end;

{ TBubbleSeries }

Constructor TBubbleSeries.Create(AOwner: TComponent);
Begin
  inherited;

  TPointerAccess(Pointer).AllowChangeSize:=False;

  FSquared:=True;
  ColorEachPoint:=True;
end;

Procedure TBubbleSeries.SetSquared(Value:Boolean);
Begin
  SetBooleanProperty(FSquared,Value);
end;

Function TBubbleSeries.NumSampleValues:Integer;
begin
  result:=8;
end;

Procedure TBubbleSeries.AddSampleValues(NumValues:Integer; OnlyMandatory:Boolean=False);
Var t : Integer;
    s : TSeriesRandomBounds;
Begin
  s:=RandomBounds(NumValues);

  with s do
  for t:=1 to NumValues do { some sample values to see something in design mode }
  Begin
    AddBubble( tmpX,                { X }
               RandomValue(Round(DifY)), { Y }
               (DifY/15.0)+Round(DifY/(10+RandomValue(15)))  { <- Radius }
               );

    tmpX:=tmpX+StepX;
  end;
end;

Procedure TBubbleSeries.PreparePointer(ValueIndex:Integer);
var tmpSize : Single;
begin
  tmpSize:=CalcPointerSize(ValueIndex);

  if FSquared then
  begin
    TPointerAccess(Pointer).SizeFloat:=tmpSize;
    TPointerAccess(Pointer).ChangeHorizSize(Round(tmpSize));
  end
  else
    TPointerAccess(Pointer).ChangeHorizSize(GetHorizAxis.CalcSizeValue(RadiusValues.Value[ValueIndex]));

  TPointerAccess(Pointer).ChangeVertSize(Round(tmpSize));
end;

Function TBubbleSeries.ApplyRadius( const Value:Double;
                                    const AList:TChartValueList;
                                    Increment:Boolean):Double;
var t : Integer;
    tmpCount : Integer;
    tmpRadius,
    tmpValues : TChartValues;
    tmp: TChartValue;
begin
  result:=Value;

  tmpCount:=Count;

  tmpValues:=AList.Value;
  tmpRadius:=RadiusValues.Value;

  if Increment then
     for t:=0 to tmpCount-1 do
     begin
       tmp:=tmpValues[t]+tmpRadius[t];

       if tmp>result then
          result:=tmp;
     end
  else
     for t:=0 to tmpCount-1 do
     begin
       tmp:=tmpValues[t]-tmpRadius[t];

       if tmp<result then
          result:=tmp;
     end;
end;

procedure TBubbleSeries.RecalcAxisMargins(const Axis:TChartAxis; var R:TRect);

  function IsValid:Boolean;
  begin
    result:=(Count>0) and
            (
             (Axis=GetHorizAxis) or
             ((HorizAxis=aBothHorizAxis) and (Axis=ParentChart.Axes.Top))
            )
            and Pointer.InflateMargins;
  end;

var tmpSize : Integer;
begin
  inherited;

  if IsValid then
  begin
    if Axis.AutomaticMaximum then
    begin
      tmpSize:=GetVertAxis.CalcSizeValue(RadiusValues.Last);

      if R.Right<tmpSize then
         R.Right:=tmpSize;
    end;

    if Axis.AutomaticMinimum then
    begin
      tmpSize:=GetVertAxis.CalcSizeValue(RadiusValues.First);

      if R.Left<tmpSize then
         R.Left:=tmpSize;
    end;
  end;
end;

Function TBubbleSeries.MaxYValue:TChartValue;
Begin
  result:=ApplyRadius(inherited MaxYValue,YValues,True);
end;

Function TBubbleSeries.MinYValue:TChartValue;
Begin
  result:=ApplyRadius(inherited MinYValue,YValues,False);
end;

Procedure TBubbleSeries.AssignFormatting(Source:TChartSeries);
begin
  if Source is TBubbleSeries then
     FSquared:=TBubbleSeries(Source).FSquared;

  inherited;
end;

Function TBubbleSeries.MaxZValue:TChartValue;
begin
  if Pointer.Draw3D then result:=RadiusValues.MaxValue
                    else result:=inherited MaxZValue;
end;

Function TBubbleSeries.MinZValue:TChartValue;
begin
  if Pointer.Draw3D then result:=-RadiusValues.MaxValue
                    else result:=inherited MinZValue;
end;

Procedure TBubbleSeries.DrawLegendShape(ValueIndex:Integer; Const Rect:TRect);
var tmp : Integer;
begin
  with Rect do
       tmp:={$IFDEF FMX}System.Round{$ENDIF}(Math.Min(Right-Left,Bottom-Top));


  TPointerAccess(Pointer).ChangeHorizSize(tmp);
  TPointerAccess(Pointer).ChangeVertSize(tmp);
  inherited;
end;

class function TBubbleSeries.GetEditorClass: String;
begin
  result:='TBubbleSeriesEditor'; // Do not localize
end;

procedure TBubbleSeries.PrepareForGallery;
begin
  inherited;
  Gradient.Visible:=True;
end;

function TBubbleSeries.GetGradient: TChartGradient;
begin
  result:=Pointer.Gradient;
end;

initialization
  RegisterTeeSeries( TBubbleSeries, @TeeMsg_GalleryBubble,
                                    @TeeMsg_GalleryStandard,1);
end.
