Skip to content

feat(charts): add the uPlot renderer alongside dygraph - #234

Open
novykh wants to merge 116 commits into
mainfrom
feat/uplot-renderer
Open

novykh wants to merge 116 commits into
mainfrom
feat/uplot-renderer

Conversation

@novykh

@novykh novykh commented Sep 1, 2026 •

Copy link
Copy Markdown
Member

Adds uPlot as a second time-series chart library, at feature parity with dygraph.

The default does not change

chartLibrary still ships as "dygraph". uPlot is opt-in — set chartLibrary: "uplot" on the SDK root, or map a single type with chartLibrariesByType.

Flipping the default later is a one-attribute change: chartLibrary is now the single selector, chartLibrariesByType defaults to {} and only overrides per-type, and getRendererForChartType falls back to chartLibrary. The flip is deliberately not in this PR — it is gated on the real-dashboard measurement protocol in docs/uplot-migration-progress.md, which has to be run in a browser on a live streaming dashboard.

What is covered

Chart types: line, area, stacked, stackedBar, multiBar, heatmap, sparkline.

Overlays, all drawn from uPlot draw hooks: alarm, alarmRange, alertTransitions, highlight, annotation, proceeded, per-series point markers, anomaly ribbon, and the anomaly-rate badge in the y-axis gutter.

Interaction: pan, wheel zoom, select, pinch, dblclick reset, click-to-annotate — using dygraph's thresholds and semantics. The crosshair lives on its own overlay canvas and is driven by the SDK's synced hoverX, not the local pointer, so cross-chart hover sync spans both renderers.

Geometry: plot area, axis ticks, borders and label widths matched to dygraph and held across resize. Collapsed y-ranges are expanded rather than left to stall uPlot's tick search.

Also in here

  • perfMonitor SDK plugin and a render-timing seam, off by default.
  • yarn perf:bench — a Playwright-driven headless dygraph-vs-uPlot benchmark, plus a browser geometry probe and a CPU profile probe under scripts/.
  • Storybook: a chart-library toolbar arg on the existing stories, a side-by-side renderer comparison story, and a showcase story.
  • src/chartLibraries/helpers/ — colour, overlay-area and dimension-visibility helpers now shared by both renderers.
  • src/helpers/deepMerge removed; it had no remaining references.

Performance

uPlot is 2.7–3.9× cheaper per render than dygraph at every CPU throttle level tested, and cheaper per frame in all 20 rendering cells of the sweep. Under saturation that converts into frames: at 6× throttle, 60±29 → 157±28 renders.

Caveats, the measurement protocol, and the earlier numbers that were retracted (everything measured before the unit-conversion rebuild fix is void) are recorded in docs/uplot-migration-progress.md.

For consumers

uplot is a new runtime dependency (~48KB), so consumers resolve it transitively — no change needed in their package.json.

The stylesheet is not an extra step: the rules uPlot needs (.u-wrap, .u-over, .u-under, .u-axis, .u-select, .u-cursor-*) are inlined in src/components/line/chartContentWrapper.js, scoped to the chart container. Upstream's remaining selectors are all legend/title chrome, and the renderer sets legend: { show: false } and no title, so uplot/dist/uPlot.min.css does not need importing.

A consumer that maps chartLibrary to its own React component must add a uplot entry. It maps to the same generic components/line component as dygraph — components/line has no renderer-specific branching, and the renderer is resolved from the chartLibrary attribute via sdk.ui. Verified in cloud-frontend: src/charts/index.js has a byType map with a dygraph key and no uplot key, and Chart does if (!Component) return null, so charts render as empty containers with no console error. So enabling uPlot there is a two-line change (uplot: Line in byType, plus the chartLibrary attribute) — not the one-attribute change docs/uplot-migration-progress.md claims.

Verification

  • Full suite green: 184 suites, 1928 passed, 2 skipped.
  • yarn build compiles clean: 537 CJS / 540 ES6.
  • Visual verification across chart types and interactions is done in Storybook via the Chart library toolbar arg.

Notes for review

Squashed from explore/uplot-spike (124 commits), which is left in place unchanged.

The docs/uplot-*.md files are the working record of the migration. uplot-migration-progress.md is the entry point; its stale sections have been corrected in a follow-up commit, with the superseded claims kept struck through rather than deleted so the history stays readable. Open parity items live in uplot-parity-worklist.md (§ "Queued work").

novykh and others added 3 commits September 30, 2026 10:50
Introduces uplot as a second time-series chart library, reaching feature
parity with dygraph across line, area, stacked, stackedBar, multiBar,
heatmap and sparkline, plus every overlay and interaction the dygraph
renderer supports.

The renderer is opt-in. The shipped default stays chartLibrary: "dygraph"
until the real-dashboard measurement in docs/uplot-migration-progress.md
has been run; flipping it is a one-attribute change because chartLibrary
is now the single selector and chartLibrariesByType only overrides
per-type.

