Skip to content

Live data with Chart.js

This lesson builds a live sensor dashboard — three Chart.js line charts fed by Ruby timers via JsBridge. It demonstrates the full multi-file pattern applied to a WebView app, with a clean separation between the data model, the WebView panel, and the wx sidebar.

charts.png

1
ruby main.rb

Requires internet access for the Chart.js CDN.

Download live_chart_app.zip

File structure

live_chart_app/
├── main.rb
├── Gemfile
├── assets/
│   └── js/
│       └── js_bridge_client.js   # JS side of the bridge library
└── lib/
    ├── dashboard_frame.rb
    ├── js_bridge.rb              # Ruby side of the bridge library
    ├── html/
    │   └── dashboard_page.rb     # HTML as a Ruby module
    ├── models/
    │   └── sensor.rb
    └── panels/
        ├── chart_panel.rb
        └── stats_panel.rb

js_bridge.rb and js_bridge_client.js are the bridge library pair from lesson 5.2 — drop them into any WebView app unchanged.

The model — lib/models/sensor.rb

Sensor is pure Ruby — no UI knowledge, no WebView. It holds baseline values and drift parameters for each series, and provides a read(series) method that returns a simulated sensor value with realistic random drift. The frame calls read on every timer tick and passes the result to the chart panel.

The HTML module — lib/html/dashboard_page.rb

The dashboard HTML lives in a Ruby module with a class method that generates the page. It accepts the sensor series configuration from Ruby so the chart colours and labels are consistent between the Ruby and JS sides:

1
2
3
4
5
6
7
8
9
module DashboardPage
  JS_CLIENT = Base64.strict_encode64(
    File.read(File.join(__dir__, '../../assets/js/js_bridge_client.js'))
  )

  def self.html(series_config)
    # HTML heredoc with #{JS_CLIENT} injected into a script src attribute
  end
end

The JS client library is loaded as a base64 data URI in the page <head>:

1
<script src="data:text/javascript;base64,#{JS_CLIENT}"></script>

Chart.js is loaded from CDN immediately after:

1
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>

The page builds its charts dynamically from the series config — adding or removing a sensor in sensor.rb automatically adds or removes a chart column.

The chart panel — lib/panels/chart_panel.rb

ChartPanel wraps the WebView and JsBridge completely. The frame knows nothing about WebView internals — it calls clean Ruby methods:

1
2
3
chart_panel.push_data(series, value, label)
chart_panel.get_stats(series) { |stats, error| ... }
chart_panel.clear_series(series)

Inside the panel, push_data calls the JS pushDataPoint method via @bridge.call. The chart updates immediately with no animation (chart.update('none')) for smooth live performance.

The ready pattern from lesson 5.2 is applied correctly — Ruby triggers ready after registering the handler:

1
2
3
4
5
@webview.evt_webview_loaded(@webview.id) do
  next if @webview.current_url == 'about:blank'
  @webview.add_script_message_handler('bridge')
  @bridge.run_script("RubyBridge.emit('ready', {})")
end

The on_ready callback notifies the frame, which then starts the data timer. No data is pushed until the page is ready.

The stats panel — lib/panels/stats_panel.rb

A plain wx panel with static text labels for each series and two buttons — Refresh stats and Clear all. It knows nothing about WebView or JsBridge. The frame wires it up via callbacks following the lesson 3.2 pattern.

The frame — lib/dashboard_frame.rb

The frame is a thin coordinator. Its jobs:

  • Create the splitter and both panels
  • Start the data timer when the chart panel signals ready
  • On each timer tick, read all sensors and push to the chart panel
  • On Refresh Stats, call get_stats for each series and pass results to the stats panel
  • On Clear All, clear each series in the chart panel
  • Stop the timer on close

The timer runs every 500ms. Each tick reads all three sensors and calls push_data for each. Because JsBridge.call is asynchronous (fire-and-forget for push_data), the timer never blocks — 500ms is plenty of time for the chart to update between ticks.

Querying data back from JS

The Refresh Stats button demonstrates true RPC — Ruby calls a JS method and gets data back:

1
2
3
4
@chart_panel.get_stats(series) do |stats, error|
  next if error
  @stats_panel.update_stats(series, stats)
end

The JS getStats method computes min, max, and mean from the chart’s current data array and returns them as a hash. Ruby receives this in the callback and updates the wx labels. This is the same @bridge.call with callback pattern from lesson 5.2 — now applied to real data.

What to take forward

  • HTML in a dedicated module keeps the frame clean
  • js_bridge.rb + js_bridge_client.js drop into any WebView app unchanged
  • The chart panel wraps all WebView/JsBridge details — the frame calls clean Ruby methods
  • Never push data until on_ready fires — guard with return unless @ready
  • Use chart.update('none') in Chart.js for smooth live updates without animation
  • JsBridge.call with a callback is the pattern for querying data back from JS

Previous: The JavaScript bridge | Next: Leaflet maps