Skyline chart
FreeA day-by-time grid whose cells rise into columns. Seen from straight above it is an exact heatmap, and colour repeats every height so nothing is lost when it is flat. Tilt it up to see the shape of the week. The busiest cell is the one ember column.
pnpm dlx shadcn@latest add @caliper/skyline-chart| 00–03h | 03–06h | 06–09h | 09–12h | 12–15h | 15–18h | 18–21h | 21–00h | |
|---|---|---|---|---|---|---|---|---|
| Monday | 13 | 37 | 79 | 154 | 200 | 207 | 144 | 71 |
| Tuesday | 15 | 42 | 88 | 172 | 223 | 230 | 161 | 79 |
| Wednesday | 17 | 43 | 87 | 168 | 216 | 224 | 157 | 79 |
| Thursday | 20 | 36 | 99 | 176 | 244 | 239 | 177 | 76 |
| Friday | 20 | 31 | 80 | 138 | 190 | 186 | 140 | 62 |
| Saturday | 18 | 14 | 26 | 31 | 46 | 39 | 34 | 18 |
| Sunday | 8 | 16 | 14 | 27 | 28 | 33 | 20 | 19 |
Thursday · 12–15h · 244 events · peak
When to use #
Use it for
- Activity by weekday and hour, load by region and time, anything with two category axes and one number.
- Showing a pattern first and the exact figure second.
Skip it when
- Comparing a few values; a bar chart reads exactly.
- Dense grids over about 12 by 12; the columns hide each other.
Decisions #
Why it is built this way, so you know what to keep when you change your copy.
Behaviour
- Flat is a complete chart
- At view 0 the grid is a heatmap that reads to the digit. Height is a second encoding of the same number, never the only one.
- One tab stop
- Arrow keys move a cursor between cells and a live readout says where it is, so a keyboard user does not tab through 56 cells.
Usage
- A table of the same numbers
- The Table switch shows a real table, and the same table is in the page for screen readers while the chart is showing.
Installation #
pnpm dlx shadcn@latest add @caliper/skyline-chart
Adds its 8 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above.
import { SkylineChart } from "@/components/ui/skyline-chart" const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; const dayNames = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]; const bands = ["00", "03", "06", "09", "12", "15", "18", "21"]; const bandNames = bands.map((b, i) => `${b}–${String((i + 1) * 3 % 24).padStart(2, "0")}h`); const activity = days.map((_, r) => bands.map((__, c) => { const weekday = r < 5; const peak = weekday ? Math.exp(-((c - 4) ** 2) / (2 * 1.5 ** 2)) + 0.4 * Math.exp(-((c - 5.7) ** 2) / 2.4) : 0.42 * Math.exp(-((c - 4.4) ** 2) / 4.5); return Math.round(peak * [0.9, 1, 0.96, 1.08, 0.82, 0.36, 0.26][r]! * 190 + ((r * 7 + c * 3) % 6) * 2 + 8); })); export function SkylineChartDemo() { return <div className="w-full max-w-2xl"><SkylineChart label="Activity by weekday and time of day" rows={days} columns={bands} rowNames={dayNames} columnNames={bandNames} values={activity} unit="events" /></div>; }
API reference #
SkylineChart
component
Accessibility #
It exposes group semantics, with aria-roledescription, aria-label, aria-describedby, aria-hidden, aria-live and aria-valuetext kept in step with state. Changes that matter are announced through a live region.
Motion #
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.