Documentation

Author a downstream MDX story

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

VisDelta Story

The MDX editor and interactive examples are maintained in the independent VisDelta Story downstream application. Open its entry page. 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, 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

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

Start with the complete sales.mjs module. These three rows have unique regions, so Bar infers its mark key. This simple example needs no explicit key() or datumKey():

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 uses the following application-owned component contract. The MDX application supplies Scrolly and Step as default components:

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 Line example with explicit identity and temporal bindings. Dates serialize to ISO strings; compare instants rather than requiring rows() to return Dates.

Optional chart presentation

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

<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 for CSS inheritance, explicit-color precedence and the difference between structural modules and CSS token themes.

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.

ConcernImplemented behaviorApplication responsibility
State constructionBuilders, rows(), toSpec() and delta() do not need a DOMKeep source data and base declarations stable; choose date and identity conventions
Mountawait sequence(states, options) requires at least two states and a browser DOMAttach an empty, exclusively owned target; set its dimensions
One static stateawait transition(state, state, options), then .progress(1)Keep a single-state story at authored position 0
Scroll positionSequence uses 0..states.length - 1; finite inputs clampMeasure anchors, apply holds and coalesce events; do not normalize the whole story to 0..1
Playback.progress() stops controller-owned playback; .play() returns the controllerChoose one input owner at a time; implement reduced-motion policy
Resize/theme.resize() retains current progress and re-reads dimensions/themeGive height: 'auto' a measurable CSS height; refresh after theme changes
InitializationCreation returns a Promise; no cancellation signal or separate controller readyBuffer desired progress and destroy late results from obsolete generations
ErrorsCreation can reject; invalid progress/dimensions throwShow the current generation's error and prevent stale errors replacing a newer preview
Cleanup.destroy() is idempotentDisconnect 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

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

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.

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

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.

On this page