# Author a downstream MDX story (/docs/guides/mdx)

> Use native VisDelta states in a narrative application, with explicit ownership of progress and asynchronous chart creation.

## VisDelta Story [#visdelta-story]

The MDX editor and interactive examples are maintained in the independent
**VisDelta Story** downstream application. [Open its entry page](/story/).
Its source lives beside this repository in `../visdelta-story/`; it consumes
VisDelta through public package exports. The app owns MDX compilation, scrolling,
shared variables, controls, live Tailwind styling and project persistence.
The application maintains its own [Story documentation](https://songhaifan.github.io/visdelta-story/docs/), with MDX authoring, controls, examples and deployment guides.

The examples below describe how another MDX application can use VisDelta's
native chart declarations and playback APIs.

## Integrate with your own MDX application [#integrate-with-your-own-mdx-application]

An MDX application can import native VisDelta states and supply them to its own
narrative components. VisDelta supplies chart state and rendering; the application
supplies MDX compilation, layouts, scrolling, editing and error presentation.
The VisDelta library itself does not export an MDX compiler or `Scrolly`/`Step` components; Studio implements these in its application layer.

## A paragraph and its chart belong together [#a-paragraph-and-its-chart-belong-together]

Start with the complete
[`sales.mjs`](https://github.com/SonghaiFan/visdelta/blob/main/examples/authoring/sales.mjs)
module. These three rows have unique regions, so Bar infers its mark key.
This simple example needs no explicit `key()` or `datumKey()`:

```js title="sales.mjs"
import { bar } from 'visdelta';

export const sales = [
  { region: 'North', revenue: 18 },
  { region: 'South', revenue: 12 },
  { region: 'West', revenue: 9 },
];

export const chart = bar(sales).x('region').y('revenue');
```

The accompanying
[`story.mdx`](https://github.com/SonghaiFan/visdelta/blob/main/examples/authoring/story.mdx)
uses the following **application-owned component contract**. The MDX application
supplies `Scrolly` and `Step` as default components:

```mdx
import { chart } from './sales.mjs'

# Which region sells the most?

<Scrolly>

<Step chart={chart}>

## Compare all three regions

Each bar shows one region's revenue.

</Step>

<Step chart={chart.highlight({ region: 'North' })}>

## Look at North

North has the highest revenue. The other regions remain visible.

</Step>

<Step chart={chart}>

## Return to the whole picture

Compare North with the other regions again.

</Step>

</Scrolly>
```

Read ordinary Markdown, use `Scrolly` to group the story, and give each `Step`
the chart to show while reading it. Each `chart` prop is a complete immutable
VisDelta declaration. Highlighting creates a new declaration; it does not modify
the base chart or inherit changes from the preceding step. Repeating `chart`
returns to the overview. The application collects those declarations in step
order and creates the sequence; authors maintain no parallel `states` array.

`chart` is a prop name, not a new builder or a rendered controller. VisDelta does
not export `chart()`, `Scrolly` or `Step`. Applications own their component
registration and may allow explicit standard MDX imports to override defaults.
They also own optional IDs, accessible titles, layouts and hold policies. Avoid
reconstructing VisDelta as another YAML grammar or a second highlighting prop.

The onboarding target is to copy the template, change fields and add a highlight
step in two minutes. This is a design target, not a measured user-study result.
More complex stories can use the
[`network-states.mjs`](https://github.com/SonghaiFan/visdelta/blob/main/examples/authoring/network-states.mjs)
Line example with explicit identity and temporal bindings. Dates serialize to
ISO strings; compare instants rather than requiring `rows()` to return `Date`s.

## Optional chart presentation [#optional-chart-presentation]

An application can expose a `chartStyle` prop on `Scrolly` and map preset names
through the existing `chartStylePresets` export:

```mdx
<Scrolly chartStyle="paper">

<Step chart={chart}>Compare all regions.</Step>

<Step chart={chart.highlight({ region: 'North' })}>Look at North.</Step>

</Scrolly>
```

`chartStyle` here is an application prop, not a new VisDelta builder or string
option. The application resolves `d3`, `paper`, `dark` or `editorial` to a module
from `visdelta/chart-style`, or accepts a native `defineChartStyle(...)` module,
and passes it as the library's `options.chartStyle`. Both `sequence()` and the
single-state `transition(chart, chart)` path need that option.

One module applies to the whole sequence. Steps keep using native chart
declarations for authored color changes; they do not interpolate style modules.
The application validates names, retains stable module identity through parent
renders, and preserves desired progress when replacing a controller for a new
module. Keep page-theme refreshes separate: they use `resize()`.

See [Styling and themes](/docs/guides/styling/) for CSS inheritance, explicit-color
precedence and the difference between structural modules and CSS token themes.

## Installed-package contract [#installed-package-contract]

Use `import 'visdelta/style.css'` once in the browser application. Pin the
package version used by the story. The current integration baseline is 0.4.2.

An unreleased fix in this checkout preserves chart-root theme/font scope when
sequence legs move out of their hidden preparation mounts. Version 0.4.2 can
measure legend labels using the empty preparation host's font after a handoff.
Integration snapshots containing this fix must use a distinct prerelease version,
not be presented as the published 0.4.2 package.

The Paper neutral-ink correction is also unreleased: older builds can inherit
light neutral marks from a dark page. Pin the tested package or integration
snapshot rather than assuming this checkout's fixes exist in 0.4.2.

| Concern | Implemented behavior | Application responsibility |
| --- | --- | --- |
| State construction | Builders, `rows()`, `toSpec()` and `delta()` do not need a DOM | Keep source data and base declarations stable; choose date and identity conventions |
| Mount | `await sequence(states, options)` requires at least two states and a browser DOM | Attach an empty, exclusively owned target; set its dimensions |
| One static state | `await transition(state, state, options)`, then `.progress(1)` | Keep a single-state story at authored position 0 |
| Scroll position | Sequence uses `0..states.length - 1`; finite inputs clamp | Measure anchors, apply holds and coalesce events; do not normalize the whole story to 0..1 |
| Playback | `.progress()` stops controller-owned playback; `.play()` returns the controller | Choose one input owner at a time; implement reduced-motion policy |
| Resize/theme | `.resize()` retains current progress and re-reads dimensions/theme | Give `height: 'auto'` a measurable CSS height; refresh after theme changes |
| Initialization | Creation returns a Promise; no cancellation signal or separate controller `ready` | Buffer desired progress and destroy late results from obsolete generations |
| Errors | Creation can reject; invalid progress/dimensions throw | Show the current generation's error and prevent stale errors replacing a newer preview |
| Cleanup | `.destroy()` is idempotent | Disconnect application observers, listeners and RAFs; remove the generation's mount |

Position domains are independent of narrative progress. `highlight()` emphasizes
marks, `where()` changes membership, and `focus()` changes the view. A legacy
field named “focus” must be interpreted by its behavior rather than translated
by spelling.

## Inline declarations and React rendering [#inline-declarations-and-react-rendering]

`chart.highlight({ region: 'North' })` produces a fresh immutable object on each
call, even when its declaration is unchanged. Collecting child props also creates
a new array. An application must not use either reference alone to rebuild its
session on every parent render or status update. Asking MDX authors to use React
memoization defeats the simple authoring contract.

Within a compiled document generation, the application can compare the complete
detached `toSpec()` declarations and retain equivalent step inputs. Preserve array
order and distinguish real data, encoding and metadata changes. Keep plugin
module identity and external dataset/reload inputs in the lifecycle dependencies;
equal serialized specs alone do not prove equivalent custom plugins or remote
data. Do not introduce a second chart normalizer.

Standard chart declarations are JSON-safe: serialization turns dates into ISO
strings and does not preserve arbitrary callbacks or class instances. A custom
builder's non-JSON output needs a conservative identity policy or a clear error,
not a fingerprint that silently drops values. Test repeated inline expressions
under parent/status/theme renders: initialization must settle, while changed
chart data must rebuild. Those component tests belong to the application.

## Isolate asynchronous generations [#isolate-asynchronous-generations]

[`mount-story.mjs`](https://github.com/SonghaiFan/visdelta/blob/main/examples/authoring/mount-story.mjs)
is a copyable application integration example. It creates a private child mount
per call, buffers the latest seek while creation is pending, supports single
states, and disposes a controller that arrives after unmount. Its `ready` property
belongs to the example wrapper, not to VisDelta's controller.

```js
const session = mountStory(host, states);
session.seek(currentPosition);
session.ready.catch(error => {
  if (thisGenerationIsCurrent()) showError(error);
});
// Theme change: session.resize();
// Effect cleanup: remove application listeners, then session.destroy();
```

Removing a mount does not cancel creation or network requests. Pending work can
finish into the detached mount before its controller is destroyed. Do not reuse
that target for a newer generation. A published preview can keep the previous
chart until a replacement is ready; implementing that swap is application policy.

Sequence preparation uses hidden mounts under `document.body`. Put shared theme
tokens on the document root so preparation and visible charts see the same
theme. Scope styles and DOM queries per instance; avoid global chart IDs.

## Verification and feedback [#verification-and-feedback]

`npm run authoring:check` installs a frozen tarball in a separate consumer,
bundles the actual example imports and checks authored endpoints against
independent static renders. It also covers pending seeks, reverse scrubbing,
jumps, resizing, static states, multiple instances, obsolete generations and
failed preparation cleanup. It never aliases package imports to source files.

A downstream application still needs its own MDX compile/build, editing/save,
scroll mapping, desktop/narrow layout and accessibility tests. Report library
issues with the package version, native states, dimensions, requested progress,
observed frame and expected behavior. An unsupported authoring option is not
automatically a renderer bug. Diagnostic API proposals remain unimplemented.
