Skip to main content
A command is a single, undoable edit to a chart’s spec — “hide the y-axis”, “widen the line”, “move the legend to the bottom”. Instead of rebuilding the spec and passing a new input, you dispatch a command and the provider applies it, recompiles, and records it on an undo stack.
Every edit that reaches the chart goes through this one path — a toolbar button, an inline edit in editable mode, or a command streamed in from an agent — so all of them land in the same history and all of them are undoable.

Dispatching

useGraphCommands returns dispatch(command, options?) and seal(). It must be called from inside a <GraphProvider>. A command is a plain object you construct with its params:
Commands that target a layer take an optional layerId. Omit it and the command finds the first layer of a compatible geom — enough for a single-series chart, while a combo chart should name the layer explicitly:
Nothing is mutated. A command reads the current spec and returns a new one, so the provider can swap the compiled result in without touching the object you passed as input.

Persisting edits

The provider’s onChange fires with the spec input a command produced. Store that if edits need to survive a reload:
Handing that same value straight back as input is a no-op — the provider recognises the echo and does not recompile.

Live gestures

A slider or colour picker that edits while it is held would otherwise leave one undo entry — and one onChange — per frame. Pass { transient: true } on those frames and call seal() when the gesture ends:
A transient dispatch recompiles and repaints like any other, but it replaces the run’s single undo entry instead of pushing a new one, and keeps the run’s oldest revert — so one undo returns to before the gesture began, not to its penultimate frame. onChange fires once, when seal() closes the run. A run covers one thing being edited. A transient dispatch aimed at something else starts its own entry, and so does a committed dispatch, an undo, a redo, or a new input, data or theme from the host — each of those closes the open run first. Forgetting to seal() delays that gesture’s onChange until one of them arrives; it does not merge two gestures into one step. Dragging something across the canvas is a different shape: move it locally in pixels and dispatch one command on drop. Transient dispatch is for gestures whose feedback genuinely needs the spec to recompile.

Undo & redo

useGraphHistory gives you the controls plus the state a history UI reads. It subscribes to the stack, so a component using it re-renders whenever a step becomes available.
boolean
Whether a step is available in each direction.
string | null
Human-readable label for the next step — "Set line width to 3" — for a button tooltip or a menu item. null when there is nothing to step.
CommandMetadata[]
The full history, for a history panel. Each entry carries id, timestamp, description and author. Both are oldest first, and in both it is the last entry that is next: the end of undoStack is what an undo reverses, the end of redoStack is what a redo re-applies.

Keyboard shortcuts

Shortcuts are opt-in. useGraphHistoryShortcuts binds them, driving the graph through an apiRef so it can be called from wherever your key handling lives, including above the provider:
It binds ⌘/Ctrl+Z, ⌘/Ctrl+Shift+Z and Ctrl+Y, and leaves alone any chord already handled by the app or typed into a text field. Pass { target } — an element, or a ref holding one — to scope it to a subtree instead of window, or { enabled: false } to unbind. A chord is only claimed when the chart has a step to take: with an empty history the keystroke is left uncancelled, so an undo your app owns elsewhere on the page still gets it. The same apiRef exposes dispatch, seal, undo and redo directly, for a toolbar or menu bar mounted outside the provider where the hooks can’t reach. undo and redo return whether the chart took the step — that’s the signal to fall through to your own handling. A step the chart declines, either because there is nothing to step or because the older spec no longer compiles, returns false.

When a command doesn’t apply

Two outcomes leave the chart exactly as it was:
  • No-op — the command found nothing to change: the value already matches, or the layer or scale it targets isn’t in the spec. Nothing recompiles and nothing joins the history, so there’s no empty step to undo past.
  • Rejected — the edit produced a spec that fails to compile. The last good chart stays on screen and the diagnostics go to the provider’s onError. An edit the chart never took is not one you can undo, so the history is left untouched.
Undo and redo are gated the same way. If restoring an older spec fails to compile — say the data loaded since no longer has a column that spec referenced — the step is refused, neither stack moves, and undo() returns false so the keystroke falls through to your app. The step stays on the stack and applies once the data supports it again, which is also to say the steps behind it are reachable only through it.

History lifecycle

The history belongs to the chart the provider is currently showing:
  • A new input from the host is a new baseline and clears the history, once that input compiles. The stored steps restore values from a spec that no longer exists, and may target layers or scales the new one doesn’t have. This is by reference, so an input rebuilt on every render leaves nothing undoable — keep it stable.
  • An input that fails to compile keeps it. The chart behind the error panel is still the last good one, so an undo is the way back to something renderable.
  • New data or a theme change keeps it. The recorded steps still address the spec they were built from.
  • History depth is capped at 100 steps; the oldest is dropped past that.

Available commands

Each takes its params as the first constructor argument and optional metadata as the second. Layer commands accept an optional layerId; scale commands take scaledAesthetic to pick the scale by the aesthetic it drives ('x', 'y', 'color', …). Axis commands take axis'x' or 'y', and 'ySecondary' where a second y axis makes sense.Titles & contentAxes & gridLegend & headlineAppearance & formattingCoordinatesLayersAddLayerCommand and RemoveLayerCommand are the two directions of every “show a trend line” or “add a series” toggle, so AddLayerCommand carries a whole layer rather than a per-feature flag.MappingScalesHighlightsAnnotationsAnnotations are addressed by an id unique across every kind, so removing and updating take the id alone. UpdateAnnotationCommand patches whichever fields you name — moving one and restyling it are the same command, so a drag and a colour picker share a path.