{

 *****************************************************************************
  See the file COPYING.modifiedLGPL.txt, included in this distribution,
  for details about the license.
 *****************************************************************************

  Authors: Alexander Klenin

}

unit TASources;

{$MODE ObjFPC}{$H+}

interface

uses
  Classes, Types, TAChartUtils, TACustomSource;

type

  { TListChartSource }

  TListChartSource = class(TCustomSortedChartSource)
  private
    FDataPoints: TStrings;
    procedure ClearCaches;
    function NewItem: PChartDataItem;
    procedure SetDataPoints(const AValue: TStrings);
    procedure UpdateCachesAfterAdd(const AX, AY: Double);
  protected
    procedure Loaded; override;
    procedure SetXCount(AValue: Cardinal); override;
    procedure SetYCount(AValue: Cardinal); override;
  public
    type
      EXListEmptyError = class(EChartError);
      EYListEmptyError = class(EChartError);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  public
    function Add(
      const AX, AY: Double; const ALabel: String = '';
      const AColor: TChartColor = clTAColor): Integer;
    function AddXListY(
      const AX: array of Double; const AY: Double; const ALabel: String = '';
      const AColor: TChartColor = clTAColor): Integer;
    function AddXListYList(const AX, AY: array of Double; const ALabel: String = '';
      const AColor: TChartColor = clTAColor): Integer;
    function AddXYList(
      const AX: Double; const AY: array of Double; const ALabel: String = '';
      const AColor: TChartColor = clTAColor): Integer;
    procedure Clear;
    procedure CopyFrom(ASource: TCustomChartSource); virtual;
    procedure Delete(AIndex: Integer);
    function SetColor(AIndex: Integer; AColor: TChartColor): Integer;
    function SetText(AIndex: Integer; const AValue: String): Integer;
    function SetXList(AIndex: Integer; const AXList: array of Double): Integer;
    function SetXValue(AIndex: Integer; const AValue: Double): Integer;
    function SetYList(AIndex: Integer; const AYList: array of Double): Integer;
    function SetYValue(AIndex: Integer; const AValue: Double): Integer;
    property UseSortedAutoDetection;
  published
    property DataPoints: TStrings read FDataPoints write SetDataPoints;
    property XCount;
    property XErrorBarData;
    property YCount;
    property YErrorBarData;
    // Sorting
    property SortBy;
    property SortDir;
    property Sorted;
    property SortIndex;
    property OnCompare;
  end;

  { TBuiltinListChartSource }

  TBuiltinListChartSource = class(TListChartSource)
  private
    FXCountMin: Cardinal;
    FYCountMin: Cardinal;
  protected
    procedure SetXCount(AValue: Cardinal); override;
    procedure SetYCount(AValue: Cardinal); override;
  public
    constructor Create(AOwner: TComponent; AXCountMin, AYCountMin: Cardinal); reintroduce; //overload;
  public
    procedure CopyFrom(ASource: TCustomChartSource); override;
  end;

  { TSortedChartSource }

  TSortedChartSource = class(TCustomSortedChartSource)
  strict private
    FListener: TListener;
    FListenerSelf: TListener;
    FOrigin: TCustomChartSource;
    procedure Changed(ASender: TObject);
    procedure SetOrigin(AValue: TCustomChartSource);
  protected
    function DoCompare(AItem1, AItem2: Pointer): Integer; override;
    function GetCount: Integer; override;
    function GetItem(AIndex: Integer): PChartDataItem; override;
    procedure ResetTransformation(ACount: Integer);
    procedure SetXCount(AValue: Cardinal); override;
    procedure SetYCount(AValue: Cardinal); override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  public
    function BasicExtent: TDoubleRect; override;
    function Extent: TDoubleRect; override;
    function ExtentCumulative: TDoubleRect; override;
    function ExtentList: TDoubleRect; override;
    function ExtentXYList: TDoubleRect; override;
    function ValuesTotal: Double; override;
    property UseSortedAutodetection;
  published
    property Origin: TCustomChartSource read FOrigin write SetOrigin;
    // Sorting
    property SortBy;
    property SortDir;
    property Sorted;
    property SortIndex;
    property OnCompare;
  end;

  { TMWCRandomGenerator }

  // Mutliply-with-carry random number generator.
  // Algorithm by George Marsaglia.
  // A generator is incapsulated in a class to allow using many simultaneous
  // random sequences, each determined by its own seed.
  TMWCRandomGenerator = class
  strict private
    FHistory: array [0..4] of LongWord;
    procedure SetSeed(AValue: Integer);
  public
    function Get: LongWord;
    function GetInRange(AMin, AMax: Integer): Integer;
    property Seed: Integer write SetSeed;
  end;

  { TRandomChartSource }

  TRandomChartSource = class(TCustomChartSource)
  strict private
    FPointsNumber: Integer;
    FRandomColors: Boolean;
    FRandomX: Boolean;
    FRandSeed: Integer;
    FXMax: Double;
    FXMin: Double;
    FYMax: Double;
    FYMin: Double;
    FYNanPercent: TPercent;
  strict private
    FCurIndex: Integer;
    FCurItem: TChartDataItem;
    FRNG: TMWCRandomGenerator;

    procedure Reset;
    procedure SetPointsNumber(AValue: Integer);
    procedure SetRandomColors(AValue: Boolean);
    procedure SetRandomX(AValue: Boolean);
    procedure SetRandSeed(AValue: Integer);
    procedure SetXMax(const AValue: Double);
    procedure SetXMin(const AValue: Double);
    procedure SetYMax(const AValue: Double);
    procedure SetYMin(const AValue: Double);
    procedure SetYNanPercent(AValue: TPercent);
  protected
    procedure ChangeErrorBars(Sender: TObject); override;
    function GetCount: Integer; override;
    function GetItem(AIndex: Integer): PChartDataItem; override;
    procedure SetXCount(AValue: Cardinal); override;
    procedure SetYCount(AValue: Cardinal); override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  public
    function IsSorted: Boolean; override;
  published
    property PointsNumber: Integer read FPointsNumber write SetPointsNumber default 0;
    property RandomColors: Boolean read FRandomColors write SetRandomColors default false;
    property RandomX: Boolean read FRandomX write SetRandomX default false;
    property RandSeed: Integer read FRandSeed write SetRandSeed;
    property XCount;
    property XMax: Double read FXMax write SetXMax;
    property XMin: Double read FXMin write SetXMin;
    property YCount;
    property YMax: Double read FYMax write SetYMax;
    property YMin: Double read FYMin write SetYMin;
    property YNanPercent: TPercent read FYNanPercent write SetYNanPercent default 0;
    property XErrorBarData;
    property YErrorBarData;
  end;

  TUserDefinedChartSource = class;

  TGetChartDataItemEvent = procedure (
    ASource: TUserDefinedChartSource; AIndex: Integer;
    var AItem: TChartDataItem) of object;

  { TUserDefinedChartSource }

  TUserDefinedChartSource = class(TCustomChartSource)
  strict private
    FItem: TChartDataItem;
    FOnGetChartDataItem: TGetChartDataItemEvent;
    FPointsNumber: Integer;
    FSorted: Boolean;
    procedure SetOnGetChartDataItem(AValue: TGetChartDataItemEvent);
    procedure SetPointsNumber(AValue: Integer);
  protected
    function GetCount: Integer; override;
    function GetItem(AIndex: Integer): PChartDataItem; override;
    procedure SetXCount(AValue: Cardinal); override;
    procedure SetYCount(AValue: Cardinal); override;
  public
    procedure EndUpdate; override;
    function IsSorted: Boolean; override;
    procedure Reset; inline;
  published
    property OnGetChartDataItem: TGetChartDataItemEvent
      read FOnGetChartDataItem write SetOnGetChartDataItem;
    property PointsNumber: Integer
      read FPointsNumber write SetPointsNumber default 0;
    property Sorted: Boolean read FSorted write FSorted default false;
    property XCount;
    property XErrorBarData;
    property YCount;
    property YErrorBarData;
  end;

  TChartAccumulationMethod = (
    camNone, camSum, camAverage, camDerivative, camSmoothDerivative);
  TChartAccumulationDirection = (cadBackward, cadForward, cadCenter);

  { TCalculatedChartSource }

  TCalculatedChartSource = class(TCustomChartSource)
  strict private
    FAccumulationDirection: TChartAccumulationDirection;
    FAccumulationMethod: TChartAccumulationMethod;
    FAccumulationRange: Cardinal;
    FHistory: TChartSourceBuffer;
    FIndex: Integer;
    FItem: TChartDataItem;
    FListener: TListener;
    FOrigin: TCustomChartSource;
    FOriginYCount: Cardinal;
    FPercentage: Boolean;
    FReorderYList: String;
    FSorted: Boolean;
    FYOrder: array of Integer;

    procedure CalcAccumulation(AIndex: Integer);
    procedure CalcDerivative(AIndex: Integer);
    procedure CalcPercentage;
    procedure Changed(ASender: TObject);
    function EffectiveAccumulationRange: Cardinal;
    procedure ExtractItem(AIndex: Integer);
    function IsDerivative: Boolean; inline;
    procedure RangeAround(AIndex: Integer; out ALeft, ARight: Integer);
    procedure SetAccumulationDirection(AValue: TChartAccumulationDirection);
    procedure SetAccumulationMethod(AValue: TChartAccumulationMethod);
    procedure SetAccumulationRange(AValue: Cardinal);
    procedure SetOrigin(AValue: TCustomChartSource);
    procedure SetPercentage(AValue: Boolean);
    procedure SetReorderYList(const AValue: String);
    procedure UpdateYOrder;
  protected
    function GetCount: Integer; override;
    function GetItem(AIndex: Integer): PChartDataItem; override;
    procedure SetXCount(AValue: Cardinal); override;
    procedure SetYCount(AValue: Cardinal); override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    function IsSorted: Boolean; override;
  published
    property AccumulationDirection: TChartAccumulationDirection
      read FAccumulationDirection write SetAccumulationDirection
      default cadBackward;
    property AccumulationMethod: TChartAccumulationMethod
      read FAccumulationMethod write SetAccumulationMethod default camNone;
    property AccumulationRange: Cardinal
      read FAccumulationRange write SetAccumulationRange default 2;

    property Origin: TCustomChartSource read FOrigin write SetOrigin;
    property Percentage: Boolean
      read FPercentage write SetPercentage default false;
    property ReorderYList: String read FReorderYList write SetReorderYList;
  end;

