stream-charts
are react-based time-series charts for viewing high frequency data, streamed in real-time using rxjs. Generally, update periods of 25 ms aren't a problem for about a hundred or so time-series. To achieve this type of performance, the charts are implemented in d3 and wrapped in react functional components using hooks.
Although still under development, there are two charts available:
Over time, I'll add additional chart types. In the meantime, I welcome any contributions to create new chart types (bar, gauges, etc).
Stream charts:
All these capabilities are on display in the example project
Please see change history for a history of changes.
npm install stream-charts
For the neuron raster chart (see example)
import {RasterChart} from "stream-charts";
// .
// .
// .
<Chart
width={useGridCellWidth()}
height={useGridCellHeight()}
margin={{...defaultMargin, top: 60, right: 60}}
color={theme.color}
backgroundColor={theme.backgroundColor}
seriesStyles={new Map([
['test1', {...defaultLineStyle, color: 'orange', lineWidth: 1, highlightColor: 'orange'}],
['test2', {...defaultLineStyle,
color: theme.name === 'light' ? 'blue' : 'gray',
lineWidth: 3,
highlightColor: theme.name === 'light' ? 'blue' : 'gray',
highlightWidth: 5}
],
])}
initialData={initialDataRef.current}
seriesFilter={filter}
seriesObservable={observableRef.current}
shouldSubscribe={running}
windowingTime={35}
>
<ContinuousAxis
axisId="x-axis-1"
location={AxisLocation.Bottom}
domain={[0, 5000]}
label="x-axis"
// font={{color: theme.color}}
/>
<ContinuousAxis
axisId="x-axis-2"
location={AxisLocation.Top}
domain={[0, 10000]}
label="x-axis"
// font={{color: theme.color}}
/>
<CategoryAxis
axisId="y-axis-1"
location={AxisLocation.Left}
categories={initialDataRef.current.map(series => series.name)}
label="y-axis"
/>
<CategoryAxis
axisId="y-axis-2"
location={AxisLocation.Right}
categories={initialDataRef.current.map(series => series.name)}
label="y-axis"
/>
<Tracker
visible={visibility.tracker}
labelLocation={TrackerLabelLocation.WithMouse}
style={{color: theme.color}}
font={{color: theme.color}}
// onTrackerUpdate={update => console.dir(update)}
/>
<Tooltip
visible={visibility.tooltip}
style={{
fontColor: theme.color,
backgroundColor: theme.backgroundColor,
borderColor: theme.color,
backgroundOpacity: 0.9,
}}
>
<RasterPlotTooltipContent
xFormatter={value => formatNumber(value, " ,.0f") + ' ms'}
yFormatter={value => formatNumber(value, " ,.1f") + ' mV'}
/>
</Tooltip>
<RasterPlot
axisAssignments={new Map([
// ['test', assignAxes("x-axis-1", "y-axis-1")],
['test1', assignAxes("x-axis-2", "y-axis-2")],
// ['test3', assignAxes("x-axis-1", "y-axis-1")],
])}
dropDataAfter={10000}
panEnabled={true}
zoomEnabled={true}
zoomKeyModifiersRequired={true}
withCadenceOf={30}
/>
</Chart>
and for the scatter chart (see example)
import {ScatterChart} from "stream-charts";
// .
// .
// .
<Chart
width={useGridCellWidth()}
height={useGridCellHeight()}
margin={{...defaultMargin, top: 60, right: 60}}
color={theme.color}
backgroundColor={theme.backgroundColor}
seriesStyles={new Map([
['test1', {...defaultLineStyle, color: 'orange', lineWidth: 1, highlightColor: 'orange'}],
['test2', {...defaultLineStyle,
color: theme.name === 'light' ? 'blue' : 'gray',
lineWidth: 3,
highlightColor: theme.name === 'light' ? 'blue' : 'gray',
highlightWidth: 5}
],
])}
initialData={initialDataRef.current}
seriesFilter={filter}
seriesObservable={observableRef.current}
shouldSubscribe={running}
windowingTime={25}
>
<ContinuousAxis
axisId="x-axis-1"
location={AxisLocation.Bottom}
domain={[10, 5000]}
label="x-axis"
/>
<ContinuousAxis
axisId="y-axis-1"
location={AxisLocation.Left}
domain={[0, 1000]}
label="y-axis"
/>
<ContinuousAxis
axisId="x-axis-2"
location={AxisLocation.Top}
domain={[100, 2500]}
label="x-axis (2)"
/>
<ContinuousAxis
axisId="y-axis-2"
location={AxisLocation.Right}
scale={d3.scaleLog()}
domain={[100, 1200]}
label="y-axis (2)"
/>
<Tracker
visible={visibility.tracker}
labelLocation={TrackerLabelLocation.WithMouse}
style={{color: theme.color}}
font={{color: theme.color}}
// onTrackerUpdate={update => console.dir(update)}
/>
<Tooltip
visible={visibility.tooltip}
style={{
fontColor: theme.color,
backgroundColor: theme.backgroundColor,
borderColor: theme.color,
backgroundOpacity: 0.9,
}}
>
<ScatterPlotTooltipContent
xLabel="t (ms)"
yLabel="count"
yValueFormatter={value => formatNumber(value, " ,.0f")}
yChangeFormatter={value => formatNumber(value, " ,.0f")}
/>
</Tooltip>
<ScatterPlot
interpolation={interpolation}
axisAssignments={new Map([
['test2', assignAxes("x-axis-2", "y-axis-2")],
])}
dropDataAfter={10000}
panEnabled={true}
zoomEnabled={true}
zoomKeyModifiersRequired={true}
withCadenceOf={30}
/>
</Chart>
stream-charts
are high-performance charts for displaying large amounts of data in real-time. The charts are wrapped in react and fed data using rxjs Observable
s. The goal stream-charts
is to display large amounts of time-series data at high frequencies while providing tools to understand the time-series.
There are currently two chart types available: a raster chart for displaying neuron spikes as a function of time, and a scatter chart for displaying time-series of data.
The chart below shows the raster chart with the bar magnifier enabled. The controls at the top of the chart are part of the example. These controls allow filtering time-series by their assigned names in real-time, displaying a tooltip when the mouse pointer is on top of a datum, displaying a tracker that show a vertical line and the current time of the mouse, and a bar magnifier, as shown in the image.
The scatter plot shown below has 30 time-series of test data, with two time axes, in which the data are updated every 25 milliseconds. A tooltip shows the times and values that came just before and just after the mouse cursor and, as well as the time and value changes.
The stream-charts
module wraps d3 elements with functional react in a way that keeps the chart (d3) updates out of the react render cycle. All stream-charts
start with the <Chart/>
root element.
<Chart/>
The Chart
component creates the main SVG element (container) holding the chart, manages a reference to that container, and is the wraps the children in the chart context provider so that they have access to the useChart hook which holds properties, styles, callbacks, subscription needed to construct the charts and make them interactive.
The Chart
s properties fall into four categories:
width (pixels)
The width (in pixels) of the container that holds the chart. The actual plot will be smaller based on the margins.
height (pixels)
The height (in pixels) of the container that holds the chart. The actual plot will be smaller based on the margins.
margin (Margin, optional) The margin (in pixels) around plot. For example, if the container has a (h, w) = (300, 600) and a margin of 10 pixels for the top, left, right, bottom, then the actual plot will have a (h, w) = (290, 590), leaving only 10 pixels around the plot for axis titles, ticks, and axis labels.
The Margin has the following shape
interface Margin {
top: number
bottom: number
left: number
right: number
}
color (string, optional)
The color of the axis lines and text, which can be overridden specifically by the axes styles.
backgroundColor (string, optional)
The color of the chart background (the whole chart, not just the plot).
svgStyle (SvgStyle, optional)
The style attributes for the main SVG element, in case you want to change those. Generally, this is not needed.The SvgStyle has the following shape
interface SvgStyle {
height?: string | number
width?: string | number
outline?: string
// any valid SVG CSS attribute
[propName: string]: any
}
seriesStyles (Map<string, SeriesLineStyle>, optional)
A map holding the data series name with an associated SeriesLineStyle. Any series listed in this map will use the associated styles for that series. Any series not in the map will use the default series styles.The SeriesLineStyle has the following shape
interface SeriesLineStyle {
color: string
lineWidth: number
// the color of the series when the user mouses over the series
highlightColor: string
// the line width of the series when the user mouses over the series
highlightWidth: number
// the line margin used for raster charts
margin?: number
}
Holds the initial (static data). This data is displayed in the chart even before subscribing to the chart-data observable. The initial data can be used to generate static charts.
initialData (Array<Series>)
An array holding the initial data series to be plotted before subscribing to the chart-data observable.The Series has the following shape
interface Series {
// the series name
readonly name: string;
// the array of time-value pairs
data: Array<Datum>;
// ... accessor functions
.
.
.
}
And the Datum has the following shape
interface Datum {
readonly time: number;
readonly value: number;
}Please note that there are a number of helper functions for creating
Series
andDatum
.
todo list the factory functions
initialData: Array<Series>
seriesFilter?: RegExp
The examples project has example code that was used to generate the charts in the images above. The StreamingRasterChart provides an example of using the raster chart. The StreamingScatterChart provides an example of using the scatter chart. Both of these examples provide controls for enabling the filtering, tooltip, tracker, and magnifier enhancements.
Each chart accepts a number of required and optional properties. The properties are divided into
Except for the plot height and width, style properties are optional. Style properties define how the plot will look. For example, the margin property defines the space between the rectangle defined by the height and width property and the plot area.
All the optional style properties have defaults (the defaults look like the example charts above). The defaults can be overridden by specifying the properties you would like to change. For example, if you would like to change only the size of the font used for the axes labels, then you can specify the property as,
<ScatterChart
// .
// .
// .
axisLabelFont={{color: 'blue'}}
// .
// .
// .
/>
In this case, the size, family, and weight of the axis labels will remain at their default values, and only the color will change from its default value to the one specified, which in this case is "blue".
The style properties common to all plots are listed in the table below.
Name | Type | Description | Example | |
---|---|---|---|---|
width | required | number | The width of the chart in pixels | 450 |
height | required | number | The height of the chart in pixels | 300 |
margin | optional | Margin | The plot margin | {top: 10, left: 10} |
axesLabelFont | optional | {size: number, color: string, family: string, weight: number} |
The font used to display the labels for the axes and ticks | {size: 14, color: '#fff'} |
backgroundColor | optional | string | The background color of the plot. Technically, this property is carried over to the SVG element holding the entire plot | '#202020' |
svgStyle | optional | css object | An object holding the React style CSS properties (i.e. camelCased) for the SVG container holding the chart. This can be used to set properties of the background. | {width: '100%'} |
The data properties define the data source, processing, and constraints.
Name | Type | Description | Example | |
---|---|---|---|---|
seriesList | required | Array of Series | A list of the series to plot. | [seriesFrom('test1')] |
seriesObservable | required | Observable of ChartData | An rxjs observable that sources chart data. | see the randomWeightDataObservable(...) function. |
windowingTime | optional | number (ms) | Controls the update frequency of the chart. Depending on the number of time-series being plotted, this number can be comfortably set at 25 ms. The default value is 100 ms | 100 |
timeWindow | required | number (ms) | The maximum time between the minTime and the maxTime . |
2000 |
These charts have been developed to be used with high-frequency dynamic data that my run for a considerable amount of time. For example, you may stream in data for a few hundred seconds, and have the plot show the last 10 seconds worth of data. To achieve this you use the timeWindow
property. Because you want to see the most recent 10 seconds of data, you set the time-window property to 10,000 ms (timeWindow={10000}
). The charts use the time-window property, and the current simulation time, to show the most recent timeWindow
milliseconds of data (in our example, the past 10 seconds). This causes the data to "slide" to the left after timeWindow
has elapsed.
The tracker, tooltip, magnifier, and filter are enhancements to the plots for exploring the displayed data. Each of these enhancements has a set of properties for determining how they are displayed. The details of the styles are given in sections below. All enhancements are optional, and, inactive by default.
The tracker, tooltip, and magnifier are activated (shown) when the mouse is in the plot area and the enhancement's visible
property is set to true. Generally, only one enhancement is used at one time.
The filter enhancement differs from the others. This isn't a visible component, rather, it controls what data is displayed in the chart. The filter allows the user to specify a regular expression that is used to filter time-series based on their name.
Name | Type | Description | Example | |
---|---|---|---|---|
tooltip | optional | TooltipStyle | Styling for the tooltip control when it is active | {visible: false, fontSize: 12, fontColor: '#d2933f'} |
magnifier | optional | RadialMagnifier or BarMagnifier | Defines the style of the radial magnifier used for the scatter chart and the bar magnifier used for the raster chart | {visible: true} |
tracker | optional | TrackerStyle | Style of the tracker line that draws a vertical line at the time represented by the current mouse position and shows that time, when the mouse is in the plot area. | {visible: false, timeWindow: 50} |
filter | optional | RexExp | A regular expression used to filter time-series based on their name. Generally, this would be specified by some control in the parent component. See for example StreamingRasterChart. | ^in[0-3]+$ |
The state properties allow you to provide callbacks when the chart state changes. There are three state changes you can plug into
boolean
By default, when the charts mount, they subscribe to the specified observable. This causes the observable to start emitting chart-data. Although the chart controls the subscription to the observable, you can control the timing of that subscription through the shouldSubscribe
property. Setting the property to false
for the initial mount tells the chart not to subscribe when it mounts. Then, at some point in time later, when you want the chart to start consuming data, simply set the shouldSubscribe
property to true
. Once the chart has subscribed to the observable, changing the value of this property has no effect.
(subscription: Subscription) => void
You hand the stream-charts
an Observable. This defines how (i.e. the pipeline) the data is generated. Only upon subscription does data flow through this pipeline. The rxjs Observable.subscribe(...)
function returns a Subscription that can be used to stop the data.
An example of an observable can be found in the randomSpikeDataObservable(...) function.
One reason to provide an onSubscription
callback is so that you have a handle on the subscription so that you can stop the data. For example, you may want to provide the user of your application a button to stop the data. Or, you may wish to stop the simulation after a certain period of time.
(seriesName: string, t: number, y: number) => void
When new data arrives from the observable, the onUpdateData
callback provides a hook into the data. For example, you may want to stop the data if, for example, the value crosses some threshold.
If you are only interested in the current time, you can use the onUpdateTime
callback.
(time: number) => void
When the time associated with the data in the stream changes, this callback provides a hook into that time. In the StreamingScatterChart, for example, this callback is used to stop the random data after 1 second (1000 ms) by cancelling the subscription.
<ScatterChart
// .
// .
// .
onSubscribe={subscription => subscriptionRef.current = subscription}
onUpdateTime={(t: number) => {
if(t > 1000) subscriptionRef.current!.unsubscribe()
}}
// .
// .
// .
/>
The axis-label font style is used as a Partial<{size: number, color: string, family: string, weight: number}>
. This means you only need to specify the values of the style that you wish to change.
Name | Type | Description | Example | Default Value |
---|---|---|---|---|
size | number | The font size in pixels | 12 |
12 |
color | string | The font color expressed as a string | 'rgba(25,25,25,0.3)' |
'#d2933f' |
family | string | The font family | 'sans-serif' |
'sans-serif' |
weight | number | The weight of the font | 350 |
300 |
The tooltip style is defined in the TooltipStyle interface. The TooltipStyle
is used as a Partial<TooltipStyle>
. This means that you only need to specify the values of the style that you wish to change.
Name | Type | Description | Example | Default Value |
---|---|---|---|---|
visible | boolean | Whether on not the tooltip control is active and will show up on mouse-over. Generally this property will be controlled by the parent component in response to the selection of the control. See, for example, StreamingScatterChart . |
false |
false |
fontSize | number | Size of the font in pixels | 12 |
12 |
fontColor | string | Color of the font represented as a string | 'blue' |
'#d2933f' |
fontFamily | string | Family of the font | '"Avenir Next" sans-serif' |
'sans-serif' |
fontWeight | number | Weight of the font represents its "thickness" | 300 |
250 |
backgroundColor | string | The background color of the tooltip | 'green ' |
'#202020' |
backgroundOpacity | number | A number between 0 and 1 defining how opaque the background is. A value of 0 means that the background is completely transparent. A value of 1 means that the background is completely opaque. | 0.5 |
0.8 |
borderColor | string | Color of the tooltip's border. | '#f8ebc6' |
'#d2933f' |
borderWidth | number | The width, in pixels, of the tooltip's border. | 3 |
1 |
borderRadius | number | The radius, in pixels, of the rectangles "corners". A value of 0 is a sharp corner. A value of 5 means that the corner is replaced by a circle of radius 5 px. | 3 |
5 |
paddingTop | number | The number of pixels between the top of the tooltip and the content. | 8 |
10 |
paddingRight | number | The number of pixels between the right side of the tooltip and the content. | 8 |
10 |
paddingLeft | number | The number of pixels between the left side of the tooltip and the content. | 8 |
10 |
paddingBottom | number | The number of pixels between the bottom of the tooltip and the content. | 8 |
10 |
Two types of magnifiers are used in stream-charts
: a radial magnifier and a bar magnifier. The radial magnifier is used in the scatter chart, and the bar magnifier is used in the raster chart.
The radial magnifier style is defined in the ScatterChart component and is used as a Partial<RadialMagnifierStyle>
, which means you only need to specify the values you wish to change.
Name | Type | Description | Example | Default Value |
---|---|---|---|---|
visible | boolean | Defines whether the magnifier is visible when the mouse is in the plot area. Generally, this would be managed by a control in the parent component. | true |
false |
radius | number | Radius, in pixels, of the magnifier lens displayed. | 125 |
100 |
magnification | number | A value between 1 and 10 that defines the amount of magnification. A value of 1 mean almost no magnification. Generally, this value should be controlled by the parent component. | 4 |
5 |
color | string | Lens axes and tick color. | 'purple' |
'#d2933f' |
lineWidth | number | Width, in pixels, of the axes and ticks. | 1 |
2 |
The bar magnifier style is defined in the RasterChart component and is used as a Partial<LineMagnifierStyle>
, which means you only need to specify the values you wish to change.
Name | Type | Description | Example | Default Value |
---|---|---|---|---|
visible | boolean | Defines whether the magnifier is visible when the mouse is in the plot area. Generally, this would be managed by a control in the parent component. | true |
false |
width | number | Width, in pixels, of the bar-magnifier lens displayed. | 50 |
125 |
magnification | number | A value between 1 and 10 that defines the amount of magnification. A value of 1 mean almost no magnification. Generally, this value should be controlled by the parent component. | 4 |
5 |
color | string | Lens axes and tick color. | 'purple' |
'#d2933f' |
lineWidth | number | Width, in pixels, of the axes and ticks. | 1 |
2 |
axisOpacity | number | A number between 0 and 1 that defines how opaque the magnifier's axes are. A value of 0 means that the axes are completely transparent. A value of 1 means that the axes are completely opaque. | 0.5 |
0.35 |
The tracker displays a vertical line in the chart area at the current mouse position, and show the time represented by that mouse position. The styles for the tracker determine how that line looks.
The tracker styles are defined in the TrackerStyle file and is used as a Partial<TrackerStyle>
, which means that you only need to specify the values you would like to change.
Name | Type | Description | Example | Default Value |
---|---|---|---|---|
visible | boolean | Defines whether the tracker is visible when the mouse is in the plot area. Generally, this would be managed by a control in the parent component. | true |
false |
color | string | Color of the tracker line. | 'rgba(55,66,77,0.88)' |
'#d2933f' |
lineWidth | number | Width, in pixels, of the vertical tracker line. | 2 |
1 |
Please note that the font style of the tracker text is controlled by the axis-label font styles described above.
This project was bootstrapped with Create React App.
Generated using TypeDoc