Getting Started

Charts

Ten chart components, hand-built in SVG and painted with the same UnoCSS tokens as everything else. They ship as a second Nuxt module, so nothing here costs anything until you ask for it.

Why it's a separate module

The charts carry no runtime dependency at all — every mark is an SVG element in a Vue template, and every scale is about forty lines of arithmetic. What they do carry is roughly 340 safelisted paint classes, because a chart picks its colors from data at runtime and UnoCSS can't see those in your source. Most projects draw no charts, and those shouldn't pay for that CSS — so the base module doesn't register them, and this one does. It's the same library either way: same Ui prefix, same preset, same app.config.ts overrides. Only the registration differs.

1. Register the module

Nothing to install — the charts are part of the package you already have. This assumes Installation is done, since the base module and the UnoCSS preset are prerequisites. Add the second entry after the first — both, not one instead of the other:

nuxt.config.ts
ts
export default defineNuxtConfig({
  modules: [
    "@unocss/nuxt",
    "@jgastager/bundy-ui",
    "@jgastager/bundy-ui/charts",
  ],
});

A missing module fails silently

Vue renders an unregistered component as an unknown element rather than raising — so a forgotten module entry shows up as a chart that simply isn't there, with nothing in the console. If a chart renders blank, check this line first.

2. Draw something

Nothing else to wire up. Rows stay in the wide shape your API already returns — one object per position on the x axis, one field per series — and series names the fields to plot.

  • Desktop
  • Mobile

aria-label is required on every chart rather than optional. An SVG plot is opaque to a screen reader, so it's the only description that exists — describe the finding, not the geometry.

Choosing a chart

Start from the question the reader is asking, not from the shape you had in mind.

The question
Reach for
How did this change over time?
…and what did it add up to, or how did the split shift?
How do these categories compare?
How do two quantities relate? (a third sizes the bubbles)
Where is this concentrated across two dimensions?
What's the split of one whole? (two or three shares)
…with the headline number in the middle
How do these profiles compare across several measures?
How far around are these, as a compact summary?
Where in a range does this one value sit?
What's the trend, inline and small?
Something none of the above draws

Colors

Charts use the library's own semantic colors — there's no separate chart palette to configure. Series that don't name one are assigned from a fixed default sequence, ordered so each neighbouring pair stays furthest apart under red/green colorblindness.

1. primary

2. pending

3. success

4. info

5. warning

6. error

Pin one per series — or per data item — with color . Any shade works, spelled exactly as the utilities do, so a forecast can be a lighter step of the same hue as its actual rather than an unrelated fourth colour:

  • Actual
  • Forecast
  • Target

neutral and secondary both sit outside the default sequence on purpose. neutral has too little chroma to read as an identity, so as an unlabelled series it looks disabled; secondary is pear, which measures ΔE 1.0 against warning under simulated protanopia — effectively one colour to a red/green colorblind reader. Both stay available by name for exactly the case above — a target, a baseline, an "Other" bucket.

One thing to keep in mind: series order is what assigns the default slots, so keep it stable across renders. Sorting or filtering series re-colors everything that moved, and a series' color should follow the series, not its current rank. These are the same colors every other component takes, so Theming is where you change them.

Charts without axes

Every chart that draws guides takes guides . Turning it off 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, in a stat tile or a table cell.

Revenue$31.2k
Deploys14

With no axes and no tooltip, the aria-label is the only place the numbers exist at all — give the range and the direction, not the shape.

When none of them fit

Every named chart is a ChartFrame with marks drawn inside it, and the frame is public for exactly that reason. For a composition the library doesn't cover — a bullet chart, a candlestick, a facet grid — draw it yourself. The frame measures its container, reserves the axis margins, handles the loading and empty states, and hands your slot a plot rectangle; the scale helpers turn your data into coordinates inside it. What you write is the marks, and nothing else.

vue
<script setup>
// A bullet chart: a measured bar per region, with the target marked across it.
const regions = [
    { region: "EMEA", actual: 82, target: 90 },
    { region: "AMER", actual: 118, target: 105 },
    { region: "APAC", actual: 74, target: 100 },
];

const names = regions.map((row) => row.region);
const domain = niceDomain([0, Math.max(...regions.map((row) => row.target))]);
const ticks = numericTickValues(domain, 5);

const x = (plot) => linearScale(domain, [plot.x, plot.right]);
const y = (plot) => bandScale(names, [plot.y, plot.bottom], 0.4);
</script>

<template>
    <UiChartFrame
        aria-label="Actual revenue against target, by region"
        :y-tick-labels="names"
        :height="220"
    >
        <template #default="{ plot, height }">
            <UiChartAxes :plot="plot" :x-ticks="placeTicks(ticks, x(plot))" x-grid />

            <rect
                v-for="row in regions"
                :key="row.region"
                :x="plot.x"
                :y="y(plot)(row.region)"
                :width="x(plot)(row.actual) - plot.x"
                :height="y(plot).bandwidth"
                :rx="6"
                class="fill-primary"
            />

            <line
                v-for="row in regions"
                :key="row.region + '-target'"
                :x1="x(plot)(row.target)"
                :x2="x(plot)(row.target)"
                :y1="y(plot)(row.region)"
                :y2="y(plot)(row.region) + y(plot).bandwidth"
                stroke-width="2"
                class="stroke-neutral-300"
            />

            <UiChartLabels
                :plot="plot"
                :height="height"
                :x-ticks="placeTicks(ticks, x(plot))"
                :y-ticks="placeTicks(names, y(plot).center)"
            />
        </template>
    </UiChartFrame>
</template>

ChartAxes , ChartLabels , ChartLegend and ChartTooltip are auto-imported alongside it, so the chrome matches the built-in charts without you restyling any of it.