Sweet Brissie life

ブリスベンでのサトウキビ博士研究生活の甘くない備忘録

React Native + Expo 記録アプリ④ react-native-chart-kit

React Native + Expo 記録アプリ開発の記事、どんどんいきます。

本記事は記録アプリの中心的機能の一つ、react-native-chart-kitによるグラフ化です。

 

 

こちら、このシリーズ記事の第一弾で既に触れているのですが、あんまり深められず...むしろ限界を感じるばかりでした。

ただ使いこなせてないだけかもしれないので、詳しい方のアドバイスを心待ちにしております。

tkd708.hatenablog.com

 

本記事のコードのjsファイルなどはこちら。

Life-Report/DailyLineChart.js at master · tkd708/Life-Report · GitHub

 

 

 

 

 

 

データの用意

react-native-chart-kitの方ではデータそのものをほとんど処理できないので、事前の整形がすべてになります。とはいっても基本的な使い方は前回記事のままですし、ちょっと補足くらいです。

           <LineChart
                data={{
                    labels: chartLabels,
                    datasets: [{
                        data: chartDatasets,
                        remarks: chartRemarks,
                        strokeWidth: 2,
                    }],
                }}

 LineChart直下のdataのオブジェクト内に全部入れます。x軸に表示させたい配列はlabelsに、y軸に表示させたい配列はdatasets.dataのにそれぞれセットします。

 

今回はこれらに加えて、remarksというのを自分で足しました。もともとのLineChartの機能としては表示されませんが、以下のtooltipの部分で使うための配列です。

 

配列を作る際の注意としては、x軸で実際に表示する順番でデータを入れていくこと。数値なら小から大になっているべきで、自分のように日付なら時系列に並んでいないといけません。labelsとdatasets.dataの配列はindexで結びつけられてx軸上に等間隔で並べられます(記事後半でもぼやいてますけど、これどうにかならないんでしょうか)。

 

 

折れ線グラフにtooltip(ツールチップ)を追加する

データを記録する際に、例えば出費で、カテゴリは外食で、金額はいくらで、という必須情報に加えて、例えばどこで何を食べた、みたいなメモ書き(Remark)も足せるようにしていました。カテゴリ毎にチャートで表示する際には、このメモ書きがデータポイントごとにタップで表示できたらなぁ、ということでこのtooltipを導入することにしました。

 

下記、素晴らしい記事でした。自分のは単にこれを組み込んだだけです。

swallow-incubate.com

 

今回の例では、useStateで保持しておく状態はVisibleかどうかのBoolean型変数と、座標とremarkの文字列をkeyに持つオブジェクト です。useEffectでVisibleがFalseになるように設定しているのは、グラフ化されているカテゴリが変更された時に、元のカテゴリでのグラフで表示されていたメモ書きが残らないようにするためです。

    const [toolTipVisiblesetToolTipVisible] = useState(false)
    const [toolTipPointsetToolTipPoint] = useState({ x: ""y: ""value: "" })

    useEffect( () => {
        setToolTipVisible(false);
    }, [categorySelected])
 

 

TooltipそのものはChartのタグの外に書いてあります。このTooltipタグが後ほどChartのタグに組み込まれるので、その際にpropsに相当する部分は受け渡されることになります。

