Skip to content

feat(interactions): admission per chart type, one owner per trigger - #127

Merged
Chenglong Wang (Chenglong-MS) merged 20 commits into
feat/interactive-apifrom
feat/interaction-admission
Sep 20, 2026
Merged

Chenglong Wang (Chenglong-MS) merged 20 commits into
feat/interactive-apifrom
feat/interaction-admission

Conversation

@xavier-shaw

@xavier-shaw xavier (xavier-shaw) commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #124. Design doc: docs/design-interactions.md (en, zh-CN).

Summary

Interaction admission: Each chart type now declares the interaction capabilities it supports, and each preset declares the capabilities it requires. Admission matches the two. validateChart(), the MCP tools, and the docs read the same declaration. The MCP chart view, the site editor, and the gallery mount from interaction_spec.

Interaction comfliction: Two interactions that share a trigger now get one owner. affordances is a map from hit kind to cursor and hover, and it is the only dispatch gate. For each shared trigger, admission either narrows one entry with an info warning or drops the later entry.

Why

The old rules admitted every preset on every chart type, even the KPI card and the pie chart. The "element semantics" check was always true, and the registry's requires was never read.

A legend click could also reach both click-highlight and legend-toggle: the series hid and the other bars dimmed. Target checks lived in three places, and four dispatch paths read none of them.

What changed

  • Core: eight capabilities, ChartInteractionSupport, INTERACTION_PRESET_REQUIREMENTS, supportedInteractionPresets().
  • Templates: all 36 chart types declare interactionSupport. navigation, reorder, and supportedRegionGestures move into it.
  • Admission: one match rule with messages that name the chart type and the missing capability. One interaction per gesture slot. triggersOf() resolves shared triggers.
  • Runtime: affordances is a required map keyed by mark, legend-item, axis-label, plot. affordsTarget() gates every path, keyboard and long-press included. An axis label click now emphasises the category's marks.
  • Validation and discovery: validateChart() reports the warnings. The MCP schema gains interaction_spec. list_chart_types returns interactions per chart type.
  • Hosts: the MCP chart view, InteractiveVegaLiteView for the editor and TripleChart, an Interactive bar editor example.
  • Docs: docs/interaction-spec.md, API reference, chart-author skill, generated reference.
  • Lab: Coverage and Conflict cases tabs.

Key changes

  • ChartTemplateDef.navigation and .reorder move into interactionSupport.
  • A factory call the chart cannot honour throws at mount. Before, it mounted and did nothing.
  • listInteractionPresets().requires is a list. The capability element-semantics is now elements.
  • affordances is required. claimsLegendActivation and claimsAxisActivation are removed.
  • Two code definitions that share a trigger throw. A spec entry always yields to a code definition.

Verification

  • 1651 library tests and 86 MCP tests pass. Lint and typecheck pass.
  • Conflict cases page in headless Chrome: each gesture reaches exactly one handler.
  • Lab survey: the code tab and the spec tab agree on all 787 cards.

🤖 Generated with Claude Code

…s as a list

Eight chart capabilities (elements, cartesian-region, angular-region, navigation,
reorder, legend, discrete-axis, index) move to core as INTERACTION_CAPABILITIES, and
ChartInteractionSupport describes what a chart type offers; ChartTemplateDef gains an
optional interactions block for it. Each registry entry lists the capabilities its preset
needs in requires, and every wrapper stamps preset on the definition it returns so a
definition made in code can be checked against the same list as a spec entry. Nothing
reads the declarations yet.
…pport

The interactions block on ChartTemplateDef replaces the navigation and reorder fields and
the supportedRegionGestures entry of semanticInteractions. All 36 chart types declare
what they offer: marks that resolve to data, the drag regions, the navigable and
reorderable axes, the legend, the discrete axis labels, and the index axis. The assembler
reads the block for navigation axes, reorder axes, and region gestures, so the compiled
facts are unchanged; a test asserts that every template carries the block. The design doc
records the table with the source of each cell and the judgment calls open for review.
… the chart's capabilities

