Published-release documentation · release compatibility

Categorical and Size Legends

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

createLegend(options?)

Creates inferred legend blocks. It supports combined line-series, color-stacked histogram, grouped ordinal-bar, grouped area, independent stroke, composite point-series, quantitative point-size, continuous-color gradient, and field-opacity legends. It also infers interval swatches for quantize, quantile, and threshold point color scales.

For a line legend that explains a categorical dash pattern, samples inherit the line’s fixed stroke color. Mapped color or stroke scales and explicit legend-block symbol overrides take precedence. Editing the line’s fixed color updates its existing samples; the sample recipe’s opacity remains independent of mark opacity.

Contextual fragment. Use an ES module with the imports, data, and prepared resource state described in this section. Caller-provided receivers: program. Resolve these names from setup in this fragment or section; alternatives branch from the same base.

program.createLegend();

A size encoding is independently eligible; color and shape are not required:

Contextual fragment. Use an ES module with the imports, data, and prepared resource state described in this section. Caller-provided receivers: program. Resolve these names from setup in this fragment or section; alternatives branch from the same base.

program.createLegend({ channels: ["size"], position: "right", count: 4 });

With one size-encoded point mark, both createLegend() and createGuides() infer the same block. Multiple size-encoded point marks require target. Standalone size legends support four edges and four plot corners, edge layout, grid controls, text styles, and borders. A size block paired with a categorical point legend supports every edge. Continuous size scales use evenly sampled domain values. Quantize, quantile, and threshold size scales show every interval and reject an explicit count.

An explicit channels array selects exactly the content to create. On a point mark encoding all three channels, use channels: ["color", "shape", "size"] to include all three, or ["color", "size"] to show color swatches and size samples without a shape explanation. Selecting just ["color"] or ["shape"] does not add a size block. Sample count requires size to be selected in a categorical request and requires a continuous size scale. Encodings and mark appearance remain unchanged.

Stroke may be selected independently with channels: ["stroke"]. When Point fill and outline use different fields, create separate color and stroke blocks; their domains, titles, symbols, edits, highlighting, and removal stay independent.

Omitting channels on a point mark infers its encoded categorical color, shape, and quantitative size. Color alone uses swatches; shape uses typed symbols; color plus size and shape plus size each create both corresponding blocks. The result matches explicitly listing those channels. If several point marks can own an inferred size companion, specify target.

Every categorical legend uses the same right-side default:

Mark Channels Position Symbol
line encoded color, stroke, and/or strokeDash right line
bar histogram color right swatch
grouped ordinal bar color right swatch
grouped area color right swatch
point inferred or selected color only right swatch
point inferred or selected shape only right typed point
point + matching line color + shape right line over typed point
quantitative point size size four edges and four corners, standalone or combined five equal-area circles
quantitative/temporal point color color right continuous gradient with five labels; explicit symbols representation shows sampled points
discretized quantitative point color color right/left/top/bottom ordered interval swatches
categorical supported-mark stroke stroke right/left/top/bottom actual fill with mapped outline
quantitative point opacity opacity right five constant-size circles with sampled opacity

A shape-only point legend also works when the chart contains unrelated lines. The automatic line-and-point symbol requires the point and line to share both the encoded color field and color scale. Removing the point’s color encoding preserves its remaining shape legend.

Option Type Default
target compatible mark ID current or unique compatible mark
channels compatible channel array; continuous guides use one color or opacity compatible encoded channels
order "scale", { values: [...] }, or { channel: "x"/"y"/"theta" }; categorical only "scale"
position four outside edges or top-left/top-right/bottom-left/bottom-right inside the plot "right"
blockLayout "row", "column", or "wrap"; layout of all blocks owned by one target existing edge flow; new corner group uses row
at finite { x, y }; occupied group top-left relative to the plot no free anchor
layout categorical "edge" or "legacy-bottom" "edge"
align "left", "center", or "right" "center"
direction "horizontal" or "vertical" "vertical" at either side; "horizontal" at top/bottom
columns positive integer all items in one row at top
offset non-negative number 8; omission clears an occupied same-edge axis, explicit values stay plot-relative
titlePosition "top" or "left" "top"
title non-empty string or false encoded field name; false initially hides it
symbol "auto", shorthand object, or layered recipe inferred from mark
labels label style object default sans-serif label style
titleStyle title style object default sans-serif title style
itemGap positive number 18 for vertical items; horizontal items keep 24 at top and 20 at bottom
rowPadding non-negative finite number omitted; opt into occupied edge-to-edge vertical row spacing
titlePadding non-negative finite number omitted; independently set title-to-body spacing
border boolean or border style object false
count size-legend symbol count from 2 through 10,000 5 for point legends
gradient { length?, thickness? } with positive values { length: 120, thickness: 12 }

