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.
<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
requiredariaLabel 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.
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.
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.
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.
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.
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.
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.
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
ariaLabelrequiredstringnumberbooleantruestring[]string[]number0Slots
legendemptydefaulttooltipEmits
pointermove[position: { x: number; y: number; plot: ChartPlot }]pointerleave[]Types
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;
}