【flutter_radar_chart】RadarChartをタップしてFeatureのindexを取得する


やりたいこと

flutter_radar_chart
RadarChartをタップした位置から角度を計算して、どのFeature付近をタップしたか検知したい。

できたもの

やったこと

大枠の流れはこんな感じです
① RadarChartの中心とタップした位置から角度を求める関数を作成する
② 360をFeatureの数で割って1辺あたりの角度を求める
③ ②で求めた角度からFetureの範囲を求める
④ ①で求めた角度が③の範囲のどこに入るかを取得する

① RadarChartの中心とタップした位置から角度を求める関数を作成する

int angle(Offset a, Offset b) {
    double r = atan2(b.dy - a.dy, b.dx - a.dx);
    if (r < 0) {
      r = r + 2 * pi;
    }

    return (r * 360 / (2 * pi)).floor();
  }

// 使い方
angle(RadarChartの中心, タップした位置)

② 360をFeatureの数で割って1辺あたりの角度を求める

Featureが5つの場合は 360/5 = 72 となります。

③ ②で求めた角度からFetureの範囲を求める

まず前提として①で取得できる角度の基準は図の0°の部分で、そこから時計回りに360°になります。
基準をFeature0を基準とした場合に 図の0°は90°の位置にあたります。
今回1辺あたりの角度は72°なので90°-72°で72°毎に分割した範囲から-18°ずらします。

斜線の範囲をタップすると0を取得したい場合の計算を例にします。
タップする範囲は基準から+-36°の範囲であるためまず32°を計算します。
1辺あたりの角度/2
次に始点からどれだけ72°を足したかを計算します。
1辺あたりの角度 * (Featureの数 - 2)
そしてそこから調整のために18°引きます 
−18

min
(72°/2 + 72° * (5 - 2)) - 18°

max
min + 72° -1

min以上かつmax未満
234° <= angle < 305° の場合に0を取得できます。

2,3,4を取得する範囲の計算方法は
1辺あたりの角度 * (Featureの数 - 2)

1辺あたりの角度 * (取得するindex - 2)
に変えるだけで期待する範囲を求めることができます。
例えば2であれば
min
(72°/2 + 0) - 18°

max
min + 72° -1

min以上かつmax未満
18° <= angle < 89° の場合に2を取得できます。

1を取得する場合のみ360°をまたぐの条件が変わって
min以上またはmax未満となります。

今回この範囲をいったんMapで保持しています。

class AngleRange {
  AngleRange({required this.min, required this.max});

  final double min;
  final double max;
}
  final anglePerSide = 360 / numberOfFeatures;
  final offset = 90 - anglePerSide;

  final indexies = List.generate(numberOfFeatures, (index) => index);

  Map<int, AngleRange> intToAngleRangeMap =
      indexies.fold({}, (result, index) {
    switch (index) {
      case 0:
        final min =
            (anglePerSide / 2 + anglePerSide * (numberOfFeatures - 2)) -
                offset;
        result[index] = AngleRange(min: min, max: min + anglePerSide - 1);
        break;
      case 1:
        break;
      default:
        final min = (anglePerSide / 2 + anglePerSide * (index - 2)) - offset;
        result[index] = AngleRange(min: min, max: min + anglePerSide - 1);
        break;
    }

    return result;
  });

④ ①で求めた角度が③の範囲のどこに入るかを取得する

min以上かつmax未満であればkeyを返し、それ以外の範囲は1を返すようにしています。

 final result = intToAngleRangeMap.entries.where((e) {
      return e.value.min <= angle && angle <= e.value.max;
    });

    return result.isEmpty ? 1 : result.first.key;

使い方

SizedBox(
    height: 300,
    width: 300,
    child: GestureDetector(
        child: RadarChart(
          ticks: ticks,
          features: features,
          data: data,
          reverseAxis: false,
          sides: features.length,
          graphColors: const [Colors.red],
          featuresTextStyle:
              const TextStyle(color: Colors.black, fontSize: 14),
        ),
        onTapDown: (detail) {
          final _angle =
              angle(const Offset(150, 150), detail.localPosition);
          setState(() {
            index = getFeatureIndex(_angle, features.length);
          });
        })),

参考

あとがき

今回5角形を例にしていますが何角形でも期待通りの動作でした。
もう少しロジック整理できそうなので、もしこうすればもっと簡単にできる方法あればコメントお願いします!