Legend
Enabled by default, at the bottom. Clicking an item hides/shows the series (toggleSeries).
Modular build
When building with grafit-charts/core, the legend is a separate module: register(legendModule).
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Team velocity' },
series: [
{ type: 'bar', xField: 'sprint', yField: 'done', name: 'Done', stacked: true },
{ type: 'bar', xField: 'sprint', yField: 'carry', name: 'Carried over', stacked: true },
],
// legend on the right; clicking an item hides the series
legend: { position: 'right' },
};
}export function getData() {
return [
{ sprint: 'S1', done: 21, carry: 4 },
{ sprint: 'S2', done: 25, carry: 6 },
{ sprint: 'S3', done: 19, carry: 3 },
{ sprint: 'S4', done: 28, carry: 5 },
{ sprint: 'S5', done: 31, carry: 2 },
];
}Options
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | show the legend |
position | LegendPlacement | 'bottom' | docking side + alignment, or a floating anchor (below) |
floating | boolean | false | overlay the whole chart area instead of reserving space |
offset | { x?: Pixels; y?: Pixels } | 0 | floating only: inset from the anchored edges |
avoidCaptions | boolean | true | floating only: title/subtitle flow around the legend box |
toggleSeries | boolean | true | click toggles visibility |
maxRows | number | 2 | rows per page in a horizontal legend |
maxWidth | Length | the chart | width the legend never goes past (below) |
maxHeight | Length | the chart | height the legend never goes past (below) |
reverse | boolean | false | render the items back to front |
item.marker | LegendMarkerOptions | — | marker glyph (below) |
item.label.fontSize | Pixels | 12 | label font size |
item.label.fontFamily | string | theme font | font family |
item.label.color | ColorValue | foreground | label color |
item.value | FontOptions | label font, muted | font/color of the value text |
item.gap | Pixels | 18 | gap between items in a row |
item.rowGap | Pixels | 8 | gap between rows |
item.markerGap | Pixels | 6 | gap between the marker and the label |
item.valueGap | Pixels | 14 | gap between the label and the value |
item.hiddenOpacity | Fraction | 0.4 | opacity of an item whose series is hidden |
background.fill | ColorValue | — | panel fill behind the items |
background.stroke | ColorValue | — | panel border color |
background.strokeWidth | Pixels | 1 | panel border width |
background.cornerRadius | Pixels | 4 | panel corner radius |
background.padding | PaddingValue | 8 / 0 | inner padding, CSS-like (below); 8 when fill/stroke is set |
background.shadow | ShadowOptions | — | drop shadow under the panel (below) |
data | LegendItemOptions[] | — | custom items (below) |
The item name is the series name (or yField if no name is set). showInLegend: false on a series removes its item.
Items that don't fit are paginated: arrows ‹ 1/3 › appear at the bottom of the legend (a horizontal legend fits maxRows rows per page, two by default). For pie/donut, clicking an item hides the sector.
Size limits
A legend takes what its items need, and with long series names a vertical one takes it from the plot. maxWidth and maxHeight bound it; what they mean follows the orientation the position sets:
| Legend | maxWidth | maxHeight |
|---|---|---|
vertical (left/right) | labels that no longer fit are cut with an ellipsis | items past it move to the next page |
horizontal (top/bottom) | items wrap onto the next row within it | caps the rows per page, along with maxRows |
legend: { position: 'right', maxWidth: 160 },Both take pixels or a percentage string — '40%' is read against the room the layout offered the legend: the chart minus its padding and the captions, or the whole chart area for a floating legend. A cap that keeps its share of the chart as it resizes is what a responsive chart usually wants:
legend: { position: 'right', maxWidth: '25%' },A malformed value ('160px', '%') is ignored and the legend stays unbounded.
A label is cut to the room its item has whether or not maxWidth is set — the chart width is the limit either way, so a name too long for the chart never runs off it.
Floating placement
position is the docking side plus an optional alignment along it: top-right docks the legend to the top edge aligned right (top centers). top-*/bottom-* lay items out in horizontal rows, left-*/right-* — in a vertical column; the first token sets the orientation.
With floating: true the legend stops reserving space and overlays the chart (CSS position: absolute style). It is anchored to the whole chart area — captions included — so a left-aligned title and a top-right floating legend sit on the same level:
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
// A floating legend anchored to the top-right corner of the whole chart —
// on the same level as the left-aligned title.
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Site traffic', textAlign: 'left', padding: { bottom: 4 } },
subtitle: { text: 'visits per month, thousands', textAlign: 'left', padding: { bottom: 12 } },
series: [
{ type: 'line', xField: 'month', yField: 'organic', name: 'Organic' },
{ type: 'line', xField: 'month', yField: 'ads', name: 'Ads' },
],
legend: {
position: 'top-right',
floating: true,
background: {
fill: 'rgba(255, 255, 255, 0.9)',
stroke: '#cbd5e1',
cornerRadius: 6,
// CSS-like shorthand: [vertical, horizontal]
padding: [8, 12],
// lifts the panel off the plot it overlays
shadow: { color: 'rgba(15, 23, 42, 0.18)', blur: 10, offsetY: 3 },
},
},
};
}export function getData() {
return [
{ month: 'Jan', organic: 42, ads: 18 },
{ month: 'Feb', organic: 48, ads: 22 },
{ month: 'Mar', organic: 55, ads: 21 },
{ month: 'Apr', organic: 61, ads: 27 },
{ month: 'May', organic: 58, ads: 33 },
{ month: 'Jun', organic: 67, ads: 30 },
{ month: 'Jul', organic: 74, ads: 36 },
{ month: 'Aug', organic: 71, ads: 41 },
];
}offset insets the box from the anchored edges (x from left/right, y from top/bottom); along a centered axis a positive value shifts right/down. background draws a panel behind the items — handy over the plot.
Since the legend overlays the caption zone, the title and subtitle flow around it by default: lines level with the legend box wrap inside the gap beside it. avoidCaptions: false restores the plain overlay — the captions keep the full chart width and the legend is drawn on top:
legend: { position: 'top-right', floating: true, avoidCaptions: false },Panel
background.padding takes any CSS-like shorthand — a single value, [vertical, horizontal], [top, right, bottom, left], or { top, right, bottom, left } (the same shorthands work for the chart-level padding).
background.shadow lifts the panel off what it overlays. Any field turns the shadow on; enabled: false removes it.
| Option | Type | Default | Description |
|---|---|---|---|
color | ColorValue | rgba(0, 0, 0, 0.2) | shadow color |
blur | Pixels | 8 | blur radius |
offsetX | Pixels | 0 | horizontal offset |
offsetY | Pixels | 2 | vertical offset |
enabled | boolean | true | false removes the shadow |
The shadow is cast by the panel fill, so it needs background.fill; the border is drawn without it.
Markers
item.marker sets the glyph for every item; a data item overrides it field by field.
| Option | Type | Default | Description |
|---|---|---|---|
shape | LegendMarkerShape | 'square' | circle, square, diamond, triangle, cross, plus, line |
path | string | — | custom glyph as SVG path data; wins over shape |
viewBox | number | 24 | side of the square the path coordinates live in |
size | Pixels | 10 | marker box side (line is drawn 1.8× wider) |
stroke | ColorValue | — | outline color; without it the glyph is filled only |
strokeWidth | Pixels | 1 | outline width, and the thickness of a line marker |
lineDash | Pixels[] | — | dashes for a line marker |
cornerRadius | Pixels | 3 | square only: corner rounding |
line draws a dash — the way a line/area series looks on the plot; path takes plain SVG path data (d), so an icon set drops straight in. The coordinates are read in a viewBox × viewBox square and scaled to size, so the same d fits any marker size:
import { getData } from './data';
import type { ChartOptions } from 'grafit-charts';
// A five-pointed star in a 24×24 viewBox — the marker scales it to `size`.
const STAR = 'M12 2 L14.6 8.9 L21.8 9.3 L16.2 13.9 L18.1 21 L12 17 L5.9 21 L7.8 13.9 L2.2 9.3 L9.4 8.9 Z';
export function createOptions(): ChartOptions {
return {
data: getData(),
title: { text: 'Delivery health' },
subtitle: { text: 'per week' },
series: [
{ type: 'bar', xField: 'week', yField: 'deploys', name: 'Deploys' },
{ type: 'line', xField: 'week', yField: 'failures', name: 'Failures', marker: { enabled: false } },
{ type: 'line', xField: 'week', yField: 'rollbacks', name: 'Rollbacks', lineDash: [5, 4] },
],
legend: {
item: { marker: { size: 12 }, gap: 22, markerGap: 8 },
data: [
// the bar keeps the default rounded square
{ name: 'Deploys', series: 'Deploys' },
// a dash reads as a line the way the series draws it
{ name: 'Failures', series: 'Failures', marker: { shape: 'line', strokeWidth: 3 } },
{ name: 'Rollbacks', series: 'Rollbacks', marker: { shape: 'line', strokeWidth: 3, lineDash: [5, 4] } },
// a static item with a custom glyph: SVG path data instead of a shape
{ name: 'SLO met', marker: { path: STAR, color: '#f59e0b' } },
],
},
};
}export function getData() {
return [
{ week: 'W1', deploys: 24, failures: 5, rollbacks: 2 },
{ week: 'W2', deploys: 31, failures: 8, rollbacks: 3 },
{ week: 'W3', deploys: 19, failures: 3, rollbacks: 1 },
{ week: 'W4', deploys: 37, failures: 9, rollbacks: 4 },
{ week: 'W5', deploys: 28, failures: 4, rollbacks: 1 },
{ week: 'W6', deploys: 34, failures: 6, rollbacks: 2 },
];
}Custom items
legend.data fully replaces the auto-derived series items. Useful when colors carry meaning inside a single series — e.g. a Gantt-style range-bar painted by a per-datum fill callback:
import { getData } from './data';
import type { ChartOptions, LegendItemOptions } from 'grafit-charts';
// Custom legend items describe the bar statuses of a Gantt-style range-bar —
// something the auto-derived per-series legend cannot show.
const STATUS_COLORS: Record<string, string> = {
done: '#22c55e',
running: '#3b82f6',
failed: '#ef4444',
queued: '#94a3b8',
};
export function createOptions(): ChartOptions {
const data = getData();
const countOf = (status: string) => String(data.filter((datum) => datum.status === status).length);
const legendItem = (status: string, name: string): LegendItemOptions => ({
name,
marker: { color: STATUS_COLORS[status] },
value: countOf(status),
});
return {
data,
title: { text: 'Pipeline run' },
subtitle: { text: 'task timeline, hours' },
series: [
{
type: 'range-bar',
xField: 'task',
yLowField: 'start',
yHighField: 'end',
direction: 'horizontal',
cornerRadius: 3,
fill: ({ datum }) => STATUS_COLORS[String(datum.status)] ?? '#94a3b8',
},
],
legend: {
data: [
legendItem('done', 'Done'),
legendItem('running', 'Running'),
{ ...legendItem('failed', 'Failed'), marker: { color: STATUS_COLORS.failed, size: 12 }, label: { fontWeight: 'bold', color: '#ef4444' } },
legendItem('queued', 'Queued'),
],
},
};
}export function getData() {
return [
{ task: 'extract', start: 0, end: 3, status: 'done' },
{ task: 'validate', start: 3, end: 5, status: 'done' },
{ task: 'transform', start: 5, end: 11, status: 'running' },
{ task: 'notify', start: 8, end: 10, status: 'failed' },
{ task: 'load', start: 11, end: 14, status: 'queued' },
{ task: 'report', start: 14, end: 16, status: 'queued' },
];
}| Option | Type | Description |
|---|---|---|
name | string | display text (required) |
series | string | binds the item to a series for toggling |
marker.color | ColorValue | marker color; a bound item inherits the series color |
marker | LegendMarkerOptions | shape/path/size — every marker option, on top of item.marker |
label | FontOptions | per-item label font/color |
value | string | value to the right of the label |
series is matched against the series id first, then its name. A bound item toggles the series on click and dims when it is hidden; an item without series (or with an unknown reference) is static — it renders, but clicking does nothing. For pie/donut, bind to an individual sector by its label (or an explicit id#index); binding to the pie series as a whole is not supported.