Ridgeline
Overlapping distributions, one ridge per group: hover to lift a ridge and read its quartiles, switch datasets and every curve morphs.
datachartnew
Preview
Ridgeline preview
Live · keyboard ready
| Series | Median | First quartile | Third quartile | Lowest | Highest | Count |
|---|---|---|---|---|---|---|
| July | 24°C | 21.8°C | 26.3°C | 18°C | 30°C | 48 |
| August | 27°C | 25°C | 29°C | 22°C | 32°C | 48 |
| September | 21.5°C | 18°C | 24°C | 14°C | 28°C | 48 |
When to use
- Seasonal or categorical distributions such as temperatures by month or response times by region.
- Showing where two groups differ in shape, such as bimodal winters.
When not to use
- Use beeswarm when each individual observation matters.
- Use bar-chart when only one summary number per group matters.
shadcn CLI
Also installs (1)
- manicat-foundation
Manual install(+)
registry.jsonjson
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "ridgeline",
"type": "registry:ui",
"title": "Ridgeline",
"description": "Overlapping distributions, one ridge per group: hover to lift a ridge and read its quartiles, switch datasets and every curve morphs.",
"categories": [
"data"
],
"dependencies": [
"motion"
],
"registryDependencies": [
"https://manicat.viraza.net/r/manicat-foundation.json"
],
"files": [
{
"path": "registry/components/ridgeline/ridgeline.tsx",
"type": "registry:component",
"target": "@components/manicat/ridgeline/ridgeline.tsx"
},
{
"path": "registry/components/ridgeline/ridgeline.module.css",
"type": "registry:component",
"target": "@components/manicat/ridgeline/ridgeline.module.css"
}
],
"docs": "Docs and live preview: https://uiarc.dev/components/ridgeline",
"meta": {
"tier": "free",
"kind": "component",
"docs": "https://manicat.viraza.net/components/ridgeline",
"markdown": "https://uiarc.dev/components/ridgeline/markdown",
"tags": [
"data",
"chart",
"new"
]
}
}Usage
Install the component, then copy this into your app. Change the content and the props to fit your screen.
example.tsxtsx
"use client";
import { Ridgeline } from "@/components/manicat/ridgeline/ridgeline";
import type { RidgelineSeries } from "@/components/manicat/ridgeline/ridgeline";
function ridge(mean: number, spread: number): number[] {
return Array.from({ length: 48 }, (_, index) => mean + Math.round(spread * Math.sin(index * 1.7) + ((index * 7) % 5) - 2));
}
const ridgeSeries: RidgelineSeries[] = [
{ id: "jul", label: "July", values: ridge(24, 4) },
{ id: "aug", label: "August", values: ridge(27, 3) },
{ id: "sep", label: "September", values: ridge(21, 5) },
];
export function RidgelineExample() {
return <Ridgeline series={ridgeSeries} label="Daily highs in Zurich" unit="°C" tint />;
}
Examples
Real screens rather than isolated props. Switch to Code to copy a whole example into your project.
Temperature distributions Overlapping density curves compare how a value is spread across groups.
Temperature distributions
Overlapping density curves compare how a value is spread across groups. preview
Live · keyboard ready
| Series | Median | First quartile | Third quartile | Lowest | Highest | Count |
|---|---|---|---|---|---|---|
| July | 24°C | 21.8°C | 26.3°C | 18°C | 30°C | 48 |
| August | 27°C | 25°C | 29°C | 22°C | 32°C | 48 |
| September | 21.5°C | 18°C | 24°C | 14°C | 28°C | 48 |
temperature-distributions-overlapping-density-curves-compare-how-a-value-is-spread-across-groups-.tsxtsx
"use client";
import { Ridgeline } from "@/components/manicat/ridgeline/ridgeline";
import type { RidgelineSeries } from "@/components/manicat/ridgeline/ridgeline";
function ridge(mean: number, spread: number): number[] {
return Array.from({ length: 48 }, (_, index) => mean + Math.round(spread * Math.sin(index * 1.7) + ((index * 7) % 5) - 2));
}
const months: RidgelineSeries[] = [
{ id: "jul", label: "July", values: ridge(24, 4) },
{ id: "aug", label: "August", values: ridge(27, 3) },
{ id: "sep", label: "September", values: ridge(21, 5) },
];
export function RidgelineTemperatureExample() {
return <Ridgeline series={months} label="Daily highs in Zurich" unit="°C" tint />;
}
Keyboard interactions
| Keys | Action |
|---|---|
| ArrowUp / ArrowDown | Lifts the previous or next ridge. |
| ArrowLeft / ArrowRight | Moves the reading cursor along the values by one axis step. |
| Home / End | Lifts the first or last ridge. |
| Escape | Lowers the ridge and clears the reading. |
Accessibility
- The plot is a focusable group; the lifted ridge's median, middle half, range, and count are announced through a polite live region.
- A visually hidden table lists median, quartiles, extremes, and count for every series.
- Every row is labelled in text; the median tint is backed by the table and tooltip.
Motion
- Curves are sampled on a shared grid, so a new dataset or domain interpolates every point: ridges swell, slide, and split in one spring.
- The first reveal rolls the ridges in from the top row down.
- The lifted ridge is a copy drawn above the others that rises 6px on a spring, with its middle half shaded and its median marked.
- The tooltip glides after the pointer and stays inside the chart. Reduced motion draws final shapes immediately.
Responsive behavior
- Label gutter and tick count adapt to width; the plot redraws at the measured size so strokes stay crisp.
- Touch taps lift a ridge and read at the tapped value.
Performance
- Densities are computed once per dataset (96 samples per ridge); animation interpolates arrays and writes path strings directly.
- Suitable for up to a few thousand observations per series.
Notes for AI
- Choose it to compare the shape of many distributions: spread, skew, and bimodality, not just averages.
- Pass raw observations, at least 20 per series, and fix domain when switching datasets so shapes move on a still axis.
- Keep rows to about 20; beyond that, use small multiples or a box plot.
- Colors come from the shared --series-1 to --series-4 tokens in foundation.css: series 1 is the accent itself and series 2 to 4 rotate its hue, validated all-pairs for color vision deficiency across every accent in light and dark. Override the tokens on any ancestor to rebrand.