const Tooltip = (props=> {
        if (props.visible) {
            return (
                <View style={{
                    marginVertical: 'auto',
                    marginHorizontal: 'auto',
                    backgroundColor: 'rgba(35, 24, 21, 1)',
                    padding: 5,
                    width: Math.max(...props.point.value.map(e => e.length)) == 0
                    ? 0
                    : Math.max(508 * Math.max(...props.point.value.map(e => e.length))),
                    //height: 25,
                    top: props.point.y - 25,
                    left: props.point.x - 8 * Math.max(...props.point.value.map(e => e.length)) / 2,
                }}
                >
                    {props.point.value.filter(e => e.length > 0).map((itemindex=> {
                        return (
                            <Text style={{
                                color: 'rgba(255, 255, 255, 1)',
                                fontSize: 11,
                                textAlign: 'center',
                                key: index
                            }}>
                                {item}
                            </Text>
                        )
                    })}
                </View >
            );
        } else {
            return null;
        }

Viewでごちゃごちゃとやっているのは、Remarkの文字列の長さによってtooltipのエリアを調節してる。今回のアプリの作り方では、同じ日に同じカテゴリでmoneyやtimeを記録した場合にはここで渡されるRemarkが配列となる。ゆえにmapと.lengthでその配列の書く文字列の長さを取り出して、長いものにViewの長さや起点を合わせる仕様に。中で表示されるTextも、配列から一つずつ取り出して列に並べる形になっている。...というこれらの操作はjsだし今回のアプリの仕様のに合わせてるだけですね。

 

汎用的なまとめとしては、propsで渡されたVisibleの状態によってViewかNullを返していて、Viewの中ではpropsから渡ってきた座標と表示する文字列を元にtooltipを形成しているだけです。

 

react-native-chart-kitとしての要点はここ。実際には更にchartConfigとかが続きますが今回は関係ないので割愛。

<LineChart
                data={{
                    labels: chartLabels,
                    datasets: [{
                        data: chartDatasets,
                        remarks: chartRemarks,
                        strokeWidth: 2,
                    }],
                }}
                onDataPointClick={(data=> {
                    if (toolTipVisible && data.x === toolTipPoint.x && data.y === toolTipPoint.y) {
                        setToolTipVisible(false);
                        return;
                    }
                    setToolTipPoint({
                        x: data.x,
                        y: data.y,
                        value: data.dataset.remarks[data.index],
                    })
                    setToolTipVisible(true);
                }}
                decorator={(data=> {
                    return (
                        <Tooltip point={{
                            x: toolTipPoint.x,
                            y: toolTipPoint.y,
                            value: toolTipPoint.value
                        }}
                            visible={toolTipVisible}
                        />
                    )
                }}

もともとLineChartの中に、onDataPointClickというものが設定できて、これによってデータポイントの座標とIndexを取得できる(data.xとdata.yとdata.index)ようになっています。最初のif文で、既に表示されているtooltipをタップした場合には非表示にし、そうでなければその下のsetToolTipPointで座標とindexに対応するremarkをそれぞれtoolTipPointのx,y,valueにセットしています。

 

で、その下のdecoratorというとこに上記で作ったtooltipタグを置いて、セットしたStateを渡してやってるわけですね。pointとvisibleに分かれてて、これらをまとめてpropsとして上記で受け取っていたわけですが、もしかして分ける必要もなくまたtooltipタグの方でもpropsじゃなく個別に展開して受け取るように書くべきでしょうか?

 

 

軸設定とグラフエリア

ここからは上手くいかなかったな、てことのメモ書きです。

 

グラフ化するライブラリなのに軸が設定できないのはどうなんでしょう。どうもデータポイントのxの値はlabelsに、yの値はdatasets.dataに、それぞれ配列として入ってくけどこれらはIndexで結びついているだけでx軸y軸に値に基づいてプロットされているわけじゃない。だからxの値はデータポイントの数で単に等間隔に配置されちゃうし、困ったことにデータポイント少なすぎるとグラフエリア自体の幅が小さくなっちゃう。ここは本当にどうにかならないのかなって調べたり試行錯誤もしてみたけれど良い解決策見つからず、、、募集中です。

 

ついでに言うとbackgroundColorも上手く処理されず、今回2つのBottom tabでChartは同じコンポーネントをそれぞれに渡す形でレンダーしていたらそれぞれのスクリーンで背景色の適用範囲が異なるという謎の事態に...backgroundGradientFrom/ToのOpacityを0にして背景色なしで統一することに(0なら背景色自体設定しても意味なかった....)。

                chartConfig={{
                    //fillShadowGradient: '#fff'
                    //backgroundColor: '#fff',
                    backgroundGradientFrom: '#fff',
                    backgroundGradientFromOpacity: 0,   
                    backgroundGradientTo: '#fff',
                    backgroundGradientToOpacity: 0,
                    decimalPlaces: 0,
                    color: (opacity = 0.5=> `rgba(10, 10, 10, ${opacity})`,
                    style: {
                        borderRadius: 5,
                    },
                }}
 

 

 

軸タイトル設定 

もう一個は些細だけれどそれぞれの軸の単位が設定できないこと。軸タイトルではなく軸ラベルにyAxisLabelあるいはyAxisSuffixで文字列を追加する、という方式なので単位が全ラベルに引っ付く形に。。。

moneyとtimeのスクリーンでDaily chartのコンポーネントを共有しているので、moneyだったら単位を前に、timeだったら単位(hourだけだけど)を後に付けるべくこのような形に。

                yAxisLabel={chartUnit!='hours' ? chartUnit + ' ' : ''}
                yAxisSuffix={chartUnit=='hours' ? ' ' + chartUnit : ''}

 

 

おわりに

この辺触ってみると、Rのggplotて本当によくできてるよなぁ。。。React Nativeの方だったらもっといろんなパッケージがあるのかな。Expoに対応しているのがこのReact-native-chart-kitだけって絶対困ると思うけど、みんなWebViewとかで上手くやりすごしてるのかな?

 

今回はただただ苦戦したことの備忘録でした、みなさんの対応策を絶賛募集しております。