Elixirのリモートもくもく会autoracexでおなじみのオーサムさん(@torifukukaiou)が昨年の名前は聞いたことあるけど使ったことないやつをせっかくだから使ってみる Advent Calendar 2020 - 25 日目(最終日) で発表されたグラフうねうね (Elixir/Phoenix)が面白そうだったので、やってみました。記事の内容を参考にしつつ自分なりのアプローチで実装ができたので、グラフの部分のみに焦点を絞り二番煎じしてみようと思います。
やりたいこと
- Phoenix.LiveViewとChart.js + chartjs-plugin-streamingを利用し、リアルタイムにグラフを更新
- LiveViewからChart.jsへのプッシュ駆動(LiveView主導)
-
LiveViewには状態をもたせない(LiveViewは受け取った値を
phx-hook
経由でグラフに渡すのみ) - LiveViewにPubSub経由で複数のクライエント(ユーザー)からデータを受け取るのを想定(今回この部分はタイマーとダミーデータで再現)
- x 軸: JavaScriptコードが点を追加する時点の時間
- y 軸: 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.js の
Chart
のラッパー - グラフの挙動を定義
- グラフ初期化の関数
- グラフに座標を追加する関数
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
}
}
LiveViewとJavaScriptとの間で通信するためのフックを定義
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
# ...
比較的少ないコード記述量でリアルタイムグラフうねうねの実装ができました。