Pass position: "bottom" explicitly to place the legend below the plot. Without rowPadding, vertical items reserve the larger of itemGap, painted sample height, and labels.fontSize * 1.2. This conservative line-height allowance retains the 18-pixel pitch for ordinary 12-pixel labels and grows for larger labels; it is not a font-specific glyph-height guarantee. Horizontal grids are unchanged. Opt-in content spacing uses each row’s actual sample and logical text bounds. It takes precedence over the vertical pitch while retaining itemGap for column separation and later reset. Both spacing policies override the corresponding legacy editLegendBlock({gap}) distance while active; resetting them restores that retained fallback. For a combined categorical-and-size legend, title: false hides the categorical title; the size block retains its independent title, matching editLegend. Bottom legends use the same item grid as top legends and can use left, center, or right alignment; side legends require center alignment. Left categorical, composite point, and size blocks use vertical flow and preserve symbol-to-label and resolved-domain order. Both sides use vertical direction and a top title. columns is a positive integer and defaults to one on the sides. Items fill each column from top to bottom in domain order before starting the next column. Each column reserves its measured symbol and label width; itemGap separates columns. Provide enough side margin for the complete grid. Moving a horizontal grid to either side preserves its column count; use titlePosition: "top" when needed. Incompatible direction or title options produce an error.

For example, this complete program places 21 years in three columns on the right:

Contextual fragment. Use an ES module with the imports, data, and prepared resource state described in this section. Resolve these names from setup in this fragment or section; alternatives branch from the same base.

import { chart } from "ggaction";

const program = chart()
  .createCanvas({
    width: 620, height: 380,
    margin: { top: 20, right: 300, bottom: 60, left: 70 }
  })
  .createData({ values: Array.from({ length: 21 }, (_, i) => ({
    x: i, y: i % 5, year: String(1980 + i)
  })) })
  .createScatterPlot({
    x: "x", y: "y", color: { field: "year", fieldType: "ordinal" },
    guides: { legend: { position: "right", columns: 3 } }
  });

Categorical legends use layout: "edge" by default, including a bottom legend with no other layout options. To preserve the former compact single row anchored near the Canvas bottom edge, specify both position: "bottom" and layout: "legacy-bottom". This mode keeps labels at Canvas height minus 28 and the title at height minus 52. It supports alignment, item gap, symbols, styles, and borders; columns, vertical direction, a left title, or a custom plot offset require layout: "edge". Layout mode is preserved by edits and replay. Large text or symbols that overlap the fixed title row, cross the plot, or exceed the Canvas produce an error; the fixed anchors do not move.

Top and bottom legends use a general item grid. columns caps the column count; direction: "horizontal" fills rows first and "vertical" fills columns first. For a single edge legend, align positions the actual outer bounds of the complete title, items and border against the plot left edge, center or right edge. Strokes and label extents count toward these bounds. offset measures the gap from the plot to the nearest outer legend edge. The final block must fit the requested Canvas. The title appears above the grid by default, or beside it with titlePosition: "left".

Each item reserves the actual extent of every symbol layer, including strokes and mapped shapes. Labels start after this shared sample slot plus labels.offset (8 for color, 10 for series). Narrower shapes retain the same label column. Side rows expand for large labels or symbols, with at least 12 pixels between the visible title and item content. Horizontal grids and borders use the same extents; inline titles are vertically centered beside the complete grid. Hidden titles reserve no space.

Plot-corner item blocks measure that 12-pixel title-to-content gap from actual painted bounds. They do not inherit the outside side legend’s fixed first-row anchor. The native corner inset and item pitch remain unchanged; content that still exceeds the inset plot fails without resizing or moving the plot.

A categorical legend resolves at most 10,000 domain items. A layered symbol recipe contains at most one line, one point, and one swatch layer.

When two or more legend blocks share the top or bottom edge, the lane starts at the plot’s left edge and keeps 40 logical pixels between complete occupied blocks. A left-positioned categorical title participates in the same inline center line as a left-positioned sampled-opacity title.

Contextual fragment. Use an ES module with the imports, data, and prepared resource state described in this section. Caller-provided receivers: densityArea. Resolve these names from setup in this fragment or section; alternatives branch from the same base.