procedure Register;

implementation

uses
  Math, StrUtils, SysUtils, TAMath, TAChartStrConsts;

type
  TCustomChartSourceAccess = class(TCustomChartSource);

  { TListChartSourceStrings }

  TListChartSourceStrings = class(TStrings)
  strict private
    FSource: TListChartSource;
    FLoadingCache: TStringList;
    procedure Parse(const AString: String; ADataItem: PChartDataItem);
  private
    procedure LoadingFinished;
  protected
    function Get(Index: Integer): String; override;
    function GetCount: Integer; override;
    procedure Put(Index: Integer; const S: String); override;
    procedure SetTextStr(const Value: string); override;
    procedure SetUpdateState(AUpdating: Boolean); override;
  public
    constructor Create(ASource: TListChartSource);
    destructor Destroy; override;
    procedure Assign(Source: TPersistent); override;
    procedure Clear; override;
    procedure Delete(Index: Integer); override;
    procedure Insert(Index: Integer; const S: String); override;
  end;

procedure Register;
begin
  RegisterComponents(
    CHART_COMPONENT_IDE_PAGE, [
      TListChartSource, TSortedChartSource, TRandomChartSource,
      TUserDefinedChartSource, TCalculatedChartSource
    ]);
end;

{ TListChartSourceStrings }

procedure TListChartSourceStrings.Assign(Source: TPersistent);
var
  SaveSorted: Boolean;
begin
  BeginUpdate;
  try
    // new data may come unsorted - to avoid exception in TCustomSortedChartSource.
    // ItemInsert() in this case, we disable FSorted temporarily - then we'll sort
    // the whole dataset if needed
    SaveSorted := FSource.FSorted;
    try
      FSource.FSorted := false;
      inherited Assign(Source);
    finally
      FSource.FSorted := SaveSorted;
    end;

    if FSource.IsSorted then FSource.Sort;
  finally
    EndUpdate;
  end;
end;

procedure TListChartSourceStrings.Clear;
begin
  if not (csLoading in FSource.ComponentState) then
    FSource.Clear
  else
    FreeAndNil(FLoadingCache);
end;

constructor TListChartSourceStrings.Create(ASource: TListChartSource);
begin
  inherited Create;
  FSource := ASource;
end;

destructor TListChartSourceStrings.Destroy;
begin
  inherited;
  FLoadingCache.Free;
end;

procedure TListChartSourceStrings.Delete(Index: Integer);
begin
  FSource.Delete(Index);
end;

function TListChartSourceStrings.Get(Index: Integer): String;

  function NumberStr(const AValue: Double): String;
  begin
    if IsNaN(AValue) then
      Result := '|'
    else
      Result := FloatToStr(AValue, DefSeparatorSettings) + '|';
  end;

var
  i: Integer;
  s: String;
  color_text_mask: String;
begin
  with FSource[Index]^ do begin
    Result := '';
    if FSource.XCount > 0 then
      Result += NumberStr(X);
    for i := 0 to High(XList) do
      Result += NumberStr(XList[i]);
    if FSource.YCount > 0 then
      Result += NumberStr(Y);
    for i := 0 to High(YList) do
      Result += NumberStr(YList[i]);
    color_text_mask := '%s|%s';
    s := Text;
    if pos('"', s) > 0 then begin
      s := StringReplace(s, '"', '""', [rfReplaceAll]);
      color_text_mask := '%s|"%s"'
    end;
    if pos('|', s) > 0 then
      color_text_mask := '%s|"%s"';
    Result += Format(color_text_mask, [IntToColorHex(Color), s]);
  end;
end;

function TListChartSourceStrings.GetCount: Integer;
begin
  if not (csLoading in FSource.ComponentState) then
    Result := FSource.Count
  else
  if Assigned(FLoadingCache) then
    Result := FLoadingCache.Count
  else
    Result := 0;
end;

procedure TListChartSourceStrings.Insert(Index: Integer; const S: String);
var
  item: PChartDataItem;
begin
  if csLoading in FSource.ComponentState then begin
    if not Assigned(FLoadingCache) then
      FLoadingCache := TStringList.Create;
    FLoadingCache.Insert(Index, S);
    exit;
  end;

  item := FSource.NewItem;
  try
    Parse(S, item);
    FSource.ItemInsert(Index, item);
  except
    Dispose(item);
    raise;
  end;
  FSource.UpdateCachesAfterAdd(item^.X, item^.Y);
end;

procedure TListChartSourceStrings.Parse(
  const AString: String; ADataItem: PChartDataItem);
