feat(interactions): declarative interaction_spec for specifying chart interactions with presets - #124
Open
xavier (xavier-shaw) wants to merge 9 commits into
Open
xavier (xavier-shaw) wants to merge 9 commits into
xavier (xavier-shaw) wants to merge 9 commits into
Conversation
Adds the JSON shape for declarative interactions beside chart_spec and
theme_spec: a list of preset entries { type, id?, options }, retained
updates applied at mount, and the surface policies. The four policy
interfaces move to core so the contract needs no runtime import;
flint-chart/interactive re-exports them under their old names. Includes
the design document that records the decisions.
INTERACTION_PRESETS maps each preset name to the factory code calls today, with the capability it needs and the gesture it uses. resolveInteractionSpec turns interaction_spec entries into the same CanvasInteractionDef values, tagged origin 'spec', with the type name as the default id. It rejects an unknown type, a flat option outside "options", a missing required option, and a duplicate id, and names the entry in every message.
The checks addVegaLiteInteractions made inline move to admitInteractions. A code definition still throws with the same message. A spec entry the chart cannot honour is dropped and reported as a ChartWarning; in a pan-versus-drag conflict the later entry yields. The plan carries the admitted list and the warnings, the Vega mount runs the admitted list, and the renderer exposes the warnings.
composeInteractiveOptions merges the spec with the code options: the spec comes first, an id shared by both sources is an error, the code wins on the surface policies, and a backend that runs no interactions ignores the spec with one info warning. The surface exposes surface.warnings, resolved after ready, and logs the list once.
…nteraction_spec The Test cases lab gains a `source` and a second route, spec-test-cases, that mounts every case from `input.interaction_spec` instead of factory calls. `modeSpec()` mirrors `modeInteractions()` entry for entry, with the surface policies in the spec. Each card shows the JSON it mounted from in a foldable, token-coloured panel with a copy button, reports dropped entries in a callout, and the page tallies ready and dropped cards. A headless comparison of both tabs found no status difference across 23 modes and 837 cards.
Contributor
|
Let's move update / dismiss out of the interaction API? |
Retained state leaves the spec. An update says what the chart shows now and arrives from outside: a host seeds a view, a story scrolls to a sentence, an agent answers a question. The code already treats it that way through options.updates, applyUpdate, setUpdates, and dispatch, so the spec keeps interactions and the surface policies and nothing else. The design document records the decision and notes the next step: a per-interaction reset list that replaces the global dismiss policy.
… state Step 1 of replacing the global dismiss policy with a per-interaction reset. Three gestures, defined by what the reader does and nothing else: click-none (a click whose hit resolves to no element), double-click (anywhere), and escape. Every preset that retains state accepts `reset` and normalises it onto its definition; presets that retain nothing have no such option. The registry records each preset's supported and default lists, the resolver rejects an unknown or unsupported gesture by name, and navigate renames click-background to click-none (a margin click now resets too). The runtime still honours only navigate's list; the dispatcher that reads the others is step 2.
…r interaction Step 2 of replacing the global dismiss policy. A click that hits nothing, a double-click, or Escape now resets only the interactions whose reset list holds that gesture, each by its own id. Host updates and the other interactions keep their state; legend-toggle drops its closure through onReset(). navigate flies home through its navigation path and the stateful brush clears through the gesture's own reset(), so the special double-click and Escape paths in the two gesture modules go away. A chart with an escape reset becomes focusable and takes focus on a pointer press, so Escape reaches the chart the reader touched last and no other. double-activate next to a double-click reset is an admission conflict. options.dismiss survives as a deprecated code option that maps onto every interaction that resets by default.
…t lists own it The global dismiss policy is gone from interaction_spec, from buildInteractiveChart, the surface, and the Vega renderer options, and the InteractionDismissPolicy type with it. Each interaction's reset list already decides what a click on nothing, a double-click, or Escape clears. The resolver rejects a spec that still carries dismiss or updates and points at the replacement. The two callers in the repo needed nothing in exchange: the Test cases lab passed the values the presets default to, and the you-draw-it demo mounts a hand-built definition with no reset list plus host updates, which no gesture resets.
Collaborator
Author
|
The
|
xavier (xavier-shaw)
added this pull request to stack #128
September 16, 2026 00:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Charts can now ask for behaviour in the same JSON document that asks for the chart. A new top-level field,
interaction_spec, lists interaction presets by name with their options, plus the surface policies. Retained state stays a host signal (applyUpdate,setUpdates,dispatch), not part of the spec. A registry maps each name to the factory code already calls, so a spec entry and a factory call are two spellings of one definition. The mount decides what the chart can honour: a spec entry it cannot honour is dropped with a warning, and the chart still renders.Design notes and the decision record:
docs/design-interaction-spec.md.Commits
core/interaction-spec.ts: theInteractionSpeccontract,interaction_speconChartAssemblyInput. The four surface-policy interfaces move to core;flint-chart/interactivere-exports them under their old names.INTERACTION_PRESETS(name → factory, capability, gesture) andresolveInteractionSpec(). Entries are{ type, id?, options }; the resolver rejects an unknown type, a flat option outsideoptions, a missing required option, and a duplicate id, naming the entry each time.admitInteractions()extracted from the inline checks inaddVegaLiteInteractions(). Code definitions still throw with the same messages. Spec entries are dropped with aChartWarning(unsupported_interaction,conflicting_interactions); in a pan-versus-drag conflict the later entry yields. The plan carries the admitted list and the Vega mount runs it.composeInteractiveOptions(): spec first, then code; an id shared by both is an error; code wins ondismiss,assistedTargeting,keyboardTargeting; a non-Vega-Lite backend ignores the spec with oneinfowarning. The surface exposessurface.warningsand logs the list once.updatesremoved from the spec after review: retained state arrives from outside the chart, so it stays onoptions.updatesand the surface methods.reset, a list ofclick-none,double-click,escape; the registry records each preset's supported and default lists; the resolver rejects an unknown or unsupported gesture by name;navigaterenamesclick-backgroundtoclick-none.legend-toggledrops closure state throughonReset(); a chart with anescapereset becomes focusable and takes focus on a pointer press;double-activatenext to adouble-clickreset is an admission conflict.dismissis removed from the spec and from the code options; the resolver rejects the key with a hint.playground/spec-test-cases). It renders the same cases as Test cases, but every chart mounts frominteraction_spec;modeSpec()mirrorsmodeInteractions()entry for entry. Each card shows the JSON it used in a foldable, token-coloured panel with a copy button, dropped entries appear in a callout, and the page tallies ready and dropped cards.Behaviour guarantees
options.dismissis removed. Writereseton the interaction, for exampleclickHighlight({ reset: ['escape'] })or{ "type": "click-highlight", "options": { "reset": ["escape"] } }. Every other caller ofbuildInteractiveChart()and the factories is unchanged, and the code path keeps every exception it had.Verification
interaction-spec,interaction-admission, andinteraction-compose.brush-angle,navigateonx,click-highlight, and a seeded annotation rendered with the annotation in place, reported the two dropped entries onsurface.warningsand once on the console, and a click emphasised a bar with theflint-interactionevent carryingclick-highlight.Upcoming work (not in this PR)
navigationandreorder;admitInteractions()reads that first. Today's checks infer support from navigation axes, region gestures, and element semantics only.validateChart()reports the same warnings; the MCP schema gainsinteraction_spec;list_chart_typesgainsinteractionsper chart type; the chart-author skill,docs/api-reference.md, a newdocs/interaction-spec.md, and the generated chart reference gain sections.texttemplate forclick-annotate; anexternal-selectbinding; guide colours grounded fromtheme_spec.🤖 Generated with Claude Code