Charts

ChartFrame

The shell every other chart is built in: it measures its container, reserves the axis margins, handles loading and empty states, places the legend and tooltip, and hands your default slot a plot rect to draw SVG into. Use it when the composition you need isn't one of the named charts.

html
<UiChartFrame :ariaLabel="..." />

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.


ARIA label

required

ariaLabel is required rather than optional, deliberately: an SVG plot is opaque to a screen reader, so this is the only description of the chart that exists. Describe the finding (“Revenue by month, 2024”) rather than the geometry (“a line chart”). Everything else here is the escape hatch — the frame measures its container, reserves the axis margins, and hands the default slot a plot rectangle to draw SVG into.

The shell every other chart in this section is built in, so anything here is true of all of them. Reach for it directly when the composition you need isn't one of the named charts — a bullet chart, a candlestick, a facet grid.

aria-label is required rather than optional, deliberately. An SVG chart is opaque to a screen reader, so it's the only description that exists. Describe the finding ("Revenue by month, 2024") rather than the geometry ("a line chart").

ARIA description

ariaDescription is the longer accessible description, for detail the label shouldn't have to carry — the shape of the trend, an anomaly worth calling out, the source of the numbers. It's read after the label, so write the two as a headline and a paragraph rather than repeating one in the other.

Traffic climbs steadily from 1,240 in January to a peak of 1,720 in May, then eases slightly in June. Sampled hourly and averaged per month.

Height

height is the plot's height in pixels, 320 by default. It's the whole sizing story on its own — width always comes from the container — and it's ignored entirely once aspectRatio is set. The frame keeps this height through its loading and empty states too, so a dashboard doesn't reflow as its panels resolve.

height = 120
height = 200
height = 320

Aspect ratio

aspectRatio sizes the plot from its container's width instead of a fixed height — pass a ratio like 16 / 9. It's what you want in a grid whose columns change width, where a fixed height makes a chart squat on wide screens and cramped on narrow ones. Setting it makes height irrelevant.

aspect-ratio = 16 / 9
aspect-ratio = 1

Guides

guides is on by default and reserves the margins the axes need. Turning it off lets the marks fill the whole box — that plus a small height is the inline sparkline shape, where empty gutters would be the only thing distinguishing it from a chart.

1,640 req/s

The <svg> is overflow-visible. An SVG viewport clips by default, and a chart's edges are exactly where content lands — a stroke centred on the plot boundary, a tick label's last character, a value label above the tallest bar. The margins are sized to hold all of it; this just stops a half-pixel of rounding shearing a mark off.

Y tick labels

yTickLabels is how the left margin gets reserved before anything is measured. Pass the *formatted* label strings, not the values: the margin comes from the widest one, approximated from character count rather than measured, because measuring text is impossible during SSR and a margin that changed on hydration would shift every mark on the page.

y-tick-labels is how the left margin gets reserved before anything is measured. Pass the formatted label strings, not the values: the margin comes from the widest one, approximated from character count rather than measured, because a margin that changed on hydration would shift every mark on the page.

X tick labels

xTickLabels matters more than it looks. Only the first and last really count: the outermost ticks usually sit exactly on the plot's edges, so half of each label hangs outside it — and without knowing them, the frame reserves nothing and the text is clipped by the container.

Has xlabel

hasXLabel tells the frame an x axis title is coming, so it reserves another row of margin for it below the ticks. It's a separate flag rather than being inferred because the title itself is passed to ChartLabels, inside the slot — by then the margins are already fixed.

Has ylabel

hasYLabel does the same for the vertical axis, widening the left margin past what the tick labels alone need. Set both flags whenever you pass both titles — they're cheap, and forgetting one is the usual reason a title ends up drawn over the ticks.

Top inset

topInset reserves extra room *above* the plot, in pixels, for anything drawn past the marks — a value label over a bar, an annotation over a peak. Worth reserving rather than assuming: niceDomain rounds the maximum up to a whole tick, which frequently lands exactly on the largest value, leaving the tallest mark flush against the plot's top edge with nowhere to put a label.

1006040
1006040

top-inset and right-inset reserve room *outside* the plot for anything drawn past the marks. Reach for them rather than assuming headroom exists: niceDomain rounds the max up to a whole tick, which frequently lands exactly on the largest value, leaving the tallest mark flush against the plot's edge.

