2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【React Native】グラフデータの詳細を表示するツールチップを実装

Last updated at Posted at 2021-02-27

はじめに

React Nativeのグラフ表示で、データポイントをタッチしたときに詳細が表示されるようなツールチップを実装しました。基本的な実装はこちらを参考にしたのですが、データポイント以外の部分をタッチしたときにツールチップが非表示にならないという課題があったので、非表示になるように工夫しました。

完成品はこちらです。

使用ライブラリ

実装手順

実装の流れを簡単にまとめました。

プロジェクト作成

expo init <project name>でプロジェクトを作成します。今回はテンプレートにblank(TypeScript)を選択しました。
プロジェクトフォルダ直下にsrc/components/LineChart.tsxを作成します。

ライブラリ導入

プロジェクト作成後、yarn add moment react-native-chart-kit react-native-svgで必要なライブラリを導入します。

グラフ作成

ツールチップ実装の前にグラフを作成します。

LineChart.tsx

import React from 'react';
import { Dimensions, StyleSheet } from 'react-native';
import { LineChart } from 'react-native-chart-kit';

export const LineChartComponent = () => {
  const lineChartWidth: number = Dimensions.get('window').width - 40;
  const lineChartHeight: number = 250;

  const labels: string[] = ['3/1', '3/2', '3/3', '3/4', '3/5', '3/6', '3/7'];
  const dataSets: number[] = [1, 4, 0, 4, 2, 2, 0];

  const data = {
    labels,
    datasets: [
      {
        data: dataSets,
        color: () => `rgba(230, 22, 115, 1)`,
        strokeWidth: 1,
      },
    ],
  };

  const chartConfig = {
    backgroundGradientFrom: '#fff',
    backgroundGradientTo: '#fff',
    decimalPlaces: 1,
    strokeWidth: 0.5,
    fillShadowGradient: '#fff',
    color: () => `rgba(89, 87, 87, 1)`,
    propsForDots: {
      r: '3',
    },
  };

  return (
    <LineChart
      data={data}
      width={lineChartWidth}
      height={lineChartHeight}
      formatYLabel={(value) =>
        Number.isInteger(Number(value)) ? String(parseInt(value, 10)) : ''
      }
      chartConfig={chartConfig}
      withVerticalLines={false}
      style={styles.lineChartStyle}
      withDots
      segments={3}
      yAxisSuffix=""
      fromZero
    />
  );
};

