Sparkline
The shape of a short series, at the size of a table cell. No axis and no ticks: the exact figure is in the label.
- Volume
- €28,400
- Payments
- 412
- Customers
- 96
- Refunds
- 7
import { Sparkline } from '@/components/sparkline'
export default function Example() {
return <Sparkline series={[12, 18, 14, 22, 19, 27, 28]} label="Payments, last 7 days: rising" />
}<script setup lang="ts">
import Sparkline from '@/components/sparkline.vue'
</script>
<template>
<Sparkline :series="[12, 18, 14, 22, 19, 27, 28]" label="Payments, last 7 days: rising" />
</template>Installation
Copy these files into the folder where you keep your components. They import each other with relative paths, so the folder can live anywhere. The packages they rely on are listed on the Installation page.
import clsx from 'clsx'
import type React from 'react'
const padding = 2
export function Sparkline({
series,
label,
width = 96,
height = 24,
color = 'text-zinc-900 dark:text-white',
className,
...props
}: {
series: number[]
label: string
width?: number
height?: number
color?: string
} & Omit<React.ComponentPropsWithoutRef<'svg'>, 'width' | 'height' | 'color' | 'children'>) {
let values = series.filter(Number.isFinite)
let points: { x: number; y: number }[] = []
if (values.length >= 2) {
let max = Math.max(...values)
let min = Math.min(...values)
let step = width / (values.length - 1)
let usable = height - padding * 2
points = values.map((value, index) => ({
x: index * step,
y: max === min ? height / 2 : padding + (1 - (value - min) / (max - min)) * usable,
}))
}
let line = points.map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x.toFixed(2)},${point.y.toFixed(2)}`).join(' ')
let area = points.length === 0 ? '' : `M 0,${height} ${line.slice(1)} L ${width},${height} Z`
return (
<svg
xmlns="http://www.w3.org/2000/svg"
{...props}
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
role="img"
aria-label={label}
className={clsx(className, 'shrink-0', color)}
>
<path d={area} fill="currentColor" className="opacity-10" />
<path
d={line}
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
vectorEffect="non-scaling-stroke"
/>
</svg>
)
}<script setup lang="ts">
import { computed } from 'vue'
const props = withDefaults(
defineProps<{
series: number[]
label: string
width?: number
height?: number
color?: string
}>(),
{ width: 96, height: 24, color: 'text-zinc-900 dark:text-white' }
)
const padding = 2
const points = computed(() => {
const values = props.series.filter(Number.isFinite)
if (values.length < 2) return []
const max = Math.max(...values)
const min = Math.min(...values)
const step = props.width / (values.length - 1)
const usable = props.height - padding * 2
return values.map((value, index) => ({
x: index * step,
y: max === min ? props.height / 2 : padding + (1 - (value - min) / (max - min)) * usable,
}))
})
const line = computed(() =>
points.value.map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x.toFixed(2)},${point.y.toFixed(2)}`).join(' ')
)
const area = computed(() =>
points.value.length === 0 ? '' : `M 0,${props.height} ${line.value.slice(1)} L ${props.width},${props.height} Z`
)
</script>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
:width="width"
:height="height"
:viewBox="`0 0 ${width} ${height}`"
role="img"
:aria-label="label"
:class="['shrink-0', color]"
>
<path :d="area" fill="currentColor" class="opacity-10" />
<path
:d="line"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
vector-effect="non-scaling-stroke"
/>
</svg>
</template>Component API
| Prop | Default | Description |
|---|---|---|
Sparklinerenders a <svg> element | ||
series | - | Values, oldest first. Values that are not finite numbers are skipped, under two values nothing is drawn, and equal values draw a flat line at mid height. |
label | - | What the curve says, for whoever cannot see it: the figure and the direction. |
width | 96 | Width in pixels. |
height | 24 | Height in pixels. |
color | text-zinc-900 dark:text-white | Text color classes. The line and the area under it read `currentColor`. |
Examples
Basic example
Pass the values, oldest first, and a label that says what a sighted reader would take from the curve. The line spans the width and is scaled to its own minimum and maximum:
import { Sparkline } from '@/components/sparkline'
export default function Example() {
return <Sparkline series={[12, 18, 14, 22, 19, 27, 28]} label="Payments, last 7 days: rising" />
}<script setup lang="ts">
import Sparkline from '@/components/sparkline.vue'
</script>
<template>
<Sparkline :series="[12, 18, 14, 22, 19, 27, 28]" label="Payments, last 7 days: rising" />
</template>Sizing
Set width and height in pixels. The stroke stays at 1.5 pixels whatever the size:
<Sparkline series={series} label="Payments, last 7 days" width={80} height={28} />
<Sparkline series={series} label="Payments, last 7 days" width={160} height={40} /><Sparkline :series="series" label="Payments, last 7 days" :width="80" :height="28" />
<Sparkline :series="series" label="Payments, last 7 days" :width="160" :height="40" />Color
Pass text color classes to color. Keep it for a verdict the label also carries:
<Sparkline series={rising} label="Payments, last 7 days: rising" color="text-emerald-600 dark:text-emerald-400" />
<Sparkline series={falling} label="Refunds, last 7 days: falling" color="text-red-600 dark:text-red-400" /><Sparkline :series="rising" label="Payments, last 7 days: rising" color="text-emerald-600 dark:text-emerald-400" />
<Sparkline :series="falling" label="Refunds, last 7 days: falling" color="text-red-600 dark:text-red-400" />In a table
The default size fits a table cell. When there is nothing to draw, show a dash rather than an empty box:
| Customer | Payments | Trend (30d) |
|---|---|---|
| Awa Diallo | 42 | |
| Moussa Traoré | 18 | |
| Nadia Market | 0 | — |
<TableCell>
{customer.payments > 0 ? (
<Sparkline series={customer.series} label={`${customer.payments} payments in the last 30 days`} />
) : (
<span className="text-zinc-400 dark:text-zinc-500">—</span>
)}
</TableCell><TableCell>
<Sparkline
v-if="customer.payments > 0"
:series="customer.series"
:label="`${customer.payments} payments in the last 30 days`"
/>
<span v-else class="text-zinc-400 dark:text-zinc-500">—</span>
</TableCell>With a figure
Beside a figure, align it to the baseline with items-end and let the number keep the left:
<div>
<dt className="truncate text-base/6 text-zinc-500 sm:text-sm/6 dark:text-zinc-400">Payments</dt>
<dd className="mt-1 flex items-end justify-between gap-4">
<span className="text-2xl/8 font-semibold text-zinc-950 tabular-nums sm:text-xl/8 dark:text-white">412</span>
<Sparkline series={[40, 52, 48, 61, 58, 70, 83]} label="Payments, last 7 days: rising" width={80} height={28} />
</dd>
</div><div>
<dt class="truncate text-base/6 text-zinc-500 sm:text-sm/6 dark:text-zinc-400">Payments</dt>
<dd class="mt-1 flex items-end justify-between gap-4">
<span class="text-2xl/8 font-semibold text-zinc-950 tabular-nums sm:text-xl/8 dark:text-white">412</span>
<Sparkline :series="[40, 52, 48, 61, 58, 70, 83]" label="Payments, last 7 days: rising" :width="80" :height="28" />
</dd>
</div>