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

  Basic types for TAChart series.

  Authors: Alexander Klenin

}
unit TACustomSeries;

{$MODE ObjFPC}{$H+}
{$WARN 6058 off : Call to subroutine "$1" marked as inline is not inlined}
interface

uses
  Classes, GraphType, Graphics, IntfGraphics, SysUtils,
  TAChartAxis, TAChartUtils, TACustomSource, TADrawUtils, TAGraph, TALegend,
  TASources, TAStyles, TATextElements, TATypes;

const
  DEF_ERR_ENDLENGTH = 5;

type
  TNearestPointParams = record
    FDistFunc: TPointDistFunc;
    FOptimizeX: Boolean;
    FPoint: TPoint;
    FRadius: Integer;
    FTargets: TNearestPointTargets;
  end;

  TNearestPointResults = record
    FDist: Integer;
    FImg: TPoint;
    FIndex: Integer;        // Point index
    FXIndex: Integer;       // Index to be used in Source.GetX()
    FYIndex: Integer;       // Index to be used in Source.GetY()
    FValue: TDoublePoint;
  end;

  { TCustomChartSeries }

  TCustomChartSeries = class(TBasicChartSeries)
  strict private
    FAxisIndexX: TChartAxisIndex;
    FAxisIndexY: TChartAxisIndex;
    FDepthBrightnessDelta: Integer;
    FLegend: TChartSeriesLegend;
    FToolTargets: TNearestPointTargets;
    FTitle: String;
    procedure SetAxisIndexX(AValue: TChartAxisIndex);
    procedure SetAxisIndexY(AValue: TChartAxisIndex);
    procedure SetDepthBrightnessDelta(AValue: Integer);
    procedure SetLegend(AValue: TChartSeriesLegend);

  protected
    procedure AfterAdd; override;
    procedure GetLegendItems(AItems: TChartLegendItems); virtual; abstract;
    procedure GetLegendItemsBasic(AItems: TChartLegendItems); override;
    function GetShowInLegend: Boolean; override;
    procedure SetActive(AValue: Boolean); override;
    procedure SetDepth(AValue: TChartDistance); override;
    procedure SetShadow(AValue: TChartShadow); override;
    procedure SetShowInLegend(AValue: Boolean); override;
    procedure SetTitle(AValue: String); virtual;
    procedure SetTransparency(AValue: TChartTransparency); override;
    procedure SetZPosition(AValue: TChartDistance); override;
    procedure StyleChanged(Sender: TObject);
    procedure UpdateParentChart;

  protected
    procedure ReadState(Reader: TReader); override;
    procedure SetParentComponent(AParent: TComponent); override;

  strict protected
    // Set series bounds in axis coordinates.
    // Some or all bounds may be left unset, in which case they will be ignored.
    procedure GetBounds(var ABounds: TDoubleRect); virtual; abstract;
    function GetIndex: Integer; override;
    function LegendTextSingle: String;
    function LegendTextStyle(AStyle: TChartStyle): String;
    procedure SetIndex(AValue: Integer); override;
    function TitleIsStored: Boolean; virtual;
    property DepthBrightnessDelta: Integer
      read FDepthBrightnessDelta write SetDepthBrightnessDelta default 0;
    property ToolTargets: TNearestPointTargets
      read FToolTargets write FToolTargets default [nptPoint];

  public
    function AxisToGraph(const APoint: TDoublePoint): TDoublePoint; inline;
    function AxisToGraphX(AX: Double): Double; override;
    function AxisToGraphY(AY: Double): Double; override;
    function GetAxisX: TChartAxis;
    function GetAxisY: TChartAxis;
    function GetAxisBounds(AAxis: TChartAxis; out AMin, AMax: Double): Boolean; override;
    function GetDepthColor(AColor: Integer; Opposite: boolean = false): Integer; virtual;
    function GetGraphBounds: TDoubleRect; override;
    function GraphToAxis(APoint: TDoublePoint): TDoublePoint;
    function GraphToAxisX(AX: Double): Double; override;
    function GraphToAxisY(AY: Double): Double; override;
    function IsRotated: Boolean;

  public
    procedure Assign(ASource: TPersistent); override;
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    function GetNearestPoint(
      const AParams: TNearestPointParams;
      out AResults: TNearestPointResults): Boolean; virtual;
    function GetParentComponent: TComponent; override;
    procedure GetSingleLegendItem(AItems: TChartLegendItems);
    function HasParent: Boolean; override;

    property AxisIndexX: TChartAxisIndex
      read FAxisIndexX write SetAxisIndexX default DEF_AXIS_INDEX;
    property AxisIndexY: TChartAxisIndex
      read FAxisIndexY write SetAxisIndexY default DEF_AXIS_INDEX;
    property Title: String read FTitle write SetTitle stored TitleIsStored;

  published
    property Legend: TChartSeriesLegend read FLegend write SetLegend;
    property Shadow;
    property ShowInLegend: Boolean
      read GetShowInLegend write SetShowInLegend stored false default true;
    property Transparency;
  end;

  TChartSeries = class;

  TChartGetMarkEvent = procedure (
    out AFormattedMark: String; AIndex: Integer) of object;
  TChartGetMarkTextEvent = procedure (ASeries: TChartSeries;
    APointIndex, AXIndex, AYIndex: Integer; var AFormattedMark: String) of object;

  { TChartSeries }

  TChartSeries = class(TCustomChartSeries)
  strict private
    FBuiltinSource: TCustomChartSource;
    FListener: TListener;
    FMarks: TChartMarks;
    FOnGetMark: TChartGetMarkEvent;
    FOnGetMarkText: TChartGetMarkTextEvent;
    FSource: TCustomChartSource;
    FStyles: TChartStyles;
    FStylesListener: TListener;

    function GetSource: TCustomChartSource;
    function IsSourceStored: boolean;
    procedure SetMarks(AValue: TChartMarks);
    procedure SetOnGetMark(AValue: TChartGetMarkEvent); deprecated;
    procedure SetOnGetMarkText(AValue: TChartGetMarkTextEvent);
    procedure SetSource(AValue: TCustomChartSource);
    procedure SetStyles(AValue: TChartStyles);
  protected
    procedure AfterAdd; override;
    procedure AfterDraw; override;
    procedure BeforeDraw; override;
    procedure CheckSource(ASource: TCustomChartSource);
    procedure GetBounds(var ABounds: TDoubleRect); override;
    function GetGraphPoint(AIndex: Integer): TDoublePoint; overload;
    function GetGraphPoint(AIndex, AXIndex, AYIndex: Integer): TDoublePoint; overload;
    function GetGraphPointX(AIndex: Integer): Double; overload; inline;
    function GetGraphPointX(AIndex, AXIndex: Integer): Double; overload; inline;
    function GetGraphPointY(AIndex: Integer): Double; overload; inline;
    function GetGraphPointY(AIndex, AYIndex: Integer): Double; overload; inline;
    function GetSeriesColor: TColor; virtual;
    function GetXMaxVal: Double;
    procedure SourceChanged(ASender: TObject); virtual;
    procedure VisitSources(
      AVisitor: TChartOnSourceVisitor; AAxis: TChartAxis; var AData); override;
    class procedure GetXYCountNeeded(out AXCount, AYCount: Cardinal); virtual;
  strict protected
    function LegendTextPoint(AIndex: Integer): String; inline;
  protected
    property Styles: TChartStyles read FStyles write SetStyles;
  public
    procedure Assign(ASource: TPersistent); override;
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

  public
    function  GetColor(AIndex: Integer): TColor;
    procedure GetMax(out X, Y: Double);
    procedure GetMin(out X, Y: Double);
    function  GetXImgValue(AIndex: Integer): Integer;
    function  GetXMax: Double;
    function  GetXMin: Double;
    function  GetXValue(AIndex: Integer): Double;
    function  GetXValues(AIndex, AXIndex: Integer): Double;
    function  GetYImgValue(AIndex: Integer): Integer;
    function  GetYMax: Double;
    function  GetYMin: Double;
    function  GetYValue(AIndex: Integer): Double;
    function  GetYValues(AIndex, AYIndex: Integer): Double;
    procedure SetColor(AIndex: Integer; AColor: TColor); inline;
    procedure SetText(AIndex: Integer; AValue: String); inline;
    procedure SetXValue(AIndex: Integer; AValue: Double); inline;
    procedure SetXValues(AIndex, AXIndex: Integer; AValue: Double);
    procedure SetYValue(AIndex: Integer; AValue: Double); inline;
    procedure SetYValues(AIndex, AYIndex: Integer; AValue: Double);
  public
    function Add(
      AValue: Double;
      AXLabel: String = ''; AColor: TColor = clTAColor): Integer; inline;
    function AddArray(const AValues: array of Double): Integer;
    function AddNull(ALabel: String = ''; AColor: TColor = clTAColor): Integer; inline;
    function AddX(
      AX: Double; ALabel: String = ''; AColor: TColor = clTAColor): Integer; inline;
    function AddXY(
      AX, AY: Double;
      AXLabel: String = ''; AColor: TColor = clTAColor): Integer; overload; inline;
    function AddXY(
      AX, AY: Double; const AYList: array of Double;
      AXLabel: String = ''; AColor: TColor = clTAColor): Integer; overload;
    function AddY(
      AY: Double; ALabel: String = ''; AColor: TColor = clTAColor): Integer; inline;
    procedure BeginUpdate;
    procedure Clear; virtual;
    function Count: Integer; inline;
    procedure Delete(AIndex: Integer); virtual;
    procedure EndUpdate;
    function Extent: TDoubleRect; virtual;
    procedure FindYRange(AXMin, AXMax: Double; var AYMin, AYMax: Double); virtual;
    function FormattedMark(AIndex: Integer; AFormat: String = '';
      AYIndex: Integer = 0; AXIndex: Integer = 0): String;
    function IsEmpty: Boolean; override;
    function ListSource: TListChartSource;
    property Marks: TChartMarks
      read FMarks write SetMarks;
    property Source: TCustomChartSource
      read GetSource write SetSource stored IsSourceStored;
  public
    // for Delphi compatibility
    function LastValueIndex: Integer; inline;
    function MaxXValue: Double;
    function MinXValue: Double;
    function MaxYValue: Double;
    function MinYValue: Double;
    property XValue[AIndex: Integer]: Double read GetXValue write SetXValue;
    property XValues[AIndex, AXIndex: Integer]: Double read GetXValues write SetXValues;
    property YValue[AIndex: Integer]: Double read GetYValue write SetYValue;
    property YValues[AIndex, AYIndex: Integer]: Double read GetYValues write SetYValues;
  published
    property Active default true;
    property ShowInLegend;
    property Title;
    property ZPosition;
  published
    property OnGetMark: TChartGetMarkEvent read FOnGetMark write SetOnGetMark; deprecated 'Use OnGetMarkText instead';  // To be removed in Laz 5
    property OnGetMarkText: TChartGetMarkTextEvent read FOnGetMarkText write SetOnGetMarkText;
  end;

  TLabelDirection = (ldLeft, ldTop, ldRight, ldBottom);

  TLinearMarkPositions = (lmpOutside, lmpPositive, lmpNegative, lmpInside);

  TSeriesPointerCustomDrawEvent = procedure (
    ASender: TChartSeries; ADrawer: IChartDrawer; AIndex: Integer;
    ACenter: TPoint) of object;

  TSeriesPointerStyleEvent = procedure (ASender: TChartSeries;
    AValueIndex: Integer; var AStyle: TSeriesPointerStyle) of object;

  TStackedNaN = (snReplaceByZero, snDoNotDraw);

  { TBasicPointSeries }

  TBasicPointSeries = class(TChartSeries)
  strict private
    FMarkPositions: TLinearMarkPositions;
    FErrorBars: array[0..1] of TChartErrorBar;
    FOnCustomDrawPointer: TSeriesPointerCustomDrawEvent;
    FOnGetPointerStyle: TSeriesPointerStyleEvent;
    function GetErrorBars(AIndex: Integer): TChartErrorBar;
    function IsErrorBarsStored(AIndex: Integer): Boolean;
    procedure SetErrorBars(AIndex: Integer; AValue: TChartErrorBar);
    procedure SetMarkPositionCentered(AValue: Boolean);
    procedure SetMarkPositions(AValue: TLinearMarkPositions);
    procedure SetPointer(AValue: TSeriesPointer);
    procedure SetStacked(AValue: Boolean);
    procedure SetStackedNaN(AValue: TStackedNaN);
  //strict
  protected
    FGraphPoints: array of TDoublePoint;
    FLoBound: Integer;
    FMinXRange: Double;
    FPointer: TSeriesPointer;
    FStacked: Boolean;
    FStackedNaN: TStackedNaN;
    FUpBound: Integer;
    FOptimizeX: Boolean;
    FSupportsZeroLevel: Boolean;
    FMarkPositionCentered: Boolean;

    procedure AfterDrawPointer(
      ADrawer: IChartDrawer; AIndex: Integer; const APos: TPoint); virtual;
    procedure DrawErrorBars(ADrawer: IChartDrawer);
    procedure DrawLabels(ADrawer: IChartDrawer; AYIndex: Integer = -1);
    procedure DrawPointers(ADrawer: IChartDrawer; AStyleIndex: Integer = 0;
      UseDataColors: Boolean = false);
    procedure FindExtentInterval(
      const AExtent: TDoubleRect; AFilterByExtent: Boolean);
    function GetLabelDataPoint(AIndex, AYIndex: Integer): TDoublePoint; virtual;
    function GetLabelDirection(AValue: Double;
      const ACenterLevel: Double): TLabelDirection;
    procedure GetLegendItemsRect(AItems: TChartLegendItems; ABrush: TBrush; APen: TPen);
    function GetXRange(AX: Double; AIndex: Integer): Double;
    function GetZeroLevel: Double; virtual;
    function HasMissingYValue(AIndex: Integer; AMaxYIndex: Integer = MaxInt): Boolean;
    function NearestXNumber(var AIndex: Integer; ADir: Integer): Double;
    function SkipMissingValues(AIndex: Integer): Boolean; virtual;
    function ToolTargetDistance(const AParams: TNearestPointParams;
      AGraphPt: TDoublePoint; APointIdx, AXIdx, AYIdx: Integer): Integer; virtual;
    procedure UpdateGraphPoints(AIndex: Integer; ACumulative: Boolean); overload; inline;
    procedure UpdateGraphPoints(AIndex, ALo, AUp: Integer; ACumulative: Boolean); overload;
    procedure UpdateLabelDirectionReferenceLevel(AIndex, AYIndex: Integer;
      var ALevel: Double); virtual;
    procedure UpdateMinXRange;

    property Pointer: TSeriesPointer read FPointer write SetPointer;
    property Stacked: Boolean read FStacked write SetStacked;
    property StackedNaN: TStackedNaN read FStackedNaN write SetStackedNaN default snReplaceByZero;

  protected
    procedure AfterAdd; override;
    procedure PrepareGraphPoints(const AExtent: TDoubleRect; AFilterByExtent: Boolean); virtual;
    procedure SourceChanged(ASender: TObject); override;
    procedure UpdateMargins(ADrawer: IChartDrawer; var AMargins: TRect); override;

    property MarkPositionCentered: Boolean
      read FMarkPositionCentered write SetMarkPositionCentered default false;
    property MarkPositions: TLinearMarkPositions
      read FMarkPositions write SetMarkPositions default lmpOutside;
    property XErrorBars: TChartErrorBar index 0 read GetErrorBars
      write SetErrorBars stored IsErrorBarsStored;
    property YErrorBars: TChartErrorBar index 1 read GetErrorBars
      write SetErrorBars stored IsErrorBarsStored;
    property OnCustomDrawPointer: TSeriesPointerCustomDrawEvent
      read FOnCustomDrawPointer write FOnCustomDrawPointer;
    property OnGetPointerStyle: TSeriesPointerStyleEvent
      read FOnGetPointerStyle write FOnGetPointerStyle;

  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  public
    procedure Assign(ASource: TPersistent); override;
    function Extent: TDoubleRect; override;
    procedure FindYRange(AXMin, AXMax: Double; var AYMin, AYMax: Double); override;
    function GetNearestPoint(
      const AParams: TNearestPointParams;
      out AResults: TNearestPointResults): Boolean; override;
    function IsPointInLabel(ADrawer: IChartDrawer; const APoint: TPoint; 
      var APointIndex, AYIndex: Integer): Boolean;
    procedure MovePoint(var AIndex: Integer; const ANewPos: TDoublePoint); override;
    procedure MovePointEx(var AIndex: Integer; AXIndex, AYIndex: Integer;
      const ANewPos: TDoublePoint); override;
    property ToolTargets default [nptPoint, nptYList];
    property ExtentPointIndexFirst: Integer read FLoBound;
    property ExtentPointIndexLast: Integer read FUpBound;
  end;

  function CreateLazIntfImage(
    out ARawImage: TRawImage; const ASize: TPoint): TLazIntfImage;

