An exceptionally fast, tiny (< 15 KB min) time series & line chart (MIT Licensed)
ΞΌPlot is a fast, memory-efficient time series & line chart based on Canvas 2D; from a cold start it can create an interactive chart containing 150,000 data points in 40ms, scaling linearly at ~4,000 pts/ms. In addition to fast initial render, the zooming and cursor performance is by far the best of any similar charting lib; at < 15 KB, it's likely the smallest and fastest time series plotter that doesn't make use of WebGL shaders or WASM, both of which have much higher startup cost and code size.
166,650 point bench: https://leeoniya.github.io/uPlot/bench/uPlot.html
2019-10-24: ΞΌPlot is now mostly feature-complete and its declarative opts API is in pretty good, future-accommodating shape. Its imperative API, docs and additional examples are still in progress.
v1.0 and API stabilization are loosely targetted for sometime before 2020-01-01. Until then, feedback, feature suggestions and real use-cases can be submitted to the issue tracker for consideration & further discussion.
- Multiple series w/toggle
- Multiple y-axes, scales & grids
- Temporal or numeric x-axis
- Line & Area styles (color, fill, width, dash)
- Zoom with auto-rescale
- Legend with live values
- Support for IANA Time Zone Names & DST
- Support for missing data
- Cursor sync for multiple charts
- Focus closest series
- Data streaming (live update)
- High / Low bands
- WIP: Range, point & line annotations
- WIP: .resize()
In order to stay lean, fast and focused the following features will not be added:
- No data parsing, aggregation, summation or statistical processing - just do it in advance. e.g. https://simplestatistics.org/, https://www.papaparse.com/
- No transitions or animations - they're always pure distractions.
- No DOM measuring; uPlot does not know how much space your dynamic labels & values will occupy, so requires explicit sizing and/or some CSS authoring.
- No stacked series or line smoothing. See links for how these are each terrible at actually communicating information.
- Probably no drag scrolling/panning. Maintaining good perf with huge datasets would require a lot of extra code & multiple
<canvas>elements to avoid continuous redraw and rescaling on each dragged pixel. However, since uPlot's performance allows rendering of very wide canvases, they can be scrolled naturally with CSS'soverflow-x: autoapplied to a narrower containing element. Pagination of data also works well.
Example: https://jsfiddle.net/4o0ge9wx/
<link rel="stylesheet" href="src/uPlot.css">
<script src="dist/uPlot.iife.min.js"></script>
<script>
const data = [
[1566453600, 1566457260, 1566460860, 1566464460], // Unix timestamps
[0.54, 0.15, 3.27, 7.51 ], // CPU
[12.85, 13.21, 13.65, 14.01 ], // RAM
[0.52, 1.25, 0.75, 3.62 ], // TCP Out
];
const opts = {
width: 800,
height: 400,
series: {
y: [
{
label: "CPU",
scale: "%",
value: v => v.toFixed(1) + "%",
color: "red",
width: 2,
dash: [10, 5],
},
{
label: "RAM",
scale: "%",
value: v => v.toFixed(1) + "%",
color: "blue",
},
{
label: "TCP Out",
scale: "mb",
value: v => v.toFixed(2) + "MB",
color: "green",
}
],
},
axes: {
y: [
{
scale: '%',
values: (vals, space) => vals.map(v => +v.toFixed(1) + "%"),
},
{
side: 3,
scale: 'mb',
values: (vals, space) => vals.map(v => +v.toFixed(2) + "MB"),
grid: null,
},
],
},
};
let uplot = new uPlot.Line(opts, data);
document.body.appendChild(uplot.root);
</script><link rel="stylesheet" href="src/uPlot.css">
<script src="dist/uPlot.iife.min.js"></script>let data = [
[1546300800, 1546387200], // x-values (timestamps)
[ 35, 71], // y-values (series 1)
[ 90, 15], // y-values (series 2)
];uPlot expects a columnar data format as shown above.
- x-values must be numbers, unique, and in ascending order.
- y-values must be numbers (or
nulls for missing data). - x-values and y-values arrays must be of equal lengths.
By default, x-values are assumed to be unix timestamps but can be treated as plain numbers via scales.x.time = false.
This format has implications that can make uPlot an awkward choice for multi-series datasets which cannot be easily aligned along their x-values.
If one series is data-dense and the other is sparse, then the latter will need to be filled in with mostly null y-values.
If each series has data at arbitrary x-values, then the x-values array must be augmented with all x-values, and all y-values arrays must be augmented with nulls, potentially leading to exponential growth in dataset size, and a structure consisting of mostly nulls.
This does not mean that all series must have identical x-values - just that they are alignable. For instance, it is possible to plot series that express different time periods, because the data is equally spaced.
Before choosing uPlot, ensure your data can conform to these requirements.
let opts = {
title: "My Chart",
id: "chart1",
class: "my-chart",
width: 800,
height: 600,
spanGaps: false,
series: {
y: [
{
// in-legend display
label: "RAM",
value: rawValue => "$" + rawValue.toFixed(2),
// series style
color: "red",
width: 1,
fill: "rgba(255, 0, 0, 0.3)",
dash: [10, 5],
}
]
}
};
let uplot = new uPlot.Line(opts, data);
document.body.appendChild(uplot.root);idandclassare optional HTML attributes to set on the chart's container<div>(uplot.root).widthandheightare required dimensions in logical [CSS] pixels of the plotting area & axes, but excludingtitleorlegenddimensions (which can be variable based on user CSS).spanGapscan be set totrueto connectnulldata points.- For a series to be rendered, it must be specified in the opts; simply having it in the data is insufficient.
- All series' options are optional;
labelwill default to "Value" andcolorwill default to "black". - Series' line
widthis specified in physical [device] pixels (e.g. on high-DPI displays with a pixel ratio = 2,width: 1will draw a line with an effective width of 0.5 logical [CSS] pixels). color,width,fill, anddashmap directly to Canvas API's ctx.strokeStyle, ctx.lineWidth, ctx.fillStyle, and ctx.setLineDash.
uPlot's API strives for brevity, uniformity and logical consistency.
Understanding the roles and processing order of data, series, scales, and axes will help with the remaining topics. The high-level rendering flow is this:
datais the first input into the system.serieshold the config of each dataset, such as visibility, styling, labels & value display in the legend, and thescalekey along which they should be drawn. Implicit scale keys arexfor thedata[0]series andyfordata[1..N].scalesreflect the min/max ranges visible within the view. All view range adjustments such as zooming and pagination are done here. If not explicitly set via opts,scalesare automatically initialized using theseriesconfig and auto-ranged using the provideddata.axesrender the ticks, values, labels and grid along theirscale. Tick & grid spacing, value granularity & formatting, timezone & DST handling is done here.
Benchmarks done on a ThinkPad T480S:
- Windows 10 x64, Chrome 78.0.3904.70
- Core i5-8350U @ 1.70GHz, 8GB RAM
- Intel HD 620 GPU, 2560x1440 res
| Bench Demo | Size (min) | Render (167k) | Total | Mem (peak) | Mem (retained) | Interact (10s) |
|---|---|---|---|---|---|---|
| uPlot | 15 KB | 39 ms | 71 ms | 19.6 MB | 3.7 MB | 154 ms |
| Flot | 172 KB | 130 ms | 190 ms | 42.7 MB | 17.3 MB | -- |
| dygraphs | 121 KB | 168 ms | 251 ms | 113 MB | 66.0 MB | 2569 ms |
| CanvasJS | 448 KB | 295 ms | 414 ms | 49.2 MB | 39.1 MB | 2401 ms |
| LightningChart | 883 KB | -- | 500 ms | 43.5 MB | 21.5 MB | 9446 ms |
| jqChart | 270 KB | 450 ms | 577 ms | 142 MB | 99.9 MB | 600 ms |
| Highcharts | 270 KB | -- | 717 ms | 71.7 MB | 40.7 MB | 1122 ms |
| ECharts | 734 KB | 513 ms | 765 ms | 179 MB | 118.8 MB | 2194 ms |
| Chart.js | 239 KB | 653 ms | 741 ms | 117 MB | 78.9 MB | 5408 ms |
| ApexCharts | 430 KB | 1269 ms | 2441 ms | 142 MB | 157.9 MB | 7559 ms |
| ZingChart | 682 KB | 2324 ms | 2518 ms | 220 MB | 175.7 MB | -- |
| amCharts | 1,034 KB | 6514 ms | 6730 ms | 397 MB | 430.0 MB | 7539 ms |
| Chartist.js | -- | -- | -- | -- | -- | -- |
| C3.js (d3-based) | -- | -- | -- | -- | -- | -- |
| dc.js (d3-based) | -- | -- | -- | -- | -- | -- |
| Plotly (d3-based) | -- | -- | -- | -- | -- | -- |
| MetricsGraphics (d3-based) | -- | -- | -- | -- | -- | -- |
| rickshaw (d3-based) | -- | -- | -- | -- | -- | -- |
Chrome 78.0.3904.108 (2019-11-23)
rend js sys size heap max heap ret
----------------------------------------------------------------------
uPlot 40 ms 72 ms 71 ms 15 KB 19.7 MB 3.8 MB
dygraphs 180 ms 241 ms 183 ms 123 KB 113.0 MB 64.1 MB
Flot 338 ms 182 ms 290 ms 172 KB 43.5 MB 17.2 MB
CanvasJS 327 ms 374 ms 60 ms 448 KB 48.5 MB 38.2 MB
LightningChart --- ms 490 ms 78 ms 883 KB 42.4 MB 21.1 MB
jqChart 506 ms 574 ms 96 ms 269 KB 134.0 MB 100.3 MB
Highcharts --- ms 707 ms 62 ms 272 KB 80.4 MB 77.2 MB
Chart.js 650 ms 700 ms 178 ms 239 KB 141.0 MB 122.2 MB
ECharts 515 ms 769 ms 1043 ms 734 KB 182.0 MB 125.0 MB
ApexCharts 1255 ms 2360 ms 67 ms 435 KB 159.4 MB 159.4 MB
ZingChart 5820 ms 5957 ms 72 ms 690 KB 182.0 MB 162.8 MB
amCharts 6732 ms 6697 ms 88 ms 1024 KB 405.8 MB 405.8 MB
- Dan Vanderkam's dygraphs was a big inspiration; in fact, my stale pull request #948 was a primary motivator for ΞΌPlot's inception.
- Adam Pearce for #15 - remove redundant lineTo commands.