var
  p: Integer = 0;
  parts: TStrings;

  function NextPart: String;
  begin
    if p < parts.Count then
      Result := parts[p]
    else
      Result := '';
    p += 1;
  end;

  function StrToFloatOrDateTime(const AStr: String): Double;
  begin
    if (AStr = '') or (AStr = '?') then
      Result := NaN
    else begin
      if not TryStrToFloat(AStr, Result, DefSeparatorSettings) and
         not TryStrToFloat(AStr, Result) and
         not TryStrToDateTime(AStr, Result)
      then
        raise EListSourceStringError.CreateFmt(rsListSourceNumericError, [NameOrClassName(FSource), AStr]);
    end;
  end;

  function StrToInt(const AStr: String): Integer;
  begin
    if (AStr = '') or (AStr = '?') then
      Result := clTAColor
    else
    if not TryStrToInt(AStr, Result) then
      raise EListSourceStringError.CreateFmt(rsListSourceColorError, [NameOrClassName(FSource), AStr]);
  end;

var
  i: Integer;
begin
  // Note: this method is called only when component loading is fully finished -
  // so FSource.XCount and FSource.YCount are already properly estabilished

  parts := Split(AString);
  try
    // There must be XCount + YCount + 2 parts of the string (+2 for Color and Text)
    // Text must be quoted if it contains '|'.
    if (Cardinal(parts.Count) <> FSource.XCount + FSource.YCount + 2) then
      raise EListSourceStringError.CreateFmt(
        rsListSourceStringFormatError, [NameOrClassName(FSource), ChopString(AString, 20)]);

    with ADataItem^ do begin
      if FSource.XCount > 0 then begin
        X := StrToFloatOrDateTime(NextPart);
        for i := 0 to High(XList) do
          XList[i] := StrToFloatOrDateTime(NextPart);
      end else
        X := NaN;
      if FSource.YCount > 0 then begin
        Y := StrToFloatOrDateTime(NextPart);
        for i := 0 to High(YList) do
          YList[i] := StrToFloatOrDateTime(NextPart);
      end else
        Y := NaN;
      Color := StrToInt(NextPart);
      Text := NextPart;
    end;
  finally
    parts.Free;
  end;
end;

procedure TListChartSourceStrings.Put(Index: Integer; const S: String);
begin
  FSource.BeginUpdate;
  try
    Parse(S, FSource[Index]);
  finally
    FSource.EndUpdate;
  end;
end;

procedure TListChartSourceStrings.SetTextStr(const Value: string);
var
  SaveSorted: Boolean;
begin
  BeginUpdate;
  try
    // new data may come unsorted - to avoid exception in TCustomSortedChartSource.
    // ItemInsert() in this case, we disable FSorted temporarily - then we'll sort
    // the whole dataset if needed
    SaveSorted := FSource.FSorted;
    try
      FSource.FSorted := false;
      inherited SetTextStr(Value);
    finally
      FSource.FSorted := SaveSorted;
    end;

    if FSource.IsSorted then FSource.Sort;
  finally
    EndUpdate;
  end;
end;

procedure TListChartSourceStrings.SetUpdateState(AUpdating: Boolean);
begin
  if not (csLoading in FSource.ComponentState) then
    if AUpdating then
      FSource.BeginUpdate
    else
      FSource.EndUpdate;
end;

procedure TListChartSourceStrings.LoadingFinished;
begin
  // csLoading in FSource.ComponentState is already cleared
  if Assigned(FLoadingCache) then
    try
      Assign(FLoadingCache);
    finally  
      FreeAndNil(FLoadingCache);
    end;
end;

{ TListChartSource }

function TListChartSource.Add(
  const AX, AY: Double; const ALabel: String = '';
  const AColor: TChartColor = clTAColor): Integer;
var
  pcd: PChartDataItem;
begin
  pcd := NewItem;
  try
    pcd^.X := AX;
    pcd^.Y := AY;
    pcd^.Color := AColor;
    pcd^.Text := ALabel;
    Result := ItemAdd(pcd);
  except
    Dispose(pcd);
    raise;
  end;
  UpdateCachesAfterAdd(AX, AY);
end;

function TListChartSource.AddXListY(
  const AX: array of Double; const AY: Double;
  const ALabel: String = ''; const AColor: TChartColor = clTAColor): Integer;