implementation

uses
  Math, PropEdits, StrUtils, LResources, Types, GraphUtil,
  TAChartStrConsts, TAGeometry, TAMath;

function CreateLazIntfImage(
  out ARawImage: TRawImage; const ASize: TPoint): TLazIntfImage;
begin
  ARawImage.Init;
  ARawImage.Description.Init_BPP32_B8G8R8A8_BIO_TTB(ASize.X, ASize.Y);
  ARawImage.CreateData(true);
  Result := TLazIntfImage.Create(0, 0);
  Result.SetRawImage(ARawImage);
end;

{ TCustomChartSeries }

procedure TCustomChartSeries.AfterAdd;
begin
  Legend.SetOwner(FChart);
  Shadow.SetOwner(FChart);
end;

procedure TCustomChartSeries.Assign(ASource: TPersistent);
begin
  if ASource is TCustomChartSeries then
    with TCustomChartSeries(ASource) do begin
      Self.FAxisIndexX := FAxisIndexX;
      Self.FAxisIndexY := FAxisIndexY;
      Self.FDepthBrightnessDelta := FDepthBrightnessDelta;
      Self.Legend := FLegend;
      Self.FTitle := FTitle;
      Self.FToolTargets := FToolTargets;
    end;
  inherited Assign(ASource);
end;

function TCustomChartSeries.AxisToGraph(
  const APoint: TDoublePoint): TDoublePoint;
begin
  Result := DoublePoint(AxisToGraphX(APoint.X), AxisToGraphY(APoint.Y));
  if IsRotated then
    Exchange(Result.X, Result.Y);
end;

function TCustomChartSeries.AxisToGraphX(AX: Double): Double;
begin
  Result := TransformByAxis(FChart.AxisList, AxisIndexX).AxisToGraph(AX)
end;

function TCustomChartSeries.AxisToGraphY(AY: Double): Double;
begin
  Result := TransformByAxis(FChart.AxisList, AxisIndexY).AxisToGraph(AY)
end;

constructor TCustomChartSeries.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FActive := true;
  FAxisIndexX := DEF_AXIS_INDEX;
  FAxisIndexY := DEF_AXIS_INDEX;
  FLegend := TChartSeriesLegend.Create(FChart);
  FToolTargets := [nptPoint];
  FShadow := TChartShadow.Create(FChart);
end;

destructor TCustomChartSeries.Destroy;
begin
  FreeAndNil(FLegend);
  FreeAndNil(FShadow);
  inherited;
end;

function TCustomChartSeries.GetAxisBounds(AAxis: TChartAxis;
  out AMin, AMax: Double): Boolean;