Right inset

rightInset reserves the same room on the opposite side, which is where a horizontal bar ends. Size it from the widest label you actually intend to draw, not from a guess.

1006040

Loading

loading replaces the plot with a skeleton of exactly the same height, so a dashboard doesn't reflow when its data lands. That's also why it belongs here rather than as a v-if around the chart — and since the named charts pass it through, this is how :loading behaves on a UiLineChart too.

The wrapper carries the height in every state — chart, skeleton, and empty alike — so a dashboard doesn't reflow as its panels resolve. That's also why loading belongs here rather than being a v-if around the chart.

Empty

empty replaces the plot with the empty state, again at the same height. Drive it from your own “no rows” check rather than letting a chart render an axis over nothing, which reads as a bug rather than an absence.

No data to display

Empty label

emptyLabel replaces the default “No data to display” with something that names what's missing and, ideally, the window it's missing from. For anything richer than a sentence — an action, an explanation — override the empty slot entirely; the frame keeps its height either way.

No requests in this window
No data source connected

Legend position

legendPosition puts the legend slot above or below the plot — bottom by default. The frame doesn't care what's in that slot: ChartLegend is the usual answer, but a colour-ramp strip or a plain sentence works the same way. Move it to top when the legend is what the reader needs before they can parse the marks.

  • Requests per second

`@pointermove`, `@pointerleave`, `tooltip` slot

The SVG fills its container at 1:1 with no viewBox, so the pointermove payload is already in scene coordinates — compare it against your own scales directly, there's no transform to undo. It carries the plot rect along with it, so a hit test needs nothing else.

It hands the default slot { plot, width, height } and expects SVG back. That inversion is the whole design: tick offsets need a scale, a scale needs the plot rectangle, and the plot rectangle needs the widest tick label to reserve a left margin for. Letting the caller build its own scales inside the rect is what breaks that circle.

The tooltip slot renders outside the SVG, over the plot. That keeps the panel ordinary DOM — real text, real wrapping, any Bundy component inside it — instead of <foreignObject> or hand-broken <tspan>s.

The SVG fills its container at 1:1 with no viewBox, so the pointermove payload is already in scene coordinates. Compare it against your own scales directly; there's no transform to undo.


API Reference

Generated from the component's source — props, slots and emits as the component actually declares them.

Props

Prop
Type
Default
Description
ariaLabelrequired
string
What the chart shows, for screen readers. Required: an SVG plot is opaque to one, so this is the only description of it that exists.
string
A longer accessible description, for detail the label shouldn't carry.
number
320
Height of the plot in pixels. Ignored when `aspectRatio` is set.
number
Width-to-height ratio (e.g. `16 / 9`), sizing the chart from its container's width.
boolean
true
Draws axes and reserves the margins they need. With it off the marks fill the whole box, which is what makes an inline sparkline rather than a small chart with empty gutters.
string[]
The y tick labels, so the left margin can be reserved before anything is measured.
string[]
The x tick labels, in order. Only the first and last matter: they're centred on ticks that often sit exactly on the plot's edges, so half of each hangs outside it.
boolean
Whether an x axis title is present, which needs another row of margin.
boolean
Whether a y axis title is present.
number
0
Extra room above the plot, in pixels, for anything drawn past the marks — a value label over a bar, an annotation over a peak. Worth reserving rather than assuming: a nice domain often ends exactly at the largest value, so the tallest mark can sit flush against the plot's top.
number
0
The same room to the right of the plot, which is where horizontal bars end.
boolean
false
Replaces the plot with a skeleton of the same height.
boolean
false
Replaces the plot with the empty state.
string
'No data to display'
Message shown in the empty state.
"top" | "bottom"
'bottom'
Where the `legend` slot renders relative to the plot.

Slots

Slot
legend
empty
default
tooltip

Emits

Event
Payload
Description
pointermove
[position: { x: number; y: number; plot: ChartPlot }]
Pointer position in scene coordinates, with the plot rect it fell in.
pointerleave
[]
The pointer left the plot.

Types

ts
interface ChartPlot {
    x: number;
    y: number;
    width: number;
    height: number;
    /** Right edge, since marks need it more often than the width. */
    right: number;
    /** Bottom edge, which is also the x axis' baseline. */
    bottom: number;
}