{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "age-pyramid",
  "type": "registry:component",
  "title": "Age pyramid",
  "description": "Two groups by age, back to back. Sample data: active customers by age, new against returning, 25–34 at 28% and 24%.",
  "registryDependencies": [
    "@chartory/core",
    "@chartory/plot-pyramid"
  ],
  "files": [
    {
      "path": "registry/chartory/components/age-pyramid.tsx",
      "type": "registry:component",
      "target": "components/chartory/age-pyramid.tsx",
      "content": "// SPDX-License-Identifier: MIT · Copyright (c) 2026 BEY AGENCY LTD · Chartory, usechartory.com\n// Age pyramid (Chartory chart 082): 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/age-pyramid\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/082-age-pyramid/data\"\nimport { def } from \"@/lib/chartory/charts/types/082-age-pyramid/layout\"\n\nregisterChart(def, sample)\n\nexport type AgePyramidData = typeof sample\nexport type AgePyramidProps = Omit<ChartViewProps, \"def\" | \"data\" | \"states\"> & {\n  /** the data to draw (default: the sample) */\n  data?: AgePyramidData\n  /** further data states: the chart morphs to them (update) or ticks through them (live, with interval) */\n  states?: AgePyramidData[]\n}\n\nexport function AgePyramid(props: AgePyramidProps) {\n  return <ChartView def={def} {...props} />\n}\n"
    },
    {
      "path": "registry/chartory/charts/types/082-age-pyramid/data.ts",
      "type": "registry:lib",
      "target": "lib/chartory/charts/types/082-age-pyramid/data.ts",
      "content": "// SPDX-License-Identifier: MIT · Copyright (c) 2026 BEY AGENCY LTD · Chartory, usechartory.com\n// 082 Customers by age · sample data (Figma chart/082-pyramid-customers).\nimport type { PyramidData } from \"../../plots/pyramid\";\n\nexport const data: PyramidData = {\n  max: 30,\n  aTok: \"data/cat-2\",\n  bTok: \"data/primary\",\n  rows: [{ label: \"18–24\", a: 14, b: 8 }, { label: \"25–34\", a: 28, b: 24 }, { label: \"35–44\", a: 24, b: 27 }, { label: \"45–54\", a: 17, b: 20 }, { label: \"55–64\", a: 11, b: 14 }, { label: \"65+\", a: 6, b: 7 }],\n};\n"
    },
    {
      "path": "registry/chartory/charts/types/082-age-pyramid/layout.ts",
      "type": "registry:lib",
      "target": "lib/chartory/charts/types/082-age-pyramid/layout.ts",
      "content": "// SPDX-License-Identifier: MIT · Copyright (c) 2026 BEY AGENCY LTD · Chartory, usechartory.com\n// 082 Age pyramid (Figma chart/082-pyramid-customers): C.pyramid, new customers left, returning right.\nimport type { ChartDef, TooltipSpec } from \"../../core/shell\";\nimport { pyramid } from \"../../plots/pyramid\";\nimport { data } from \"./data\";\n\nconst wob = (i: number, step: number) => Math.sin(i * 2.1 + step * 1.7 + 0.4);\n/** Shares that still sum to 100 (largest remainder), each at most the axis max. */\nfunction reshare(vals: number[], f: (i: number) => number, max: number): number[] {\n  const tot = vals.reduce((a, b) => a + b, 0), raw = vals.map((v, i) => v * f(i)), s = raw.reduce((a, b) => a + b, 0);\n  const ex = raw.map((v) => Math.min(max, (v / s) * tot)), out = ex.map(Math.floor);\n  const order = ex.map((v, i) => [v - Math.floor(v), i]).sort((a, b) => b[0] - a[0]);\n  for (let r = tot - out.reduce((a, b) => a + b, 0), j = 0; r > 0; j++) { const i = order[j % order.length][1]; if (out[i] < max) { out[i]++; r--; } }\n  return out;\n}\n/** A later quarter: the age mix of new and returning customers shifts; each side still sums to 100 %. */\nfunction update(d0: unknown, step: number): typeof data {\n  const d = d0 as typeof data;\n  const a = reshare(d.rows.map((r) => r.a), (i) => 1 + 0.14 * wob(i, step), d.max);\n  const b = reshare(d.rows.map((r) => r.b), (i) => 1 + 0.14 * wob(i + 7, step), d.max);\n  return { ...d, rows: d.rows.map((r, i) => ({ ...r, a: a[i], b: b[i] })) };\n}\n\n/** Hover: one age band (both bars and their %): the share of new and of returning customers in it. */\nfunction tip(d0: unknown, g: number): TooltipSpec | null {\n  const d = d0 as typeof data, r = d.rows[g];\n  return r ? { kind: \"series\", caps: \"AGE \" + r.label, rows: [{ label: \"New\", value: r.a + \"%\", swatch: d.aTok }, { label: \"Returning\", value: r.b + \"%\", swatch: d.bTok }] } : null;\n}\n\nexport const def: ChartDef = {\n  no: 82,\n  slug: \"age-pyramid\",\n  legend: [[\"data/cat-2\", \"New\"], [\"data/primary\", \"Returning\"]],\n  plot: (K, p, w, d) => pyramid(K, p, w, (d as typeof data | undefined) ?? data),\n  tip,\n  update,\n  describe: {\n    title: \"Customers by age\",\n    summary: `Share of new and returning customers by age band: 25–34 is the largest new band (${data.rows[1].a}%), 35–44 the largest returning band (${data.rows[2].b}%).`,\n    table: { head: [\"Age\", \"New (%)\", \"Returning (%)\"], rows: data.rows.map((r) => [r.label, r.a, r.b]) },\n  },\n};\n"
    }
  ],
  "categories": [
    "charts",
    "distribution"
  ],
  "meta": {
    "access": "free",
    "no": 82,
    "family": "Distribution"
  }
}