var
  ex: TDoubleRect;
  axIndexX, axIndexY: Integer;
begin
  axIndexX := GetAxisX.Index;
  axIndexY := GetAxisY.Index;

  if (AAxis.Index = axIndexX) or (AAxis.Index = axIndexY) then begin
    ex := EmptyExtent;
    GetBounds(ex);
    with ex do begin
      UpdateBoundsByAxisRange(FChart.AxisList, axIndexX, a.X, b.X);
      UpdateBoundsByAxisRange(FChart.AxisList, axIndexY, a.Y, b.Y);
      if IsRotated then begin
        Exchange(a.X, a.Y);
        Exchange(b.X, b.Y);
      end;
    end;
    AMin := TDoublePointBoolArr(ex.a)[AAxis.IsVertical];
    AMax := TDoublePointBoolArr(ex.b)[AAxis.IsVertical];
    Result := true;
  end else
    Result := false;
end;

function TCustomChartSeries.GetAxisX: TChartAxis;
begin
  if InRange(AxisIndexX, 0, FChart.AxisList.Count - 1) then
    Result := FChart.AxisList[AxisIndexX]
  else
    Result := FChart.BottomAxis;
end;

function TCustomChartSeries.GetAxisY: TChartAxis;
begin
  if InRange(AxisIndexY, 0, FChart.AxisList.Count - 1) then
    Result := FChart.AxisList[AxisIndexY]
  else
    Result := FChart.LeftAxis;
end;

function TCustomChartSeries.GetDepthColor(AColor: Integer;
  Opposite: Boolean = false): Integer;
var
  h, l, s: Byte;
begin
  ColorToHLS(AColor, h, l, s);
  if Opposite then
    Result := HLSToColor(h, EnsureRange(Integer(l) - FDepthBrightnessDelta, 0, 255), s)
  else
    Result := HLSToColor(h, EnsureRange(Integer(l) + FDepthBrightnessDelta, 0, 255), s);
end;

function TCustomChartSeries.GetGraphBounds: TDoubleRect;
begin
  Result := EmptyExtent;
  if Active then GetBounds(Result);
  with Result do begin
    UpdateBoundsByAxisRange(FChart.AxisList, AxisIndexX, a.X, b.X);
    UpdateBoundsByAxisRange(FChart.AxisList, AxisIndexY, a.Y, b.Y);
    TransformByAxis(FChart.AxisList, AxisIndexX).UpdateBounds(a.X, b.X);
    TransformByAxis(FChart.AxisList, AxisIndexY).UpdateBounds(a.Y, b.Y);
    if IsRotated then begin
      Exchange(a.X, a.Y);
      Exchange(b.X, b.Y);
    end;
  end;
end;

function TCustomChartSeries.GetIndex: Integer;
begin
  Result := FChart.Series.List.IndexOf(Self);
end;

procedure TCustomChartSeries.GetLegendItemsBasic(AItems: TChartLegendItems);
var
  i, oldCount: Integer;
begin
  oldCount := AItems.Count;
  if Assigned(Legend.OnDraw) then
    for i := 0 to Legend.UserItemsCount - 1 do
      AItems.Add(TLegendItemUserDrawn.Create(i, Legend.OnDraw, LegendTextSingle))
  else
    GetLegendItems(AItems);
  for i := oldCount to AItems.Count - 1 do begin
    AItems[i].Owner := Self;
    Legend.InitItem(AItems[i], i - oldCount, FChart.Legend);
  end;
end;

function TCustomChartSeries.GetNearestPoint(
  const AParams: TNearestPointParams;
  out AResults: TNearestPointResults): Boolean;
begin
  Unused(AParams);
  AResults.FDist := MaxInt;
  AResults.FImg := Point(0, 0);
  AResults.FIndex := 0;
  AResults.FValue := ZeroDoublePoint;
  Result := false;
end;

function TCustomChartSeries.GetParentComponent: TComponent;
begin
  Result := FChart;
end;

function TCustomChartSeries.GetShowInLegend: Boolean;
begin
  Result := Legend.Visible;
end;

procedure TCustomChartSeries.GetSingleLegendItem(AItems: TChartLegendItems);
var
  oldMultiplicity: TLegendMultiplicity;
begin
  ParentChart.DisableRedrawing;
  oldMultiplicity := Legend.Multiplicity;
  try
    Legend.Multiplicity := lmSingle;
    GetLegendItemsBasic(AItems);
  finally
    Legend.Multiplicity := oldMultiplicity;
    ParentChart.EnableRedrawing;
  end;
end;

function TCustomChartSeries.GraphToAxis(APoint: TDoublePoint): TDoublePoint;
begin
  if IsRotated then
    Exchange(APoint.X, APoint.Y);
  Result := DoublePoint(GraphToAxisX(APoint.X), GraphToAxisY(APoint.Y));
end;

function TCustomChartSeries.GraphToAxisX(AX: Double): Double;
begin
  Result := TransformByAxis(FChart.AxisList, AxisIndexX).GraphToAxis(AX)
end;

function TCustomChartSeries.GraphToAxisY(AY: Double): Double;
begin
  Result := TransformByAxis(FChart.AxisList, AxisIndexY).GraphToAxis(AY)
end;

function TCustomChartSeries.HasParent: Boolean;
begin
  Result := true;
end;

function TCustomChartSeries.IsRotated: Boolean;
var
  x_normal, y_normal: Boolean;
  x_axis: TChartAxis = nil;
  y_axis: TChartAxis = nil;
begin
  if InRange(AxisIndexX, 0, FChart.AxisList.Count-1) then
    x_axis := FChart.AxisList[AxisIndexX];
  if InRange(AxisIndexY, 0, FChart.AxisList.Count-1) then
    y_axis := FChart.AxisList[AxisIndexY];
  x_normal := (x_axis = nil) or (not x_axis.IsVertical);
  y_normal := (y_axis = nil) or y_axis.IsVertical;
  Result := (not x_normal) and (not y_normal);
end;

function TCustomChartSeries.LegendTextSingle: String;
begin
  if Legend.Format = '' then
    Result := Title
  else
    Result := Format(Legend.Format, [Title, Index]);
end;

function TCustomChartSeries.LegendTextStyle(AStyle: TChartStyle): String;
begin
  if Legend.Format = '' then
    Result := AStyle.Text
  else
    Result := Format(Legend.Format, [AStyle.Text, AStyle.Index]);
end;

procedure TCustomChartSeries.ReadState(Reader: TReader);
begin
  inherited ReadState(Reader);
  if Reader.Parent is TChart then
    TChart(Reader.Parent).AddSeries(Self);
end;

procedure TCustomChartSeries.SetActive(AValue: Boolean);
begin
  if FActive = AValue then exit;
  FActive := AValue;
  UpdateParentChart;
end;

procedure TCustomChartSeries.SetAxisIndexX(AValue: TChartAxisIndex);
begin
  if FAxisIndexX = AValue then exit;
  FAxisIndexX := AValue;
  UpdateParentChart;
end;

procedure TCustomChartSeries.SetAxisIndexY(AValue: TChartAxisIndex);
begin
  if FAxisIndexY = AValue then exit;
  FAxisIndexY := AValue;
  UpdateParentChart;
end;

procedure TCustomChartSeries.SetDepth(AValue: TChartDistance);
begin
  if FDepth = AValue then exit;
  FDepth := AValue;
  UpdateParentChart;
end;

procedure TCustomChartSeries.SetDepthBrightnessDelta(AValue: Integer);
begin
  if FDepthBrightnessDelta = AValue then exit;
  FDepthBrightnessDelta := AValue;
  UpdateParentChart;
end;

procedure TCustomChartSeries.SetIndex(AValue: Integer);
begin
  with FChart.Series.List do
    Move(Index, EnsureRange(AValue, 0, Count - 1));
end;

procedure TCustomChartSeries.SetLegend(AValue: TChartSeriesLegend);
begin
  if FLegend = AValue then exit;
  FLegend.Assign(AValue);
  UpdateParentChart;
end;

procedure TCustomChartSeries.SetParentComponent(AParent: TComponent);
begin
  if not (csLoading in ComponentState) then
    (AParent as TChart).AddSeries(Self);
end;

procedure TCustomChartSeries.SetShadow(AValue: TChartShadow);
begin
  if FShadow = AValue then exit;
  FShadow.Assign(AValue);
  UpdateParentChart;
end;

procedure TCustomChartSeries.SetShowInLegend(AValue: Boolean);
begin
  Legend.Visible := AValue;
end;

procedure TCustomChartSeries.SetTitle(AValue: String);
begin
  if FTitle = AValue then exit;
  FTitle := AValue;
  UpdateParentChart;
end;

procedure TCustomChartSeries.SetTransparency(AValue: TChartTransparency);
begin
  if FTransparency = AValue then exit;
  FTransparency := AValue;
  UpdateParentChart;
end;

procedure TCustomChartSeries.SetZPosition(AValue: TChartDistance);
begin
  if FZPosition = AValue then exit;
  FZPosition := AValue;
  UpdateParentChart;
end;

procedure TCustomChartSeries.StyleChanged(Sender: TObject);
begin
  if ParentChart <> nil then
    ParentChart.StyleChanged(Sender);
end;

function TCustomChartSeries.TitleIsStored: Boolean;
begin
  Result := Title <> '';
end;

procedure TCustomChartSeries.UpdateParentChart;
begin
  if ParentChart <> nil then
    ParentChart.StyleChanged(Self);
end;

{ TChartSeries }

function TChartSeries.Add(AValue: Double; AXLabel: String; AColor: TColor): Integer;
begin
  Result := AddXY(GetXMaxVal + 1, AValue, AXLabel, AColor);
end;

function TChartSeries.AddArray(const AValues: array of Double): Integer;
var
  a: Double;
begin
  Result := ListSource.Count;
  for a in AValues do
    Add(a);
end;