densityArea.createLegend({
  position: "top",
  direction: "vertical",
  columns: 3,
  titlePosition: "left",
  offset: 8
});

Automatic recipes refresh when a matching companion line is added, removed, or rebound to another color scale. Creating the legend before or after the line produces the same result. Explicit symbol recipes retain their layers and order through those changes; editLegend({ symbol: "auto" }) restores inference. Title visibility and text styles remain intact.

Item order without changing color

createLegend and editLegend accept the same categorical order policy. An explicit non-empty list puts those categories first and appends omitted categories in source first-appearance order. Explicit scale-domain entries absent from the source remain at the end. Unknown and duplicate categories are errors. Color, shape, and dash assignments remain attached to category values.

To follow the same target’s categorical x, y, or theta domain, use order: { channel: "theta" } (substitute x or y as needed). The linked encoding must use the same field and category set as the legend. Its later order/scale changes update the legend. Removing that encoding or changing it to an incompatible field/domain fails; first reset with editLegend({ order: "scale" }). Omitted order on an edit preserves the policy. Continuous and interval legends do not accept categorical ordering.

Inside a complete chart’s guides.legend, linked channels follow that chart’s position roles: categorical Cartesian positions use x or y; Pie, Rose, and Radial Bar use theta. Line, Area, Density, and Parallel facade declarations offer "scale" or explicit values, since their declared positions do not provide a categorical axis to link.

Combined categorical and size layout

Point legends can combine categorical color or shape with quantitative size on any edge. For a point chart with both encodings, this fragment places a bordered legend above the plot:

Contextual fragment. Use an ES module with the imports, data, and prepared resource state described in this section. Caller-provided receivers: program. Resolve these names from setup in this fragment or section; alternatives branch from the same base.

program.createLegend({
  channels: ["color", "size"],
  position: "top",
  count: 3,
  offset: 30,
  columns: 2,
  border: true
});

At top and bottom, categorical content precedes size content with 40 pixels between their occupied bounds. Blocks wrap to another row away from the plot when necessary. The whole group, including its border, follows align and offset. direction, columns, titlePosition, and itemGap apply to both item grids. Newly combined blocks share label and title typography at every edge, including the default title color #334155. Each block retains its own title text, sample sizes, and label gap. A size legend created independently keeps its stored styles when added to a categorical legend; its standalone default title color remains #0f172a. Opting into blockLayout or moving the group to a corner or free anchor retains this typography inheritance and each symbol’s paint. Placement and typography inheritance are independent, including after Canvas, scale, data, and persistence replay. Title spacing also includes labels that are taller than the samples. A size border retained from an earlier standalone legend stays inside the shared outer border. Other legends place this complete group as one block. Combined legends require layout: "edge"; "legacy-bottom" is unsupported. A separately positioned size legend can coexist with a categorical "legacy-bottom" legend on another edge. Each keeps its own placement and styles.

Combine ordinal size and shape

createLegend({ combine: "size-shape" }) draws one categorical block whose sample uses both the mapped shape and the mapped physical area. It requires one point owner, the same nominal or ordinal field for both encodings, and identical typed, ordered ordinal domains. Explicit channels must contain exactly "shape" and "size"; with omitted channels, an encoded color legend remains an independent block. Different owners, fields, grain, numeric size scales, or domain order are rejected.

Omitting combine, or creating with "separate", preserves the previous output. An edited combination is retained unless explicitly changed. Whole-target editLegend({ combine: "separate" }) splits the pair and copies shared text, title and placement styles; combining independent blocks uses supplied styles or the combined native defaults, rather than choosing one old block’s styles. A split preserves an independent color block. Incompatible display maps must be reset before splitting.

Either member selects the same combined block. Removing either member removes the entire block without removing either mark encoding. Fixed sample shape or size overrides conflict with the mapping; stroke, fill, opacity, labels and categorical item order retain their existing controls. Zero mapped area retains its category and text slot. Current data and scale revisions revalidate both encodings, so an invalid revision fails instead of silently splitting the block.

Recipe layer opacity

Red and green legend circles with blue outlines composed at one shared layer opacity
The complete fill and outline form one translucent sample without a darker overlap.

Each symbol.layers line, point or swatch accepts optional opacity in0..1. The supplied value applies once to that layer’s complete fill and outline; it does not change other recipe layers, labels, title or frame. Omission keeps historical fallback appearance. Replacing a recipe without opacity, including symbol:'auto', removes its explicit layer opacity.

