Use Vega-Lite with Streaming data in React

December 03, 2020

written by:

An easy guide to create a Vega graph with repeatedly incoming data and React lifecycle hooks.

vega streaming data react

Vega-Lite is a lightweight grammar or visualization specification language for interactive graphics. By creating a JSON Vega specification structure, you define how your source data will map or interact with your view area. Vega-Lite includes support for data transformations such as aggregation, binning, filtering, and sorting, as well as visual transformations such as stacking and faceting. Moreover, they can be composed into layered and multi-view displays or made interactive with selections. If you are not familiar with writing a Vega-Lite spec, you can find more information here.

This tutorial will guide through the process of creating an area graph for a stream of new data in a Vega-Lite.

Therefore, we want

  • to define a Vega-Lite spec for our graph to visualize the data
  • create a sine wave function to simulate streaming data.
  • set the view callback in Vega-Lite to a variable
  • use a hook to update the graph each moment

Vega-Lite Spec

Let's start with the spec we are defining. Most of the layers are for styling and interaction reasons.

const spec: VisualizationSpec = {
  $schema: 'https://vega.github.io/schema/vega-lite/v4.json',
  description: 'Powersupply',
  height: 200,
  width: 700,
  data: { name: 'data' },
  layer: [
    {
      encoding: {
        x: {
          field: 'x',
          type: 'ordinal',
          axis: {
            title: 'x axis',
          },
        },
        y: {
          field: 'value',
          type: 'quantitative',
          axis: {
            title: 'values',
          },
        },
      },
      layer: [
        {
          mark: {
            type: 'area',
            line: {
              color: 'darkslategray',
            },
            color: {
              x1: 1,
              y1: 1,
              x2: 1,
              y2: 0,
              gradient: 'linear',
              stops: [
                {
                  offset: 0,
                  color: 'white',
                },
                {
                  offset: 1,
                  color: 'darkslategray',
                },
              ],
            },
          },
        },
        {
          selection: {
            label: {
              type: 'single',
              nearest: true,
              on: 'mouseover',
              encodings: ['x'],
              empty: 'none',
            },
          },
          mark: { type: 'rule', color: 'gray' },
          encoding: {
            tooltip: [{ field: 'value', title: 'value ', type: 'ordinal' }],
            opacity: {
              condition: { selection: 'label', value: 1 },
              value: 0,
            },
          },
        },
      ],
    },
  ],
};

The graph would be as follows: vega streaming data react graph


sineDataSupplier Function

The sine wave function is supposed to return us data, which draws a sine curve in Vega. For each value on the x-axis (x), sine values (y) are generated and stored in an object, which is returned at the end.

import ...

const sineDataSupplier = (x: number) => {
  const y = 100 / 2 + 40 * Math.sin(x / 2);
  return { x: x, value: Math.floor(y) };
};

export function AreaGraph() { ... }

vega-view callback

In the Vega-Lite component we set an arrow function to store the view in a variable.

<VegaLite
  spec={spec}
  actions={false}
  renderer={'svg'}
  onNewView={(view) => setView(view)}
/>

As you might expect, the view is an object provided by Vega to modify the displayed vega-graph. We get it by a callback through the attribute onNewView which we then store in a state to use it whenever we need to.

You will find more detailed information about what the view can do in the documentation.


useEffect Hook to modify

Inside the React function we need a useEffect hook, which contains an updateGraph function. updateGraph first fetches the data objects from the sine wave function (in general this could be any source of data), then it modifies the vega-graph with the changeset() method by inserting (insert(...)) and deleting (remove(...)) corresponding data sets and changes the vega-view. Variable z tells us how many data sets are supposed to be deleted from the graph.

useEffect(() => {
  let z = -20;
  let x = 0;

  function updateGraph() {
    const data = sineDataSupplier(x);
    x++;
    z++;

    const cs = vega
      .changeset()
      .insert(data)
      .remove((t: { x: number; value: number }) => {
        return t.x < z;
      });

    view.change('data', cs).run();
  }
});

