Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Selections

Selections drive interactive scene filtering. Two kinds (point and interval), two reactive modes (client and server), one wire protocol (CustomEvent('prism:select')).

Declaring a selection

{
  "selection": {
    "brush": {"type": "interval", "encodings": ["x"]},
    "click": {"type": "point", "encodings": ["color"]}
  },
  "mark": "bar",
  "encoding": {
    "x": {"field": "brand_id", "type": "nominal"},
    "y": {"field": "score", "type": "quantitative"},
    "color": {
      "condition": {"selection": "click", "field": "category"},
      "value": "#d1d5db"
    }
  }
}

Point vs interval

KindTriggerState
pointClick on a mark{points: [{layerID, rowID}]}
intervalDrag-brush on plot region{range: {channel, min, max}}

Reactive modes

ModeLoop
clientBrush/click → DOM class toggle on marks. Zero network.
serverBrush/click → POST /prism/scene with synthesized filter → re-render.
bothApply client immediately, server in background.

Cross-chart filtering

<prism-coordinator>
  <prism-chart spec="overview.prism.json"></prism-chart>
  <prism-chart spec="detail.prism.json"></prism-chart>
</prism-coordinator>

Both charts declaring the same selection ID synchronize via the coordinator. A brush on the overview filters the detail.

URL state

Selection state round-trips through window.location.hash so shareable links restore the brush:

https://your-app.example/dashboard#prism-sel:<base64>

Falls back to localStorage when the encoded state exceeds 1024 characters.

Hit-test attributes

Every SVG mark carries:

  • data-prism-layer="<layer-id>"
  • data-prism-datum-row="<row-id>"

The JS port reads these to resolve clicks back to source rows.

Structured event shape

Every prism:select CustomEvent carries the same structured payload across browser, Go, and Twirp contexts. The shape mirrors the Go selection.Event struct (package github.com/frankbardon/prism/selection):

{
  "scene_id":     "scene-0",
  "selection_id": "brush",
  "kind":         "point",          // "point" | "interval" | "lasso"
  "timestamp":    1716826200000,    // ms since epoch
  "marks": [
    { "mark_index": 0, "instance_key": "layer-0:42" }
  ],
  "data_rows": [
    { "dataset_name": "cohort.pulse", "row_index": 42 }
  ],
  "data_extent": { "x": { "min": 10, "max": 50 } },   // interval/lasso
  "pixel_extent": { "x": { "min": 120, "max": 480 } },// interval/lasso, optional
  "spec_path": "/selection/brush"
}

mark_index is the index of the layer in the spec’s layer array (or 0 for a single-mark spec). instance_key is <layer_id>:<row_id> and is stable across re-renders for the same source row. data_extent is the canonical (renderer-size-independent) representation of an interval brush; pixel_extent is best-effort UI-overlay info.

The browser handler:

chart.addEventListener("prism:select", (ev) => {
  for (const mark of ev.detail.marks) {
    // mark.mark_index, mark.instance_key
  }
});

The Go side builds the same shape from raw input via selection.Build(...). Legacy id and state keys are retained on the event payload for back-compat with handlers written before the structured-event upgrade.

Driving conditional encodings

A selection name can drive a per-channel condition clause so marks switch fills, strokes, or opacities live as the selection state changes. See the brush_highlight gallery fixture and the highlight-on-brush cookbook recipe.

Worked examples