Charts
Read when: putting a chart on a screen.
Owns: composing a chart with the registry's chart component and Unovis, its config, the y
domain, the types that trip the compiler, and what data a chart may show. Chart colours are in
theme.md.
TL;DR
npx shadcn-vue@latest add chart, plus@unovis/vueand@unovis/ts(checked 2026-11-03).- The
chartcomponent does not wrap Unovis. You composeVis*components insideChartContainer. - Colours come from the config as
var(--chart-n), and series reference them asvar(--color-<key>). - Pin the y domain yourself.
- Plot real data only.
Composition
<script setup lang="ts">
import { VisArea, VisAxis, VisLine, VisXYContainer } from '@unovis/vue';
import { computed } from 'vue';
import {
ChartContainer,
ChartCrosshair,
ChartTooltip,
ChartTooltipContent,
componentToString,
type ChartConfig,
} from '@/components/ui/chart';
type Point = { label: string; value: number };
type Row = Point & { index: number };
const props = defineProps<{ points: Point[] }>();
const headroom = (values: number[]): [number, number] => [0, Math.max(1, ...values)];
const config = { value: { label: 'Invoices', color: 'var(--chart-1)' } } satisfies ChartConfig;
const rows = computed<Row[]>(() => props.points.map((p, index) => ({ ...p, index })));
const domain = computed(() => headroom(props.points.map((p) => p.value)));
const label = (tick: number | Date) => rows.value[Number(tick)]?.label ?? '';
const whole = (tick: number | Date) => String(Math.round(Number(tick)));
</script>
<template>
<ChartContainer :config="config" class="aspect-auto h-64">
<VisXYContainer :data="rows" :y-domain="domain" :margin="{ top: 8, bottom: 8 }">
<VisArea :x="(d: Row) => d.index" :y="(d: Row) => d.value" color="var(--color-value)" :opacity="0.15" />
<VisLine :x="(d: Row) => d.index" :y="(d: Row) => d.value" color="var(--color-value)" />
<VisAxis type="x" :tick-line="false" :domain-line="false" :grid-line="false" :tick-format="label" />
<VisAxis type="y" :tick-line="false" :domain-line="false" :tick-format="whole" :num-ticks="4" />
<ChartTooltip />
<ChartCrosshair :template="componentToString(config, ChartTooltipContent, { labelFormatter: label })" />
</VisXYContainer>
</ChartContainer>
</template>
configmaps each series key to alabel, acolor(ortheme: { light, dark }) and an optionalicon.ChartStyle, insideChartContainer, turns each into a--color-<key>property scoped to that chart; thecolorprops reference it.ChartTooltipandChartCrosshairare Unovis'sVisTooltipandVisCrosshairre-exported; the look comes fromChartTooltipContent(labelKey,nameKey,indicator,hideLabel,hideIndicator).ChartLegendContentadds a legend.- Give the container a height (
h-64) andaspect-auto, or it takes the default aspect ratio.
Pin the y domain
Left to choose, Unovis pads the scale, and a small period renders as a few pixels of ink. Compute
the domain (headroom above) and pass :y-domain, with :num-ticks small enough that labels
don't crowd. Math.max(1, …) keeps an all-zero series from collapsing the axis.
Types
- Unovis types a tick as
number | Date, so a formatter must accept both, even when the axis plots a row index. - A
&intersection inside a template expression breaks the Vue template compiler. Declaretype Row = Point & { index: number }in the script and use the alias.
The data has to be real
A chart needs a real series. Don't invent one and don't ship placeholder numbers: an honest empty
chart (with Empty over it) beats a full one that is not. Load the series where every other read
happens (a page prop, a query). When tests run on a different database engine than production,
group by period in application code, because date functions differ between engines.
Direction
Axes run left to right even inside an Arabic page; that is the normal convention for a time series. Leave it.