The variables x and z should be increased with each call. To modify them as constants, we use the useRef hook. You can simply change the values inside the function by using the ref.

Since we do not know when the callback is delivered with the view-instance, we must not call the initial updateGraph function in the useEffect until view is set. From this point on, we also set an interval timer for the repeated call of the updateGraph function.

export function AreaGraph() {
  const [view, setView] = useState<View>();
  const z = -20;
  const x = 0;

  const ref = useRef({
    x,
    z,
  });

  useEffect(() => {
    function updateGraph() {
      const data = sineDataSupplier(ref.current.x);
      ref.current.x++;
      ref.current.z++;

      const cs = vega
        .changeset()
        .insert(data)
        .remove((t: { x: number; value: number }) => {
          return t.x < ref.current.z;
        });

      view.change('data', cs).run();
    }

    if (view) {
      updateGraph();
      const interval: number = setInterval(updateGraph, 1111);
      return () => clearInterval(interval);
    }
  }, [view]);
}

Complete Code

The complete component looks like this

import React, { useEffect, useRef, useState } from 'react';
import { VegaLite, View } from 'react-vega';
import { VisualizationSpec } from 'vega-embed';
import * as vega from 'vega';

const sineDataSupplier = (x: number) => {
  const y = 100 / 2 + 40 * Math.sin(x / 2);
  return { x: x, value: Math.floor(y) };
};

export function AreaGraph() {
  const [view, setView] = useState<View>();
  const z = -20;
  const x = 0;

  const ref = useRef({
    x,
    z,
  });

  useEffect(() => {
    function updateGraph() {
      const data = sineDataSupplier(ref.current.x);
      ref.current.x++;
      ref.current.z++;

      const cs = vega
        .changeset()
        .insert(data)
        .remove((t: { x: number; value: number }) => {
          return t.x < ref.current.z;
        });

      view.change('data', cs).run();
    }

    if (view) {
      updateGraph();
      const interval: number = setInterval(updateGraph, 1111);
      return () => clearInterval(interval);
    }
  }, [view]);

  const spec: VisualizationSpec = {
    $schema: 'https://vega.github.io/schema/vega-lite/v4.json',
    description: 'Streaming Data',
    height: 200,
    width: 600,
    data: { name: 'data' },
    layer: [
      {
        encoding: {
          x: {
            field: 'x',
            type: 'ordinal',
            axis: {
              title: 'x axis',
            },
          },
          y: {
            field: 'value',
            type: 'quantitative',
            axis: {
              title: 'values',
            },
          },
        },
        layer: [
          {
            mark: {
              type: 'area',
              line: {
                color: 'darkslategray',
              },
              color: {
                x1: 1,
                y1: 1,
                x2: 1,
                y2: 0,
                gradient: 'linear',
                stops: [
                  {
                    offset: 0,
                    color: 'white',
                  },
                  {
                    offset: 1,
                    color: 'darkslategray',
                  },
                ],
              },
            },
          },
          {
            selection: {
              label: {
                type: 'single',
                nearest: true,
                on: 'mouseover',
                encodings: ['x'],
                empty: 'none',
              },
            },
            mark: { type: 'rule', color: 'gray' },
            encoding: {
              tooltip: [{ field: 'value', title: 'value ', type: 'ordinal' }],
              opacity: {
                condition: { selection: 'label', value: 1 },
                value: 0,
              },
            },
          },
        ],
      },
    ],
  };

  return (
    <>
      <h3>React Vega Streaming Data</h3>
      <div>
        <VegaLite
          spec={spec}
          actions={false}
          renderer={'svg'}
          onNewView={(view) => setView(view)}
        />
      </div>
    </>
  );
}
export default AreaGraph;

For further details, please read the Vega Documentation for Streaming Data.