function TChartSeries.AddNull(ALabel: String; AColor: TColor): Integer;
begin
  Result := ListSource.Add(SafeNan, SafeNan, ALabel, AColor);
end;

function TChartSeries.AddX(AX: Double; ALabel: String; AColor: TColor): Integer;
begin
  Result := ListSource.Add(AX, SafeNan, ALabel, AColor);
end;

function TChartSeries.AddXY(
  AX, AY: Double; const AYList: array of Double;
  AXLabel: String; AColor: TColor): Integer;
begin
  Result := ListSource.Add(AX, AY, AXLabel, AColor);
  ListSource.SetYList(Result, AYList);
end;

function TChartSeries.AddXY(
  AX, AY: Double; AXLabel: String; AColor: TColor): Integer;
begin
  Result := ListSource.Add(AX, AY, AXLabel, AColor);
end;

function TChartSeries.AddY(AY: Double; ALabel: String; AColor: TColor): Integer;
begin
  Result := Add(AY, ALabel, AColor);
end;

procedure TChartSeries.AfterAdd;
begin
  inherited;
  Marks.SetOwner(FChart);
  Marks.Arrow.SetOwner(FChart);
  Marks.Margins.SetOwner(FChart);
end;

procedure TChartSeries.AfterDraw;
begin
  inherited AfterDraw;
  Source.AfterDraw;
end;

procedure TChartSeries.Assign(ASource: TPersistent);
begin
  if ASource is TChartSeries then
    with TChartSeries(ASource) do begin
      Self.Marks.Assign(FMarks);
      Self.FOnGetMark := FOnGetMark;
      Self.FOnGetMarkText := FOnGetMarkText;
      Self.Source := FSource;
      Self.Styles := FStyles;
    end;
  inherited Assign(ASource);
end;

procedure TChartSeries.BeforeDraw;
begin
  inherited BeforeDraw;
  Source.BeforeDraw;
end;

procedure TChartSeries.BeginUpdate;
begin
  ListSource.BeginUpdate;
end;

procedure TChartSeries.CheckSource(ASource: TCustomChartSource);
var
  nx, ny: Cardinal;
begin
  if ASource = nil then
    exit;
  GetXYCountNeeded(nx, ny);
  if ASource.XCount < nx then
    raise EXCountError.CreateFmt(rsSourceCountError, [ClassName, nx, 'x']);
  if ASource.YCount < ny then
    raise EYCountError.CreateFmt(rsSourceCountError, [ClassName, ny, 'y']);
end;

procedure TChartSeries.Clear;
begin
  ListSource.Clear;
end;

function TChartSeries.Count: Integer;
begin
  Result := Source.Count;
end;

function TChartSeries.LastValueIndex: Integer;
begin
  Result := Source.Count - 1;
end;

constructor TChartSeries.Create(AOwner: TComponent);
const
  BUILTIN_SOURCE_NAME = 'Builtin';
var
  nx, ny: Cardinal;
begin
  inherited Create(AOwner);

  FListener := TListener.Create(@FSource,  @SourceChanged);
  GetXYCountNeeded(nx, ny);
  FBuiltinSource := TBuiltinListChartSource.Create(Self, nx, ny);
  FBuiltinSource.Name := BUILTIN_SOURCE_NAME;
  FBuiltinSource.Broadcaster.Subscribe(FListener);
  FMarks := TChartMarks.Create(FChart);
  FStylesListener := TListener.Create(@FStyles,  @StyleChanged);
end;

procedure TChartSeries.Delete(AIndex: Integer);
begin
  ListSource.Delete(AIndex);
end;

destructor TChartSeries.Destroy;
begin
  FreeAndNil(FListener);
  FreeAndNil(FBuiltinSource);
  FreeAndNil(FMarks);
  FreeAndNil(FStylesListener);
  inherited;
end;

procedure TChartSeries.EndUpdate;
begin
  ListSource.EndUpdate;
  UpdateParentChart;
end;

function TChartSeries.Extent: TDoubleRect;
begin
  Result := Source.ExtentCumulative;
end;

procedure TChartSeries.FindYRange(AXMin, AXMax: Double;
  var AYMin, AYMax: Double);
begin
  Source.FindYRange(AXMin, AXMax, false, AYMin, AYMax);
end;

function TChartSeries.FormattedMark(AIndex: Integer; AFormat: String;
  AYIndex, AXIndex: Integer): String;
begin
  if Assigned(FOnGetMark) then
    FOnGetMark(Result, AIndex)
  else
  begin
    Result := Source.FormatItem(
      IfThen(AFormat = '', Marks.Format, AFormat), AIndex, AYIndex);
    if Assigned(FOnGetMarkText) then
      FOnGetMarkText(Self, AIndex, AXIndex, AYIndex, Result)
  end;
end;

procedure TChartSeries.GetBounds(var ABounds: TDoubleRect);
var
  i: Integer;
begin
  if IsEmpty or (not Active) then exit;
  with Extent do
    for i := Low(coords) to High(coords) do
      if not IsInfinite(coords[i]) then
        ABounds.coords[i] := coords[i];
end;

function TChartSeries.GetColor(AIndex: Integer): TColor;
begin
  Result := ColorDef(Source[AIndex]^.Color, GetSeriesColor);
end;

function TChartSeries.GetGraphPoint(AIndex: Integer): TDoublePoint;
begin
  Result.X := GetGraphPointX(AIndex);
  Result.Y := GetGraphPointY(AIndex);
  if IsRotated then
    Exchange(Result.X, Result.Y);
end;

function TChartSeries.GetGraphPoint(AIndex, AXIndex, AYIndex: Integer): TDoublePoint;
begin
  Result.X := GetGraphPointX(AIndex, AXIndex);
  Result.Y := GetGraphPointY(AIndex, AYIndex);
  if IsRotated then
    Exchange(Result.X, Result.Y);
end;

function TChartSeries.GetGraphPointX(AIndex: Integer): Double;
begin
  if Source.XCount = 0 then
    Result := AxisToGraphX(AIndex)
  else
    Result := AxisToGraphX(Source[AIndex]^.X);
end;

function TChartSeries.GetGraphPointX(AIndex, AXIndex: Integer): Double;
begin
  if Source.XCount = 0 then
    Result := AxisToGraphX(AIndex)
  else
    Result := AxisToGraphX(Source[AIndex]^.GetX(AXIndex));
end;

function TChartSeries.GetGraphPointY(AIndex: Integer): Double;
begin
  Result := AxisToGraphY(Source[AIndex]^.Y);
end;

function TChartSeries.GetGraphPointY(AIndex, AYIndex: Integer): Double;
begin
  Result := AxisToGraphY(Source[AIndex]^.GetY(AYIndex));
end;

procedure TChartSeries.GetMax(out X, Y: Double);
begin
  X := Source.XOfMax;
  Y := Extent.b.Y;
end;

procedure TChartSeries.GetMin(out X, Y: Double);
begin
  X := Source.XOfMin;
  Y := Extent.a.Y;
end;

function TChartSeries.GetSeriesColor: TColor;
begin
  Result := clTAColor;
end;

function TChartSeries.GetSource: TCustomChartSource;
begin
  if Assigned(FSource) then
    Result := FSource
  else
    Result := FBuiltinSource;
end;

function TChartSeries.GetXImgValue(AIndex: Integer): Integer;
begin
  Result := ParentChart.XGraphToImage(Source[AIndex]^.X);
end;

function TChartSeries.GetXMax: Double;
begin
  Result := Extent.b.X;
end;

function TChartSeries.MaxXValue: Double;
begin
  Result := Extent.b.X;
end;

function TChartSeries.GetXMaxVal: Double;
begin
  if Count > 0 then
    Result := Source[Count - 1]^.X
  else
    Result := 0;
end;

class procedure TChartSeries.GetXYCountNeeded(out AXCount, AYCount: Cardinal);
begin
  AXCount := 0;
  AYCount := 1;
end;

function TChartSeries.GetXMin: Double;
begin
  Result := Extent.a.X;
end;

function TChartSeries.MinXValue: Double;
begin
  Result := Extent.a.X;
end;

function TChartSeries.GetXValue(AIndex: Integer): Double;
begin
  if Source.XCount > 0 then
    Result := Source[AIndex]^.X
  else
    Result := AIndex;
end;

function TChartSeries.GetXValues(AIndex, AXIndex: Integer): Double;
begin
  if AXIndex > 0 then
    Result := Source[AIndex]^.XList[AXIndex - 1]
  else
    Result := Source[AIndex]^.X;
end;

function TChartSeries.GetYImgValue(AIndex: Integer): Integer;
begin
  Result := ParentChart.YGraphToImage(Source[AIndex]^.Y);
end;

function TChartSeries.GetYMax: Double;
begin
  Result := Extent.b.Y;
end;

function TChartSeries.MaxYValue: Double;
begin
  Result := Extent.b.Y;
end;

function TChartSeries.GetYMin: Double;
begin
  Result := Extent.a.Y;
end;

function TChartSeries.MinYValue: Double;
begin
  Result := Extent.a.Y;
end;

function TChartSeries.GetYValue(AIndex: Integer): Double;
begin
  Result := Source[AIndex]^.Y;
end;

function TChartSeries.GetYValues(AIndex, AYIndex: Integer): Double;
begin
  if AYIndex = 0 then
    Result := GetYValue(AIndex)
  else
    Result := Source[AIndex]^.YList[AYIndex - 1];
end;

function TChartSeries.IsEmpty: Boolean;
begin
  Result := Count = 0;
end;

function TChartSeries.IsSourceStored: boolean;
begin
  Result := FSource <> nil;
end;

function TChartSeries.LegendTextPoint(AIndex: Integer): String;
begin
  Result := FormattedMark(AIndex, Legend.Format);
end;

