Line Graphs

This longform guide covers what's achievable with Charts, how to use them with varying types of data, visual customisation, and points towards advanced tricks!

Getting Started

Electric UI bundles our own graphing implementation designed specifically for high-performance realtime use. It's designed specifically for high performance, leveraging GPU acceleration and carefully tuned data handling backends to maximise performance and/or battery life on portable devices.

Performance scales nearly linearly if you try to draw one very high-speed variable, a dozen lines on one chart, or a dozen different variables on different charts1.

In our tests with mid-range compute resources (Intel 6-series iGPU) , we've demonstrated between 1-3 million points on screen with realtime datarates exceeding 50k points added per second. This is while reaching render targets above 120fps.

Realtime Charts

Data is drawn on the left of the chart as it arrives, and moves left as it 'ages'. The window is usually a fixed time period.

These charts are what most use-cases would expect to use for continuous real-time use.

First, import the charting components we'll need and create a datasource with the message identifier we want to chart.

import {
ChartContainer,
LineChart,
RealTimeDomain,
VerticalAxis,
HorizontalAxis,
} from '@electricui/components-desktop-charts'
import { MessageDataSource } from '@electricui/core-timeseries'

The datasource is responsible for filtering messages arriving from hardware, and then references those values against a timestamp in a format optimised for charting.

The actual datasource used here is a MessageDataSource and should be declared outside of the component, at the module level.

MessageDataSource and DataTransformers clone themselves for each Device automatically, and this operation caches the result, comparing datasources by referential equality.

Don't re-create a DataSource every render by placing them inside the component.

  1. const sensorDataSource = new MessageDataSource('heartbeat')
  2. export const OverviewPage = () => {
  3. return <React.Fragment>Chart goes here</React.Fragment>
  4. }

With a datasource providing our timeseries data, we need to add a LineChart component to plot data.

ChartContainer provides the GPU accelerated (WebGL) context. Child functions are used to add and control the chart behaviour. We'll add a LineChart to plot our data, as well as a RealTimeDomain to tell the chart to display a 10 second (10,000 ms) window.

  1. <ChartContainer>
  2. <LineChart dataSource={sensorDataSource} />
  3. <RealTimeDomain window={10000} />
  4. <TimeAxis />
  5. <VerticalAxis />
  6. </ChartContainer>

The TimeAxis and VerticalAxis components are responsible for drawing the axis lines and labels.

Single-shot Trigger Charts

Triggered charts which draw a single window of data at once, without streaming/scrolling behaviour. When re-triggered, will draw an entire window of data prior to the trigger point.

Best used when users need to evaluate the shape of the curve for longer periods of time, or when data is arriving at high rates while being viewed with a short duration window.

To trigger a redraw when a hardware sync message arrives, create a second datasource to act as the trigger. Instead of a RealTimeDomain, use a TriggerDomain and provide the new trigger source.

The TriggerDomain needs an accessor to select the time

import {
ChartContainer,
LineChart,
TriggerDomain,
VerticalAxis,
HorizontalAxis,
} from '@electricui/components-desktop-charts'
import { MessageDataSource } from '@electricui/core-timeseries'
  1. const sensorDataSource = new MessageDataSource('position')
  2. const triggerDataSource = new MessageDataSource('trigger')
  1. <ChartContainer>
  2. <LineChart dataSource={sensorDataSource} />
  3. <TriggerDomain
  4. dataSource={triggerDataSource}
  5. accessor={(event, time) => time}
  6. window={10000}
  7. />
  8. <TimeAxis />
  9. <VerticalAxis />
  10. </ChartContainer>

In situations where the trigger is a threshold on our signal of interest, we can write a custom DataSource which only emits events when our condition is met.

  1. let triggerMax: number = 90
  2. const triggerDataSource = new MessageDataSource('dba', (message, emit) => {
  3. if ( message.payload > triggerMax) {
  4. // Generate the trigger event with a dummy value
  5. const event = new Event(message.metadata.timestamp, 1)
  6. emit(event)
  7. }
  8. }
  9. })

To offset the chart from the event timestamp, simply add or subtract milliseconds from the timestamp when generating the event.

  1. // Shift the trigger event half a second into the past
  2. const event = new Event(message.metadata.timestamp - 500, 1)

This allows control over which window of time is being plotted - data before, around, or after the the trigger time.

When using a trigger event and offset which is shorter than the window duration, newer data after the trigger will stream in realtime, leading to a rolling trigger behaviour!

Rolling Triggered Charts

Oscilloscope style chart which starts drawing when a trigger condition is met. New data is appended to the right of the curve as new triggers fire.

Best used when plotting plotting a long duration window without waiting for the full duration to elapse. This behaviour is well suited for drawing impulse-style charts when tuning a PID controller, or monitoring the behaviour of a system after a change in operating mode.

This is just a variant on a triggered chart as described above. This example also adds a naiive debounce timer to prevent the graph from triggering too often

  1. let lastTrigger: number = 0
  2. let triggerThreshold: number = 75
  3. const triggerDS = new MessageDataSource('dba', (message, emit) => {
  4. // Debounce - only consider an event if we haven't recently done so
  5. if (message.metadata.timestamp - lastTrigger > 3000) {
  6. // Adaptive thresholding - only consider magnitudes larger than a previous max
  7. if (Math.abs(message.payload) > triggerThreshold) {
  8. lastTrigger = message.metadata.timestamp
  9. const event = new Event(message.metadata.timestamp + 700, 1)
  10. emit(event)
  11. }
  12. }
  13. })
  1. <TriggerDomain
  2. window={1000}
  3. dataSource={triggerDS}
  4. accessor={(event, time) => time}
  5. yMin={-20}
  6. yMax={20}
  7. />

Charting arrays & structures

Different datasources

Add many different data sources to the same ChartContainer by adding additional LineChart children.

  1. const ambientTempDataSource = new MessageDataSource('ambient_c')
  2. const cathodeTempDataSource = new MessageDataSource('cathode_c')
  1. <ChartContainer>
  2. <LineChart dataSource={ambientTempDataSource} maxItems={250} />
  3. <LineChart dataSource={cathodeTempDataSource} maxItems={500} />
  4. <RealTimeDomain window={10000} delay={50} />
  5. <TimeAxis />
  6. <VerticalAxis />
  7. </ChartContainer>

Array data

LineChart follows the standard accessor syntax for accessing array elements. This allows you to directly access the array index of interest.

const sensorArrayDataSource = new MessageDataSource('sensors')
  1. <ChartContainer>
  2. <LineChart
  3. dataSource={sensorArrayDataSource}
  4. accessor={state => state.sensors[0]}
  5. />
  6. <LineChart
  7. dataSource={sensorArrayDataSource}
  8. accessor={state => state.sensors[1]}
  9. />
  10. ...
  11. </ChartContainer>

To plot an unknown number of values in an array, we can write a simple inline function to create as many LineChart children as needed.

To display all of the (unknown number) temperature sensors, we need to know how many sensors we need to add, so we use the useHardwareState() hook to get the sensor state, then use .length to get the number of array items.

  1. export const ServoDetails = () => {
  2. const numProbes: number | null = useHardwareState(
  3. state => (state.sensors || []).length,
  4. )
  5. return (
  6. <div>
  7. <ChartContainer>
  8. {Array.from(new Array(numProbes)).map((_, index) => (
  9. <LineChart
  10. dataSource={sensorArrayDataSource}
  11. accessor={state => state.sensors[index]}
  12. />
  13. ))}
  14. ...
  15. </ChartContainer>
  16. </div>
  17. )
  18. }

Structured data

Charting a member of a custom data type is almost the same as using array elements. The accessor specifies the target member of the custom type.

const motorDataSource = new MessageDataSource('motor')
  1. <ChartContainer>
  2. <LineChart
  3. dataSource={motorDataSource}
  4. accessor={state => state.motor.position_goal}
  5. maxItems={2000}
  6. />
  7. <LineChart
  8. dataSource={motorDataSource}
  9. accessor={state => state.motor.position_actual}
  10. maxItems={2000}
  11. />
  12. ...
  13. </ChartContainer>

