Masatoshi Nishiguchi
Posted on • Edited on
Real-time charting with Elixir Phoenix.LiveView + Chart.js
This is written in Japanese. I might convert it to English later, maybe.
Goals
- Real-time charting withPhoenix.LiveView +Chart.js +chartjs-plugin-streaming
- Drive the charting bypush fromLiveView toChart.js
- LiveView has no state
- LiveView receives new data from multiple clients viaPubSub, which we simulate by using timer and fake data
- LiveView sends data to the chart via
phx-hook
- x axis: a point of time ourJavaScript code adds a datapoint to the chart
- y axis: a value sent from ourLiveView
- Create a dataset per unique user name
前提
erlang 24.1.7elixir 1.13.0-otp-24phoenix 1.6.2phoenix_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-etcimportChartfrom'chart.js/auto'import'chartjs-adapter-luxon'importChartStreamingfrom'chartjs-plugin-streaming'Chart.register(ChartStreaming)// A wrapper of Chart.js that configures the realtime line chart.exportdefaultclass{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)']constconfig={type:'line',data:{datasets:[]},options:{datasets:{// https://www.chartjs.org/docs/3.6.0/charts/line.html#dataset-propertiesline:{// 線グラフに丸みを帯びさせる。tension:0.3}},plugins:{// https://nagix.github.io/chartjs-plugin-streaming/2.0.0/guide/options.htmlstreaming:{// 表示する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=newChart(ctx,config)}addPoint(label,value){constdataset=this._findDataset(label)||this._createDataset(label)dataset.data.push({x:Date.now(),y:value})this.chart.update()}destroy(){this.chart.destroy()}_findDataset(label){returnthis.chart.data.datasets.find((dataset)=>dataset.label===label)}_createDataset(label){constnewDataset={label,data:[],borderColor:colors.pop()}this.chart.data.datasets.push(newDataset)returnnewDataset}}
LiveViewとJavaScriptとの間で通信するためのフックを定義
LiveView がマウントされたときに実行する処理を書きます。
// assets/js/live_view_hooks/line_chart_hook.js// 前項で定義したJSファイルをインポートする。importRealtimeLineChartfrom'../line_chart'exportdefault{mounted(){// グラフを初期化する。this.chart=newRealtimeLineChart(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.jsimportLineChartfrom'./line_chart_hook'exportdefault{LineChart}
assets/js/app.js
ファイルでLiveSocket
にフックを登録します。
// assets/js/app.jsimport'phoenix_html'import{Socket}from'phoenix'import{LiveSocket}from'phoenix_live_view'importtopbarfrom'../vendor/topbar'importLiveViewHooksfrom'./live_view_hooks'letcsrfToken=document.querySelector("meta[name='csrf-token']").getAttribute('content')letliveSocket=newLiveSocket('/live',Socket,{hooks:LiveViewHooks,params:{_csrf_token:csrfToken}})// ...
グラフを表示するLiveViewを定義
# lib/mnishiguchi_web/live/chart_live.exdefmoduleMnishiguchiWeb.ChartLivedouseMnishiguchiWeb,:live_view@implPhoenix.LiveViewdefmount(_params,_session,socket)doifconnected?(socket)do# 本来はPubSubでデータを受信するところだが、今回そこはタイマーで再現する。:timer.send_interval(1000,self(),:update_chart)end{:ok,socket}end@implPhoenix.LiveViewdefrender(assigns)do~H""" <div> <!-- フックをセットする。 本LiveViewにおいてグラフ更新はJavascriptの責任範囲なので、あらかじめ`phx-update="ignore"`により LiveViewにグラフ更新されないようにしておく。 --> <canvas n">chart-canvas" phx-update="ignore" phx-hook="LineChart"></canvas> </div> """end@implPhoenix.LiveViewdefhandle_info(:update_chart,socket)do# ダミーデータを生成し、"new-point"イベントを発信する。{:noreply,Enum.reduce(1..5,socket,fni,acc->push_event(acc,"new-point",%{label:"User#{i}",value:Enum.random(50..150)+i*10})end)}endend
LiveViewのルートを忘れずに定義する。
# lib/mnishiguchi_web/router.exdefmoduleMnishiguchiWeb.RouterdouseMnishiguchiWeb,:router# ...scope"/",MnishiguchiWebdopipe_through:browser# ...live"/chart",ChartLiveend# ...
比較的少ないコード記述量でリアルタイムグラフうねうねの実装ができました。
🎉🎉🎉
Resources
Top comments(1)
Subscribe
For further actions, you may consider blocking this person and/orreporting abuse