Skip to content

ribbon ​

Fills a band between data points (edge-to-edge) — areas, streamgraphs, and sankey ribbons. Takes the array of refs returned by selectAll().

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

gf.layer([
  gf
    .chart(lakeTotals)
    .flow(gf.spread({ by: "lake", dir: "x", spacing: 64 }))
    .mark(gf.blank({ h: "count" }).name("points")),
  gf.chart(gf.selectAll("points")).mark(gf.ribbon({ opacity: 0.6 })),
]).render(root, { w: 400, h: 250, axes: true });

Signature ​

ts
ribbon({ stroke?, strokeWidth = 0, opacity?, mixBlendMode = "normal", dir = "x", curve?, along?, from?, to?, w?, h?, emX?, emY? })

Parameters ​

Edge-mode connector — a filled band between the facing edges of consecutive marks (areas, streamgraphs, sankey ribbons).

OptionTypeDefaultDescription
fillstring | FieldExprFill color of the band, or a field name or accessor for a color scale (constant within the band). Omitted, the band takes the color of the marks it connects.
strokestring | FieldExprStroke color of the band's outline, or a field name or accessor for a color scale (constant within the band).
strokeWidthnumber0Stroke width in pixels.
opacitynumberOpacity, 0 to 1.
mixBlendMode"normal" | "multiply""normal"Blend mode where bands overlap.
dir"x" | "y"Connection axis.
curveCurveScreen-space band-edge shape, made by a call in the Curve family: Curve.linear(), Curve.bezier(), Curve.step(), Curve.monotone(), Curve.smooth() or Curve.catmullRom(). Curve.step() steps both edges, as a stepped area does. Curve.monotone() is piecewise monotone: between two neighboring points each edge only rises or only falls, so it never goes past either point, though the band still turns where the data turns (d3 curveMonotoneX, Vega-Lite interpolate "monotone"); Curve.smooth() is a rounder reading over the same parameter, and can go a little past a point; Curve.catmullRom() is a centripetal Catmull-Rom on screen and can overshoot. Omitted, it is Curve.monotone() on a homogeneous continuous connection axis, else a Curve.bezier() band.
fromstring
tostring
alongstringNames a flow tier by its by field: that tier becomes the path tier (threading its groups in order) and every OTHER grouping tier splits. Omitted: the path tier is inferred from the flow shape. Naming a field that matches no tier, or using along where the mark doesn't fuse over this chart's own flow (a refs bag, or the pairwise from/to form), is an error.
emXbooleanBlank-fusion anchor key: placed directly in .mark() position, ribbon(opts) elaborates to an invisible anchor tier (a blank() carrying just {w, h, emX, emY}) plus this connector — see the mark construct's doc. Ignored by ribbon itself.
emYbooleanBlank-fusion anchor key — see emX. Ignored by ribbon itself.
wnumber | string | FieldExprBlank-fusion anchor key — see emX. Ignored by ribbon itself.
hnumber | string | FieldExprBlank-fusion anchor key — see emX. Ignored by ribbon itself.

curve takes a member of the Curve family: Curve.linear(), Curve.bezier(), Curve.step(), Curve.monotone(), Curve.smooth() or Curve.catmullRom(). When curve is omitted, ribbon inspects the connection axis: over a homogeneous continuous axis (a stacked area / streamgraph sampling a continuous variable) it smooths the band edges with Curve.monotone() — matching its line sibling — and otherwise draws a bezier band, Curve.bezier() (the band equivalent of a straight line: the honest connector between discrete regions, as in a sankey or a categorical ribbon).

A ribbon drawn with Curve.monotone() or Curve.smooth() takes the knots of its curve the same way a line does, and both edges of the band use the same knots. Curve.step() steps both edges, as Vega-Lite's stepped area does. The Curve page compares the curves.

Like line, ribbon has a bag form (over a GoFishRef[], shown below) and a pairwise form ribbon({ from, to }) over rows whose from/to columns hold refs (one band per row, after resolve).

Default grouping ​

A ribbon fused into a flow — in .mark() position or as .layer() sugar over the previous tier's marks — splits at the flow's own grouping by default: one tier lays the band's path, and every other grouping in the flow splits it into separate bands. You don't restate the split — the flow one line up already declared it, and ribbon has no option that spells the split directly.

When you need a different path tier than the one inference would pick, name it with along: along: "year" finds the flow tier whose by is "year", makes it the path, and splits by every other grouping tier instead. Naming a field no tier groups by is an error. This doesn't apply to a ribbon drawn over an explicit refs bag (chart(selectAll(...))) or the pairwise { from, to } form — along is only meaningful when the ribbon fuses into a chart's own flow, and throws if used on either of those. A refs bag spells its split structurally instead, with an upstream flow(group({ by: "species" })).

Sugar: .layer(ribbon(...)) ​

When the ribbon traces a chart's own marks, skip the two-layer selectAll recipe and chain .layer() on the builder — this is the canonical simple ribbon-chart spelling:

ts
chart(seafood, { axes: true })
  .flow(
    spread({ by: "lake", dir: "x", spacing: 64 }),
    stack({ by: field("species").sort("count"), dir: "y" })
  )
  .mark(rect({ h: "count", fill: "species" }))
  .layer(ribbon({ opacity: 0.8 }))
  .render(container, { w: 400, h: 400 });

No by needed: the stack({ by: "species" }) tier already told the flow how to group, so the ribbon splits into one band per species by default.

See .layer() for the full semantics, including the zBelow-by-default paint order and the desugaring to the explicit layer([...])

  • selectAll form below (which is still what you want to trace another chart's marks).

Sugar: .mark(ribbon(...)) (blank-fusion) ​

When there's no earlier tier at all — just raw data that needs both fresh anchors and a connector — place ribbon(...) directly in .mark() position and skip .layer() too. This is the fused spelling of the Area chart and Ridgeline chart gallery examples:

ts
chart(seafood, { axes: true })
  .flow(spread({ by: "lake", dir: "x", spacing: 64 }))
  .mark(ribbon({ h: "count", opacity: 0.8 }));

// ...is sugar for the explicit two-tier form:
chart(seafood, { axes: true })
  .flow(spread({ by: "lake", dir: "x", spacing: 64 }))
  .mark(blank({ h: "count" }))
  .layer(ribbon({ opacity: 0.8 }));

A grouped flow fuses the same way, with no by needed — a streamgraph is one line:

ts
chart(seafood, { axes: true })
  .flow(
    spread({ by: "lake", dir: "x", spacing: 64, alignment: "middle" }),
    stack({ by: "species", dir: "y" })
  )
  .mark(ribbon({ h: "count", fill: "species", opacity: 0.8 }));

fill here is a shared field name, homogeneous within each species group (the default split, above) — it resolves through the color scale per group, the same as it would if fill were declared on an explicit anchor blank().

See .layer()'s blank-fusion section for the full desugaring rule (the {w, h, emX, emY} anchor/connector key split, .name() chaining, and when the rule doesn't fire).

The w/h/emX/emY anchor channels are only meaningful when ribbon gets to synthesize its own anchors this way; passing them to a ribbon that instead connects already-drawn marks (an empty-scope chart() tier inside .layer(), or chart(selectAll(...))/chart(ref(...))) is an error, since there's nothing left for them to anchor.

Example ​

ts
chart(selectAll("bars"))
  .mark(ribbon({ opacity: 0.3 }))
  .render(container, { w: 500, h: 300 });