Streamgraph
Layered streams on a wiggle baseline that morph between ranges, with a layer you can isolate and read week by week.
datachartnew
Preview
Streamgraph preview
Live · keyboard ready
Support tickets by topic, Week of Jul 6 to Week of Jul 15. Billing, latest 58 tickets. Access, latest 44 tickets. Performance, latest 52 tickets. Design, latest 36 tickets.
| Date | Billing | Access | Performance | Design |
|---|---|---|---|---|
| Week of Jul 6 | 40 tickets | 74 tickets | 54 tickets | 23 tickets |
| Week of Jul 7 | 62 tickets | 77 tickets | 41 tickets | 20 tickets |
| Week of Jul 8 | 76 tickets | 74 tickets | 23 tickets | 20 tickets |
| Week of Jul 9 | 81 tickets | 67 tickets | 20 tickets | 20 tickets |
| Week of Jul 10 | 74 tickets | 56 tickets | 20 tickets | 20 tickets |
| Week of Jul 11 | 60 tickets | 45 tickets | 20 tickets | 20 tickets |
| Week of Jul 12 | 47 tickets | 36 tickets | 30 tickets | 24 tickets |
| Week of Jul 13 | 40 tickets | 33 tickets | 47 tickets | 33 tickets |
| Week of Jul 14 | 43 tickets | 36 tickets | 56 tickets | 38 tickets |
| Week of Jul 15 | 58 tickets | 44 tickets | 52 tickets | 36 tickets |
When to use
- Showing the changing share of topics, channels, or genres week by week.
- Storytelling dashboards where the shape of seasonality and spikes is the point.
- Comparing ranges where a morph between them explains what changed.
When not to use
- Use line-chart when exact values or comparisons between series matter.
- Use bar-chart for a few periods or categories.
- Avoid it for data with negative values; streams stack magnitudes.
shadcn CLI
Also installs (1)
- manicat-foundation
Manual install(+)
registry.jsonjson
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "streamgraph",
"type": "registry:ui",
"title": "Streamgraph",
"description": "Layered streams on a wiggle baseline that morph between ranges, with a layer you can isolate and read week by week.",
"categories": [
"data"
],
"dependencies": [
"motion"
],
"registryDependencies": [
"https://manicat.viraza.net/r/manicat-foundation.json"
],
"files": [
{
"path": "registry/components/streamgraph/streamgraph.tsx",
"type": "registry:component",
"target": "@components/manicat/streamgraph/streamgraph.tsx"
},
{
"path": "registry/components/streamgraph/streamgraph.module.css",
"type": "registry:component",
"target": "@components/manicat/streamgraph/streamgraph.module.css"
}
],
"docs": "Docs and live preview: https://uiarc.dev/components/streamgraph",
"meta": {
"tier": "free",
"kind": "component",
"docs": "https://manicat.viraza.net/components/streamgraph",
"markdown": "https://uiarc.dev/components/streamgraph/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 { Streamgraph } from "@/components/manicat/streamgraph/streamgraph";
import type { StreamgraphDatum, StreamgraphSeries } from "@/components/manicat/streamgraph/streamgraph";
const DAY = 86_400_000;
function iso(date: Date): string {
return date.toISOString().slice(0, 10);
}
function monthDay(date: Date): string {
return date.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
}
function range(from: Date, days: number): Date[] {
return Array.from({ length: days }, (_, index) => new Date(from.getTime() + index * DAY));
}
const streamSeries: StreamgraphSeries[] = [
{ key: "billing", label: "Billing" },
{ key: "access", label: "Access" },
{ key: "performance", label: "Performance" },
{ key: "design", label: "Design" },
];
const streamData: StreamgraphDatum[] = range(new Date(Date.UTC(2026, 6, 6, 12)), 10).map((date, index) => ({
key: iso(date),
label: "Week of " + monthDay(date),
axisLabel: "W" + (27 + index),
values: {
billing: Math.max(20, 40 + Math.round(30 * Math.sin(index / 1.6) + index * 4)),
access: Math.max(20, 55 + Math.round(22 * Math.sin(index / 1.9 + 1))),
performance: Math.max(20, 30 + Math.round(26 * Math.sin(index / 1.4 + 2))),
design: Math.max(20, 20 + Math.round(18 * Math.sin(index / 1.7 + 3))),
},
}));
export function StreamgraphExample() {
return <Streamgraph data={streamData} series={streamSeries} label="Support tickets by topic" unit="tickets" legend />;
}
Examples
Real screens rather than isolated props. Switch to Code to copy a whole example into your project.
Support load by topic Stacked flowing areas showing how the mix of tickets shifts week to week.
Support load by topic
Stacked flowing areas showing how the mix of tickets shifts week to week. preview
Live · keyboard ready
Support tickets by topic, Week of Jul 6 to Week of Sep 7. Billing, latest 58 tickets. Access, latest 44 tickets. Performance, latest 52 tickets.
| Date | Billing | Access | Performance |
|---|---|---|---|
| Week of Jul 6 | 40 tickets | 74 tickets | 54 tickets |
| Week of Jul 13 | 62 tickets | 77 tickets | 41 tickets |
| Week of Jul 20 | 76 tickets | 74 tickets | 23 tickets |
| Week of Jul 27 | 81 tickets | 67 tickets | 8 tickets |
| Week of Aug 3 | 74 tickets | 56 tickets | 4 tickets |
| Week of Aug 10 | 60 tickets | 45 tickets | 13 tickets |
| Week of Aug 17 | 47 tickets | 36 tickets | 30 tickets |
| Week of Aug 24 | 40 tickets | 33 tickets | 47 tickets |
| Week of Aug 31 | 43 tickets | 36 tickets | 56 tickets |
| Week of Sep 7 | 58 tickets | 44 tickets | 52 tickets |
support-load-by-topic-stacked-flowing-areas-showing-how-the-mix-of-tickets-shifts-week-to-week-.tsxtsx
"use client";
import { Streamgraph } from "@/components/manicat/streamgraph/streamgraph";
import type { StreamgraphDatum, StreamgraphSeries } from "@/components/manicat/streamgraph/streamgraph";
const DAY = 86_400_000;
const topics: StreamgraphSeries[] = [
{ key: "billing", label: "Billing" },
{ key: "access", label: "Access" },
{ key: "performance", label: "Performance" },
];
const tickets: StreamgraphDatum[] = Array.from({ length: 10 }, (_, index) => {
const date = new Date(Date.UTC(2026, 6, 6, 12) + index * 7 * DAY);
return {
key: date.toISOString().slice(0, 10),
label: "Week of " + date.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" }),
axisLabel: "W" + (27 + index),
values: {
billing: 40 + Math.round(30 * Math.sin(index / 1.6) + index * 4),
access: 55 + Math.round(22 * Math.sin(index / 1.9 + 1)),
performance: 30 + Math.round(26 * Math.sin(index / 1.4 + 2)),
},
};
});
export function StreamgraphTicketsExample() {
return <Streamgraph data={tickets} series={topics} label="Support tickets by topic" unit="tickets" legend />;
}
Keyboard interactions
| Keys | Action |
|---|---|
| Tab | Focuses the plot and reads the latest point. |
| ArrowLeft / ArrowRight | Moves through time. |
| ArrowUp / ArrowDown | Moves the isolated layer up or down the stack. |
| PageUp / PageDown / Home / End | Jumps by a sixth of the range, or to the first or last point. |
| Escape | Clears the reading. |
Accessibility
- The plot is a focusable group; each reading is announced with the point, the isolated layer's value, and the total.
- Legend toggles are buttons with aria-pressed; hovering or focusing one isolates its layer.
- A visually hidden table lists every point and layer value.
- Layers are separated by a 2px surface gap and named in place, so identity never rests on shade alone.
Motion
- The stream swells out of a hairline along its centre the first time it is seen.
- New data blends every layer on a shared grid, so ranges of different lengths morph without redrawing.
- Hidden layers thin to nothing while the rest reflow; the crosshair and tooltip glide on springs.
- Reduced motion jumps to the final shapes and keeps the crosshair and tooltip without travel.
Responsive behavior
- The plot fills its container; axis labels thin out to one per 72px, counted back from the latest.
- Direct labels hide where a layer is too thin or the plot too narrow; the tooltip and legend still name it.
- Touch drags scrub through time with pointer capture while vertical page scroll stays free.
Performance
- Each layer is resampled once per data change onto 161 points; paint recomputes the baseline and writes paths directly.
- Hover state re-renders only the tooltip and a few attributes; morphs never re-render React per frame.
Notes for AI
- Choose it for how a mix changes over time when the overall rhythm matters more than exact totals.
- Put the most important layer first; it runs through the middle of the stream.
- Use offset="zero" when people need to read totals against a baseline, like a stacked area chart.
- Keep layers to about eight; fold the rest into Other.