This is written in Japanese. I might convert it to English later, maybe.
Goals
- Real-time charting with Phoenix.LiveView + Chart.js + chartjs-plugin-streaming
- Drive the charting by push from LiveView to Chart.js
- LiveView has no state
- LiveView receives new data from multiple clients via PubSub, which we simulate by using timer and fake data
-
LiveView sends data to the chart via
phx-hook
- x axis: a point of time our JavaScript code adds a datapoint to the chart
- y axis: a value sent from our LiveView
- Create a dataset per unique user name
前提
erlang 24.1.7
elixir 1.13.0-otp-24
phoenix 1.6.2
phoenix_live_view 0.17.1
https://nagix.github.io/chartjs-plugin-streaming/2.0.0/guide/getting-started.html#integration
依存関係をインストール
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
# ...
比較的少ないコード記述量でリアルタイムグラフうねうねの実装ができました。
🎉🎉🎉
Top comments (1)
Thank you a lot for documenting this integration !!
It saves me some hours !