Charts
ChartLegend
The swatch-and-label list every multi-series chart renders under its plot, with an optional muted value per entry and a slot for custom rows. Presentational by default; `interactive` turns each entry into a button for an app that filters series.
<UiChartLegend />Needs the charts module
This component ships in @jgastager/bundy-ui/charts, a separate module so projects that never draw a chart don't carry the paint classes these need. Nothing extra to install — just add the module entry. See Charts.
Options
One block per prop — what it does, and what it looks like set.
Items
items is one entry per series, each a key, a label and a color — the same semantic name the mark is painted with, which chartSequenceColor(index) hands you in the order the charts assign them. Every multi-series chart renders one of these under its plot already, so you only reach for it directly on a chart you've hand-built in a UiChartFrame.
- Desktop
- Mobile
- Tablet
When you filter series, keep each one's colour tied to its own identity, not to its index among the *visible* series. Recomputing colours from the filtered list repaints the survivors every time something is toggled, which reads as the data changing.
text is a plain string, so formatting stays with the caller — the legend never sees a number. A vertical legend with any text stretches its rows to the widest one so the texts share a right edge, and shrink-wraps itself so that column stays compact rather than spanning its container; align then places the whole column (left, centred or right) instead of aligning each row. Give the legend class="w-full" if you'd rather have the texts pinned to the container's far edge.
Orientation
orientation flows the entries in a row or a column. horizontal wraps them under the plot — right while the labels are short. vertical gives each one its own line, which is what you want for long labels, for a value printed beside each label, or for a legend running down the side of the plot instead of under it.
- Desktop · 8,680 visits
- Mobile · 7,630 visits
- Tablet · 1,430 visits
- Desktop · 8,680 visits
- Mobile · 7,630 visits
- Tablet · 1,430 visits
Align
align follows the orientation: it justifies a row, and cross-aligns a column.
- Desktop
- Mobile
- Tablet
- Desktop
- Mobile
- Tablet
- Desktop
- Mobile
- Tablet
Interactive
interactive turns each entry into a button that emits select. The legend never hides a series by itself — which series exist is the chart's data question — so the surrounding component owns the filtered set and passes hidden back down.
Presentational by default. interactive makes each entry a button that emits select, but the legend never hides a series itself — which series exist is the chart's data question, so the parent owns the filtered list and passes hidden back down.
Group
group ties the legend to any chart that names the same legendGroup, so hovering an entry dims that chart's other marks wherever the legend is placed — a card header, a sidebar, beside a second chart. The chart hands the legend its entries, colors included, so items can be left off. Switch the chart's own legend off with legend set to false. A group is just a shared name: two charts on it dim together.
Put the same string on the legend's group and on the chart's legendGroup and hovering an entry dims that chart's other marks, wherever the legend sits. The chart also hands the legend its entries, so items can be left off — the keys and colors a chart assigns (a pie's follow its sort order) aren't something to reproduce by hand. Without a group the legend only dims its own entries. The name is app-wide, so keep it specific (traffic, not chart); and a group-fed legend placed before its chart in the markup fills in on the client, since the server hasn't run the chart yet — pass items if that flash matters.
Examples
The component in real use: compositions that answer "how do I build X with this", rather than one prop at a time.
A muted value beside each entry
text on an item is a trailing, muted string — the count and share of a slice, the size of a bucket. In a vertical legend it's right-aligned and every row stretches to the widest one, so the texts share a right edge and read as a column; align then places that whole column. In a horizontal one it simply follows its label. It's a string, so the formatting stays with you — a pie or donut chart fills it itself with legendValues. hidden dims the whole row, text included, and with interactive the text is part of the button's accessible name.
- Positiv18 · 67 %
- Neutral7 · 26 %
- Negativ2 · 7 %
- Positiv18 · 67 %
- Neutral7 · 26 %
- Negativ2 · 7 %
Custom rows
#item replaces what's inside each row, scoped with the item, its index, whether the row is hovered, and the swatchClass the default swatch is painted with. The row's list item, its hover and focus handling, and the button that interactive adds all stay outside the slot — so a custom row keeps dimming the chart's slices through group. Give an item a slot name of its own to override just that row.
- Positiv
- Neutral
- Negativ
A slot replaces only what sits inside each row's wrapper. The list item that drives hover and group dimming, and the button interactive adds, are not part of it — so a custom row can't lose them, and can't opt out of them either. The wrapper is a flex row with a small gap and text-sm, so a slot that draws its own layout wants a single root with w-full.
API Reference
Generated from the component's source — props, slots and emits as the component actually declares them.
Props
ChartLegendItem[]"horizontal" | "vertical"'horizontal'"start" | "center" | "end"'start'booleanfalsestringSlots
item.slot ?? 'item'Emits
select[key: string]hover[key: string | null]Types
interface ChartLegendItem {
/** The series key this entry stands for — what `select` emits. */
key: string;
/** Text shown beside the swatch. */
label: string;
/** The series' semantic color, painted onto the swatch as a utility class. */
color: ChartColorName;
/** Dims the entry, for a series the surrounding app has filtered out. */
hidden?: boolean;
/**
* Trailing muted text — the figure this entry stands for ('18 · 67 %', '24 GB'). Right-aligned
* when the legend is `vertical`, so the texts form a column; it follows the label after a small
* gap when `horizontal`. A string rather than a number, so formatting stays with the caller.
* The whole entry dims with `hidden`, and with `interactive` the text is part of the button's
* accessible name.
*/
text?: string;
/**
* Name of the slot rendering this entry's row, instead of the shared `#item` slot.
*/
slot?: string;
}type ChartColorName = ChartColorFamily | `${ChartColorFamily}-${ChartColorShade}` | `[${string}]`;