{**********************************************}
{   TSeriesRegionTool                          }
{   Copyright (c) 2006-2020 by Marjan Slatinek }
{**********************************************}
unit TeeSeriesRegion;
{$I TeeDefs.inc}

interface

uses
  {$IFNDEF LINUX}
  {$IFNDEF FMX}
  Windows,
  {$ENDIF}
  {$ENDIF}

  SysUtils, Classes,

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

  {$IFDEF FMX}
  FMX.Types,

  {$IFDEF D17}
  System.UIConsts,
  {$ENDIF}

  {$IFDEF D19}
  FMX.Graphics,
  {$ENDIF}

  {$ELSE}
  Graphics, Controls,
  {$ENDIF}

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

type
  // Series region tool - it colors bounded area under the (series) curve.

  TSeriesRegionTool=class(TTeeCustomToolSeries)
  private
    FAutoBounds: Boolean;
    FDrawBehindSeries: Boolean;
    FLowerBound: Double;
    FOrigin: Double;
    FOriginPen: TTeeHiddenPen;
    FTransparency: TTeeTransparency;
    FUpperBound: Double;
    FUseOrigin: Boolean;

    ISeriesDrawn : Boolean;

    function GetColor:TColor;
    function GetGradient:TTeeGradient;
    function IntersectionPoint(const Value: Double; out y: Double): Integer;
    procedure SetAutoBounds(const Value: Boolean);
    procedure SetColor(const Value: TColor);
    procedure SetDrawBehindSeries(const Value: Boolean);
    procedure SetGradient(const Value: TTeeGradient);
    procedure SetLowerBound(const Value: Double);
    procedure SetOrigin(const Value: Double);
    procedure SetOriginPen(const Value: TTeeHiddenPen);
    procedure SetTransparency(const Value: TTeeTransparency);
    procedure SetUpperBound(const Value: Double);
    procedure SetUseOrigin(const Value: Boolean);
  protected
    procedure ChartEvent(AEvent: TChartToolEvent); override;
    procedure DrawRegion; virtual;
    class Function GetEditorClass:String; override;
    Procedure SeriesEvent(AEvent:TChartToolEvent; ASeries:TChartSeries); override;
    procedure SetSeries(const Value: TChartSeries); override;
  public
    Constructor Create(AOwner:TComponent); override;
    Destructor Destroy; override;

    procedure Assign(Source: TPersistent); override;
    class Function Description:String; override;
    class Function LongDescription:String; override;
  published
    property AutoBounds:Boolean read FAutoBounds write SetAutoBounds default True;

    (*:If true, draw region behind series. If false, draw region over series.*)
    property DrawBehindSeries:Boolean read FDrawBehindSeries write
                                              SetDrawBehindSeries default True;

    property Gradient:TTeeGradient read GetGradient write SetGradient;

    (*:Region left (series not mandatory values lower) bound.*)
    property LowerBound: double read FLowerBound write SetLowerBound;

    // Region bottom or upper limit. Used only if UseOrigin is true
    property Origin: Double read FOrigin write SetOrigin;

    // Used to draw a line at Origin
    property OriginPen: TTeeHiddenPen read FOriginPen write SetOriginPen;

    // Region right (series not mandatory values upper) bound.
    property UpperBound: double read FUpperBound write SetUpperBound;

    property UseOrigin:Boolean read FUseOrigin write SetUseOrigin default True;

    property Active;
    property Brush;
    property Color:TColor read GetColor write SetColor default {$IFDEF FMX}claWhite{$ELSE}clWhite{$ENDIF};
    property Pen;
    property Series;
    property Transparency:TTeeTransparency read FTransparency
                                           write SetTransparency default 0;
  end;

implementation

uses
  Math,
  {$IFDEF FMX}
  FMXTee.ProConstants, FMXTee.Series
  {$ELSE}
  TeeProCo, Series
  {$ENDIF}
  ;

{ TSeriesRegionTool }

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

  FAutoBounds:=True;
  FDrawBehindSeries:=True;
  FUseOrigin:=True;
  Brush.BackColor:={$IFDEF FMX}claWhite{$ELSE}clWhite{$ENDIF};

  FOriginPen:=TTeeHiddenPen.Create(CanvasChanged);
end;

Destructor TSeriesRegionTool.Destroy;
begin
  FOriginPen.Free;
  inherited;
end;

procedure TSeriesRegionTool.Assign(Source: TPersistent);
begin
  if Source is TSeriesRegionTool then
  with TSeriesRegionTool(Source) do
  begin
    Self.FAutoBounds:=FAutoBounds;
    Self.FDrawBehindSeries := DrawBehindSeries;
    Self.FLowerBound := LowerBound;
    Self.FOrigin := Origin;
    Self.FOriginPen.Assign(OriginPen);
    Self.FUseOrigin := UseOrigin;
    Self.FTransparency := Transparency;
    Self.FUpperBound := UpperBound;
  end;

  inherited;
