Skip to content

ref / selectAll ​

ref is the single reference noun in GoFish, and it works in two positions:

  • Inline in a layout — ref("a") inside a .relate() clause, ref(token).row[2] anywhere — it resolves at layout time against the name tree, hygienically scoped (see scoping). This is the ref mark.
  • As chart data — chart(ref("maxBar")).mark(text(...)) — it resolves at build time against the named-layer registry and stands in for the one node registered under that name.

selectAll(name) is the plural chart-data verb: it returns an array of refs, one per node a named mark produced (node-unit; aggregate or not, no flattening). Pass either form as the data argument to a second chart() call to build overlays and connectors.

Think of selectAll as the DOM's querySelectorAll (always a collection) and ref(name)-as-data as querySelector (the one-or-bust singular).

js
const lakeTotals = Object.entries(_.groupBy(seafood, "lake")).map(
  ([lake, items]) => ({
    lake,
    count: items.reduce((sum, item) => sum + item.count, 0),
  })
);

gf.layer([
  // Step 1: name the mark
  gf
    .chart(lakeTotals)
    .flow(gf.spread({ by: "lake", dir: "x" }))
    .mark(gf.rect({ h: "count" }).name("bars")),

  // Step 2: selectAll those nodes as data for a connector
  gf
    .chart(gf.selectAll("bars"))
    .mark(gf.line({ stroke: "coral", strokeWidth: 2 })),
]).render(root, { w: 400, h: 250, axes: true });

Signature ​

ts
ref(name: string): GoFishRef;                // singular; see ref mark for all forms
selectAll(layerName: string): GoFishRef[];   // one ref per matching node

ref has several other inline forms (Token, path, direct node) — see the ref mark for the full signature. This page covers its use as chart data alongside selectAll.

Parameters ​

ParameterTypeDescription
namestringThe name of the layer to reference (registered via .name() on a mark)

Singular as data: exactly one ​

When you pass ref(name) as chart data it must resolve to exactly one node:

  • Zero matches → error. Nothing was registered under that name in scope.
  • More than one match → error, with a hint to use selectAll(name) instead. A named mark that produced several nodes is a collection, and the singular reference refuses to silently pick one.
ts
gf.layer([
  gf
    .chart(data)
    .flow(/* ... */)
    .mark(gf.rect({ h: "total" }).name("kpi")),
  // ref("kpi") as chart data: one node; throws on 0 or >1 nodes
  gf.chart(gf.ref("kpi")).mark(gf.text({ text: "peak" })),
]);

Node-unit selection ​

selectAll selects at node granularity: one ref per node the named mark produced, never flattened and never merged. Each ref points at a placed node, so overlay marks position themselves relative to it, and ref.datum is that node's data bag.

ts
const bars = gf.selectAll("bars"); // GoFishRef[]
bars[0].datum; // the raw row-bag behind the first bar

See ref.datum for what the bag contains (a 1-row array for a fully-split leaf, all the partition's rows for an auto-summed aggregate).

Why a ref, not a "selection"? ​

GoFish models a selection as a plain array of refs rather than a bespoke selection object, and this is deliberate:

  • A ref is structurally a one-element selection. ref(name) (one ref) and selectAll (an array of refs) are the singular/plural of the very same noun, so there is nothing new to learn — the ref you get from a selection behaves exactly like a ref you wrote by hand inline.
  • Geometry is decoupled from data. A ref points at a placed node; you read its placement off the ref (that is how line/ribbon draw) and its bound datum via ref.datum. Selecting does not flatten or reshape your data.
  • Batch operations live in .flow, not on the noun. Unlike D3, where the selection object owns .data(), .attr(), .filter(), etc., GoFish keeps a selection inert. To partition, re-key, or re-encode a selection you run it through operators in .flow (e.g. group, spread) — see path-aware by below.

Hygienic scoping ​

Layer-name lookup is hygienic: a name registered via .name() is visible only within its scope and does not cross component boundaries. A name registered on a mark inside a createMark component is internal to that component — it is not selectable from outside. This is the same component-boundary rule that string-name ref resolution always followed inline, so the inline-layout and chart-data lookup paths now share one scoping rule.

Within that boundary the two positions differ in reach. As chart data, a name selects every node it was given in the chart, so a mark repeated per row can carry one name. Inline, a string ref (and a .relate() operand) takes the nearest match to the layer that relates it; see the ref mark.

Inline selectAll is not supported yet ​

selectAll is a chart-data verb only. Using it inline inside a layout throws — pass it as the data argument to a chart() instead. (Inline plural references may arrive later; for now use a named layer + selectAll as data.)

Connectors take selectAll directly ​

line and ribbon consume an array of refs and read placed geometry off them, so feed them selectAll:

ts
gf.chart(gf.selectAll("points")).mark(gf.line({ stroke: "black" }));

When the connector traces a chart's own marks, chaining .layer() with a bare connector mark (or with by) is sugar for this two-layer selectAll recipe — only reach for selectAll by hand to connect another chart's marks.

Path-aware by after a selection ​

After selectAll, the stream items are refs, not raw records. Operators' by option reads a ref through its .datum rows, so you write the same bare field name you would on raw records:

ts
gf.chart(gf.selectAll("bars"))
  .flow(gf.group({ by: "species" })) // not "datum.species"
  .mark(gf.ribbon({ opacity: 0.8 }));

On a ref, a field resolves to a scalar only if every row in the ref's bag agrees on that field (homogeneity collapse — SQL's ONLY_FULL_GROUP_BY rule); otherwise it is undefined. So by: "lake" works on lake-aggregate bars (all rows share a lake) but by: "species" does not until you disaggregate. See spread for the full explanation.

pluck(source, path) — every value at a path ​

Where by's path projection collapses a row-bag to a single scalar (and goes undefined when the bag disagrees), pluck is its un-collapsed counterpart: it returns the full set of distinct values present at a path — "every possible value here."

ts
import { pluck } from "gofish-graphics";

pluck(ref, "species"); // → ["Bass", "Trout", ...] (distinct across the bag)

source may be a ref (reads its .datum bag), a row array, or a single row. Reach for pluck when a field is multi-valued in the current bag and you want to enumerate its values rather than group by it — the case where by: "field" would resolve to undefined.

JavaScript only

pluck is exported from the JS package (gofish-graphics). The Python wrapper does not expose it yet.

project(source, path) — the single value at a path ​

project is the collapsing counterpart to pluck: it returns the one value at a path when the bag agrees on it (the same homogeneity collapse by performs), and undefined otherwise. Where pluck(ref, "site") gives you the whole multiset ["Morris"], project(ref, "site") gives you "Morris" directly — no indexing.

ts
import { project } from "gofish-graphics";

project(ref, "site"); // → "Morris"  (undefined if the bag disagrees on "site")

Reach for project to read a field off the datum a mark is bound to — for example inside a .zOrder(d => …) callback, where d is the bag of refs flowing into the mark:

ts
const isEmphasized = (site: unknown) =>
  site === "Morris" || site === "Grand Rapids";

area({ opacity: 0.7 }).zOrder((d) =>
  isEmphasized(project(d, "site")) ? 1 : 0
);

source accepts the same inputs as pluck (a ref, a row array, or a single row).

JavaScript only

project is exported from the JS package (gofish-graphics). The Python wrapper does not expose it yet.