Renderer
- Plot geometry, axis ticks, borders and label widths matched to dygraph,
  and held across resize.
- Stacking: per-value +/- accumulation for diverging stacks, correct draw
  order, ranges and mixed signs, stepped interiors, per-pixel row
  thinning.
- Overlays on draw hooks: alarm, alarmRange, alertTransitions, highlight,
  annotation, proceeded, point markers, the anomaly ribbon and the
  anomaly-rate badge in the y-axis gutter.
- Crosshair on its own overlay canvas, driven by the SDK's synced hoverX
  rather than the local pointer, so cross-chart sync spans both
  renderers.
- Navigation: pan, wheel zoom, select, pinch, dblclick reset, with
  dygraph's thresholds and click semantics.
- Smooth curves via uPlot's built-in spline; a collapsed y-range is
  expanded instead of stalling uPlot's tick search.

SDK
- chartLibrariesByType maps a chart type to a renderer, with
  getRendererForChartType and isTimeSeriesRenderer resolving through it.
- perfMonitor plugin and a render-timing seam, off by default.

Tooling
- Playwright-driven headless benchmark (yarn perf:bench), a browser
  geometry probe and a CPU profile probe.
- Storybook: a chart-library toolbar arg, a side-by-side renderer
  comparison story and a showcase story.

Measured: uPlot is 2.7-3.9x cheaper per render than dygraph at every CPU
throttle level, and cheaper per frame in all 20 rendering cells of the
sweep. Under saturation that converts into frames. Details, caveats and
the retracted earlier numbers are in docs/uplot-migration-progress.md.

Note for consumers: uplot ships as a runtime dependency (~48KB) and its
CSS (uplot/dist/uPlot.min.css) is functional, not cosmetic, so a consumer
must import it.

Squashed from the explore/uplot-spike branch.
The "Remaining work" list was written 2026-07-15 and never revised as the
work landed, so it still described heatmap, the overlays, bars and the
stacked top stroke as missing. All four are done; each is now marked with
the file:line that implements it, and the section points at the parity
worklist for genuinely open items.

Two claims were wrong rather than merely stale:

- Consumers do not need to import uplot/dist/uPlot.min.css. The rules the
  renderer actually uses are inlined in chartContentWrapper.js, and the
  only upstream selectors missing from it are legend and title chrome,
  which never render (legend is disabled, no title is set).

- The flip is not a one-attribute change. In cloud-frontend it takes
  three, two of them permanent, and two fail silently: byType needs a
  uplot entry or every chart renders as an empty container with no
  error, and getMenuChartAttributes hardcodes dygraph per-chart, which
  overrides the root attribute and makes both halves of an A/B measure
  dygraph.

Also records that Playwright is available but only drives Storybook,
refreshes the test and build counts, and drops a local absolute path.
Tests asserting formatted timestamps assumed UTC and failed on machines in
other timezones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@novykh
novykh force-pushed the feat/uplot-renderer branch from d4323dc to 0969af9 Compare September 30, 2026 07:55
novykh added 26 commits October 1, 2026 00:32
uPlot 1.6 keeps the pixel ratio on the constructor (uPlot.pxRatio), not on
chart instances, so every self.pxRatio read fell back to 1. On high-density
screens this doubled the y-axis label density and mis-sized plot budgets,
bar outlines, hover dots, zoom math and the anomaly/annotation markers.
- uPlot charts get their own series palette ([light, dark] pairs) that clears
  colour-blind separation on adjacent series; red stays reserved for alerts.
  dygraph keeps dimensionColors.js, and the container colour memo is keyed per
  palette so both renderers can sit in one container.
- Horizontal grid only, no tick marks or axis border, muted axis labels
  (themeAxisLabelColor), 2px lines and a cached area gradient.
- Y labels at least 30px apart, and ticks rounded past the scale range are
  dropped so edge labels are no longer clipped.
- Design refresh story compares both renderers; showcase payload helpers move
  to helpers/makeWavePayload for reuse.