end;

procedure TSeriesRegionTool.ChartEvent(AEvent: TChartToolEvent);
begin
  inherited;

  if AEvent=cteBeforeDrawSeries then
     iSeriesDrawn := False;
end;

class function TSeriesRegionTool.Description: String;
begin
  Result := TeeMsg_SeriesRegion;
end;

class Function TSeriesRegionTool.LongDescription:String;
begin
  Result := TeeMsg_SeriesRegionDesc;
end;

procedure TSeriesRegionTool.DrawRegion;

  procedure ClipChartRect;
  var tmpRect : TRect;
  begin
    if Series.UseAxis then
    begin
      tmpRect.Left:=Series.GetHorizAxis.IStartPos;
      tmpRect.Right:=Series.GetHorizAxis.IEndPos;
      tmpRect.Top:=Series.GetVertAxis.IStartPos;
      tmpRect.Bottom:=Series.GetVertAxis.IEndPos;
    end
    else
      tmpRect:=ParentChart.ChartRect;

    {$IFDEF FMX}
    tmpRect.Inflate(-1,-1);
    {$ELSE}
    Dec(tmpRect.Bottom,2);
    Dec(tmpRect.Right,1);
    Inc(tmpRect.Top,2);
    Inc(tmpRect.Left,1);
    {$ENDIF}

    with ParentChart.Canvas do
         ClipRectangle(RectFromRectZ(tmpRect, Series.StartZ));
  end;

var
  ptsLine : TPointArray;

  procedure DrawPolyline;
  begin
    if ParentChart.View3D then
       ParentChart.Canvas.Polyline(ptsLine,Series.StartZ)
    else
       ParentChart.Canvas.Polyline(ptsLine);
  end;

var lb,ub    : Double;
    yl,yu    : Double;
    pts      : TPointArray;
    plen,
    i,
    first,
    last     : Integer;
    tmpR     : TRect;
    tmpBlend : TTeeBlend;
    isStairs : Boolean;
    index    : Integer;
begin
  if Active and Visible and Assigned(ParentChart) and Assigned(Series) then
  begin
    lb:=Series.NotMandatoryValueList.MinValue;
    ub:=Series.NotMandatoryValueList.MaxValue;

    isStairs:=(Series is TCustomSeries) and (Series as TCustomSeries).Stairs;

    if not FAutoBounds then
    begin
      lb:=Max(lb,FLowerBound);
      ub:=Min(ub,FUpperBound);
    end;

    // plot only if it makes sense
    if (ub>Series.NotMandatoryValueList.MinValue) and
       (lb<Series.NotMandatoryValueList.MaxValue) then
    begin
      first:=IntersectionPoint(lb,yl);
      last:=IntersectionPoint(ub,yu);

      if last<first then
         SwapInteger(last,first);

      plen:= last - first + 1;

      if isStairs then
         plen:=(plen*2)-1;

      SetLength(pts,plen+4); // 4 extra points
      try
        for i := 0 to plen-1 do
        begin
          if isStairs then
             index:=i div 2
          else
             index:=i;

          if isStairs and (i mod 2 <> 0) then //It's a stair point
             pts[i].X := Series.CalcXPos(index+first+1)
          else
             pts[i].X := Series.CalcXPos(index+first);

          pts[i].Y := Series.CalcYPos(index+first);
        end;

        // upper bound intersect point
        pts[plen].X := Series.CalcXPosValue(ub);
        pts[plen].Y := Series.CalcYPosValue(yu);

        // upper bound origin point
        pts[plen+1].X := pts[plen].X;

        if FUseOrigin then
           pts[plen+1].Y := Min(Series.GetVertAxis.IEndPos,
                                Series.CalcYPosValue(FOrigin))
        else
           pts[plen+1].Y := Series.GetVertAxis.IEndPos;

        // lower bound origin point
        pts[plen+2].X := Series.CalcXPosValue(lb);
        pts[plen+2].Y := pts[plen+1].Y;

        // lower bound intersect point
        pts[plen+3].X := pts[plen+2].X;
        pts[plen+3].Y := Series.CalcYPosValue(yl);

        // Draw

        if ParentChart.ClipPoints then
           ClipChartRect;

        if Transparency>0 then
        begin
          if ParentChart.Canvas.SupportsAlpha then
             tmpR:=TeeZeroRect
          else
          begin
            tmpR:=PolygonBounds(pts);

            if ParentChart.View3D then
               tmpR:=ParentChart.Canvas.RectFromRectZ(tmpR,Series.StartZ);
          end;

          tmpBlend:=ParentChart.Canvas.BeginBlending(tmpR,Transparency);
        end
        else
          tmpBlend:=nil;

        ParentChart.Canvas.AssignBrush(Brush);

        ParentChart.Canvas.Pen.Style:=psClear;

        if ParentChart.View3D then
           ParentChart.Canvas.PolygonWithZ(pts,Series.StartZ)
        else
           ParentChart.Canvas.Polygon(pts);

        if Pen.Visible then
        begin
          ParentChart.Canvas.AssignVisiblePen(Pen);

          SetLength(ptsLine,plen);
          try
            for i:=0 to pLen-1 do
                ptsLine[i]:=pts[i];

            DrawPolyline;
          finally
            ptsLine:=nil;
          end;
        end;

        if OriginPen.Visible then
        begin
          ParentChart.Canvas.AssignVisiblePen(OriginPen);

          SetLength(ptsLine,4);
          try
            ptsLine[0]:=pts[pLen];
            ptsLine[1]:=pts[pLen+1];
            ptsLine[2]:=pts[pLen+2];
            ptsLine[3]:=pts[pLen+3];

            DrawPolyline;
          finally
            ptsLine:=nil;
          end;
        end;

        if Transparency>0 then
           ParentChart.Canvas.EndBlending(tmpBlend);

        if ParentChart.ClipPoints then
           ParentChart.Canvas.UnClipRectangle;

      finally
        pts := nil;
      end;
    end;
  end;
