> ## Documentation Index
> Fetch the complete documentation index at: https://docs.graphy.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Commands & history

A **command** is an undoable edit to a chart's spec, such as hiding an axis or moving the legend. Dispatch it through the provider to apply the change, recompile the chart and record an undo step.

Import command classes from `@graphysdk/react/editable` and hooks from `@graphysdk/react`.

```tsx theme={null}
import { SetAxisVisibilityCommand } from '@graphysdk/react/editable';
import { useGraphCommands } from '@graphysdk/react';

function AxisToggle({ visible }: { visible: boolean }) {
  const { dispatch } = useGraphCommands();

  return (
    <button
      onClick={() =>
        dispatch(new SetAxisVisibilityCommand({ axis: 'y', visible: !visible }))
      }
    >
      {visible ? 'Hide' : 'Show'} y-axis
    </button>
  );
}
```

The [edit menu](https://docs.graphy.dev/sdk-next/editing/menu.md), custom controls and commands received from an agent all share the same history.

## Dispatching

`useGraphCommands` returns `dispatch(command, options?)` and `commit()`. It must be called from inside a `<GraphProvider>`.

Construct a command with its parameters, then dispatch it:

```tsx theme={null}
dispatch(
  new SetStyleRuleCommand({
    list: 'defaults',
    rule: style.geom.line({ strokeWidth: 3 }),
  })
);
dispatch(new SetScaleDomainCommand({ scaledAesthetic: 'y', domainMin: 0 }));
dispatch(new SetLegendPositionCommand({ position: 'bottom' }));
```

Most layer commands accept an optional `layerId` and default to the first compatible layer. Specify it for a combo chart:

```tsx theme={null}
dispatch(
  new SetLineCurveCommand({
    layerId: 'trend',
    curve: 'smooth',
  })
);
```

Set the layer's `id` when building the spec to use the same name in commands, highlights and annotation anchors:

```tsx theme={null}
pipe(
  createSpec({ x: 'month' }),
  geom.bar({ id: 'bars' }),
  geom.line({ id: 'trend' })
);
```

If omitted, an ID is assigned at compile time.

`SetLineStyleCommand`, `SetPointSizeCommand`, `SetPointSymbolCommand`, `SetBarCornerRadiusCommand` and `RemoveLayerCommand` **require** `layerId`.

Commands return a new spec without mutating the input. The provider reports committed changes through `onSpecChange`; see [Saving the chart](#saving-the-chart).

### Live gestures

For a slider or color picker, dispatch updates with `{ transient: true }` and call `commit()` when the gesture ends. The chart updates live, but the whole gesture creates one undo step and one `onSpecChange` callback:

```tsx theme={null}
import { SetStyleRuleCommand } from '@graphysdk/react/editable';
import { style, useGraphCommands } from '@graphysdk/react';

function CornerRadiusSlider({ cornerRadius }: { cornerRadius: number }) {
  const { dispatch, commit } = useGraphCommands();

  return (
    <input
      type="range"
      min={0}
      max={16}
      value={cornerRadius}
      onChange={(event) =>
        dispatch(
          new SetStyleRuleCommand({
            list: 'overrides',
            rule: style.graph({ cornerRadius: event.target.valueAsNumber }),
          }),
          { transient: true }
        )
      }
      onPointerUp={commit}
      onBlur={commit}
    />
  );
}
```

A transient run combines consecutive updates of the same command type and target. Undo restores the spec from before the run began.

Changing the command type or target closes the run. So does a committed dispatch, undo, redo, or an external change to `spec`, `data` or color scheme. Always call `commit()` at the end of a gesture: without it, `onSpecChange` is delayed and later updates to the same target can merge into that undo step.

For canvas dragging, you can update the position locally and dispatch once on drop. Use transient dispatch when live feedback needs a recompiled spec.

## Undo & redo

Call `useGraphHistory` inside the provider to read history and undo or redo changes. It re-renders the component when history changes:

```tsx theme={null}
import { useGraphHistory } from '@graphysdk/react';

function UndoRedoToolbar() {
  const { undo, redo, canUndo, canRedo, undoDescription, redoDescription } =
    useGraphHistory();

  return (
    <>
      <button
        disabled={!canUndo}
        title={undoDescription ?? undefined}
        onClick={undo}
      >
        Undo
      </button>
      <button
        disabled={!canRedo}
        title={redoDescription ?? undefined}
        onClick={redo}
      >
        Redo
      </button>
    </>
  );
}
```

**canUndo / canRedo** `boolean`

Whether a step is available in each direction.

**undoDescription / redoDescription** `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 no step is available.

**undoStack / redoStack** `CommandMetadata[]`

Full history entries with `id`, `timestamp`, `description` and `author`.
`undoStack` is oldest first; `redoStack` follows the order of undo operations.
In both stacks, the last entry is the next step.

### Keyboard shortcuts

Call `useGraphHistoryShortcuts(handleRef)` to enable keyboard shortcuts. The handle lets the hook run outside the provider:

```tsx theme={null}
import { useRef } from 'react';
import {
  type GraphHandle,
  GraphProvider,
  useGraphHistoryShortcuts,
} from '@graphysdk/react';
import { EditableGraphRenderer } from '@graphysdk/react/editable';

function EditableChart() {
  const handleRef = useRef<GraphHandle>(null);
  useGraphHistoryShortcuts(handleRef);

  return (
    <GraphProvider spec={spec} data={data} handleRef={handleRef}>
      <EditableGraphRenderer mode="edit" />
    </GraphProvider>
  );
}
```

It binds ⌘/Ctrl+Z, ⌘/Ctrl+Shift+Z and Ctrl+Y. It ignores shortcuts already handled by the app and those entered in text fields. Pass `{ target }` with an element or ref to scope the listener to that subtree instead of `window`. A `null` target or `{ enabled: false }` disables the listener.

The hook prevents the default key action only when the chart successfully undoes or redoes a step. Otherwise, your app can handle it.

For controls outside the provider, `handleRef.current` also exposes `commands.dispatch`, `commands.commit`, `undo`, `redo`, `getScene` and `subscribe`. Both `undo()` and `redo()` return `false` if no step is available or the restored spec fails to compile.

## When a command doesn't apply

Two outcomes leave the chart and history unchanged:

* **No-op:** the value already matches, or the target layer or scale is missing. Nothing recompiles.
* **Rejected:** the edited spec fails to compile. The last good chart stays visible and the provider reports diagnostics through `onError`.

When calling `command.apply(spec)` directly, custom geoms require the chart's compiler lookup: `command.apply(spec, { findGeom: (name) => compiler.findGeom(name) })`. The provider supplies this automatically.

Undo and redo also require a valid spec. If new data lacks a column an older spec needs, that step returns `false` and stays on the stack. It blocks earlier steps until the data supports it again.

## History lifecycle

History follows the provider's current chart:

* **A new `spec` reference** clears history after successful compilation. [Keep it stable](https://docs.graphy.dev/sdk-next/rendering/provider-and-renderer.md#keep-inputs-stable) across renders. Passing back the exact value from `onSpecChange` is an exception and preserves history.
* **A spec that fails to compile** preserves the last good chart's history.
* **Changes to `data`, color scheme or renderer mode** preserve history.
* **Remounting the provider** starts a new history. Changing the theme or plugins requires a remount with a new React `key`.
* **History is capped at 100 steps.** Further edits drop the oldest step.

## Saving the chart

Use the provider's `onSpecChange` to save edits. It receives the current spec after each committed change, undo or redo, and once when a transient run closes. It does not fire on mount or when you supply a new `spec`.

This browser example saves to `localStorage` and restores the spec on the next visit:

```tsx theme={null}
import { useState } from 'react';
import { GraphProvider, type Data, type Spec } from '@graphysdk/react';
import { EditableGraphRenderer } from '@graphysdk/react/editable';

const STORAGE_KEY = 'report-chart';

function readSavedSpec(): Spec | null {
  const saved = localStorage.getItem(STORAGE_KEY);
  return saved ? (JSON.parse(saved) as Spec) : null;
}

function SavedChart({ initialSpec, data }: { initialSpec: Spec; data: Data }) {
  const [spec, setSpec] = useState<Spec>(() => readSavedSpec() ?? initialSpec);

  return (
    <GraphProvider
      spec={spec}
      data={data}
      onSpecChange={(next) => {
        setSpec(next);
        localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
      }}
    >
      <EditableGraphRenderer mode="edit" />
    </GraphProvider>
  );
}
```

A spec is [plain JSON](https://docs.graphy.dev/sdk-next/concepts/serializable-spec.md), so `JSON.stringify` and `JSON.parse` are enough to save and restore it. Store the chart's data separately.

Passing the exact `onSpecChange` value back as `spec` preserves history without recompiling. A spec restored from storage starts a new history; saving the spec does not save the undo stack.

## Available commands

> [accordion] All commands
>
> Each command takes parameters as its first constructor argument and optional metadata as its second.
>
> * Scale commands identify the scale with `scaledAesthetic`: `'x'`, `'y'`, `'color'`, etc.
> * Axis commands use `axis: 'x' | 'y'`. `SetAxisLabelCommand`, `SetAxisVisibilityCommand` and `SetAxisTickModeCommand` also accept `'ySecondary'`; omitting its value removes the override so it follows `y`. On `y`, use `keepSecondaryAxis: true` to preserve the secondary axis's current setting.
> * `SetGridLineStyleCommand` accepts `axis: 'both'` to style the whole grid in one undo step.
>
> `AddLayerCommand`, `AddHighlightCommand` and `AddAnnotationCommand` generate an `id` if omitted. Serialization preserves that ID so replaying the command uses the same identity.
>
> `SequenceCommand({ commands })` applies command instances in order as one undo step, using the first command's target and description. It skips no-ops and records nothing if the final spec is unchanged. It serializes the nested commands; deserializing an empty sequence fails with `INVALID_SEQUENCE`.
>
> **Chart type**
>
> | Command | Params |
> | - | - |
> | `SetGraphTypeCommand` | `{ coordType: 'cartesian' \| 'flip', geom, position }`, or `{ coordType: 'polar', geom, position, theta, innerRadius }` |
> | `SetMappingCommand` | `aesthetic`, `variable: string \| null` |
>
> `SetGraphTypeCommand` changes the coordinate system, data layers and required scales as one undo step. Reference lines and fitted trends are preserved. `readGraphType(spec)` reads the current type for a picker, returning `null` for combinations it cannot represent.
>
> `SetMappingCommand` maps a chart-wide aesthetic to a column and infers its scale. Pass `null` to unmap it. Layer-specific `aes` mappings stay unchanged.
>
> **Titles & content**
>
> | Command | Params |
> | - | - |
> | `SetContentTitleCommand` | `title` |
> | `SetContentSubtitleCommand` | `subtitle` |
> | `SetContentCaptionCommand` | `caption` |
> | `SetContentSourceCommand` | `source` |
> | `ToggleContentVisibilityCommand` | `part`, `visible` |
>
> **Axes & grid**
>
> | Command | Params |
> | - | - |
> | `SetAxisLabelCommand` | `axis`, `label` |
> | `SetAxisPositionCommand` | `axis`, `position` |
> | `SetAxisVisibilityCommand` | `axis`, `visible` |
> | `SetAxisTicksVisibilityCommand` | `axis`, `visible` |
> | `SetAxisTickModeCommand` | `axis`, `tickMode` |
> | `SetGridVisibilityCommand` | `axis`, `visible: boolean \| null` (`null` defers to the compiler) |
> | `SetGridLineStyleCommand` | `axis: 'x' \| 'y' \| 'both'`, `lineStyle?`, `strokeWidth?` (only those given are written; `null` defers to the stylesheet) |
>
> **Legend & headline**
>
> | Command | Params |
> | - | - |
> | `SetLegendPositionCommand` | `position` |
> | `SetLegendAlignCommand` | `align` |
> | `SetLegendPlacementCommand` | `position`, `align` |
> | `SetLegendDisplayCommand` | `display` |
> | `SetHeadlineShowCommand` | `show` |
> | `SetHeadlinePositionCommand` | `position` |
> | `SetHeadlineSizeCommand` | `size` |
> | `SetHeadlineCompareWithCommand` | `compareWith` |
>
> **Appearance & formatting**
>
> | Command | Params |
> | - | - |
> | `SetHighlightDimStyleCommand` | `dimStyle: 'dim' \| 'desaturate'` |
> | `SetNumberFormatDecimalsCommand` | `decimals` |
> | `SetNumberFormatAbbreviationCommand` | `abbreviation` |
>
> For the chart's background, border, corner radius or text scale, use `SetStyleRuleCommand` with `style.graph({ … })`. See [Styling](https://docs.graphy.dev/sdk-next/config/styling.md).
>
> **Coordinates**
>
> | Command | Params |
> | - | - |
> | `SetCoordLimitsCommand` | `xLimits`, `yLimits` |
> | `SetPolarInnerRadiusCommand` | `innerRadius` |
> | `SetPolarStartAngleCommand` | `startAngle` |
>
> **Layers**
>
> | Command | Params |
> | - | - |
> | `AddLayerCommand` | `layer`, `index?` |
> | `RemoveLayerCommand` | `layerId` |
> | `SetLayerPositionCommand` | `layerId?`, `position` |
> | `SetLayerStatCommand` | `layerId?`, `stat` (a name such as `'count'`, an object such as `stat.count()`, or a plugin stat) |
> | `SetLayerYScaleTypeCommand` | `layerId?`, `yScaleType` |
> | `SetLineCurveCommand` | `layerId?`, `curve` |
> | `SetLineMissingValuesCommand` | `layerId?`, `missingValues` |
> | `ToggleLinePointsCommand` | `layerId?`, `showPoints`, `where?`, `removedLayer?` |
> | `SetLineStyleCommand` | `layerId`, `where?`, `lineStyle?`, `strokeWidth?: number \| null`, `showFill?` |
> | `SetPointSizeCommand` | `layerId`, `size: number \| null`, `where?` |
> | `SetPointSymbolCommand` | `layerId`, `symbol: StylePointSymbol \| null`, `where?` |
> | `SetBarWidthCommand` | `layerId?`, `width?` |
> | `SetBarCornerRadiusCommand` | `layerId`, and `corners: Partial<GeomCornerRadii>` or `cornerRadius: GeomCornerRounding \| null` |
> | `SetRuleValueCommand` | `layerId?`, `value` |
> | `SetRuleLabelCommand` | `layerId?`, `label` |
> | `ToggleGoalLineCommand` | `showGoalLine`, `layerId?`, `value?`, `label?`, `removedLayer?` |
> | `SetStatLineCommand` | `line: 'none' \| 'trend' \| 'average'`, `layerId?`, `method?`, `group?`, `label?`, `removedLayer?` |
> | `ToggleDataLabelsCommand` | `layerId?`, `showDataLabels` |
> | `SetDataLabelsFormatCommand` | `layerId?`, `format` |
> | `ToggleCategoryLabelsCommand` | `layerId?`, `showCategoryLabels` |
> | `ToggleStackTotalsCommand` | `layerId?`, `showStackTotals` |
>
> `AddLayerCommand` takes a complete layer. Toggles that remove layers (`ToggleLinePointsCommand`, `ToggleGoalLineCommand`, `SetStatLineCommand`) preserve a `removedLayer` so undo restores its settings and identity.
>
> `SetLineStyleCommand`, `SetPointSizeCommand`, `SetPointSymbolCommand`, `SetBarWidthCommand` and `SetBarCornerRadiusCommand` update layer-scoped style rules while preserving unrelated properties. Line style, point size and point symbol also accept `where` to target one group. Without it, they update the layer and remove conflicting group overrides, except overrides that keep points hidden.
>
> `ToggleLinePointsCommand` also accepts `where`. It hides a group's points with `size: 0` and removes the point layer when no groups remain visible. Showing points without `where` clears all hidden-point overrides. Changing point size preserves hidden groups; if the layer itself has `size: 0`, only explicitly visible groups are resized.
>
> `SetLineStyleCommand` changes only the fields you pass, as one undo step. `lineStyle` accepts `'solid'`, `'dashed'` or `'dotted'`; `strokeWidth: null` restores the stylesheet default; `showFill` toggles fill beneath a line and is ignored by areas.
>
> ```tsx theme={null}
> dispatch(
>   new SetLineStyleCommand({
>     layerId: 'trend',
>     lineStyle: 'dashed',
>     strokeWidth: 3,
>   })
> );
> dispatch(
>   new SetLineStyleCommand({
>     layerId: 'trend',
>     where: { variable: 'region', eq: 'EU' },
>     showFill: true,
>   })
> );
> ```
>
> **Styles**
>
> | Command | Params |
> | - | - |
> | `SetStyleRuleCommand` | `list`, `rule`, `index?` |
> | `SetObservationsColorCommand` | `target`, `color: string \| null` |
>
> `SetStyleRuleCommand` edits widths, dashes, opacity and colors. It identifies a rule by its `select` and `when`, not its ID. To give the rule an ID, pass it through the builder: `style.geom.line({ strokeWidth: 3 }, { id: 'emphasis' })`.
>
> ```tsx theme={null}
> // insert or replace
> dispatch(
>   new SetStyleRuleCommand({
>     list: 'overrides',
>     rule: style.geom.line({ strokeWidth: 3 }),
>   })
> );
>
> // remove every entry at that address
> dispatch(
>   new SetStyleRuleCommand({
>     list: 'overrides',
>     rule: { select: { target: 'geom', kind: 'line' }, declarations: null },
>   })
> );
> ```
>
> Updating a matching rule replaces its declarations while preserving its cascade position. `declarations: null` removes all matching rules and does nothing if none match. See [Styling](https://docs.graphy.dev/sdk-next/config/styling.md) for supported targets and properties.
>
> `SetObservationsColorCommand` colors a selection in one undo step. It takes `target: { kind: 'observations', layerId, scope, predicate }` and a hex `color`, or `null` to remove the color override.
>
> **Scales**
>
> | Command | Params |
> | - | - |
> | `SetScaleDomainCommand` | `scaledAesthetic`, `domainMin`, `domainMax` |
> | `SetScaleRangeCommand` | `scaledAesthetic`, `range` |
> | `SetScaleTransformCommand` | `scaledAesthetic`, `transform` |
> | `SetScaleReverseCommand` | `scaledAesthetic`, `reverse` |
>
> `SetScaleRangeCommand` assigns values to a discrete scale's groups in order, or the colormap stops of a continuous color scale. For a color scale, `range: null` restores the theme's list for its class: categorical, sequential or diverging.
>
> **Highlights**
>
> | Command | Params |
> | - | - |
> | `AddHighlightCommand` | `predicate`, `id?`, `scope?`, `layerId?`, `index?` |
> | `RemoveHighlightCommand` | `id` |
>
> **Annotations**
>
> | Command | Params |
> | - | - |
> | `AddAnnotationCommand` | `kind`, `annotation`, `index?` |
> | `RemoveAnnotationCommand` | `id` |
> | `UpdateAnnotationCommand` | `kind`, `id`, `patch` |
> | `MoveAnnotationCommand` | `id`, `translation` |
> | `SetDifferenceArrowStyleCommand` | `annotationId`, `arrow?`, `label?` |
>
> Annotation IDs are unique across kinds. `UpdateAnnotationCommand` patches only the fields you supply. `MoveAnnotationCommand` moves by a relative translation in panel fractions, clamped to the panel; undo restores the previous position.
>
> `SetDifferenceArrowStyleCommand` edits a difference arrow's line (`arrow`) and label (`label`) style overrides in one step. Pass `null` to remove an entry; omit it to leave it unchanged.

## Commands over the wire

Use `commandRegistry` to serialize a command as `{ type, params, metadata }` and deserialize it back into an instance. Commands received from an agent, collaborator or session log use the same dispatch and history as local edits:

```tsx theme={null}
import {
  commandRegistry,
  type SerializedCommand,
} from '@graphysdk/react/editable';

const payload: SerializedCommand = commandRegistry.serialize(command);
dispatch(commandRegistry.deserialize(payload));
```

`deserialize` throws for unknown command types. Check that sender and receiver use compatible SDK versions. Serialized parameters include generated IDs so replay preserves them.

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:

```tsx theme={null}
import { useGraphSelection, useSceneSelector } from '@graphysdk/react';
import { readObservationsTargetData } from '@graphysdk/react/editable';

const [target] = useGraphSelection();
const scene = useSceneSelector((compiled) => compiled);

if (target?.kind === 'observations') {
  const data = readObservationsTargetData({ scene, target });
  // {
  //   columns: [{ key: 'quarter', label: 'Quarter', valueFormat: { type: 'text' } }, ...],
  //   rows: [{ quarter: 'Q2', region: 'North', sales: 420 }],
  // }
}
```

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: []`.

## Related

* [Editing overview](https://docs.graphy.dev/sdk-next/editing/index.md) — setup and renderer modes
* [The edit menu](https://docs.graphy.dev/sdk-next/editing/menu.md) — which command each menu row dispatches
* [Provider & renderer](https://docs.graphy.dev/sdk-next/rendering/provider-and-renderer.md) — `onSpecChange`, `handleRef`, `mode`
* [Serializable spec](https://docs.graphy.dev/sdk-next/concepts/serializable-spec.md) — the spec commands edit, and how to persist it


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.