React Native + Chart.jsでアプリ画面にレーダーチャートを表示する


React Nativeにおいてもグラフ描画ライブラリはいくつか存在するが、必ずしも要求する図表モードがなかったり、グラフのUIが要件に沿わなかったり、と障害に遭遇する。あなたも「Webアプリならば、あれを使うのに!」とか「あのライブラリのReact Native版はないの?」とか思い及ぶシーンは少なくないかもしれない。

わたしの体験では、レーダーチャートのUIが要件にマッチするものが見当たらず、Webアプリ制作で馴染みのあるChart.jsを使ったということがあった。

このエントリーでは、React Native + Chart.jsでグラフ描画する方法を共有したい。

はじめに

React NativeのコンポーネントにChart.jsでグラフ描画するに際して、React Native用のものを探すと「react-native-chartjs」なるものを見つけることができた。

これはReact NativeのWebViewコンポーネントを介して、HTMLのcanvasにグラフを描画するコンポーネントだ。これがもっとも簡単な方法と思われた。

ところが今日時点のバージョンだと、非推奨のライフサイクルメソッド(componentWillMount)使用による警告が発せられて、グラフ描画ができなかった。解消する手立てを講じるのも面倒くさく。

非常に残念に思いながら、自ら<WebView/>で実装することを決めた。

グラフ描画する

このサンプルは、以下のようなレーダーチャート描画を想定している。

index.html

Chart.jsのグラフ描画に必要なHTMLファイルを外部ファイルで用意する。当該ソースコードは、Webサイト制作の経験があるならば説明不要だろうと思う。CDNにホスティングされているChart.jsを読み込んでいる。

<html>
  <head>
    <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0">
    <script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0/dist/Chart.min.js"></script>
  </head>
  <body style="margin: 0; padding: 0;">
    <canvas id="chart" width="225" height="160"></canvas>
  </body>
</html>

ChartScreen.js

当該ファイルは、React Nativeの任意のコンポーネントだ。ファイル名に意味はなく、開発都合に合わせて命名してもらって構わない。

<WebView/>のsource propsにHTMLを渡すのだが、injectedJavaScript propsに渡すJavaScript同様にテンプレートリテラル記法にすれば、プラットフォーム間の相違がなくスムーズに済む。しかし可読性を損なってしまうことから外部ファイル化を検討してみた。

HTMLの外部ファイル化に際して、すこし工夫が必要になる。iOSとAndroidでファイル参照に差異があるそうで、たとえばiOSならば<Image/>のsource propsのようにrequire()で認識するが、Androidは{uri: ファイルパス}の形式で定義しなければならないわけだ。しかもプロジェクトルートからではなく、file://プロトコルを含むデバイス上のローカルファイルを指す必要がある。

もしExpo環境下ならば、Asset APIを使うとローカルファイル参照は容易い。Asset.fromModule(assetHtml);の具合に、require()で参照した値を渡すとlocalUriプロパティにローカルファイルパスが反映する。あとはプラットフォームを判定して、WebViewに渡すだけだ。

外部ファイルを読み込むことを配慮して、インジケータを表示してもよいかもしれない。これは<WebView/>のstartInLoadingState propsが担ってくれる。

import React, { useEffect, useState } from 'react';
import { View, Platform } from 'react-native';
import { WebView } from 'react-native-webview';
import { Asset } from 'expo-asset';
const assetHtml = require('../assets/html/index.html');
const download = async () => {
  let file = Asset.fromModule(assetHtml);
  if (file.localUri !== null) return file;
  await file.downloadAsync();
  return file;
}
const getSource = file => {
  if (Platform.OS !== 'android') return assetHtml;
  if (file === null) return {}
  return { uri: file.localUri };
}
export default function ChartScreen () {
  const [file, setFile] = useState(null);
  useEffect(() => {
    if (Platform.OS === 'android') {
      download().then(downloadFile => {
        setFile(downloadFile);
      });
    }
  }, []);
  const js = `
    var ctx = document.getElementById('chart').getContext('2d');
    var myChart = new Chart(ctx, {
      type: 'radar',
      data: {
        labels: ['現代文', '現代社会', '英語表現', '数学Ⅰ', '物理基礎', '科学基礎'],
        datasets: [{
          backgroundColor: 'rgba(255, 205, 86, .2)',
          borderColor: '#FFCD56',
          pointBackgroundColor: '#FFCD56',
          data: [80, 94, 78, 46, 39, 47],
        }],
      },
      options: {
        animation: true,
        showTooltips: false,
        legend: {
          display: false,
        },
        responsive: true,
        scale: {
          display: true,
          pointLabels: {
            fontSize: 10,
            fontColor: '#231815',
          },
          ticks: {
            display: true,
            fontSize: 12,
            fontColor: '#707070',
            maxTicksLimit: 6,
            min: 0,
            max: 100,
            beginAtZero: true,
          },
          gridLines: {
            display: true,
            color: '#EAEAEA',
          },
        },
      },
    });
  `;
  return (
    <WebView
      allowFileAccess={ true }
      source={ getSource(file) }
      injectedJavaScript={ js }
      startInLoadingState
    />
  );
}

まとめ

React Native + Chart.jsでグラフ描画する方法の共有だった。

グラフ描画に限らずWebコンテンツを必要とするシーンならば、<WebView/>に外部HTMLファイル読み込みの手法は役立つかもしれない。サンプルとして示すためHTMLは非常に簡素なものだったが、もっとしっかりコーディングすることにはなりそうだ。

このエントリーが、あなたのクリエイティビティを刺激するものであると期待したい。


Leave a Reply

Your email address will not be published. Required fields are marked *