A visualization control changes the state that a chart is explaining. It is not a tooltip, a legend, or an arbitrary dashboard widget. Semiotic controls should be controlled, semantic, inspectable, and usable through the same chart state on pointer, keyboard, touch, and mobile HTML paths.
The control surface
DirectManipulationControl is the small shared primitive for an SVG control drawn in a frame overlay. The frame keeps its own scales; the control receives a pointer-to-value adapter. That division lets one component work over XY, ordinal, geographic, radial, and custom chart coordinates without embedding a frame dependency in the controls bundle.
4 of 8 values currently meet the threshold. The control emits data-viz-control="threshold", supports pointer capture, and is a keyboard slider.
JSX
import { DirectManipulationControl } from "semiotic/controls" function ThresholdOverlay({ scaleY, width, threshold, setThreshold }) { return ( <DirectManipulationControl controlType="threshold" value={threshold} min={0} max={100} step={1} x={width - 12} y={scaleY(threshold)} label="Priority threshold" valueText={`Priority threshold: ${threshold}`} pointerToValue={(event) => scaleY.invert(pointerInOverlay(event).y)} onChange={setThreshold} labelText="drag threshold" /> ) } // The chart owns its scales. The control owns focus, keyboard nudging, // pointer capture, clamping, and a stable data-viz-control semantic.
Current control families
Direct overlays
DirectManipulationControl is for thresholds, partition boundaries, reporting windows, and other one-value manipulations embedded in a chart.
It exposes data-viz-control with one of the public semantic control types.
Range and brush
CircularBrush provides a self-contained cyclical range. XY and ordinal brushes remain frame-owned because they must track scale changes, bin snapping, streaming, and selections.
Mobile alternatives
MobileStandardControls and useMobileRangeControls provide native buttons, ranges, and legend controls that drive the same controlled state as desktop gestures.
Bundle strategy
Import frame-independent controls from semiotic/controls. This entry point carries React control surfaces but no frame renderer, canvas scene builder, geographic projection, physics kernel, or D3 brush lifecycle. Keep frame-specific brushes in their existing subpaths so importing a small control does not pull an XY or ordinal frame into an application.
JSX
// Frame-independent controls: no XY, ordinal, geo, network, or physics renderer. import { DirectManipulationControl, CircularBrush, MobileStandardControls, } from "semiotic/controls" // Frame-owned brushing stays close to its scale and streaming contracts. import { StreamXYFrame } from "semiotic/xy" import { StreamOrdinalFrame } from "semiotic/ordinal"
The root semiotic export remains available for compatibility. Prefer the controls entry for applications that place controls in their own shell, annotation layer, or design system.
What can be lifted, and what should stay put
| Existing surface | Status | Scope | Decision |
|---|
DirectManipulationControl | Public now | Frame-independent SVG overlay | Keep generic. It receives geometry and pointer-to-value conversion from the chart, then owns drag, keyboard, ARIA, and semantic control identity. |
CircularBrush | Public now | Cyclical range control | Move with the controls bundle. Its geometry is self-contained and it does not need frame state. |
MobileStandardControls + useMobileRangeControls | Public now | HTML fallback and mobile rail | Move with the controls bundle. It is the non-hover counterpart to chart-local gestures. |
XYBrushOverlay / OrdinalBrushOverlay | Frame-owned | Scale, bin, streaming, selection-aware range brush | Do not export raw D3 overlays. Lift shared selection semantics and accessibility conventions, while keeping scale lifecycles inside each frame. |
MinimapChart brush | Composite pattern | Overview + detail navigation | Keep as a chart HOC. A minimap is visual context plus a brush, not merely a generic control. |
DetailsPanel | Observation companion | Click/hover follow-up | Do not classify as a control. It consumes observations and supplies explanation after a selection is made. |
One contract, multiple frames
A shared control should describe state, not dictate rendering. Every frame can bind the same semantics through a local adapter: an XY threshold uses scale.invert; a geographic seam usesprojection.invert; a radial control uses angle-to-domain conversion. The control itself should not import any of those scale systems.
- XY and custom: overlay control plus an x/y scale adapter.
- Ordinal: overlay control plus a band or value-scale adapter.
- Geo: overlay control plus a projected-coordinate adapter.
- Network and physics: overlay control plus a layout-state adapter; dragging a node is not automatically a data control.
- HOCs: expose controlled props first. A control should update the same value a developer could pass directly.
Accessibility, observations, annotations, and AI
Accessibility
Controls use slider semantics, meaningful aria-valuetext, Arrow keys, Shift+Arrow, Home, End, and a visible focus state. Every drag needs an HTML or keyboard alternative.
Observability
Direct controls now emit control-start, control-change, and control-end into the existing onObservation stream. The adapter remains usable without an observation provider.
Annotations
Frame overlays default to non-interactive so labels do not steal hover. A real control explicitly opts into pointer events, remains above marks, and may drive annotation state through the same controlled value.
AI and portable recipes
Recipe controls declarations now give agents and serializers a stable vocabulary for target state, domain, keyboard alternative, annotation state, and expected control observations without serializing pointer handlers.
JS
// Portable recipe control declaration. controls: [{ id: "priority-threshold", type: "threshold", target: "priorityScore", domain: [0, 100], step: 1, label: "Priority threshold", valueText: "Priority threshold: {value}", keyboard: "slider", minimumTargetSize: 24, alternatives: ["number-input", "mobile-standard-control"], observations: ["control-start", "control-change", "control-end"], }]
Control audit
auditVisualizationControls is the portable counterpart to the scene and mobile audits. Recipe registration rejects declarations missing a semantic type, state target, ordered domain, keyboard path, value text, or 24px minimum target. It also reports observation coverage and invalid quantization steps.
JS
import { auditVisualizationControls } from "semiotic/controls" const audit = auditVisualizationControls({ controls: recipe.controls }) // Checks semantic type, state target, value domain, keyboard path, // human-readable value text, target size, and control-change observation.
Completed foundations
- Frame-agnostic control observations now forward into
onObservation. - Portable recipes can declare
controls with state, access, and observation metadata. - XY and ordinal brushes share keyboard and ARIA semantics while keeping their scale-specific D3 lifecycle in their own bundles.
- The portable control audit covers target size, keyboard path, value text, semantic type, state binding, domain, and step.