DEV Community

Masatoshi Nishiguchi
Masatoshi Nishiguchi

Posted on • Edited on

7 2

Real-time charting with Elixir Phoenix.LiveView + Chart.js

This is written in Japanese. I might convert it to English later, maybe.

Goals

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
// package.json

{
  "dependencies": {
    "chart.js": "^3.6.1",
    "chartjs-adapter-luxon": "^1.1.0",
    "chartjs-plugin-streaming": "^2.0.0",
    "luxon": "^2.1.1",
  }
}
Enter fullscreen mode Exit fullscreen mode

グラフを操作する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
  }
}
Enter fullscreen mode Exit fullscreen mode

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()
  }
}
Enter fullscreen mode Exit fullscreen mode

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

// assets/js/live_view_hooks/index.js

import LineChart from './line_chart_hook'

export default {
  LineChart
}
Enter fullscreen mode Exit fullscreen mode

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
  }
})

// ...
Enter fullscreen mode Exit fullscreen mode

グラフを表示する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
Enter fullscreen mode Exit fullscreen mode

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

# lib/mnishiguchi_web/router.ex

defmodule MnishiguchiWeb.Router do
  use MnishiguchiWeb, :router

  # ...

  scope "/", MnishiguchiWeb do
    pipe_through :browser

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

  # ...
Enter fullscreen mode Exit fullscreen mode

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

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

🎉🎉🎉

Resources

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (1)

Collapse
 
mushu8 profile image
Alexandre Sagette

Thank you a lot for documenting this integration !!
It saves me some hours !

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more