function TChartSeries.ListSource: TListChartSource;
begin
  if not (Source is TListChartSource) then
    raise EEditableSourceRequired.Create(rsSourceNotEditable);
  Result := TListChartSource(Source);
end;

procedure TChartSeries.SetColor(AIndex: Integer; AColor: TColor);
begin
  ListSource.SetColor(AIndex, AColor);
end;

procedure TChartSeries.SetMarks(AValue: TChartMarks);
begin
  if FMarks = AValue then exit;
  FMarks.Assign(AValue);
end;

procedure TChartSeries.SetOnGetMark(AValue: TChartGetMarkEvent);
begin
  if TMethod(FOnGetMark) = TMethod(AValue) then exit;
  FOnGetMark := AValue;
  UpdateParentChart;
end;

procedure TChartSeries.SetOnGetMarkText(AValue: TChartGetMarkTextEvent);
begin
  if TMethod(FOnGetMarkText) = TMethod(AValue) then exit;
  FOnGetMarkText := AValue;
  UpdateParentChart;
end;

procedure TChartSeries.SetSource(AValue: TCustomChartSource);
begin
  if AValue = FBuiltinSource then
    AValue := nil;
  if FSource = AValue then
    exit;
  CheckSource(AValue);
  if FListener.IsListening then
    Source.Broadcaster.Unsubscribe(FListener);
  FSource := AValue;
  Source.Broadcaster.Subscribe(FListener);
  SourceChanged(Self);
end;

procedure TChartSeries.SetStyles(AValue: TChartStyles);
begin
  if FStyles = AValue then exit;
  if FStylesListener.IsListening then
    Styles.Broadcaster.Unsubscribe(FStylesListener);
  FStyles := AValue;
  if Styles <> nil then
    Styles.Broadcaster.Subscribe(FStylesListener);
  UpdateParentChart;
end;

procedure TChartSeries.SetText(AIndex: Integer; AValue: String);
begin
  ListSource.SetText(AIndex, AValue);
end;

procedure TChartSeries.SetXValue(AIndex: Integer; AValue: Double); inline;
begin
  ListSource.SetXValue(AIndex, AValue);
end;

procedure TChartSeries.SetXValues(AIndex, AXIndex: Integer; AValue: Double);
begin
  if AXIndex = 0 then
    ListSource.SetXValue(AIndex, AValue)
  else
    ListSource.Item[AIndex]^.XList[AXIndex - 1] := AValue;
end;

procedure TChartSeries.SetYValue(AIndex: Integer; AValue: Double); inline;
begin
  ListSource.SetYValue(AIndex, AValue);
end;

procedure TChartSeries.SetYValues(AIndex, AYIndex: Integer; AValue: Double);
begin
  if AYIndex = 0 then
    ListSource.SetYValue(AIndex, AValue)
  else
    ListSource.Item[AIndex]^.YList[AYIndex - 1] := AValue;
end;

procedure TChartSeries.SourceChanged(ASender: TObject);
begin
  if (ASender <> FBuiltinSource) and (ASender is TCustomChartSource) then
    try
      CheckSource(TCustomChartSource(ASender));
    except
      Source := nil; // revert to built-in source
      raise;
    end;
  StyleChanged(ASender);
end;

procedure TChartSeries.VisitSources(
  AVisitor: TChartOnSourceVisitor; AAxis: TChartAxis; var AData);
begin
  if (AAxis = GetAxisX) or (AAxis = GetAxisY) then
    AVisitor(Source, AData);
end;

{ TBasicPointSeries }

procedure TBasicPointSeries.AfterAdd;
var
  i: Integer;
begin
  inherited AfterAdd;
  if Pointer <> nil then
    Pointer.SetOwner(ParentChart);
  for i := 0 to 1 do
    if FErrorBars[i] <> nil then
      FErrorBars[i].SetOwner(ParentChart);
end;

procedure TBasicPointSeries.AfterDrawPointer(
  ADrawer: IChartDrawer; AIndex: Integer; const APos: TPoint);
begin
  Unused(ADrawer);
  Unused(AIndex, APos);
end;

procedure TBasicPointSeries.Assign(ASource: TPersistent);
begin
  if ASource is TBasicPointSeries then
    with TBasicPointSeries(ASource) do begin
      Self.FMarkPositions := MarkPositions;
      if Self.FPointer <> nil then
        Self.FPointer.Assign(Pointer);
      Self.Stacked := Stacked;
      Self.FSupportsZeroLevel := FSupportsZeroLevel;
      Self.FMarkPositionCentered := FMarkPositionCentered;
    end;
  inherited Assign(ASource);
end;

constructor TBasicPointSeries.Create(AOwner: TComponent);
begin
  inherited;
  FErrorBars[0] := TChartErrorBar.Create(FChart);
  FErrorBars[1] := TChartErrorBar.Create(FChart);
  FOptimizeX := true;
  FLoBound := 0;
  FUpBound := Count - 1;
  ToolTargets := [nptPoint, nptYList];
end;

destructor TBasicPointSeries.Destroy;
begin
  FreeAndNil(FErrorBars[0]);
  FreeAndNil(FErrorBars[1]);
  FreeAndNil(FPointer);
  inherited;
end;

procedure TBasicPointSeries.DrawErrorBars(ADrawer: IChartDrawer);

  procedure EndBar(p: TPoint; w: Integer; IsHorBar: Boolean);
  begin
    if IsHorBar then
      ADrawer.Line(Point(p.x, p.y-w), Point(p.x, p.y+w))
    else
      ADrawer.Line(Point(p.x-w, p.y), Point(p.x+w, p.y));
  end;

  procedure DrawErrorBar(p: TDoublePoint; vp, vn: Double; w: Integer;
    IsXError: Boolean);
  var
    p1, p2: TDoublePoint;
    imgPt1, imgPt2: TPoint;
    isHorBar: Boolean;
  begin
    isHorBar := (IsXError and not IsRotated) or (IsRotated and not IsXError);

    if IsHorBar then begin
      p1 := DoublePoint(vp, p.Y);
      p2 := DoublePoint(vn, p.Y);
    end else begin
      p1 := DoublePoint(p.X, vp);
      p2 := DoublePoint(p.X, vn);
    end;
    imgPt1 := ParentChart.GraphToImage(p1);
    imgPt2 := ParentChart.GraphToImage(p2);
    ADrawer.Line(imgPt1, imgPt2);

    EndBar(imgPt1, w, isHorBar);
    EndBar(imgPt2, w, isHorBar);
  end;

  procedure InternalDrawErrorBars(IsXError: Boolean);
  var
    i: Integer;
    p: TDoublePoint;
    vp, vn: Double;
    w, w0: Integer;
    errbar: TChartErrorBar;
  begin
    if Assigned(Pointer) then
      w0 := IfThen(IsXError, Pointer.VertSize, Pointer.HorizSize)
    else
      w0 := DEF_ERR_ENDLENGTH;
    errbar := TChartErrorBar(IfThen(IsXError, XErrorBars, YErrorBars));
    w := ADrawer.Scale(IfThen(errBar.Width = -1, w0, errBar.Width));

    for i := FLoBound to FUpBound do begin
      p := FGraphPoints[i - FLoBound];
      if not ParentChart.IsPointInViewPort(p) then continue;
      if IsXError then begin
        if Source.GetXErrorBarLimits(i, vp, vn) then
          DrawErrorBar(p, AxisToGraphX(vp), AxisToGraphX(vn), w, true);
      end else begin
        if Source.GetYErrorBarLimits(i, vp, vn) then
          DrawErrorBar(p, AxisTographY(vp), AxisToGraphY(vn), w, false);
      end;
    end;
  end;

begin
  // Draw x error bars
  if Assigned(XErrorBars) and XErrorBars.Visible and Source.HasXErrorBars then
  begin
    ADrawer.Pen := XErrorBars.Pen;
    InternalDrawErrorBars(true);
  end;

  // Draw y error bars
  if Assigned(YErrorBars) and YErrorBars.Visible and Source.HasYErrorBars then
  begin
    ADrawer.Pen := YErrorBars.Pen;
    InternalDrawErrorBars(false);
  end;
end;

function TBasicPointSeries.IsPointInLabel(ADrawer: IChartDrawer; 
  const APoint: TPoint; var APointIndex, AYIndex: Integer): Boolean;
const
  OFFSETS: array [TLabelDirection] of TPoint = (
    (X: -1; Y: 0), 
    (X: 0; Y: -1), 
    (X: 1; Y: 0), 
    (X: 0; Y: 1)
  );
var
  y, ysum: Double;
  g: TDoublePoint;
  pt: TPoint;
  i, si: Integer;
  style: TChartStyle;
  lfont: TFont;
  curr, prev: Double;
  ext: TDoubleRect;
  yIsNaN: Boolean;
  centerLvl: Double;
  center: TPoint;
  dir: TLabelDirection;
  txt: String;