const styles = StyleSheet.create({
  lineChartStyle: {
    borderWidth: 0.5,
    borderRadius: 10,
    borderColor: 'grey',
    marginHorizontal: 10,
    paddingVertical: 10,
  },
}

data={data}の部分でデータを読み込みます。
dataの中身については、labelsが横軸、datasetsが縦軸に対応しています。データポイントと線の色(color)、線の太さ(strokeWidth)などをdatasetsの中で設定することができます。

  const data = {
    labels,
    datasets: [
      {
        data: dataSets,
        color: () => `rgba(230, 22, 115, 1)`,
        strokeWidth: 1,
      },
    ],
  };

opacityの設定を行うことでcolorの透過率を変えることもできます。

color: (opacity = 1) => `rgba(230, 22, 115, ${opacity})`,

chartConfig={chartConfig}の部分でグラフの設定を行います。グラフの背景色(background~)やデータポイントのサイズ(propsForDots)を設定することができます。

  const chartConfig = {
    backgroundGradientFrom: '#fff',
    backgroundGradientTo: '#fff',
    decimalPlaces: 1,
    strokeWidth: 0.5,
    fillShadowGradient: '#fff',
    color: () => `rgba(89, 87, 87, 1)`,
    propsForDots: {
      r: '3',
    },
  };

その他にも縦横軸のカスタマイズやグラフの基準点を常に0にする設定などいろいろあります。

ツールチップ表示

ツールチップの表示をできるようにします。ただ、この時点では非表示にすることはできません。

LineChart.tsx

import React, { useState } from 'react';
import { Dimensions, StyleSheet, View } from 'react-native';
import { LineChart } from 'react-native-chart-kit';
import { Rect, Text as TextSVG, Svg } from 'react-native-svg';

export const LineChartComponent = () => {
  const lineChartWidth: number = Dimensions.get('window').width - 40;
  const lineChartHeight: number = 250;

  const labels: string[] = ['3/1', '3/2', '3/3', '3/4', '3/5', '3/6', '3/7'];
  const dataSets: number[] = [1, 4, 0, 4, 2, 2, 0];

  const [tooltipPos, setTooltipPos] = useState({
    x: 0,
    y: 0,
    visible: false,
    value: 0,
    date: '',
  });

  const data = {
    labels,
    datasets: [
      {
        data: dataSets,
        color: () => `rgba(230, 22, 115, 1)`,
        strokeWidth: 1,
      },
    ],
  };

  const chartConfig = {
    backgroundGradientFrom: '#fff',
    backgroundGradientTo: '#fff',
    decimalPlaces: 1,
    strokeWidth: 0.5,
    fillShadowGradient: '#fff',
    color: () => `rgba(89, 87, 87, 1)`,
    propsForDots: {
      r: '3',
    },
  };

  return (
    <LineChart
      data={data}
      width={lineChartWidth}
      height={lineChartHeight}
      formatYLabel={(value) =>
        Number.isInteger(Number(value)) ? String(parseInt(value, 10)) : ''
      }
      chartConfig={chartConfig}
      withVerticalLines={false}
      style={styles.lineChartStyle}
      withDots
      segments={3}
      yAxisSuffix=""
      fromZero
      decorator={() => {
        return tooltipPos.visible ? (
          <View>
            <Svg>
              <Rect
                x={tooltipPos.x - 45}
                y={tooltipPos.y - 15}
                rx={5}
                ry={5}
                width="50"
                height="40"
                fill="red"
                opacity={0.5}
              />
              <TextSVG
                x={tooltipPos.x - 20}
                y={tooltipPos.y}
                fill="#fff"
                fontSize="10"
                fontWeight="bold"
                textAnchor="middle"
              >
                {tooltipPos.date}
              </TextSVG>
              <TextSVG
                x={tooltipPos.x - 26}
                y={tooltipPos.y + 17}
                fill="#fff"
                fontSize="14"
                fontWeight="bold"
                textAnchor="middle"
              >
                {tooltipPos.value}&nbsp;</TextSVG>
            </Svg>
          </View>
        ) : null;
      }}
      onDataPointClick={(data) => {
        return setTooltipPos((prevState) => {
          return {
            ...prevState,
            x: data.x,
            y: data.y,
            value: data.value,
            date: labels[data.index],
            visible: true,
          };
        });
      }}
    />
  );
};

const styles = StyleSheet.create({
  lineChartStyle: {
    borderWidth: 0.5,
    borderRadius: 10,
    borderColor: 'grey',
    marginHorizontal: 10,
    paddingVertical: 10,
  },
});

ツールチップを表示するために肝となるのが、onDataPointClickdecoratorの2つのプロパティです。

onDataPointClick

このプロパティと使うと、タッチしたデータポイントの位置(x, y)や値(value)などの情報を取得することができます。
ここで取得した情報を表示内容や表示位置の設定に利用します。

Object {
  "dataset": Object {
    "color": [Function color],
    "data": Array [
      1,
      4,
      0,
      4,
      2,
      2,
      0,
    ],
    "strokeWidth": 1,
  },
  "getColor": [Function getColor],
  "index": 1,
  "value": 4,
  "x": 110.28571428571428,
  "y": 16,
}

onDataPointClickで取得した情報をdecoratorに渡すために、tooltipPosというstateを定義します。

  const [tooltipPos, setTooltipPos] = useState({
    x: 0,
    y: 0,
    visible: false,
    value: 0,
    date: '',
  });

decorator

こちらのプロパティでグラフ上に追加の図を表示することができます。
tooltipPosのx, yでタッチしたデータポイント付近に図が表示されるようにし、date, valueでデータ詳細を表示します。

      decorator={() => {
        return (
          <View>
            <Svg>
              <Rect
                x={tooltipPos.x - 45}
                y={tooltipPos.y - 15}
                rx={5}
                ry={5}
                width="50"
                height="40"
                fill="red"
                opacity={0.5}
              />
              <TextSVG
                x={tooltipPos.x - 20}
                y={tooltipPos.y}
                fill="#fff"
                fontSize="10"
                fontWeight="bold"
                textAnchor="middle"
              >
                {tooltipPos.date}
              </TextSVG>
              <TextSVG
                x={tooltipPos.x - 26}
                y={tooltipPos.y + 17}
                fill="#fff"
                fontSize="14"
                fontWeight="bold"
                textAnchor="middle"
              >
                {tooltipPos.value}&nbsp;
              </TextSVG>
            </Svg>
          </View>
        );
      }}

ツールチップ非表示

データポイント以外をタッチしたときにツールチップが非表示になるようにします。

LineChart.tsx

import React, { useEffect, useState } from 'react';
import { Dimensions, StyleSheet, View } from 'react-native';
import { LineChart } from 'react-native-chart-kit';
import { Rect, Text as TextSVG, Svg } from 'react-native-svg';

export const LineChartComponent = () => {
  const lineChartWidth: number = Dimensions.get('window').width - 40;
  const lineChartHeight: number = 250;

  const labels: string[] = ['3/1', '3/2', '3/3', '3/4', '3/5', '3/6', '3/7'];
  const dataSets: number[] = [1, 4, 0, 4, 2, 2, 0];

  const START_POINT = 0;
  const TOUCH_AREA = 10;

  const [tooltipPos, setTooltipPos] = useState({
    x: 0,
    y: 0,
    visible: false,
    value: 0,
    date: '',
  });
  const [touchLocation, setTouchLocation] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const isOutOfTouchArea =
      tooltipPos.x - TOUCH_AREA >= touchLocation.x ||
      touchLocation.x >= tooltipPos.x + TOUCH_AREA ||
      tooltipPos.y - TOUCH_AREA >= touchLocation.y ||
      touchLocation.y >= tooltipPos.y + TOUCH_AREA;

    if (tooltipPos.x === START_POINT) return;

    if (isOutOfTouchArea) {
      return setTooltipPos((prevState) => {
        return {
          ...prevState,
          visible: false,
        };
      });
    }
  }, [touchLocation]);

  const data = {
    labels,
    datasets: [
      {
        data: dataSets,
        color: () => `rgba(230, 22, 115, 1)`,
        strokeWidth: 1,
      },
    ],
  };

  const chartConfig = {
    backgroundGradientFrom: '#fff',
    backgroundGradientTo: '#fff',
    decimalPlaces: 1,
    strokeWidth: 0.5,
    fillShadowGradient: '#fff',
    color: () => `rgba(89, 87, 87, 1)`,
    propsForDots: {
      r: '3',
    },
  };

  return (
    <View
      onTouchEnd={(e) => {
        setTouchLocation({
          x: e.nativeEvent.locationX,
          y: e.nativeEvent.locationY,
        });
      }}
    >
      <LineChart
        data={data}
        width={lineChartWidth}
        height={lineChartHeight}
        formatYLabel={(value) =>
          Number.isInteger(Number(value)) ? String(parseInt(value, 10)) : ''
        }
        chartConfig={chartConfig}
        withVerticalLines={false}
        style={styles.lineChartStyle}
        withDots
        segments={3}
        yAxisSuffix=""
        fromZero
        decorator={() => {
          return tooltipPos.visible ? (
            <View>
              <Svg>
                <Rect
                  x={tooltipPos.x - 45}
                  y={tooltipPos.y - 15}
                  rx={5}
                  ry={5}
                  width="50"
                  height="40"
                  fill="red"
                  opacity={0.5}
                />
                <TextSVG
                  x={tooltipPos.x - 20}
                  y={tooltipPos.y}
                  fill="#fff"
                  fontSize="10"
                  fontWeight="bold"
                  textAnchor="middle"
                >
                  {tooltipPos.date}
                </TextSVG>
                <TextSVG
                  x={tooltipPos.x - 26}
                  y={tooltipPos.y + 17}
                  fill="#fff"
                  fontSize="14"
                  fontWeight="bold"
                  textAnchor="middle"
                >
                  {tooltipPos.value}&nbsp;</TextSVG>
              </Svg>
            </View>
          ) : null;
        }}
        onDataPointClick={(data) => {
          return setTooltipPos((prevState) => {
            return {
              ...prevState,
              x: data.x,
              y: data.y,
              value: data.value,
              date: labels[data.index],
              visible: true,
            };
          });
        }}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  lineChartStyle: {
    borderWidth: 0.5,
    borderRadius: 10,
    borderColor: 'grey',
    marginHorizontal: 10,
    paddingVertical: 10,
  },
});

