caliper uidocs

Skyline chart

Free

A 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.

Installs components/ui/skyline-chart.tsxDepends on @radix-ui/react-slot, lucide-reactMotion none in component source
pnpm dlx shadcn@latest add @caliper/skyline-chart
Activity by weekday and time of day
00–03h03–06h06–09h09–12h12–15h15–18h18–21h21–00h
Monday13377915420020714471
Tuesday15428817222323016179
Wednesday17438716821622415779
Thursday20369917624423917776
Friday20318013819018614062
Saturday1814263146393418
Sunday816142728332019

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 #

terminal
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.

example.tsx
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
PropTypeDefaultDescription
label *string—What the chart shows, for example "Activity by weekday and time".
rows *readonly string[]—Short row labels drawn on the grid.
columns *readonly string[]—Short column labels drawn on the grid.
values *readonly (readonly number[])[]—One array of numbers per row, one number per column.
rowNamesreadonly string[]—Full names for the readout and the table. Defaults to the short labels.
columnNamesreadonly string[]—
unitstring"events"
format((value: number) => string)(n: number) => n.toLocaleString("en-US")
viewnumber—0 (heatmap, seen from above) to 1 (skyline). Controlled when set.
defaultViewnumber1
onViewChange((view: number) => void)—
statusDepthStatus"ready"
messagesDepthMessages—
onRetry(() => void)—
disabledboolean—
classNamestring—

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.

KeyBehaviour
→Moves to the next item or value.
←Moves to the previous item or value.
↓Moves to the next item or value.
↑Moves to the previous item or value.
HomeJumps to the first item or the minimum.
EndJumps to the last item or the maximum.

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.