begin
  Result := false;
  if not Marks.IsMarkLabelsVisible then exit;

  lfont := TFont.Create;
  try
    lfont.Assign(Marks.LabelFont);
    ext := Extent;
    centerLvl := AxisToGraphY((ext.a.y + ext.b.y) * 0.5);
    UpdateLabelDirectionReferenceLevel(0, 0, centerLvl);
    
    for i := FLoBound to FUpBound do begin
      if SkipMissingValues(i) then
        continue;
      prev := IfThen(FSupportsZeroLevel, GetZeroLevel, 0.0);
      for si := 0 to Source.YCount - 1 do begin
        g := GetLabelDataPoint(i, si);
        if FStacked then begin
          if si = 0 then begin
            y := Source[i]^.Y;
            yIsNaN := IsNaN(y);
            ysum := IfThen(yIsNaN, prev, y);
          end else begin
            y := Source[i]^.YList[si-1];
            yIsNaN := IsNaN(y);
            if yIsNaN then y := 0.0;
            if Stacked then begin
              ysum += y;
              y := ysum;
            end;
          end;
          if IsRotated then
            g.X := AxisToGraphY(y)
            // Axis-to-graph transformation is independent of axis rotation ->
            // Using AxisToGraph_Y_ is correct!
          else
            g.Y := AxisToGraphY(y);
        end else
          yIsNaN := IsNaN(g.y);

        txt := FormattedMark(i, '', si);
        if txt = '' then
          continue;

        curr := TDoublePointBoolArr(g)[not IsRotated];
        if FMarkPositionCentered then begin
          if IsRotated then
            g := DoublePoint((curr + prev) * 0.5, g.y)
          else
            g := DoublePoint(g.x, (curr + prev) * 0.5);
        end;
        if Stacked then
          prev := curr;

        // check only the requested y index
        if (AYIndex >= 0) then begin
          if si < AYIndex then
            Continue
          else if si > AYIndex then
            break;
        end;

        with ParentChart do
          if
            ((Marks.YIndex = MARKS_YINDEX_ALL) or (Marks.YIndex = si)) and
            IsPointInViewPort(g) and (not yIsNaN)
          then begin
            if Styles <> nil then begin
              style := Styles.StyleByIndex(si);
              if style.UseFont then
                Marks.LabelFont.Assign(style.Font)
              else
                Marks.LabelFont.Assign(lfont);
            end;
            UpdateLabelDirectionReferenceLevel(i, si, centerLvl);
            dir := GetLabelDirection(IfThen(IsRotated, g.X, g.Y), centerLvl);
            pt := GraphToImage(g);
            if Marks.RotationCenter = rcCenter then
              center := pt + OFFSETS[dir] * Marks.CenterOffset(ADrawer, txt)
            else 
              center := pt + OFFSETS[dir] * Marks.CenterHeightOffset(ADrawer, txt);
            
            Result := Marks.IsPointInLabel(
              ADrawer, 
              APoint,
              pt,
              center,
              txt
            );
            if Result then
            begin
              APointIndex := i;
              AYIndex := si;
              exit;
            end;
          end;
      end;
    end;

  finally
    Marks.LabelFont.Assign(lfont);
    ParentChart.EnableRedrawing;
    lfont.Free;
  end;
end;
    
procedure TBasicPointSeries.DrawLabels(ADrawer: IChartDrawer; AYIndex: Integer = -1);
// Using AYIndex is workaround for issue #35077
var
  prevLabelPoly: TPointArray;

  procedure DrawLabel(
    const AText: String; const ADataPoint: TPoint; ADir: TLabelDirection);
  const
    OFFSETS: array [TLabelDirection] of TPoint =
      ((X: -1; Y: 0), (X: 0; Y: -1), (X: 1; Y: 0), (X: 0; Y: 1));
  var
    center: TPoint;
  begin
    if AText = '' then exit;

    if Marks.RotationCenter = rcCenter then
      center := ADataPoint + OFFSETS[ADir] * Marks.CenterOffset(ADrawer, AText)
    else
      center := ADataPoint + OFFSETS[ADir] * Marks.CenterHeightOffset(ADrawer, AText);
    Marks.DrawLabel(ADrawer, ADataPoint, center, AText, prevLabelPoly);
  end;

var
  y, ysum: Double;
  g: TDoublePoint;
  i, si: Integer;
  style: TChartStyle;
  lfont: TFont;
  curr, prev: Double;
  ext: TDoubleRect;
  yIsNaN: Boolean;
  centerLvl: Double;
begin
  if not Marks.IsMarkLabelsVisible then exit;
  lfont := TFont.Create;
  try
    lfont.Assign(Marks.LabelFont);
    ParentChart.DisableRedrawing;
    ext := Extent;
    centerLvl := AxisToGraphY((ext.a.y + ext.b.y) * 0.5);
    UpdateLabelDirectionReferenceLevel(0, 0, centerLvl);

    for i := FLoBound to FUpBound do begin
      if SkipMissingValues(i) then
        continue;
      prev := IfThen(FSupportsZeroLevel, GetZeroLevel, 0.0);
      for si := 0 to Source.YCount - 1 do begin
        g := GetLabelDataPoint(i, si);
        if FStacked then begin
          if si = 0 then begin
            y := Source[i]^.Y;
            yIsNaN := IsNaN(y);
            ysum := IfThen(yIsNaN, prev, y);
          end else begin
            y := Source[i]^.YList[si-1];
            yIsNaN := IsNaN(y);
            if yIsNaN then y := 0.0;
            if Stacked then begin
              ysum += y;
              y := ysum;
            end;
          end;
          if IsRotated then
            g.X := AxisToGraphY(y)
            // Axis-to-graph transformation is independent of axis rotation ->
            // Using AxisToGraph_Y_ is correct!
          else
            g.Y := AxisToGraphY(y);
        end else
          yIsNaN := IsNaN(g.y);

        curr := TDoublePointBoolArr(g)[not IsRotated];
        if FMarkPositionCentered then begin
          if IsRotated then
            g := DoublePoint((curr + prev) * 0.5, g.y)
          else
            g := DoublePoint(g.x, (curr + prev) * 0.5);
        end;
        if Stacked then
          prev := curr;

        // Draw only the requested y index
        if (AYIndex >= 0) then begin
          if si < AYIndex then
            Continue
          else if si > AYIndex then
            break;
        end;

        with ParentChart do
          if
            ((Marks.YIndex = MARKS_YINDEX_ALL) or (Marks.YIndex = si)) and
            IsPointInViewPort(g) and (not yIsNaN)
          then begin
            if Styles <> nil then begin
              style := Styles.StyleByIndex(si);
              if (style <> nil) and style.UseFont then
                Marks.LabelFont.Assign(style.Font)
              else
                Marks.LabelFont.Assign(lfont);
            end;
            UpdateLabelDirectionReferenceLevel(i, si, centerLvl);
            DrawLabel(
              FormattedMark(i, '', si),
              GraphToImage(g),
              GetLabelDirection(IfThen(IsRotated, g.X, g.Y), centerLvl)
            );
          end;
      end;
    end;

  finally
    Marks.LabelFont.Assign(lfont);
    ParentChart.EnableRedrawing;
    lfont.Free;
  end;
end;

{ Draws the pointers of the series.
  If ChartStyles are attached to the series then the pointer brush is determined
  by the style with the specified index. }
procedure TBasicPointSeries.DrawPointers(ADrawer: IChartDrawer;
  AStyleIndex: Integer = 0; UseDataColors: Boolean = false);
var
  i: Integer;
  p: TDoublePoint;
  ai: TPoint;
  lPointer: TSeriesPointer;
  ps, saved_ps: TSeriesPointerStyle;
  brushAlreadySet: boolean;
  c: TColor;
  style: TChartStyle;
begin
  Assert(Pointer <> nil, 'Series pointer');
  if (not Pointer.Visible) or (Length(FGraphPoints) = 0) then exit;
  if (Styles <> nil) then
  begin
    style := Styles.StyleByIndex(AStyleIndex);
    if (style <> nil) and style.UsePointer and (not style.Pointer.Visible) then
      exit;
  end;

  for i := FLoBound to FUpBound do begin
    p := FGraphPoints[i - FLoBound];
    if not ParentChart.IsPointInViewPort(p) then continue;
    ai := ParentChart.GraphToImage(p);
    if Assigned(FOnCustomDrawPointer) then
      FOnCustomDrawPointer(Self, ADrawer, i, ai)
    else
    begin
      lPointer := Pointer;
      brushAlreadySet := false;
      if (Styles <> nil) then
      begin
        style := Styles.StyleByIndex(AStyleIndex);
        if style <> nil then
        begin
          brushAlreadySet := style.UseBrush;
          if style.UsePointer then
          begin
            lPointer := style.Pointer;
            brushAlreadySet := false;
          end;
        end;
      end;
      if Assigned(FOnGetPointerStyle) then
      begin
        saved_ps := lPointer.Style;
        ps := saved_ps;
        FOnGetPointerStyle(self, i, ps);
        lPointer.LockUpdate;   // Avoid recursion
        lPointer.Style := ps;
        lPointer.UnlockUpdate;
      end;
      if brushAlreadySet then
        Styles.Apply(ADrawer, AStyleIndex);
      if UseDataColors then c := Source[i]^.Color else c := clTAColor;
      lPointer.Draw(ADrawer, ai, c, brushAlreadySet);
      AfterDrawPointer(ADrawer, i, ai);
      if Assigned(FOnGetPointerStyle) then
      begin
        lPointer.LockUpdate;
        lPointer.Style := saved_ps;
        lPointer.UnlockUpdate;
      end;
    end;
  end;
end;

function TBasicPointSeries.Extent: TDoubleRect;
begin
  if FStacked then
    Result := Source.ExtentCumulative
  else
    Result := Source.ExtentList;
end;

// Find an interval of x-values intersecting the extent.
// Requires monotonic (but not necessarily increasing) axis transformation.
procedure TBasicPointSeries.FindExtentInterval(
  const AExtent: TDoubleRect; AFilterByExtent: Boolean);
var
  axisExtent: TDoubleInterval;
begin
  FLoBound := 0;
  FUpBound := Count - 1;
  if AFilterByExtent then begin
    with AExtent do
      if IsRotated then
        axisExtent := DoubleInterval(GraphToAxisX(a.Y), GraphToAxisX(b.Y))
      else
        axisExtent := DoubleInterval(GraphToAxisX(a.X), GraphToAxisX(b.X));
    Source.FindBounds(axisExtent.FStart, axisExtent.FEnd, FLoBound, FUpBound);
    FLoBound := Max(FLoBound - 1, 0);
    FUpBound := Min(FUpBound + 1, Count - 1);
  end;
end;

procedure TBasicPointSeries.FindYRange(AXMin, AXMax: Double; var AYMin, AYMax: Double);
begin
  Source.FindYRange(AXMin, AXMax, FStacked, AYMin, AYMax);