グラフのタッチ位置を取得するために、touchLocationという新しいstateを定義し、LineChartコンポーネントをViewコンポーネントでラッピングします。

const [touchLocation, setTouchLocation] = useState({ x: 0, y: 0 });

~中略~
    <View
      onTouchEnd={(e) => {
        setTouchLocation({
          x: e.nativeEvent.locationX,
          y: e.nativeEvent.locationY,
        });
      }}
    >
      <LineChart
        data={data}
~後略~

useEffectで、touchLocationが変化したとき(タッチしたとき)に、タッチした位置がデータポイントから10以上離れていればツールチップを非表示にするという処理を追加します。

  useEffect(() => {
    const isOutOfTouchArea =
      tooltipPos.x - TOUCH_AREA >= touchLocation.x ||
      touchLocation.x >= tooltipPos.x + TOUCH_AREA ||
      tooltipPos.y - TOUCH_AREA >= touchLocation.y ||
      touchLocation.y >= tooltipPos.y + TOUCH_AREA;

    if (tooltipPos.x === START_POINT) return;

    if (isOutOfTouchArea) {
      return setTooltipPos((prevState) => {
        return {
          ...prevState,
          visible: false,
        };
      });
    }
  }, [touchLocation]);

一方、データポイントをタッチしたときにツールチップを表示する処理を追加します。

        onDataPointClick={(data) => {
          return setTooltipPos((prevState) => {
            return {
              ...prevState,
              x: data.x,
              y: data.y,
              value: data.value,
              date: labels[data.index],
              visible: true,
            };
          });
        }}

これで実装完了です。

おわりに

ツールチップの導入で、react-native-chart-kitはカスタマイズの自由度が高いことがわかりました。他にUX向上につながる機能があるかどうか、いろいろ触りながら確かめてみたいと思います。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?