Static Storybook mockups (Mockups/*) for the redesign: legend and hover
recipes, the modern card (attention headline, adaptive legend, quiet chrome)
and the modern treatment of the other chart types, tooltip and dropdown.
The plan records the approved decisions, the delivery contract
(designFlavour "modern") and where every current function lives.
useIsModern gates everything rendered under designFlavour "modern"; the
default and minimal flavours keep their current output. Shared tokens hold
the numerals font, radii and the adaptive legend choice by width and
series count.
Under designFlavour "modern" the d3pie chart renders an SVG donut with
gapped slices, a centre total that switches to the hovered slice, and a
legend with value and share; hovering a slice or row dims the rest. It
keeps the library's top five plus grouped remainder and follows hover,
hidden dimensions and units. The d3pie library skips its own drawing in
this flavour; default and minimal render as before.
Under designFlavour "modern" the bars chart shows ranked horizontal bars
with name, value and units and an emphasised top bar. It ranks by value
unless dimensionsSort is set, keeps hover re-ranking, hidden dimensions,
windowing and, with cols "full", non-empty anomaly rates and annotation
flags. Default and minimal keep the dimensions grid.
Under designFlavour "modern" the gauge renders as SVG: a 240 degree
rounded track, a gradient value arc that takes the zone colour inside a
warning or critical zone, zone segments on an outer ring built from
gaugeThresholds, a knob at the value, the value and units in the centre,
a sparkline of the window, min and max labels, and a Warning / Critical /
within-thresholds pill driven by the value zone and payload alerts.

The gauge library skips the canvas gauge under modern but keeps mounting,
the value range, yAxisChange and rendered events. Default and minimal
flavours keep the canvas gauge unchanged.
Under designFlavour "modern" the table dropdowns (nodes, instances,
dimensions, labels, group by) get a search field with a count
placeholder, share-of-volume bars, muted zero anomaly rates, alert
pills for non-zero counts and a footer with the selection count, Clear,
Reset and Apply. Secondary columns (Unique, Instances, Metrics, Min,
Avg, Max, Range) stay reachable behind a column toggle whose choice is
kept in the optional filterColumnVisibility attribute. Single select
dropdowns (aggregate, time aggregation, context scope) get rounded
modern items. Default and minimal flavours are unchanged.
…tention state

Under designFlavour "modern" the number chart shows the value in the numerals
font, its units, the change against the window mean, a sparkline of the window
and an attention pill when alerts are raised. Value thresholds from
gaugeThresholds colour the value. Other flavours render as before.
Under designFlavour "modern" the easy pie library skips the canvas and the
component draws a thin SVG ring with the value in the centre, using the same row
and value range as the canvas ring. The arc stays neutral unless an alert is
raised or a value threshold is crossed. Other flavours keep the canvas ring.
…malies

Under designFlavour "modern" the uPlot overlays follow the redesign
mockups; the default and minimal flavours draw exactly as before.

- New opt-in "threshold" overlay ({ warning, critical, direction }):
  dashed lines with a labelled pill on the left edge and a faint band
  above each threshold; close labels slide right; a critical value below
  the warning one bands downwards. No producer sends threshold values
  yet, so nothing draws until a consumer adds the overlay.
- Annotations hang a small flag from the plot top on a thin rule, so no
  marker covers the time axis labels.
- The anomaly ribbon becomes a strip of small rounded marks along the
  top edge whose opacity follows the anomaly rate.
- Alarm, alarm range and alert transition overlays use themed alert
  colours with thinner lines and lighter tints.
- Alert colours are new [light, dark] theme attributes.
Under designFlavour "modern" the optional focusedDimensionId attribute
fades every other series (lines, areas, stacked and bars) so the series
under the legend pointer stands out. Other flavours ignore it.
Under designFlavour "modern" the line card picks its legend from the card
width and series count: labels at the line ends, one wrapping line, a side
table (Last or At cursor, Mean, Max, anomaly rate; sortable headers,
scrolling with a fade) or none on tiles. A visible legend follows the
hovered point and replaces the popover; without one a compact tooltip
shows up to six sorted rows, anomaly and annotation columns only when
non-empty, and the granularity. Hovering an entry focuses its series.

Click and Shift/Ctrl/Cmd+click toggling, dimension sort, the heatmap
legend, highlight range zoom, latest/hovered time and the drawer
("Compare, drill down, correlate") stay reachable. The optional
legendLayout attribute forces a layout. Default and minimal flavours
render as before.
Under designFlavour "modern" the line card header becomes a title row with
hover-revealed actions (filters, the existing toolbox elements, More), a
one-line scope summary that opens the existing FilterToolbox and carries the
loading, error and no-data states, and an attention headline built from the
alert summary and alarm overlays.

The More menu holds the navigation modes, zoom in/out/reset, the anomaly and
annotation layers, dimension sort, the existing settings tabs, chart info and
reload. The floating navigation toolbox gives way to a reset chip while the
window is zoomed. Settings content accepts an optional initial tab.

Default and minimal flavours render as before.
Under designFlavour "modern" the table shows a status dot per row from the node or instance alert counts, a small meter in percentage cells, a trend line in the other value cells, calmer row rules and a stronger sorted header. Group boxes draw rounded boxes from a single-hue sequential scale, lay groups out as labelled rows, show a stepped scale legend and highlight values at or above the new optional groupBoxesThreshold attribute in the error colour.

The default and minimal flavours render as before.
The modern header opens the filter bar from its scope line, so the line card
no longer renders the fixed bar as well. Without a header (hasHeader false)
the fixed bar stays, so filters remain reachable.
Readouts use 0-2 decimals by converted magnitude instead of the auto-scaled maximum of 4, and keep staticFractionDigits when the user set it.
novykh added 23 commits October 1, 2026 13:23
@novykh
novykh force-pushed the feat/uplot-renderer branch from 61b029d to 33db114 Compare October 2, 2026 19:40
kapantzak
kapantzak previously approved these changes Oct 2, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants