12
1

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 1 year has passed since last update.

Phoenix.LiveViewでリアルタイムにグラフうねうね

Last updated at Posted at 2021-12-04

Elixirのリモートもくもく会autoracexでおなじみのオーサムさん(@torifukukaiou)が昨年の名前は聞いたことあるけど使ったことないやつをせっかくだから使ってみる Advent Calendar 2020 - 25 日目(最終日) で発表されたグラフうねうね (Elixir/Phoenix)が面白そうだったので、やってみました。記事の内容を参考にしつつ自分なりのアプローチで実装ができたので、グラフの部分のみに焦点を絞り二番煎じしてみようと思います。

やりたいこと

graph uneune Screen Recording 2021-11-28 at 5 33 48 PM

前提

  • 既存のPhoenixアプリがあり、それにグラフ用のLiveViewを追加する前提とします。
erlang             24.1.7
elixir             1.13.0-otp-24
phoenix            1.6.2
phoenix_live_view  0.17.1

依存関係をインストール

npm install --save --prefix assets \
  chart.js luxon chartjs-adapter-luxon chartjs-plugin-streaming
package.json
{
  "dependencies": {
    "chart.js": "^3.6.1",
    "chartjs-adapter-luxon": "^1.1.0",
    "chartjs-plugin-streaming": "^2.0.0",
    "luxon": "^2.1.1",
  }
}

グラフを操作するJavaScriptを定義

  • assets/js/line_chart.js
  • Chart.jsChartのラッパー
  • グラフの挙動を定義
  • グラフ初期化の関数
  • グラフに座標を追加する関数
assets/js/line_chart.js

// https://www.chartjs.org/docs/3.6.1/getting-started/integration.html#bundlers-webpack-rollup-etc
import Chart from 'chart.js/auto'
import 'chartjs-adapter-luxon'
import ChartStreaming from 'chartjs-plugin-streaming'
Chart.register(ChartStreaming)

// A wrapper of Chart.js that configures the realtime line chart.
export default class {
  constructor(ctx) {
    this.colors = [
      'rgba(255, 99, 132, 1)',
      'rgba(54, 162, 235, 1)',
      'rgba(255, 206, 86, 1)',
      'rgba(75, 192, 192, 1)',
      'rgba(153, 102, 255, 1)',
      'rgba(255, 159, 64, 1)'
    ]

    const config = {
      type: 'line',
      data: { datasets: [] },
      options: {
        datasets: {
          // https://www.chartjs.org/docs/3.6.0/charts/line.html#dataset-properties
          line: {
            // 線グラフに丸みを帯びさせる。
            tension: 0.3
          }
        },
        plugins: {
          // https://nagix.github.io/chartjs-plugin-streaming/2.0.0/guide/options.html
          streaming: {
            // 表示するX軸の幅をミリ秒で指定。
            duration: 60 * 1000,
            // Chart.jsに点をプロットする猶予を与える。
            delay: 1500
          }
        },
        scales: {
          x: {
            // chartjs-plugin-streamingプラグインの機能をつかうための型。
            type: 'realtime'
          },
          y: {
            // あらかじめY軸の範囲をChart.jsに教えてあげると、グラフの更新がスムーズです。
            suggestedMin: 50,
            suggestedMax: 200
          }
        }
      }
    }

    this.chart = new Chart(ctx, config)
  }

  addPoint(label, value) {
    const dataset = this._findDataset(label) || this._createDataset(label)
    dataset.data.push({x: Date.now(), y: value})
    this.chart.update()
  }

  destroy() {
    this.chart.destroy()
  }

  _findDataset(label) {
    return this.chart.data.datasets.find((dataset) => dataset.label === label)
  }

  _createDataset(label) {
    const newDataset = {label, data: [], borderColor: colors.pop()}
    this.chart.data.datasets.push(newDataset)
    return newDataset
  }
}

LiveViewJavaScriptとの間で通信するためのフックを定義

LiveView がマウントされたときに実行する処理を書きます。

assets/js/live_view_hooks/line_chart_hook.js
// 前項で定義したJSファイルをインポートする。
import RealtimeLineChart from '../line_chart'

export default {
  mounted() {
    // グラフを初期化する。
    this.chart = new RealtimeLineChart(this.el)

    // LiveViewから'new-point'イベントを受信時、座標を追加する。
    this.handleEvent('new-point', ({ label, value }) => {
      this.chart.addPoint(label, value)
    })
  },
  destroyed() {
    // 使用後はちゃんと破壊する。
    this.chart.destroy()
  }
}

個人的にindex.jsファイルで整理するスタイルが気に入ってます。

assets/js/live_view_hooks/index.js
import LineChart from './line_chart_hook'

export default {
  LineChart
}

assets/js/app.jsファイルでLiveSocketにフックを登録します。

assets/js/app.js
import 'phoenix_html'
import { Socket } from 'phoenix'
import { LiveSocket } from 'phoenix_live_view'
import topbar from '../vendor/topbar'

import LiveViewHooks from './live_view_hooks'

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content')
let liveSocket = new LiveSocket('/live', Socket, {
  hooks: LiveViewHooks,
  params: {
    _csrf_token: csrfToken
  }
})

// ...

グラフを表示するLiveViewを定義

lib/mnishiguchi_web/live/chart_live.ex
defmodule MnishiguchiWeb.ChartLive do
  use MnishiguchiWeb, :live_view

  @impl Phoenix.LiveView
  def mount(_params, _session, socket) do
    if connected?(socket) do
      # 本来はPubSubでデータを受信するところだが、今回そこはタイマーで再現する。
      :timer.send_interval(1000, self(), :update_chart)
    end

    {:ok, socket}
  end

  @impl Phoenix.LiveView
  def render(assigns) do
    ~H"""
    <div>
      <!--
      フックをセットする。
      本LiveViewにおいてグラフ更新はJavascriptの責任範囲なので、あらかじめ`phx-update="ignore"`により
      LiveViewにグラフ更新されないようにしておく。
      -->
      <canvas
        id="chart-canvas"
        phx-update="ignore"
        phx-hook="LineChart"></canvas>
    </div>
    """
  end

  @impl Phoenix.LiveView
  def handle_info(:update_chart, socket) do
    # ダミーデータを生成し、"new-point"イベントを発信する。
    {:noreply,
     Enum.reduce(1..5, socket, fn i, acc ->
       push_event(
         acc,
         "new-point",
         %{label: "User #{i}", value: Enum.random(50..150) + i * 10}
       )
     end)}
  end
end

LiveView のルートを忘れずに定義する。

lib/mnishiguchi_web/router.ex
defmodule MnishiguchiWeb.Router do
  use MnishiguchiWeb, :router

  # ...

  scope "/", MnishiguchiWeb do
    pipe_through :browser

    # ...
    live "/chart", ChartLive
  end

  # ...

graph uneune Screen Recording 2021-11-28 at 5 33 48 PM

比較的少ないコード記述量でリアルタイムグラフうねうねの実装ができました。

:tada::tada::tada:

12
1
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
12
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?