C++ Builder XE4 > TeeChart > クリックした点に近い系列データの明示 > fix bug: Clicked()で位置ずれする探索の補正 + 備考

```txt:動作環境
C++ Builder XE4
TeeChart Lite v2013.08.130414 32bit VCL
```

### 関連

- [C++ Builder XE4 > TeeChart > クリックした点に近い系列データの明示 > 点と値の表示 > 注意点: 2つの系列で異なるインデックスを取得する](https://qiita.com/7of9/items/c085ab5bbe501f2c005a)

Clicked()で探索したインデックスは位置ずれすることがある。その補正を検討した。

### 検討内容

- Clicked()で1つずれる
- 1つずれた位置から前後のインデックスを含めて最探索する
- [位置ずれの補正前]と[補正後]を見ることができるように、Series5を[補正後]として追加
- Series1に対してSeries2のマーカーを表示する
- Series3に対して以下の2つのマーカーを表示する
    - Series4: 補正前
    - Series5: 補正後

### フォームデザイン

![2018-12-11_16h18_55.png](https://qiita-image-store.s3.amazonaws.com/0/32870/2492a3aa-b5da-447a-02ee-a75538cd8953.png)


### 実装 v0.2

```Unit1.h
//---------------------------------------------------------------------------

#ifndef Unit1H
#define Unit1H
//---------------------------------------------------------------------------
#include <System.Classes.hpp>
#include <Vcl.Controls.hpp>
#include <Vcl.StdCtrls.hpp>
#include <Vcl.Forms.hpp>
#include <Vcl.ExtCtrls.hpp>
#include <VCLTee.Chart.hpp>
#include <VCLTee.Series.hpp>
#include <VCLTee.TeEngine.hpp>
#include <VCLTee.TeeProcs.hpp>
//---------------------------------------------------------------------------
class TForm1 : public TForm
{
__published:	// IDE で管理されるコンポーネント
	TChart *Chart1;
	TFastLineSeries *Series1;
	TEdit *E_xvalue;
	TEdit *E_yvalue;
	TPointSeries *Series2;
	TFastLineSeries *Series3;
	TPointSeries *Series4;
	TPointSeries *Series5;
	void __fastcall Chart1MouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift,
          int X, int Y);
private:	// ユーザー宣言
	int __fastcall TForm1::addMarker_nearestIndex(TDateTime referenceDt, int oneOffIndex, TFastLineSeries *targetPtr, TPointSeries *markerPtr);
public:		// ユーザー宣言
	__fastcall TForm1(TComponent* Owner);
};
//---------------------------------------------------------------------------
extern PACKAGE TForm1 *Form1;
//---------------------------------------------------------------------------
#endif
```

```Unit1.cpp
//---------------------------------------------------------------------------

#include <vcl.h>
#pragma hdrstop

#include <DateUtils.hpp>
//#include <float.h>
#include "Unit1.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------

/*
v0.2 2018/12/11
	- fix bug: Clicked()により位置ずれが起きる
		+ addMarker_nearestIndex()追加
*/


__fastcall TForm1::TForm1(TComponent* Owner)
	: TForm(Owner)
{
    // テスト用の系列データ追加

    Chart1->Series[0]->XValues->DateTime = true;
    Chart1->BottomAxis->DateTimeFormat = L"nn:ss";

    TDateTime dt;

    double yval;
    // 一つ目のシリーズ
    dt = Now();
    for (int idx=0; idx < 10; idx++) {
        yval = (1+ idx) % 3;
        Series1->AddXY(dt, yval, "", clRed);
		dt = IncSecond(dt, 1);
    }

    // 二つ目のシリーズ
    dt = Now();
    for (int idx=0; idx < 10; idx++) {
        yval = (1+ idx + 1) % 3;
		Series3->AddXY(dt, yval, "", clRed);
        dt = IncSecond(dt, 1);
	}
}
//---------------------------------------------------------------------------

#define POINT_NOT_FOUND (-1)

void __fastcall TForm1::Chart1MouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift,
          int X, int Y)
{
	TFastLineSeries *targetPtrs[] = { Series1, Series3 };
    int numSer = sizeof(targetPtrs) / sizeof(targetPtrs[0]);
	TPointSeries *markerPtrs[] = { Series2, Series4 };

    // グラフ上端から下端まで走査してインデックスを見つける
    int top = Chart1->ChartRect.Top;
    int btm = Chart1->ChartRect.Bottom;
    int index = POINT_NOT_FOUND;  // 系列のインデックスが入る

    for(int tgtIdx = 0; tgtIdx < numSer; tgtIdx++) {  // tgtIdx: target index
        for(int ypos = top; ypos <= btm; ypos++) {
            index = targetPtrs[tgtIdx]->Clicked(X, ypos);
            if (index != POINT_NOT_FOUND) {
                break;
            }
        }

        // A. クリックが外れていたときに消す
        if (index == POINT_NOT_FOUND) {
            if (markerPtrs[tgtIdx]->Count() > 0) {
                markerPtrs[tgtIdx]->Delete(0);
                E_xvalue->Text = L"";
                E_yvalue->Text = L"";
            }
            continue;
        }

        // B. クリックが線上の時に表示

        // 既存のものは消す
        if (markerPtrs[tgtIdx]->Count() > 0) {
            markerPtrs[tgtIdx]->Delete(0);
        }

        // 点の追加
		double xval = targetPtrs[tgtIdx]->XValue[index];
        double yval = targetPtrs[tgtIdx]->YValue[index];
		markerPtrs[tgtIdx]->AddXY(xval, yval);

#if 1 // 2018/12/11
		// Clicked()でずれるため、近傍を見つけなおす. 対象として[Series5]をマーカーとして使う
		if (tgtIdx == 0) {
			int correctedIdx = addMarker_nearestIndex(/*referenceDt=*/xval, /*oneOffIndex=*/index,
				/*targetPtr=*/Series3, /*markerPtr=*/Series5);
			int nop = 1; // nop(No Operation) // for breakpoint
		}
#endif

        E_xvalue->Text = String().sprintf(L"%.3f", xval);
        E_yvalue->Text = String().sprintf(L"%.3f", yval);
	}
}
//---------------------------------------------------------------------------

static const int kIndex_notFound = -1; // 探索が失敗した時の値

int __fastcall TForm1::addMarker_nearestIndex(TDateTime referenceDt, int oneOffIndex, TFastLineSeries *targetPtr, TPointSeries *markerPtr)
{
	// [探索済みインデックス±1]のインデックスにおいてDateTimeが一番近いインデックスを調べなおす
	// Clicked()でインデックスがずれるための対処
	//
	// 戻り値: 探索した[targetPtr]のインデックス

	// 1. 引数エラー処理
	if (targetPtr == NULL ||
		markerPtr == NULL) {
		return kIndex_notFound;
	}

	// 2. 近傍の探索しなおし
	__int64 diff_msec = 1e9; // 1e9: 任意の値 | 最小値を見つけるための初期値
	int minPos = kIndex_notFound; // 探索後のインデックス値(>=0)を格納する

	//   2-1 既存の点は消す
	if (markerPtr->Count() > 0) {
		markerPtr->Delete(0);
	}

	//   2-2 近傍を探しなおし
	for(int idx=-1; idx<=1 ; idx++) {  // 近傍の日時をチェック
		double xval = targetPtr->XValue[oneOffIndex + idx];
		__int64 wrk = MilliSecondsBetween(xval, referenceDt);  // 絶対値で返る
		if (diff_msec > wrk) {
			diff_msec = wrk;
			minPos = (oneOffIndex + idx);
		}
	}

	// 3. マーカーを加える
	if (minPos != kIndex_notFound) {
		double xval = targetPtr->XValue[minPos];
		double yval = targetPtr->YValue[minPos];
		markerPtr->AddXY(xval, yval);

		return minPos;
	}

	return kIndex_notFound;

}
```


### 動作例

![2018-12-11_15h59_08.png](https://qiita-image-store.s3.amazonaws.com/0/32870/e7d40233-113a-1247-2aca-2a3d19469557.png)

- 赤色: 位置ずれした結果 (Clicked()の結果そのまま)
- 黄色: 位置補正した結果 (今回の実装で補正)

### 関連

- [System.DateUtils.MilliSecondsBetween](http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.DateUtils.MilliSecondsBetween)


### 実装 v0.3 > 関数の役割の変更

```
v0.3 2018/12/11 関数の整理
	- refactor > addMarker_nearestIndex()をfindFastLineSeriesIndex_oneOffIndexOf()に変更
		+ マーカー追加処理はChart1MouseDown()へ変更
		+ インデックスを返すように変更
		+ マーカー追加処理の除去
```

```Unit1.h
//---------------------------------------------------------------------------

#ifndef Unit1H
#define Unit1H
//---------------------------------------------------------------------------
#include <System.Classes.hpp>
#include <Vcl.Controls.hpp>
#include <Vcl.StdCtrls.hpp>
#include <Vcl.Forms.hpp>
#include <Vcl.ExtCtrls.hpp>
#include <VCLTee.Chart.hpp>
#include <VCLTee.Series.hpp>
#include <VCLTee.TeEngine.hpp>
#include <VCLTee.TeeProcs.hpp>
//---------------------------------------------------------------------------
class TForm1 : public TForm
{
__published:	// IDE で管理されるコンポーネント
	TChart *Chart1;
	TFastLineSeries *Series1;
	TEdit *E_xvalue;
	TEdit *E_yvalue;
	TPointSeries *Series2;
	TFastLineSeries *Series3;
	TPointSeries *Series4;
	TPointSeries *Series5;
	void __fastcall Chart1MouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift,
          int X, int Y);
private:	// ユーザー宣言
	int __fastcall TForm1::findFastLineSeriesIndex_oneOffIndexOf(TDateTime referenceDt, int oneOffIndex, TFastLineSeries *targetPtr);
public:		// ユーザー宣言
	__fastcall TForm1(TComponent* Owner);
};
//---------------------------------------------------------------------------
extern PACKAGE TForm1 *Form1;
//---------------------------------------------------------------------------
#endif
```

```Unit1.cpp
//---------------------------------------------------------------------------

#include <vcl.h>
#pragma hdrstop

#include <DateUtils.hpp>
//#include <float.h>
#include "Unit1.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------

/*
v0.3 2018/12/11 関数の整理
	- refactor > addMarker_nearestIndex()をfindFastLineSeriesIndex_oneOffIndexOf()に変更
		+ マーカー追加処理はChart1MouseDown()へ変更
		+ インデックスを返すように変更
		+ マーカー追加処理の除去
v0.2 2018/12/11
	- fix bug: Clicked()により位置ずれが起きる
		+ addMarker_nearestIndex()追加
		+ [kIndex_notFound]追加
*/

static const int kIndex_notFound = -1; // 探索が失敗した時の値


__fastcall TForm1::TForm1(TComponent* Owner)
	: TForm(Owner)
{
    // テスト用の系列データ追加

    Chart1->Series[0]->XValues->DateTime = true;
    Chart1->BottomAxis->DateTimeFormat = L"nn:ss";

    TDateTime dt;

    double yval;
    // 一つ目のシリーズ
    dt = Now();
    for (int idx=0; idx < 10; idx++) {
        yval = (1+ idx) % 3;
        Series1->AddXY(dt, yval, "", clRed);
		dt = IncSecond(dt, 1);
    }

    // 二つ目のシリーズ
    dt = Now();
    for (int idx=0; idx < 10; idx++) {
        yval = (1+ idx + 1) % 3;
		Series3->AddXY(dt, yval, "", clRed);
        dt = IncSecond(dt, 1);
	}
}
//---------------------------------------------------------------------------

#define POINT_NOT_FOUND (-1)

void __fastcall TForm1::Chart1MouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift,
          int X, int Y)
{
	TFastLineSeries *targetPtrs[] = { Series1, Series3 };
    int numSer = sizeof(targetPtrs) / sizeof(targetPtrs[0]);
	TPointSeries *markerPtrs[] = { Series2, Series4 };

    // グラフ上端から下端まで走査してインデックスを見つける
    int top = Chart1->ChartRect.Top;
    int btm = Chart1->ChartRect.Bottom;
    int index = POINT_NOT_FOUND;  // 系列のインデックスが入る

    for(int tgtIdx = 0; tgtIdx < numSer; tgtIdx++) {  // tgtIdx: target index
        for(int ypos = top; ypos <= btm; ypos++) {
            index = targetPtrs[tgtIdx]->Clicked(X, ypos);
            if (index != POINT_NOT_FOUND) {
                break;
            }
        }

        // A. クリックが外れていたときに消す
        if (index == POINT_NOT_FOUND) {
            if (markerPtrs[tgtIdx]->Count() > 0) {
                markerPtrs[tgtIdx]->Delete(0);
                E_xvalue->Text = L"";
                E_yvalue->Text = L"";
            }
            continue;
        }

        // B. クリックが線上の時に表示

        // 既存のものは消す
        if (markerPtrs[tgtIdx]->Count() > 0) {
            markerPtrs[tgtIdx]->Delete(0);
        }

        // 点の追加
		double xval = targetPtrs[tgtIdx]->XValue[index];
        double yval = targetPtrs[tgtIdx]->YValue[index];
		markerPtrs[tgtIdx]->AddXY(xval, yval);

#if 1 // 2018/12/11
		// Clicked()でずれるため、近傍を見つけなおす. 対象として[Series5]をマーカーとして使う
		if (tgtIdx == 0) {
			int correctedIdx = findFastLineSeriesIndex_oneOffIndexOf(/*referenceDt=*/xval, /*oneOffIndex=*/index,
				/*targetPtr=*/Series3);
			int nop = 1; // nop(No Operation) // for breakpoint
			// マーカーの追加
			if (correctedIdx != kIndex_notFound) {
				if (Series5->Count() > 0) {
                    Series5->Delete(0);
				}
				double xval = Series3->XValue[correctedIdx];
				double yval = Series3->YValue[correctedIdx];
				Series5->AddXY(xval, yval);
			}
		}
#endif

        E_xvalue->Text = String().sprintf(L"%.3f", xval);
        E_yvalue->Text = String().sprintf(L"%.3f", yval);
	}
}
//---------------------------------------------------------------------------

int __fastcall TForm1::findFastLineSeriesIndex_oneOffIndexOf(TDateTime referenceDt, int oneOffIndex, TFastLineSeries *targetPtr)
{
	// [探索済みインデックス±1]のインデックスにおいてDateTimeが一番近いインデックスを調べなおす
	// Clicked()でインデックスがずれるための対処
	//
	// 戻り値: 探索した[targetPtr]のインデックス

	// 1. 引数エラー処理
	if (targetPtr == NULL) {
		return kIndex_notFound;
	}

	// 2. 近傍の探索しなおし
	__int64 diff_msec = 1e9; // 1e9: 任意の値 | 最小値を見つけるための初期値
	int minPos = kIndex_notFound; // 探索後のインデックス値(>=0)を格納する
	//
	for(int idx=-1; idx<=1 ; idx++) {  // 近傍の日時をチェック
		double xval = targetPtr->XValue[oneOffIndex + idx];
		__int64 wrk = MilliSecondsBetween(xval, referenceDt);  // 絶対値で返る
		if (diff_msec > wrk) {
			diff_msec = wrk;
			minPos = (oneOffIndex + idx);
		}
	}
	//
	if (minPos != kIndex_notFound) {
		return minPos;
	}
	//
	return kIndex_notFound;
}
```


### 備考

実際に使ってみたところ[-1,1]の探索では不十分であった。
データ数が多く、１ピクセルに多数のデータが乗っている状況では、[-30,30]などの探索範囲拡大が必要となる。

実際のデータを参考に検討することになる。