Line and Area Marks

Aggregate cars line chart grouped by origin
Temporal aggregate paths with grouped series.

Line and area marks materialize ordered backend-neutral paths. Lines connect values; areas close two edges or one density edge against a baseline.

Line marks

createLineMark({ id?, data?, stroke?, strokeWidth?, opacity?, curve?, closed? } = {})

const program = chart()
  .createData({ values: cars })
  .createLineMark({ stroke: "#7c3aed", opacity: 0.55 })
  .encodeX({ field: "Year", fieldType: "temporal" })
  .encodeY({ field: "Acceleration", aggregate: "mean" });

The first ID is "line", data defaults to current data, stroke width defaults to 2, opacity defaults to 1, and curve defaults to "linear". A line begins as an empty path collection because later grouping determines series cardinality. Complete encodings materialize sorted commands; color and stroke dash may regroup them.

For a direct quantitative line, encodeX and encodeY may be called in either order. The first action stores valid incomplete semantic and scale state while the path remains empty; the second completes the same final layer, resolved scales, and graphics in both orders. Aggregate y lines are different: their grain requires a compatible temporal x encoding and rejects a quantitative partial that would change the meaning of the line.

When a line is layered immediately after a compatible encoded mark, omitted data and positions are inferred. Compatible aggregate grain is inferred too, so an aggregate trend over aggregate bars needs no repeated encodeX or encodeY call:

const layered = chart()
  .createData({ values: cars })
  .createBarMark({ id: "bars" })
  .encodeX({ field: "Year", fieldType: "temporal" })
  .encodeY({ field: "Acceleration", aggregate: "mean" })
  .createLineMark({ id: "trend", strokeWidth: 3 });

Both layers reference the same x/y scales. The bar owns its bandwidth while line vertices use the shared bar centers. Incompatible bin, stack, or offset policies are not transferred. Pass data explicitly to assemble an independent line with explicit encodings and scale IDs.

editLineMark({ target?, stroke?, strokeWidth?, opacity?, curve?, closed? })

program.editLineMark({
  curve: "monotone",
  stroke: "#7c3aed",
  strokeWidth: 4,
  opacity: 0.55
});

Supported curves are linear, step, step-before, step-after, basis, cardinal, monotone, and natural. Smooth curves use cubic commands and two-point series fall back to linear. Monotone paths require strictly increasing materialized x values.

A constant stroke conflicts with field-driven encodeColor. Appearance is stored and reapplied whenever scale, Canvas, or grouping changes rebuild paths.

Polar lines and radar paths

Line marks also accept theta/radius positions. The two encoding actions may be called in either order; one channel remains valid semantic state but does not produce a path until both are present.

const radar = chart()
  .createData({ values: rows })
  .createLineMark({ closed: true, strokeWidth: 2.5 })
  .encodeTheta({ field: "category", fieldType: "nominal" })
  .encodeR({ field: "score", scale: { domain: [0, 1] } })
  .encodeGroup({ field: "series" });

closed defaults to false. When true, every series ends with one closing Z command; the first row is not duplicated. editLineMark({ closed }) switches an existing Polar line between open and closed. Polar lines currently accept only curve: "linear"; other interpolation modes remain available to Cartesian lines. Color, stroke dash, grouping, legends, scale edits, Canvas resizing, filtering, selection, and highlighting all rebuild the same path through the shared line materialization lifecycle.

Area marks

createAreaMark({ id?, data?, fill?, opacity?, stroke?, strokeWidth?, curve? } = {})

const area = chart()
  .createData({ values: intervalRows })
  .createAreaMark()
  .encodeX({ field: "year", fieldType: "temporal" })
  .encodeYRange({ lower: "lower", upper: "upper" });

Area fill defaults to "#4c78a8", opacity to 0.2, and curve to "linear". An area becomes renderable with exactly one ranged orientation, or with a complete density value/density pair. encodeGroup creates one closed path per nominal group without creating a scale or legend.

editAreaMark({ target?, fill?, opacity?, stroke?, strokeWidth?, curve? })

program.editAreaMark({
  opacity: 0.35,
  stroke: "#334155",
  strokeWidth: 1.5,
  curve: "cardinal"
});

stroke: false removes both outline and stored width. A width-only edit requires an active outline. Constant fill cannot replace a field-driven color encoding. Complete paths rematerialize immediately; incomplete paths retain the configuration until their encodings become renderable.

Arc marks

createArcMark({ id?, data?, innerRadius?, padAngle?, fill?, opacity?, stroke?, strokeWidth? } = {})

const donut = chart()
  .createCanvas({ width: 640, height: 500, margin: 55 })
  .createData({ values: cars })
  .createArcMark({ innerRadius: 0.56, padAngle: 1.5 })
  .encodeTheta({ field: "Origin", aggregate: "count" })
  .encodeColor({ field: "Origin", palette: "tableau10" });

innerRadius is a ratio from 0 inclusive to 1 exclusive. padAngle uses degrees. Count theta creates proportional pie or donut sectors. Categorical theta plus quantitative encodeR creates equal-angle radial sectors; repeated rows in one angle band are drawn larger first so smaller overlays remain visible. Arc graphics are ordinary closed path commands, so renderers do not interpret Polar semantics.

editArcMark({ target?, innerRadius?, padAngle?, fill?, opacity?, stroke?, strokeWidth? })

Geometry and appearance edits rebuild complete sectors. An incomplete arc retains the edited settings until both required encodings exist. Constant fill cannot replace a field-driven color encoding.

Position encodings · Polar line tutorial · Series encodings · Density · Error bands