Skip to content

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).

ts
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' },
  };
}
ts
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

OptionTypeDefaultDescription
enabledbooleantrueshow the legend
positionLegendPlacement'bottom'docking side + alignment, or a floating anchor (below)
floatingbooleanfalseoverlay the whole chart area instead of reserving space
offset{ x?: Pixels; y?: Pixels }0floating only: inset from the anchored edges
avoidCaptionsbooleantruefloating only: title/subtitle flow around the legend box
toggleSeriesbooleantrueclick toggles visibility
maxRowsnumber2rows per page in a horizontal legend
maxWidthLengththe chartwidth the legend never goes past (below)
maxHeightLengththe chartheight the legend never goes past (below)
reversebooleanfalserender the items back to front
item.markerLegendMarkerOptionsmarker glyph (below)
item.label.fontSizePixels12label font size
item.label.fontFamilystringtheme fontfont family
item.label.colorColorValueforegroundlabel color
item.valueFontOptionslabel font, mutedfont/color of the value text
item.gapPixels18gap between items in a row
item.rowGapPixels8gap between rows
item.markerGapPixels6gap between the marker and the label
item.valueGapPixels14gap between the label and the value
item.hiddenOpacityFraction0.4opacity of an item whose series is hidden
background.fillColorValuepanel fill behind the items
background.strokeColorValuepanel border color
background.strokeWidthPixels1panel border width
background.cornerRadiusPixels4panel corner radius
background.paddingPaddingValue8 / 0inner padding, CSS-like (below); 8 when fill/stroke is set
background.shadowShadowOptionsdrop shadow under the panel (below)
dataLegendItemOptions[]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:

LegendmaxWidthmaxHeight
vertical (left/right)labels that no longer fit are cut with an ellipsisitems past it move to the next page
horizontal (top/bottom)items wrap onto the next row within itcaps the rows per page, along with maxRows
js
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:

js
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:

ts
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 },
      },
    },
  };
}
ts
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:

js
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.

OptionTypeDefaultDescription
colorColorValuergba(0, 0, 0, 0.2)shadow color
blurPixels8blur radius
offsetXPixels0horizontal offset
offsetYPixels2vertical offset
enabledbooleantruefalse 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.

OptionTypeDefaultDescription
shapeLegendMarkerShape'square'circle, square, diamond, triangle, cross, plus, line
pathstringcustom glyph as SVG path data; wins over shape
viewBoxnumber24side of the square the path coordinates live in
sizePixels10marker box side (line is drawn 1.8× wider)
strokeColorValueoutline color; without it the glyph is filled only
strokeWidthPixels1outline width, and the thickness of a line marker
lineDashPixels[]dashes for a line marker
cornerRadiusPixels3square 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:

ts
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' } },
      ],
    },
  };
}
ts
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:

ts
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'),
      ],
    },
  };
}
ts
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' },
  ];
}
OptionTypeDescription
namestringdisplay text (required)
seriesstringbinds the item to a series for toggling
marker.colorColorValuemarker color; a bound item inherits the series color
markerLegendMarkerOptionsshape/path/size — every marker option, on top of item.marker
labelFontOptionsper-item label font/color
valuestringvalue 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.