Chart.jsのレーダーチャートのpointlabelにonClickイベントをつける

# さっくり知りたい人のための要約
- Chart.jsにはpointLabelへのonClickはデフォルトで用意されていない
- 自作する場合は、`<canvas></canvas>`にonClickイベントをつけて座標計算する必要がある
    - マウスのクリックイベントから座標を取得して、canvas内のpointLabelの座標内か判断する必要がある
    - pointLabelの座標も自力で計算する必要がある
- [実行結果](#完成図) | [ソース]((https://github.com/NakashimaNozomi/chartjs-label-on-click)) | [私なりの解説](#3-クリックされたらクリックされた箇所がpointlabelかを座標計算して判定)

# はじめに
- __pointLabel__ってどこのこと？
→レーダーチャートのチャート外に表示されてる文字の部分（ここ:point_down_tone5:のこと）
![スクリーンショット 2020-07-11 13.15.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84532/946eae80-0042-5b87-acf3-956f92e483dd.png)

- Chart.js用意されてないの？
→ない...はず....(v2.9.3現在)
後で詳細に説明するけれど、canvasのどこをクリックして、それがラベルのある座標なのかを計算してむりくり実装するしかなさそう。
[有志が計算部分省略するためにchartObjectにpointLabelの座標を入れてくれ〜という提案](https://github.com/chartjs/Chart.js/issues/6549)をしているのでこのissue盛り上げて採用されよう:muscle_tone2::muscle_tone2:

- あなたが考案したソース？
→いいえ、[こちらのStackOverflow](https://stackoverflow.com/a/58296237)の回答者[simoncoggins](https://stackoverflow.com/users/73522/simoncoggins)から引用したものであり、彼の実装を元にコメントを日本語訳したくらいのものを紹介します。
前述のissue書いてる人も同一人物です。感謝...:pray::pray::pray:

# いざ実装:point_up_tone3:
今回の記事では v2.9.3 の Chart.js を利用します。(この記事を下書きに放置している間に2.9.4が出てるっぽい）
また、jQueryとかもなしのピュアなjavascriptで記載します。 jQueryとか他使っている人は適宜読み替えてくださいませ。

## 完成図
(実は[Github](https://github.com/NakashimaNozomi/chartjs-label-on-click)も用意してあるけれどまあCodePenあればいらないカモ)
<p class="codepen" data-height="416" data-theme-id="light" data-default-tab="js,result" data-user="_nakashimamura" data-slug-hash="MWKXQLN" style="height: 416px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="MWKXQLN">
  <span>See the Pen <a href="https://codepen.io/_nakashimamura/pen/MWKXQLN">
  MWKXQLN</a> by 中村＠ガウチャ (<a href="https://codepen.io/_nakashimamura">@_nakashimamura</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

## 実装手順
1. レーダーチャートを描画
2. クリックイベントをレーダーチャートを描画するcanvasに設定
3. クリックされたらクリックされた箇所がpointLabelかを座標計算して判定
4. ラベルだったときは必要な処理をする！ 終わり！:hugging:

＊今回はcallbackは特別なことせずに、ただ`alert`でイベントから受け取った情報を出すようにします


## いざいざ実装
本題は 3. からなのでそこだけ読みたい人は

### 1. レーダーチャートを描画
HTML側はChart.jsとjsの読み込み、それとcanvasの設定。`#chartCanbas`にレーダーチャートを表示させる。
(Chart.jsをnpmでインストールしてjs側でimportしても良いけど今回はちゃちゃっとCDNから読み込んじゃう）

```html:index.html
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.js"></script>
<script src="./local.js"></script>

<!-- レーダーチャートを表示させるcanvas -->
<canvas id="chartCanvas"></canvas>
```

```javascript:local.js
window.onload = () => {
  const chartArea = document.getElementById("chartCanvas"); // canvasのDom
  const radar = new Chart(new Chart(target.getContext("2d"), {type: "radar" .../* 省略 */...});
};
```

レーダーチャートの表示部分は今回の記事では__紹介しません__。
[公式ドキュメント](https://www.chartjs.org/docs/latest/charts/radar.html)とか、日本語翻訳してくれてる有志のサイトを参考に作ると良いかと！


### 2. クリックイベントをレーダーチャートを描画するcanvasに設定

特別なこともなく、いつものようにcanvasにonclickイベントを登録するだけ。
__忘れずeventオブジェクトを引数で受け取ってください。__

```javascript
window.onload = () => {
  const chartArea = document.getElementById("chartCanvas");
  const radar = new Chart(new Chart(target.getContext("2d"), {type: "radar" .../* 省略 */...});

  // 以下canvasにクリックイベントを登録
  chartArea.onclick = (e) => {
    //
  }
};
```

### 3. クリックされたらクリックされた箇所がpointLabelかを座標計算して判定
さて、ここからが本記事の本題！:upside_down:
コードは全て`chartArea.onclick = (e) => {`内にかかれるものとして読んでください。

座標計算の流れとしては今回は
1. クリック位置を取得
2. ラベルの数だけループを回し、
3. ラベルがどこに描画されているかを計算 (これが大変)
4. クリック位置がラベルの範囲か計算・判定
5. ラベルをクリックしたのなら目的の処理を行う
6. ループを抜ける
となっています。

__1. クリック位置を取得__

```javascript
// canvas内のクリックしたx,y座標
const mouseX = e.offsetX;
const mouseY = e.offsetY;
```

__2. ラベルがどこに描画されているかを計算__
`new Chart()`した際の戻り値であるchartObjectにラベルの座標はなので、
marginやpadding、fontSizeの設定値やラベルの文字数から計算する他ないです。
しかもラベルは１つじゃないのでループを回して全てを計算する必要があるという分けです。

:star: 前準備
まず`目盛り文字の高さ`と`一番外側の目盛り文字のレーダーチャート中心からの距離`が必要ですので取得しておきます。
ラベルはこの目盛りより外に表示されますからね。
![スクリーンショット 2021-02-28 16.15.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84532/f784d187-49fd-5b22-55e3-7b7169a677d8.png)

```javascript
const helpers = Chart.helpers; // 
const scale = radar.scale; // 軸
const opts = scale.options; // チャート全体の設定
const tickOpts = opts.ticks; // 目盛り

// 目盛りの高さ
// 基本はfontsize + 5px
const tickBackdropHeight =
  tickOpts.display && opts.display // chartの設定で optionで目盛りが表示されているか
    ? helpers.valueOrDefault(
        tickOpts.fontSize,
        Chart.defaults.global.defaultFontSize
      ) + 5
    : 0;

// 目盛りの中央からの距離
const outerDistance = scale.getDistanceFromCenterForValue(
  opts.ticks.reverse ? scale.min : scale.max
);
```
`Chart.helpers`についてはバージョン違いだが、[こちら](https://www.chartjs.org/docs/3.0.0-alpha/typedoc/modules/_helpers_helpers_core_.html)を参照。
scaleが持っているmethodは同じくバージョン違いだが[こちら](https://www.chartjs.org/docs/next/typedoc/classes/scales.radiallinearscale.html#getdistancefromcenterforvalue)が参考になるっぽい。

:star: ラベルの位置を計算
ではループを回してラベル類の位置を特定していきましょう。

```javascript
// ポイントラベル分ループ
for (var i = 0; i < scale.pointLabels.length; i++) {
  // 軸ラベルによってチャート上部に余分な空白があるので削除
  const extra = i === 0 ? tickBackdropHeight / 2 : 0;
  const pointLabelPosition = scale.getPointPosition(
    i,
    outerDistance + extra + 5
  );

  // ラベルサイズ情報を取得
  const plSize = scale._pointLabelSizes[i];

  // ラベルのtextAlignを取得する(ラベルの描画位置が変わるので)
  const angleRadians = scale.getIndexAngle(i);
  const angle = helpers.toDegrees(angleRadians);
  let textAlign = "right";
  if (angle == 0 || angle == 180) {
    textAlign = "center";
  } else if (angle < 180) {
    textAlign = "left";
  }

  // ラベルの垂直オフセット位置を取得
  // drawPointLabels()から取得し計算
  let verticalTextOffset = 0;
  if (angle === 90 || angle === 270) {
    verticalTextOffset = plSize.h / 2;
  } else if (angle > 270 || angle < 90) {
    verticalTextOffset = plSize.h;
  }

  // 対象のラベルの範囲の位置を計算(padding含み)
  const labelTop = pointLabelPosition.y - verticalTextOffset - labelPadding;
  const labelHeight = 2 * labelPadding + plSize.h;
  const labelBottom = labelTop + labelHeight;

  const labelWidth = plSize.w + 2 * labelPadding;
  let labelLeft;
  switch (textAlign) {
    case "center":
      labelLeft = pointLabelPosition.x - labelWidth / 2;
      break;
    case "left":
      labelLeft = pointLabelPosition.x - labelPadding;
      break;
    case "right":
      labelLeft = pointLabelPosition.x - labelWidth + labelPadding;
      break;
    default:
      console.warn("WARNING: unknown textAlign " + textAlign);
  }
  let labelRight = labelLeft + labelWidth;

  // ...以降クリックされたか判定などなど...

} // end of for
```
まんま貼り付けただけですが、textAlignを自力で取得したりするところがみそですね。
CSS的なのtextAlignではなく、レーダーチャートのの外周、左右上下のどこに描画されているかを計算しています。


__5. ラベルをクリックしたのなら目的の処理を行う__
__6. ループを抜ける__
for文の最後に以下で判定して完成でっす！ :tada: 

```javascript
  // クリックされた範囲がラベルか判定
  if (
    mouseX >= labelLeft &&
    mouseX <= labelRight &&
    mouseY <= labelBottom &&
    mouseY >= labelTop
  ) {
    // したい処理をここに記載！！
    // なにか値が必要な場合もここまでで取得できていそうなのでなんでもできるかと！
    break; //ラベルクリックがわかったので処理を終了
  }
```


以上で目的は達成かと思います。


# 最後に
本当に以下の回答に Thanks! :bow: 
https://stackoverflow.com/a/58296237