December 03, 2020 •
written by:
An easy guide to create a Vega graph with repeatedly incoming data and React lifecycle hooks.
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
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:
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() { ... }
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.
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]);
}
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.