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.
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 you X-values are sorted (default mode), the you can use binary search in XValues to find the closest value quickly.
For example, you might 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;
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.