Visual Customisation

Charts allow customisation of line colour, thickness, axis ticks and formatting and more. Annotations provide horizontal or vertical lines to callout key values, with optional rich content drawn anywhere on the chart.

Axis Labels

Either or both axis can be labelled to provide context to the data. Most charts will have a VerticalAxis and HorizontalAxis . Documentation is here and covers formatting the axis tick values and grid density.

  1. <ChartContainer>
  2. <TimeAxis label="Time (seconds)" />
  3. <VerticalAxis label="Temperature °C" />
  4. </ChartContainer>

Coloring

LineChart accepts a colour property, documented here.

<LineChart dataSource={tempDataSource} color="#0066CC" />
<LineChart dataSource={tempDataSource} color={Colors.RED4} />

If specifying colours manually, be aware of contrast issues when using the same colours in light mode and dark mode.

To specify a colour for each mode, conditionally swap colour using the useDarkMode() hook as the driver.

  1. export const ChartComponent = (props: RouteComponentProps) => {
  2. const darkMode = useDarkMode()
  3. return (
  4. <React.Fragment>
  5. <ChartContainer>
  6. <LineChart
  7. dataSource={tempDataSource}
  8. maxItems={1000}
  9. color={darkMode ? '#559a75' : '#81ebb2'}
  10. />
  11. <RealTimeDomain window={10000} />
  12. <TimeAxis />
  13. <VerticalAxis />
  14. </ChartContainer>
  15. </React.Fragment>

Annotations

Annotations are used to provide callouts for a key feature(s) on the Chart. This could be to indicate a setpoint for an alarm, a threshold for a sensor, or to make the maximum of the curve more easily readable.

Red and Blue horizontal lines overlayed on sine-wave, shows min and max values

Add a HorizontalAnnotation to the ChartContainer, and provde a datasource setup to provide the value of interest.

In this example, we'll add two horizontal lines to the DataSource manually.

  1. // Create dataSources to drive the annotation values
  2. // This message is a custom type with 'low' and 'high' values
  3. const thresholdDataSource = new MessageDataSource('temp_limits')
  1. export const ChartPage = (props: RouteComponentProps) => {
  2. return (
  3. <React.Fragment>
  4. <ChartContainer>
  5. ...
  6. <HorizontalAnnotation
  7. dataSource={thresholdDataSource}
  8. accessor={state => state.temp_limits.low}
  9. color="#0066CC"
  10. />
  11. <HorizontalAnnotation
  12. dataSource={thresholdDataSource}
  13. accessor={event => event.temp_limits.high}
  14. color="#CC1F00"
  15. />
  16. </ChartContainer>
  17. </React.Fragment>
  18. )
  19. }

Control visual properties for colour, grid-line style and behaviour. See the API reference for more details.

Advanced Features

The Charting engine can do a lot more than just plot lines as data comes in.

A lot of the power behind detailed and useful charts is the ability to process data on the UI computer to provide additional context to a signal. This is done at the DataSource level with DataTransformers.

Additionally, there are graphics tricks which can heavily stylise charts during the render step. This is possible because Charts are backed by WebGL, allowing shaders to be applied to geometry and the scene.

2D Charts

When working with sensor data which is best expressed with respect to another variable rather than time, a 2D plot might be a better option.

These plots are useful when visualising GPS position trails, accelerometer XY readings aka G-Force indicator, or representing lidar sensor data as an example.

Refer to the TimeSlicedLineChart component docs for a basic reference integration.

Data Transformers

UI-side processing of inbound data helps provide things like realtime filters, averaging, or custom mathematical transforms. This is detailed in the Datasources guide.

For a more advanced example, we provide a copy-paste example for realtime Fast Fourier Transforms (FFT).


  1. A maximum of 16 ChartContainers can be drawn on one screen by default - contact us if you need to raise this limit.