begin
  if Length(AX) = 0 then
    raise EXListEmptyError.Create('AddXListY: XList is empty');

  { Optimization: prevent useless notifications.
    Don't call BeginUpdate() to avoid invalidating the caches. }
  Inc(FUpdateCount);
  try
    Result := Add(AX[0], AY, ALabel, AColor);
    if Length(AX) > 1 then
      Result := SetXList(Result, AX[1..High(AX)]);
  finally
    Dec(FUpdateCount);
  end;
  UpdateCachesAfterAdd(AX[0], AY);
end;

function TListChartSource.AddXListYList(const AX, AY: array of Double;
  const ALabel: String = ''; const AColor: TChartColor = clTAColor): Integer;
begin
  if Length(AX) = 0 then
    raise EXListEmptyError.Create('AddXListYList: XList is empty');
  if Length(AY) = 0 then
    raise EYListEmptyError.Create('AddXListYList: YList is empty');

  { Optimization: prevent useless notifications.
    Don't call BeginUpdate() to avoid invalidating the caches. }
  Inc(FUpdateCount);
  try
    Result := Add(AX[0], AY[0], ALabel, AColor);
    if Length(AX) > 1 then
      Result := SetXList(Result, AX[1..High(AX)]);
    if Length(AY) > 1 then
      Result := SetYList(Result, AY[1..High(AY)]);
  finally
    Dec(FUpdateCount);
  end;
  UpdateCachesAfterAdd(AX[0], AY[0]);
end;

function TListChartSource.AddXYList(
  const AX: Double; const AY: array of Double;
  const ALabel: String = ''; const AColor: TChartColor = clTAColor): Integer;
begin
  if Length(AY) = 0 then
    raise EYListEmptyError.Create('AddXYList: YList is empty');

  { Optimization: prevent useless notifications.
    Don't call BeginUpdate() to avoid invalidating the caches. }
  Inc(FUpdateCount);
  try
    Result := Add(AX, AY[0], ALabel, AColor);
    if Length(AY) > 1 then
      Result := SetYList(Result, AY[1..High(AY)]);
  finally
    Dec(FUpdateCount);
  end;
  UpdateCachesAfterAdd(AX, AY[0]);
end;

procedure TListChartSource.Clear;
var
  i: Integer;
begin
  for i := 0 to Count - 1 do
    Dispose(ItemInternal[i]);
  FData.Clear;
  ItemDeleted(-1);
  ClearCaches;
  Notify;
end;

procedure TListChartSource.ClearCaches;
begin
  // When updating, we are not allowed to set "Valid" for caches - cached
  // data is not synchronized with real data when updating, so setting "Valid"
  // could lead to reading outdated cache contents, when calling methods like
  // BasicExtent() when still in update mode
  FBasicExtent := EmptyExtent;
  FBasicExtentIsValid := not IsUpdating;
  FCumulativeExtent := EmptyExtent;
  FCumulativeExtentIsValid := not IsUpdating;
  FXListExtent := EmptyExtent;
  FXListExtentIsValid := not IsUpdating;
  FYListExtent := EmptyExtent;
  FYListExtentIsValid := not IsUpdating;
  FValuesTotal := 0;
  FValuesTotalIsValid := not IsUpdating;
end;

procedure TListChartSource.CopyFrom(ASource: TCustomChartSource);
var
  i: Integer;
  pcd: PChartDataItem;
begin
  if ASource = Self then exit;
  BeginUpdate;
  try
    Clear;
    XCount := ASource.XCount;
    YCount := ASource.YCount;
    FData.Capacity := ASource.Count;

    pcd := nil;
    try // optimization: don't execute try..except..end in a loop
      for i := 0 to ASource.Count - 1 do begin
        pcd := NewItem;
        pcd^.CopyFrom(ASource[i]);
        FData.Add(pcd); // don't use ItemAdd() here
        pcd := nil;
      end;

      // Remove NaN-points from the end. They can occur when ASource is
      // a DbChartSource for which RecordCount may be too large.
      for i := Count-1 downto 0 do
      begin
        pcd := FData[i];
        if IsNaN(pcd^.Point) then begin
          Dispose(pcd);
          FData.Delete(i);
        end else
          break;
      end;
    except
      if pcd <> nil then
        Dispose(pcd);
      raise;
    end;

    // We added data directly, without using ItemAdd() calls,
    // so we can no longer be sure, that data is sorted.
    ResetSortedAutoDetection;

    if HasSameSorting(ASource) then
      SetSortedAutoDetected
    else
    if IsSorted then
      Sort;
  finally
    EndUpdate;
  end;
end;

constructor TListChartSource.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FDataPoints := TListChartSourceStrings.Create(Self);
  UseSortedAutoDetection := true;
  ClearCaches;
end;

procedure TListChartSource.Delete(AIndex: Integer);
begin
  // Optimization
  if IsUpdating then begin
    Dispose(ItemInternal[AIndex]);
    FData.Delete(AIndex);
    ItemDeleted(AIndex);
    exit;
  end;

  with ItemInternal[AIndex]^ do begin
    FBasicExtentIsValid := FBasicExtentIsValid and
      (((FBasicExtent.a.X < X) and (X < FBasicExtent.b.X)) or (XCount = 0)) and
      (((FBasicExtent.a.Y < Y) and (Y < FBasicExtent.b.Y)) or (YCount = 0));
    if FValuesTotalIsValid then
      FValuesTotal -= NumberOr(Y);
  end;
  FCumulativeExtentIsValid := false;
  FXListExtentIsValid := false;
  FYListExtentIsValid := false;
  Dispose(ItemInternal[AIndex]);
  FData.Delete(AIndex);
  ItemDeleted(AIndex);
  Notify;
end;

destructor TListChartSource.Destroy;
begin
  if Assigned(FData) then
    Clear;
  FreeAndNil(FDataPoints);
  inherited;
end;

function TListChartSource.NewItem: PChartDataItem;
begin
  New(Result);
  if XCount > 1 then SetLength(Result^.XList, XCount - 1);
  if YCount > 1 then SetLength(Result^.YList, YCount - 1);
end;

function TListChartSource.SetColor(AIndex: Integer; AColor: TChartColor): Integer;
begin
  with ItemInternal[AIndex]^ do begin
    if Color = AColor then exit(AIndex);
    Color := AColor;
  end;
  Result := ItemModified(AIndex);
  Notify;
end;

procedure TListChartSource.SetDataPoints(const AValue: TStrings);
begin
  if FDataPoints = AValue then exit;
  FDataPoints.Assign(AValue);
end;

function TListChartSource.SetText(AIndex: Integer; const AValue: String): Integer;
begin
  with ItemInternal[AIndex]^ do begin
    if Text = AValue then exit(AIndex);
    Text := AValue;
  end;
  Result := ItemModified(AIndex);
  Notify;
end;

procedure TListChartSource.SetXCount(AValue: Cardinal);
var
  i, nx: Integer;
begin
  if AValue = FXCount then exit;
  FXCount := AValue;
  nx := Max(FXCount - 1, 0);
  for i := 0 to Count - 1 do
    SetLength(ItemInternal[i]^.XList, nx);
  InvalidateCaches;
  Notify;
end;

function TListChartSource.SetXList(
  AIndex: Integer; const AXList: array of Double): Integer;
var
  i: Integer;
begin
  with ItemInternal[AIndex]^ do
    for i := 0 to Min(High(AXList), High(XList)) do
      XList[i] := AXList[i];
  FXListExtentIsValid := false;
  Result := ItemModified(AIndex);
  Notify;
end;

function TListChartSource.SetXValue(AIndex: Integer; const AValue: Double): Integer;
var
  oldX: Double;

  procedure UpdateExtent;
  begin
    if (not FBasicExtentIsValid) or (XCount = 0) then exit;

    if not IsNan(AValue) then begin
      if AValue < FBasicExtent.a.X then
        FBasicExtent.a.X := AValue
      else if AValue > FBasicExtent.b.X then
        FBasicExtent.b.X := AValue;
    end;

    if not IsNan(oldX) then
      FBasicExtentIsValid := (oldX <> FBasicExtent.a.X) and (oldX <> FBasicExtent.b.X);
  end;

begin
  with ItemInternal[AIndex]^ do begin
    if IsEquivalent(X, AValue) then exit(AIndex); // IsEquivalent() can compare also NaNs
    oldX := X;
    X := AValue;
  end;
  UpdateExtent;
  Result := ItemModified(AIndex);
  Notify;
end;

procedure TListChartSource.SetYCount(AValue: Cardinal);
var
  i, ny: Integer;
begin
  if AValue = FYCount then exit;
  FYCount := AValue;
  ny := Max(FYCount - 1, 0);
  for i := 0 to Count - 1 do
    SetLength(ItemInternal[i]^.YList, ny);
  InvalidateCaches;
  Notify;
end;

function TListChartSource.SetYList(
  AIndex: Integer; const AYList: array of Double): Integer;
var
  i: Integer;
begin
  with ItemInternal[AIndex]^ do
    for i := 0 to Min(High(AYList), High(YList)) do
      YList[i] := AYList[i];
  FCumulativeExtentIsValid := false;
  FYListExtentIsValid := false;
  Result := ItemModified(AIndex);
  Notify;
end;

function TListChartSource.SetYValue(AIndex: Integer; const AValue: Double): Integer;
var
  oldY: Double;

  procedure UpdateExtent;
  begin
    if (not FBasicExtentIsValid) or (YCount = 0) then exit;

    if not IsNan(AValue) then begin
      if AValue < FBasicExtent.a.Y then
        FBasicExtent.a.Y := AValue
      else if AValue > FBasicExtent.b.Y then
        FBasicExtent.b.Y := AValue;
    end;

    if not IsNan(oldY) then
      FBasicExtentIsValid := (oldY <> FBasicExtent.a.Y) and (oldY <> FBasicExtent.b.Y);
  end;

begin
  with ItemInternal[AIndex]^ do begin
    if IsEquivalent(Y, AValue) then exit(AIndex); // IsEquivalent() can compare also NaNs
    oldY := Y;
    Y := AValue;
  end;
  if FValuesTotalIsValid then
    FValuesTotal += NumberOr(AValue) - NumberOr(oldY);
  UpdateExtent;
  Result := ItemModified(AIndex);
  Notify;
end;

procedure TListChartSource.UpdateCachesAfterAdd(const AX, AY: Double);
begin
  if IsUpdating then exit; // Optimization
  if FBasicExtentIsValid then begin
    if FXCount > 0 then UpdateMinMax(AX, FBasicExtent.a.X, FBasicExtent.b.X);
    if FYCount > 0 then UpdateMinMax(AY, FBasicExtent.a.Y, FBasicExtent.b.Y);
  end;
  if FValuesTotalIsValid then
    FValuesTotal += NumberOr(AY);
  FCumulativeExtentIsValid := false;
  FXListExtentIsValid := false;
  FYListExtentIsValid := false;
  Notify;
end;

procedure TListChartSource.Loaded;
begin
  inherited; // clears csLoading in ComponentState
  (FDataPoints as TListChartSourceStrings).LoadingFinished;
end;


{ TBuiltinListChartSource }

constructor TBuiltinListChartSource.Create(AOwner: TComponent; AXCountMin, AYCountMin: Cardinal);
begin
  inherited Create(AOwner);
  FXCountMin := AXCountMin;
  FYCountMin := AYCountMin;
  if FXCount < FXCountMin then
    FXCount := FXCountMin;
  if FYCount < FYCountMin then
    FYCount := FYCountMin;
end;

procedure TBuiltinListChartSource.CopyFrom(ASource: TCustomChartSource);
begin
  if ASource.XCount < FXCountMin then
    raise EXCountError.CreateFmt(rsSourceCountError2, [ClassName, FXCountMin, 'x']);
  if ASource.YCount < FYCountMin then
    raise EYCountError.CreateFmt(rsSourceCountError2, [ClassName, FYCountMin, 'y']);
  inherited;
end;

procedure TBuiltinListChartSource.SetXCount(AValue: Cardinal);
begin
  if AValue < FXCountMin then
    raise EXCountError.CreateFmt(rsSourceCountError2, [ClassName, FXCountMin, 'x']);
  inherited;
end;

procedure TBuiltinListChartSource.SetYCount(AValue: Cardinal);
begin
  if AValue < FYCountMin then
    raise EYCountError.CreateFmt(rsSourceCountError2, [ClassName, FYCountMin, 'y']);
  inherited;
end;

{ TSortedChartSource }

constructor TSortedChartSource.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FXCount := MaxInt;    // Allow source to be used by any series while Origin = nil
  FYCount := MaxInt;
  FListener := TListener.Create(@FOrigin, @Changed);
  FListenerSelf := TListener.Create(nil, @Changed);
  Broadcaster.Subscribe(FListenerSelf);
end;

destructor TSortedChartSource.Destroy;
begin
  ResetTransformation(0);
  FreeAndNil(FListenerSelf);
  FreeAndNil(FListener);
  inherited;
end;

procedure TSortedChartSource.Changed(ASender: TObject);
begin
  if ASender = Self then begin
    // We can get here only due to FListenerSelf's notification.
    // If some of our own (not Origin's) sorting properties was changed and we
    // are sorted, then our Sort() method has been called, so the transformation
    // is valid; but if we are no longer sorted, only notification is sent (so
    // we are here), so we must reinitialize the transformation to return to
    // the transparent (i.e. unsorted) state.
    if not IsSorted then
      ResetTransformation(Count);
    exit;
  end;

  if FOrigin <> nil then begin
    FXCount := Origin.XCount;
    FYCount := Origin.YCount;
    ResetTransformation(Origin.Count);
    if IsSorted and (not HasSameSorting(Origin)) then SortNoNotify;
  end else begin
    FXCount := MaxInt;    // Allow source to be used by any series while Origin = nil
    FYCount := MaxInt;
    ResetTransformation(0);
  end;
  Notify;
end;

function TSortedChartSource.DoCompare(AItem1, AItem2: Pointer): Integer;
var
  item1, item2: TChartDataItem;
begin
  // some data sources use same memory buffer for every item read,
  // so local copies must be made before comparing two items
  item1 := Origin.Item[PInteger(AItem1)^]^;

  // avoid sharing same memory by item1's and item2's reference-
  // counted variables
  item1.MakeUnique;

  item2 := Origin.Item[PInteger(AItem2)^]^;

  Result := inherited DoCompare(@item1, @item2);
end;

function TSortedChartSource.GetCount: Integer;
begin
  if Origin <> nil then
    Result := Origin.Count
  else
    Result := 0;
end;

function TSortedChartSource.GetItem(AIndex: Integer): PChartDataItem;
begin
  if Origin <> nil then
    Result := PChartDataItem(Origin.Item[PInteger(FData.Items[AIndex])^])
  else
    Result := nil;
end;

procedure TSortedChartSource.ResetTransformation(ACount: Integer);
var
  i: Integer;
  pint: PInteger;
begin
  if ACount > FData.Count then begin
    for i := 0 to FData.Count - 1 do
      PInteger(FData.List^[i])^ := i;

    FData.Capacity := ACount;

    pint := nil;
    try // optimization: don't execute try..except..end in a loop
      for i := FData.Count to ACount - 1 do begin
        New(pint);
        pint^ := i;
        FData.Add(pint); // don't use ItemAdd() here
        pint := nil;
      end;
    except
      if pint <> nil then
        Dispose(pint);
      raise;
    end;
  end else
  begin
    for i := ACount to FData.Count - 1 do
      Dispose(PInteger(FData.List^[i]));

    FData.Count := ACount;
    FData.Capacity := ACount; // release needless memory

    for i := 0 to FData.Count - 1 do
      PInteger(FData.List^[i])^ := i;
  end;
end;

procedure TSortedChartSource.SetOrigin(AValue: TCustomChartSource);
begin
  if AValue = Self then
    AValue := nil;
  if FOrigin = AValue then exit;
  if FOrigin <> nil then
    FOrigin.Broadcaster.Unsubscribe(FListener);
  FOrigin := AValue;
  if FOrigin <> nil then
    FOrigin.Broadcaster.Subscribe(FListener);
  Changed(nil);
end;

procedure TSortedChartSource.SetXCount(AValue: Cardinal);
begin
  Unused(AValue);
  raise EXCountError.Create('Cannot set XCount');
end;

procedure TSortedChartSource.SetYCount(AValue: Cardinal);
begin
  Unused(AValue);
  raise EYCountError.Create('Cannot set YCount');
end;

function TSortedChartSource.BasicExtent: TDoubleRect;
begin
  if Origin = nil then
    Result := EmptyExtent
  else
    Result := Origin.BasicExtent;
end;

function TSortedChartSource.Extent: TDoubleRect;
begin
  if Origin = nil then
    Result := EmptyExtent
  else
    Result := Origin.Extent;
end;

function TSortedChartSource.ExtentCumulative: TDoubleRect;
begin
  if Origin = nil then
    Result := EmptyExtent
  else
    Result := Origin.ExtentCumulative;
end;

function TSortedChartSource.ExtentList: TDoubleRect;
begin
  if Origin = nil then
    Result := EmptyExtent
  else
    Result := Origin.ExtentList;
end;

function TSortedChartSource.ExtentXYList: TDoubleRect;
begin
  if Origin = nil then
    Result := EmptyExtent
  else
    Result := Origin.ExtentXYList;
end;

function TSortedChartSource.ValuesTotal: Double;
begin
  if Origin = nil then
    Result := 0
  else
    Result := Origin.ValuesTotal;
end;

{ TMWCRandomGenerator }

function TMWCRandomGenerator.Get: LongWord;
const
  MULT: array [0..4] of UInt64 = (5115, 1776, 1492, 2111111111, 1);
var
  i: Integer;
  s: UInt64;
begin
  s := 0;
  for i := 0 to High(FHistory) do
    s += MULT[i] * FHistory[i];
  FHistory[3] := FHistory[2];
  FHistory[2] := FHistory[1];
  FHistory[1] := FHistory[0];
  FHistory[4] := Hi(s);
  FHistory[0] := Lo(s);
  Result := FHistory[0];
end;

function TMWCRandomGenerator.GetInRange(AMin, AMax: Integer): Integer;
var
  m: UInt64;
begin
  m := AMax - AMin + 1;
  m *= Get;
  // m is now equidistributed on [0, (2^32-1) * range],
  // so its upper double word is equidistributed on [0, range].
  Result := Integer(Hi(m)) + AMin;
end;

procedure TMWCRandomGenerator.SetSeed(AValue: Integer);
var
  i: Integer;
begin
  FHistory[0] := AValue;
  // Use trivial LCG for seeding
  for i := 1 to High(FHistory) do
    FHistory[i] := Lo(Int64(FHistory[i - 1]) * 29943829 - 1);
  // Skip some initial values to increase randomness.
  for i := 1 to 20 do
    Get;
end;

{ TRandomChartSource }

constructor TRandomChartSource.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FCurItem.Color := clTAColor;
  FRNG := TMWCRandomGenerator.Create;
  RandSeed := Trunc(Frac(Now) * MaxInt);
end;

destructor TRandomChartSource.Destroy;
begin
  FreeAndNil(FRNG);
  inherited;
end;

procedure TRandomChartSource.ChangeErrorBars(Sender: TObject);
begin
  Unused(Sender);
  Reset;
end;

function TRandomChartSource.GetCount: Integer;
begin
  Result := FPointsNumber;
end;

function TRandomChartSource.GetItem(AIndex: Integer): PChartDataItem;

  function GetRandomColor: TChartColor;
  begin
    Result := FRNG.Get and $00FFFFFF;
  end;
  
  function GetRandomX: Double;
  begin
    Result := FRNG.Get / High(LongWord) * (XMax - XMin) + XMin;
  end;

  function GetRandomY: Double;
  begin
    if (YNanPercent > 0) and (FRNG.GetInRange(0, 100) <= YNanPercent) then
      Result := SafeNan
    else if YMax <= YMin then
      Result := YMin
    else
      Result := FRNG.Get / High(LongWord) * (YMax - YMin) + YMin;
  end;

var
  i: Integer;
  fp, fn: Double;
begin
  if FCurIndex > AIndex then begin
    FRNG.Seed := FRandSeed;
    FCurIndex := -1;
  end;
  while FCurIndex < AIndex do begin
    FCurIndex += 1;
    if XCount > 0 then begin
      SetLength(FCurItem.XList, Max(XCount - 1, 0));
      if (XMax <= XMin) or (Count = 1) then begin
        FCurItem.X := XMin;
        for i := 0 to XCount - 2 do FCurItem.XList[i] := XMin;
      end else begin
        if FRandomX then begin
          FCurItem.X := GetRandomX;
          for i := 0 to XCount - 2 do
            FCurItem.XList[i] := GetRandomX;
        end else begin
          FCurItem.X := FCurIndex / (Count - 1) * (XMax - XMin) + XMin;
          for i := 0 to XCount - 2 do
            FCurItem.XList[i] := FCurItem.X;
        end;
      end;
      // Make sure that x values belonging to an error bar are random and
      // multiplied by the percentage given by ErrorBarData.Pos/NegDelta.
      fp := XErrorBarData.ValuePlus * PERCENT;
      if XErrorBarData.ValueMinus = -1 then fn := fp else fn := XErrorBarData.ValueMinus * PERCENT;
      for i := 0 to XCount - 2 do
        if (XErrorBarData.Kind = ebkChartSource) then begin
          if (i+1 = XErrorBarData.IndexPlus) then FCurItem.XList[i] := GetRandomX * fp;
          if (i+1 = XErrorBarData.IndexMinus) then FCurItem.XList[i] := GetRandomX * fn;
        end;
    end;
    if YCount > 0 then begin
      FCurItem.Y := GetRandomY;
      SetLength(FCurItem.YList, Max(YCount - 1, 0));
      for i := 0 to YCount - 2 do begin
        FCurItem.YList[i] := GetRandomY;
        // If this y value is that of an error bar assume that the error is
        // a percentage of the y value calculated. The percentage is the
        // ErrorBarData.Pos/NegDelta.
        fp := YErrorBarData.ValuePlus * PERCENT;
        if YErrorBarData.ValueMinus = -1 then fn := fp else fn := YErrorBarData.ValueMinus * PERCENT;
        if (YErrorBarData.Kind = ebkChartSource) then begin
          if (i+1 = YErrorBarData.IndexPlus) then FCurItem.YList[i] := GetRandomY * fp;
          if (i+1 = YErrorBarData.IndexMinus) then FCurItem.YList[i] := GetRandomY * fn;
        end;
      end;
    end;
    if FRandomColors then
      FCurItem.Color := GetRandomColor
    else
      FCurItem.Color := clTAColor;
    
  end;
  Result := @FCurItem;
end;

function TRandomChartSource.IsSorted: Boolean;
begin
  Result := not RandomX;
end;

procedure TRandomChartSource.Reset;
begin
  FCurIndex := -1;
  InvalidateCaches;
  Notify;
end;

procedure TRandomChartSource.SetPointsNumber(AValue: Integer);
begin
  if FPointsNumber = AValue then exit;
  FPointsNumber := AValue;
  Reset;
end;

procedure TRandomChartSource.SetRandomColors(AValue: Boolean);
begin
  if FRandomColors = AValue then exit;
  FRandomColors := AValue;
  Reset;
end;

procedure TRandomChartSource.SetRandomX(AValue: Boolean);
begin
  if FRandomX = AValue then exit;
  FRandomX := AValue;
  Reset;
end;

procedure TRandomChartSource.SetRandSeed(AValue: Integer);
begin
  if FRandSeed = AValue then exit;
  FRandSeed := AValue;
  FRNG.Seed := AValue;
  Reset;
end;

procedure TRandomChartSource.SetXCount(AValue: Cardinal);
begin
  if XCount = AValue then exit;
  FXCount := AValue;
  Reset;
end;

procedure TRandomChartSource.SetXMax(const AValue: Double);
begin
  if FXMax = AValue then exit;
  FXMax := AValue;
  Reset;
end;

procedure TRandomChartSource.SetXMin(const AValue: Double);
begin
  if FXMin = AValue then exit;
  FXMin := AValue;
  Reset;
end;

procedure TRandomChartSource.SetYCount(AValue: Cardinal);
begin
  if YCount = AValue then exit;
  FYCount := AValue;
  Reset;
end;

procedure TRandomChartSource.SetYMax(const AValue: Double);
begin
  if FYMax = AValue then exit;
  FYMax := AValue;
  InvalidateCaches;
  Notify;
end;

procedure TRandomChartSource.SetYMin(const AValue: Double);
begin
  if FYMin = AValue then exit;
  FYMin := AValue;
  Reset;
end;

procedure TRandomChartSource.SetYNanPercent(AValue: TPercent);
begin
  if FYNanPercent = AValue then exit;
  FYNanPercent := AValue;
  Reset;
end;

{ TUserDefinedChartSource }

procedure TUserDefinedChartSource.EndUpdate;
begin
  // There is no way to detect if the extent changed --
  // so call Reset to be a bit safer, but a bit slower.
  Reset;
  inherited EndUpdate;
end;

function TUserDefinedChartSource.GetCount: Integer;
begin
  Result := FPointsNumber;
end;

function TUserDefinedChartSource.GetItem(AIndex: Integer): PChartDataItem;
begin
  SetDataItemDefaults(FItem);
  if Assigned(FOnGetChartDataItem) then
    FOnGetChartDataItem(Self, AIndex, FItem);
  Result := @FItem;
end;

function TUserDefinedChartSource.IsSorted: Boolean;
begin
  Result := FSorted;
end;

procedure TUserDefinedChartSource.Reset;
begin
  InvalidateCaches;
  Notify;
end;

procedure TUserDefinedChartSource.SetOnGetChartDataItem(
  AValue: TGetChartDataItemEvent);
begin
  if TMethod(FOnGetChartDataItem) = TMethod(AValue) then exit;
  FOnGetChartDataItem := AValue;
  Reset;
end;

procedure TUserDefinedChartSource.SetPointsNumber(AValue: Integer);
begin
  if FPointsNumber = AValue then exit;
  FPointsNumber := AValue;
  Reset;
end;

procedure TUserDefinedChartSource.SetXCount(AValue: Cardinal);
begin
  if FXCount = AValue then exit;
  FXCount := AValue;
  SetLength(FItem.XList, Max(XCount - 1, 0));
  Reset;
end;

procedure TUserDefinedChartSource.SetYCount(AValue: Cardinal);
begin
  if FYCount = AValue then exit;
  FYCount := AValue;
  SetLength(FItem.YList, Max(YCount - 1, 0));
  Reset;
end;

{ TCalculatedChartSource }

procedure TCalculatedChartSource.CalcAccumulation(AIndex: Integer);
var
  lastItemIndex: Integer = -1;

  function GetOriginItem(AItemIndex: Integer): PChartDataItem;
  begin
    Result := @FItem;
    if lastItemIndex = AItemIndex then exit;
    ExtractItem(AItemIndex);
    lastItemIndex := AItemIndex;
  end;

var
  i, oldLeft, oldRight, newLeft, newRight: Integer;
begin
  if AccumulationDirection = cadCenter then
    FHistory.Capacity := EffectiveAccumulationRange * 2
  else
    FHistory.Capacity := EffectiveAccumulationRange;
  RangeAround(FIndex, oldLeft, oldRight);
  RangeAround(AIndex, newLeft, newRight);
  if
    (FIndex < 0) or (Abs(oldLeft - newLeft) > 1) or
    (Abs(oldRight - newRight) > 1)
  then begin
    FHistory.Clear;
    for i := newLeft to newRight do
      FHistory.AddLast(GetOriginItem(i)^);
  end
  else begin
    if FHistory.Capacity = 0 then
      for i := oldLeft to newLeft - 1 do
        FHistory.RemoveValue(GetOriginItem(i)^)
    else
      for i := oldLeft to newLeft - 1 do
        FHistory.RemoveFirst;
    if FHistory.Capacity = 0 then
      for i := oldRight downto newRight + 1 do
        FHistory.RemoveValue(GetOriginItem(i)^)
    else
      for i := oldRight downto newRight + 1 do
        FHistory.RemoveLast;
    for i := oldLeft - 1 downto newLeft do
      FHistory.AddFirst(GetOriginItem(i)^);
    for i := oldRight + 1 to newRight do
      FHistory.AddLast(GetOriginItem(i)^);
  end;
  GetOriginItem(AIndex);
  case AccumulationMethod of
    camSum:
      FHistory.GetSum(FItem);
    camAverage: begin
      FHistory.GetSum(FItem);
      FItem.MultiplyY(1 / (newRight - newLeft + 1));
    end;
    camDerivative, camSmoothDerivative:
      CalcDerivative(AIndex);
    else ;
  end;
  FIndex := AIndex;
end;

procedure TCalculatedChartSource.CalcDerivative(AIndex: Integer);

  procedure WeightedSum(const ACoeffs: array of Double; ADir, ACount: Integer);
  var
    i, j: Integer;
    prevItem: PChartDataItem;
  begin
    for j := 0 to ACount - 1 do begin
      prevItem := FHistory.GetPtr(AIndex + ADir * j);
      FItem.Y += prevItem^.Y * ADir * ACoeffs[j];
      for i := 0 to High(FItem.YList) do
        FItem.YList[i] += prevItem^.YList[i] * ADir * ACoeffs[j];
    end;
  end;

// Derivative is approximated by finite differences
// with accuracy order of (AccumulationRange - 1).
// Smoothed derivative coefficients are based on work
// by Pavel Holoborodko (http://www.holoborodko.com/pavel/).
const
  COEFFS_BF: array [Boolean, 2..7, 0..6] of Double = (
    ( (     -1, 1,     0,    0,     0,   0,    0),
      (   -3/2, 2,  -1/2,    0,     0,   0,    0),
      (  -11/6, 3,  -3/2,  1/3,     0,   0,    0),
      ( -25/12, 4,    -3,  4/3,  -1/4,   0,    0),
      (-137/60, 5,    -5, 10/3,  -5/4, 1/5,    0),
      ( -49/20, 6, -15/2, 20/3, -15/4, 6/5, -1/6)
    ),
    ( (   -1,     1,     0,   0,    0,    0,    0),
      ( -1/2,     0,   1/2,   0,    0,    0,    0),
      ( -1/4,  -1/4,   1/4, 1/4,    0,    0,    0),
      ( -1/8,  -1/4,     0, 1/4,  1/8,    0,    0),
      (-1/16, -3/16,  -1/8, 1/8, 3/16, 1/16,    0),
      (-1/32,  -1/8, -5/32,   0, 5/32,  1/8, 1/32)
    ));
  COEFFS_C: array [Boolean, 2..5, 0..4] of Double = (
    ( (0,  1/2,     0,     0,      0),
      (0,  2/3, -1/12,     0,      0),
      (0,  3/4, -3/20,  1/60,      0),
      (0,  4/5,  -1/5, 4/105, -1/280)
    ),
    ( (0,  1/2,    0,    0,     0),
      (0,  1/4,  1/8,    0,     0),
      (0, 5/32,  1/8, 1/32,     0),
      (0, 7/64, 7/64, 3/64, 1/128)
    ));
var
  ar, iLeft, iRight, dir: Integer;
  isSmooth: Boolean;
  dx: Double;
begin
  RangeAround(AIndex, iLeft, iRight);
  case CASE_OF_TWO[iLeft = AIndex, iRight = AIndex] of
    cotNone: begin
      dx := Max(
        FItem.X - FHistory.GetPtr(AIndex - iLeft - 1)^.X,
        FHistory.GetPtr(AIndex - iLeft + 1)^.X - FItem.X);
      ar := Min(Min(AIndex - iLeft, iRight - AIndex) + 1, High(COEFFS_C[false]));
      dir := 0;
    end;
    cotFirst: begin
      dx := FHistory.GetPtr(1)^.X - FItem.X;
      ar := Min(iRight - AIndex + 1, High(COEFFS_BF[false]));
      dir := 1;
    end;
    cotSecond: begin
      dx := FItem.X - FHistory.GetPtr(AIndex - iLeft - 1)^.X;
      ar := Min(AIndex - iLeft + 1, High(COEFFS_BF[false]));
      dir := -1;
    end;
    cotBoth: begin
      FItem.SetY(SafeNan);
      exit;
    end
  end;
  if dx = 0 then begin
    FItem.SetY(SafeNan);
    exit;
  end;
  FItem.SetY(0.0);
  AIndex -= iLeft;
  isSmooth := AccumulationMethod = camSmoothDerivative;
  if dir = 0 then begin
    WeightedSum(COEFFS_C[isSmooth][ar], -1, ar);
    WeightedSum(COEFFS_C[isSmooth][ar], +1, ar);
  end
  else
    WeightedSum(COEFFS_BF[isSmooth][ar], dir, ar);
  FItem.MultiplyY(1 / dx);
end;

procedure TCalculatedChartSource.CalcPercentage;
var
  s: Double;
begin
  if not Percentage then exit;
  s := (FItem.Y + Sum(FItem.YList)) * PERCENT;
  if s = 0 then exit;
  FItem.MultiplyY(1 / s);
end;

procedure TCalculatedChartSource.Changed(ASender: TObject);
begin
  if FOrigin <> nil then begin
    FSortBy := TCustomChartSourceAccess(Origin).SortBy;
    FSortDir := TCustomChartSourceAccess(Origin).SortDir;
    FSortIndex := TCustomChartSourceAccess(Origin).SortIndex;
    // We recalculate Y values, so we can't guarantee, that transformed
    // data is still sorted by Y or by Origin's custom algorithm
    FSorted := (FSortBy in [sbX, sbColor, sbText]) and Origin.IsSorted;
    FXCount := Origin.XCount;
    // FYCount is set below, in the UpdateYOrder() call
  end else begin
    FSortBy := sbX;
    FSortDir := sdAscending;
    FSortIndex := 0;
    FSorted := false;
    FXCount := MaxInt;    // Allow source to be used by any series while Origin = nil
    FYCount := MaxInt;
  end;

  if
    (FOrigin <> nil) and (ASender = FOrigin) and
    (FOrigin.YCount <> FOriginYCount)
  then begin
    UpdateYOrder;
    exit;
  end;
  FIndex := -1;
  InvalidateCaches;
  Notify;
end;

constructor TCalculatedChartSource.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FXCount := MaxInt;    // Allow source to be used by any series while Origin = nil
  FYCount := MaxInt;
  FAccumulationRange := 2;
  FIndex := -1;
  FHistory := TChartSourceBuffer.Create;
  FListener := TListener.Create(@FOrigin, @Changed);
end;

destructor TCalculatedChartSource.Destroy;
begin
  FreeAndNil(FHistory);
  FreeAndNil(FListener);
  inherited;
end;

function TCalculatedChartSource.EffectiveAccumulationRange: Cardinal;
const
  MAX_DERIVATIVE_RANGE = 10;
begin
  if IsDerivative and (AccumulationRange = 0) then
    Result := MAX_DERIVATIVE_RANGE
  else
    Result := AccumulationRange;
end;

procedure TCalculatedChartSource.ExtractItem(AIndex: Integer);

  function YByOrder(AOrderIndex: Integer): Double;
  begin
    if AOrderIndex <= 0 then
      Result := FItem.Y
    else
      Result := FItem.YList[AOrderIndex - 1];
  end;

var
  t: TDoubleDynArray = nil;
  i: Integer;
begin
  FItem := Origin[AIndex]^;
  if Length(FYOrder) > 0 then begin
    SetLength(t, High(FYOrder));
    for i := 1 to High(FYOrder) do
      t[i - 1] := YByOrder(FYOrder[i]);
    FItem.Y := YByOrder(FYOrder[0]);
    FItem.YList := t;
  end else
    FItem.YList := nil;
end;

function TCalculatedChartSource.GetCount: Integer;
begin
  if Origin <> nil then
    Result := Origin.Count
  else
    Result := 0;
end;

function TCalculatedChartSource.GetItem(AIndex: Integer): PChartDataItem;
begin
  if Origin = nil then exit(nil);
  Result := @FItem;
  if FIndex = AIndex then exit;
  if (AccumulationMethod = camNone) or (AccumulationRange = 1) then
    ExtractItem(AIndex)
  else
    CalcAccumulation(AIndex);
  CalcPercentage;
end;

function TCalculatedChartSource.IsDerivative: Boolean;
begin
  Result := AccumulationMethod in [camDerivative, camSmoothDerivative];
end;

function TCalculatedChartSource.IsSorted: Boolean;
begin
  Result := FSorted;
end;

procedure TCalculatedChartSource.RangeAround(
  AIndex: Integer; out ALeft, ARight: Integer);
var
  ar: Integer;
begin
  ar := EffectiveAccumulationRange;
  ar := Math.IfThen(ar = 0, MaxInt div 2, ar - 1);
  ALeft := AIndex - Math.IfThen(AccumulationDirection = cadForward, 0, ar);
  ARight := AIndex + Math.IfThen(AccumulationDirection = cadBackward, 0, ar);
  ALeft := EnsureRange(ALeft, 0, Count - 1);
  ARight := EnsureRange(ARight, 0, Count - 1);
end;

procedure TCalculatedChartSource.SetAccumulationDirection(
  AValue: TChartAccumulationDirection);
begin
  if FAccumulationDirection = AValue then exit;
  FAccumulationDirection := AValue;
  Changed(nil);
end;

procedure TCalculatedChartSource.SetAccumulationMethod(
  AValue: TChartAccumulationMethod);
begin
  if FAccumulationMethod = AValue then exit;
  FAccumulationMethod := AValue;
  Changed(nil);
end;

procedure TCalculatedChartSource.SetAccumulationRange(AValue: Cardinal);
begin
  if FAccumulationRange = AValue then exit;
  FAccumulationRange := AValue;
  Changed(nil);
end;

procedure TCalculatedChartSource.SetOrigin(AValue: TCustomChartSource);
begin
  if AValue = Self then
    AValue := nil;
  if FOrigin = AValue then exit;
  if FOrigin <> nil then
    FOrigin.Broadcaster.Unsubscribe(FListener);
  FOrigin := AValue;
  if FOrigin <> nil then
    FOrigin.Broadcaster.Subscribe(FListener);
  UpdateYOrder;
end;

procedure TCalculatedChartSource.SetPercentage(AValue: Boolean);
begin
  if FPercentage = AValue then exit;
  FPercentage := AValue;
  Changed(nil);
end;

procedure TCalculatedChartSource.SetReorderYList(const AValue: String);
begin
  if FReorderYList = AValue then exit;
  FReorderYList := AValue;
  UpdateYOrder;
end;

procedure TCalculatedChartSource.SetXCount(AValue: Cardinal);
begin
  Unused(AValue);
  raise EXCountError.Create('Cannot set XCount');
end;

procedure TCalculatedChartSource.SetYCount(AValue: Cardinal);
begin
  Unused(AValue);
  raise EYCountError.Create('Cannot set YCount');
end;

procedure TCalculatedChartSource.UpdateYOrder;
var
  order: TStringList;
  i: Integer;
begin
  if FOrigin = nil then begin
    FOriginYCount := 0;
    FYCount := MaxInt;    // Allow source to be used by any series while Origin = nil
    FYOrder := nil;
    FItem.YList := nil;
    Changed(nil);
    exit;
  end;

  FOriginYCount := FOrigin.YCount;
  if FOriginYCount = 0 then
    FYOrder := nil
  else
  if ReorderYList = '' then begin
    SetLength(FYOrder, FOriginYCount);
    for i := 0 to High(FYOrder) do
      FYOrder[i] := i;
  end
  else begin
    order := TStringList.Create;
    try
      order.CommaText := ReorderYList;
      SetLength(FYOrder, order.Count);
      for i := 0 to High(FYOrder) do
        FYOrder[i] := EnsureRange(StrToIntDef(order[i], 0), 0, FOriginYCount - 1);
    finally
      order.Free;
    end;
  end;
  FYCount := Length(FYOrder);

  SetLength(FItem.YList, Max(High(FYOrder), 0));
  Changed(nil);
end;

end.