end;

function TBasicPointSeries.GetErrorBars(AIndex: Integer): TChartErrorBar;
begin
  Result := FErrorBars[AIndex];
end;

function TBasicPointSeries.GetLabelDataPoint(AIndex, AYIndex: Integer): TDoublePoint;
begin
  Result := GetGraphPoint(AIndex, 0, AYIndex);
end;

function TBasicPointSeries.GetLabelDirection(AValue: Double;
  const ACenterLevel: Double): TLabelDirection;
const
  DIR: array [Boolean, Boolean] of TLabelDirection =
    ((ldTop, ldBottom), (ldRight, ldLeft));
var
  isNeg: Boolean;
  ref: Double;
begin
  case MarkPositions of
    lmpPositive: isNeg := false;
    lmpNegative: isNeg := true;
    lmpOutside,
    lmpInside :
      begin
        ref := IfThen(FSupportsZeroLevel, AxisToGraphY(GetZeroLevel), ACenterLevel);
        if AValue < ref then
          isNeg := true
        else
        if AValue > ref then
          isNeg := false
        else
        if not FSupportsZeroLevel then
          isNeg := false
        else
          isNeg := AValue < ACenterLevel;
        if MarkPositions = lmpInside then
          isNeg := not isNeg;
      end;
  end;
  if Assigned(GetAxisY) then
    if (IsRotated and ParentChart.IsRightToLeft) xor GetAxisY.Inverted then
      isNeg := not isNeg;
  Result := DIR[IsRotated, isNeg];
end;

procedure TBasicPointSeries.GetLegendItemsRect(
  AItems: TChartLegendItems; ABrush: TBrush; APen: TPen);
var
  i: Integer;
  li: TLegendItemBrushPenRect;
  s: TChartStyle;
  addToLegend: Boolean;
begin
  case Legend.Multiplicity of
    lmSingle:
      begin
        li := TLegendItemBrushPenRect.Create(ABrush, APen, LegendTextSingle);
        li.TextFormat := Legend.TextFormat;
        AItems.Add(li);
      end;
    lmPoint:
      for i := 0 to Count - 1 do begin
        li := TLegendItemBrushPenRect.Create(ABrush, APen, LegendTextPoint(i));
        li.Color := GetColor(i);
        li.TextFormat := Legend.TextFormat;
        AItems.Add(li);
      end;
    lmStyle:
      if Styles <> nil then
        for s in Styles.Styles do
        begin
          addToLegend := true;
          if Assigned(Styles.OnAddStyleToLegend) then
            Styles.OnAddStyleToLegend(s, self, addToLegend);
          if addToLegend then
            AItems.Add(TLegendItemBrushPenRect.Create(
              IfThen(s.UseBrush, s.Brush, ABrush) as TBrush,
              IfThen(s.UsePen, s.Pen, APen) as TPen,
              LegendTextStyle(s)
            ));
        end;
  end;
end;

function TBasicPointSeries.GetNearestPoint(
  const AParams: TNearestPointParams;
  out AResults: TNearestPointResults): Boolean;

  function GetGrabBound(ARadius: Integer): Double;
  begin
    if IsRotated then
      Result := ParentChart.YImageToGraph(AParams.FPoint.Y + ARadius)
    else
      Result := ParentChart.XImageToGraph(AParams.FPoint.X + ARadius);
    Result := GraphToAxisX(Result);
  end;

var
  dist, tmpDist, i, j, lb, ub: Integer;
  sp, tmpSp: TDoublePoint;
  pt, tmpPt: TDoublePoint;
begin
  AResults.FDist := Sqr(AParams.FRadius) + 1;  // the dist func does not calc sqrt
  AResults.FIndex := -1;
  AResults.FXIndex := 0;
  AResults.FYIndex := 0;
  if IsEmpty then exit(false);
  if not RequestValidChartScaling then exit(false);

  if FOptimizeX and AParams.FOptimizeX then
    Source.FindBounds(
      GetGrabBound(-AParams.FRadius),
      GetGrabBound( AParams.FRadius), lb, ub)
  else begin
    lb := 0;
    ub := Count - 1;
  end;

  dist := AResults.FDist;
  for i := lb to ub do begin
    sp := Source[i]^.Point;
    if IsNan(sp) then
      continue;

    // Since axis transformation may be non-linear, the distance should be
    // measured in screen coordinates. With high zoom ratios this may lead to
    // an integer overflow, so ADistFunc should use saturation arithmetics.

    // Find nearest point of datapoint at (x, y)
    if (nptPoint in AParams.FTargets) and (nptPoint in ToolTargets) then
    begin
      pt := AxisToGraph(sp);
      dist := Min(dist, ToolTargetDistance(AParams, pt, i, 0, 0));
    end;

    // Find nearest point to additional y values (at x).
    // In case of stacked data points check the stacked values.
    if (dist > 0) and (nptYList in AParams.FTargets) and (nptYList in ToolTargets)
    then begin
      tmpSp := sp;
      for j := 0 to Source.YCount - 2 do begin
        if FStacked then
          tmpSp.Y += Source[i]^.YList[j] else
          tmpSp.Y := Source[i]^.YList[j];
        tmpPt := AxisToGraph(tmpSp);
        tmpDist := ToolTargetDistance(AParams, tmpPt, i, 0, j + 1);
        if tmpDist < dist then begin
          dist := tmpDist;
          sp := tmpSp;
          pt := tmpPt;
          AResults.FYIndex := j + 1;    // FYIndex = 0 refers to the regular y
        end;
      end;
    end;

    // Find nearest point of additional x values (at y)
    if (dist > 0) and (nptXList in AParams.FTargets) and (nptXList in ToolTargets)
    then begin
      tmpSp := sp;
      for j := 0 to Source.XCount - 2 do begin
        tmpSp.X := Source[i]^.XList[j];
        tmpPt := AxisToGraph(tmpSp);
        tmpDist := ToolTargetDistance(AParams, tmpPt, i, j + 1, 0);
        if tmpDist < dist then begin
          dist := tmpDist;
          sp := tmpSp;
          pt := tmpPt;
          AResults.FXIndex := j + 1;   // FXindex = 0 refers to the regular x
        end;
      end;
    end;

    // The case nptCustom is not handled here, it depends on the series type.
    // TBarSeries, for example, checks whether AParams.FPoint is inside a bar.

    if dist >= AResults.FDist then
      continue;

    AResults.FDist := dist;
    AResults.FIndex := i;
    AResults.FValue := sp;
    AResults.FImg := ParentChart.GraphToImage(pt);
    if dist = 0 then break;
  end;
  Result := AResults.FIndex >= 0;
end;

function TBasicPointSeries.GetXRange(AX: Double; AIndex: Integer): Double;
var
  wl, wr: Double;
  i: Integer;
begin
  if Source.XCount > 0 then begin
    i := AIndex - 1;
    wl := Abs(AX - NearestXNumber(i, -1));
    i := AIndex + 1;
    wr := Abs(AX - NearestXNumber(i, +1));
    Result := NumberOr(SafeMin(wl, wr), 1.0);
  end else
    Result := 1.0;
end;

function TBasicPointSeries.GetZeroLevel: Double;
begin
  Result := 0.0;
end;

{ Returns true if the data point at the given index has at least one missing
  y value (NaN) }
function TBasicPointSeries.HasMissingYValue(AIndex: Integer;
  AMaxYIndex: Integer = MaxInt): Boolean;
var
  j: Integer;
begin
  Result := IsNaN(Source[AIndex]^.Y);
  if not Result then
    for j := 0 to Min(AMaxYIndex, Source.YCount)-2 do
      if IsNaN(Source[AIndex]^.YList[j]) then
        exit(true);
end;

function TBasicPointSeries.IsErrorBarsStored(AIndex: Integer): Boolean;
begin
  with FErrorBars[AIndex] do
    Result := Visible or (Width <> -1) or (Pen.Color <> clBlack) or
      (not Pen.Cosmetic) or (Pen.EndCap <> pecRound) or
      (Pen.JoinStyle <> pjsRound) or (Pen.Mode <> pmCopy) or
      (Pen.Style <> psSolid) or (Pen.Width <> 1);
end;

procedure TBasicPointSeries.MovePoint(
  var AIndex: Integer; const ANewPos: TDoublePoint);
var
  p: TDoublePoint;
begin
  if not InRange(AIndex, 0, Count - 1) then exit;
  p := GraphToAxis(ANewPos);
  with ListSource do begin
    AIndex := SetXValue(AIndex, p.X);
    SetYValue(AIndex, p.Y);
  end;
end;

procedure TBasicPointSeries.MovePointEx(var AIndex: Integer;
  AXIndex, AYIndex: Integer; const ANewPos: TDoublePoint);
var
  sp: TDoublePoint;
  sum: Double;
  j: Integer;
begin
  Unused(AXIndex);

  if not InRange(AIndex, 0, Count - 1) then
    exit;

  sp := GraphToAxis(ANewPos);
  case AYIndex of
   -1: begin
        // ListSource.SetXValue(AIndex, sp.X);
        // ListSource.SetYValue(AIndex, sp.Y);
       end;
    0: begin
         ListSource.SetXValue(AIndex, sp.X);
         ListSource.SetYValue(AIndex, sp.Y);
       end;
    else
      if FStacked then begin
        sum := 0;
        for j := 0 to AYIndex - 1 do
          sum := sum + YValues[AIndex, j];
        YValues[AIndex, AYIndex] := sp.Y - sum;
      end else
        YValues[AIndex, AYIndex] := sp.Y;
      UpdateParentChart;
  end;
end;

function TBasicPointSeries.NearestXNumber(
  var AIndex: Integer; ADir: Integer): Double;
begin
  while InRange(AIndex, 0, Count - 1) do
    with Source[AIndex]^ do
      if IsNan(X) then
        AIndex += ADir
      else
        exit(AxisToGraphX(X));
  Result := SafeNan;
