{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "streamgraph",
  "type": "registry:component",
  "title": "Streamgraph",
  "description": "How the mix shifts through the year. Sample data: shipments by four modes, January to December, October at 47K.",
  "registryDependencies": [
    "@chartory/core",
    "@chartory/plot-stream2"
  ],
  "files": [
    {
      "path": "registry/chartory/components/streamgraph.tsx",
      "type": "registry:component",
      "target": "components/chartory/streamgraph.tsx",
      "content": "// SPDX-License-Identifier: MIT · Copyright (c) 2026 BEY AGENCY LTD · Chartory, usechartory.com\n// Streamgraph (Chartory chart 014): sizes xs | s | m | l, laid out again from the data at each\n// size; collection \"hearth\" (default) or \"signal\" (with @chartory/signal, Code and Complete). Data in the type's shape (default: the sample). Docs:\n// https://usechartory.com/docs/charts/streamgraph\nimport { ChartView, type ChartViewProps } from \"@/lib/chartory/charts/chart-view\"\nimport { registerChart } from \"@/lib/chartory/charts/defs\"\nimport { data as sample } from \"@/lib/chartory/charts/types/014-streamgraph/data\"\nimport { def } from \"@/lib/chartory/charts/types/014-streamgraph/layout\"\n\nregisterChart(def, sample)\n\nexport type StreamgraphData = typeof sample\nexport type StreamgraphProps = Omit<ChartViewProps, \"def\" | \"data\" | \"states\"> & {\n  /** the data to draw (default: the sample) */\n  data?: StreamgraphData\n  /** further data states: the chart morphs to them (update) or ticks through them (live, with interval) */\n  states?: StreamgraphData[]\n}\n\nexport function Streamgraph(props: StreamgraphProps) {\n  return <ChartView def={def} {...props} />\n}\n"
    },
    {
      "path": "registry/chartory/charts/types/014-streamgraph/data.ts",
      "type": "registry:lib",
      "target": "lib/chartory/charts/types/014-streamgraph/data.ts",
      "content": "// SPDX-License-Identifier: MIT · Copyright (c) 2026 BEY AGENCY LTD · Chartory, usechartory.com\n// 014 · sample data (Figma chart/014-streamgraph, logistics: monthly shipments by mode in K, 2026).\nimport type { Stream2Data } from \"../../plots/stream2\";\n\nexport const data: Stream2Data = {\n  now: 9,\n  nowLabel: \"OCT · 47K\",\n  xl: { 0: \"JAN\", 3: \"APR\", 6: \"JUL\", 9: \"OCT\", 11: \"DEC\" },\n  layers: [\n    { name: \"Road\", tok: \"data/primary\", vals: [14, 13, 16, 19, 22, 24, 23, 21, 22, 23, 30, 27] },\n    { name: \"Sea\", tok: \"data/cat-2\", vals: [9, 8, 10, 12, 15, 17, 19, 18, 15, 10, 10, 9] },\n    { name: \"Rail\", tok: \"data/cat-3\", vals: [6, 6, 7, 8, 8, 7, 6, 6, 8, 8, 9, 8] },\n    { name: \"Air\", tok: \"data/cat-5\", vals: [2, 2, 3, 3, 4, 4, 4, 4, 5, 6, 10, 12] },\n  ],\n};\n"
    },
    {
      "path": "registry/chartory/charts/types/014-streamgraph/layout.ts",
      "type": "registry:lib",
      "target": "lib/chartory/charts/types/014-streamgraph/layout.ts",
      "content": "// SPDX-License-Identifier: MIT · Copyright (c) 2026 BEY AGENCY LTD · Chartory, usechartory.com\n// 014 Streamgraph (Figma chart/014-streamgraph): C.stream2 on shipments by mode; the layers carry their own labels\n// (no legend).\nimport type { ChartDef, TooltipSpec } from \"../../core/shell\";\nimport { stream2 } from \"../../plots/stream2\";\nimport { data } from \"./data\";\nimport { frame, type Frame, type Text } from \"../../core/node\";\n\n/** Deterministic wave in [-1, 1] per index and update step (no randomness: the same step gives the same period). */\nconst wave = (i: number, step: number, f = 1.7) => Math.sin(step * 2.3 + i * f + 0.5);\n/** A text is named by its characters, so an update state takes the sample's text names (same order): its value\n *  labels then stay the same marks and morph in place instead of reading as new ones. */\nfunction keepNames(pl: Frame, ref: Frame): Frame {\n  const texts = (f: Frame): Text[] => f.children.flatMap((c) => (c.removed ? [] : c.kind === \"text\" ? [c] : c.kind === \"frame\" ? texts(c) : []));\n  const a = texts(pl), b = texts(ref);\n  if (a.length === b.length) a.forEach((t, i) => { if (t.name === t.text.slice(0, 40)) t.name = b[i].name; });\n  return pl;\n}\n\nconst months = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\nconst oct = data.layers.reduce((a, l) => a + l.vals[9], 0);\n\n/** Hover tooltip: one mode's layer over the year (now, peak month, year total and share). */\nconst tip = (d: unknown, g: number): TooltipSpec => {\n  const o = d as typeof data, l = o.layers[g];\n  const sum = (v: number[]) => v.reduce((a, b) => a + b, 0);\n  const all = o.layers.reduce((a, x) => a + sum(x.vals), 0), pk = Math.max(...l.vals);\n  return {\n    kind: \"range\",\n    caps: `${l.name.toUpperCase()} · ${Math.round((sum(l.vals) / all) * 100)}% OF 2026`,\n    rows: [\n      { label: \"Now · \" + months[o.now], value: l.vals[o.now] + \"K\", swatch: l.tok },\n      { label: \"Peak · \" + months[l.vals.indexOf(pk)], value: pk + \"K\", swatch: l.tok },\n      { label: \"Year\", value: sum(l.vals) + \"K\", swatch: l.tok },\n    ],\n  };\n};\n\nexport const def: ChartDef = {\n  no: 14,\n  slug: \"streamgraph\",\n  legend: null,\n  plot: (K, p, w, d) => {\n    const o = (d as typeof data | undefined) ?? data;\n    const pl = stream2(K, p, w, o);\n    return o === data ? pl : keepNames(pl, stream2(K, frame(null, \"sample\"), w, data));\n  },\n  tip,\n  update: (d, step) => {\n    const o = d as typeof data;\n    // later years: each mode moves ±12 % a month (whole K); the now chip carries the new month total\n    const layers = o.layers.map((l, j) => ({ ...l, vals: l.vals.map((v, i) => Math.max(1, Math.round(v * (1 + 0.12 * wave(i + j * 4, step))))) }));\n    // a layer is labelled where its thickest month away from \"now\" is ≥ 16 px; plots are 120 (XS, S) to 230 (L) px for\n    // the largest month total, so a layer labelled at 120 keeps ≥ 16·max/120 and one unlabelled at 230 stays < 16·max/230\n    const n = o.layers[0].vals.length;\n    const ok = (i: number) => i >= 1 && i < n - 2 && Math.abs(i - o.now) > 1;\n    const peak = (v: number[]) => Math.max(...v.filter((_, i) => ok(i)));\n    const top = (ls: { vals: number[] }[]) => Math.max(...ls[0].vals.map((_, i) => ls.reduce((a, l) => a + l.vals[i], 0)));\n    const M0 = top(o.layers);\n    for (let pass = 0; pass < 6; pass++) {\n      const M = top(layers);\n      layers.forEach((l, j) => {\n        const p0 = peak(o.layers[j].vals), pk = peak(l.vals);\n        if (p0 * 120 >= 16 * M0 && pk * 120 < 16 * M) l.vals[l.vals.findIndex((v, i) => ok(i) && v === pk)] = Math.ceil((16 * M) / 120);\n        if (p0 * 230 < 16 * M0 && pk * 230 >= 16 * M) l.vals = l.vals.map((v, i) => (ok(i) ? Math.min(v, Math.ceil((16 * M) / 230) - 1) : v));\n      });\n    }\n    const now = layers.reduce((a, l) => a + l.vals[o.now], 0);\n    return { ...o, layers, nowLabel: o.nowLabel.replace(/\\d+K$/, now + \"K\") };\n  },\n  describe: {\n    title: \"Shipments by mode\",\n    summary: `Monthly shipments in K by mode, 2026, stacked around a centre line; October: ${oct}K in total (${data.layers.map((l) => `${l.name} ${l.vals[9]}K`).join(\", \")}).`,\n    table: { head: [\"Month\", ...data.layers.map((l) => l.name + \" (K)\")], rows: months.map((mo, i) => [mo, ...data.layers.map((l) => l.vals[i])]) },\n  },\n};\n"
    }
  ],
  "categories": [
    "charts",
    "change-over-time"
  ],
  "meta": {
    "access": "free",
    "no": 14,
    "family": "Change over time"
  }
}
