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 spec, you dispatch a command. The provider applies it, recompiles, and records it on an undo stack. Import command classes from @graphysdk/react/editable and hooks from @graphysdk/react.
Every edit that reaches the chart goes through this one path: a toolbar button, an inline edit in annotate mode, or a command streamed in from an agent. All of them land in the same history and all of them are undoable.

Dispatching

useGraphCommands returns dispatch(command, options?) and commit(). It must be called from inside a <GraphProvider>. A command is a plain object you construct with its params:
Most commands that target a layer take an optional layerId. Omit it and the command finds the first layer of a compatible geom. That is enough for a single-layer chart; a combo chart should name the layer:
A layer’s id comes from the spec. Set it when you build the layer, and every reference to it (commands, highlights, annotation anchors) can use that name:
Leave id off and one is assigned at compile time. A layer always has a stable identity; you just don’t choose the name. Four commands require layerId: SetLineStyleCommand, SetPointSizeCommand and SetBarCornerRadiusCommand, which write per-layer stylesheet entries and so need an address, plus RemoveLayerCommand, which needs to know what it is removing. Nothing is mutated. A command reads the current spec and returns a new one, so the provider swaps the compiled result in without touching the object you passed as spec.

Persisting edits

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

Live gestures

A slider or color picker that edits while it is held would otherwise leave one undo entry, and one onSpecChange, per frame. Pass { transient: true } on those frames and call commit() 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. One undo therefore returns to before the gesture began, not to its previous frame. onSpecChange fires once, when commit() closes the run. A run covers one thing being edited: dispatches fold together only while they are the same command type against the same edit target. A different target, or a different command type on the same target, starts its own entry. So does a committed dispatch, an undo, a redo, or a new spec, data or color scheme from the host; each closes the open run first. Forgetting to commit() delays that gesture’s onSpecChange 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 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, such as "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. undoStack runs oldest first; redoStack runs in the order the steps were undone. In both, the last entry 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 a handleRef, 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; null binds nothing. Pass { 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 handleRef exposes commands.dispatch, commands.commit, undo and redo, for a toolbar or menu bar mounted outside the provider where the hooks can’t reach, plus getScene and subscribe for reading the chart from there. undo and redo return whether the chart took the step; false is the signal to fall through to your own handling. The chart declines a step when there is nothing to step or when the older spec no longer compiles.

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 is 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.
A command applied outside the provider, with command.apply(spec), reads only the built-in geoms. A color command on a custom geom is then a no-op. Pass the chart’s geoms as the second argument, command.apply(spec, { findGeom: (name) => compiler.findGeom(name) }), using the compiler the chart was built with. Commands dispatched through the provider get this already. Undo and redo are gated the same way. If restoring an older spec fails to compile, say because 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. The steps behind it are reachable only through it.

History lifecycle

The history belongs to the chart the provider is currently showing:
  • A new spec 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 a spec rebuilt on every render leaves nothing undoable; keep it stable.
  • A spec 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 color scheme change keeps it. The recorded steps still address the spec they were built from. The theme and the plugins are fixed at mount; switching either means remounting the provider with a new React key, which starts a fresh history.
  • 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. Scale commands take scaledAesthetic to pick the scale by the aesthetic it drives ('x', 'y', 'color', …). Axis commands take axis: 'x' | 'y'. SetAxisLabelCommand, SetAxisVisibilityCommand and SetAxisTickModeCommand also accept 'ySecondary', and omitting their value there drops the override, so the secondary axis follows y again. On y, those two reach both vertical axes unless keepSecondaryAxis: true leaves the secondary axis as it is drawn. The two grid line-style commands accept 'both' so a change across the whole grid is one undo entry.AddLayerCommand, AddHighlightCommand and AddAnnotationCommand separate what you construct with from what serializes: the constructor’s id is optional and minted when omitted, while the serialized params carry it settled, so a deserialized command replays to the same spec instead of adding a second thing.SequenceCommand({ commands }) takes live command instances and applies them as one undo step, in the order given, each against the spec the one before produced. It is about whatever its first command is about, and carries that command’s description. A command that does nothing is skipped, and the sequence does nothing when together they leave the spec unchanged. It serializes like any other command, with its params holding the serialized list; a deserialized sequence with an empty list fails with INVALID_SEQUENCE.Chart typeWhat a chart is (a column chart, a stacked bar, a donut) spans the coordinate system, every data layer and the scales those geoms need, so SetGraphTypeCommand writes them together as one undo entry. readGraphType(spec) reads it back for a picker, returning null for a combo chart no single option describes. Reference lines and fitted trends keep their own shape across the switch.SetMappingCommand points one aesthetic of the chart-wide mapping at a column (null unmaps it) and infers the scale that column needs. A layer’s own aes is left alone.Titles & contentAxes & gridLegend & headlineAppearance & formattingPaint, including the graph’s background, border, corner radius and text scale, is a stylesheet edit rather than a config one: use SetStyleRuleCommand with style.graph({ … }). See Styling for the vocabulary.CoordinatesLayersAddLayerCommand and RemoveLayerCommand are the two directions of every “show a trend line” or “add a layer” toggle, so AddLayerCommand carries a whole layer rather than a per-feature flag. The toggles that remove a layer of their own (ToggleLinePointsCommand, ToggleGoalLineCommand, SetStatLineCommand) carry a removedLayer for the same reason: it lets undo bring back the layer’s own settings and identity rather than a fresh one.SetLineStyleCommand, SetPointSizeCommand, SetBarWidthCommand and SetBarCornerRadiusCommand are typed shortcuts over the stylesheet. Each writes a { layer }-scoped entry through SetStyleRuleCommand, merging into whatever else that layer’s entry paints. SetPointSizeCommand and SetLineStyleCommand take an optional where, which writes that paint on one group instead of the whole layer. Without where, the paint also leaves every group’s rule on that layer, so each group takes the layer’s value.ToggleLinePointsCommand takes a where too. One point layer draws the dots of every group, so a group’s points hide by drawing that group at size: 0. Hiding the last group that draws dots removes the point layer, and showing points without a where takes every size 0 away. SetPointSizeCommand without a where leaves a group at size 0 hidden, and while the layer itself draws at size 0 it sizes only the groups shown on top of it.SetLineStyleCommand sets a line’s or an area’s dash, stroke width and fill as one undo step, writing only the fields you pass. lineStyle is solid, dashed, or dotted; strokeWidth: null hands the width back to the stylesheet; showFill draws the wash beneath a line, and an area ignores it. Without where, the fields you pass also leave every group’s rule on that layer, so the whole path takes the new value.
