はじめに
React Nativeのグラフ表示で、データポイントをタッチしたときに詳細が表示されるようなツールチップを実装しました。基本的な実装はこちらを参考にしたのですが、データポイント以外の部分をタッチしたときにツールチップが非表示にならないという課題があったので、非表示になるように工夫しました。
使用ライブラリ
実装手順
実装の流れを簡単にまとめました。
プロジェクト作成
expo init <project name>
でプロジェクトを作成します。今回はテンプレートにblank(TypeScript)
を選択しました。
プロジェクトフォルダ直下にsrc/components/LineChart.tsx
を作成します。
ライブラリ導入
プロジェクト作成後、yarn add moment react-native-chart-kit react-native-svg
で必要なライブラリを導入します。
グラフ作成
ツールチップ実装の前にグラフを作成します。
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にする設定などいろいろあります。
ツールチップ表示
ツールチップの表示をできるようにします。ただ、この時点では非表示にすることはできません。
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} 件
</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,
},
});
ツールチップを表示するために肝となるのが、onDataPointClick
とdecorator
の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} 件
</TextSVG>
</Svg>
</View>
);
}}
ツールチップ非表示
データポイント以外をタッチしたときにツールチップが非表示になるようにします。
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} 件
</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向上につながる機能があるかどうか、いろいろ触りながら確かめてみたいと思います。