Contextual fragment. Use an ES module with the imports, data, and prepared resource state described in this section. Caller-provided receivers: program. Resolve these names from setup in this fragment or section; alternatives branch from the same base.

program.createLegend({
  channels: ['shape'],
  symbol: {layers: [
    {type: 'line', opacity: 0.3},
    {type: 'point', fill: 'red', stroke: 'blue', strokeWidth: 4, opacity: 0.7}
  ]}
});

A mapped size-shape block retains its mapped shape and physical area when the point layer supplies opacity. Canvas uses a bounded transparent glyph source; SVG uses whole-element opacity. PDF rasterizes only the explicitly affected glyph, preserving surrounding vector output. Existing block symbol-opacity patches retain their established behavior and do not opt into this new path.

Independent legend appearance

Horizontal color gradient with a dashed frame and bottom title above an independent size legend in the bottom left
A 120 by 16 color ramp clears its bottom title by 12 pixels; only the size block moves to the bottom left.

The complete example starts with a color legend, then edits its gradient, orientation, title and border. Its horizontal ramp measures 120 by 16 logical pixels. The bottom title clears the full strip, ticks and labels by 12 pixels. The border uses one continuous [4, 2, 6, 1] dash pattern around the frame. A later block edit moves only the size legend to "bottom-left"; the gradient keeps its own placement.

Layered recipe opacity

A line and outlined orange point with an explicit opacity of 0.7 beside an unchanged text label
Line and point layers carry explicit opacity while the label remains opaque.

The linked canonical source has a createRecipeOpacityProgram(opacity) factory. It builds one line-and-point recipe with an explicit opacity of 0, 0.7 or 1 on both layers. These are separate authored recipes; the categorical legend does not infer opacity from individual categories. Each case retains the same 40-pixel line, 6-pixel point radius, outline and opaque label. The original single-point opacity example remains available above.

Sample windows

Keep an encoded size while showing only a vertical slice of its symbol:

Contextual fragment. Use an ES module with the imports, data, and prepared resource state described in this section. Caller-provided receivers: program. Resource selectors used here: target: "points". Resolve these names from setup in this fragment or section; alternatives branch from the same base.

program.createLegend({ channels: ["size"], clipHeight: 30 });
program.editLegendSymbols({ channel: "size", clipHeight: 24 });
program.editLegendBlock({ target: "points", channel: "size", clipHeight: "auto" });

A positive finite clipHeight centers the window on each glyph anchor. The original radius, physical area, shape and stroke remain intact; the symbol is cropped rather than shrunk. Labels, titles and frames stay outside the window. Item spacing uses the visible sample and label bounds. Smaller symbols retain their original height.

This applies to sampled size, opacity and color-symbol legends, combined legends, and categorical recipes containing a point. Every layer of a composite sample shares its window. Gradient ramps, interval swatches, line-only recipes and stroke-width samples reject this option. Editing with "auto" removes the window; omitting it preserves the current setting.

Quantitative color and size

createLegend({combine: "color-size"}) creates one numeric sample list for a Cartesian circle point mark. Both encodings must use the same quantitative field and grain, continuous scales, and identical finite domain endpoints. Each sample uses the original color mapper and sqrt(mappedArea / Math.PI); transformed scales keep their independent mappings. Mark data and mappings do not change. Omission keeps separate legends.

Contextual fragment. Use an ES module with the imports, data, and prepared resource state described in this section. Caller-provided receivers: program. Resource selectors used here: target: "points". Resolve these names from setup in this fragment or section; alternatives branch from the same base.

program.createLegend({combine: "color-size", values: [0, 5, 10]});
program.editLegendSymbols({channel: "color", symbol: {stroke: "black", strokeWidth: 1, opacity: 0.7}});
program.editLegendBlock({target: "points", channel: "size", labelMap: [{value: 5, label: "middle"}]});
program.editLegend({combine: "separate"});

Color and size select the same block. Exact values, automatic count, numeric format, labels, title, local position, border and sample window each have one owner. Mapped fill, size and radius cannot be overridden. A sample’s stroke, stroke width and opacity can be styled. Splitting restores a gradient and a size list, retains compatible shared styles and placement, and carries exact samples only to the size list. Recombining starts with combined defaults. Incompatible field, shape, grain or endpoint edits fail atomically.

The combined legends example compares samples 0, 5 and 10 with independently specified black, gray and white circles of radius 1, 2 and 3.

Legend overview · Composite symbols · Editing legends

Documentation provenance

293 actions · sha256:e755591a2e5f5e2b494867583876e28020a478c5220b77bdc6ad469a6224891d