1

I have the following problem: I am using Delphi XE3 with TeeChart and I'd like to retrieve a Y value or the value index of a serie by a given X value. My serie is a time series with dates on the X axis. I know the date on the chart and I want to display the nearest corresponding Y value to this date.

Is there any method or function of the TChart or TChartSeries component to achieve this? Or do I need to iterate through the series until I reached the selected date?

It is not possible to use the CursorPostion methods, because the cursor could be anywhere.

Thanks in advance for your help.

FlorianSchunke
  • 571
  • 5
  • 15

2 Answers2

3

You can use Locate method of TChartValueList to get index of appropriate data entry.

Example from help:

tmp:=LineSeries1.XValues.Locate(EncodeDate(2007,1,1));
if tmp<>-1 then ...

Edit: This method works for exact coincidence.

If your X-values are sorted (default mode), then you can use binary search in XValues to find the closest value quickly.
For example, we can modify this code to return the closest value index instead of -1, or use linear interpolation (if applicable) for two neighbor values.

  //assumes A.Order = loAscending (default)
  function FindClosestIndex(const Value: Double; A: TChartValueList): Integer;
  var
    ahigh, j, alow: integer;
  begin
    // extra cases
    if A.Count = 0 then
      Exit(-1);
    if Value <= A.First then
      Exit(0);
    if Value >= A.Last then
      Exit(A.Count - 1);

    // binary search
    alow := 0;
    ahigh := A.Count - 1;
    while ahigh - alow > 1 do begin
      j := (ahigh + alow) div 2;
      if Value <= A[j] then
        ahigh := j
      else
        alow := j;
    end;

    // choose the closest from ahigh, alow
    Result := ahigh - Ord(A[ahigh] - Value >= Value - A[alow])
  end;
MBo
  • 77,366
  • 5
  • 53
  • 86
  • First of all thanks for your quick reply. Unfortunately the Locate methode gives only a valid index if the serie contains exactly the given date. In my case it is very unlikely to pass a date to the method which the serie exactly contains. The date is only nearby. – FlorianSchunke May 12 '16 at 07:53
  • I do not understand why this answer has got too few likes. It is brilliant! – Paul Jan 13 '17 at 13:17
0

A solution is an interpolation algorithm as shown at the All Features\Welcome!\Chart styles\Standard\Line(Strip)\Interpolating Lines example in the features demo. Here's the complete code for the example:

unit Line_Interpolate;
{$I TeeDefs.inc}

interface

uses
  {$IFNDEF LINUX}
  Windows, Messages,
  {$ENDIF}
  SysUtils, Classes,
  {$IFDEF CLX}
  QGraphics, QControls, QForms, QDialogs, QExtCtrls, QStdCtrls, QComCtrls,
  {$ELSE}
  Graphics, Controls, Forms, Dialogs, ExtCtrls, StdCtrls, ComCtrls,
  {$ENDIF}
  Base, TeEngine, Series, TeeProcs, Chart, TeeTools, TeeGDIPlus;

type
  TLineInterpolateForm = class(TBaseForm)
    Series1: TLineSeries;
    CheckBox1: TCheckBox;
    Series2: TLineSeries;
    Series3: TLineSeries;
    ChartTool1: TCursorTool;
    ChartTool2: TGridBandTool;
    procedure FormCreate(Sender: TObject);
    procedure Chart1AfterDraw(Sender: TObject);
    procedure ChartTool1Change(Sender: TCursorTool; x, y: Integer;
      const XValue, YValue: Double; Series: TChartSeries;
      ValueIndex: Integer);
  private
    { Private declarations }
    xval: Double;
    function InterpolateLineSeries(Series: TChartSeries;XValue: Double): Double; overload;
    function InterpolateLineSeries(Series: TChartSeries; FirstIndex,
                                  LastIndex: Integer; XValue: Double): Double; overload;
  public
    { Public declarations }
  end;

implementation

{$IFNDEF CLX}
{$R *.DFM}
{$ELSE}
{$R *.xfm}
{$ENDIF}

procedure TLineInterpolateForm.FormCreate(Sender: TObject);
var i: Integer;
begin
  inherited;

  for i:=0 to Chart1.SeriesCount-1 do
    Chart1[i].FillSampleValues;
end;

function TLineInterpolateForm.InterpolateLineSeries(Series: TChartSeries;
  XValue: Double): Double;
begin
  result:=InterpolateLineSeries(Series,Series.FirstDisplayedIndex,Series.LastValueIndex,XValue);
end;

function TLineInterpolateForm.InterpolateLineSeries(Series: TChartSeries;
  FirstIndex, LastIndex: Integer; XValue: Double): Double;
var
  Index: Integer;
  dx,dy: Double;
begin
  for Index:=FirstIndex to LastIndex do
    if Series.XValues.Value[Index]>XValue then break;

  //safeguard
  if (Index<1) then Index:=1
  else if (Index>=Series.Count) then Index:=Series.Count-1;

  // y=(y2-y1)/(x2-x1)*(x-x1)+y1
  dx:=Series.XValues.Value[Index] - Series.XValues.Value[Index-1];
  dy:=Series.YValues.Value[Index] - Series.YValues.Value[Index-1];

  if (dx<>0) then
    result:=dy*(XValue - Series.XValues.Value[Index-1])/dx + Series.YValues.Value[Index-1]
  else result:=0;
end;

procedure TLineInterpolateForm.Chart1AfterDraw(Sender: TObject);
var xs, ys, i: Integer;
begin
  if CheckBox1.Checked then
  begin
    xs := Chart1.Axes.Bottom.CalcXPosValue(xval);

    for i:=0 to Chart1.SeriesCount - 1 do
    begin
      ys := Chart1[i].GetVertAxis.CalcYPosValue(InterpolateLineSeries(Chart1[i],xval));
      Chart1.Canvas.Brush.Color := Chart1[i].Color;
      Chart1.Canvas.Ellipse(xs-4,ys-4,xs+4,ys+4);
    end;
  end;
end;

procedure TLineInterpolateForm.ChartTool1Change(Sender: TCursorTool; x,
  y: Integer; const XValue, YValue: Double; Series: TChartSeries;
  ValueIndex: Integer);
var
  i: Integer;
begin
  xval := XValue;

  With Chart1.Title.Text do
  begin
    Clear;
    for i:=0 to Chart1.SeriesCount - 1 do
        Add(Chart1[i].Name + ': Y('+FloatToStrF(XValue, ffNumber, 8, 2)+')= ' +
            FloatToStrF(InterpolateLineSeries(Chart1[i],XValue), ffNumber, 8, 2)+#13#10);
  end;
end;

initialization
  RegisterClass(TLineInterpolateForm);
end.
Narcís Calvet
  • 7,304
  • 5
  • 27
  • 47
  • This is exactly what I want to AVOID to do: "Or do I need to iterate through the series until I reached the selected date?". In your code you iterate through all points of the serie until you find a `XValue` which is greater than the given `x`: `for Index:=FirstIndex to LastIndex do if Series.XValues.Value[Index]>XValue then break;`. If you have about 100k data points it is to slowly. Therefore the binary search is the best solution. – FlorianSchunke May 20 '16 at 10:14
  • @FlorianSchunke sure. That was just an alternative for series with little points. – Narcís Calvet May 20 '16 at 21:03