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

Consume Structured Selection Events

Every prism:select CustomEvent carries a structured Event payload (mirrors the Go selection.Event struct). The same shape travels across browser, Go-native, and Twirp contexts so one handler works against any binding.

The event shape

{
  "scene_id":     "scene-0",
  "selection_id": "brush",
  "kind":         "point",          // "point" | "interval" | "lasso"
  "timestamp":    1716826200000,
  "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 } },
  "pixel_extent": { "x": { "min": 120, "max": 480 } },
  "spec_path":    "/selection/brush"
}

mark_index is the layer’s index in the spec’s layer array (or 0 for unlayered charts). instance_key is stable across re-renders for the same source row — derive joins and lookups from it.

Browser: forward selections to a sidebar

<prism-chart id="chart" spec="./bar.prism.json"></prism-chart>
<aside id="sidebar"></aside>

<script type="module">
  const chart   = document.getElementById("chart");
  const sidebar = document.getElementById("sidebar");

  chart.addEventListener("prism:select", (ev) => {
    const e = ev.detail;
    if (e.kind === "point") {
      sidebar.innerHTML = e.marks
        .map(m => `<div>${m.instance_key}</div>`)
        .join("");
    } else if (e.kind === "interval" && e.data_extent?.x) {
      const { min, max } = e.data_extent.x;
      sidebar.textContent = `x ∈ [${min}, ${max}]`;
    }
  });
</script>

The event bubbles + composes through Shadow DOM, so listening on document or any ancestor also works.

Browser: cross-app forwarding (Slack, websocket, postMessage)

Because the event is fully structured, you can serialise it directly:

chart.addEventListener("prism:select", (ev) => {
  socket.send(JSON.stringify(ev.detail));
});

No translation step — the receiver gets the same selection.Event shape the renderer emitted.

Go: build an event from raw input

The Go side exposes the same shape via the selection package. Use it from a Twirp handler, MCP tool, or any server-side selection synthesis path:

import "github.com/frankbardon/prism/selection"

ev, err := selection.Build(selection.BuildInput{
    SceneID:     "scene-0",
    SelectionID: "brush",
    Kind:        selection.KindPoint,
    Points: []selection.PointHit{
        {LayerID: "layer-0", RowID: 42},
    },
}, sceneDoc, spec)
if err != nil {
    return err
}
body, _ := json.Marshal(ev)
// body is byte-identical to the browser-side CustomEvent.detail.

selection.Build walks the SceneDoc to resolve mark_index and dataset_name from the (layer_id, row_id) pair. Unknown layers (stale events after re-render) come back with mark_index = -1 so the consumer can decide whether to drop the entry.

Back-compat

Pre-existing handlers that consumed {id, state} keys still work — those fields are retained on the event payload alongside the new structured ones.

Worked examples