<link rel="stylesheet" href="dist/uPlot.min.css">
<script src="dist/uPlot.iife.min.js"></script>
uPlot supports aligned data (mode: 1, the default) and per-series faceted data (mode: 2).
The example and alignment requirements below describe mode 1.
Data Modes compares their data shapes, facet configuration, ordering, cursor behavior, selection, and automatic ranging.
let data = [
[1546300800, 1546387200], // x-values (timestamps)
[ 35, 71], // y-values (series 1)
[ 90, 15], // y-values (series 2)
];
Mode 1 uses the columnar data format shown above.
nulls for missing data).By default, x-values are assumed to be unix timestamps (seconds since 1970-01-01 00:00:00) but can be treated as plain numbers via scales.x.time = false.
JavaScript uses millisecond-precision timestamps, but this precision is rarely necessary on calendar-aware time: true scales/plots, which honor DST, timezones, leap years, etc.
For sub-second periods, itās recommended to set time: false and simply use ms offsets from 0.
If you truly need calendar-aware ms level precision, simply provide the timestamps as floats, e.g. 1575354886.419.
More infoā¦.
Alignment can increase storage for series with different X values.
Mode 1 needs the union of their X values and corresponding null entries in each Y array.
Mode 2 retains separate X arrays and avoids cross-series null padding, but its interaction and ranging behavior differs.
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.
For unaligned or unordered data, see mode 2 and its current behavior.
let opts = {
title: "My Chart",
id: "chart1",
class: "my-chart",
width: 800,
height: 600,
series: [
{},
{
// initial toggled state (optional)
show: true,
spanGaps: false,
// in-legend display
label: "RAM",
value: (self, rawValue) => rawValue == null ? '' : "$" + rawValue.toFixed(2),
// series style
stroke: "red",
width: 1,
fill: "rgba(255, 0, 0, 0.3)",
dash: [10, 5],
}
],
};
let uplot = new uPlot(opts, data, document.body);
id and class are optional HTML attributes to set on the chartās container <div> (uplot.root).width and height are required dimensions in plotting area, axes & ticks, but excluding title or legend dimensions (which can be variable based on user CSS).spanGaps can be set to true to connect null data points.label will default to āValueā and stroke will default to āblackā.width is the seriesā line width in CSS pixels.stroke, width, fill, and dash map directly to Canvas APIās ctx.strokeStyle, ctx.lineWidth, ctx.fillStyle, and ctx.setLineDash.The dom options control which chart elements uPlot creates. All flags default to true and apply only during construction.
| Option | Effect of false |
|---|---|
dom.over |
Omits .u-over. Disables the built-in cursor, hover points, local plot mouse interaction, and over-layer selection. |
dom.under |
Omits .u-under and disables under-layer selection. |
dom.uplot |
Makes the canvas the root. Suppresses all other built-in DOM, including the title, legend, cursor, selection, and axis rectangles. |
axes[i].dom |
Omits that axisās .u-axis rectangle. Preserves axis drawing and layout. |
Selection requires its chosen layer: select.over: true uses .u-over, and false uses .u-under. uPlot does not move selection to another layer.
Cursor and series synchronization remain available without .u-over. Existing synchronization filters still apply.
A canvas-only chart can still show axes:
const opts = {
width: 300,
height: 120,
dom: { uplot: false },
scales: { x: { time: false } },
series: [{}, { stroke: "blue" }],
};
For a sparkline without axes, the additional option is axes: [{ show: false }, { show: false }].
With dom.uplot: false, u.root === u.ctx.canvas. The canvas retains the .uplot class, id, and custom class.
setSize(), setPxRatio(), and destroy() operate on that canvas.
An interactive sparkline can retain only the root, canvas, and overlay:
const opts = {
width: 150,
height: 30,
dom: { under: false },
legend: { show: false },
select: { show: false },
axes: [{ show: false }, { show: false }],
scales: { x: { time: false } },
series: [{}, { stroke: "blue" }],
};
When no title or visible legend exists, the root div also serves as .u-wrap. This removes the nested wrapper even without explicit dom options.
A missing overlay or underlay has the value null in u.over or u.under.
Without .u-over, u.rect returns null without measuring the canvas or firing a syncRect hook.
With .u-over, rectangle measurement is unchanged. Only cursor-enabled charts expose u.syncRect() and invalidate the rectangle cache on scroll and window resize.
Plugin opts callbacks run in plugin order on a copy of the constructor options, before option-dependent initialization.
Each callback can modify the options or return replacement options for the next callback.
The provisional self object has uid and status, but no chart DOM, canvas context, or chart methods.
The constructor uses the final options to initialize that same object and create the DOM.
Use hooks.init for code that needs the chart DOM or methods, including code previously in an opts callback.
For a merged root, u.root.querySelector(".u-wrap") returns null because the root itself is the wrapper.
A root-inclusive lookup is u.root.matches(".u-wrap") ? u.root : u.root.querySelector(".u-wrap").
Borders and padding belong on an external host, not the merged root or canvas.
High/Low bands are defined by two adjacent data series in low,high order and matching opts with series.band = true.
const opts = {
series: [
{},
{
label: "Low",
fill: "rgba(0, 255, 0, .2)",
band: true,
},
{
label: "High",
fill: "rgba(0, 255, 0, .2)",
band: true,
},
],
};
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:
data is the first input into the system.series holds the config of each dataset, such as visibility, styling, labels & value display in the legend, and the scale key along which they should be drawn. Implicit scale keys are x for the data[0] series and y for data[1..N].scales reflect 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, scales are automatically initialized using the series config and auto-ranged using the provided data.axes render the ticks, values, labels and grid along their scale. Tick & grid spacing, value granularity & formatting, timezone & DST handling is done here.You may have noticed in the previous examples that series and axes arrays begin with {}.
This represents options/overrides for the x series and axis.
They are required due to the way uPlot sets defaults:
data[0], series[0] and axes[0] represent & inherit x defaults, e.g:
"x" scale w/ auto: falsedata[1..N], series[1..N] and axes[1..N] represent & inherit y defaults, e.g:
"y" scale w/ auto: trueWhile somewhat unusual, keeping x & y opts in flat arrays [rather than splitting them] serves several purposes:
series[i] maps to data[i]i into the arrays without needing further contextSeries with differing units can be plotted along additional scales and display corresponding y-axes.
series.scale key.axis with the scale key.let opts = {
series: [
{},
{
label: "CPU",
stroke: "red",
scale: "%",
value: (self, rawValue) => rawValue == null ? '' : rawValue.toFixed(1) + "%",
}
{
label: "RAM",
stroke: "blue",
scale: "%",
value: (self, rawValue) => rawValue == null ? '' : rawValue.toFixed(1) + "%",
},
{
label: "TCP",
stroke: "green",
scale: "mb",
value: (self, rawValue) => rawValue == null ? '' : rawValue.toFixed(2) + "MB",
},
],
axes: [
{},
{
scale: "%",
values: (self, ticks) => ticks.map(rawValue => rawValue.toFixed(1) + "%"),
},
{
scale: "mb",
values: (self, ticks) => ticks.map(rawValue => rawValue.toFixed(2) + "MB"),
side: 1,
grid: {show: false},
},
],
};
side is the where to place the axis (0: top, 1: right, 2: bottom, 3: left).Sometimes itās useful to provide an additional axis to display alternate units, e.g. °F / °C. This is done using dependent scales.
let opts = {
series: [
{},
{
label: "Temp",
stroke: "red",
scale: "F",
},
],
axes: [
{},
{
scale: "F",
values: (self, ticks) => ticks.map(rawValue => rawValue + "° F"),
},
{
scale: "C",
values: (self, ticks) => ticks.map(rawValue => rawValue + "° C"),
side: 1,
grid: {show: false},
}
],
scales: {
"C": {
from: "F",
range: (self, fromMin, fromMax) => [
(fromMin - 32) * 5/9,
(fromMax - 32) * 5/9,
],
}
},
from specifies the scale on which this one depends.range converts fromās min/max into this oneās min/max.If a scale does not need auto-ranging from the visible data, you can provide static min/max values. This is also a performance optimization, since the data does not need to be scanned on every view change.
let opts = {
scales: {
"%": {
auto: false,
range: [0, 100],
}
},
}
u.setRange(scaleKey, min, max) sets concrete bounds and bypasses scale.range().
u.setScale() accepts object bounds. Null bounds request scale calculation.
A cursor.drag.setRange callback can adjust the bounds from built-in drag zoom.
It receives ordered min and max bounds. Return an adjusted [min, max] tuple, or return null to cancel the scale change.
Other scale operations do not call this callback.
let opts = {
cursor: {
drag: {
x: true,
setRange: (u, scaleKey, min, max) => {
if (scaleKey != "x")
return [min, max];
return [
Math.floor(min / 10) * 10,
Math.ceil(max / 10) * 10,
];
},
},
},
};
The default x scale is temporal, but can be switched to plain numbers. This can be used to plot functions.
let opts = {
scales: {
"x": {
time: false,
}
},
}
A scaleās default distribution is linear distr: 1, but can be switched to indexed/evenly-spaced.
This is useful when youād like to squash periods with no data, such as weekends.
Keep in mind that this will prevent logical temporal tick baselines such as start of day or start of month.
let opts = {
scales: {
"x": {
distr: 2,
}
},
}
Range.Config controls ordinary numeric ranging through uPlot.rangeNum() and tick-aware numeric ranging through scale.axis.
Both rangers use the same anchor precedence:
zeroIf.Each side accepts hard, soft, and pad. There is no mode property or implicit soft: 0.
An omitted or null soft supplies no soft anchor. Each side defaults to pad: 0.1.
An explicit soft is an exact preferred endpoint, subject to hard limits:
min.soft is active exactly when rawMin >= min.soft.max.soft is active exactly when rawMax <= max.soft.If data crosses a soft endpoint, that anchor becomes inactive and normal padding applies, subject to hard limits and zeroIf.
An active anchor overrides padding on its side. Soft activation does not depend on padded bounds or tick positions.
For example, min.soft: 10 anchors the minimum at 10 for raw data [20, 100].
For raw data [5, 100], the same soft anchor is inactive.
The top-level zeroIf defaults to 0.1 when omitted or null. A value of 0 disables proximity anchoring.
The proximity rule uses raw extrema and rawSpan = rawMax - rawMin, before padding, hard clipping, or flat-range normalization:
rawMin <= zeroIf * rawSpan.-rawMax <= zeroIf * rawSpan.Hard limits and active explicit soft anchors take precedence over zero proximity.
Null soft limits do not disable zeroIf. Zero can still occur through ordinary padding or tick selection when proximity anchoring is disabled.
const range = {
min: { soft: null, pad: 0.1 },
max: { soft: 100, pad: 0.1 },
zeroIf: 0.1,
};
Tick-aware ranging resolves anchors before one tick-grid selection. It has no natural-grid prepass or retry to determine soft activation.
Anchored sides ignore even huge padding values whose multiplication by the raw span overflows. Unanchored padding requirements still apply.
Active endpoints must align with the selected built-in increment for tick-aware ranging.
For flat zero data, an explicit upper zero endpoint uses a negative fallback. Explicit zero endpoints on both sides retain the positive fallback.
The positional rangeNum(min, max, mult, zeroAffinity) form uses mult as padding. Its zeroAffinity flag enables or disables the default zero affinity.
The axis-ranging policy explains tick-aware padding and unsupported inputs.
Range.SoftMode and Range.Limit.mode are removed. The following changes apply to range configuration:
| Previous configuration | Migration |
|---|---|
mode: 0 |
Remove mode and omit soft, or set soft: null. |
mode: 1 |
Remove mode and retain soft. |
mode: 2 or mode: 3, with nonzero soft |
There is no direct equivalent. If the old conditional behavior is necessary, use a custom range function. |
Old default soft: 0, mode: 3 |
Remove both fields. The default zeroIf: 0.1 replaces this policy. |
The new zero-proximity policy uses raw data, not padded bounds or a natural tick grid. It does not promise identical legacy bounds.
An explicit soft: 0 now requests a preferred zero endpoint whenever data stays on that side of zero, regardless of distance.
If the old configuration used mode: 0 to avoid zero anchoring, also set zeroIf: 0.
This disables proximity anchoring, but does not forbid a zero endpoint from padding or tick selection.
Most options are self-explanatory:
let opts = {
axes: [
{},
{
show: true,
label: "Population",
labelSize: 30,
labelFont: "bold 12px Arial",
font: "12px Arial",
gap: 5,
size: 50,
stroke: "red",
grid: {
show: true,
stroke: "#eee",
width: 2,
dash: [],
},
ticks: {
show: true,
stroke: "#eee",
width: 2,
dash: [],
size: 10,
}
}
]
}
size & labelSize represent the perpendicular dimensions assigned to values and labels DOM elements, respectively. In the above example, the full width of this y-axis would be 30 + 50; for an x-axis, it would be its height.gap is the space between axis ticks and values.Customizing the tick/grid spacing, value formatting and granularity is somewhat more involved:
let opts = {
axes: [
{
space: 40,
incrs: [
// minute divisors (# of secs)
1,
5,
10,
15,
30,
// hour divisors
60,
60 * 5,
60 * 10,
60 * 15,
60 * 30,
// day divisors
3600,
// ...
],
// [0]: minimum num secs in found axis split (tick incr)
// [1]: default tick format
// [2-7]: rollover tick formats
// [8]: mode: 0: replace [1] -> [2-7], 1: concat [1] + [2-7]
values: [
// tick incr default year month day hour min sec mode
[3600 * 24 * 365, "{YYYY}", null, null, null, null, null, null, 1],
[3600 * 24 * 28, "{MMM}", "\n{YYYY}", null, null, null, null, null, 1],
[3600 * 24, "{M}/{D}", "\n{YYYY}", null, null, null, null, null, 1],
[3600, "{h}{aa}", "\n{M}/{D}/{YY}", null, "\n{M}/{D}", null, null, null, 1],
[60, "{h}:{mm}{aa}", "\n{M}/{D}/{YY}", null, "\n{M}/{D}", null, null, null, 1],
[1, ":{ss}", "\n{M}/{D}/{YY} {h}:{mm}{aa}", null, "\n{M}/{D} {h}:{mm}{aa}", null, "\n{h}:{mm}{aa}", null, 1],
[0.001, ":{ss}.{fff}", "\n{M}/{D}/{YY} {h}:{mm}{aa}", null, "\n{M}/{D} {h}:{mm}{aa}", null, "\n{h}:{mm}{aa}", null, 1],
],
// splits:
}
],
}
space is the tick selection target in CSS pixels. A smaller target selects smaller divisors. It also accepts (self, axisIdx, scaleMin, scaleMax, dim) => space, where dim is the plot dimension along the axis in CSS pixels. Horizontal selection uses the provisional width. Final spacing can be smaller after overflow padding.incrs are divisors available for segmenting the axis to produce ticks. can also be a function of the form (self) => divisors.nice controls calendar adjustments in the built-in time-axis splits. Its defaults are {dst: true, first: false}.
dst: true realigns multi-hour ticks to a local-midnight cadence across DST transitions. false keeps uniform elapsed-time intervals. Day-based ticks retain their local-midnight correction with either setting.first: true enforces month-start ticks for multi-day increments and removes a preceding tick if the spacing is too small. false continues the cadence across month boundaries without forcing a tick on the first. Monthly and yearly increments are unchanged.values can be:
(self, ticks, space) => values where ticks is an array of raw values along the axisā scale, space is the determined tick spacing in CSS pixels and values is an array of formatted tick labels.Layout uses a fixed order without convergence cycles. The order depends on whether a horizontal axis uses size: 'auto'.
Without automatic horizontal axes:
axis.size callback once with (self, null, axisIdx). These heights must include space for rotation, truncation, and multiline labels.With automatic horizontal axes:
axis.size callbacks run once and still receive (self, null, axisIdx), not the selected labels.In both orders, each vertical axis.size callback receives (self, values, axisIdx) once. values contains the formatted labels, or [] if there are no ticks. Vertical size callbacks never receive null.
Automatic sizing: size: 'auto' reserves the tick size, gap, and selected label extent without extra axis padding. Automatic sizing and tuple overflow support only default alignment (align: 0) and alignTo: 1. They support unrotated multiline labels and single-line rotations on bottom axes. Numeric sizes and size callbacks retain custom alignment support.
Padding: Each side accepts only a number, null, or a [min, max] tuple, in top/right/bottom/left order. Numbers reserve fixed padding in CSS pixels. The default null retains the existing heuristic, including with automatic axis sizes.
Before tick selection, baseline padding reserves numeric values, heuristic values for null, or tuple minima. Top and bottom tuples use only their minima. After horizontal label selection and formatting, uPlot resolves left and right tuple overflow together with the final vertical axis widths. It credits existing axis space and limits the padding to each tupleās bounds. A finite maximum can allow label clipping.
Top and bottom padding stay fixed after ticks. Overflow does not select or format ticks again, so final horizontal spacing can be smaller than the axis.space target.
Plot dimensions must remain positive after axis sizes and padding. Behavior for nonpositive plot dimensions is undefined.
Side participation: sidesWithAxes derives from axis configuration at initialization, not from size callback results. Hidden or inactive axes do not participate. For visible, active axes:
size, size: 'auto', or a size callback makes the axis participate.size: 0 does not make the axis participate, unless an axis title has nonzero labelSize.A zero return from a size callback no longer changes side occupancy for default padding.
For a grid-only axis, use numeric size: 0 without an axis title reservation. To hide the axis fully, including its grid, use show: false. For other padding behavior, set explicit numbers or bounds.
Compatibility: axis.size no longer receives cycleNum. Its signature remains (self, values: Axis.StaticValues | null, axisIdx) => number because horizontal callbacks receive null. Axis.StaticValues can contain strings, numbers, and null entries.
Padding callbacks and the PaddingPhase enum no longer exist. There are no public padding phases or tick-aware custom padding callbacks.
For migration, replace padding callbacks with numbers or [min, max] bounds. The axis autosize demo shows automatic sizing and tuple padding.
Layout commit: setSize() updates self.width and self.height immediately. Until commit, self.bbox, coordinate transforms, and DOM geometry retain the previous completed layout. Before the initial commit, all self.bbox fields are zero. batch() completes its pending layout synchronously before it returns. A previously queued commit does not draw again after this flush.
setPxRatio() updates self.pxRatio immediately. Font scaling and path invalidation use the final requested ratio at commit. Cancelled ratio changes retain the existing fonts and paths unless another change invalidates them.
Identical setSize() or setPxRatio() requests are no-ops. For an explicit axis and layout refresh, call redraw(false, true). The setSize hook still reports outer size updates and internal plot geometry or axis changes. Identical requests do not trigger this hook.
redraw(true, true) also rebuilds series paths and refreshes automatic ranges. Built-in scanners reuse valid extrema caches when data and the visible window are unchanged. Pending data or X-range updates retain their normal invalidation behavior. Custom scanner and range callbacks still run. After any data change, including in-place array edits, call setData() rather than only redraw().
Multiple size changes before a commit produce one setSize notification, even if the final size matches the previous layout.
Canvas state: uPlot invalidates its canvas state cache only when it resets the canvas backing store. Callbacks must preserve self.ctx state. The demo saves and restores the canvas state around font changes to preserve the font cache.
cache selects which categories to retain after rendering. Omitted categories default to true.
cache: { paths: false, data: false }
This configuration discards both categories after rendering. cache: { data: false } discards data but retains paths.
clearCache() discards all categories immediately without scheduling a redraw. An explicit selector discards only categories set to true:
u.clearCache({ paths: true }); // Paths only.
u.clearCache({ data: true }); // Data only.
u.clearCache({}); // Nothing.
Data disposal replaces retained columns with empty arrays while preserving the dataset structure, including mode-2 facets. It also releases ordinal index arrays and the internal opts.data copy. It does not mutate caller-owned arrays, clear the canvas, or reset scale bounds and extrema. A later setData() can supply new data for another render.
Data disposal is for non-interactive charts such as sparklines. Cursor interaction, legend toggling, resize, pixel-ratio updates, and other data-dependent features must be disabled. The implementation does not enforce these restrictions yet.
Manual data disposal must occur after rendering. Automatic disposal normally completes before ready, but retains data needed by pending render work. With cache.data: false, hooks must not call setData(). Plugins and callbacks can retain their own references, which this API cannot release.
destroy() releases data and path caches regardless of the cache configuration.
This includes raw data, cumulative stack data, stack baselines (_base), the internal opts.data copy, and series and point paths.
Destroy hooks see empty data and cleared path caches. Small axis, legend, and cursor state remains available until garbage collection.
Cleanup does not mutate caller-owned arrays. Repeated destroy() calls have no effect. A destroyed chart cannot be reused.
Default DOM hover markers share the built-in canvas marker position calculation. This calculation uses the bitmap plot rectangle, series.pxAlign, and the canvas point stroke offset.
The DOM position uses the completed canvas-to-CSS scale, without another CSS-pixel rounding step. Hover centers can therefore have fractional CSS coordinates. Stationary markers update after external resize, axis layout changes, and pixel-ratio changes. Pending size or pixel-ratio requests do not change their displayed geometry before commit.
The alignment target is the canvas marker center, not the series line. Different point and line stroke widths can produce different drawing offsets. CSS circles and canvas arcs can still differ in antialiasing and border appearance.
cursor.points.bbox overrides automatic center alignment and size. Its existing CSS positioning behavior remains unchanged.
Custom point paths with different placement require an explicit bounding-box callback. The default calculation cannot infer arbitrary path geometry.
This change addresses the coordinate mismatch reported in #1108 and #779.