Program Composition

Blue point panel and orange replacement bar panel aligned in one horizontal dashboard
The stable main slot retains points while the detail slot shows its replacement bars.

Combine already-authored chart programs without merging their datasets, marks, scales, or guides. The result is another immutable ChartProgram whose parent Canvas contains concrete snapshots of its named children.

Arrange complete programs

import { hconcat, vconcat } from "ggaction";

const row = hconcat({
  id: "overview",
  programs: [
    { id: "main", program: scatterplot },
    { id: "detail", program: barChart }
  ],
  gap: 20,
  align: "center",
  padding: 16
});

const dashboard = vconcat({
  programs: [row, trendChart],
  gap: 18
});

This is a composition fragment: scatterplot, barChart, and trendChart must each be a complete program with one materialized Canvas. See the runnable repository example for complete child construction and Browser Canvas rendering.

The representative output deliberately keeps two different final grammars: the main slot contains a titled point chart on a blue panel, while the detail slot contains the titled orange bar chart installed by replaceCompositionChild. Distinct child backgrounds and the visible parent gap make the retained slot, replacement slot, and parent layout observable in the full image and its gallery thumbnail.

Both functions require at least two programs. A direct ChartProgram receives the deterministic slot name view-1, view-2, and so on. Use { id, program } when later code must replace a stable slot.

Option Default Effect
id "composition" Names the composition for deterministic graphic namespaces
programs required Ordered complete child programs or { id?, program } entries
gap 16 Non-negative distance between adjacent children
align "center" "start", "center", or "end" cross-axis placement
padding 0 on every side Non-negative scalar or partial four-side object

Automatic and explicit child sizes

The parent Canvas size is inferred from child dimensions, gap, and padding. For hconcat, children whose height was omitted in createCanvas expand to the largest child height. For vconcat, children whose width was omitted expand to the largest child width. A unit child rematerializes against that resolved size. A nested composition keeps its intrinsic child layout and align places its complete snapshot inside the larger cross-axis slot; the outer composition does not stretch inner facet cells, gaps, or guide geometry. An explicitly authored child width or height is never overwritten.

The parent background is white. Child Canvas backgrounds are preserved, and nested compositions keep independent clipping and coordinate scopes.

Compose Cartesian and Polar charts

A complete Cartesian or Polar chart can be a direct or nested concat child. The composition does not reinterpret theta, radius, x, y, scales, guides, or selections. It snapshots each finished child into one namespaced concrete graphic tree, so Canvas and PNG renderers use the same result.

When a nested child changes, replace it in each ancestor explicitly:

const revisedPolarRow = polarRow.replaceCompositionChild({
  target: "detail",
  program: revisedPolarChart
});

const revisedDashboard = dashboard.replaceCompositionChild({
  target: "polarRow",
  program: revisedPolarRow
});

This preserves immutable earlier programs and makes the affected ancestor layout visible in the action trace. See the cross-feature dashboard source for a nested Polar replacement next to a Cartesian facet.

Repeat the current chart by a field

Cars scatterplots repeated by origin with one shared cylinder legend
One direct-source chart repeated with shared scales and a parent legend.

Call facet on one complete unit chart to repeat it for each observed field value:

import { chart } from "ggaction";

const faceted = chart()
  .createCanvas({ width: 250, height: 230 })
  .createData({ values: cars })
  .createPointMark()
  .encodeX({ field: "Horsepower" })
  .encodeY({ field: "Miles_per_Gallon" })
  .encodePointRadius({ value: 2.5 })
  .encodeColor({ field: "Cylinders", fieldType: "ordinal" })
  .facet({
    field: "Origin",
    columns: 3,
    guides: { legend: "shared" }
  })
  .createTitle({ text: "Horsepower and Fuel Economy" });

The input rows in this runnable fragment must contain complete values for the encoded fields. See the repository example for the complete data preparation and guide options.

facet uses field values in source first-appearance order. It infers one common row-preserving dataset ancestor, then filters and replays supported derived data independently inside each cell. Omitted columns creates one row; a positive value wraps cells row-major.

Option Default Effect
id "facet" Names the parent and deterministic child namespaces
field required Direct-source field whose values define cells
data unique common ancestor Selects the row-preserving partition dataset explicitly
columns number of values Sets the grid column count
gap 16 Sets horizontal and vertical cell spacing
align "center" Aligns unequal cells inside grid tracks
padding 0 on every side Adds scalar or four-side parent padding
scales every channel "shared" Sets "shared" or "independent" per x, y, xOffset, yOffset, color, size, shape, opacity, or strokeDash
guides.axes "each" "outer" keeps x axes on the bottommost occupied cell in each column and y axes on the leftmost occupied cell in each row
guides.legend false "shared" promotes one compatible parent-owned categorical, gradient, discretized-color, size, or opacity legend

Shared auto domains use the full faceted result; independent auto domains are resolved from each cell. An explicit semantic domain always wins. Regression, density, interval/error-band, and box-summary/outlier datasets are replayed after the cell filter, so each panel receives a fresh statistical result rather than a clipped copy of the full-chart result.

The current Cartesian slice supports point, line, area, histogram, aggregate bar, ranged bar, rule, regression, density, interval/error-band, and box-plot layers when they share one valid row-preserving partition ancestor. A shared legend is accepted only when every represented child scale and legend recipe is concretely compatible; scale resolution alone does not make a guide shareable. Polar sources cannot currently be faceted. Calling facet on a Polar source throws before creating partial children because theta/radius facet scale and guide resolution are not implemented. Polar charts remain supported as concat children.

Create a chart title after facet so the title is owned directly by the parent. A title that already fits the unit Canvas is promoted for authoring order compatibility rather than repeated in every cell.

The parent title aligns to the union of the child plot bounds, excluding cell margins and the shared legend. Each repeated header is likewise centered on its own child plot—not on the complete child Canvas—so asymmetric axis space does not visually offset panel titles.

Edit the repeated header style without addressing generated graphic IDs:

const emphasized = faceted.editFacetHeaders({
  fontSize: 13,
  fontWeight: 700,
  color: "#0f172a",
  offset: 10
});

Facet-header weights follow the shared Canvas font-weight policy.

Edit the layout

const revised = row.editCompositionLayout({
  gap: 28,
  align: "start",
  padding: { left: 12, right: 12 }
});

At least one option is required. Omitted values retain their current settings; a partial padding object updates only the named sides. The action preserves all child IDs and references and rebuilds the parent snapshot.

Facet parents use this same action for gap, align, and padding; derived cell programs and facet value order remain unchanged. Parent-title and header anchors are recomputed from the newly translated child plot bounds.

Replace one stable slot

const replaced = revised.replaceCompositionChild({
  target: "detail",
  program: donutChart
});

The replacement must be a complete unit or nested composition program. Its new size participates in layout inference, while the target ID and position in the ordered child list remain stable. Earlier parent and child programs are not mutated.

Facet cells are derived from one canonical source and cannot be replaced with replaceCompositionChild.

State, trace, and action scope

children maps stable slot IDs to retained immutable programs. compositionSpec stores concat direction or facet intent together with order, gap, alignment, and padding. The parent graphicSpec stores the fully materialized, namespaced child Canvas tree; renderers read only that concrete state.

hconcat, vconcat, and facet trace useProgram children followed by materializeComposition. Layout edits and replacement rematerialize from the retained child programs. Ordinary data, mark, encoding, scale, and guide actions apply only to unit programs and reject a composition parent. Facet titles and facet-header edits are explicit parent-owned exceptions. Edit a child first, then replace its slot when a concat dashboard needs a changed chart.