StylesSetStyleRuleCommand is the one command for paint (widths, dashes, opacities, colors beyond the palette). An entry is addressed structurally: the rule’s own select and when say which entry it is, so the command writes whatever paints those elements under those conditions, whoever authored it. An entry’s id travels inside the rule, through the builder’s options bag: style.geom.line({ strokeWidth: 3 }, { id: 'emphasis' }).
Declarations at an address the list already holds replace that entry’s, keeping its position in the cascade. declarations: null removes every entry at the address; an address the list doesn’t hold is a no-op. See Styling for the targets and the vocabulary each one takes.SetObservationsColorCommand colors a selection of observations as one undo step, by writing the style rules that selection needs. target is an observations edit target, { kind: 'observations', layerId, scope, predicate }. color is a hex string, or null to remove the color. ScalesSetScaleRangeCommand sets the values a discrete scale hands its groups in order, such as the colors of a color scale. range: null hands a color scale back to the theme’s categorical colors.HighlightsAnnotationsAnnotations are addressed by an id unique across every kind, so removing, updating and moving take the id alone. UpdateAnnotationCommand patches whichever fields you name; moving one and restyling it are the same command, so a drag and a color picker share a path. MoveAnnotationCommand is the specialized drag: a translation in panel fractions, clamped inside the panel, relative rather than absolute, and reverted by the inverse patch since clamping isn’t invertible. SetDifferenceArrowStyleCommand writes one difference arrow’s line entry (arrow) and label entry (label) in the overrides list as one step; null removes an entry and an omitted one is left alone.

Commands over the wire

Commands are registered by type, so they round-trip through JSON. commandRegistry turns a command into a SerializedCommand, { type, params, metadata }, and a SerializedCommand back into a live instance. That is what lets an edit arrive from an agent, a collaborator, or a replayed session log and land on the same history as a click:
deserialize throws on a type it doesn’t know, so treat an unrecognised payload as a version mismatch rather than a no-op. This round trip is also why the Params types are the settled ones: a replayed command reproduces the same spec rather than minting a second id. Use readObservationsTargetData to read the selected observations as Data. Values come from the layer after its transforms and stat, before stacking. The payload includes each mapped variable once and omits unmapped columns and non-stat internal variables. Send the data beside the target so a receiver can reply with a command on the same observations target:
Column keys follow the spec’s mapping. A sum of sales returns { quarter: 'Q1', sales: 550 }; a count has no source variable, so its key is count, and a summary’s interval goes out as summaryLower and summaryUpper. Reshaped variables keep the names assigned by the reshape. If keys collide, stat outputs use their own names (sum, count, summaryEstimate, summaryLower, summaryUpper), with a numeric suffix if needed, such as count_2. Columns include their source variables’ labels and value formats. Lookup formats, which vary by observation, are omitted. Dates and value formats are copied so editing the payload leaves the scene unchanged. The function returns null if the layer is missing or cannot evaluate the predicate. A valid predicate that matches nothing returns the columns with rows: [].