The assembler confirms the declared capabilities against the bound encodings and writes
the active list, with the chart type, into _interactionSemantics. Admission replaces its
four inferred checks with one rule: every capability in the preset's requires list must be
present, or a spec entry drops with a message that names the chart type and the missing
property, and a code definition throws. A plan the assembler did not annotate is read the
way the compile step read it, so hand-built plans keep their behaviour. The region
capability covers either kind of drag region, because brush-x on a polar chart is honoured
as an angular brush; only brush-angle needs the angular one. A survey of both lab tabs
showed 787 cards with no difference between code and spec; the KPI card lost the region
presets, and the scatter family gained the index declaration its lab cases relied on.
…t types against presets

INTERACTION_PRESET_REQUIREMENTS in core is the single table of what each preset needs;
the registry entries read it. declaredInteractionCapabilities() and
supportedInteractionPresets() turn a template's interactions block into the presets it can
honour by declaration, so hosts and docs can list them without the runtime. The Interactions
lab gains a Coverage tab: all 36 Vega-Lite chart types against all 20 presets, with the
representative test case assembled per row to show which presets are active for that data,
which the chart type supports but the data does not confirm, and which it never offers.
…ver interaction support

validateChart() checks interaction_spec the way the mount does: a malformed spec is an
invalid_interaction_spec error, an entry the assembled chart cannot honour is the same
unsupported_interaction warning the surface reports, and a backend that runs no
interactions reports the spec as ignored. The MCP tool schema gains interaction_spec with
the preset names as an enum, toAssemblyInput passes it through, validate_chart reports the
drops, and list_chart_types returns the interaction presets each chart type supports from
the template declaration.
… chart type supports

A new Using interactions page (docs/interaction-spec.md, with a zh-CN mirror, registered in
the site catalog) explains the spec shape, the twenty presets with what each needs and its
default reset, the reset gestures, how a chart type declares support, the warnings and where
to read them, and the equivalence of a spec entry and a factory call. The API reference gains
interaction_spec on ChartAssemblyInput with a section of its own. The chart-author skill gains
an Interactions section with the authoring rules. The generated Vega-Lite reference prints an
Interactions line per chart type from the same declaration the mount enforces.
…m interaction_spec

The MCP chart view mounts buildInteractiveChart() with the CSP-safe expression
interpreter when the input lists interactions, on the same preview input the static render
sizes; the static render keeps running for the PNG export, and the surface warnings join the
assembler's. The site gains one spec-aware Vega-Lite component, InteractiveVegaLiteView,
used by the editor and by TripleChart whenever the input carries interaction entries; a
TestCase may carry an interactionSpec, and the editor ships an Interactive bar example. The
index chart stage and the chart-to-external lab, which used presets only, now ask for them
in interaction_spec.
@xavier-shaw
xavier (xavier-shaw) marked this pull request as draft September 14, 2026 17:36
Review of Stages A to C with the maintainer:
- the template block is interactionSupport, not interactions
- the drag-region capability is cartesian-region, and the four polar templates declare both
  regions, because a rectangle or lasso resolves their arcs and brush-x is honoured as a sector
- the assembler is the one authority on a chart's capabilities: it derives the confirmed list
  from declaredInteractionCapabilities and the bound encodings, writes _interactionSemantics
  for every Vega-Lite chart, and capabilities is a required field of the admission plan; the
  gates that replayed the old inferred rules are gone, a definition made by hand needs nothing
- preset requirements live in INTERACTION_PRESET_REQUIREMENTS only; the registry entries and
  the definition type no longer carry a copy, and admission no longer imports the registry
- one phrase table for the capabilities, one generator pass shared by the two lab tabs, no
  wrapper around the preview sizing, no dedupe of disjoint warning lists
- the coverage tab pins its header, marks unsupported cells with a cross, and slants the
  preset names
- docs: stages B and C, this review, the future steps set aside, and the changelog
…r the geometry permits

reorder and discreteAxis on every cartesian template except Violin and Range Area, which
refused reorder before, and legend on every template that returns legendFields. The
assembler still confirms each one against the bound encodings, so a chart without a discrete
axis or a discrete legend drops the preset as before; what changes is that a categorical
scatter reorders again and a violin split by shift toggles its legend again.
docs/design-interactions.md, with a zh-CN mirror, explains the interaction model the way
design-semantics.md explains semantic types: the three documents a chart is made of, presets and
definitions, the spec and its resolver, the chart-semantics dictionary, the eight capabilities
with what the type declares and what the data confirms, the requirement table, the match and
the conflict rules, reset, the pipeline, hosts and discovery, and how to extend the model. The
appendix table of all 36 declarations is generated by gen:reference between markers, so it
cannot go stale. The dated decision log docs/design-interaction-spec.md is removed; its history
lives in the commits and the two pull requests. The Vega-Lite reference is regenerated for the
templates that gained declarations.
@xavier-shaw xavier (xavier-shaw) changed the title feat(interactions): admission per chart type, validation and discovery, hosts, and declarative extensions feat(interactions): admission and validation of chart interactions Sep 16, 2026
@xavier-shaw
xavier (xavier-shaw) added this pull request to stack #128 September 16, 2026 00:56
@xavier-shaw
xavier (xavier-shaw) marked this pull request as ready for review September 16, 2026 00:58
The runtime mounts one navigation, one region drag, and one element drag.
A second spec entry in a slot now yields with a conflicting_interactions
warning, the way a second navigate did; before, the runtime kept the first
and said nothing.
A definition's affordances become a required map from the kind of hit to
its cursor and hover. The runtime sends a hit only to the interactions that
afford its kind, on every path: the click and hover lists, and the keyboard,
context, long-press, and double-click paths that had no gate. The two claim
flags and the hand-made target checks in seven handlers go, because a key
says the same thing once. clickHighlight() gains withoutAffordances(), a
copy that affords fewer targets and keeps its preset and reset.
A trigger is one gesture on one kind of hit. triggersOf(definition) reads
the triggers a definition takes from its event source, affordance keys,
state group, and reset list. For each trigger two admitted definitions
share, the one that can give it up and keep the rest does so through
withoutAffordances with an info warning; otherwise the later entry yields
whole, a spec entry always yields to code, and two code definitions throw.
The three pair rules and the slot loop become rows of this one loop.

Fixes the legend click that both click-highlight and legend-toggle
answered: the series hid and every other bar dimmed.
Ten cases, each a pair of presets that share a trigger, with the live
chart, the spec, the warning the surface reports, and what to try.
The mount filtered the authored list through an identity set of the
admitted definitions. A definition that admission replaced with a narrower
copy was neither in the set nor in the authored list, so it was never
mounted: click-highlight vanished from every chart where it yielded a
trigger. The mounted list is now the admitted one, in the author's order,
matched by id, with external definitions passed through.
The render loop sent an axis target to the label painter only and skipped
the render keys the axis resolver attached to its element, so a click on a
discrete axis label through click-highlight or axis-highlight changed
nothing. The element now also falls through to the mark key loop: the
category's marks emphasise, the rest mute, and click-none clears it.
Two charts per row, a title, the warning under the chart, and the spec in
a collapsed panel; the story and the try-it text are gone.
@xavier-shaw xavier (xavier-shaw) changed the title feat(interactions): admission and validation of chart interactions feat(interactions): admission per chart type, one owner per trigger Sep 18, 2026
The scale navigation controller honours the transition option the geo
controller already had: the domain tweens from the frame on screen to the
target, the centre linearly and the span geometrically in the axis's
transformed space, and a render during the tween keeps its frame.

navigate() flies home over 400 ms unless resetTransition says otherwise;
brushZoom() tweens into the brushed region on commit and flies home on
reset, from a gesture or the reset control. { duration: 0 } turns one off.
Panning and wheel zooming still follow the pointer.
@Chenglong-MS
Chenglong Wang (Chenglong-MS) merged commit 705ee1c into main Sep 20, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants