LineChart

One or more trends over an ordered x axis, with a shared crosshair tooltip listing every series at the hovered position. The default for change over time — and with `guides="false"` and a small `height`, the inline sparkline in a stat tile or table cell.

vue
<UiLineChart :data="..." :categoryKey="..." :series="..." :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.


Options

One block per prop — what it does, and what it looks like set.

Data

required

data keeps rows in the wide shape your API already returns — one object per x position, one field per series. No reshaping, no melting into long form.

  • Desktop
  • Mobile

data stays in the wide shape an API returns ({ month: "Jan", desktop: 120, mobile: 80 }); the component folds it into the long rows the marks want. Rows whose value isn't a finite number are dropped rather than zeroed, so a gap in the data reads as a gap.

The hover fade is a CSS transition over the library's own 250ms duration, so it costs nothing and honours prefers-reduced-motion. There's no animate prop and nothing tweens on a data change — the marks are plain SVG elements Vue patches directly.

Category key

required

categoryKey sets which field on each row is its x value. Everything else in the row is a candidate series.

Series

required

series is field names for the quick case, or { key, label, color } when the legend needs better words than your column names. Order assigns the palette slots, so keep it stable across renders.

  • desktop
  • mobile
  • Desktop
  • Mobile

Colors are the library's own semantic ones — primary, secondary, success, warning, error, info, pending, neutral — and any shade of them, primary-400 through primary-950, exactly as the bg-* utilities spell them. Set color per series (or per item) to pin one; leave it unset to take the next slot of the default sequence. neutral and secondary are never handed out automatically, so an unlabelled series never lands on them.


Examples

The component in real use: compositions that answer "how do I build X with this", rather than one prop at a time.

Per-series line style

A series' dashed and points override this chart's own line style and dots for that one line — a projection that shouldn't read as an actual reading, drawn dashed with no dots, next to the real figures drawn solid with theirs.

  • Actual
  • Projected

ARIA label

required

ariaLabel sets what the chart shows, in one sentence. Required — an unlabelled chart is an unreadable one, and it's the only thing a screen reader has to go on.

ARIA description

ariaDescription is the detail the label shouldn't carry — the trend, the outlier, the thing a sighted reader gets from the shape.

Rises steadily from $41.2k in week 1 to $50.1k in week 8, with small dips in weeks 3 and 6.

Categories become an evenly-spaced point scale by default — dates included. Twelve monthly buckets are drawn evenly regardless of month length, which is right for buckets and wrong for irregular timestamps. When the gaps carry meaning, hold the x values as numbers (hours past midnight, epoch milliseconds) and set x-type="number": each row is then placed where its value falls, and the ticks come from the scale rather than from the rows.

Curve

curve says what happens between two samples. linear joins them and claims nothing else. smooth is monotone cubic — it rounds corners without bulging past a value the data never contained. The three step variants say the value held and then jumped, which is what a price, a setting or a plan tier actually did: step-after holds until the next sample, step-before jumps first, step splits the difference. Reach for a step curve whenever “between two samples” has no meaning.

linear
smooth
step
step-before
step-after

Points

points marks the real samples. Worth turning on whenever the series is sparse enough that a reader might mistake the line for continuous measurement.

  • Desktop
  • Mobile

Stroke width

strokeWidth is the line's thickness in pixels. Raise it for a line that has to hold up in a dense dashboard; lower it to step a comparison series back behind a busier one.

X label

xLabel names what the x ticks are counting, for the cases the surrounding copy doesn't. Drawn dimmed where the ticks are muted — the reader needs it once, and the ticks continuously.

Y label

yLabel is the same, for the value axis.

  • Desktop
  • Mobile

X tick size

xTickSize is the box the x-tick slot draws into, and the room reserved for it below the plot. The slot takes whatever SVG you hand it — an icon, a logo, or two lines of text where one would have crowded, as here. It gets x and y for the box's centre and box for the rect itself.

  • Desktop
  • Mobile

Two lines of text in the slot is the usual reason to reach for it — a month over its year, a weekday over its date — since the axis then prints the repeating part once per tick instead of squeezing it into one line.

Y tick size

yTickSize is the same for the y-tick slot, taken out of the left margin instead — where the number is the whole margin, since a tick drawing its own content has no text for the chart to estimate a width from. Marking the axis rather than each point is what keeps a unit out of six labels.

Value format

valueFormat formats the y-axis ticks and the tooltip values together, so the two can never disagree.

Category format

categoryFormat rewrites the x tick labels without touching your data — how a key like 2025-03 becomes Mar while the rows keep their real values.

X type

xType decides whether the x values are names or quantities. A category axis gives every row an equal slot, so four readings taken at 4:43, 5:34, 8:00 and 11:25 come out evenly spaced — the three-hour gap before 8:00 looking the same as the fifty minutes before 5:34. number places each one where its value falls, and the gaps become part of the reading.

A number axis changes three things beyond the spacing. Rows are drawn in value order rather than data order, so an unsorted array no longer doubles the line back across the plot. A row whose category isn't a number is dropped, the way a row with a non-finite value already is — there's nowhere on the scale to put it. And categoryFormat is never called: the ticks are numbers the scale chose, so xFormat formats them, and the tooltip's title with them. Everything else — folding, focus, the focusChange payload — still speaks in the category string your data held.

X format

xFormat is categoryFormat for a numeric axis: it takes the number and returns the label, on the ticks and in the tooltip's title. Without it the axis prints the raw quantity — 4.717 where the reading was taken at 4:43.

X min

xMin holds the left end of a numeric axis at a value no row reaches — the shift starts at midnight whether or not anyone took a reading then. It's yMin's counterpart and gives way the same way: a row further left still widens the axis.

X max

xMax does the same at the right end. The pair is what turns four readings into a timeline: 0 to 12 draws the whole morning, the readings sit where they happened, and the empty stretches read as time nobody measured.

X ticks

xTicks names the ticks outright, for when the scale's idea of a round number isn't yours. Every three hours here — left alone the axis ticks at 0, 2, 4 and so on, which is round arithmetic and the wrong grid for a clock.

Grid

grid is the horizontal rules from the y ticks. Dropping them keeps the axes and their labels.

Zero baseline

zeroBaseline is off by default: a trend line reads best against its own range, and anchoring to zero flattens exactly the variation the chart exists to show. Turn it on when the reader is meant to compare magnitudes and the distance to zero is part of the answer.

The y axis is inferred from the data, not anchored to zero — for a trend line, a zero baseline usually flattens the exact variation the chart exists to show. zero-baseline turns it on when magnitudes rather than shape are the point.

Y min

yMin holds the bottom of the y axis at a value the data needn't reach — a 1–5 satisfaction score read on the 1–5 scale it was collected on, rather than on the narrow band the six weeks happen to occupy. A reading below it still widens the axis.

yMin and yMax hold the axis open; they don't clamp the data. A value outside a bound widens the axis rather than being drawn past the plot's edge, so a bound is safe to set from a scale you know (0–100%, a 1–5 rating) without checking the numbers first. A bound the data stays inside is used exactly — y-max="100" ends the axis at 100, not at the next round tick above it.

Y max

yMax does the same at the top, and the pair is what fixes a scale outright — every chart of the same measure drawn on the same axis, so two of them can be compared side by side instead of each being scaled to its own best week.

Tooltip

tooltip is the crosshair and the panel that follows it, dropped together.

  • Desktop
  • Mobile

The tooltip is grouped on x — one row per series at the hovered position — because comparing series at the same moment is the question a multi-line chart is asked. Set tooltip="false" to drop both it and the crosshair.

Animate

animate is on by default: the lines rise out of the floor on the first draw and bend from old values to new. Roll new values to compare it with the chart on the right, which has it off. Only the marks move — axes, tooltips and labels always read the real numbers — and a reader who has asked their system for reduced motion gets no motion either way.

  • Desktop
  • Mobile
  • Desktop
  • Mobile

Legend

legend is on by default from two series up. Force it off when the surrounding heading already names what's plotted.

Legend position

legendPosition moves the key above the plot with top — useful when the chart sits directly under a heading and the legend reads as part of it.

  • Desktop
  • Mobile

Legend align

legendAlign places the key horizontally. start lines it up with the plot's left edge, which is where the eye already is after reading the axis.

  • Desktop
  • Mobile
  • Desktop
  • Mobile
  • Desktop
  • Mobile

Legend orientation

legendOrientation flows the entries in a row or a column. vertical gives each series its own line — what long labels, or a value printed beside each label, want.

  • Desktop · 8,680 visits
  • Mobile · 7,630 visits

Legend group

legendGroup names a group that a standalone UiChartLegend with the same group belongs to, so the legend can sit anywhere — here above the plot — and still dim the other lines while you hover an entry. The chart hands the legend its entries, colors included, so it needs no items. Switch the chart's own legend off if you don't want two.

Guides

guides drops the axes, ticks, labels and grid *and* the margins they reserve, so the line fills the whole box. That plus a small height is the inline sparkline, for a stat tile or a table cell.

MRR$50.1k

guides="false" drops the axes, ticks, labels and grid *and* the margins they reserve, so the marks fill the whole box. That plus a small height is the inline-sparkline shape — there's no separate component for it.

Height

height is the plot's height in pixels. Ignored once aspectRatio is set.

Aspect ratio

aspectRatio sizes the chart from its container's width instead of a fixed height — so it keeps its proportions across breakpoints. Resize the preview to watch it hold.

  • Desktop
  • Mobile

Loading

loading swaps in a skeleton of exactly the chart's height, so nothing around it moves when the data lands. An empty data array gets the empty state instead, also at full height.

  • Desktop
  • Mobile
No data to display
  • Desktop
  • Mobile

@focus-change

Fires with the category under the pointer, and null when it leaves. The chart already shows a tooltip; this is for driving something *outside* it — a readout in a card header, a linked table row, a second chart scrubbing in step with this one.

Hovering:—

  • Desktop
  • Mobile

API Reference

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

Props

Prop
Type
Default
Description
datarequired
Record<string, unknown>[]
—
The rows to plot, in their natural wide shape — one object per position on the x axis, with one field per series: `[{ month: "Jan", desktop: 120, mobile: 80 }, …]`.
categoryKeyrequired
string
—
Field holding each row's x value.
seriesrequired
ChartSeriesInput[]
—
The series to draw, as field names or `{ key, label, color, dashed, points }` objects. Order assigns the default colors, so keep it stable — a series' color should follow the series, not its rank. `dashed` and `points` override this chart's own line style and dot visibility for that one series — a dashed forecast line with no dots among solid ones that have them.
ariaLabelrequired
string
—
What the chart shows, for screen readers. Required: an unlabelled chart is an unreadable one.
string
—
A longer accessible description, for detail the label shouldn't carry.
ChartCurveName
'linear'
How the line gets between points. `smooth` is monotone cubic, which rounds corners without inventing peaks the data doesn't contain.
boolean
false
Draws a dot at every point, showing which values are real and which are interpolation. A series can override this for itself via its own `points`.
number
2
Stroke width in pixels.
string
—
Title for the x axis.
string
—
Title for the y axis.
number
24
Side of the square box each x tick's slot content draws into, in pixels — and the room reserved for it below the plot. Ignored without the `x-tick` slot: text ticks size themselves.
number
24
The same for the `y-tick` slot, reserved out of the left margin instead.
(value: number) => string
—
Formats y-axis ticks and tooltip values.
(value: string) => string
—
Formats x-axis tick labels.
"category" | "number"
'category'
How the x axis reads the category values. `category` is a name per row, evenly spaced — twelve months, five channels. `number` reads them as quantities and places each row where its value falls, so the gaps between readings are drawn to scale: a morning's readings at 4:43, 5:34, 8:00 and 11:25 sit where they happened rather than a quarter of the plot apart. The ticks stop being the readings too — they become the round numbers the scale wants, bounded by `xMin`/`xMax` and named outright by `xTicks`. A row whose category isn't a number is dropped on a `number` axis, the same way a row with no value is: there's nowhere on the scale to put it.
(value: number) => string
—
Formats a `number` axis's ticks and the tooltip title — 8.5 into `8:30`. Replaces `categoryFormat`, which a numeric axis never calls.
number
—
Lowest value the x axis reaches on a `number` axis, whether or not a row gets there — a shift charted across the whole morning rather than across its first and last reading. Data outside it still widens the axis. Ignored on a `category` axis, which has no numbers to bound.
number
—
Highest value the x axis reaches, on the same terms.
number[]
—
Tick values for a `number` axis, when the round ones aren't the useful ones — hours across a timeline held in minutes, where the scale's idea of a round number is 100 and yours is 60. They're labels only: they neither move the marks nor bound the axis.
boolean
true
Draws horizontal grid rules from the y ticks.
boolean
false
Starts the y axis at zero. Off by default: for a trend line a zero baseline usually flattens the very variation the chart exists to show.
number
—
Lowest value the y axis reaches, whether or not the data gets there. With `yMax`, that's a fixed scale — 0 to 100% on a chart whose best week was 62%, so the reader sees the headroom instead of a line that fills the plot at every magnitude. A bound holds the axis open; it doesn't cap the data. A value below it still widens the axis, because a point drawn outside the plot is a worse answer than a scale that grew.
number
—
Highest value the y axis reaches, on the same terms.
boolean
true
Shows the crosshair and tooltip on hover.
boolean
true
Raises the lines out of the plot's floor on the first draw, and bends them from old values to new whenever the data changes. Only the marks move: the axes and the tooltip always read the real numbers. Readers who ask their system for reduced motion get no animation either way.
boolean
—
Shows the legend. Defaults to on for two or more series.
"top" | "bottom"
'bottom'
Where the legend sits relative to the plot.
"start" | "center" | "end"
'start'
Horizontal placement of the legend.
"horizontal" | "vertical"
'horizontal'
How the legend entries flow.
string
—
Names the group a `<UiChartLegend group="…">` elsewhere on the page belongs to, so hovering its entries dims this chart's other marks. Set it — usually with `:legend="false"` — to place the legend anywhere you like instead of under the plot.
boolean
true
Draws the axes, ticks, labels and grid, and reserves their margins. Off, plus a small `height`, is the inline-sparkline shape.
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
false
Replaces the plot with a skeleton of the same height.

Slots

Slot
default
x-tick
Receives `{ tick, index, x, y, size, box }`.
y-tick
Receives `{ tick, index, x, y, size, box }`.

Emits

Event
Payload
Description
focusChange
[category: string | null]
The category under the pointer, or `null` once it leaves.

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;
}
ts
type ChartSeriesInput = string | ChartSeries;
ts
type ChartCurveName = "linear" | "smooth" | "step" | "step-before" | "step-after";