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 = "auto", 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 for a color scale. Omitted, the band takes the color of the marks it connects.
strokestringStroke color.
strokeWidthnumber0Stroke width in pixels.
opacitynumberOpacity, 0 to 1.
mixBlendMode"normal" | "multiply""normal"Blend mode where bands overlap.
dir"x" | "y"Connection axis.
curveanyScreen-space band-edge shape ("linear" | bezier() | "step" | "monotone" | "smooth" | "catmullRom"). "step" steps both edges, as a stepped area does. "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"); "smooth" is a rounder reading over the same parameter, and can go a little past a point; "catmullRom" is a centripetal Catmull-Rom on screen and can overshoot. Omitted = "auto" (monotone on a homogeneous continuous connection axis, else a 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 accepts the strings "linear", "bezier", "step", "monotone", "smooth" or "catmullRom", or a CurveSpec factory: bezier(), orthogonal(), arc({ direction: "up" | "down" }), or perfectArrows({ bow }). "linear" has no factory, because linear() is the coordinate transform. The default "auto" inspects the connection axis: over a homogeneous continuous axis (a stacked area / streamgraph sampling a continuous variable) it smooths the band edges with "monotone" — matching its line sibling — and otherwise draws a bezier band (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 "monotone" or "smooth" takes the knots of its curve the same way a line does. It uses the values of the field it runs along when they are numbers in order along the band. If they are not, it uses the positions on a continuous connection axis, and otherwise the distances between the points on the screen. Both edges of the band use the same knots.

"monotone" is piecewise monotone. Between two neighboring points, each edge only rises or only falls, so it never goes past either point. It does not make the whole edge monotone: the band still turns where the data turns, and the peak sits exactly on the data point. It is the same curve as d3's curveMonotoneX and Vega-Lite's interpolate: "monotone". "smooth" is a rounder curve over the same knots, and can go a little past a point. "step" steps both edges, as Vega-Lite's stepped area does. The curves table on the line page compares them. "catmullRom" is a centripetal Catmull-Rom spline through the edge points on the screen. Its knots are always the distances between those points, and it can overshoot between two of them.

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 });