end;

procedure TBasicPointSeries.PrepareGraphPoints(
  const AExtent: TDoubleRect; AFilterByExtent: Boolean);
var
  i: Integer;
begin
  FindExtentInterval(AExtent, AFilterByExtent);

  SetLength(FGraphPoints, Max(FUpBound - FLoBound + 1, 0));
  if (AxisIndexX < 0) and (AxisIndexY < 0) then begin
    // Optimization: bypass transformations in the default case.
    if Source.XCount > 0 then
      for i := FLoBound to FUpBound do
        with Source[i]^ do
          FGraphPoints[i - FLoBound] := DoublePoint(X, Y)
    else
      for i := FLoBound to FUpBound do
        with Source[i]^ do
          FGraphPoints[i - FLoBound] := DoublePoint(i, Y);
  end else
    for i := FLoBound to FUpBound do
      FGraphPoints[i - FLoBound] := GetGraphPoint(i);
end;

procedure TBasicPointSeries.SetErrorBars(AIndex: Integer;
  AValue: TChartErrorBar);
begin
  FErrorBars[AIndex] := AValue;
  UpdateParentChart;
end;

procedure TBasicPointSeries.SetMarkPositionCentered(AValue: Boolean);
begin
  if FMarkPositionCentered = AValue then exit;
  FMarkPositionCentered := AValue;
  UpdateParentChart;
end;

procedure TBasicPointSeries.SetMarkPositions(AValue: TLinearMarkPositions);
begin
  if FMarkPositions = AValue then exit;
  FMarkPositions := AValue;
  UpdateParentChart;
end;

procedure TBasicPointSeries.SetPointer(AValue: TSeriesPointer);
begin
  FPointer.Assign(AValue);
  UpdateParentChart;
end;

procedure TBasicPointSeries.SetStacked(AValue: Boolean);
begin
  if FStacked = AValue then exit;
  FStacked := AValue;
  UpdateParentChart;
end;

procedure TBasicPointSeries.SetStackedNaN(AValue: TStackedNaN);
begin
  if FStackedNaN = AValue then exit;
  FStackedNaN := AValue;
  UpdateParentChart;
end;

{ Returns true when the data point at the specified index contains missing
  values in a way such that the point cannot be drawn. }
function TBasicPointSeries.SkipMissingValues(AIndex: Integer): Boolean;
begin
  Result := IsNan(Source[AIndex]^.X);
  if not Result then
    Result := FStacked and (FStackedNaN = snDoNotDraw) and HasMissingYValue(AIndex);
end;

function TBasicPointSeries.ToolTargetDistance(const AParams: TNearestPointParams;
  AGraphPt: TDoublePoint; APointIdx, AXIdx, AYIdx: Integer): Integer;
var
  pt: TPoint;
begin
  Unused(APointIdx);
  Unused(AXIdx, AYIdx);
  if IsNaN(AGraphPt) then
    exit(MaxInt)
  else begin
    pt := ParentChart.GraphToImage(AGraphPt);
    Result := AParams.FDistFunc(AParams.FPoint, pt);
  end;
end;

// AIndex refers to the index into YList here.
// The ordinary Y value has Index = -1.
procedure TBasicPointSeries.UpdateGraphPoints(AIndex, ALo, AUp: Integer;
  ACumulative: Boolean);
var
  i, j: Integer;
  y: Double;
begin
  if IsRotated then begin
    if ACumulative then begin
      if FStacked and (FStackedNaN = snReplaceByZero) then
        for i := ALo to AUp do
        begin
          y := NumberOr(Source[i]^.Y, IfThen(FSupportsZeroLevel, GetZeroLevel, 0.0));
          for j := 0 to AIndex do
            y += NumberOr(Source[i]^.YList[j], 0.0);
          FGraphPoints[i - ALo].X := AxisToGraphY(y)
        end
      else
        for i := ALo to AUp do
        begin
          y := Source[i]^.Y;
          for j := 0 to AIndex do
            y += Source[i]^.YList[j];
          FGraphPoints[i - ALo].X := AxisToGraphY(y);
        end;
    end else
    if AIndex = -1 then
      for i := ALo to AUp do
        FGraphPoints[i - ALo].X := AxisToGraphY(Source[i]^.Y)
    else
      for i := ALo to AUp do
        FGraphPoints[i - ALo].X := AxisToGraphY(Source[i]^.YList[AIndex]);
  end
  else begin
    if ACumulative then begin
      if FStacked and (FStackedNaN = snReplaceByZero) then
        for i := ALo to AUp do
        begin
          y := NumberOr(Source[i]^.Y, IfThen(FSupportsZeroLevel, GetZeroLevel, 0.0));
          for j := 0 to AIndex do
            y += NumberOr(Source[i]^.YList[j], 0.0);
          FGraphPoints[i - ALo].Y := AxisToGraphY(y);
        end
      else
        for i := ALo to AUp do begin
          y := Source[i]^.Y;
          for j := 0 to AIndex do
            y += Source[i]^.YList[j];
          FGraphPoints[i - ALo].Y := AxisToGraphY(y);
        end;
    end else
    if AIndex = -1 then
      for i := ALo to AUp do
        FGraphPoints[i - ALo].Y := AxisToGraphY(Source[i]^.Y)
    else
      for i := ALo to AUp do
        FGraphPoints[i - ALo].Y := AxisToGraphY(Source[i]^.YList[AIndex]);
  end;
end;

procedure TBasicPointSeries.UpdateGraphPoints(AIndex: Integer;
  ACumulative: Boolean);
begin
  UpdateGraphPoints(AIndex, FLoBound, FUpBound, ACumulative);
end;

procedure TBasicPointSeries.SourceChanged(ASender: TObject);
begin
  FLoBound := 0;
  FUpBound := Count - 1;
  inherited;
end;

procedure TBasicPointSeries.UpdateMargins(
  ADrawer: IChartDrawer; var AMargins: TRect);
var
  i, dist, j: Integer;
  labelText: String;
  dir: TLabelDirection;
  m: array [TLabelDirection] of Integer absolute AMargins;
  gp: TDoublePoint;
  scMarksDistance: Integer;
  center: Double;
  ysum: Double;
begin
  if not Marks.IsMarkLabelsVisible or not Marks.AutoMargins then exit;
  if IsEmpty then exit;

  {FLoBound and FUpBound fields may be outdated here (if axis' range has been
   changed after the last series' painting). FLoBound and FUpBound will be fully
   updated later, in a PrepareGraphPoints() call. But we need them now. If data
   source is sorted by X in the ascending order, obtaining FLoBound and FUpBound
   is very fast (binary search) - so we call FindExtentInterval() with True as
   the second parameter. Otherwise, obtaining FLoBound and FUpBound requires
   enumerating all the data points to see, if they are in the current chart's
   viewport. But this is exactly what we are going to do in the loop below, so
   obtaining true FLoBound and FUpBound values makes no sense in this case - so
   we call FindExtentInterval() with False as the second parameter, thus setting
   FLoBound to 0 and FUpBound to Count-1}
  FindExtentInterval(ParentChart.CurrentExtent, Source.IsSortedByXAsc);

  with Extent do
    center := AxisToGraphY((a.y + b.y) * 0.5);
  UpdateLabelDirectionReferenceLevel(0, 0, center);
  scMarksDistance := ADrawer.Scale(Marks.Distance);
  for i := FLoBound to FUpBound do begin
    j := 0;
    gp := GetLabelDataPoint(i, 0);
    while true do begin
      if not ParentChart.IsPointInViewPort(gp) then break;
      labelText := FormattedMark(i, '', j);
      if labelText = '' then break;

      UpdateLabelDirectionReferenceLevel(i, j, center);
      dir := GetLabelDirection(TDoublePointBoolArr(gp)[not IsRotated], center);
      with Marks.MeasureLabel(ADrawer, labelText) do
        dist := IfThen(dir in [ldLeft, ldRight], cx, cy);
      if Marks.DistanceToCenter then
        dist := dist div 2;

      if MarkPositions <> lmpInside then
        m[dir] := Max(m[dir], dist + scMarksDistance);

      if (Source.YCount > 1) and (j = 0) then begin
        if FStacked then begin
          ysum := 0;
          for j := 0 to Source.YCount-1 do
            ysum += NumberOr(Source.Item[i]^.GetY(j), 0.0);
          TDoublePointBoolArr(gp)[not IsRotated] := AxisToGraphY(ysum);
        end else
          gp := GetLabelDataPoint(i, Source.YCount-1);
        j := Source.YCount-1;
      end else
        break;
    end;
  end;
end;

{ Can be overridden for a data-point dependent reference level, such as in
  TBubbleSeries. AIndex refers to chart source. }
procedure TBasicPointSeries.UpdateLabelDirectionReferenceLevel(AIndex, AYIndex: Integer;
  var ALevel: Double);
begin
  Unused(AIndex, AYIndex, ALevel);
end;

procedure TBasicPointSeries.UpdateMinXRange;
var
  x, prevX: Double;
  i: Integer;
begin
  if (Count < 2) or (Source.XCount = 0) then begin
    FMinXRange := 1.0;
    exit;
  end;
  x := Source[0]^.X;
  prevX := Source[1]^.X;
  FMinXRange := Abs(x - prevX);
  for i := 2 to Count - 1 do begin
    x := Source[i]^.X;
    FMinXRange := SafeMin(Abs(x - prevX), FMinXRange);
    prevX := x;
  end;
end;

procedure SkipObsoleteProperties;
const
  LEGEND_NOTE = 'Obsolete, use TCustomChartSeries.ShowInLegend instead';
begin
  RegisterPropertyToSkip(TCustomChartSeries, 'ShowInLegend', LEGEND_NOTE, '');
end;

initialization
  SkipObsoleteProperties;

end.

