How to use selection
Selection lets you connect marks across charts — for example, drawing a line through scatterplot points or filling the area between stacked bars. It works in two steps:
- Name a mark using
.name("layerName")to register its nodes - Reference those nodes using
select_all("layerName")(an array ofrefs) orref("layerName")(a single ref) as data for another chart
select_all is the querySelectorAll of GoFish — one ref per named node, never flattened. ref(name) as data is the singular querySelector: it returns one ref and raises if the layer matched zero or more than one node.
Basic pattern
layer([
# Chart 1: create marks and name them
chart(data)
.flow(spread(by="category", dir="x"))
.mark(rect(h="value").name("bars")),
# Chart 2: select_all those marks as data for a connector
chart(select_all("bars")).mark(line()),
])The layer function renders both charts in the same coordinate space, allowing the second chart to overlay the first.
TIP
For the common case of threading a connector through a chart's own marks, chaining .layer() with a bare connector mark is shorter sugar for this two-layer recipe. Reach for the explicit layer([...])
select_allform when connecting a different chart's marks or when you need a custom paint order.
Example: Connected scatterplot
line and ribbon take an array of refs directly and read placed geometry off them, so feed them select_all:
from gofish import layer, chart, scatter, circle, line, select_all
layer([
chart(driving_shifts)
.flow(scatter(by="year", x="miles", y="gas"))
.mark(circle(r=4, fill="white", stroke="black", stroke_width=2).name("points")),
chart(select_all("points")).mark(line(stroke="black", stroke_width=2)),
]).render(w=400, h=250, axes=True)The line mark connects all selected points in order.
Example: Invisible blank with line
Sometimes you want a connecting line without visible points. Use blank() to create invisible anchor points:
from gofish import layer, chart, scatter, blank, line, select_all
layer([
chart(catch_locations)
.flow(scatter(by="lake", x="x", y="y"))
.mark(blank().name("points")),
chart(select_all("points")).mark(line(stroke="steelblue", stroke_width=2)),
]).render(w=400, h=250, axes=True)Example: Re-encoding a selection by its data
Connectors often need to re-partition the selected nodes — a ribbon/stream chart draws one area per species through bars that were laid out per lake. Even though the selected stream is now refs (not raw records), you still group by the bare field name: group(by="species"), not group(by="datum.species").
from gofish import layer, chart, spread, stack, derive, group, rect, ribbon, select_all
layer([
chart(seafood)
.flow(
spread(by="lake", dir="x", spacing=64),
derive(lambda d: sorted(d, key=lambda r: r["count"], reverse=True)),
stack(by="species", dir="y"),
)
.mark(rect(h="count", fill="species").name("bars")),
chart(select_all("bars"))
.flow(group(by="species"))
.mark(ribbon(opacity=0.8)),
]).render(w=500, h=300, axes=True)Here each bar is a single species row, so species collapses cleanly to one value. On a ref, a field resolves only when every row in the ref's bag agrees on that field (homogeneity collapse); if it is multi-valued the result is None and you must disaggregate first. See path-aware by.
When the chart being re-partitioned is the first chart's own marks, skip this manual group() + select_all wiring entirely: chain .layer(ribbon(opacity= 0.8)) straight onto the producing chart, with no split option at all — the stack(by="species") tier already told the flow how to group, so the fused ribbon splits by species by default. If you need a different path tier than the one the default infers, name it with along — see the ribbon docs for the full rule. See .layer().
Example: A single-node reference
When a layer holds exactly one node, ref(name) as chart data returns that one ref — handy for diagrammatic annotations. It raises if the layer matched more than one node, which catches mistakes early:
from gofish import layer, chart, scatter, blank, text, ref, select_all
layer([
chart(data).flow(scatter(by="id", x="x", y="y")).mark(blank().name("origin")),
# ref("origin") returns one ref; errors if "origin" matched 0 or >1 nodes
chart(ref("origin")).mark(text(text="start")),
])How it works
When you call .name("layerName") on a mark, each node it produces is registered in a shared layer context during rendering. select_all("layerName") returns a lazy selector that resolves, when the second chart renders, to one ref per registered node; ref("layerName") as data resolves to the single ref (erroring otherwise).
Each ref:
- Points at the placed node, so overlay marks position themselves relative to it
- Exposes the bound datum via
ref.datum— the raw bag of rows behind the node (a 1-row list if fully split, all the partition's rows if it is an auto-summed aggregate)
Common use cases
| Goal | Pattern |
|---|---|
| Line through points | circle().name("points") → select_all("points") + line() |
| Area under line | blank().name("points") → select_all("points") + ribbon() |
| Ribbon / stream | rect().name("bars") → select_all("bars") + group(by="species") + ribbon() |
| Single annotation | Name one mark → chart(ref("name")) borrows that one node |