end;

class function TSeriesRegionTool.GetEditorClass: String;
begin
  result:='TSeriesRegionEditor';
end;

function TSeriesRegionTool.GetColor:TColor;
begin
  result:=Brush.BackColor;
end;

function TSeriesRegionTool.GetGradient:TTeeGradient;
begin
  result:=Brush.Gradient;
end;

Function TSeriesRegionTool.IntersectionPoint(const Value: double; out y: double): Integer;
var i: Integer;
    k: double;
begin
  i := 0;
  y := Series.MandatoryValueList[i];

  while (Value>Series.NotMandatoryValueList[i]) and (i<Series.Count) do
        Inc(i);

  // We have two choices:
  // #1: value is exactly at point coordinate
  // #2: value is between two points - use linear interpolation to calculate y

  with Series do
  if Value=NotMandatoryValueList[i] then
     y := MandatoryValueList[i]
  else
  if (i>0) and (i<Series.Count) then
  begin
    k := (MandatoryValueList[i]-MandatoryValueList[i-1]) /
         (NotMandatoryValueList[i]-NotMandatoryValueList[i-1]);

    y := MandatoryValueList[i-1] + k*(Value-NotMandatoryValueList[i-1]);
  end;

  Result := i; // return point index
end;

procedure TSeriesRegionTool.SetColor(const Value: TColor);
begin
  Brush.BackColor:=Value;
end;

procedure TSeriesRegionTool.SetDrawBehindSeries(const Value: Boolean);
begin
  SetBooleanProperty(FDrawBehindSeries,Value);
end;

procedure TSeriesRegionTool.SetGradient(const Value: TTeeGradient);
begin
  Gradient.Assign(Value);
end;

procedure TSeriesRegionTool.SetLowerBound(const Value: double);
begin
  SetDoubleProperty(FLowerBound,Value);
end;

procedure TSeriesRegionTool.SetOrigin(const Value: double);
begin
  SetDoubleProperty(FOrigin,Value);
end;

procedure TSeriesRegionTool.SetOriginPen(const Value: TTeeHiddenPen);
begin
  FOriginPen.Assign(Value);
end;

procedure TSeriesRegionTool.SetSeries(const Value: TChartSeries);
begin
  inherited;
  Repaint;
end;

procedure TSeriesRegionTool.SetTransparency(const Value: TTeeTransparency);
begin
  if FTransparency<>Value then
  begin
    FTransparency:=Value;
    Repaint;
  end;
end;

procedure TSeriesRegionTool.SetUpperBound(const Value: double);
begin
  SetDoubleProperty(FUpperBound,Value);
end;

procedure TSeriesRegionTool.SetUseOrigin(const Value: boolean);
begin
  SetBooleanProperty(FUseOrigin,Value);
end;

procedure TSeriesRegionTool.SetAutoBounds(const Value: Boolean);
begin
  SetBooleanProperty(FAutoBounds,Value);
end;

procedure TSeriesRegionTool.SeriesEvent(AEvent: TChartToolEvent; ASeries:TChartSeries);
begin
  if Assigned(Series) and (ASeries=Series) then
     if ((AEvent=cteBeforeDrawSeries) and DrawBehindSeries) or
        ((AEvent=cteAfterDrawSeries) and (not DrawBehindSeries)) then
            DrawRegion;
end;

initialization
  RegisterTeeTools([TSeriesRegionTool]);
finalization
  UnRegisterTeeTools([TSeriesRegionTool]);
end.
