@pixerate/editor 0.46.1 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/THIRD_PARTY_NOTICES.md +20 -0
  2. package/dist/agent/index.cjs +252 -34
  3. package/dist/agent/index.cjs.map +1 -1
  4. package/dist/agent/index.d.cts +19 -7
  5. package/dist/agent/index.d.ts +19 -7
  6. package/dist/agent/index.js +228 -10
  7. package/dist/agent/index.js.map +1 -1
  8. package/dist/{builtins-CZW-aYKS.d.cts → builtins-1kqwLUnb.d.cts} +2 -2
  9. package/dist/{builtins-DghNQbT-.d.ts → builtins-BKYlHH1s.d.ts} +2 -2
  10. package/dist/chart/index.cjs +1596 -1849
  11. package/dist/chart/index.cjs.map +1 -1
  12. package/dist/chart/index.d.cts +760 -548
  13. package/dist/chart/index.d.ts +760 -548
  14. package/dist/chart/index.js +1499 -1752
  15. package/dist/chart/index.js.map +1 -1
  16. package/dist/{chunk-3RPOW4CS.js → chunk-2CJ3LPYQ.js} +12 -10
  17. package/dist/{chunk-3RPOW4CS.js.map → chunk-2CJ3LPYQ.js.map} +1 -1
  18. package/dist/{chunk-PQM5W7ZN.js → chunk-4FX5ACVG.js} +29 -2
  19. package/dist/{chunk-PQM5W7ZN.js.map → chunk-4FX5ACVG.js.map} +1 -1
  20. package/dist/{chunk-HHQ4XJWT.js → chunk-AMWXSYCI.js} +2 -2
  21. package/dist/chunk-EKYHKQPG.js +1895 -0
  22. package/dist/chunk-EKYHKQPG.js.map +1 -0
  23. package/dist/chunk-HXUL3LS7.js +113 -0
  24. package/dist/chunk-HXUL3LS7.js.map +1 -0
  25. package/dist/{chunk-ZRNHGAQT.js → chunk-JQ3BB3E6.js} +43 -1891
  26. package/dist/chunk-JQ3BB3E6.js.map +1 -0
  27. package/dist/chunk-LTUM52X6.js +4074 -0
  28. package/dist/chunk-LTUM52X6.js.map +1 -0
  29. package/dist/{chunk-J3R2OWOE.cjs → chunk-MUCYWI6W.cjs} +608 -15
  30. package/dist/chunk-MUCYWI6W.cjs.map +1 -0
  31. package/dist/chunk-PIMCHMZE.cjs +521 -0
  32. package/dist/chunk-PIMCHMZE.cjs.map +1 -0
  33. package/dist/{chunk-TK2XZK6Z.js → chunk-PKBWLEL7.js} +594 -1
  34. package/dist/chunk-PKBWLEL7.js.map +1 -0
  35. package/dist/chunk-PRSJJZBS.cjs +4074 -0
  36. package/dist/chunk-PRSJJZBS.cjs.map +1 -0
  37. package/dist/{chunk-EFDCLENW.cjs → chunk-RIWPYSKU.cjs} +29 -2
  38. package/dist/chunk-RIWPYSKU.cjs.map +1 -0
  39. package/dist/chunk-U6IZKC7B.cjs +113 -0
  40. package/dist/chunk-U6IZKC7B.cjs.map +1 -0
  41. package/dist/{chunk-EWCOFSCQ.cjs → chunk-UN33YOXW.cjs} +252 -2100
  42. package/dist/chunk-UN33YOXW.cjs.map +1 -0
  43. package/dist/{chunk-Q25ZS4F7.js → chunk-VKPEPWKZ.js} +72 -92
  44. package/dist/chunk-VKPEPWKZ.js.map +1 -0
  45. package/dist/{chunk-N4MLNJPO.cjs → chunk-WXEHCVJQ.cjs} +160 -180
  46. package/dist/chunk-WXEHCVJQ.cjs.map +1 -0
  47. package/dist/{chunk-K2LVZ4FA.cjs → chunk-XFYUFVAX.cjs} +3 -3
  48. package/dist/{chunk-K2LVZ4FA.cjs.map → chunk-XFYUFVAX.cjs.map} +1 -1
  49. package/dist/chunk-XP5IUZUO.cjs +1895 -0
  50. package/dist/chunk-XP5IUZUO.cjs.map +1 -0
  51. package/dist/chunk-XUUD43OV.js +521 -0
  52. package/dist/chunk-XUUD43OV.js.map +1 -0
  53. package/dist/{chunk-U7VGDSNY.cjs → chunk-YU4WQE5M.cjs} +101 -99
  54. package/dist/chunk-YU4WQE5M.cjs.map +1 -0
  55. package/dist/commands/index.cjs +93 -11
  56. package/dist/commands/index.cjs.map +1 -1
  57. package/dist/commands/index.d.cts +9 -7
  58. package/dist/commands/index.d.ts +9 -7
  59. package/dist/commands/index.js +92 -10
  60. package/dist/commands/index.js.map +1 -1
  61. package/dist/controller-ChjCCMLl.d.cts +822 -0
  62. package/dist/controller-DtbEo3om.d.ts +822 -0
  63. package/dist/extensions/index.cjs +32 -3
  64. package/dist/extensions/index.cjs.map +1 -1
  65. package/dist/extensions/index.d.cts +5 -2
  66. package/dist/extensions/index.d.ts +5 -2
  67. package/dist/extensions/index.js +31 -2
  68. package/dist/generation/index.cjs +2 -2
  69. package/dist/generation/index.js +1 -1
  70. package/dist/{index-CfOQf70b.d.cts → index-DoHqT1mq.d.cts} +222 -5
  71. package/dist/{index-jwJ_onWf.d.ts → index-YLZ0fKqE.d.ts} +222 -5
  72. package/dist/index.cjs +38 -10
  73. package/dist/index.cjs.map +1 -1
  74. package/dist/index.d.cts +13 -5
  75. package/dist/index.d.ts +13 -5
  76. package/dist/index.js +52 -24
  77. package/dist/index.js.map +1 -1
  78. package/dist/parameters/index.d.cts +1 -1
  79. package/dist/parameters/index.d.ts +1 -1
  80. package/dist/{registry-vz6HOiiy.d.cts → registry-CaR7LC6W.d.cts} +2 -0
  81. package/dist/{registry-BfVD-vVg.d.ts → registry-rVtFJjRM.d.ts} +2 -0
  82. package/dist/spreadsheet/index.cjs +5 -3
  83. package/dist/spreadsheet/index.cjs.map +1 -1
  84. package/dist/spreadsheet/index.js +24 -22
  85. package/dist/tools/index.cjs +3 -3
  86. package/dist/tools/index.js +2 -2
  87. package/dist/vector/index.d.cts +1 -1
  88. package/dist/vector/index.d.ts +1 -1
  89. package/package.json +2 -1
  90. package/dist/chunk-EFDCLENW.cjs.map +0 -1
  91. package/dist/chunk-EWCOFSCQ.cjs.map +0 -1
  92. package/dist/chunk-J3R2OWOE.cjs.map +0 -1
  93. package/dist/chunk-N4MLNJPO.cjs.map +0 -1
  94. package/dist/chunk-Q25ZS4F7.js.map +0 -1
  95. package/dist/chunk-TK2XZK6Z.js.map +0 -1
  96. package/dist/chunk-U7VGDSNY.cjs.map +0 -1
  97. package/dist/chunk-ZRNHGAQT.js.map +0 -1
  98. /package/dist/{chunk-HHQ4XJWT.js.map → chunk-AMWXSYCI.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/chunk-PRSJJZBS.cjs","../src/chart/color.ts","../src/chart/spec.ts","../src/chart/mermaid.ts","../src/chart/date.ts","../src/chart/numfmt.ts","../src/chart/scale.ts","../src/chart/layout-shared.ts","../src/chart/stats.ts","../src/chart/trend.ts","../src/chart/layout-cartesian.ts","../src/chart/layout-families.ts","../src/chart/layout.ts","../src/chart/summary.ts","../src/chart/svg.ts","../src/chart/presets.ts","../src/chart/controller.ts","../src/chart/sparkline.ts"],"names":["text","r2","key","fy"],"mappings":"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A;AACyE,EAAA;AACjD,EAAA;AAC2C,EAAA;AACA,EAAA;AACjE,EAAA;AACA,EAAA;AACY,EAAA;AACN,IAAA;AACwB,IAAA;AACA,IAAA;AACxC,EAAA;AACoB,EAAA;AACF,IAAA;AACuD,IAAA;AACF,IAAA;AAC/D,IAAA;AACuB,IAAA;AACX,IAAA;AACE,MAAA;AACkF,QAAA;AAC9B,QAAA;AACzE,MAAA;AACO,MAAA;AACR,IAAA;AACwE,IAAA;AAClE,IAAA;AAC4F,IAAA;AACvD,IAAA;AACX,IAAA;AACa,MAAA;AACrB,MAAA;AACmF,QAAA;AACzG,MAAA;AACF,IAAA;AAC8B,IAAA;AAC8E,MAAA;AACDC,MAAAA;AAC3G,IAAA;AACO,IAAA;AACR,EAAA;AACkB,EAAA;AACkB,EAAA;AACf,EAAA;AACgB,EAAA;AACD,EAAA;AACjB,EAAA;AAC4D,EAAA;AAC9D,EAAA;AACQ,EAAA;AACN,IAAA;AACe,IAAA;AACb,IAAA;AACJ,IAAA;AACkD,IAAA;AAC4C,IAAA;AAC9G,EAAA;AACM,EAAA;AACR;AAG+H;AACrC,EAAA;AAC1F;AjB8wHkH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/editor/editor/packages/core/dist/chunk-PRSJJZBS.cjs","sourcesContent":[null,"// Portions ported from gridcraft-main/crates/chart/src/resolve.rs (palette tint cycle) and\n// crates/chart/src/render.rs (`on`: label colour from the fill), and designcraft-main/crates/color/src/cms/mod.rs\n// (`simulate_cvd`, Machado–Oliveira–Fernandes 2009 matrices) (MIT OR Apache-2.0, © ArtCraft Team and contributors).\n// See THIRD_PARTY_NOTICES.md.\n\n/** An sRGB colour with channels from 0 to 1. */\nexport type ChartRgb = [number, number, number];\n\nconst NAMED: Record<string, string> = {\n\tblack: '#000000',\n\twhite: '#ffffff',\n\tred: '#ff0000',\n\tgreen: '#008000',\n\tblue: '#0000ff',\n\tyellow: '#ffff00',\n\torange: '#ffa500',\n\tpurple: '#800080',\n\tgray: '#808080',\n\tgrey: '#808080',\n\tteal: '#008080',\n\tnavy: '#000080',\n\tmaroon: '#800000',\n\tolive: '#808000',\n\tsilver: '#c0c0c0',\n\tlime: '#00ff00',\n\taqua: '#00ffff',\n\tcyan: '#00ffff',\n\tfuchsia: '#ff00ff',\n\tmagenta: '#ff00ff',\n\tpink: '#ffc0cb',\n\tbrown: '#a52a2a'\n};\n\n/**\n * Reads `#rgb`, `#rrggbb`, `#rrggbbaa`, `rgb()` / `rgba()` and a few colour names. Returns\n * `null` for anything else (CSS variables, `hsl()`, …), so callers can leave such colours alone.\n */\nexport function parseChartColor(color: string): ChartRgb | null {\n\tconst text = color.trim().toLowerCase();\n\tconst hex = NAMED[text] ?? text;\n\tlet m = /^#([0-9a-f]{3})$/.exec(hex);\n\tif (m) return [...m[1]].map((c) => parseInt(c + c, 16) / 255) as ChartRgb;\n\tm = /^#([0-9a-f]{6})([0-9a-f]{2})?$/.exec(hex);\n\tif (m) return [0, 2, 4].map((i) => parseInt(m![1].slice(i, i + 2), 16) / 255) as ChartRgb;\n\tm = /^rgba?\\(\\s*([\\d.]+)%?[\\s,]+([\\d.]+)%?[\\s,]+([\\d.]+)%?(?:[\\s,/]+[\\d.]+%?)?\\s*\\)$/.exec(text);\n\tif (m) {\n\t\tconst pct = text.includes('%');\n\t\treturn [m[1], m[2], m[3]].map((v) => Math.max(0, Math.min(1, Number(v) / (pct ? 100 : 255)))) as ChartRgb;\n\t}\n\treturn null;\n}\n\nexport function chartRgbToHex(rgb: ChartRgb): string {\n\treturn `#${rgb.map((c) => Math.round(Math.max(0, Math.min(1, c)) * 255).toString(16).padStart(2, '0')).join('')}`;\n}\n\n/**\n * A lighter (`amount` > 0, towards white) or darker (`amount` < 0, towards black) shade of\n * `color`. Colours it cannot read (CSS variables) come back unchanged.\n */\nexport function shadeChartColor(color: string, amount: number): string {\n\tconst rgb = parseChartColor(color);\n\tif (!rgb || !amount) return color;\n\tconst t = Math.max(-1, Math.min(1, amount));\n\treturn chartRgbToHex(rgb.map((c) => (t > 0 ? c + (1 - c) * t : c * (1 + t))) as ChartRgb);\n}\n\n/** Shades used once a palette is used up: the same hues, lighter then darker (GridCraft's cycle). */\nexport const CHART_PALETTE_SHADES: readonly number[] = [0, 0.4, -0.25, 0.6, -0.5, 0.2];\n\n/** The palette colour for the `index`-th item: the palette in order, then lighter and darker rounds of it. */\nexport function cyclePaletteColor(palette: readonly string[], index: number): string {\n\tconst base = palette[index % palette.length];\n\tconst round = Math.floor(index / palette.length) % CHART_PALETTE_SHADES.length;\n\treturn shadeChartColor(base, CHART_PALETTE_SHADES[round]);\n}\n\nconst toLinear = (c: number) => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));\nconst fromLinear = (c: number) => (c <= 0.0031308 ? c * 12.92 : 1.055 * Math.pow(c, 1 / 2.4) - 0.055);\n\n/** WCAG relative luminance (0 black – 1 white). */\nexport function chartLuminance(rgb: ChartRgb): number {\n\tconst [r, g, b] = rgb.map(toLinear);\n\treturn 0.2126 * r + 0.7152 * g + 0.0722 * b;\n}\n\n/** WCAG contrast ratio (1–21) between two colours, or `null` when either cannot be read. */\nexport function chartContrastRatio(a: string, b: string): number | null {\n\tconst ra = parseChartColor(a);\n\tconst rb = parseChartColor(b);\n\tif (!ra || !rb) return null;\n\tconst la = chartLuminance(ra);\n\tconst lb = chartLuminance(rb);\n\treturn (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);\n}\n\n/**\n * Dark or white text, whichever reads better on `fill`; `fallback` when the fill cannot be\n * read (a CSS variable).\n */\nexport function chartTextOn(fill: string, fallback: string, dark = '#0f172a', light = '#ffffff'): string {\n\tconst rgb = parseChartColor(fill);\n\tif (!rgb) return fallback;\n\tconst darkRatio = chartContrastRatio(fill, dark)!;\n\tconst lightRatio = chartContrastRatio(fill, light)!;\n\treturn darkRatio >= lightRatio ? dark : light;\n}\n\n/** OKLab (Ottosson) from sRGB. */\nexport function chartRgbToOklab(rgb: ChartRgb): [number, number, number] {\n\tconst [r, g, b] = rgb.map(toLinear);\n\tconst l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);\n\tconst m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);\n\tconst s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);\n\treturn [\n\t\t0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,\n\t\t1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,\n\t\t0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s\n\t];\n}\n\n/** Perceptual distance between two colours (Euclidean in OKLab; ≈0.02 is just noticeable). */\nexport function chartColorDistance(a: ChartRgb, b: ChartRgb): number {\n\tconst p = chartRgbToOklab(a);\n\tconst q = chartRgbToOklab(b);\n\treturn Math.hypot(p[0] - q[0], p[1] - q[1], p[2] - q[2]);\n}\n\nexport type ChartVision = 'normal' | 'protanopia' | 'deuteranopia' | 'tritanopia';\n\n// Machado, Oliveira & Fernandes (2009), severity 1.0, applied in linear RGB.\nconst CVD: Record<Exclude<ChartVision, 'normal'>, number[][]> = {\n\tprotanopia: [\n\t\t[0.152286, 1.052583, -0.204868],\n\t\t[0.114503, 0.786281, 0.099216],\n\t\t[-0.003882, -0.048116, 1.051998]\n\t],\n\tdeuteranopia: [\n\t\t[0.367322, 0.860646, -0.227968],\n\t\t[0.280085, 0.672501, 0.047413],\n\t\t[-0.01182, 0.04294, 0.968881]\n\t],\n\ttritanopia: [\n\t\t[1.255528, -0.076749, -0.178779],\n\t\t[-0.078411, 0.930809, 0.147602],\n\t\t[0.004733, 0.691367, 0.3039]\n\t]\n};\n\n/** How a colour looks with a colour-vision deficiency (Machado et al. 2009, full severity). */\nexport function simulateChartVision(rgb: ChartRgb, vision: ChartVision): ChartRgb {\n\tif (vision === 'normal') return rgb;\n\tconst lin = rgb.map(toLinear);\n\tconst m = CVD[vision];\n\treturn m.map((row) => fromLinear(Math.max(0, Math.min(1, row[0] * lin[0] + row[1] * lin[1] + row[2] * lin[2])))) as ChartRgb;\n}\n\nexport interface ChartColorPair {\n\ta: number;\n\tb: number;\n\tcolors: [string, string];\n\tdistance: number;\n}\n\nexport interface ChartColorCheck {\n\t/** No problems found. */\n\tok: boolean;\n\t/** Per vision: the closest pair of colours and their OKLab distance. */\n\tclosest: Partial<Record<ChartVision, ChartColorPair>>;\n\t/** Contrast ratio of each colour against the surface (null when unreadable). */\n\tcontrast: (number | null)[];\n\t/** Problems, as sentences. */\n\tissues: string[];\n\t/** Colours that could not be read (CSS variables, `hsl()`); they are skipped. */\n\tskipped: string[];\n}\n\nexport interface ChartColorCheckOptions {\n\t/** The page colour behind the marks (default white). */\n\tsurface?: string;\n\t/** Smallest OKLab distance two series may have (default 0.08). */\n\tminDistance?: number;\n\t/** Smallest contrast ratio against the surface (default 1.5; WCAG asks 3 for graphics). */\n\tminContrast?: number;\n\t/** Visions to check (default all four). */\n\tvisions?: readonly ChartVision[];\n}\n\n/**\n * Checks that series colours can be told apart, with normal vision and the three\n * colour-vision deficiencies, and stand out from the surface. Returns the closest pair per\n * vision and problems as sentences.\n */\nexport function checkChartColors(colors: readonly string[], options: ChartColorCheckOptions = {}): ChartColorCheck {\n\tconst surface = options.surface ?? '#ffffff';\n\tconst minDistance = options.minDistance ?? 0.08;\n\tconst minContrast = options.minContrast ?? 1.5;\n\tconst visions = options.visions ?? (['normal', 'protanopia', 'deuteranopia', 'tritanopia'] as const);\n\tconst parsed = colors.map(parseChartColor);\n\tconst skipped = colors.filter((_, i) => !parsed[i]);\n\tconst issues: string[] = [];\n\tconst closest: ChartColorCheck['closest'] = {};\n\tfor (const vision of visions) {\n\t\tconst seen = parsed.map((c) => (c ? simulateChartVision(c, vision) : null));\n\t\tlet best: ChartColorPair | undefined;\n\t\tfor (let a = 0; a < seen.length; a++) {\n\t\t\tfor (let b = a + 1; b < seen.length; b++) {\n\t\t\t\tif (!seen[a] || !seen[b]) continue;\n\t\t\t\tconst distance = chartColorDistance(seen[a]!, seen[b]!);\n\t\t\t\tif (!best || distance < best.distance) best = { a, b, colors: [colors[a], colors[b]], distance: Math.round(distance * 1000) / 1000 };\n\t\t\t}\n\t\t}\n\t\tif (best) {\n\t\t\tclosest[vision] = best;\n\t\t\tif (best.distance < minDistance) {\n\t\t\t\tconst who = vision === 'normal' ? '' : ` with ${vision}`;\n\t\t\t\tissues.push(`Colours ${best.a + 1} (${best.colors[0]}) and ${best.b + 1} (${best.colors[1]}) are hard to tell apart${who} (distance ${best.distance}, want at least ${minDistance}).`);\n\t\t\t}\n\t\t}\n\t}\n\tconst contrast = colors.map((c) => {\n\t\tconst ratio = chartContrastRatio(c, surface);\n\t\treturn ratio === null ? null : Math.round(ratio * 100) / 100;\n\t});\n\tcontrast.forEach((ratio, i) => {\n\t\tif (ratio !== null && ratio < minContrast) issues.push(`Colour ${i + 1} (${colors[i]}) barely stands out from the background ${surface} (contrast ${ratio}:1, want at least ${minContrast}:1).`);\n\t});\n\treturn { ok: issues.length === 0, closest, contrast, issues, skipped };\n}\n","import { cyclePaletteColor } from './color.js';\nimport type {\n\tChartAxis,\n\tChartCurve,\n\tChartIssue,\n\tChartLabelPosition,\n\tChartLegend,\n\tChartLegendPosition,\n\tChartMarker,\n\tChartNumberFormat,\n\tChartOrientation,\n\tChartSeries,\n\tChartSeriesType,\n\tChartSpec,\n\tChartStacking,\n\tChartTheme,\n\tChartTrendline,\n\tChartTrendlineType,\n\tChartType,\n\tChartValue\n} from './types.js';\n\nexport const CHART_TYPES: readonly ChartType[] = ['bar', 'line', 'area', 'scatter', 'bubble', 'pie', 'donut', 'radar', 'waterfall', 'histogram', 'box', 'funnel', 'treemap'];\nexport const CHART_MARKERS: readonly ChartMarker[] = ['circle', 'square', 'diamond', 'triangle', 'cross', 'none'];\nexport const CHART_TRENDLINE_TYPES: readonly ChartTrendlineType[] = ['linear', 'exponential', 'logarithmic', 'power', 'polynomial', 'moving-average'];\nexport const CHART_LABEL_POSITIONS: readonly ChartLabelPosition[] = ['auto', 'inside', 'outside', 'center'];\nexport const CHART_AXIS_KINDS: readonly NonNullable<ChartAxis['kind']>[] = ['category', 'date'];\nexport const CHART_SERIES_TYPES: readonly ChartSeriesType[] = ['bar', 'line', 'area', 'scatter'];\nexport const CHART_STACKINGS: readonly ChartStacking[] = ['none', 'stacked', 'percent'];\nexport const CHART_CURVES: readonly ChartCurve[] = ['linear', 'smooth', 'step'];\nexport const CHART_ORIENTATIONS: readonly ChartOrientation[] = ['vertical', 'horizontal'];\nexport const CHART_LEGEND_POSITIONS: readonly ChartLegendPosition[] = ['top', 'bottom', 'left', 'right'];\nexport const CHART_NUMBER_STYLES: readonly NonNullable<ChartNumberFormat['style']>[] = ['number', 'percent', 'currency', 'compact'];\n\n/** Labels for pickers, shared by the React and Svelte components. */\nexport const CHART_TYPE_LABELS: Readonly<Record<ChartType, string>> = {\n\tbar: 'Bar',\n\tline: 'Line',\n\tarea: 'Area',\n\tscatter: 'Scatter',\n\tbubble: 'Bubble',\n\tpie: 'Pie',\n\tdonut: 'Donut',\n\tradar: 'Radar',\n\twaterfall: 'Waterfall',\n\thistogram: 'Histogram',\n\tbox: 'Box plot',\n\tfunnel: 'Funnel',\n\ttreemap: 'Treemap'\n};\n\n/** How a chart type is laid out. */\nexport type ChartFamily = 'cartesian' | 'xy' | 'pie' | 'radar' | 'waterfall' | 'histogram' | 'box' | 'funnel' | 'treemap';\n\n/** A colour-blind-friendly categorical palette (Okabe–Ito order, tuned for light and dark backgrounds). */\nexport const DEFAULT_CHART_PALETTE: readonly string[] = ['#4269d0', '#efb118', '#ff725c', '#6cc5b0', '#3ca951', '#ff8ab7', '#a463f2', '#97bbf5', '#9c6b4e', '#9498a0'];\n\nexport const DEFAULT_CHART_THEME: ChartTheme = {\n\tbackground: '#ffffff',\n\tsurface: '#ffffff',\n\ttext: '#0f172a',\n\tmutedText: '#64748b',\n\tgrid: '#e2e8f0',\n\taxis: '#94a3b8',\n\tselection: '#2563eb',\n\tpositive: '#3ca951',\n\tnegative: '#e5484d',\n\tfontFamily: 'system-ui, -apple-system, \"Segoe UI\", sans-serif',\n\tfontSize: 11,\n\ttitleSize: 15\n};\n\n/**\n * A theme that reads the page's `--background`, `--foreground`, `--muted-foreground`,\n * `--border` and `--primary` colour variables (full colours, as the other components'\n * inline styles use them), with light fallbacks. The React and Svelte canvases use it.\n */\nexport const CSS_VARIABLE_CHART_THEME: ChartTheme = {\n\t...DEFAULT_CHART_THEME,\n\tbackground: 'transparent',\n\tsurface: 'var(--background, #ffffff)',\n\ttext: 'var(--foreground, #0f172a)',\n\tmutedText: 'var(--muted-foreground, #64748b)',\n\tgrid: 'var(--border, #e2e8f0)',\n\taxis: 'var(--muted-foreground, #94a3b8)',\n\tselection: 'var(--primary, #2563eb)',\n\tfontFamily: 'inherit'\n};\n\n/** Bar, line, area and scatter: categories (or numeric x) against values, series may mix. */\nexport function isCartesianChart(type: ChartType): boolean {\n\treturn type === 'bar' || type === 'line' || type === 'area' || type === 'scatter';\n}\n\n/** Whether the chart plots series against a numeric x axis (bubble charts, and scatter charts with x values). */\nexport function isXyChart(spec: Pick<ChartSpec, 'type' | 'series'>): boolean {\n\tif (spec.type === 'bubble') return true;\n\treturn spec.type === 'scatter' && spec.series.some((s) => !s.hidden && Array.isArray(s.x) && s.x.some((v) => v !== null));\n}\n\n/** How a chart is laid out (see `ChartType`). */\nexport function chartFamily(spec: Pick<ChartSpec, 'type' | 'series'>): ChartFamily {\n\tif (isXyChart(spec)) return 'xy';\n\tswitch (spec.type) {\n\t\tcase 'pie':\n\t\tcase 'donut':\n\t\t\treturn 'pie';\n\t\tcase 'radar':\n\t\tcase 'waterfall':\n\t\tcase 'histogram':\n\t\tcase 'box':\n\t\tcase 'funnel':\n\t\tcase 'treemap':\n\t\t\treturn spec.type;\n\t\tcase 'bubble':\n\t\t\treturn 'xy';\n\t\tdefault:\n\t\t\treturn 'cartesian';\n\t}\n}\n\n/** Chart types whose legend lists categories (one colour per category) rather than series. */\nexport function chartColorsByCategory(type: ChartType): boolean {\n\treturn type === 'pie' || type === 'donut' || type === 'funnel' || type === 'treemap';\n}\n\nexport function defaultChartAxis(value: boolean): ChartAxis {\n\treturn { show: true, title: '', min: null, max: null, step: null, gridlines: value, format: {} };\n}\n\n/** A complete spec with every default filled in. */\nexport function createChartSpec(partial: Partial<ChartSpec> = {}): ChartSpec {\n\treturn normalizeChartSpec(partial).spec;\n}\n\n/** The colour a series is drawn with. */\nexport function chartSeriesColor(spec: Pick<ChartSpec, 'palette' | 'series'>, seriesIndex: number): string {\n\tconst series = spec.series[seriesIndex];\n\tif (series?.color) return series.color;\n\treturn cyclePaletteColor(spec.palette.length ? spec.palette : DEFAULT_CHART_PALETTE, seriesIndex);\n}\n\n/** The colour a pie slice is drawn with (by category). */\nexport function chartSliceColor(spec: Pick<ChartSpec, 'palette'>, index: number): string {\n\treturn cyclePaletteColor(spec.palette.length ? spec.palette : DEFAULT_CHART_PALETTE, index);\n}\n\n/** The colour of one bar or point: its override, else the series colour. */\nexport function chartPointColor(spec: Pick<ChartSpec, 'palette' | 'series'>, seriesIndex: number, index: number): string {\n\treturn spec.series[seriesIndex]?.pointColors?.[index] || chartSeriesColor(spec, seriesIndex);\n}\n\n/** How a series is drawn in a cartesian chart. */\nexport function chartSeriesType(spec: Pick<ChartSpec, 'type'>, series: ChartSeries): ChartSeriesType {\n\tif (series.type) return series.type;\n\tif (spec.type === 'bubble') return 'scatter';\n\treturn isCartesianChart(spec.type) ? (spec.type as ChartSeriesType) : 'bar';\n}\n\n/** An id not used by any series (`series-1`, `series-2`, …). */\nexport function uniqueChartSeriesId(series: readonly Pick<ChartSeries, 'id'>[], base = 'series'): string {\n\tconst used = new Set(series.map((s) => s.id));\n\tfor (let i = series.length + 1; ; i++) {\n\t\tconst id = `${base}-${i}`;\n\t\tif (!used.has(id)) return id;\n\t}\n}\n\n/**\n * Reads a number the way people type it in a data grid: `1,234.5`, `12%` (→ 0.12),\n * `$40`, `(3)` (→ -3). Blank → `null`. Returns `undefined` when it is not a number.\n */\nexport function parseChartValue(input: unknown): ChartValue | undefined {\n\tif (input === null || input === undefined) return null;\n\tif (typeof input === 'number') return Number.isFinite(input) ? input : undefined;\n\tif (typeof input !== 'string') return undefined;\n\tlet text = input.trim();\n\tif (!text || text === '-' || text === '—') return null;\n\tlet negative = false;\n\tif (/^\\(.*\\)$/.test(text)) {\n\t\tnegative = true;\n\t\ttext = text.slice(1, -1).trim();\n\t}\n\tconst percent = text.endsWith('%');\n\tif (percent) text = text.slice(0, -1).trim();\n\ttext = text.replace(/^[$€£¥]/, '').replace(/,/g, '').replace(/\\s+/g, '');\n\tif (!/^[-+]?(\\d+\\.?\\d*|\\.\\d+)(e[-+]?\\d+)?$/i.test(text)) return undefined;\n\tlet value = Number(text);\n\tif (percent) value /= 100;\n\tif (negative) value = -value;\n\treturn Number.isFinite(value) ? value : undefined;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value);\n\nfunction pick<T extends string>(value: unknown, options: readonly T[], fallback: T, path: string, issues: ChartIssue[]): T {\n\tif (value === undefined) return fallback;\n\tif (typeof value === 'string' && (options as readonly string[]).includes(value)) return value as T;\n\tissues.push({ path, message: `${path} must be one of ${options.map((o) => `\"${o}\"`).join(', ')}; got ${JSON.stringify(value)}.` });\n\treturn fallback;\n}\n\nfunction text(value: unknown, fallback: string, path: string, issues: ChartIssue[]): string {\n\tif (value === undefined || value === null) return fallback;\n\tif (typeof value === 'string') return value;\n\tif (typeof value === 'number' || typeof value === 'boolean') return String(value);\n\tissues.push({ path, message: `${path} must be text.` });\n\treturn fallback;\n}\n\nfunction flag(value: unknown, fallback: boolean, path: string, issues: ChartIssue[]): boolean {\n\tif (value === undefined) return fallback;\n\tif (typeof value === 'boolean') return value;\n\tissues.push({ path, message: `${path} must be true or false.` });\n\treturn fallback;\n}\n\nfunction bound(value: unknown, path: string, issues: ChartIssue[]): number | null {\n\tif (value === undefined || value === null || value === '') return null;\n\tconst n = typeof value === 'string' ? Number(value) : value;\n\tif (typeof n === 'number' && Number.isFinite(n)) return n;\n\tissues.push({ path, message: `${path} must be a number or null.` });\n\treturn null;\n}\n\nfunction normalizeFormat(value: unknown, path: string, issues: ChartIssue[]): ChartNumberFormat {\n\tif (value === undefined || value === null) return {};\n\tif (!isRecord(value)) {\n\t\tissues.push({ path, message: `${path} must be an object.` });\n\t\treturn {};\n\t}\n\tconst out: ChartNumberFormat = {};\n\tif (value.style !== undefined) out.style = pick(value.style, CHART_NUMBER_STYLES, 'number', `${path}.style`, issues);\n\tif (value.decimals !== undefined) {\n\t\tconst d = Number(value.decimals);\n\t\tif (Number.isInteger(d) && d >= 0 && d <= 10) out.decimals = d;\n\t\telse issues.push({ path: `${path}.decimals`, message: `${path}.decimals must be a whole number from 0 to 10.` });\n\t}\n\tfor (const key of ['currency', 'prefix', 'suffix', 'code'] as const) {\n\t\tif (value[key] !== undefined) out[key] = text(value[key], '', `${path}.${key}`, issues);\n\t}\n\treturn out;\n}\n\nexport function normalizeChartAxis(value: unknown, fallback: ChartAxis, path: string, issues: ChartIssue[] = []): ChartAxis {\n\tif (value === undefined) return { ...fallback, format: { ...fallback.format } };\n\tif (!isRecord(value)) {\n\t\tissues.push({ path, message: `${path} must be an object.` });\n\t\treturn { ...fallback, format: { ...fallback.format } };\n\t}\n\tlet min = 'min' in value ? bound(value.min, `${path}.min`, issues) : fallback.min;\n\tlet max = 'max' in value ? bound(value.max, `${path}.max`, issues) : fallback.max;\n\tif (min !== null && max !== null && min >= max) {\n\t\tissues.push({ path: `${path}.max`, message: `${path}.max (${max}) must be greater than ${path}.min (${min}).` });\n\t\tmin = fallback.min;\n\t\tmax = fallback.max;\n\t}\n\tlet step = 'step' in value ? bound(value.step, `${path}.step`, issues) : fallback.step;\n\tif (step !== null && !(step > 0)) {\n\t\tissues.push({ path: `${path}.step`, message: `${path}.step must be greater than 0 (or null for automatic).` });\n\t\tstep = fallback.step;\n\t}\n\tconst axis: ChartAxis = {\n\t\tshow: flag(value.show, fallback.show, `${path}.show`, issues),\n\t\ttitle: text(value.title, fallback.title, `${path}.title`, issues),\n\t\tmin,\n\t\tmax,\n\t\tstep,\n\t\tgridlines: flag(value.gridlines, fallback.gridlines, `${path}.gridlines`, issues),\n\t\tformat: 'format' in value ? normalizeFormat(value.format, `${path}.format`, issues) : { ...fallback.format }\n\t};\n\tconst kind = 'kind' in value ? (value.kind === null ? undefined : pick(value.kind, CHART_AXIS_KINDS, 'category', `${path}.kind`, issues)) : fallback.kind;\n\tif (kind && kind !== 'category') axis.kind = kind;\n\treturn axis;\n}\n\nfunction normalizeLegend(value: unknown, issues: ChartIssue[]): ChartLegend {\n\tconst fallback: ChartLegend = { show: true, position: 'bottom' };\n\tif (value === undefined) return fallback;\n\tif (!isRecord(value)) {\n\t\tissues.push({ path: 'legend', message: 'legend must be an object.' });\n\t\treturn fallback;\n\t}\n\treturn {\n\t\tshow: flag(value.show, fallback.show, 'legend.show', issues),\n\t\tposition: pick(value.position, CHART_LEGEND_POSITIONS, fallback.position, 'legend.position', issues)\n\t};\n}\n\n/** Values as numbers or nulls; bad entries become null and are reported. */\nexport function normalizeChartValues(value: unknown, length: number | undefined, path: string, issues: ChartIssue[] = []): ChartValue[] {\n\tif (value === undefined) return length ? Array(length).fill(null) : [];\n\tif (!Array.isArray(value)) {\n\t\tissues.push({ path, message: `${path} must be an array of numbers.` });\n\t\treturn length ? Array(length).fill(null) : [];\n\t}\n\tconst out = value.map((v, i) => {\n\t\tconst parsed = parseChartValue(v);\n\t\tif (parsed === undefined) {\n\t\t\tissues.push({ path: `${path}[${i}]`, message: `${path}[${i}] is not a number: ${JSON.stringify(v)}.` });\n\t\t\treturn null;\n\t\t}\n\t\treturn parsed;\n\t});\n\tif (length === undefined) return out;\n\tif (out.length > length) return out.slice(0, length);\n\twhile (out.length < length) out.push(null);\n\treturn out;\n}\n\nfunction normalizeSeries(value: unknown, length: number, used: Set<string>, index: number, issues: ChartIssue[]): ChartSeries | null {\n\tconst path = `series[${index}]`;\n\tif (!isRecord(value)) {\n\t\tissues.push({ path, message: `${path} must be an object with a name and values.` });\n\t\treturn null;\n\t}\n\tlet id = typeof value.id === 'string' && value.id.trim() ? value.id.trim() : '';\n\tif (!id || used.has(id)) {\n\t\tif (id) issues.push({ path: `${path}.id`, message: `Series id \"${id}\" is used twice; the second was renamed.` });\n\t\tfor (let n = index + 1; ; n++) {\n\t\t\tid = `series-${n}`;\n\t\t\tif (!used.has(id)) break;\n\t\t}\n\t}\n\tused.add(id);\n\tconst series: ChartSeries = {\n\t\tid,\n\t\tname: text(value.name, `Series ${index + 1}`, `${path}.name`, issues),\n\t\tvalues: normalizeChartValues(value.values, length, `${path}.values`, issues)\n\t};\n\tif (typeof value.color === 'string' && value.color.trim()) series.color = value.color.trim();\n\tif (value.type !== undefined && value.type !== null) series.type = pick(value.type, CHART_SERIES_TYPES, 'bar', `${path}.type`, issues);\n\tif (value.hidden === true) series.hidden = true;\n\tif (value.axis !== undefined && value.axis !== null && pick(value.axis, ['primary', 'secondary'] as const, 'primary', `${path}.axis`, issues) === 'secondary') series.axis = 'secondary';\n\tfor (const key of ['x', 'sizes'] as const) {\n\t\tif (value[key] === undefined || value[key] === null) continue;\n\t\tconst list = normalizeChartValues(value[key], length, `${path}.${key}`, issues);\n\t\tif (list.some((v) => v !== null)) series[key] = list;\n\t}\n\tif (key(value, 'sizes') && series.sizes?.some((v) => v !== null && v < 0)) {\n\t\tissues.push({ path: `${path}.sizes`, message: `${path}.sizes must not be negative.` });\n\t\tseries.sizes = series.sizes.map((v) => (v !== null && v < 0 ? null : v));\n\t}\n\tif (value.pointColors !== undefined && value.pointColors !== null) {\n\t\tif (!Array.isArray(value.pointColors)) issues.push({ path: `${path}.pointColors`, message: `${path}.pointColors must be an array of colours or nulls.` });\n\t\telse {\n\t\t\tconst colors = Array.from({ length }, (_, i) => {\n\t\t\t\tconst c = (value.pointColors as unknown[])[i];\n\t\t\t\treturn typeof c === 'string' && c.trim() ? c.trim() : null;\n\t\t\t});\n\t\t\tif (colors.some((c) => c !== null)) series.pointColors = colors;\n\t\t}\n\t}\n\tif (value.marker !== undefined && value.marker !== null) series.marker = pick(value.marker, CHART_MARKERS, 'circle', `${path}.marker`, issues);\n\tif (value.curve !== undefined && value.curve !== null) series.curve = pick(value.curve, CHART_CURVES, 'linear', `${path}.curve`, issues);\n\tif (value.dataLabels !== undefined && value.dataLabels !== null) series.dataLabels = flag(value.dataLabels, false, `${path}.dataLabels`, issues);\n\tif (value.format !== undefined && value.format !== null) {\n\t\tconst format = normalizeFormat(value.format, `${path}.format`, issues);\n\t\tif (Object.keys(format).length) series.format = format;\n\t}\n\tif (value.trendlines !== undefined && value.trendlines !== null) {\n\t\tif (!Array.isArray(value.trendlines)) issues.push({ path: `${path}.trendlines`, message: `${path}.trendlines must be an array.` });\n\t\telse {\n\t\t\tconst lines = value.trendlines.map((t, i) => normalizeChartTrendline(t, `${path}.trendlines[${i}]`, issues)).filter((t): t is ChartTrendline => t !== null);\n\t\t\tif (lines.length) series.trendlines = lines;\n\t\t}\n\t}\n\treturn series;\n}\n\nconst key = (value: Record<string, unknown>, name: string) => value[name] !== undefined && value[name] !== null;\n\n/** Checks one trendline; `null` (reported) when it is unusable. */\nexport function normalizeChartTrendline(value: unknown, path: string, issues: ChartIssue[] = []): ChartTrendline | null {\n\tif (!isRecord(value)) {\n\t\tissues.push({ path, message: `${path} must be an object with a type.` });\n\t\treturn null;\n\t}\n\tconst type = pick(value.type, CHART_TRENDLINE_TYPES, 'linear', `${path}.type`, issues);\n\tconst line: ChartTrendline = { type };\n\tconst whole = (name: 'order' | 'period', min: number, max: number) => {\n\t\tif (!key(value, name)) return;\n\t\tconst n = Number(value[name]);\n\t\tif (Number.isInteger(n) && n >= min && n <= max) line[name] = n;\n\t\telse issues.push({ path: `${path}.${name}`, message: `${path}.${name} must be a whole number from ${min} to ${max}.` });\n\t};\n\tif (type === 'polynomial') whole('order', 2, 6);\n\tif (type === 'moving-average') whole('period', 2, 1000);\n\tfor (const name of ['forward', 'backward'] as const) {\n\t\tif (!key(value, name)) continue;\n\t\tconst n = Number(value[name]);\n\t\tif (Number.isFinite(n) && n >= 0) {\n\t\t\tif (n > 0) line[name] = n;\n\t\t} else issues.push({ path: `${path}.${name}`, message: `${path}.${name} must be a number of x units, 0 or more.` });\n\t}\n\tif (key(value, 'intercept')) {\n\t\tconst n = Number(value.intercept);\n\t\tif (!Number.isFinite(n)) issues.push({ path: `${path}.intercept`, message: `${path}.intercept must be a number or null.` });\n\t\telse if (type === 'linear' || type === 'polynomial' || type === 'exponential') {\n\t\t\tif (type === 'exponential' && n <= 0) issues.push({ path: `${path}.intercept`, message: `${path}.intercept must be greater than 0 for an exponential trendline.` });\n\t\t\telse line.intercept = n;\n\t\t} else issues.push({ path: `${path}.intercept`, message: `A ${type} trendline cannot have a fixed intercept; use linear, polynomial or exponential.` });\n\t}\n\tfor (const name of ['showEquation', 'showR2'] as const) if (key(value, name) && flag(value[name], false, `${path}.${name}`, issues)) line[name] = true;\n\tfor (const name of ['name', 'color'] as const) {\n\t\tif (!key(value, name)) continue;\n\t\tconst t = text(value[name], '', `${path}.${name}`, issues).trim();\n\t\tif (t) line[name] = t;\n\t}\n\treturn line;\n}\n\n/**\n * Turns any input (a partial spec, saved JSON, an agent's arguments) into a complete,\n * valid `ChartSpec`. Every value is checked; problems are reported as sentences in\n * `issues` and replaced by defaults, so the result is always drawable. Series values are\n * padded or trimmed to the number of categories.\n */\nexport function normalizeChartSpec(input: unknown): { spec: ChartSpec; issues: ChartIssue[] } {\n\tconst issues: ChartIssue[] = [];\n\tconst value = isRecord(input) ? input : {};\n\tif (input !== undefined && !isRecord(input)) issues.push({ path: '', message: 'A chart must be an object.' });\n\tconst type = pick(value.type, CHART_TYPES, 'bar', 'type', issues);\n\tconst rawSeries = Array.isArray(value.series) ? value.series : [];\n\tif (value.series !== undefined && !Array.isArray(value.series)) issues.push({ path: 'series', message: 'series must be an array.' });\n\n\tlet categories: string[];\n\tif (Array.isArray(value.categories)) categories = value.categories.map((c, i) => text(c, '', `categories[${i}]`, issues));\n\telse {\n\t\tif (value.categories !== undefined) issues.push({ path: 'categories', message: 'categories must be an array of labels.' });\n\t\tconst longest = rawSeries.reduce<number>((n, s) => Math.max(n, isRecord(s) && Array.isArray(s.values) ? s.values.length : 0), 0);\n\t\tcategories = Array.from({ length: longest }, (_, i) => `${i + 1}`);\n\t}\n\n\tconst used = new Set<string>();\n\tconst series = rawSeries.map((s, i) => normalizeSeries(s, categories.length, used, i, issues)).filter((s): s is ChartSeries => s !== null);\n\n\tlet palette = [...DEFAULT_CHART_PALETTE];\n\tif (Array.isArray(value.palette)) {\n\t\tconst colors = value.palette.filter((c): c is string => typeof c === 'string' && c.trim() !== '');\n\t\tif (colors.length) palette = colors;\n\t\telse issues.push({ path: 'palette', message: 'palette must list at least one colour.' });\n\t} else if (value.palette !== undefined) issues.push({ path: 'palette', message: 'palette must be an array of colours.' });\n\n\tconst fraction = (name: string, fallback: number, min: number, max: number) => {\n\t\tif (value[name] === undefined) return fallback;\n\t\tconst r = Number(value[name]);\n\t\tif (value[name] !== null && Number.isFinite(r) && r >= min && r <= max) return r;\n\t\tissues.push({ path: name, message: `${name} must be a number from ${min} to ${max}.` });\n\t\treturn fallback;\n\t};\n\tconst innerRadius = fraction('innerRadius', 0.55, 0, 0.9);\n\tconst gapWidth = fraction('gapWidth', 0.28, 0, 0.9);\n\tconst overlap = fraction('overlap', -0.12, -1, 1);\n\tlet bins: number | null = null;\n\tif (value.bins !== undefined && value.bins !== null) {\n\t\tconst b = Number(value.bins);\n\t\tif (Number.isInteger(b) && b >= 1 && b <= 500) bins = b;\n\t\telse issues.push({ path: 'bins', message: 'bins must be a whole number from 1 to 500, or null for automatic.' });\n\t}\n\n\tconst spec: ChartSpec = {\n\t\tversion: 1,\n\t\ttype,\n\t\ttitle: text(value.title, '', 'title', issues),\n\t\tsubtitle: text(value.subtitle, '', 'subtitle', issues),\n\t\tcategories,\n\t\tseries,\n\t\txAxis: normalizeChartAxis(value.xAxis, defaultChartAxis(false), 'xAxis', issues),\n\t\tyAxis: normalizeChartAxis(value.yAxis, defaultChartAxis(true), 'yAxis', issues),\n\t\tyAxis2: normalizeChartAxis(value.yAxis2, defaultChartAxis(false), 'yAxis2', issues),\n\t\tlegend: normalizeLegend(value.legend, issues),\n\t\tstacking: pick(value.stacking, CHART_STACKINGS, 'none', 'stacking', issues),\n\t\torientation: pick(value.orientation, CHART_ORIENTATIONS, 'vertical', 'orientation', issues),\n\t\tcurve: pick(value.curve, CHART_CURVES, 'linear', 'curve', issues),\n\t\tpoints: flag(value.points, true, 'points', issues),\n\t\tdataLabels: flag(value.dataLabels, false, 'dataLabels', issues),\n\t\tlabelPosition: pick(value.labelPosition, CHART_LABEL_POSITIONS, 'auto', 'labelPosition', issues),\n\t\tpalette,\n\t\tinnerRadius,\n\t\tgapWidth,\n\t\toverlap,\n\t\tbins,\n\t\tshowTotal: flag(value.showTotal, true, 'showTotal', issues)\n\t};\n\treturn { spec, issues };\n}\n\n/** A deep copy (specs are plain JSON). */\nexport function cloneChartSpec(spec: ChartSpec): ChartSpec {\n\treturn {\n\t\t...spec,\n\t\tcategories: [...spec.categories],\n\t\tseries: spec.series.map(cloneChartSeries),\n\t\txAxis: { ...spec.xAxis, format: { ...spec.xAxis.format } },\n\t\tyAxis: { ...spec.yAxis, format: { ...spec.yAxis.format } },\n\t\tyAxis2: { ...spec.yAxis2, format: { ...spec.yAxis2.format } },\n\t\tlegend: { ...spec.legend },\n\t\tpalette: [...spec.palette]\n\t};\n}\n\nexport function cloneChartSeries(series: ChartSeries): ChartSeries {\n\tconst copy: ChartSeries = { ...series, values: [...series.values] };\n\tif (series.x) copy.x = [...series.x];\n\tif (series.sizes) copy.sizes = [...series.sizes];\n\tif (series.pointColors) copy.pointColors = [...series.pointColors];\n\tif (series.format) copy.format = { ...series.format };\n\tif (series.trendlines) copy.trendlines = series.trendlines.map((t) => ({ ...t }));\n\treturn copy;\n}\n\nexport interface ChartTableOptions {\n\t/**\n\t * `columns` (default): the first column holds categories and each other column is a\n\t * series. `rows`: the first row holds categories and each other row is a series.\n\t */\n\tseriesIn?: 'columns' | 'rows';\n\t/**\n\t * The first row (or column for `rows`) holds series names (default `true`). `auto`: only\n\t * when one of its value cells is not a number.\n\t */\n\theader?: boolean | 'auto';\n\t/**\n\t * Read the first column as numeric x values (scatter and bubble charts): every series gets\n\t * them as `x`, and the categories are their labels.\n\t */\n\txFromFirstColumn?: boolean;\n}\n\nexport interface ChartTableData {\n\tcategories: string[];\n\tseries: ChartSeries[];\n\tissues: ChartIssue[];\n}\n\n/**\n * Reads a grid of cells (from CSV, a spreadsheet range or the clipboard) as categories and\n * series. Cells that are not numbers become gaps and are reported.\n */\nexport function chartDataFromTable(rows: ReadonlyArray<ReadonlyArray<unknown>>, options: ChartTableOptions = {}): ChartTableData {\n\tlet grid = rows.map((r) => [...r]).filter((r) => r.some((c) => c !== null && c !== undefined && String(c).trim() !== ''));\n\tif (options.seriesIn === 'rows') {\n\t\tconst width = Math.max(0, ...grid.map((r) => r.length));\n\t\tgrid = Array.from({ length: width }, (_, c) => grid.map((r) => r[c]));\n\t}\n\tconst issues: ChartIssue[] = [];\n\tif (!grid.length) return { categories: [], series: [], issues };\n\tconst width = Math.max(...grid.map((r) => r.length));\n\tconst header = options.header === 'auto' ? grid[0].slice(1).some((c) => c !== null && c !== undefined && String(c).trim() !== '' && parseChartValue(c) === undefined) : (options.header ?? true);\n\tconst names = header ? grid[0] : [];\n\tconst body = header ? grid.slice(1) : grid;\n\tconst categories = body.map((r, i) => {\n\t\tconst c = r[0];\n\t\treturn c === null || c === undefined || String(c).trim() === '' ? `${i + 1}` : String(c).trim();\n\t});\n\tconst series: ChartSeries[] = [];\n\tfor (let col = 1; col < width; col++) {\n\t\tconst name = names[col] !== undefined && String(names[col]).trim() ? String(names[col]).trim() : `Series ${col}`;\n\t\tconst values = body.map((r, i) => {\n\t\t\tconst parsed = parseChartValue(r[col] ?? null);\n\t\t\tif (parsed === undefined) {\n\t\t\t\tissues.push({ path: `${name}[${i}]`, message: `\"${String(r[col])}\" in ${name} (${categories[i]}) is not a number; it was left blank.` });\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\treturn parsed;\n\t\t});\n\t\tseries.push({ id: `series-${col}`, name, values });\n\t}\n\tif (options.xFromFirstColumn) {\n\t\tconst x = body.map((r, i) => {\n\t\t\tconst parsed = parseChartValue(r[0] ?? null);\n\t\t\tif (parsed === undefined) issues.push({ path: `x[${i}]`, message: `\"${String(r[0])}\" in the first column is not a number; that point has no x.` });\n\t\t\treturn parsed ?? null;\n\t\t});\n\t\tfor (const s of series) s.x = [...x];\n\t}\n\treturn { categories, series, issues };\n}\n\n/** The chart's data as a grid: a header row of series names, then one row per category. */\nexport function chartDataToTable(spec: Pick<ChartSpec, 'categories' | 'series'>, options: { categoryHeader?: string } = {}): (string | number | null)[][] {\n\treturn [\n\t\t[options.categoryHeader ?? '', ...spec.series.map((s) => s.name)],\n\t\t...spec.categories.map((category, i) => [category, ...spec.series.map((s) => s.values[i] ?? null)])\n\t];\n}\n\n/** A colour input value (`#rrggbb`) for a CSS colour: `#abc` expands, non-hex colours show as black. */\nexport function chartColorInputValue(color: string): string {\n\tif (/^#[0-9a-f]{6}$/i.test(color)) return color.toLowerCase();\n\tif (/^#[0-9a-f]{3}$/i.test(color)) return `#${[...color.slice(1)].map((c) => c + c).join('')}`.toLowerCase();\n\treturn '#000000';\n}\n","// The xychart-beta, pie and radar-beta grammars follow Ursula's statement adapters (src/xychart, src/pie,\n// src/radar), and the metadata comment follows its `%% @ursula` line (src/source/metadata.ts).\nimport { chartFamily, defaultChartAxis, normalizeChartSpec } from './spec.js';\nimport type { ChartAxis, ChartIssue, ChartSeries, ChartSpec, ChartValue } from './types.js';\n\n/** The comment line that carries what Mermaid cannot express. Every Mermaid tool ignores `%%` comments. */\nexport const CHART_MERMAID_META_PREFIX = '%% @pixerate ';\n\nexport type ChartMermaidFamily = 'xychart' | 'pie' | 'radar';\n\nexport interface ChartMermaidOptions {\n\t/** Write the `%% @pixerate {…}` line so the chart round-trips exactly (default `true`). */\n\tmeta?: boolean;\n\t/** Indentation of statements (default four spaces). */\n\tindent?: string;\n}\n\nconst quote = (text: string) => `\"${text.replace(/\"/g, '#quot;')}\"`;\nconst unquote = (text: string) => {\n\tconst t = text.trim();\n\treturn (t.length >= 2 && t.startsWith('\"') && t.endsWith('\"') ? t.slice(1, -1) : t).replace(/#quot;/g, '\"');\n};\nconst word = (text: string) => (/^[A-Za-z0-9_]+$/.test(text) ? text : quote(text));\nconst num = (value: number) => (Number.isInteger(value) ? String(value) : String(Number(value.toPrecision(10))));\n\n/** Splits on `sep` outside quotes and brackets. */\nfunction splitOutside(text: string, sep: string): string[] {\n\tconst out: string[] = [];\n\tlet cur = '';\n\tlet quoted = false;\n\tlet depth = 0;\n\tfor (const ch of text) {\n\t\tif (ch === '\"') quoted = !quoted;\n\t\telse if (!quoted && (ch === '[' || ch === '{')) depth++;\n\t\telse if (!quoted && (ch === ']' || ch === '}')) depth--;\n\t\tif (ch === sep && !quoted && depth === 0) {\n\t\t\tout.push(cur);\n\t\t\tcur = '';\n\t\t} else cur += ch;\n\t}\n\tout.push(cur);\n\treturn out;\n}\n\n/** The Mermaid family a chart is written as. */\nexport function chartMermaidFamily(spec: Pick<ChartSpec, 'type' | 'series'>): ChartMermaidFamily {\n\tconst family = chartFamily(spec);\n\tif (family === 'pie' || family === 'funnel' || family === 'treemap') return 'pie';\n\tif (family === 'radar') return 'radar';\n\treturn 'xychart';\n}\n\nfunction slug(label: string, i: number, taken: Set<string>): string {\n\tlet base = label.normalize('NFKD').replace(/[^\\w-]+/g, '_').replace(/^_+|_+$/g, '') || `a${i + 1}`;\n\tif (!/^[A-Za-z_]/.test(base)) base = `a_${base}`;\n\tlet id = base;\n\tfor (let k = 2; taken.has(id); k++) id = `${base}_${k}`;\n\ttaken.add(id);\n\treturn id;\n}\n\n/** Fields that differ from what the Mermaid text alone would give, for the metadata line. */\nfunction metaFor(spec: ChartSpec, text: string): Record<string, unknown> | null {\n\tconst base = chartFromMermaid(text, { ignoreMeta: true }).spec;\n\tconst meta: Record<string, unknown> = {};\n\tconst same = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b);\n\tfor (const key of Object.keys(spec) as (keyof ChartSpec)[]) {\n\t\tif (key === 'series' || key === 'version') continue;\n\t\tif (!same(spec[key], base[key])) meta[key] = spec[key];\n\t}\n\tconst series = spec.series.map((s, i) => {\n\t\tconst b = base.series[i];\n\t\tconst out: Record<string, unknown> = {};\n\t\tfor (const [k, v] of Object.entries(s)) {\n\t\t\tif (k === 'values') {\n\t\t\t\t// Gaps are written as 0 in Mermaid; record where they are, and exact values that rounded.\n\t\t\t\tif (b && !same(v, b.values)) out.values = v;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!b || !same(v, (b as unknown as Record<string, unknown>)[k])) out[k] = v;\n\t\t}\n\t\treturn out;\n\t});\n\tif (series.some((s) => Object.keys(s).length) || spec.series.length !== base.series.length) meta.series = series;\n\treturn Object.keys(meta).length ? meta : null;\n}\n\n/**\n * Writes a chart as Mermaid: `xychart-beta` for bar, line, area and scatter charts (areas and\n * scatters drawn as lines), `pie` for pies, donuts, funnels and treemaps, `radar-beta` for\n * radars. Anything Mermaid cannot hold (stacking, colours, formats, gaps, trendlines, other\n * chart types) goes into one `%% @pixerate {…}` comment so `chartFromMermaid` gets the chart\n * back exactly, while any Mermaid renderer still draws the basic chart.\n */\nexport function chartToMermaid(spec: ChartSpec, options: ChartMermaidOptions = {}): string {\n\tconst ind = options.indent ?? ' ';\n\tconst family = chartMermaidFamily(spec);\n\tconst visible = spec.series.filter((s) => !s.hidden);\n\tconst lines: string[] = [];\n\tconst v = (x: ChartValue) => (x === null || !Number.isFinite(x) ? '0' : num(x));\n\tif (family === 'pie') {\n\t\tconst s = visible[0] ?? spec.series[0];\n\t\tlines.push(`pie${spec.dataLabels ? ' showData' : ''}`);\n\t\tif (spec.title) lines.push(`${ind}title ${spec.title}`);\n\t\tspec.categories.forEach((c, i) => {\n\t\t\tconst value = s?.values[i];\n\t\t\tif (value !== null && value !== undefined && Number.isFinite(value) && value > 0) lines.push(`${ind}${quote(c)} : ${num(value)}`);\n\t\t});\n\t} else if (family === 'radar') {\n\t\tlines.push('radar-beta');\n\t\tif (spec.title) lines.push(`${ind}title ${spec.title}`);\n\t\tconst taken = new Set<string>();\n\t\tconst ids = spec.categories.map((c, i) => slug(c, i, taken));\n\t\tlines.push(`${ind}axis ${ids.map((id, i) => (id === spec.categories[i] ? id : `${id}[\"${spec.categories[i].replace(/\"/g, \"'\")}\"]`)).join(', ')}`);\n\t\tconst seriesTaken = new Set<string>(ids);\n\t\tfor (const [i, s] of spec.series.entries()) {\n\t\t\tconst id = slug(s.id, i, seriesTaken);\n\t\t\tlines.push(`${ind}curve ${id}[\"${s.name.replace(/\"/g, \"'\")}\"]{${s.values.map(v).join(', ')}}`);\n\t\t}\n\t\tif (spec.yAxis.max !== null) lines.push(`${ind}max ${num(spec.yAxis.max)}`);\n\t\tif (spec.yAxis.min !== null) lines.push(`${ind}min ${num(spec.yAxis.min)}`);\n\t\tif (!spec.legend.show) lines.push(`${ind}showLegend false`);\n\t} else {\n\t\tlines.push(`xychart-beta${spec.orientation === 'horizontal' ? ' horizontal' : ''}`);\n\t\tif (spec.title) lines.push(`${ind}title ${quote(spec.title)}`);\n\t\tconst xTitle = spec.xAxis.title ? ` ${quote(spec.xAxis.title)}` : '';\n\t\tlines.push(`${ind}x-axis${xTitle} [${spec.categories.map(word).join(', ')}]`);\n\t\tconst y = spec.yAxis;\n\t\tif (y.title || y.min !== null || y.max !== null) {\n\t\t\tlet out = `${ind}y-axis`;\n\t\t\tif (y.title) out += ` ${quote(y.title)}`;\n\t\t\tif (y.min !== null || y.max !== null) {\n\t\t\t\t// Mermaid wants both ends; the missing one comes from the data.\n\t\t\t\tconst all = spec.series.flatMap((s) => s.values).filter((x): x is number => x !== null && Number.isFinite(x));\n\t\t\t\tconst lo = y.min ?? Math.min(0, ...all);\n\t\t\t\tconst hi = y.max ?? Math.max(lo + 1, ...all);\n\t\t\t\tout += ` ${num(lo)} --> ${num(hi)}`;\n\t\t\t}\n\t\t\tlines.push(out);\n\t\t}\n\t\tfor (const s of spec.series) {\n\t\t\tconst kind = (s.type ?? (spec.type === 'bar' ? 'bar' : 'line')) === 'bar' ? 'bar' : 'line';\n\t\t\tlines.push(`${ind}${kind} ${quote(s.name)} [${s.values.map(v).join(', ')}]`);\n\t\t}\n\t}\n\tlet text = `${lines.join('\\n')}\\n`;\n\tif (options.meta !== false) {\n\t\tconst meta = metaFor(spec, text);\n\t\tif (meta) {\n\t\t\tconst [header, ...rest] = lines;\n\t\t\ttext = `${[header, `${CHART_MERMAID_META_PREFIX}${JSON.stringify({ v: 1, ...meta })}`, ...rest].join('\\n')}\\n`;\n\t\t}\n\t}\n\treturn text;\n}\n\nexport interface ChartMermaidParseOptions {\n\t/** Accept a statement that is still being written (an unclosed `[` list): for streaming views. */\n\tpartial?: boolean;\n\t/** Ignore the `%% @pixerate` line (what the Mermaid text alone says). */\n\tignoreMeta?: boolean;\n}\n\nexport interface ChartMermaidResult {\n\tspec: ChartSpec;\n\t/** Problems, with 1-based `line` numbers. Lines that could not be read are listed and skipped. */\n\tissues: ChartIssue[];\n\tfamily: ChartMermaidFamily | null;\n\t/** The `%% @pixerate` line was found and applied. */\n\thasMeta: boolean;\n}\n\nconst RANGE = /^(-?\\d+(?:\\.\\d+)?)\\s*-->\\s*(-?\\d+(?:\\.\\d+)?)$/;\n\nfunction takeTitle(text: string): { title?: string; rest: string } {\n\tconst t = text.trim();\n\tif (t.startsWith('\"')) {\n\t\tconst end = t.indexOf('\"', 1);\n\t\tif (end > 0) return { title: unquote(t.slice(0, end + 1)), rest: t.slice(end + 1).trim() };\n\t}\n\tif (!t || t.startsWith('[') || RANGE.test(t) || /^-?\\d/.test(t)) return { rest: t };\n\tconst m = /^(\\S+)\\s*(.*)$/.exec(t)!;\n\treturn { title: m[1], rest: m[2].trim() };\n}\n\nfunction parseList(text: string, partial: boolean): string[] | undefined {\n\tconst t = text.trim();\n\tlet m = /^\\[(.*)\\]$/s.exec(t);\n\tif (!m && partial) m = /^\\[(.*)$/s.exec(t);\n\tif (!m) return undefined;\n\tif (!m[1].trim()) return [];\n\tconst items = splitOutside(m[1], ',').map((x) => x.trim());\n\t// A partial list may end mid-item: drop an empty or unfinished last entry.\n\tif (partial && !/\\]$/.test(t)) {\n\t\tconst last = items[items.length - 1];\n\t\tif (!last || (last.startsWith('\"') && !/\"$/.test(last.slice(1)))) items.pop();\n\t}\n\treturn items.map(unquote);\n}\n\n/**\n * Reads Mermaid `xychart-beta`, `pie` or `radar-beta` text as a chart. Never throws: lines it\n * cannot read are reported with their line numbers and skipped, and the result always draws.\n * A `%% @pixerate {…}` line written by `chartToMermaid` restores everything Mermaid cannot hold.\n */\nexport function chartFromMermaid(text: string, options: ChartMermaidParseOptions = {}): ChartMermaidResult {\n\tconst issues: ChartIssue[] = [];\n\tconst partial = !!options.partial;\n\tconst all = text.replace(/^/, '').split(/\\r?\\n/);\n\tlet start = 0;\n\tlet frontTitle: string | undefined;\n\t// Frontmatter (--- … ---) may carry a title.\n\tif (all[0]?.trim() === '---') {\n\t\tconst end = all.findIndex((l, i) => i > 0 && l.trim() === '---');\n\t\tif (end > 0) {\n\t\t\tfor (const l of all.slice(1, end)) {\n\t\t\t\tconst m = /^title:\\s*(.+)$/.exec(l.trim());\n\t\t\t\tif (m) frontTitle = unquote(m[1]);\n\t\t\t}\n\t\t\tstart = end + 1;\n\t\t}\n\t}\n\tlet family: ChartMermaidFamily | null = null;\n\tlet headerLine = -1;\n\tlet horizontal = false;\n\tlet showData = false;\n\tlet meta: Record<string, unknown> | null = null;\n\tfor (let i = start; i < all.length; i++) {\n\t\tconst t = all[i].trim();\n\t\tif (!t || t.startsWith('%%')) continue;\n\t\tlet m: RegExpExecArray | null;\n\t\tif ((m = /^xychart(?:-beta)?(?:\\s+(horizontal|vertical))?\\s*$/i.exec(t))) {\n\t\t\tfamily = 'xychart';\n\t\t\thorizontal = m[1]?.toLowerCase() === 'horizontal';\n\t\t} else if ((m = /^pie(\\s+showData)?(?:\\s+title\\s+(.*))?\\s*$/i.exec(t))) {\n\t\t\tfamily = 'pie';\n\t\t\tshowData = !!m[1];\n\t\t\tif (m[2]) frontTitle = m[2].trim();\n\t\t} else if (/^radar(?:-beta)?\\s*$/i.test(t)) family = 'radar';\n\t\telse issues.push({ path: 'header', line: i + 1, message: `Line ${i + 1}: expected a chart header (xychart-beta, pie or radar-beta), got \"${t.slice(0, 40)}\".` });\n\t\theaderLine = i;\n\t\tbreak;\n\t}\n\tlet title = frontTitle ?? '';\n\tconst categories: string[] = [];\n\tconst series: ChartSeries[] = [];\n\tconst xAxis: ChartAxis = defaultChartAxis(false);\n\tconst yAxis: ChartAxis = defaultChartAxis(true);\n\tconst radarAxes: { id: string; label: string }[] = [];\n\tconst pieSlices: { label: string; value: number }[] = [];\n\tlet legendShow = true;\n\tconst counts = { bar: 0, line: 0 };\n\tconst lineIssue = (i: number, message: string) => issues.push({ path: `line ${i + 1}`, line: i + 1, message: `Line ${i + 1}: ${message}` });\n\tfor (let i = headerLine + 1; family && i < all.length; i++) {\n\t\tconst raw = all[i];\n\t\tconst t = raw.trim();\n\t\tif (!t) continue;\n\t\tif (t.startsWith(CHART_MERMAID_META_PREFIX.trim())) {\n\t\t\tif (options.ignoreMeta) continue;\n\t\t\ttry {\n\t\t\t\tconst parsed = JSON.parse(t.slice(CHART_MERMAID_META_PREFIX.trim().length).trim());\n\t\t\t\tif (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) meta = parsed;\n\t\t\t\telse lineIssue(i, 'the @pixerate metadata is not a JSON object; it was ignored.');\n\t\t\t} catch {\n\t\t\t\tif (!partial) lineIssue(i, 'the @pixerate metadata is not valid JSON; it was ignored.');\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (t.startsWith('%%')) continue;\n\t\tconst isLast = i === all.length - 1 || all.slice(i + 1).every((l) => !l.trim());\n\t\tlet m: RegExpExecArray | null;\n\t\tif ((m = /^title\\s+(.+)$/i.exec(t))) {\n\t\t\ttitle = unquote(m[1]);\n\t\t\tcontinue;\n\t\t}\n\t\tif (family === 'xychart') {\n\t\t\tif ((m = /^x-axis\\b(.*)$/i.exec(t))) {\n\t\t\t\tconst { title: xt, rest } = takeTitle(m[1]);\n\t\t\t\tif (xt) xAxis.title = xt;\n\t\t\t\tconst list = parseList(rest, partial && isLast);\n\t\t\t\tif (list) categories.splice(0, categories.length, ...list);\n\t\t\t\telse if (RANGE.test(rest)) {\n\t\t\t\t\tconst r = RANGE.exec(rest)!;\n\t\t\t\t\txAxis.min = Number(r[1]);\n\t\t\t\t\txAxis.max = Number(r[2]);\n\t\t\t\t} else if (rest) lineIssue(i, `expected x-axis [a, b, …] or a range \"min --> max\", got \"${rest.slice(0, 40)}\".`);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif ((m = /^y-axis\\b(.*)$/i.exec(t))) {\n\t\t\t\tconst { title: yt, rest } = takeTitle(m[1]);\n\t\t\t\tif (yt) yAxis.title = yt;\n\t\t\t\tconst r = RANGE.exec(rest);\n\t\t\t\tif (r) {\n\t\t\t\t\tyAxis.min = Number(r[1]);\n\t\t\t\t\tyAxis.max = Number(r[2]);\n\t\t\t\t} else if (rest) lineIssue(i, `expected y-axis \"min --> max\", got \"${rest.slice(0, 40)}\".`);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif ((m = /^(bar|line)\\b(.*)$/i.exec(t))) {\n\t\t\t\tconst kind = m[1].toLowerCase() as 'bar' | 'line';\n\t\t\t\tconst { title: st, rest } = takeTitle(m[2]);\n\t\t\t\tconst list = parseList(rest, partial && isLast);\n\t\t\t\tif (!list) {\n\t\t\t\t\tif (!(partial && isLast)) lineIssue(i, `expected ${kind} [1, 2, 3], got \"${rest.slice(0, 40)}\".`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst values = list.map((x) => (x.trim() === '' ? null : Number(x)));\n\t\t\t\tif (values.some((x) => x !== null && !Number.isFinite(x))) {\n\t\t\t\t\tlineIssue(i, `${kind} values must be numbers.`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tcounts[kind]++;\n\t\t\t\tseries.push({ id: `series-${series.length + 1}`, name: st ?? `${kind === 'bar' ? 'Bar' : 'Line'} ${counts[kind]}`, values: values as ChartValue[], type: kind });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t} else if (family === 'pie') {\n\t\t\tif ((m = /^\"((?:[^\"\\\\]|\\\\.)*)\"\\s*:\\s*(-?\\d+(?:\\.\\d+)?)\\s*$/.exec(t))) {\n\t\t\t\tpieSlices.push({ label: m[1].replace(/#quot;/g, '\"'), value: Number(m[2]) });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (/^showData\\s*$/i.test(t)) {\n\t\t\t\tshowData = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t} else if (family === 'radar') {\n\t\t\tif ((m = /^axis\\s+(.+)$/.exec(t))) {\n\t\t\t\tfor (const part of splitOutside(m[1], ',')) {\n\t\t\t\t\tconst a = /^([A-Za-z_][\\w-]*)\\s*(?:\\[\\s*\"((?:[^\"\\\\]|\\\\.)*)\"\\s*\\])?$/.exec(part.trim());\n\t\t\t\t\tif (a) radarAxes.push({ id: a[1], label: a[2] ?? a[1] });\n\t\t\t\t\telse lineIssue(i, `could not read the axis \"${part.trim()}\".`);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif ((m = /^curve\\s+([A-Za-z_][\\w-]*)\\s*(?:\\[\\s*\"((?:[^\"\\\\]|\\\\.)*)\"\\s*\\])?\\s*\\{(.*)\\}?\\s*$/.exec(t))) {\n\t\t\t\tconst body = m[3].replace(/\\}\\s*$/, '');\n\t\t\t\tconst entries = body.trim() ? splitOutside(body, ',').map((x) => x.trim()).filter(Boolean) : [];\n\t\t\t\tconst ids = radarAxes.map((a) => a.id);\n\t\t\t\tconst values: ChartValue[] = ids.map(() => null);\n\t\t\t\tlet k = 0;\n\t\t\t\tfor (const e of entries) {\n\t\t\t\t\tconst keyed = /^([A-Za-z_][\\w-]*)\\s*:\\s*(-?\\d+(?:\\.\\d+)?)$/.exec(e);\n\t\t\t\t\tif (keyed) {\n\t\t\t\t\t\tconst at = ids.indexOf(keyed[1]);\n\t\t\t\t\t\tif (at >= 0) values[at] = Number(keyed[2]);\n\t\t\t\t\t\telse lineIssue(i, `curve ${m[1]} names an unknown axis \"${keyed[1]}\".`);\n\t\t\t\t\t} else if (/^-?\\d+(?:\\.\\d+)?$/.test(e)) {\n\t\t\t\t\t\tif (k < values.length) values[k] = Number(e);\n\t\t\t\t\t\telse values.push(Number(e));\n\t\t\t\t\t\tk++;\n\t\t\t\t\t} else lineIssue(i, `could not read \"${e}\" in curve ${m[1]}.`);\n\t\t\t\t}\n\t\t\t\tseries.push({ id: m[1], name: m[2] ?? m[1], values });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif ((m = /^(max|min|ticks|graticule|showLegend)\\s+(\\S+)$/.exec(t))) {\n\t\t\t\tif (m[1] === 'max') yAxis.max = Number(m[2]);\n\t\t\t\telse if (m[1] === 'min') yAxis.min = Number(m[2]);\n\t\t\t\telse if (m[1] === 'showLegend') legendShow = m[2] !== 'false';\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif (!(partial && isLast)) lineIssue(i, `could not read \"${t.slice(0, 60)}\"; it was skipped.`);\n\t}\n\n\tlet input: Record<string, unknown>;\n\tif (family === 'pie') {\n\t\tinput = {\n\t\t\ttype: 'pie',\n\t\t\ttitle,\n\t\t\tdataLabels: showData,\n\t\t\tcategories: pieSlices.map((s) => s.label),\n\t\t\tseries: [{ id: 'series-1', name: title || 'Values', values: pieSlices.map((s) => s.value) }]\n\t\t};\n\t} else if (family === 'radar') {\n\t\tinput = { type: 'radar', title, categories: radarAxes.map((a) => a.label), series, yAxis, legend: { show: legendShow, position: 'bottom' } };\n\t} else {\n\t\tconst allBar = series.length > 0 && series.every((s) => s.type === 'bar');\n\t\tconst allLine = series.length > 0 && series.every((s) => s.type === 'line');\n\t\tconst type = allLine ? 'line' : 'bar';\n\t\tconst longest = Math.max(0, ...series.map((s) => s.values.length));\n\t\tconst cats = categories.length ? categories : Array.from({ length: longest }, (_, i) => `${i + 1}`);\n\t\tinput = {\n\t\t\ttype,\n\t\t\ttitle,\n\t\t\torientation: horizontal ? 'horizontal' : 'vertical',\n\t\t\tcategories: cats,\n\t\t\txAxis,\n\t\t\tyAxis,\n\t\t\t// Series that match the chart type do not need their own type.\n\t\t\tseries: series.map((s) => (allBar || allLine || s.type === type ? { ...s, type: undefined } : s))\n\t\t};\n\t}\n\tif (meta) {\n\t\tconst { v: _v, series: metaSeries, ...rest } = meta as Record<string, unknown> & { v?: number; series?: Record<string, unknown>[] };\n\t\tObject.assign(input, rest);\n\t\tif (Array.isArray(metaSeries)) {\n\t\t\tconst base = (input.series as Record<string, unknown>[]) ?? [];\n\t\t\tinput.series = metaSeries.map((extra, i) => ({ ...(base[i] ?? {}), ...(extra ?? {}) }));\n\t\t}\n\t}\n\tconst { spec, issues: specIssues } = normalizeChartSpec(input);\n\tif (!family && !partial && !issues.length) issues.push({ path: 'header', message: 'The text is empty: expected a chart header (xychart-beta, pie or radar-beta).' });\n\treturn { spec, issues: [...issues, ...specIssues], family, hasMeta: !!meta };\n}\n\nexport interface FoundChartMermaid {\n\tsource: string;\n\tfamily: ChartMermaidFamily;\n\t/** 1-based line where the block starts in the input. */\n\tline: number;\n}\n\n/**\n * Finds Mermaid charts in Markdown or chat text: fenced ```mermaid blocks (and unlabelled\n * fences) whose header is xychart-beta, pie or radar-beta, else the whole text when it is one.\n */\nexport function findMermaidCharts(text: string): FoundChartMermaid[] {\n\tconst lines = text.replace(/^/, '').split(/\\r?\\n/);\n\tconst found: FoundChartMermaid[] = [];\n\tconst familyOf = (body: string): ChartMermaidFamily | null => {\n\t\tconst header = body.split(/\\r?\\n/).find((l) => l.trim() && !l.trim().startsWith('%%') && l.trim() !== '---');\n\t\tif (!header) return null;\n\t\tif (/^\\s*xychart(-beta)?\\b/.test(header)) return 'xychart';\n\t\tif (/^\\s*pie\\b/.test(header)) return 'pie';\n\t\tif (/^\\s*radar(-beta)?\\b/.test(header)) return 'radar';\n\t\treturn null;\n\t};\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst open = /^ {0,3}(`{3,}|~{3,})\\s*([\\w-]*)/.exec(lines[i]);\n\t\tif (!open) continue;\n\t\tconst fence = open[1];\n\t\tconst info = open[2].toLowerCase();\n\t\tlet end = i + 1;\n\t\twhile (end < lines.length && !new RegExp(`^ {0,3}${fence[0]}{${fence.length},}\\\\s*$`).test(lines[end])) end++;\n\t\tconst body = lines.slice(i + 1, end).join('\\n');\n\t\tif (!info || info === 'mermaid') {\n\t\t\tconst family = familyOf(body);\n\t\t\tif (family) found.push({ source: `${body.trimEnd()}\\n`, family, line: i + 2 });\n\t\t}\n\t\ti = end;\n\t}\n\tif (!found.length) {\n\t\tconst family = familyOf(text);\n\t\tif (family) found.push({ source: `${text.trim()}\\n`, family, line: 1 });\n\t}\n\treturn found;\n}\n\n","// The label ladder follows `ruler_steps` in filmcraft-main/crates/ui-egui/src/panels/timeline.rs (the first\n// step on a ladder that keeps labels a minimum distance apart) (MIT OR Apache-2.0, © ArtCraft Team and\n// contributors), adapted to calendar units. See THIRD_PARTY_NOTICES.md.\n\nconst MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\n\n/**\n * Reads a category label as a UTC date: `2024`, `2024-03`, `2024-03-05`, ISO date-times,\n * `2024/03/05`, `Mar 2024` and `5 Mar 2024`. Returns milliseconds since the epoch, or `null`.\n */\nexport function parseChartDate(label: string): number | null {\n\tconst text = label.trim();\n\tlet m = /^(\\d{4})$/.exec(text);\n\tif (m) return Date.UTC(Number(m[1]), 0, 1);\n\tm = /^(\\d{4})[-/](\\d{1,2})$/.exec(text);\n\tif (m) return valid(Number(m[1]), Number(m[2]) - 1, 1);\n\tm = /^(\\d{4})[-/](\\d{1,2})[-/](\\d{1,2})(?:[T ](\\d{1,2}):(\\d{2})(?::(\\d{2}))?(?:\\.\\d+)?(Z|[+-]\\d{2}:?\\d{2})?)?$/.exec(text);\n\tif (m) {\n\t\tif (m[4] !== undefined) {\n\t\t\tconst t = Date.parse(text.replace(' ', 'T').replace(/\\//g, '-') + (m[7] ? '' : 'Z'));\n\t\t\treturn Number.isFinite(t) ? t : null;\n\t\t}\n\t\treturn valid(Number(m[1]), Number(m[2]) - 1, Number(m[3]));\n\t}\n\tm = /^(?:(\\d{1,2})\\s+)?([A-Za-z]{3})[a-z]*\\.?,?\\s+(\\d{4})$/.exec(text);\n\tif (m) {\n\t\tconst month = MONTHS.findIndex((x) => x.toLowerCase() === m![2].toLowerCase());\n\t\tif (month >= 0) return valid(Number(m[3]), month, m[1] ? Number(m[1]) : 1);\n\t}\n\treturn null;\n}\n\nfunction valid(y: number, mo: number, d: number): number | null {\n\tconst t = Date.UTC(y, mo, d);\n\tconst date = new Date(t);\n\treturn date.getUTCFullYear() === y && date.getUTCMonth() === mo && date.getUTCDate() === d ? t : null;\n}\n\n/** Calendar steps from fine to coarse. */\nexport const CHART_DATE_STEPS = [\n\t{ unit: 'day', count: 1 },\n\t{ unit: 'day', count: 2 },\n\t{ unit: 'week', count: 1 },\n\t{ unit: 'week', count: 2 },\n\t{ unit: 'month', count: 1 },\n\t{ unit: 'month', count: 3 },\n\t{ unit: 'month', count: 6 },\n\t{ unit: 'year', count: 1 },\n\t{ unit: 'year', count: 2 },\n\t{ unit: 'year', count: 5 },\n\t{ unit: 'year', count: 10 },\n\t{ unit: 'year', count: 25 },\n\t{ unit: 'year', count: 50 },\n\t{ unit: 'year', count: 100 }\n] as const;\n\nexport type ChartDateStep = (typeof CHART_DATE_STEPS)[number];\n\n/** Which bucket of `step` a time falls in (equal buckets share a label). */\nfunction bucket(t: number, step: ChartDateStep): number {\n\tconst d = new Date(t);\n\tconst y = d.getUTCFullYear();\n\tswitch (step.unit) {\n\t\tcase 'day':\n\t\t\treturn Math.floor(Math.floor(t / 86400000) / step.count);\n\t\tcase 'week':\n\t\t\t// Weeks start on Monday (1970-01-05 was one).\n\t\t\treturn Math.floor(Math.floor((t / 86400000 - 4) / 7) / step.count);\n\t\tcase 'month':\n\t\t\treturn Math.floor((y * 12 + d.getUTCMonth()) / step.count);\n\t\tcase 'year':\n\t\t\treturn Math.floor(y / step.count);\n\t}\n}\n\n/** A label for a date at `step`: years as `2024`, months as `Mar 2024` (`Mar` within a year), days as `5 Mar`. */\nexport function formatChartDate(t: number, step: ChartDateStep, previous: number | null = null): string {\n\tconst d = new Date(t);\n\tconst y = d.getUTCFullYear();\n\tconst mo = MONTHS[d.getUTCMonth()];\n\tconst sameYear = previous !== null && new Date(previous).getUTCFullYear() === y;\n\tif (step.unit === 'year') return String(y);\n\tif (step.unit === 'month') return sameYear ? mo : `${mo} ${y}`;\n\treturn sameYear ? `${d.getUTCDate()} ${mo}` : `${d.getUTCDate()} ${mo} ${y}`;\n}\n\nexport interface ChartDateLabels {\n\t/** The calendar step chosen. */\n\tstep: ChartDateStep;\n\t/** Category indexes that get a label (the first category in each bucket). */\n\tindexes: number[];\n\t/** Label text by category index. */\n\tlabels: Map<number, string>;\n}\n\n/**\n * Picks the finest calendar step whose labels fit along a category axis. Categories are\n * evenly spaced (`band` pixels each), so a label goes on the first category of each bucket,\n * and the step is the first on the ladder whose neighbouring labels are at least their own\n * width plus `gap` apart. Returns `null` when fewer than half the categories read as dates.\n */\nexport function chartDateLabels(categories: readonly string[], band: number, measure: (text: string) => number, gap = 10): ChartDateLabels | null {\n\tconst times = categories.map(parseChartDate);\n\tif (times.filter((t) => t !== null).length < Math.max(1, categories.length / 2)) return null;\n\t// Steps much finer than the data's own spacing would only relabel the same points.\n\tconst present = times.filter((t): t is number => t !== null).sort((x, y) => x - y);\n\tconst gaps = present.slice(1).map((t, i) => t - present[i]).filter((g) => g > 0).sort((x, y) => x - y);\n\tconst spacing = gaps.length ? gaps[Math.floor(gaps.length / 2)] : 0;\n\tconst DAY = 86400000;\n\tconst length = (step: ChartDateStep) => step.count * (step.unit === 'day' ? DAY : step.unit === 'week' ? 7 * DAY : step.unit === 'month' ? 30 * DAY : 365 * DAY);\n\tfor (const step of CHART_DATE_STEPS) {\n\t\tif (length(step) < spacing * 0.5 && step !== CHART_DATE_STEPS[CHART_DATE_STEPS.length - 1]) continue;\n\t\tconst indexes: number[] = [];\n\t\tconst labels = new Map<number, string>();\n\t\tlet last: number | null = null;\n\t\tlet lastTime: number | null = null;\n\t\ttimes.forEach((t, i) => {\n\t\t\tif (t === null) return;\n\t\t\tconst b = bucket(t, step);\n\t\t\tif (b === last) return;\n\t\t\tlast = b;\n\t\t\tindexes.push(i);\n\t\t\tlabels.set(i, formatChartDate(t, step, lastTime));\n\t\t\tlastTime = t;\n\t\t});\n\t\tlet fits = true;\n\t\tfor (let k = 1; k < indexes.length && fits; k++) {\n\t\t\tconst a = labels.get(indexes[k - 1])!;\n\t\t\tconst b = labels.get(indexes[k])!;\n\t\t\tif ((indexes[k] - indexes[k - 1]) * band < (measure(a) + measure(b)) / 2 + gap) fits = false;\n\t\t}\n\t\tif (fits || step === CHART_DATE_STEPS[CHART_DATE_STEPS.length - 1]) return { step, indexes, labels };\n\t}\n\treturn null;\n}\n","import { NumberFormat } from '../spreadsheet/numfmt/index.js';\n\n/**\n * A compiled spreadsheet number format code for chart labels, e.g. `#,##0.00`, `0.0%`,\n * `#,##0,,\"M\"`, `$#,##0;[Red]($#,##0);\"–\"` or `yyyy-mm-dd`. Backed by the spreadsheet's\n * format engine (`NumberFormat`), so charts and sheets write numbers the same way; colours\n * such as `[Red]` are ignored on charts.\n */\nexport interface ChartFormatCode {\n\tformat(value: number): string;\n\t/** The code is a date or time format (values are serial day numbers from 1899-12-30). */\n\tisDate: boolean;\n\t/** The code shows a percentage (values are multiplied by 100). */\n\tisPercent: boolean;\n}\n\n/** Compiles a spreadsheet format code (cached). */\nexport function compileChartFormatCode(code: string): ChartFormatCode {\n\tconst fmt = NumberFormat.parseCached(code);\n\treturn {\n\t\tisDate: fmt.isDate(),\n\t\tisPercent: fmt.isPercent(),\n\t\tformat: (value: number) => (Number.isFinite(value) ? fmt.formatNumber(value).text : '')\n\t};\n}\n\n/** Formats `value` with a spreadsheet format code. */\nexport function formatChartValueWithCode(value: number, code: string): string {\n\treturn compileChartFormatCode(code).format(value);\n}\n\n/** A spreadsheet serial day number (1 = 1900-01-01, with the 1900 leap-year quirk) as a UTC date. */\nexport function chartSerialToDate(serial: number): Date {\n\t// Day 60 is the fictional 1900-02-29; serials from 61 count from 1899-12-30, earlier ones from 1899-12-31.\n\tconst origin = serial < 61 ? Date.UTC(1899, 11, 31) : Date.UTC(1899, 11, 30);\n\treturn new Date(origin + Math.round(serial * 86400000));\n}\n\n/** A UTC date as a spreadsheet serial day number. */\nexport function chartDateToSerial(date: Date): number {\n\tconst serial = (date.getTime() - Date.UTC(1899, 11, 30)) / 86400000;\n\treturn serial < 61 ? serial - 1 : serial;\n}\n","import { formatChartValueWithCode } from './numfmt.js';\nimport type { ChartNumberFormat } from './types.js';\n\n/** A \"nice\" step (1, 2, 2.5 or 5 × 10ⁿ) for about `count` intervals over `span`. */\nexport function niceStep(span: number, count: number): number {\n\tif (!(span > 0) || !(count > 0)) return 1;\n\tconst raw = span / count;\n\tconst power = Math.pow(10, Math.floor(Math.log10(raw)));\n\tconst fraction = raw / power;\n\tconst nice = fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 2.5 ? 2.5 : fraction <= 5 ? 5 : 10;\n\treturn nice * power;\n}\n\n/** Removes float noise such as 0.30000000000000004. */\nconst clean = (n: number) => (Math.abs(n) < 1e-12 ? 0 : Number(n.toPrecision(12)));\n\nexport interface NiceScale {\n\tmin: number;\n\tmax: number;\n\tstep: number;\n\tticks: number[];\n}\n\n/**\n * Extends `[min, max]` to round numbers and lists the ticks between them. Fixed ends\n * (`fixedMin` / `fixedMax`) are kept exactly; ticks then start at the first nice value\n * inside them.\n */\nexport function niceScale(min: number, max: number, count = 5, fixedMin: number | null = null, fixedMax: number | null = null): NiceScale {\n\t// Non-finite fixed ends fall back to the data range, and a non-finite data range to 0..1.\n\tif (fixedMin !== null && !Number.isFinite(fixedMin)) fixedMin = null;\n\tif (fixedMax !== null && !Number.isFinite(fixedMax)) fixedMax = null;\n\tlet lo = fixedMin ?? min;\n\tlet hi = fixedMax ?? max;\n\tif (!Number.isFinite(lo) || !Number.isFinite(hi)) {\n\t\tfixedMin = fixedMax = null;\n\t\tlo = 0;\n\t\thi = 1;\n\t}\n\tif (lo > hi) [lo, hi] = [hi, lo];\n\tif (lo === hi) {\n\t\tconst pad = lo === 0 ? 1 : Math.abs(lo) * 0.1;\n\t\t// A flat series at 0 grows upwards; anything else gets room on both free sides.\n\t\tif (fixedMin === null && lo !== 0) lo -= pad;\n\t\tif (fixedMax === null || lo === hi) hi += pad;\n\t}\n\t// Near ±Number.MAX_VALUE the span overflows to Infinity: halve it and double the step.\n\tconst span = hi - lo;\n\tconst step = Number.isFinite(span) ? niceStep(span, count) : niceStep(hi / 2 - lo / 2, count) * 2;\n\t// Rounding outwards can overflow too; keep the data end then.\n\tconst finiteOr = (n: number, fallback: number) => (Number.isFinite(n) ? n : fallback);\n\tconst niceMin = fixedMin ?? finiteOr(snap(Math.floor(lo / step + 1e-9) * step, step), lo);\n\tconst niceMax = fixedMax ?? finiteOr(snap(Math.ceil(hi / step - 1e-9) * step, step), hi);\n\t// Ticks are first + i·step (never a running sum, which stalls once step is below the\n\t// float spacing, e.g. near 1e16) and deduplicated where neighbours round to one float.\n\tconst firstIndex = Math.ceil(niceMin / step - 1e-9);\n\tconst lastIndex = Math.floor(niceMax / step + 1e-9);\n\tconst n = Math.min(999, Math.max(-1, lastIndex - firstIndex));\n\tconst ticks: number[] = [];\n\tfor (let i = 0; i <= n; i++) {\n\t\tconst t = snap((firstIndex + i) * step, step);\n\t\tif (Number.isFinite(t) && t !== ticks[ticks.length - 1]) ticks.push(t);\n\t}\n\treturn { min: niceMin, max: niceMax, step, ticks };\n}\n\n/** `clean`, unless that moves `n` by a noticeable part of `step` (large numbers such as 1e16 + 2). */\nfunction snap(n: number, step: number): number {\n\tconst c = clean(n);\n\treturn Math.abs(c - n) <= step * 1e-6 ? c : n;\n}\n\n/** Decimals needed to write ticks `step` apart exactly: 1 → 0, 2.5 → 1, 0.25 → 2 (at most 6). */\nexport function stepDecimals(step: number): number {\n\tif (!(step > 0) || !Number.isFinite(step)) return 0;\n\tfor (let d = 0; d < 6; d++) {\n\t\tconst scaled = step * Math.pow(10, d);\n\t\tif (Math.abs(scaled - Math.round(scaled)) < 1e-9) return d;\n\t}\n\treturn 6;\n}\n\n/** Decimals that show the leading digit of `value` (0.12 → 1, 0.012 → 2; at most 6). */\nfunction magnitudeDecimals(value: number): number {\n\tif (!(value > 0) || value >= 1) return 0;\n\treturn Math.min(6, Math.max(0, Math.ceil(-Math.log10(value) - 1e-9)));\n}\n\nconst COMPACT = [\n\t{ value: 1e12, suffix: 'T' },\n\t{ value: 1e9, suffix: 'B' },\n\t{ value: 1e6, suffix: 'M' },\n\t{ value: 1e3, suffix: 'K' }\n];\n\nfunction grouped(value: number, decimals: number): string {\n\tconst fixed = Math.abs(value).toFixed(decimals);\n\tconst [int, frac] = fixed.split('.');\n\tconst withCommas = int.replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',');\n\treturn `${value < 0 && Number(fixed) !== 0 ? '-' : ''}${withCommas}${frac ? `.${frac}` : ''}`;\n}\n\nconst CURRENCY_SYMBOLS: Record<string, string> = { USD: '$', EUR: '€', GBP: '£', JPY: '¥', CNY: '¥', INR: '₹', AUD: 'A$', CAD: 'C$' };\n\n/**\n * Writes a number for a chart label. Locale-independent (so labels and tests are the same\n * everywhere): `,` thousands, `.` decimals. `decimals` defaults to `fallbackDecimals`\n * (e.g. from the tick step), else up to 2 trimmed.\n */\nexport function formatChartNumber(value: number | null | undefined, format: ChartNumberFormat = {}, fallbackDecimals?: number): string {\n\tif (value === null || value === undefined || !Number.isFinite(value)) return '';\n\tif (format.code) return `${format.prefix ?? ''}${formatChartValueWithCode(value, format.code)}${format.suffix ?? ''}`;\n\tconst style = format.style ?? 'number';\n\tlet body: string;\n\tconst decimalsFor = (v: number) => {\n\t\tif (format.decimals !== undefined) return format.decimals;\n\t\tif (fallbackDecimals !== undefined) return fallbackDecimals;\n\t\treturn Number.isInteger(v) ? 0 : Math.abs(v) < 1 ? Math.min(4, Math.max(2, magnitudeDecimals(Math.abs(v)) + 1)) : 2;\n\t};\n\tconst trim = (s: string) => (format.decimals === undefined && fallbackDecimals === undefined && s.includes('.') ? s.replace(/\\.?0+$/, '') : s);\n\tif (style === 'percent') {\n\t\tconst v = value * 100;\n\t\tconst d = format.decimals ?? (fallbackDecimals !== undefined ? Math.max(0, fallbackDecimals - 2) : Number.isInteger(clean(v)) ? 0 : 1);\n\t\tbody = `${grouped(clean(v), d)}%`;\n\t} else if (style === 'compact') {\n\t\tconst unit = COMPACT.find((u) => Math.abs(value) >= u.value);\n\t\tif (unit) {\n\t\t\tconst v = value / unit.value;\n\t\t\tconst d = format.decimals ?? (Math.abs(v) >= 100 || Number.isInteger(clean(v)) ? 0 : 1);\n\t\t\tbody = `${trim(grouped(v, d))}${unit.suffix}`;\n\t\t} else body = trim(grouped(value, decimalsFor(value)));\n\t} else if (style === 'currency') {\n\t\tconst code = (format.currency || 'USD').toUpperCase();\n\t\tconst symbol = CURRENCY_SYMBOLS[code] ?? `${code} `;\n\t\tconst d = format.decimals ?? (fallbackDecimals !== undefined ? fallbackDecimals : Number.isInteger(value) ? 0 : 2);\n\t\tconst s = grouped(Math.abs(value), d);\n\t\tbody = `${value < 0 ? '-' : ''}${symbol}${s}`;\n\t} else body = trim(grouped(value, decimalsFor(value)));\n\treturn `${format.prefix ?? ''}${body}${format.suffix ?? ''}`;\n}\n\n/** Linear map from `domain` to `range`. */\nexport function linearMap(value: number, domain: readonly [number, number], range: readonly [number, number]): number {\n\tconst [d0, d1] = domain;\n\tconst [r0, r1] = range;\n\tif (d1 === d0) return (r0 + r1) / 2;\n\treturn r0 + ((value - d0) / (d1 - d0)) * (r1 - r0);\n}\n","import { niceScale, type NiceScale } from './scale.js';\nimport type { ChartAxis, ChartMark, ChartMarker, ChartRect, ChartScene, ChartSelection, ChartSpec, ChartTheme, ChartValueScale, ChartWarning } from './types.js';\n\nexport type ChartTextMeasure = (text: string, size: number, weight?: number) => number;\n\nexport interface ChartLayoutOptions {\n\twidth: number;\n\theight: number;\n\ttheme?: Partial<ChartTheme>;\n\t/** Highlighted in the scene (selection outlines, `selected` flags). */\n\tselection?: ChartSelection | null;\n\t/** Text width in pixels (default: an estimate from the character count). */\n\tmeasureText?: ChartTextMeasure;\n\t/** Shown when there is nothing to draw (default \"No data\"). */\n\temptyMessage?: string;\n\t/**\n\t * Fixes the value axis to this range (over `yAxis.min` / `yAxis.max`). Canvases pass\n\t * `ChartPointDrag.valueDomain` during a drag so the axis does not rescale under the pointer.\n\t */\n\tvalueDomain?: { min: number; max: number } | null;\n\t/** Fixes the secondary value axis the same way (`ChartPointDrag.valueDomain2`). */\n\tvalueDomain2?: { min: number; max: number } | null;\n\t/** Fixes the numeric x axis of a scatter or bubble chart (`ChartPointDrag.xDomain`). */\n\txDomain?: { min: number; max: number } | null;\n\t/** Shows the value of this point next to it (a `drag-readout` text mark, kept inside the plot). */\n\treadout?: { seriesId: string; index: number } | null;\n}\n\n/** A DOM-free text width estimate (average glyph ≈ 0.56 em, bold ≈ 0.6 em). */\nexport const estimateChartTextWidth: ChartTextMeasure = (text, size, weight = 400) => text.length * size * (weight >= 600 ? 0.6 : 0.56);\n\n/** Whether two selections point at the same thing. */\nexport function sameChartSelection(a: ChartSelection | null | undefined, b: ChartSelection | null | undefined): boolean {\n\tif (!a || !b || a.kind !== b.kind) return false;\n\tswitch (a.kind) {\n\t\tcase 'axis':\n\t\t\treturn a.axis === (b as typeof a).axis;\n\t\tcase 'series':\n\t\t\treturn a.seriesId === (b as typeof a).seriesId;\n\t\tcase 'point':\n\t\tcase 'trendline':\n\t\t\treturn a.seriesId === (b as typeof a).seriesId && a.index === (b as typeof a).index;\n\t\tcase 'category':\n\t\t\treturn a.index === (b as typeof a).index;\n\t\tdefault:\n\t\t\treturn true;\n\t}\n}\n\n/**\n * Black or white text for a label drawn on `fill` (#rgb, #rrggbb or rgb()), by perceived\n * luminance (0.299 R + 0.587 G + 0.114 B). Unparseable colours (CSS variables) get white.\n */\nexport function contrastingTextColor(fill: string): string {\n\tlet rgb: number[] | null = null;\n\tconst hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(fill.trim());\n\tif (hex) {\n\t\tconst h = hex[1].length === 3 ? [...hex[1]].map((c) => c + c).join('') : hex[1];\n\t\trgb = [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));\n\t} else {\n\t\tconst fn = /^rgba?\\(\\s*([\\d.]+)[\\s,]+([\\d.]+)[\\s,]+([\\d.]+)/i.exec(fill.trim());\n\t\tif (fn) rgb = fn.slice(1, 4).map(Number);\n\t}\n\tif (!rgb) return '#ffffff';\n\tconst luminance = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;\n\treturn luminance > 0.5 ? '#0f172a' : '#ffffff';\n}\n\nexport const r2 = (n: number) => Math.round(n * 100) / 100;\n\n/** Shared state while one scene is laid out. */\nexport interface LayoutCtx {\n\tspec: ChartSpec;\n\toptions: ChartLayoutOptions;\n\ttheme: ChartTheme;\n\tmeasure: ChartTextMeasure;\n\tfs: number;\n\twidth: number;\n\theight: number;\n\tmarks: ChartMark[];\n\twarnings: ChartWarning[];\n\tselection: ChartSelection | null;\n\tisSelected(target: ChartSelection): boolean;\n\tseriesSelected(id: string): boolean;\n\toutline(key: string, box: ChartRect): void;\n\twarn(code: ChartWarning['code'], message: string): void;\n}\n\n/** What a family layout adds to the scene. */\nexport interface FamilyResult {\n\tplot: ChartRect;\n\tvalueScale?: ChartValueScale;\n\tvalueScale2?: ChartValueScale;\n\txScale?: ChartValueScale;\n\tpercent?: boolean;\n}\n\nexport type ChartSceneBase = Omit<ChartScene, 'marks' | 'warnings' | 'theme' | 'width' | 'height'>;\n\nexport interface Pt {\n\tc: number;\n\tv: number;\n}\n\n/** Path through points (in category/value pixel space) with the chosen curve; `map` turns them into x/y. */\nexport function curvePath(points: Pt[], curve: ChartSpec['curve'], map: (p: Pt) => [number, number], moveFirst = true): string {\n\tif (!points.length) return '';\n\tconst xy = points.map(map);\n\tlet d = `${moveFirst ? 'M' : 'L'}${r2(xy[0][0])},${r2(xy[0][1])}`;\n\tif (points.length === 1) return d;\n\tif (curve === 'step') {\n\t\tfor (let i = 1; i < points.length; i++) {\n\t\t\tconst mid = (points[i - 1].c + points[i].c) / 2;\n\t\t\tconst a = map({ c: mid, v: points[i - 1].v });\n\t\t\tconst b = map({ c: mid, v: points[i].v });\n\t\t\td += `L${r2(a[0])},${r2(a[1])}L${r2(b[0])},${r2(b[1])}L${r2(xy[i][0])},${r2(xy[i][1])}`;\n\t\t}\n\t\treturn d;\n\t}\n\tif (curve === 'smooth' && points.length > 2) {\n\t\t// Monotone cubic (Fritsch–Carlson): smooth, never overshoots the data.\n\t\tconst n = points.length;\n\t\tconst dc = points.slice(1).map((p, i) => p.c - points[i].c);\n\t\tconst slope = points.slice(1).map((p, i) => (dc[i] === 0 ? 0 : (p.v - points[i].v) / dc[i]));\n\t\tconst tangent = points.map((_, i) => {\n\t\t\tif (i === 0) return slope[0];\n\t\t\tif (i === n - 1) return slope[n - 2];\n\t\t\tconst a = slope[i - 1];\n\t\t\tconst b = slope[i];\n\t\t\tif (a * b <= 0) return 0;\n\t\t\tconst wa = 2 * dc[i] + dc[i - 1];\n\t\t\tconst wb = dc[i] + 2 * dc[i - 1];\n\t\t\treturn (wa + wb) / (wa / a + wb / b);\n\t\t});\n\t\tfor (let i = 1; i < n; i++) {\n\t\t\tconst h = dc[i - 1] / 3;\n\t\t\tconst p0 = points[i - 1];\n\t\t\tconst p1 = points[i];\n\t\t\tconst a = map({ c: p0.c + h, v: p0.v + tangent[i - 1] * h });\n\t\t\tconst b = map({ c: p1.c - h, v: p1.v - tangent[i] * h });\n\t\t\td += `C${r2(a[0])},${r2(a[1])} ${r2(b[0])},${r2(b[1])} ${r2(xy[i][0])},${r2(xy[i][1])}`;\n\t\t}\n\t\treturn d;\n\t}\n\tfor (let i = 1; i < xy.length; i++) d += `L${r2(xy[i][0])},${r2(xy[i][1])}`;\n\treturn d;\n}\n\nexport function arcPath(cx: number, cy: number, outer: number, inner: number, start: number, end: number): string {\n\tconst full = end - start >= Math.PI * 2 - 1e-9;\n\tif (full) end = start + Math.PI * 2 - 1e-4;\n\tconst p = (r: number, a: number) => `${r2(cx + r * Math.sin(a))},${r2(cy - r * Math.cos(a))}`;\n\tconst large = end - start > Math.PI ? 1 : 0;\n\tlet d = `M${p(outer, start)}A${r2(outer)},${r2(outer)} 0 ${large} 1 ${p(outer, end)}`;\n\tif (inner > 0) d += `L${p(inner, end)}A${r2(inner)},${r2(inner)} 0 ${large} 0 ${p(inner, start)}Z`;\n\telse d += `L${r2(cx)},${r2(cy)}Z`;\n\treturn d;\n}\n\nexport function truncate(text: string, maxWidth: number, size: number, measure: ChartTextMeasure): string {\n\tif (measure(text, size) <= maxWidth) return text;\n\tlet out = text;\n\twhile (out.length > 1 && measure(`${out}…`, size) > maxWidth) out = out.slice(0, -1);\n\treturn `${out}…`;\n}\n\n/** The outline of a point marker centred on (cx, cy) with \"radius\" r, as an SVG path. */\nexport function markerPath(shape: Exclude<ChartMarker, 'circle' | 'none'>, cx: number, cy: number, r: number): string {\n\tswitch (shape) {\n\t\tcase 'square':\n\t\t\treturn `M${r2(cx - r)},${r2(cy - r)}H${r2(cx + r)}V${r2(cy + r)}H${r2(cx - r)}Z`;\n\t\tcase 'diamond': {\n\t\t\tconst d = r * 1.3;\n\t\t\treturn `M${r2(cx)},${r2(cy - d)}L${r2(cx + d)},${r2(cy)}L${r2(cx)},${r2(cy + d)}L${r2(cx - d)},${r2(cy)}Z`;\n\t\t}\n\t\tcase 'triangle': {\n\t\t\tconst d = r * 1.3;\n\t\t\treturn `M${r2(cx)},${r2(cy - d)}L${r2(cx + d * 0.87)},${r2(cy + d * 0.5)}L${r2(cx - d * 0.87)},${r2(cy + d * 0.5)}Z`;\n\t\t}\n\t\tcase 'cross': {\n\t\t\tconst d = r;\n\t\t\treturn `M${r2(cx - d)},${r2(cy - d)}L${r2(cx + d)},${r2(cy + d)}M${r2(cx - d)},${r2(cy + d)}L${r2(cx + d)},${r2(cy - d)}`;\n\t\t}\n\t}\n}\n\n/** The tick count for an axis `length` pixels long (about one per 56 px, 2–10). */\nexport const tickCount = (length: number) => Math.max(2, Math.min(10, Math.round(length / 56)));\n\n/**\n * A value scale over [lo, hi] for an axis `length` pixels long: the axis' fixed ends and\n * step win, then `frozen` (a drag in progress) fixes both ends. `headroom` adds 5% above a\n * maximum that lands exactly on the top tick (lines and points would touch the frame).\n */\nexport function axisScale(lo: number, hi: number, length: number, axis: ChartAxis, frozen?: { min: number; max: number } | null, headroom = false): NiceScale {\n\tconst fixedMin = frozen && Number.isFinite(frozen.min) ? frozen.min : axis.min;\n\tconst fixedMax = frozen && Number.isFinite(frozen.max) ? frozen.max : axis.max;\n\tconst step = axis.step;\n\tif (step && step > 0 && Number.isFinite(lo) && Number.isFinite(hi)) {\n\t\tlet min = fixedMin ?? Math.floor(lo / step + 1e-9) * step;\n\t\tlet max = fixedMax ?? Math.ceil(hi / step - 1e-9) * step;\n\t\tif (!(max > min)) max = min + step;\n\t\tconst first = Math.ceil(min / step - 1e-9);\n\t\tconst last = Math.floor(max / step + 1e-9);\n\t\tif (last - first <= 200) {\n\t\t\tconst clean = (n: number) => (Math.abs(n) < 1e-12 ? 0 : Number(n.toPrecision(12)));\n\t\t\tmin = clean(min);\n\t\t\tmax = clean(max);\n\t\t\tconst ticks: number[] = [];\n\t\t\tfor (let i = first; i <= last; i++) ticks.push(clean(i * step));\n\t\t\treturn { min, max, step, ticks };\n\t\t}\n\t}\n\tlet scale = niceScale(lo, hi, tickCount(length), fixedMin, fixedMax);\n\tconst roomy = hi + (hi - lo) * 0.05;\n\tif (headroom && fixedMax === null && hi !== 0 && hi > lo && scale.max === hi && Number.isFinite(roomy)) scale = niceScale(lo, roomy, tickCount(length), fixedMin, fixedMax);\n\treturn scale;\n}\n\n/** Liang–Barsky: the part of segment a→b inside `rect`, or null. */\nexport function clipSegment(a: [number, number], b: [number, number], rect: ChartRect): [[number, number], [number, number]] | null {\n\tlet t0 = 0;\n\tlet t1 = 1;\n\tconst dx = b[0] - a[0];\n\tconst dy = b[1] - a[1];\n\tconst checks: [number, number][] = [\n\t\t[-dx, a[0] - rect.x],\n\t\t[dx, rect.x + rect.width - a[0]],\n\t\t[-dy, a[1] - rect.y],\n\t\t[dy, rect.y + rect.height - a[1]]\n\t];\n\tfor (const [p, q] of checks) {\n\t\tif (p === 0) {\n\t\t\tif (q < 0) return null;\n\t\t\tcontinue;\n\t\t}\n\t\tconst r = q / p;\n\t\tif (p < 0) {\n\t\t\tif (r > t1) return null;\n\t\t\tif (r > t0) t0 = r;\n\t\t} else {\n\t\t\tif (r < t0) return null;\n\t\t\tif (r < t1) t1 = r;\n\t\t}\n\t}\n\treturn [\n\t\t[a[0] + t0 * dx, a[1] + t0 * dy],\n\t\t[a[0] + t1 * dx, a[1] + t1 * dy]\n\t];\n}\n\n/** A polyline clipped to `rect` as path data (separate runs where it leaves and re-enters). */\nexport function clippedPolyline(points: [number, number][], rect: ChartRect): string {\n\tlet d = '';\n\tlet open = false;\n\tlet last: [number, number] | null = null;\n\tfor (let i = 1; i < points.length; i++) {\n\t\tconst seg = clipSegment(points[i - 1], points[i], rect);\n\t\tif (!seg) {\n\t\t\topen = false;\n\t\t\tcontinue;\n\t\t}\n\t\tconst [p, q] = seg;\n\t\tif (!open || !last || Math.abs(last[0] - p[0]) > 0.01 || Math.abs(last[1] - p[1]) > 0.01) d += `M${r2(p[0])},${r2(p[1])}`;\n\t\td += `L${r2(q[0])},${r2(q[1])}`;\n\t\tlast = q;\n\t\topen = true;\n\t}\n\treturn d;\n}\n","// Portions ported from gridcraft-main/crates/chart/src/render.rs (`histogram` binning, `quantile` and\n// `box_whisker` statistics, `squarify` treemap layout, `decimate`) (MIT OR Apache-2.0, © ArtCraft Team and\n// contributors). See THIRD_PARTY_NOTICES.md.\nimport type { ChartRect, ChartValue } from './types.js';\n\nexport interface ChartHistogramBin {\n\t/** Lower edge (exclusive, except for the first bin). */\n\tfrom: number;\n\t/** Upper edge (inclusive). */\n\tto: number;\n\tcount: number;\n}\n\n/**\n * Bins values into `bins` equal-width bins (default √n, from 1 to 100). Bins are\n * right-inclusive, the first closed on both sides, so a value on an edge belongs to the bin\n * that ends there. Gaps and non-finite values are ignored.\n */\nexport function chartHistogram(values: readonly ChartValue[], bins: number | null = null): ChartHistogramBin[] {\n\tconst vals = values.filter((v): v is number => v !== null && Number.isFinite(v));\n\tif (!vals.length) return [];\n\tlet lo = Infinity;\n\tlet hi = -Infinity;\n\tfor (const v of vals) {\n\t\tlo = Math.min(lo, v);\n\t\thi = Math.max(hi, v);\n\t}\n\tconst span = hi - lo;\n\tlet count = bins ?? Math.max(1, Math.min(100, Math.ceil(Math.sqrt(vals.length))));\n\tif (!(span > 0)) count = 1;\n\tconst width = count === 1 && !(span > 0) ? 1 : span / count;\n\tconst edges = Array.from({ length: count }, (_, b) => (b + 1 === count ? Math.max(hi, lo + b * width) : lo + (b + 1) * width));\n\tconst counts = Array<number>(count).fill(0);\n\tfor (const v of vals) {\n\t\t// First edge >= v (binary search).\n\t\tlet a = 0;\n\t\tlet z = count;\n\t\twhile (a < z) {\n\t\t\tconst mid = (a + z) >> 1;\n\t\t\tif (edges[mid] < v) a = mid + 1;\n\t\t\telse z = mid;\n\t\t}\n\t\tcounts[Math.min(a, count - 1)]++;\n\t}\n\treturn counts.map((c, b) => ({ from: clean(lo + b * width), to: clean(edges[b]), count: c }));\n}\n\nconst clean = (n: number) => (Math.abs(n) < 1e-12 ? 0 : Number(n.toPrecision(12)));\n\n/** The `q` quantile (0–1) of sorted values, interpolating linearly between ranks (`q·(n-1)`). */\nexport function chartQuantile(sorted: readonly number[], q: number): number {\n\tconst n = sorted.length;\n\tif (!n) return 0;\n\tconst pos = q * (n - 1);\n\tconst i = Math.floor(pos);\n\tconst a = sorted[i];\n\tconst b = sorted[i + 1] ?? a;\n\treturn a + (b - a) * (pos - i);\n}\n\nexport interface ChartBoxStats {\n\tmin: number;\n\tmax: number;\n\tq1: number;\n\tmedian: number;\n\tq3: number;\n\tmean: number;\n\t/** Whisker ends: the furthest values within 1.5 × IQR of the box. */\n\tlowerWhisker: number;\n\tupperWhisker: number;\n\t/** Values beyond the whiskers. */\n\toutliers: number[];\n\tcount: number;\n}\n\n/** Box-and-whisker statistics (Tukey fences at 1.5 × IQR); `null` without values. */\nexport function chartBoxStats(values: readonly ChartValue[]): ChartBoxStats | null {\n\tconst v = values.filter((x): x is number => x !== null && Number.isFinite(x)).sort((a, b) => a - b);\n\tif (!v.length) return null;\n\tconst q1 = chartQuantile(v, 0.25);\n\tconst median = chartQuantile(v, 0.5);\n\tconst q3 = chartQuantile(v, 0.75);\n\tconst iqr = q3 - q1;\n\tconst lo = q1 - 1.5 * iqr;\n\tconst hi = q3 + 1.5 * iqr;\n\treturn {\n\t\tmin: v[0],\n\t\tmax: v[v.length - 1],\n\t\tq1,\n\t\tmedian,\n\t\tq3,\n\t\tmean: v.reduce((t, x) => t + x, 0) / v.length,\n\t\tlowerWhisker: v.find((x) => x >= lo) ?? q1,\n\t\tupperWhisker: [...v].reverse().find((x) => x <= hi) ?? q3,\n\t\toutliers: v.filter((x) => x < lo || x > hi),\n\t\tcount: v.length\n\t};\n}\n\n/** Squarified treemap (Bruls et al.): positive `values` packed into `rect`, largest first. Returns one rect per input index (zero-size for values ≤ 0). */\nexport function chartSquarify(values: readonly number[], rect: ChartRect): ChartRect[] {\n\tconst out: ChartRect[] = values.map(() => ({ x: rect.x, y: rect.y, width: 0, height: 0 }));\n\tconst items = values\n\t\t.map((v, i) => [i, v] as [number, number])\n\t\t.filter(([, v]) => v > 0 && Number.isFinite(v))\n\t\t.sort((a, b) => b[1] - a[1]);\n\tconst total = items.reduce((t, [, v]) => t + v, 0);\n\tconst area = rect.width * rect.height;\n\tif (!(total > 0 && area > 0)) return out;\n\tconst scale = area / total;\n\tconst rem = { ...rect };\n\tlet row: [number, number][] = [];\n\tconst worst = (r: number[], side: number) => {\n\t\tconst s = r.reduce((t, v) => t + v, 0);\n\t\tif (s <= 0 || side <= 0) return Infinity;\n\t\tconst mn = Math.min(...r);\n\t\tconst mx = Math.max(...r);\n\t\treturn Math.max((side * side * mx) / (s * s), (s * s) / (side * side * mn));\n\t};\n\tconst layoutRow = (r: [number, number][]) => {\n\t\tconst s = r.reduce((t, [, a]) => t + a, 0);\n\t\tif (s <= 0) return;\n\t\tif (rem.width >= rem.height) {\n\t\t\tconst cw = s / Math.max(1e-6, rem.height);\n\t\t\tlet y = rem.y;\n\t\t\tfor (const [j, a] of r) {\n\t\t\t\tconst h = a / Math.max(1e-6, cw);\n\t\t\t\tout[j] = { x: rem.x, y, width: cw, height: h };\n\t\t\t\ty += h;\n\t\t\t}\n\t\t\trem.x += cw;\n\t\t\trem.width = Math.max(0, rem.width - cw);\n\t\t} else {\n\t\t\tconst rh = s / Math.max(1e-6, rem.width);\n\t\t\tlet x = rem.x;\n\t\t\tfor (const [j, a] of r) {\n\t\t\t\tconst w = a / Math.max(1e-6, rh);\n\t\t\t\tout[j] = { x, y: rem.y, width: w, height: rh };\n\t\t\t\tx += w;\n\t\t\t}\n\t\t\trem.y += rh;\n\t\t\trem.height = Math.max(0, rem.height - rh);\n\t\t}\n\t};\n\tlet i = 0;\n\twhile (i < items.length) {\n\t\tconst [j, v] = items[i];\n\t\tconst a = v * scale;\n\t\tconst side = Math.min(rem.width, rem.height);\n\t\tconst cur = row.map(([, x]) => x);\n\t\tif (!row.length || worst([...cur, a], side) <= worst(cur, side)) {\n\t\t\trow.push([j, a]);\n\t\t\ti++;\n\t\t} else {\n\t\t\tlayoutRow(row);\n\t\t\trow = [];\n\t\t}\n\t}\n\tif (row.length) layoutRow(row);\n\treturn out;\n}\n\n/**\n * Thins a long polyline (sorted by x) to the first, lowest, highest and last point of each\n * pixel column, which draws identically. Returns the input when it has `max` points or fewer.\n */\nexport function decimateChartPoints<T extends { x: number; y: number }>(points: readonly T[], max = 2000): T[] {\n\tif (points.length <= max) return [...points];\n\tconst out: T[] = [];\n\tlet i = 0;\n\twhile (i < points.length) {\n\t\tconst col = Math.floor(points[i].x);\n\t\tconst start = i;\n\t\tlet mn = i;\n\t\tlet mx = i;\n\t\tlet k = i + 1;\n\t\twhile (k < points.length && Math.floor(points[k].x) === col) {\n\t\t\tif (points[k].y < points[mn].y) mn = k;\n\t\t\tif (points[k].y > points[mx].y) mx = k;\n\t\t\tk++;\n\t\t}\n\t\tconst end = k - 1;\n\t\tfor (const idx of [...new Set([start, mn, mx, end])].sort((a, b) => a - b)) out.push(points[idx]);\n\t\ti = k;\n\t}\n\treturn out;\n}\n\nexport interface ChartSeriesSummary {\n\tid: string;\n\tname: string;\n\t/** Values present (not gaps). */\n\tcount: number;\n\tgaps: number;\n\tmin: number | null;\n\tmax: number | null;\n\tmean: number | null;\n\tsum: number | null;\n\t/** Category label of the minimum and maximum. */\n\tminAt: string | null;\n\tmaxAt: string | null;\n\t/** Direction of a least-squares line through the values in order. */\n\ttrend: 'rising' | 'falling' | 'flat' | null;\n\t/** Relative change from the first to the last value, e.g. 0.25 for +25%. */\n\tchange: number | null;\n}\n\nconst round = (n: number) => Number(n.toPrecision(6));\n\n/** Numbers about one series, for agents that check a chart without looking at it. */\nexport function summarizeChartSeries(series: { id: string; name: string; values: readonly ChartValue[] }, categories: readonly string[]): ChartSeriesSummary {\n\tconst present = series.values.map((v, i) => [i, v] as const).filter((p): p is readonly [number, number] => p[1] !== null && Number.isFinite(p[1]));\n\tconst base: ChartSeriesSummary = { id: series.id, name: series.name, count: present.length, gaps: series.values.length - present.length, min: null, max: null, mean: null, sum: null, minAt: null, maxAt: null, trend: null, change: null };\n\tif (!present.length) return base;\n\tlet lo = present[0];\n\tlet hi = present[0];\n\tlet sum = 0;\n\tfor (const p of present) {\n\t\tif (p[1] < lo[1]) lo = p;\n\t\tif (p[1] > hi[1]) hi = p;\n\t\tsum += p[1];\n\t}\n\tconst mean = sum / present.length;\n\tlet trend: ChartSeriesSummary['trend'] = null;\n\tif (present.length >= 2) {\n\t\tconst mx = present.reduce((t, p) => t + p[0], 0) / present.length;\n\t\tlet sxy = 0;\n\t\tlet sxx = 0;\n\t\tfor (const [i, v] of present) {\n\t\t\tsxy += (i - mx) * (v - mean);\n\t\t\tsxx += (i - mx) ** 2;\n\t\t}\n\t\tconst slope = sxx ? sxy / sxx : 0;\n\t\tconst spread = hi[1] - lo[1];\n\t\t// Flat when the fitted line moves less than 5% of the spread over the whole series.\n\t\tconst travel = Math.abs(slope * (present[present.length - 1][0] - present[0][0]));\n\t\ttrend = !spread || travel < spread * 0.05 ? 'flat' : slope > 0 ? 'rising' : 'falling';\n\t}\n\tconst first = present[0][1];\n\tconst last = present[present.length - 1][1];\n\treturn {\n\t\t...base,\n\t\tmin: round(lo[1]),\n\t\tmax: round(hi[1]),\n\t\tmean: round(mean),\n\t\tsum: round(sum),\n\t\tminAt: categories[lo[0]] ?? String(lo[0]),\n\t\tmaxAt: categories[hi[0]] ?? String(hi[0]),\n\t\ttrend,\n\t\tchange: present.length >= 2 && first !== 0 ? round((last - first) / Math.abs(first)) : null\n\t};\n}\n","// Portions ported from gridcraft-main/crates/chart/src/trend.rs (MIT OR Apache-2.0, © ArtCraft Team and\n// contributors). See THIRD_PARTY_NOTICES.md.\nimport type { ChartTrendline, ChartTrendlineType } from './types.js';\n\n/**\n * A fitted regression trendline. Exponential, logarithmic and power fits are linear fits of\n * transformed data (`ln y ~ x`, `y ~ ln x`, `ln y ~ ln x`), as spreadsheets document them;\n * points the transform cannot take (values ≤ 0) are skipped, and R² is for the transformed fit.\n */\nexport interface ChartTrendFit {\n\ttype: Exclude<ChartTrendlineType, 'moving-average'>;\n\t/** Coefficients `c0…cd` of `Y = Σ cₖ·Xᵏ` in the transformed variables. */\n\tcoefficients: number[];\n\t/** Coefficient of determination; `null` when undefined (all y equal). */\n\tr2: number | null;\n\t/** The equation as a label, e.g. `y = 0.5x² - 2x + 1` or `y = 3e^(0.2x)`. */\n\tequation: string;\n\t/** Number of points used. */\n\tpoints: number;\n\t/** The fitted y at `x`, or `null` outside the fit's domain (x ≤ 0 for log and power). */\n\tevaluate(x: number): number | null;\n}\n\nconst degree = (t: ChartTrendline): number => (t.type === 'polynomial' ? Math.max(2, Math.min(6, t.order ?? 2)) : 1);\nconst logX = (type: ChartTrendlineType) => type === 'logarithmic' || type === 'power';\nconst logY = (type: ChartTrendlineType) => type === 'exponential' || type === 'power';\n\n/**\n * Fits `trendline` to the points. Returns `null` for moving averages (see\n * `chartMovingAverage`) and degenerate data: too few usable points, all x equal, or a\n * singular system.\n */\nexport function fitChartTrendline(trendline: ChartTrendline, points: ReadonlyArray<readonly [number, number]>): ChartTrendFit | null {\n\tconst type = trendline.type;\n\tif (type === 'moving-average') return null;\n\tconst d = degree(trendline);\n\tconst data = points\n\t\t.filter(([x, y]) => Number.isFinite(x) && Number.isFinite(y) && (!logX(type) || x > 0) && (!logY(type) || y > 0))\n\t\t.map(([x, y]) => [logX(type) ? Math.log(x) : x, logY(type) ? Math.log(y) : y] as const);\n\tif (data.length < 2 || data.length <= d) return null;\n\t// A fixed intercept applies to linear, exponential and polynomial fits; it fixes Y at x = 0.\n\tlet fixed: number | null = null;\n\tconst b = trendline.intercept;\n\tif (b !== undefined && b !== null && Number.isFinite(b)) {\n\t\tif (type === 'linear' || type === 'polynomial') fixed = b;\n\t\telse if (type === 'exponential' && b > 0) fixed = Math.log(b);\n\t}\n\tconst n = data.length;\n\tconst center = fixed !== null ? 0 : data.reduce((t, p) => t + p[0], 0) / n;\n\tconst scale = data.reduce((m, p) => Math.max(m, Math.abs(p[0] - center)), 0);\n\tif (!(Number.isFinite(scale) && scale > 0)) return null;\n\tconst first = fixed !== null ? 1 : 0;\n\tconst b0 = fixed ?? 0;\n\t// Normal equations over the unknown powers first..d of u = (x - center) / scale.\n\tconst m = d + 1 - first;\n\tconst ata = Array.from({ length: m }, () => Array<number>(m).fill(0));\n\tconst aty = Array<number>(m).fill(0);\n\tfor (const [x, y] of data) {\n\t\tconst u = (x - center) / scale;\n\t\tconst pows = Array.from({ length: m }, (_, k) => Math.pow(u, k + first));\n\t\tfor (let i = 0; i < m; i++) {\n\t\t\taty[i] += pows[i] * (y - b0);\n\t\t\tfor (let j = 0; j < m; j++) ata[i][j] += pows[i] * pows[j];\n\t\t}\n\t}\n\tconst sol = solve(ata, aty);\n\tif (!sol) return null;\n\tconst a = Array<number>(d + 1).fill(b0);\n\tsol.forEach((v, k) => (a[k + first] = v));\n\tconst coefficients = expand(a, center, scale);\n\tif (!coefficients) return null;\n\tconst poly = (x: number) => {\n\t\tconst u = (x - center) / scale;\n\t\treturn a.reduceRight((acc, c) => acc * u + c, 0);\n\t};\n\tconst mean = data.reduce((t, p) => t + p[1], 0) / n;\n\tlet ssRes = 0;\n\tlet ssTot = 0;\n\tfor (const [x, y] of data) {\n\t\tssRes += (y - poly(x)) ** 2;\n\t\t// A fixed intercept is LINEST(y - b, x, FALSE): the total sum of squares is not centred.\n\t\tssTot += fixed !== null ? (y - b0) ** 2 : (y - mean) ** 2;\n\t}\n\tconst r2 = ssTot > 0 && Number.isFinite(ssTot) && Number.isFinite(ssRes) ? 1 - ssRes / ssTot : null;\n\treturn {\n\t\ttype,\n\t\tcoefficients,\n\t\tr2,\n\t\tequation: equationText(type, coefficients),\n\t\tpoints: n,\n\t\tevaluate(x: number) {\n\t\t\tlet xx = x;\n\t\t\tif (logX(type)) {\n\t\t\t\tif (!(x > 0)) return null;\n\t\t\t\txx = Math.log(x);\n\t\t\t}\n\t\t\tconst p = poly(xx);\n\t\t\tconst y = logY(type) ? Math.exp(p) : p;\n\t\t\treturn Number.isFinite(y) ? y : null;\n\t\t}\n\t};\n}\n\n/** Gaussian elimination with partial pivoting; `null` when (nearly) singular. */\nfunction solve(a: number[][], b: number[]): number[] | null {\n\tconst n = b.length;\n\tconst big = a.flat().reduce((m, v) => Math.max(m, Math.abs(v)), 0);\n\tif (!(Number.isFinite(big) && big > 0)) return null;\n\tfor (let col = 0; col < n; col++) {\n\t\tlet piv = col;\n\t\tfor (let r = col + 1; r < n; r++) if (Math.abs(a[r][col]) > Math.abs(a[piv][col])) piv = r;\n\t\t[a[col], a[piv]] = [a[piv], a[col]];\n\t\t[b[col], b[piv]] = [b[piv], b[col]];\n\t\tconst p = a[col][col];\n\t\tif (!Number.isFinite(p) || Math.abs(p) <= big * 1e-13) return null;\n\t\tfor (let r = col + 1; r < n; r++) {\n\t\t\tconst f = a[r][col] / p;\n\t\t\tfor (let c = col; c < n; c++) a[r][c] -= f * a[col][c];\n\t\t\tb[r] -= f * b[col];\n\t\t}\n\t}\n\tconst x = Array<number>(n).fill(0);\n\tfor (let r = n - 1; r >= 0; r--) {\n\t\tlet s = 0;\n\t\tfor (let c = r + 1; c < n; c++) s += a[r][c] * x[c];\n\t\tx[r] = (b[r] - s) / a[r][r];\n\t}\n\treturn x.every(Number.isFinite) ? x : null;\n}\n\n/** Coefficients in X of `Σ aₖ·((X - c) / s)ᵏ` (binomial expansion). */\nfunction expand(a: number[], c: number, s: number): number[] | null {\n\tconst out = Array<number>(a.length).fill(0);\n\ta.forEach((ak, k) => {\n\t\tconst lead = ak / Math.pow(s, k);\n\t\tlet binom = 1;\n\t\tfor (let j = 0; j <= k; j++) {\n\t\t\tout[j] += lead * binom * Math.pow(-c, k - j);\n\t\t\tbinom = (binom * (k - j)) / (j + 1);\n\t\t}\n\t});\n\treturn out.every(Number.isFinite) ? out : null;\n}\n\nconst POWERS = ['', 'x', 'x²', 'x³', 'x⁴', 'x⁵', 'x⁶'];\n\nfunction equationText(type: ChartTrendFit['type'], coef: number[]): string {\n\tconst c0 = coef[0] ?? 0;\n\tconst c1 = coef[1] ?? 0;\n\tif (type === 'exponential') return `y = ${trendNumberText(Math.exp(c0))}e^(${trendNumberText(c1)}x)`;\n\tif (type === 'power') return `y = ${trendNumberText(Math.exp(c0))}x^${trendNumberText(c1)}`;\n\tif (type === 'logarithmic') return terms([[c1, 'ln(x)'], [c0, '']]);\n\treturn terms(coef.map((c, k) => [c, POWERS[k] ?? 'x'] as [number, string]).reverse());\n}\n\n/** `y = a·t1 ± b·t2 …`, skipping terms whose coefficient shows as 0 and unit coefficients on powers of x. */\nfunction terms(parts: [number, string][]): string {\n\tlet s = 'y = ';\n\tlet first = true;\n\tfor (const [c, t] of parts) {\n\t\tconst txt = trendNumberText(Math.abs(c));\n\t\tif (txt === '0') continue;\n\t\tconst neg = c < 0;\n\t\tif (first) s += neg ? '-' : '';\n\t\telse s += neg ? ' - ' : ' + ';\n\t\tif (txt !== '1' || !t) s += txt;\n\t\ts += t;\n\t\tfirst = false;\n\t}\n\treturn first ? `${s}0` : s;\n}\n\n/** A coefficient as trendline labels show it: up to four decimals, scientific when tiny or huge. */\nexport function trendNumberText(v: number): string {\n\tif (!Number.isFinite(v)) return '#NUM!';\n\tconst a = Math.abs(v);\n\tif (a === 0) return '0';\n\tif (a < 1e-4 || a >= 1e11) {\n\t\tconst [mant, exp] = v.toExponential(4).split('e');\n\t\tconst e = Number(exp);\n\t\treturn `${trimZeros(mant)}E${e < 0 ? '-' : '+'}${String(Math.abs(e)).padStart(2, '0')}`;\n\t}\n\tconst s = trimZeros(v.toFixed(4));\n\treturn s === '-0' ? '0' : s;\n}\n\nconst trimZeros = (s: string) => (s.includes('.') ? s.replace(/0+$/, '').replace(/\\.$/, '') : s);\n\n/**\n * Moving average of `period` consecutive points in plotted order: point `i` (from\n * `period - 1`) is `(xᵢ, mean(yᵢ₋ₚ₊₁ … yᵢ))`. Empty when there are fewer points than `period`.\n */\nexport function chartMovingAverage(points: ReadonlyArray<readonly [number, number]>, period: number): [number, number][] {\n\tconst p = Math.max(2, Math.min(1000, Math.round(period) || 2));\n\tconst pts = points.filter(([x, y]) => Number.isFinite(x) && Number.isFinite(y));\n\tconst out: [number, number][] = [];\n\tlet sum = 0;\n\tpts.forEach(([x, y], i) => {\n\t\tsum += y;\n\t\tif (i >= p) sum -= pts[i - p][1];\n\t\tif (i >= p - 1) out.push([x, sum / p]);\n\t});\n\treturn out;\n}\n\n/** The legend name spreadsheets give a trendline, e.g. `Linear (Sales)`. */\nexport function chartTrendlineName(trendline: ChartTrendline, seriesName: string): string {\n\tif (trendline.name) return trendline.name;\n\tconst kind: Record<ChartTrendlineType, string> = {\n\t\tlinear: 'Linear',\n\t\texponential: 'Expon.',\n\t\tlogarithmic: 'Log.',\n\t\tpolynomial: 'Poly.',\n\t\tpower: 'Power',\n\t\t'moving-average': `${Math.max(2, trendline.period ?? 2)} per. Mov. Avg.`\n\t};\n\treturn `${kind[trendline.type]} (${seriesName})`;\n}\n\n/** Whether trendlines can be drawn for this chart type and stacking (unstacked cartesian and xy charts). */\nexport function chartSupportsTrendlines(type: string, stacking: string): boolean {\n\treturn (type === 'bar' || type === 'line' || type === 'area' || type === 'scatter' || type === 'bubble') && stacking === 'none';\n}\n","// Trendline sampling and clipping follow gridcraft-main/crates/chart/src/render.rs (`draw_trendline`,\n// `clip_segment`) and the label rules its `on` / fit checks use; marker de-duplication and polyline decimation\n// follow its `draw_markers` and `decimate` (MIT OR Apache-2.0, © ArtCraft Team and contributors). See\n// THIRD_PARTY_NOTICES.md.\nimport { chartDateLabels } from './date.js';\nimport {\n\taxisScale,\n\tclippedPolyline,\n\tcontrastingTextColor,\n\tcurvePath,\n\tmarkerPath,\n\tr2,\n\ttruncate,\n\ttype FamilyResult,\n\ttype LayoutCtx,\n\ttype Pt\n} from './layout-shared.js';\nimport { formatChartNumber, linearMap, stepDecimals, type NiceScale } from './scale.js';\nimport { chartPointColor, chartSeriesColor, chartSeriesType } from './spec.js';\nimport { decimateChartPoints } from './stats.js';\nimport { chartMovingAverage, chartSupportsTrendlines, chartTrendlineName, fitChartTrendline, trendNumberText } from './trend.js';\nimport type { ChartAxis, ChartMark, ChartMarker, ChartNumberFormat, ChartRect, ChartSelection, ChartSeries, ChartSeriesType, ChartValueScale } from './types.js';\n\n/** Data labels are skipped on series with more points than this. */\nexport const MAX_CHART_DATA_LABELS = 200;\n/** Lines with more points than this are thinned per pixel column. */\nexport const MAX_CHART_LINE_POINTS = 2000;\n/** Markers beyond this many are thinned to one per 2px cell. */\nexport const MAX_CHART_MARKERS = 1000;\n\nexport interface ValueAxisInput {\n\tlo: number;\n\thi: number;\n\taxis: ChartAxis;\n\tformat: ChartNumberFormat;\n\tfrozen?: { min: number; max: number } | null;\n\theadroom?: boolean;\n\t/** Which selection the tick labels pick. */\n\tkey: 'y' | 'y2';\n}\n\nexport interface ScaleInfo {\n\tscale: NiceScale;\n\trange: [number, number];\n\t/** Value → pixel, clamped to the axis. */\n\tpx(v: number): number;\n\t/** Value → pixel, not clamped. */\n\traw(v: number): number;\n\t/** Pixel of 0 (or the nearest end). */\n\tzero: number;\n\tformat: ChartNumberFormat;\n\ttickText(t: number): string;\n}\n\nexport interface CategoryFrame {\n\tplot: ChartRect;\n\thorizontal: boolean;\n\tn: number;\n\tband: number;\n\tcatStart: number;\n\tcatLen: number;\n\tcatCenter(i: number): number;\n\tmap(p: Pt): [number, number];\n\tvalue: ScaleInfo;\n\tvalue2?: ScaleInfo;\n}\n\nexport interface CategoryFrameInput {\n\tlabels: readonly string[];\n\thorizontal: boolean;\n\tprimary: ValueAxisInput;\n\tsecondary?: ValueAxisInput;\n\t/** What clicking category label `i` selects (default: the category). */\n\tlabelTarget?: (i: number) => ChartSelection | undefined;\n\t/** Category axis settings (default `spec.xAxis`). */\n\tcategoryAxis?: ChartAxis;\n}\n\nfunction scaleInfo(scale: NiceScale, range: [number, number], format: ChartNumberFormat): ScaleInfo {\n\tconst domain: [number, number] = [scale.min, scale.max];\n\tconst raw = (v: number) => linearMap(v, domain, range);\n\tconst px = (v: number) => raw(Math.min(scale.max, Math.max(scale.min, v)));\n\treturn { scale, range, px, raw, zero: px(0), format, tickText: (t) => formatChartNumber(t, format, format.decimals ?? stepDecimals(scale.step)) };\n}\n\n/**\n * The axes of a category chart: category labels along one side (rotated, thinned or read as\n * dates as space allows), one or two value axes, grid lines and axis titles. Returns the plot\n * box and value ↔ pixel maps.\n */\nexport function categoryFrame(ctx: LayoutCtx, area: ChartRect, input: CategoryFrameInput): CategoryFrame {\n\tconst { spec, theme, measure, fs, marks } = ctx;\n\tconst { labels, horizontal, primary, secondary } = input;\n\tconst xAxis = input.categoryAxis ?? spec.xAxis;\n\tconst n = Math.max(1, labels.length);\n\tconst titleBand = fs * 1.3 + 6;\n\tconst labelTarget = input.labelTarget ?? ((i: number) => ({ kind: 'category', index: i }) as ChartSelection);\n\n\tconst estimate = (v: ValueAxisInput, length: number) => {\n\t\tconst s = axisScale(v.lo, v.hi, length, v.axis, v.frozen, v.headroom);\n\t\tconst info = scaleInfo(s, [0, 1], v.format);\n\t\treturn Math.max(0, ...s.ticks.map((t) => measure(info.tickText(t), fs)));\n\t};\n\tconst valueLen0 = horizontal ? area.width : area.height;\n\tconst valueLabelWidth = primary.axis.show ? estimate(primary, valueLen0) : 0;\n\tconst valueLabelWidth2 = secondary?.axis.show ? estimate(secondary, valueLen0) : 0;\n\tconst catLabelWidth = Math.max(0, ...labels.map((c) => measure(c, fs)));\n\n\tlet left: number;\n\tlet bottom: number;\n\tlet right = 8;\n\tlet top = horizontal ? 4 : fs / 2 + 2;\n\tlet rotate = 0;\n\tlet dateLabels: ReturnType<typeof chartDateLabels> = null;\n\tlet rotatedMax = 0;\n\tif (horizontal) {\n\t\tleft = (xAxis.show ? Math.min(catLabelWidth, area.width * 0.35) + 8 : 2) + (xAxis.title ? titleBand : 0);\n\t\tbottom = (primary.axis.show ? fs * 1.6 : 2) + (primary.axis.title ? titleBand : 0);\n\t\tright = Math.max(8, valueLabelWidth / 2);\n\t\tif (secondary) top = (secondary.axis.show ? fs * 1.6 : 4) + (secondary.axis.title ? titleBand : 0);\n\t} else {\n\t\tleft = (primary.axis.show ? valueLabelWidth + 8 : 2) + (primary.axis.title ? titleBand : 0);\n\t\tif (secondary) right = (secondary.axis.show ? valueLabelWidth2 + 8 : 8) + (secondary.axis.title ? titleBand : 0);\n\t\tconst band = (area.width - left - right) / n;\n\t\tif (xAxis.show && xAxis.kind === 'date') dateLabels = chartDateLabels(labels, band, (t) => measure(t, fs));\n\t\tif (xAxis.show && !dateLabels && catLabelWidth > band * 0.92) rotate = -40;\n\t\tconst sin = Math.sin(Math.PI / 4.5);\n\t\tconst catHeight = rotate ? Math.min(catLabelWidth * sin + fs, area.height * 0.4) : fs * 1.6;\n\t\tif (rotate) rotatedMax = (catHeight - fs) / sin;\n\t\tbottom = (xAxis.show ? catHeight : 2) + (xAxis.title ? titleBand : 0);\n\t}\n\tconst plot: ChartRect = {\n\t\tx: area.x + left,\n\t\ty: area.y + top,\n\t\twidth: Math.max(1, area.width - left - right),\n\t\theight: Math.max(1, area.height - bottom - top)\n\t};\n\tconst valueLen = horizontal ? plot.width : plot.height;\n\tconst range: [number, number] = horizontal ? [plot.x, plot.x + plot.width] : [plot.y + plot.height, plot.y];\n\tconst value = scaleInfo(axisScale(primary.lo, primary.hi, valueLen, primary.axis, primary.frozen, primary.headroom), range, primary.format);\n\tconst value2 = secondary ? scaleInfo(axisScale(secondary.lo, secondary.hi, valueLen, secondary.axis, secondary.frozen, secondary.headroom), range, secondary.format) : undefined;\n\n\tconst catStart = horizontal ? plot.y : plot.x;\n\tconst catLen = horizontal ? plot.height : plot.width;\n\tconst band = catLen / n;\n\tconst catCenter = (i: number) => catStart + band * (i + 0.5);\n\tconst map = (p: Pt): [number, number] => (horizontal ? [p.v, p.c] : [p.c, p.v]);\n\n\t// Grid lines\n\tconst gridAt = (info: ScaleInfo, key: string) =>\n\t\tinfo.scale.ticks.forEach((t, i) => {\n\t\t\tconst p = info.px(t);\n\t\t\tconst [x1, y1] = map({ c: catStart, v: p });\n\t\t\tconst [x2, y2] = map({ c: catStart + catLen, v: p });\n\t\t\tmarks.push({ kind: 'line', key: `grid-${key}-${i}`, role: 'grid', x1, y1, x2, y2, stroke: theme.grid, strokeWidth: 1 });\n\t\t});\n\tif (primary.axis.gridlines) gridAt(value, 'y');\n\tif (value2 && secondary!.axis.gridlines) gridAt(value2, 'y2');\n\tif (xAxis.gridlines) {\n\t\tfor (let i = 1; i < n; i++) {\n\t\t\tconst c = catStart + band * i;\n\t\t\tconst [x1, y1] = map({ c, v: range[0] });\n\t\t\tconst [x2, y2] = map({ c, v: range[1] });\n\t\t\tmarks.push({ kind: 'line', key: `grid-x-${i}`, role: 'grid', x1, y1, x2, y2, stroke: theme.grid, strokeWidth: 1 });\n\t\t}\n\t}\n\n\t// Value tick labels\n\tconst ticksFor = (info: ScaleInfo, v: ValueAxisInput, far: boolean) => {\n\t\tif (!v.axis.show) return;\n\t\tconst target: ChartSelection = { kind: 'axis', axis: v.key };\n\t\tinfo.scale.ticks.forEach((t, i) => {\n\t\t\tconst p = info.px(t);\n\t\t\tconst label = info.tickText(t);\n\t\t\tconst key = `tick-${v.key}-${i}`;\n\t\t\tif (horizontal) {\n\t\t\t\tif (far) marks.push({ kind: 'text', key, role: 'tick-label', x: p, y: plot.y - 6, text: label, anchor: 'middle', baseline: 'bottom', size: fs, fill: theme.mutedText, target });\n\t\t\t\telse marks.push({ kind: 'text', key, role: 'tick-label', x: p, y: plot.y + plot.height + 6, text: label, anchor: 'middle', baseline: 'top', size: fs, fill: theme.mutedText, target });\n\t\t\t} else if (far) marks.push({ kind: 'text', key, role: 'tick-label', x: plot.x + plot.width + 8, y: p, text: label, anchor: 'start', baseline: 'middle', size: fs, fill: theme.mutedText, target });\n\t\t\telse marks.push({ kind: 'text', key, role: 'tick-label', x: plot.x - 8, y: p, text: label, anchor: 'end', baseline: 'middle', size: fs, fill: theme.mutedText, target });\n\t\t});\n\t};\n\tticksFor(value, primary, false);\n\tif (value2) ticksFor(value2, secondary!, true);\n\n\t// Category axis line and labels\n\tconst xTarget: ChartSelection = { kind: 'axis', axis: 'x' };\n\tif (xAxis.show) {\n\t\tconst [x1, y1] = map({ c: catStart, v: value.zero });\n\t\tconst [x2, y2] = map({ c: catStart + catLen, v: value.zero });\n\t\tmarks.push({ kind: 'line', key: 'axis-x', role: 'axis', x1, y1, x2, y2, stroke: theme.axis, strokeWidth: 1, target: xTarget });\n\t\tconst every = dateLabels ? 1 : horizontal ? Math.max(1, Math.ceil((fs * 1.3) / band)) : rotate ? Math.max(1, Math.ceil((fs * 1.4) / band)) : 1;\n\t\tlet hidden = 0;\n\t\tlet truncated = 0;\n\t\tlabels.forEach((c, i) => {\n\t\t\tconst text0 = dateLabels ? dateLabels.labels.get(i) : i % every ? undefined : c;\n\t\t\tif (text0 === undefined) {\n\t\t\t\thidden++;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst target = labelTarget(i);\n\t\t\tconst selected = target ? ctx.isSelected(target) : false;\n\t\t\tconst fill = selected ? theme.selection : theme.mutedText;\n\t\t\tconst extra = target ? { target, selected } : {};\n\t\t\tif (horizontal) {\n\t\t\t\tconst maxW = left - (xAxis.title ? titleBand : 0) - 8;\n\t\t\t\tconst text = truncate(text0, maxW, fs, measure);\n\t\t\t\tif (text !== text0) truncated++;\n\t\t\t\tmarks.push({ kind: 'text', key: `tick-x-${i}`, role: 'tick-label', x: plot.x - 8, y: catCenter(i), text, anchor: 'end', baseline: 'middle', size: fs, fill, ...extra });\n\t\t\t} else if (rotate) {\n\t\t\t\t// Rotated labels run down and to the left: keep them inside the chart.\n\t\t\t\tconst room = (catCenter(i) - 2) / Math.cos(Math.PI / 4.5);\n\t\t\t\tconst text = truncate(text0, Math.max(fs, Math.min(rotatedMax, room)), fs, measure);\n\t\t\t\tif (text !== text0) truncated++;\n\t\t\t\tmarks.push({ kind: 'text', key: `tick-x-${i}`, role: 'tick-label', x: catCenter(i), y: plot.y + plot.height + 8, text, anchor: 'end', baseline: 'middle', size: fs, fill, rotate, ...extra });\n\t\t\t} else {\n\t\t\t\tconst text = dateLabels ? text0 : truncate(text0, Math.max(band - 4, fs * 2), fs, measure);\n\t\t\t\tif (text !== text0) truncated++;\n\t\t\t\tmarks.push({ kind: 'text', key: `tick-x-${i}`, role: 'tick-label', x: catCenter(i), y: plot.y + plot.height + 6, text, anchor: dateLabels ? 'start' : 'middle', baseline: 'top', size: fs, fill, ...extra });\n\t\t\t}\n\t\t});\n\t\tif (dateLabels) {\n\t\t\t// Date labels sit at the start of their bucket; mark it with a short tick.\n\t\t\tfor (const i of dateLabels.indexes) {\n\t\t\t\tconst x = catCenter(i);\n\t\t\t\tmarks.push({ kind: 'line', key: `tick-mark-x-${i}`, role: 'tick', x1: x, y1: plot.y + plot.height, x2: x, y2: plot.y + plot.height + 4, stroke: theme.axis, strokeWidth: 1 });\n\t\t\t}\n\t\t}\n\t\tif (hidden && !dateLabels) ctx.warn('labels-hidden', `${hidden} of ${labels.length} category labels are hidden so the rest do not overlap.`);\n\t\tif (truncated) ctx.warn('labels-truncated', `${truncated} category label${truncated === 1 ? ' was' : 's were'} shortened to fit.`);\n\t}\n\n\t// Axis titles: category title beside the category labels, value titles beside their ticks.\n\tconst yTarget: ChartSelection = { kind: 'axis', axis: 'y' };\n\tconst valueTitle = primary.axis.title;\n\tconst catTitle = xAxis.title;\n\tconst bottomTitle = horizontal ? valueTitle : catTitle;\n\tconst leftTitle = horizontal ? catTitle : valueTitle;\n\tif (bottomTitle)\n\t\tmarks.push({ kind: 'text', key: `axis-title-${horizontal ? 'y' : 'x'}`, role: 'axis-title', x: plot.x + plot.width / 2, y: area.y + area.height, text: bottomTitle, anchor: 'middle', baseline: 'bottom', size: fs + 1, weight: 500, fill: theme.text, target: horizontal ? yTarget : xTarget });\n\tif (leftTitle)\n\t\tmarks.push({ kind: 'text', key: `axis-title-${horizontal ? 'x' : 'y'}`, role: 'axis-title', x: area.x, y: plot.y + plot.height / 2, text: leftTitle, anchor: 'middle', baseline: 'top', size: fs + 1, weight: 500, fill: theme.text, rotate: -90, target: horizontal ? xTarget : yTarget });\n\tif (secondary?.axis.title) {\n\t\tconst target: ChartSelection = { kind: 'axis', axis: 'y2' };\n\t\tif (horizontal) marks.push({ kind: 'text', key: 'axis-title-y2', role: 'axis-title', x: plot.x + plot.width / 2, y: area.y, text: secondary.axis.title, anchor: 'middle', baseline: 'top', size: fs + 1, weight: 500, fill: theme.text, target });\n\t\telse marks.push({ kind: 'text', key: 'axis-title-y2', role: 'axis-title', x: area.x + area.width, y: plot.y + plot.height / 2, text: secondary.axis.title, anchor: 'middle', baseline: 'top', size: fs + 1, weight: 500, fill: theme.text, rotate: 90, target });\n\t}\n\t// The selected axis gets an outline around its labels.\n\tfor (const axis of ['x', 'y', 'y2'] as const) {\n\t\tif (!ctx.isSelected({ kind: 'axis', axis })) continue;\n\t\tif (axis === 'y2' && !secondary) continue;\n\t\tlet box: ChartRect;\n\t\tif (axis === 'y2') box = horizontal ? { x: plot.x - 4, y: area.y, width: plot.width + 8, height: plot.y - area.y - 2 } : { x: plot.x + plot.width + 2, y: plot.y - fs / 2, width: area.x + area.width - plot.x - plot.width - 2, height: plot.height + fs };\n\t\telse {\n\t\t\tconst onLeft = (axis === 'y') !== horizontal;\n\t\t\tbox = onLeft\n\t\t\t\t? { x: area.x, y: plot.y - fs / 2, width: left - 2, height: plot.height + fs }\n\t\t\t\t: { x: plot.x - 4, y: plot.y + plot.height + 2, width: plot.width + 8, height: area.y + area.height - plot.y - plot.height - 2 };\n\t\t}\n\t\tctx.outline(`axis-${axis}-selection`, box);\n\t}\n\treturn { plot, horizontal, n, band, catStart, catLen, catCenter, map, value, value2 };\n}\n\nexport const toValueScale = (info: ScaleInfo, axis: 'x' | 'y'): ChartValueScale => ({ axis, domain: [info.scale.min, info.scale.max], range: info.range, ticks: info.scale.ticks });\n\ninterface Stack {\n\t/** Per category: [bottom, top] of this series' segment, or null for a gap. */\n\tsegments: ([number, number] | null)[];\n}\n\nconst order = (t: ChartSeriesType) => (t === 'area' ? 0 : t === 'bar' ? 1 : t === 'line' ? 2 : 3);\n\n/** A point marker: a circle mark, or a path for other shapes. */\nexport function pointMark(base: { key: string; target?: ChartSelection; selected?: boolean; opacity?: number }, shape: ChartMarker | 'hit', cx: number, cy: number, r: number, fill: string, stroke?: string, strokeWidth?: number): ChartMark {\n\tif (shape === 'circle' || shape === 'hit' || shape === 'none') return { kind: 'circle', role: 'point', ...base, cx, cy, r, fill, ...(stroke ? { stroke, strokeWidth } : {}) };\n\tconst closed = shape !== 'cross';\n\treturn { kind: 'path', role: 'point', ...base, d: markerPath(shape, cx, cy, r), fill: closed ? fill : 'none', stroke: stroke ?? fill, strokeWidth: strokeWidth ?? 2 };\n}\n\ninterface TrendLabel {\n\ttext: string;\n\tcolor: string;\n\ttarget: ChartSelection;\n}\n\n/**\n * Draws the trendlines of one series: `points` in data units, `toPx` maps them to the scene,\n * `xRange` is the fitted span (extended by forward/backward).\n */\nfunction drawTrendlines(ctx: LayoutCtx, plot: ChartRect, series: ChartSeries, color: string, points: [number, number][], toPx: (x: number, y: number) => [number, number], labels: TrendLabel[]) {\n\t(series.trendlines ?? []).forEach((t, ti) => {\n\t\tconst target: ChartSelection = { kind: 'trendline', seriesId: series.id, index: ti };\n\t\tconst selected = ctx.isSelected(target);\n\t\tconst stroke = t.color ?? color;\n\t\tlet px: [number, number][] = [];\n\t\tif (t.type === 'moving-average') px = chartMovingAverage(points, t.period ?? 2).map(([x, y]) => toPx(x, y));\n\t\telse {\n\t\t\tconst fit = fitChartTrendline(t, points);\n\t\t\tif (!fit) {\n\t\t\t\tctx.warn('values-skipped', `The ${t.type} trendline on \"${series.name}\" could not be fitted (too few usable points).`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst xs = points.map((p) => p[0]);\n\t\t\tconst lo = Math.min(...xs) - (t.backward ?? 0);\n\t\t\tconst hi = Math.max(...xs) + (t.forward ?? 0);\n\t\t\tconst steps = t.type === 'linear' ? 1 : 200;\n\t\t\tfor (let k = 0; k <= steps; k++) {\n\t\t\t\tconst x = lo + ((hi - lo) * k) / steps;\n\t\t\t\tconst y = fit.evaluate(x);\n\t\t\t\tif (y !== null) px.push(toPx(x, y));\n\t\t\t}\n\t\t\tif (t.showEquation) labels.push({ text: fit.equation, color: stroke, target });\n\t\t\tif (t.showR2) labels.push({ text: fit.r2 === null ? 'R² = #N/A' : `R² = ${trendNumberText(fit.r2)}`, color: stroke, target });\n\t\t}\n\t\tconst d = clippedPolyline(px, plot);\n\t\tif (!d) return;\n\t\tctx.marks.push({ kind: 'path', key: `trendline-${series.id}-${ti}`, role: 'trendline', d, fill: 'none', stroke, strokeWidth: selected ? 3 : 1.5, dash: '6 4', target, selected });\n\t});\n}\n\nfunction drawTrendLabels(ctx: LayoutCtx, plot: ChartRect, labels: TrendLabel[]) {\n\tlabels.forEach((l, i) =>\n\t\tctx.marks.push({ kind: 'text', key: `trendline-label-${i}`, role: 'trendline-label', x: plot.x + plot.width - 4, y: plot.y + 4 + i * (ctx.fs + 3), text: l.text, anchor: 'end', baseline: 'top', size: ctx.fs - 1, fill: l.color, target: l.target })\n\t);\n}\n\n/** Where a bar's value label goes, or null when it does not fit. */\nfunction barLabel(ctx: LayoutCtx, text: string, rect: ChartRect, horizontal: boolean, positive: boolean, stacked: boolean, fill: string, plot: ChartRect): { x: number; y: number; anchor: 'start' | 'middle' | 'end'; baseline: 'top' | 'middle' | 'bottom'; fill: string } | null {\n\tconst { measure, fs, spec, theme } = ctx;\n\tconst size = fs - 1;\n\tconst tw = measure(text, size);\n\tconst th = size + 2;\n\tconst along = horizontal ? rect.width : rect.height;\n\tconst across = horizontal ? rect.height : rect.width;\n\tconst need = horizontal ? tw + 6 : th + 2;\n\tconst fitsInside = along >= need && across >= (horizontal ? th : Math.min(tw, across + 1));\n\tlet position = spec.labelPosition;\n\tif (position === 'auto') position = stacked ? 'center' : 'outside';\n\tconst cx = rect.x + rect.width / 2;\n\tconst cy = rect.y + rect.height / 2;\n\tconst inner = contrastingTextColor(fill);\n\tconst centre = () => ({ x: cx, y: cy, anchor: 'middle' as const, baseline: 'middle' as const, fill: inner });\n\tconst insideEnd = () =>\n\t\thorizontal\n\t\t\t? { x: positive ? rect.x + rect.width - 4 : rect.x + 4, y: cy, anchor: (positive ? 'end' : 'start') as 'end' | 'start', baseline: 'middle' as const, fill: inner }\n\t\t\t: { x: cx, y: positive ? rect.y + 3 : rect.y + rect.height - 3, anchor: 'middle' as const, baseline: (positive ? 'top' : 'bottom') as 'top' | 'bottom', fill: inner };\n\tconst outside = () =>\n\t\thorizontal\n\t\t\t? { x: positive ? rect.x + rect.width + 4 : rect.x - 4, y: cy, anchor: (positive ? 'start' : 'end') as 'start' | 'end', baseline: 'middle' as const, fill: theme.text }\n\t\t\t: { x: cx, y: positive ? rect.y - 4 : rect.y + rect.height + 4, anchor: 'middle' as const, baseline: (positive ? 'bottom' : 'top') as 'bottom' | 'top', fill: theme.text };\n\tconst outsideFits = () => {\n\t\tif (horizontal) return positive ? rect.x + rect.width + 4 + tw <= plot.x + plot.width + 8 : rect.x - 4 - tw >= plot.x - 8;\n\t\treturn positive ? rect.y - 4 - th >= plot.y - fs : rect.y + rect.height + 4 + th <= plot.y + plot.height + fs;\n\t};\n\tif (position === 'center') return fitsInside ? centre() : null;\n\tif (position === 'inside') return fitsInside ? insideEnd() : stacked ? null : outside();\n\t// outside\n\tif (stacked) return fitsInside ? centre() : null;\n\tif (outsideFits()) return outside();\n\treturn fitsInside ? insideEnd() : null;\n}\n\n/** Bar, line, area and scatter series over categories (a combo chart when they mix). */\nexport function layoutCartesian(ctx: LayoutCtx, area: ChartRect, visible: ChartSeries[]): FamilyResult {\n\tconst { spec, theme, marks, measure, fs } = ctx;\n\tconst horizontal = spec.orientation === 'horizontal';\n\tconst n = spec.categories.length;\n\tconst types = new Map<string, ChartSeriesType>(visible.map((s) => [s.id, chartSeriesType(spec, s)]));\n\tconst stacked = spec.stacking !== 'none';\n\tconst percent = spec.stacking === 'percent';\n\tconst onSecondary = (s: ChartSeries) => s.axis === 'secondary';\n\tconst useSecondary = visible.some(onSecondary);\n\n\t// Stack bars together and areas together, per axis (lines and scatter are never stacked).\n\tconst stacks = new Map<string, Stack>();\n\tif (stacked) {\n\t\tfor (const kind of ['bar', 'area'] as const) {\n\t\t\tfor (const secondary of [false, true]) {\n\t\t\t\tconst group = visible.filter((s) => types.get(s.id) === kind && onSecondary(s) === secondary);\n\t\t\t\tif (!group.length) continue;\n\t\t\t\tconst totals = Array.from({ length: n }, (_, i) => group.reduce((t, s) => t + Math.abs(s.values[i] ?? 0), 0));\n\t\t\t\tconst pos = Array(n).fill(0);\n\t\t\t\tconst neg = Array(n).fill(0);\n\t\t\t\tfor (const s of group) {\n\t\t\t\t\tconst segments = s.values.map((raw, i) => {\n\t\t\t\t\t\tif (raw === null && kind === 'bar') return null;\n\t\t\t\t\t\tlet v = raw ?? 0;\n\t\t\t\t\t\tif (percent) v = totals[i] ? v / totals[i] : 0;\n\t\t\t\t\t\tconst base = v >= 0 ? pos[i] : neg[i];\n\t\t\t\t\t\tconst seg: [number, number] = [base, base + v];\n\t\t\t\t\t\tif (v >= 0) pos[i] += v;\n\t\t\t\t\t\telse neg[i] += v;\n\t\t\t\t\t\treturn seg;\n\t\t\t\t\t});\n\t\t\t\t\tstacks.set(s.id, { segments });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tconst range = (secondary: boolean) => {\n\t\tlet lo = Infinity;\n\t\tlet hi = -Infinity;\n\t\tconst see = (v: number) => {\n\t\t\tlo = Math.min(lo, v);\n\t\t\thi = Math.max(hi, v);\n\t\t};\n\t\tconst group = visible.filter((s) => onSecondary(s) === secondary);\n\t\tfor (const s of group) {\n\t\t\tconst stack = stacks.get(s.id);\n\t\t\tif (stack) stack.segments.forEach((seg) => seg && (see(seg[0]), see(seg[1])));\n\t\t\telse s.values.forEach((v) => v !== null && Number.isFinite(v) && see(v));\n\t\t}\n\t\tif (!Number.isFinite(lo)) {\n\t\t\tlo = 0;\n\t\t\thi = 1;\n\t\t}\n\t\tif (group.some((s) => types.get(s.id) === 'bar' || types.get(s.id) === 'area')) {\n\t\t\tlo = Math.min(lo, 0);\n\t\t\thi = Math.max(hi, 0);\n\t\t}\n\t\tconst lineLike = group.some((s) => types.get(s.id) === 'line' || types.get(s.id) === 'scatter');\n\t\treturn { lo, hi, lineLike };\n\t};\n\tconst pctFormat = (f: ChartNumberFormat): ChartNumberFormat => (percent && !f.style && !f.code ? { ...f, style: 'percent' } : f);\n\tconst p = range(false);\n\tconst s2 = useSecondary ? range(true) : null;\n\tconst frame = categoryFrame(ctx, area, {\n\t\tlabels: spec.categories,\n\t\thorizontal,\n\t\tprimary: { lo: p.lo, hi: p.hi, axis: spec.yAxis, format: pctFormat(spec.yAxis.format), frozen: ctx.options.valueDomain, headroom: p.lineLike, key: 'y' },\n\t\tsecondary: s2 ? { lo: s2.lo, hi: s2.hi, axis: spec.yAxis2, format: pctFormat(spec.yAxis2.format), frozen: ctx.options.valueDomain2, headroom: s2.lineLike, key: 'y2' } : undefined\n\t});\n\tconst { plot, band, catCenter, map, catStart } = frame;\n\tconst infoOf = (s: ChartSeries) => (onSecondary(s) && frame.value2 ? frame.value2 : frame.value);\n\n\t// Bars: groups take (1 - gapWidth) of each category; bars in a group overlap by `overlap`.\n\tconst bars = visible.filter((s) => types.get(s.id) === 'bar');\n\tconst barAxes = [...new Set(bars.map((s) => onSecondary(s)))];\n\tconst slots = stacked ? barAxes.length : bars.length;\n\tconst groupWidth = band * (1 - spec.gapWidth);\n\tconst o = Math.max(-1, Math.min(0.99, spec.overlap));\n\tconst barWidth = slots ? groupWidth / (slots - (slots - 1) * o) : 0;\n\tconst slotOf = (s: ChartSeries) => (stacked ? barAxes.indexOf(onSecondary(s)) : bars.indexOf(s));\n\tconst labelMarks: ChartMark[] = [];\n\tconst trendLabels: TrendLabel[] = [];\n\tlet hiddenLabels = 0;\n\tconst trendsAllowed = chartSupportsTrendlines(spec.type, spec.stacking);\n\n\tconst drawOrder = [...visible].sort((a, b) => order(types.get(a.id)!) - order(types.get(b.id)!));\n\tfor (const s of drawOrder) {\n\t\tconst si = spec.series.indexOf(s);\n\t\tconst color = chartSeriesColor(spec, si);\n\t\tconst type = types.get(s.id)!;\n\t\tconst stack = stacks.get(s.id);\n\t\tconst selSeries = ctx.seriesSelected(s.id);\n\t\tconst seriesTarget: ChartSelection = { kind: 'series', seriesId: s.id };\n\t\tconst info = infoOf(s);\n\t\tconst valuePx = info.px;\n\t\tconst labelsOn = (s.dataLabels ?? spec.dataLabels) && s.values.length <= MAX_CHART_DATA_LABELS;\n\t\tif ((s.dataLabels ?? spec.dataLabels) && !labelsOn) ctx.warn('data-labels-hidden', `Data labels are off for \"${s.name}\": it has more than ${MAX_CHART_DATA_LABELS} points.`);\n\t\tconst labelFormat = s.format ?? info.format;\n\t\tconst dataLabel = (v: number) => formatChartNumber(v, labelFormat, labelFormat.decimals ?? undefined);\n\n\t\tif (type === 'bar') {\n\t\t\tconst slot = slotOf(s);\n\t\t\ts.values.forEach((raw, i) => {\n\t\t\t\tconst seg = stack ? stack.segments[i] : raw === null ? null : ([0, raw] as [number, number]);\n\t\t\t\tif (!seg || raw === null || !Number.isFinite(raw)) return;\n\t\t\t\tconst a = valuePx(seg[0]);\n\t\t\t\tconst b = valuePx(seg[1]);\n\t\t\t\tconst c0 = catCenter(i) - groupWidth / 2 + slot * barWidth * (1 - o);\n\t\t\t\tconst cw = barWidth;\n\t\t\t\tconst target: ChartSelection = { kind: 'point', seriesId: s.id, index: i };\n\t\t\t\tconst selected = ctx.isSelected(target);\n\t\t\t\tconst [x, y] = map({ c: c0, v: Math.min(a, b) });\n\t\t\t\tconst rect = horizontal ? { x: Math.min(a, b), y: c0, width: Math.abs(b - a), height: cw } : { x, y, width: cw, height: Math.abs(b - a) };\n\t\t\t\tconst fill = chartPointColor(spec, si, i);\n\t\t\t\tmarks.push({\n\t\t\t\t\tkind: 'rect',\n\t\t\t\t\tkey: `bar-${s.id}-${i}`,\n\t\t\t\t\trole: 'bar',\n\t\t\t\t\t...rect,\n\t\t\t\t\tfill,\n\t\t\t\t\tradius: Math.min(2, cw / 4),\n\t\t\t\t\ttarget,\n\t\t\t\t\tselected: selected || selSeries,\n\t\t\t\t\t...(selSeries ? { stroke: theme.selection, strokeWidth: 1.5 } : {})\n\t\t\t\t});\n\t\t\t\tif (selected) ctx.outline(`bar-${s.id}-${i}-selection`, { x: rect.x - 2, y: rect.y - 2, width: rect.width + 4, height: rect.height + 4 });\n\t\t\t\tif (labelsOn) {\n\t\t\t\t\tconst v = percent ? seg[1] - seg[0] : raw;\n\t\t\t\t\tconst text = dataLabel(v);\n\t\t\t\t\tconst at = barLabel(ctx, text, rect, horizontal, seg[1] >= seg[0], stacked, fill, plot);\n\t\t\t\t\tif (!at) {\n\t\t\t\t\t\thiddenLabels++;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tlabelMarks.push({ kind: 'text', key: `label-${s.id}-${i}`, role: 'data-label', ...at, text, size: fs - 1, target });\n\t\t\t\t}\n\t\t\t});\n\t\t} else {\n\t\t\t// Line, area and scatter share the point positions.\n\t\t\tconst curve = s.curve ?? spec.curve;\n\t\t\tconst pointValue = (i: number): number | null => {\n\t\t\t\tconst v = stack ? (stack.segments[i]?.[1] ?? null) : s.values[i];\n\t\t\t\treturn v !== null && Number.isFinite(v) ? v : null;\n\t\t\t};\n\t\t\tconst runs: Pt[][] = [];\n\t\t\tlet run: Pt[] = [];\n\t\t\tfor (let i = 0; i < n; i++) {\n\t\t\t\tconst v = pointValue(i);\n\t\t\t\tif (v === null) {\n\t\t\t\t\tif (run.length) runs.push(run);\n\t\t\t\t\trun = [];\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\trun.push({ c: catCenter(i), v: valuePx(v) });\n\t\t\t}\n\t\t\tif (run.length) runs.push(run);\n\t\t\tconst thin = (r: Pt[]) => {\n\t\t\t\tif (r.length <= MAX_CHART_LINE_POINTS) return r;\n\t\t\t\tconst pts = decimateChartPoints(r.map((p) => ({ x: p.c, y: p.v, p })), MAX_CHART_LINE_POINTS).map((q) => q.p);\n\t\t\t\tctx.warn('points-decimated', `\"${s.name}\" has ${r.length} points; its line is drawn with ${pts.length} (one column of pixels each).`);\n\t\t\t\treturn pts;\n\t\t\t};\n\t\t\tconst drawn = runs.map(thin);\n\n\t\t\tif (type === 'area') {\n\t\t\t\t// Stacked areas fill down to the series below; others down to zero.\n\t\t\t\tconst parts = drawn.map((r) => {\n\t\t\t\t\tconst baseline = stack\n\t\t\t\t\t\t? r.map((pt) => ({ c: pt.c, v: valuePx(stack.segments[Math.round((pt.c - catStart) / band - 0.5)]?.[0] ?? 0) })).reverse()\n\t\t\t\t\t\t: [\n\t\t\t\t\t\t\t\t{ c: r[r.length - 1].c, v: info.zero },\n\t\t\t\t\t\t\t\t{ c: r[0].c, v: info.zero }\n\t\t\t\t\t\t\t];\n\t\t\t\t\treturn `${curvePath(r, curve, map)}${stack ? curvePath(baseline, curve, map, false) : baseline.map((pt) => `L${map(pt).map(r2).join(',')}`).join('')}Z`;\n\t\t\t\t});\n\t\t\t\tmarks.push({ kind: 'path', key: `area-${s.id}`, role: 'area', d: parts.join(''), fill: color, opacity: 0.22, target: seriesTarget, selected: selSeries });\n\t\t\t}\n\t\t\tif (type === 'line' || type === 'area') {\n\t\t\t\tmarks.push({ kind: 'path', key: `line-${s.id}`, role: 'line', d: drawn.map((r) => curvePath(r, curve, map)).join(''), fill: 'none', stroke: color, strokeWidth: selSeries ? 3.5 : 2, target: seriesTarget, selected: selSeries });\n\t\t\t}\n\t\t\tconst marker: ChartMarker = s.marker ?? (type === 'scatter' || spec.points ? 'circle' : 'none');\n\t\t\tconst showDots = marker !== 'none';\n\t\t\tconst thinMarkers = n > MAX_CHART_MARKERS;\n\t\t\tconst cells = new Set<string>();\n\t\t\tfor (let i = 0; i < n; i++) {\n\t\t\t\tconst v = pointValue(i);\n\t\t\t\tif (v === null || s.values[i] === null) continue;\n\t\t\t\tconst [cx, cy] = map({ c: catCenter(i), v: valuePx(v) });\n\t\t\t\tif (thinMarkers) {\n\t\t\t\t\tconst cell = `${Math.round(cx / 2)},${Math.round(cy / 2)}`;\n\t\t\t\t\tif (cells.has(cell)) continue;\n\t\t\t\t\tcells.add(cell);\n\t\t\t\t}\n\t\t\t\tconst target: ChartSelection = { kind: 'point', seriesId: s.id, index: i };\n\t\t\t\tconst selected = ctx.isSelected(target);\n\t\t\t\tconst r = type === 'scatter' ? 4.5 : 3.5;\n\t\t\t\tconst pointColor = chartPointColor(spec, si, i);\n\t\t\t\tconst fill = showDots ? (type === 'scatter' ? pointColor : theme.surface) : 'transparent';\n\t\t\t\t// Without markers the point still gets a transparent target so it can be picked and dragged.\n\t\t\t\tmarks.push(\n\t\t\t\t\tpointMark({ key: `point-${s.id}-${i}`, target, selected: selected || selSeries, opacity: type === 'scatter' ? 0.85 : undefined }, showDots ? marker : 'hit', cx, cy, showDots ? r : 6, fill, showDots ? pointColor : undefined, 2)\n\t\t\t\t);\n\t\t\t\tif (selected) marks.push({ kind: 'circle', key: `point-${s.id}-${i}-selection`, role: 'selection', cx, cy, r: r + 3.5, fill: 'none', stroke: theme.selection, strokeWidth: 1.5 });\n\t\t\t\tif (labelsOn) {\n\t\t\t\t\tconst raw = s.values[i]!;\n\t\t\t\t\tconst shown = percent && stack ? stack.segments[i]![1] - stack.segments[i]![0] : raw;\n\t\t\t\t\tlabelMarks.push({\n\t\t\t\t\t\tkind: 'text',\n\t\t\t\t\t\tkey: `label-${s.id}-${i}`,\n\t\t\t\t\t\trole: 'data-label',\n\t\t\t\t\t\tx: horizontal ? cx + 8 : cx,\n\t\t\t\t\t\ty: horizontal ? cy : cy - 8,\n\t\t\t\t\t\ttext: dataLabel(shown),\n\t\t\t\t\t\tanchor: horizontal ? 'start' : 'middle',\n\t\t\t\t\t\tbaseline: horizontal ? 'middle' : 'bottom',\n\t\t\t\t\t\tsize: fs - 1,\n\t\t\t\t\t\tfill: theme.text,\n\t\t\t\t\t\ttarget\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (trendsAllowed && s.trendlines?.length) {\n\t\t\t// Category charts fit against x = 1, 2, 3, … (as spreadsheets do).\n\t\t\tconst points: [number, number][] = [];\n\t\t\ts.values.forEach((v, i) => v !== null && Number.isFinite(v) && points.push([i + 1, v]));\n\t\t\tconst toPx = (x: number, y: number) => map({ c: catStart + band * (x - 0.5), v: info.raw(y) });\n\t\t\tdrawTrendlines(ctx, plot, s, color, points, toPx, trendLabels);\n\t\t}\n\t}\n\tmarks.push(...labelMarks);\n\tdrawTrendLabels(ctx, plot, trendLabels);\n\tif (hiddenLabels) ctx.warn('data-labels-hidden', `${hiddenLabels} data label${hiddenLabels === 1 ? '' : 's'} did not fit and ${hiddenLabels === 1 ? 'is' : 'are'} hidden.`);\n\treturn {\n\t\tplot,\n\t\tvalueScale: toValueScale(frame.value, horizontal ? 'x' : 'y'),\n\t\t...(frame.value2 ? { valueScale2: toValueScale(frame.value2, horizontal ? 'x' : 'y') } : {}),\n\t\tpercent\n\t};\n}\n\n/** Scatter (with x values) and bubble charts: numeric x against numeric y. */\nexport function layoutXy(ctx: LayoutCtx, area: ChartRect, visible: ChartSeries[]): FamilyResult {\n\tconst { spec, theme, marks, measure, fs } = ctx;\n\tconst bubble = spec.type === 'bubble';\n\tconst pointsOf = (s: ChartSeries) =>\n\t\ts.values\n\t\t\t.map((y, i) => ({ i, x: s.x?.[i] ?? (bubble || !s.x ? i + 1 : null), y, size: s.sizes?.[i] ?? null }))\n\t\t\t.filter((p): p is { i: number; x: number; y: number; size: number | null } => p.x !== null && p.y !== null && Number.isFinite(p.x) && Number.isFinite(p.y));\n\tconst all = visible.map((s) => ({ s, pts: pointsOf(s) }));\n\tlet xlo = Infinity;\n\tlet xhi = -Infinity;\n\tlet ylo = Infinity;\n\tlet yhi = -Infinity;\n\tlet maxSize = 0;\n\tfor (const { s, pts } of all) {\n\t\tfor (const p of pts) {\n\t\t\txlo = Math.min(xlo, p.x);\n\t\t\txhi = Math.max(xhi, p.x);\n\t\t\tylo = Math.min(ylo, p.y);\n\t\t\tyhi = Math.max(yhi, p.y);\n\t\t\tif (p.size !== null && p.size > maxSize) maxSize = p.size;\n\t\t}\n\t\t// Forecasts widen the x axis.\n\t\tfor (const t of s.trendlines ?? []) {\n\t\t\tif (!pts.length) continue;\n\t\t\txlo = Math.min(xlo, Math.min(...pts.map((p) => p.x)) - (t.backward ?? 0));\n\t\t\txhi = Math.max(xhi, Math.max(...pts.map((p) => p.x)) + (t.forward ?? 0));\n\t\t}\n\t}\n\tif (!Number.isFinite(xlo)) [xlo, xhi, ylo, yhi] = [0, 1, 0, 1];\n\tconst titleBand = fs * 1.3 + 6;\n\tconst yAxis = spec.yAxis;\n\tconst xAxis = spec.xAxis;\n\tconst yEst = axisScale(ylo, yhi, area.height, yAxis, ctx.options.valueDomain, true);\n\tconst yText = (t: number, step: number) => formatChartNumber(t, yAxis.format, yAxis.format.decimals ?? stepDecimals(step));\n\tconst left = (yAxis.show ? Math.max(0, ...yEst.ticks.map((t) => measure(yText(t, yEst.step), fs))) + 8 : 2) + (yAxis.title ? titleBand : 0);\n\tconst bottom = (xAxis.show ? fs * 1.6 : 2) + (xAxis.title ? titleBand : 0);\n\tconst plot: ChartRect = { x: area.x + left, y: area.y + fs / 2 + 2, width: Math.max(1, area.width - left - 12), height: Math.max(1, area.height - bottom - fs / 2 - 2) };\n\tconst maxR = bubble ? Math.max(4, Math.min(plot.width, plot.height) * 0.08) : 0;\n\t// Bubbles get room so they are not cut by the frame.\n\tconst inset = bubble ? maxR : 0;\n\tconst yScale = axisScale(ylo, yhi, plot.height, yAxis, ctx.options.valueDomain, true);\n\tconst xScale = axisScale(xlo, xhi, plot.width, xAxis, ctx.options.xDomain, true);\n\tconst xRange: [number, number] = [plot.x + inset, plot.x + plot.width - inset];\n\tconst yRange: [number, number] = [plot.y + plot.height - inset, plot.y + inset];\n\tconst xRaw = (x: number) => linearMap(x, [xScale.min, xScale.max], xRange);\n\tconst yRaw = (y: number) => linearMap(y, [yScale.min, yScale.max], yRange);\n\tconst xPx = (x: number) => xRaw(Math.min(xScale.max, Math.max(xScale.min, x)));\n\tconst yPx = (y: number) => yRaw(Math.min(yScale.max, Math.max(yScale.min, y)));\n\tconst xTarget: ChartSelection = { kind: 'axis', axis: 'x' };\n\tconst yTarget: ChartSelection = { kind: 'axis', axis: 'y' };\n\n\tif (yAxis.gridlines) yScale.ticks.forEach((t, i) => marks.push({ kind: 'line', key: `grid-y-${i}`, role: 'grid', x1: plot.x, y1: yPx(t), x2: plot.x + plot.width, y2: yPx(t), stroke: theme.grid, strokeWidth: 1 }));\n\tif (xAxis.gridlines) xScale.ticks.forEach((t, i) => marks.push({ kind: 'line', key: `grid-x-${i}`, role: 'grid', x1: xPx(t), y1: plot.y, x2: xPx(t), y2: plot.y + plot.height, stroke: theme.grid, strokeWidth: 1 }));\n\tif (yAxis.show) yScale.ticks.forEach((t, i) => marks.push({ kind: 'text', key: `tick-y-${i}`, role: 'tick-label', x: plot.x - 8, y: yPx(t), text: yText(t, yScale.step), anchor: 'end', baseline: 'middle', size: fs, fill: theme.mutedText, target: yTarget }));\n\tif (xAxis.show) {\n\t\tconst y0 = yScale.min <= 0 && yScale.max >= 0 ? yPx(0) : plot.y + plot.height;\n\t\tmarks.push({ kind: 'line', key: 'axis-x', role: 'axis', x1: plot.x, y1: y0, x2: plot.x + plot.width, y2: y0, stroke: theme.axis, strokeWidth: 1, target: xTarget });\n\t\tconst texts = xScale.ticks.map((t) => formatChartNumber(t, xAxis.format, xAxis.format.decimals ?? stepDecimals(xScale.step)));\n\t\tconst widest = Math.max(0, ...texts.map((t) => measure(t, fs)));\n\t\tconst spacing = xScale.ticks.length > 1 ? Math.abs(xPx(xScale.ticks[1]) - xPx(xScale.ticks[0])) : Infinity;\n\t\tconst every = Math.max(1, Math.ceil((widest + 8) / spacing));\n\t\txScale.ticks.forEach((t, i) => {\n\t\t\tif (i % every) return;\n\t\t\tmarks.push({ kind: 'text', key: `tick-x-${i}`, role: 'tick-label', x: xPx(t), y: plot.y + plot.height + 6, text: texts[i], anchor: 'middle', baseline: 'top', size: fs, fill: theme.mutedText, target: xTarget });\n\t\t});\n\t}\n\tif (xAxis.title) marks.push({ kind: 'text', key: 'axis-title-x', role: 'axis-title', x: plot.x + plot.width / 2, y: area.y + area.height, text: xAxis.title, anchor: 'middle', baseline: 'bottom', size: fs + 1, weight: 500, fill: theme.text, target: xTarget });\n\tif (yAxis.title) marks.push({ kind: 'text', key: 'axis-title-y', role: 'axis-title', x: area.x, y: plot.y + plot.height / 2, text: yAxis.title, anchor: 'middle', baseline: 'top', size: fs + 1, weight: 500, fill: theme.text, rotate: -90, target: yTarget });\n\tfor (const axis of ['x', 'y'] as const) {\n\t\tif (!ctx.isSelected({ kind: 'axis', axis })) continue;\n\t\tctx.outline(`axis-${axis}-selection`, axis === 'y' ? { x: area.x, y: plot.y - fs / 2, width: left - 2, height: plot.height + fs } : { x: plot.x - 4, y: plot.y + plot.height + 2, width: plot.width + 8, height: area.y + area.height - plot.y - plot.height - 2 });\n\t}\n\n\tconst labelMarks: ChartMark[] = [];\n\tconst trendLabels: TrendLabel[] = [];\n\t// Bigger bubbles first, so small ones stay clickable on top.\n\tfor (const { s, pts } of all) {\n\t\tconst si = spec.series.indexOf(s);\n\t\tconst color = chartSeriesColor(spec, si);\n\t\tconst type = chartSeriesType(spec, s);\n\t\tconst selSeries = ctx.seriesSelected(s.id);\n\t\tconst seriesTarget: ChartSelection = { kind: 'series', seriesId: s.id };\n\t\tif (type === 'line' || type === 'area') {\n\t\t\tconst xy = pts.map((p) => ({ x: xPx(p.x), y: yPx(p.y) }));\n\t\t\tconst thinned = xy.length > MAX_CHART_LINE_POINTS ? decimateChartPoints(xy, MAX_CHART_LINE_POINTS) : xy;\n\t\t\tif (thinned.length < xy.length) ctx.warn('points-decimated', `\"${s.name}\" has ${xy.length} points; its line is drawn with ${thinned.length}.`);\n\t\t\tconst d = curvePath(thinned.map((p) => ({ c: p.x, v: p.y })), s.curve ?? spec.curve, (p) => [p.c, p.v]);\n\t\t\tif (d) marks.push({ kind: 'path', key: `line-${s.id}`, role: 'line', d, fill: 'none', stroke: color, strokeWidth: selSeries ? 3.5 : 2, target: seriesTarget, selected: selSeries });\n\t\t}\n\t\tconst marker: ChartMarker = bubble ? 'circle' : (s.marker ?? (type === 'scatter' || spec.points ? 'circle' : 'none'));\n\t\tconst ordered = bubble ? [...pts].sort((a, b) => (b.size ?? 0) - (a.size ?? 0)) : pts;\n\t\tconst labelsOn = (s.dataLabels ?? spec.dataLabels) && pts.length <= MAX_CHART_DATA_LABELS;\n\t\tconst labelFormat = s.format ?? yAxis.format;\n\t\tconst cells = new Set<string>();\n\t\tfor (const p of ordered) {\n\t\t\tconst cx = xPx(p.x);\n\t\t\tconst cy = yPx(p.y);\n\t\t\tif (pts.length > MAX_CHART_MARKERS) {\n\t\t\t\tconst cell = `${Math.round(cx / 2)},${Math.round(cy / 2)}`;\n\t\t\t\tif (cells.has(cell)) continue;\n\t\t\t\tcells.add(cell);\n\t\t\t}\n\t\t\tconst target: ChartSelection = { kind: 'point', seriesId: s.id, index: p.i };\n\t\t\tconst selected = ctx.isSelected(target);\n\t\t\tconst pointColor = chartPointColor(spec, si, p.i);\n\t\t\tconst r = bubble ? (maxSize > 0 && p.size !== null && p.size > 0 ? Math.max(2, Math.sqrt(p.size / maxSize) * maxR) : 2) : 4.5;\n\t\t\tif (bubble) marks.push({ kind: 'circle', key: `point-${s.id}-${p.i}`, role: 'point', cx, cy, r, fill: pointColor, stroke: pointColor, strokeWidth: 1, opacity: 0.6, target, selected: selected || selSeries });\n\t\t\telse marks.push(pointMark({ key: `point-${s.id}-${p.i}`, target, selected: selected || selSeries, opacity: 0.85 }, marker === 'none' ? 'hit' : marker, cx, cy, marker === 'none' ? 6 : r, marker === 'none' ? 'transparent' : pointColor, marker === 'none' ? undefined : pointColor, 2));\n\t\t\tif (selected) marks.push({ kind: 'circle', key: `point-${s.id}-${p.i}-selection`, role: 'selection', cx, cy, r: r + 3.5, fill: 'none', stroke: theme.selection, strokeWidth: 1.5 });\n\t\t\tif (labelsOn) labelMarks.push({ kind: 'text', key: `label-${s.id}-${p.i}`, role: 'data-label', x: cx, y: cy - r - 4, text: formatChartNumber(p.y, labelFormat), anchor: 'middle', baseline: 'bottom', size: fs - 1, fill: theme.text, target });\n\t\t}\n\t\tif (s.trendlines?.length) drawTrendlines(ctx, plot, s, color, pts.map((p) => [p.x, p.y]), (x, y) => [xRaw(x), yRaw(y)], trendLabels);\n\t}\n\tmarks.push(...labelMarks);\n\tdrawTrendLabels(ctx, plot, trendLabels);\n\treturn {\n\t\tplot,\n\t\tvalueScale: { axis: 'y', domain: [yScale.min, yScale.max], range: yRange, ticks: yScale.ticks },\n\t\txScale: { axis: 'x', domain: [xScale.min, xScale.max], range: xRange, ticks: xScale.ticks }\n\t};\n}\n\n/** Legend entries for trendlines (dashed lines after their series). */\nexport function trendlineLegendItems(spec: { series: ChartSeries[]; type: string; stacking: string }, color: (i: number) => string) {\n\tif (!chartSupportsTrendlines(spec.type, spec.stacking)) return [];\n\treturn spec.series.flatMap((s, si) =>\n\t\ts.hidden ? [] : (s.trendlines ?? []).map((t, ti) => ({ label: chartTrendlineName(t, s.name), color: t.color ?? color(si), target: { kind: 'trendline', seriesId: s.id, index: ti } as ChartSelection, after: s.id }))\n\t);\n}\n","// Waterfall, funnel, radar, histogram, box-and-whisker and treemap layouts follow\n// gridcraft-main/crates/chart/src/render.rs (`waterfall`, `funnel`, `radar`, `histogram`, `box_whisker`,\n// `treemap`) (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\nimport { categoryFrame, toValueScale } from './layout-cartesian.js';\nimport { arcPath, axisScale, contrastingTextColor, r2, truncate, type FamilyResult, type LayoutCtx } from './layout-shared.js';\nimport { formatChartNumber, linearMap, stepDecimals } from './scale.js';\nimport { chartSeriesColor, chartSliceColor } from './spec.js';\nimport { chartBoxStats, chartHistogram, chartSquarify } from './stats.js';\nimport type { ChartMark, ChartRect, ChartSelection, ChartSeries } from './types.js';\n\nconst sliceColor = (ctx: LayoutCtx, series: ChartSeries, i: number) => series.pointColors?.[i] || chartSliceColor(ctx.spec, i);\n\n/** Pie and donut: the first visible series, one slice per category. */\nexport function layoutPie(ctx: LayoutCtx, area: ChartRect, series: ChartSeries): FamilyResult {\n\tconst { spec, theme, marks, fs, selection } = ctx;\n\tconst values = spec.categories.map((_, i) => {\n\t\tconst v = series.values[i];\n\t\treturn v !== null && Number.isFinite(v) ? Math.max(0, v) : 0;\n\t});\n\tconst total = values.reduce((a, b) => a + b, 0);\n\tconst labelsOn = series.dataLabels ?? spec.dataLabels;\n\tconst labelRoom = labelsOn ? fs * 2.2 : 4;\n\tconst outer = Math.max(4, Math.min(area.width, area.height) / 2 - labelRoom);\n\tconst inner = spec.type === 'donut' ? outer * spec.innerRadius : 0;\n\tconst cx = area.x + area.width / 2;\n\tconst cy = area.y + area.height / 2;\n\tconst plot = { x: cx - outer, y: cy - outer, width: outer * 2, height: outer * 2 };\n\tif (total <= 0) {\n\t\tmarks.push({ kind: 'text', key: 'empty', role: 'empty', x: cx, y: cy, text: 'No positive values', anchor: 'middle', baseline: 'middle', size: fs + 1, fill: theme.mutedText });\n\t\treturn { plot };\n\t}\n\tlet angle = 0;\n\tconst format = series.format ?? spec.yAxis.format;\n\tlet hidden = 0;\n\tvalues.forEach((v, i) => {\n\t\tif (v <= 0) return;\n\t\tconst sweep = (v / total) * Math.PI * 2;\n\t\tconst start = angle;\n\t\tconst end = angle + sweep;\n\t\tangle = end;\n\t\tconst target: ChartSelection = { kind: 'point', seriesId: series.id, index: i };\n\t\tconst selected = ctx.isSelected(target) || ctx.isSelected({ kind: 'category', index: i });\n\t\tconst seriesSel = selection?.kind === 'series' && selection.seriesId === series.id;\n\t\t// The selected slice pops out a little.\n\t\tconst mid = (start + end) / 2;\n\t\tconst off = selected ? 6 : 0;\n\t\tconst ox = cx + off * Math.sin(mid);\n\t\tconst oy = cy - off * Math.cos(mid);\n\t\tmarks.push({\n\t\t\tkind: 'path',\n\t\t\tkey: `slice-${i}`,\n\t\t\trole: 'slice',\n\t\t\td: arcPath(ox, oy, outer, inner, start, end),\n\t\t\tfill: sliceColor(ctx, series, i),\n\t\t\tstroke: selected || seriesSel ? theme.selection : theme.surface,\n\t\t\tstrokeWidth: selected || seriesSel ? 2 : 1,\n\t\t\ttarget,\n\t\t\tselected: selected || seriesSel\n\t\t});\n\t\tif (!labelsOn) return;\n\t\tif (sweep <= 0.12) {\n\t\t\thidden++;\n\t\t\treturn;\n\t\t}\n\t\tconst share = v / total;\n\t\tconst text = format.style || format.code ? formatChartNumber(v, format) : `${Math.round(share * 100)}%`;\n\t\tconst lr = outer + fs * 1.1;\n\t\tconst lx = ox + lr * Math.sin(mid);\n\t\tconst ly = oy - lr * Math.cos(mid);\n\t\tconst anchor = Math.abs(Math.sin(mid)) < 0.2 ? 'middle' : Math.sin(mid) > 0 ? 'start' : 'end';\n\t\tmarks.push({ kind: 'text', key: `label-slice-${i}`, role: 'data-label', x: lx, y: ly, text, anchor, baseline: 'middle', size: fs, fill: theme.text, target });\n\t});\n\tif (hidden) ctx.warn('data-labels-hidden', `${hidden} slice label${hidden === 1 ? ' is' : 's are'} hidden: the slices are too thin.`);\n\tif (spec.type === 'donut' && inner > fs * 2) {\n\t\tmarks.push({ kind: 'text', key: 'donut-total', role: 'data-label', x: cx, y: cy, text: formatChartNumber(total, format), anchor: 'middle', baseline: 'middle', size: Math.min(theme.titleSize + 4, inner / 2), weight: 600, fill: theme.text, target: { kind: 'series', seriesId: series.id } });\n\t}\n\treturn { plot };\n}\n\n/** Radar: one spoke per category, one polygon per series. */\nexport function layoutRadar(ctx: LayoutCtx, area: ChartRect, visible: ChartSeries[]): FamilyResult {\n\tconst { spec, theme, marks, fs, measure } = ctx;\n\tconst n = spec.categories.length;\n\tconst labelW = Math.min(area.width * 0.25, Math.max(0, ...spec.categories.map((c) => measure(c, fs))));\n\tconst radius = Math.max(8, Math.min(area.width / 2 - labelW - 8, area.height / 2 - fs * 1.6));\n\tconst cx = area.x + area.width / 2;\n\tconst cy = area.y + area.height / 2;\n\tlet lo = 0;\n\tlet hi = -Infinity;\n\tfor (const s of visible) for (const v of s.values) if (v !== null && Number.isFinite(v)) (lo = Math.min(lo, v)), (hi = Math.max(hi, v));\n\tif (!Number.isFinite(hi)) hi = 1;\n\tconst scale = axisScale(lo, hi, radius, spec.yAxis, ctx.options.valueDomain);\n\tconst rOf = (v: number) => linearMap(Math.min(scale.max, Math.max(scale.min, v)), [scale.min, scale.max], [0, radius]);\n\tconst angle = (i: number) => (i / n) * Math.PI * 2;\n\tconst at = (i: number, r: number): [number, number] => [cx + r * Math.sin(angle(i)), cy - r * Math.cos(angle(i))];\n\tconst yTarget: ChartSelection = { kind: 'axis', axis: 'y' };\n\t// Rings and spokes\n\tif (spec.yAxis.gridlines)\n\t\tscale.ticks.forEach((t, k) => {\n\t\t\tconst r = rOf(t);\n\t\t\tif (r <= 0) return;\n\t\t\tconst d = Array.from({ length: n }, (_, i) => at(i, r)).map(([x, y], i) => `${i ? 'L' : 'M'}${r2(x)},${r2(y)}`).join('') + 'Z';\n\t\t\tmarks.push({ kind: 'path', key: `grid-ring-${k}`, role: 'grid', d, fill: 'none', stroke: theme.grid, strokeWidth: 1 });\n\t\t});\n\tfor (let i = 0; i < n; i++) {\n\t\tconst [x, y] = at(i, radius);\n\t\tmarks.push({ kind: 'line', key: `spoke-${i}`, role: 'axis', x1: cx, y1: cy, x2: x, y2: y, stroke: theme.grid, strokeWidth: 1, target: { kind: 'axis', axis: 'x' } });\n\t\tif (spec.xAxis.show) {\n\t\t\tconst [lx, ly] = at(i, radius + 8);\n\t\t\tconst s = Math.sin(angle(i));\n\t\t\tconst c = Math.cos(angle(i));\n\t\t\tconst target: ChartSelection = { kind: 'category', index: i };\n\t\t\tconst selected = ctx.isSelected(target);\n\t\t\tmarks.push({ kind: 'text', key: `tick-x-${i}`, role: 'tick-label', x: lx, y: ly, text: truncate(spec.categories[i], labelW, fs, measure), anchor: Math.abs(s) < 0.2 ? 'middle' : s > 0 ? 'start' : 'end', baseline: Math.abs(c) < 0.2 ? 'middle' : c > 0 ? 'bottom' : 'top', size: fs, fill: selected ? theme.selection : theme.mutedText, target, selected });\n\t\t}\n\t}\n\tif (spec.yAxis.show)\n\t\tscale.ticks.forEach((t, k) => {\n\t\t\tconst r = rOf(t);\n\t\t\tmarks.push({ kind: 'text', key: `tick-y-${k}`, role: 'tick-label', x: cx + 4, y: cy - r, text: formatChartNumber(t, spec.yAxis.format, spec.yAxis.format.decimals ?? stepDecimals(scale.step)), anchor: 'start', baseline: 'bottom', size: fs - 1, fill: theme.mutedText, target: yTarget });\n\t\t});\n\tfor (const s of visible) {\n\t\tconst si = spec.series.indexOf(s);\n\t\tconst color = chartSeriesColor(spec, si);\n\t\tconst selSeries = ctx.seriesSelected(s.id);\n\t\tconst pts = s.values.map((v, i) => (v !== null && Number.isFinite(v) ? at(i, rOf(v)) : null));\n\t\tconst present = pts.filter((p): p is [number, number] => p !== null);\n\t\tif (!present.length) continue;\n\t\tconst d = present.map(([x, y], i) => `${i ? 'L' : 'M'}${r2(x)},${r2(y)}`).join('') + 'Z';\n\t\tmarks.push({ kind: 'path', key: `area-${s.id}`, role: 'area', d, fill: color, opacity: s.type === 'line' ? 0 : 0.15, target: { kind: 'series', seriesId: s.id }, selected: selSeries });\n\t\tmarks.push({ kind: 'path', key: `line-${s.id}`, role: 'line', d, fill: 'none', stroke: color, strokeWidth: selSeries ? 3.5 : 2, target: { kind: 'series', seriesId: s.id }, selected: selSeries });\n\t\tpts.forEach((p, i) => {\n\t\t\tif (!p) return;\n\t\t\tconst target: ChartSelection = { kind: 'point', seriesId: s.id, index: i };\n\t\t\tconst selected = ctx.isSelected(target);\n\t\t\tmarks.push({ kind: 'circle', key: `point-${s.id}-${i}`, role: 'point', cx: p[0], cy: p[1], r: spec.points ? 3.5 : 6, fill: spec.points ? theme.surface : 'transparent', ...(spec.points ? { stroke: color, strokeWidth: 2 } : {}), target, selected: selected || selSeries });\n\t\t\tif (selected) marks.push({ kind: 'circle', key: `point-${s.id}-${i}-selection`, role: 'selection', cx: p[0], cy: p[1], r: 7, fill: 'none', stroke: theme.selection, strokeWidth: 1.5 });\n\t\t\tif (s.dataLabels ?? spec.dataLabels) marks.push({ kind: 'text', key: `label-${s.id}-${i}`, role: 'data-label', x: p[0], y: p[1] - 6, text: formatChartNumber(s.values[i], s.format ?? spec.yAxis.format), anchor: 'middle', baseline: 'bottom', size: fs - 1, fill: theme.text, target });\n\t\t});\n\t}\n\treturn { plot: { x: cx - radius, y: cy - radius, width: radius * 2, height: radius * 2 } };\n}\n\n/** Waterfall: the first series as running increases (positive colour) and decreases (negative colour). */\nexport function layoutWaterfall(ctx: LayoutCtx, area: ChartRect, series: ChartSeries): FamilyResult {\n\tconst { spec, theme, marks, fs } = ctx;\n\tconst isTotal = (label: string) => /\\b(sub)?total\\b/i.test(label);\n\tconst steps: { label: string; index: number | null; from: number; to: number; total: boolean }[] = [];\n\tlet running = 0;\n\tspec.categories.forEach((label, i) => {\n\t\tconst v = series.values[i];\n\t\tif (isTotal(label)) {\n\t\t\tsteps.push({ label, index: i, from: 0, to: running, total: true });\n\t\t\treturn;\n\t\t}\n\t\tconst delta = v !== null && Number.isFinite(v) ? v : 0;\n\t\tsteps.push({ label, index: i, from: running, to: running + delta, total: false });\n\t\trunning += delta;\n\t});\n\tif (spec.showTotal && !(steps.length && steps[steps.length - 1].total)) steps.push({ label: 'Total', index: null, from: 0, to: running, total: true });\n\tlet lo = 0;\n\tlet hi = 0;\n\tfor (const s of steps) {\n\t\tlo = Math.min(lo, s.from, s.to);\n\t\thi = Math.max(hi, s.from, s.to);\n\t}\n\tconst horizontal = spec.orientation === 'horizontal';\n\tconst frame = categoryFrame(ctx, area, {\n\t\tlabels: steps.map((s) => s.label),\n\t\thorizontal,\n\t\tprimary: { lo, hi, axis: spec.yAxis, format: spec.yAxis.format, frozen: ctx.options.valueDomain, key: 'y' },\n\t\tlabelTarget: (k) => (steps[k].index !== null ? { kind: 'category', index: steps[k].index! } : undefined)\n\t});\n\tconst { band, catCenter, map, value } = frame;\n\tconst w = band * (1 - spec.gapWidth);\n\tconst color = chartSeriesColor(spec, spec.series.indexOf(series));\n\tconst format = series.format ?? spec.yAxis.format;\n\tconst labelsOn = series.dataLabels ?? spec.dataLabels;\n\tlet prevEnd: number | null = null;\n\tsteps.forEach((s, k) => {\n\t\tconst a = value.px(s.from);\n\t\tconst b = value.px(s.to);\n\t\tconst c0 = catCenter(k) - w / 2;\n\t\tconst [x, y] = map({ c: c0, v: Math.min(a, b) });\n\t\tconst rect = horizontal ? { x: Math.min(a, b), y: c0, width: Math.max(1, Math.abs(b - a)), height: w } : { x, y, width: w, height: Math.max(1, Math.abs(b - a)) };\n\t\tconst fill = s.total ? color : s.to >= s.from ? theme.positive : theme.negative;\n\t\tconst target: ChartSelection = s.index !== null ? { kind: 'point', seriesId: series.id, index: s.index } : { kind: 'series', seriesId: series.id };\n\t\tconst selected = ctx.isSelected(target);\n\t\tmarks.push({ kind: 'rect', key: `bar-${series.id}-${s.index ?? 'total'}`, role: 'bar', ...rect, fill, radius: Math.min(2, w / 4), target, selected });\n\t\tif (selected) ctx.outline(`bar-${series.id}-${s.index ?? 'total'}-selection`, { x: rect.x - 2, y: rect.y - 2, width: rect.width + 4, height: rect.height + 4 });\n\t\t// Dashed connector from the previous bar's end to this bar's start.\n\t\tif (prevEnd !== null) {\n\t\t\tconst [x1, y1] = map({ c: catCenter(k - 1) + w / 2, v: value.px(prevEnd) });\n\t\t\tconst [x2, y2] = map({ c: c0, v: value.px(prevEnd) });\n\t\t\tmarks.push({ kind: 'line', key: `connector-${k}`, role: 'connector', x1, y1, x2, y2, stroke: theme.axis, strokeWidth: 1, dash: '3 3' });\n\t\t}\n\t\tprevEnd = s.to;\n\t\tif (labelsOn) {\n\t\t\tconst text = formatChartNumber(s.total ? s.to : s.to - s.from, format);\n\t\t\tconst up = s.to >= s.from;\n\t\t\tconst [lx, ly] = map({ c: catCenter(k), v: (horizontal ? Math.max(a, b) + 4 : up ? Math.min(a, b) - 4 : Math.max(a, b) + 4) });\n\t\t\tmarks.push({ kind: 'text', key: `label-${series.id}-${s.index ?? 'total'}`, role: 'data-label', x: lx, y: ly, text, anchor: horizontal ? 'start' : 'middle', baseline: horizontal ? 'middle' : up ? 'bottom' : 'top', size: fs - 1, fill: theme.text, target });\n\t\t}\n\t});\n\treturn { plot: frame.plot, valueScale: toValueScale(value, horizontal ? 'x' : 'y') };\n}\n\n/** Histogram: every value of the visible series, binned; one bar per series in each bin. */\nexport function layoutHistogram(ctx: LayoutCtx, area: ChartRect, visible: ChartSeries[]): FamilyResult {\n\tconst { spec, marks, fs, theme } = ctx;\n\tconst pooled = visible.flatMap((s) => s.values);\n\tconst bins = chartHistogram(pooled, spec.bins);\n\tif (!bins.length) return { plot: area };\n\tconst edgeFormat = spec.xAxis.format;\n\tconst edges = (b: (typeof bins)[number], k: number) => `${k === 0 ? '[' : '('}${formatChartNumber(b.from, edgeFormat)}, ${formatChartNumber(b.to, edgeFormat)}]`;\n\t// Count per series per bin, with the same edges.\n\tconst counts = visible.map((s) => {\n\t\tconst c = bins.map(() => 0);\n\t\tfor (const v of s.values) {\n\t\t\tif (v === null || !Number.isFinite(v)) continue;\n\t\t\tlet k = bins.findIndex((b) => v <= b.to);\n\t\t\tif (k < 0) k = bins.length - 1;\n\t\t\tc[k]++;\n\t\t}\n\t\treturn c;\n\t});\n\tconst maxCount = Math.max(1, ...counts.flat());\n\tconst frame = categoryFrame(ctx, area, {\n\t\tlabels: bins.map(edges),\n\t\thorizontal: false,\n\t\tprimary: { lo: 0, hi: maxCount, axis: { ...spec.yAxis, format: spec.yAxis.format.code || spec.yAxis.format.style ? spec.yAxis.format : { decimals: 0 } }, format: spec.yAxis.format.code || spec.yAxis.format.style ? spec.yAxis.format : { decimals: 0 }, frozen: null, key: 'y' },\n\t\tlabelTarget: () => undefined\n\t});\n\tconst { band, catCenter, value, plot } = frame;\n\tconst k = visible.length;\n\tconst gap = k > 1 ? band * spec.gapWidth * 0.5 : 0;\n\tconst w = (band - gap) / k;\n\tvisible.forEach((s, si) => {\n\t\tconst color = chartSeriesColor(spec, spec.series.indexOf(s));\n\t\tconst target: ChartSelection = { kind: 'series', seriesId: s.id };\n\t\tconst selected = ctx.seriesSelected(s.id);\n\t\tcounts[si].forEach((c, b) => {\n\t\t\tif (!c) return;\n\t\t\tconst top = value.px(c);\n\t\t\tconst x = catCenter(b) - band / 2 + gap / 2 + si * w;\n\t\t\tmarks.push({ kind: 'rect', key: `bar-${s.id}-${b}`, role: 'bar', x, y: top, width: w, height: value.zero - top, fill: color, stroke: theme.surface, strokeWidth: 0.75, opacity: k > 1 ? 0.85 : undefined, target, selected });\n\t\t\tif (s.dataLabels ?? spec.dataLabels) marks.push({ kind: 'text', key: `label-${s.id}-${b}`, role: 'data-label', x: x + w / 2, y: top - 3, text: String(c), anchor: 'middle', baseline: 'bottom', size: fs - 1, fill: theme.text, target });\n\t\t});\n\t});\n\treturn { plot };\n}\n\n/** Box and whiskers: one box per visible series (quartiles, median, mean ×, whiskers at 1.5 × IQR, outliers). */\nexport function layoutBox(ctx: LayoutCtx, area: ChartRect, visible: ChartSeries[]): FamilyResult {\n\tconst { spec, marks, theme, fs } = ctx;\n\tconst stats = visible.map((s) => chartBoxStats(s.values));\n\tlet lo = Infinity;\n\tlet hi = -Infinity;\n\tfor (const st of stats) if (st) (lo = Math.min(lo, st.min)), (hi = Math.max(hi, st.max));\n\tif (!Number.isFinite(lo)) [lo, hi] = [0, 1];\n\tconst frame = categoryFrame(ctx, area, {\n\t\tlabels: visible.map((s) => s.name),\n\t\thorizontal: false,\n\t\tprimary: { lo, hi, axis: spec.yAxis, format: spec.yAxis.format, frozen: ctx.options.valueDomain, headroom: true, key: 'y' },\n\t\tlabelTarget: (i) => ({ kind: 'series', seriesId: visible[i].id })\n\t});\n\tconst { band, catCenter, value, plot } = frame;\n\tconst bw = Math.min(80, band * (1 - spec.gapWidth) * 0.6);\n\tvisible.forEach((s, i) => {\n\t\tconst st = stats[i];\n\t\tif (!st) return;\n\t\tconst color = chartSeriesColor(spec, spec.series.indexOf(s));\n\t\tconst target: ChartSelection = { kind: 'series', seriesId: s.id };\n\t\tconst selected = ctx.seriesSelected(s.id);\n\t\tconst c = catCenter(i);\n\t\tconst y = value.px;\n\t\tconst line = (key: string, x1: number, y1: number, x2: number, y2: number, width = 1.25, stroke = color) => marks.push({ kind: 'line', key, role: 'whisker', x1, y1, x2, y2, stroke, strokeWidth: width, target });\n\t\tline(`whisker-hi-${s.id}`, c, y(st.upperWhisker), c, y(st.q3));\n\t\tline(`whisker-lo-${s.id}`, c, y(st.q1), c, y(st.lowerWhisker));\n\t\tline(`cap-hi-${s.id}`, c - bw / 4, y(st.upperWhisker), c + bw / 4, y(st.upperWhisker));\n\t\tline(`cap-lo-${s.id}`, c - bw / 4, y(st.lowerWhisker), c + bw / 4, y(st.lowerWhisker));\n\t\tconst top = y(st.q3);\n\t\tconst bottom = y(st.q1);\n\t\tmarks.push({ kind: 'rect', key: `box-${s.id}`, role: 'box', x: c - bw / 2, y: top, width: bw, height: Math.max(0.5, bottom - top), fill: color, opacity: 0.3, stroke: selected ? theme.selection : color, strokeWidth: selected ? 2.5 : 1.5, target, selected });\n\t\tline(`median-${s.id}`, c - bw / 2, y(st.median), c + bw / 2, y(st.median), 2.5);\n\t\tconst my = y(st.mean);\n\t\tmarks.push({ kind: 'path', key: `mean-${s.id}`, role: 'whisker', d: `M${r2(c - 3)},${r2(my - 3)}L${r2(c + 3)},${r2(my + 3)}M${r2(c - 3)},${r2(my + 3)}L${r2(c + 3)},${r2(my - 3)}`, stroke: color, strokeWidth: 1.25, fill: 'none', target });\n\t\tst.outliers.slice(0, 500).forEach((o, k) => marks.push({ kind: 'circle', key: `outlier-${s.id}-${k}`, role: 'outlier', cx: c, cy: y(o), r: 2.5, fill: 'none', stroke: color, strokeWidth: 1, target }));\n\t\tif (s.dataLabels ?? spec.dataLabels) marks.push({ kind: 'text', key: `label-${s.id}-median`, role: 'data-label', x: c + bw / 2 + 4, y: y(st.median), text: formatChartNumber(st.median, s.format ?? spec.yAxis.format), anchor: 'start', baseline: 'middle', size: fs - 1, fill: theme.text, target });\n\t});\n\treturn { plot, valueScale: toValueScale(value, 'y') };\n}\n\n/** Funnel: the first series, one centred bar per category, widths proportional to the values. */\nexport function layoutFunnel(ctx: LayoutCtx, area: ChartRect, series: ChartSeries): FamilyResult {\n\tconst { spec, marks, theme, fs, measure } = ctx;\n\tconst n = spec.categories.length;\n\tconst labelW = spec.xAxis.show ? Math.min(area.width * 0.3, Math.max(0, ...spec.categories.map((c) => measure(c, fs)))) + 10 : 0;\n\tconst plot: ChartRect = { x: area.x + labelW, y: area.y, width: Math.max(1, area.width - labelW), height: area.height };\n\tconst max = Math.max(0, ...series.values.map((v) => (v !== null && Number.isFinite(v) ? Math.abs(v) : 0)));\n\tconst band = plot.height / Math.max(1, n);\n\tconst h = band * (1 - Math.min(0.5, spec.gapWidth * 0.5));\n\tconst format = series.format ?? spec.yAxis.format;\n\tconst labelsOn = series.dataLabels ?? true;\n\tconst cx = plot.x + plot.width / 2;\n\tspec.categories.forEach((label, i) => {\n\t\tconst v = series.values[i];\n\t\tconst cy = plot.y + band * (i + 0.5);\n\t\tconst target: ChartSelection = { kind: 'point', seriesId: series.id, index: i };\n\t\tconst selected = ctx.isSelected(target);\n\t\tif (spec.xAxis.show) {\n\t\t\tconst catTarget: ChartSelection = { kind: 'category', index: i };\n\t\t\tmarks.push({ kind: 'text', key: `tick-x-${i}`, role: 'tick-label', x: plot.x - 8, y: cy, text: truncate(label, labelW - 10, fs, measure), anchor: 'end', baseline: 'middle', size: fs, fill: ctx.isSelected(catTarget) ? theme.selection : theme.mutedText, target: catTarget });\n\t\t}\n\t\tif (v === null || !Number.isFinite(v) || max <= 0) return;\n\t\tconst w = Math.max(1, (Math.abs(v) / max) * plot.width);\n\t\tconst fill = sliceColor(ctx, series, i);\n\t\tmarks.push({ kind: 'rect', key: `bar-${series.id}-${i}`, role: 'bar', x: cx - w / 2, y: cy - h / 2, width: w, height: h, fill, radius: 2, target, selected });\n\t\tif (selected) ctx.outline(`bar-${series.id}-${i}-selection`, { x: cx - w / 2 - 2, y: cy - h / 2 - 2, width: w + 4, height: h + 4 });\n\t\tif (labelsOn) {\n\t\t\tconst text = formatChartNumber(v, format);\n\t\t\tconst fits = measure(text, fs) + 8 <= w && fs + 2 <= h;\n\t\t\tmarks.push({ kind: 'text', key: `label-${series.id}-${i}`, role: 'data-label', x: fits ? cx : cx + w / 2 + 4, y: cy, text, anchor: fits ? 'middle' : 'start', baseline: 'middle', size: fs, fill: fits ? contrastingTextColor(fill) : theme.text, target });\n\t\t}\n\t});\n\treturn { plot };\n}\n\n/** Treemap: the first series' positive values as nested rectangles (squarified), labelled where they fit. */\nexport function layoutTreemap(ctx: LayoutCtx, area: ChartRect, series: ChartSeries): FamilyResult {\n\tconst { spec, marks, theme, fs, measure } = ctx;\n\tconst values = spec.categories.map((_, i) => {\n\t\tconst v = series.values[i];\n\t\treturn v !== null && Number.isFinite(v) ? v : 0;\n\t});\n\tconst rects = chartSquarify(values, area);\n\tconst format = series.format ?? spec.yAxis.format;\n\tconst labelsOn = series.dataLabels ?? true;\n\tlet unlabelled = 0;\n\trects.forEach((r, i) => {\n\t\tif (r.width <= 0 || r.height <= 0) return;\n\t\tconst target: ChartSelection = { kind: 'point', seriesId: series.id, index: i };\n\t\tconst selected = ctx.isSelected(target) || ctx.isSelected({ kind: 'category', index: i });\n\t\tconst fill = sliceColor(ctx, series, i);\n\t\tmarks.push({ kind: 'rect', key: `cell-${i}`, role: 'cell', ...r, fill, stroke: selected ? theme.selection : theme.surface, strokeWidth: selected ? 2.5 : 1.5, target, selected });\n\t\tif (!labelsOn) return;\n\t\tconst room = r.width - 8;\n\t\tif (room < fs * 2 || r.height < fs * 1.5) {\n\t\t\tunlabelled++;\n\t\t\treturn;\n\t\t}\n\t\tconst color = contrastingTextColor(fill);\n\t\tmarks.push({ kind: 'text', key: `label-cell-${i}`, role: 'data-label', x: r.x + 4, y: r.y + 4, text: truncate(spec.categories[i], room, fs, measure), anchor: 'start', baseline: 'top', size: fs, weight: 600, fill: color, target });\n\t\tif (r.height >= fs * 3) marks.push({ kind: 'text', key: `label-value-${i}`, role: 'data-label', x: r.x + 4, y: r.y + 6 + fs * 1.2, text: truncate(formatChartNumber(values[i], format), room, fs - 1, measure), anchor: 'start', baseline: 'top', size: fs - 1, fill: color, target });\n\t});\n\tif (unlabelled) ctx.warn('data-labels-hidden', `${unlabelled} treemap cell${unlabelled === 1 ? ' is' : 's are'} too small for a label.`);\n\treturn { plot: area };\n}\n\nexport type { ChartMark };\n","// The legend height cap and the final non-finite pass follow gridcraft-main/crates/chart/src/render.rs\n// (`legend`, `finalize`) (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\nimport { layoutCartesian, layoutXy, trendlineLegendItems } from './layout-cartesian.js';\nimport { layoutBox, layoutFunnel, layoutHistogram, layoutPie, layoutRadar, layoutTreemap, layoutWaterfall } from './layout-families.js';\nimport { estimateChartTextWidth, sameChartSelection, type ChartLayoutOptions, type FamilyResult, type LayoutCtx } from './layout-shared.js';\nimport { formatChartNumber, linearMap } from './scale.js';\nimport { chartColorsByCategory, chartFamily, chartSeriesColor, chartSeriesType, chartSliceColor, DEFAULT_CHART_THEME } from './spec.js';\nimport type { ChartMark, ChartRect, ChartScene, ChartSelection, ChartSpec, ChartTheme, ChartWarning } from './types.js';\n\nexport { contrastingTextColor, estimateChartTextWidth, sameChartSelection, type ChartLayoutOptions, type ChartTextMeasure } from './layout-shared.js';\n\ninterface LegendItem {\n\tlabel: string;\n\tcolor: string;\n\ttarget: ChartSelection;\n\tfaded: boolean;\n\tselected: boolean;\n\tshape: 'square' | 'line' | 'dash';\n}\n\nfunction legendItems(spec: ChartSpec, ctx: LayoutCtx): LegendItem[] {\n\tconst family = chartFamily(spec);\n\t// Funnel bars are labelled along the side already.\n\tif (spec.type === 'funnel') return [];\n\tif (chartColorsByCategory(spec.type)) {\n\t\tconst first = spec.series.find((s) => !s.hidden);\n\t\treturn spec.categories.map((c, i) => ({\n\t\t\tlabel: c,\n\t\t\tcolor: first?.pointColors?.[i] || chartSliceColor(spec, i),\n\t\t\ttarget: { kind: 'category', index: i },\n\t\t\tfaded: false,\n\t\t\tselected: ctx.isSelected({ kind: 'category', index: i }),\n\t\t\tshape: 'square'\n\t\t}));\n\t}\n\tif (family === 'waterfall') {\n\t\tconst first = spec.series.find((s) => !s.hidden);\n\t\tif (!first) return [];\n\t\tconst target: ChartSelection = { kind: 'series', seriesId: first.id };\n\t\treturn [\n\t\t\t{ label: 'Increase', color: ctx.theme.positive, target, faded: false, selected: false, shape: 'square' },\n\t\t\t{ label: 'Decrease', color: ctx.theme.negative, target, faded: false, selected: false, shape: 'square' },\n\t\t\t{ label: 'Total', color: chartSeriesColor(spec, spec.series.indexOf(first)), target, faded: false, selected: false, shape: 'square' }\n\t\t];\n\t}\n\tif (family === 'box') return [];\n\tconst trends = trendlineLegendItems(spec, (i) => chartSeriesColor(spec, i));\n\tconst items: LegendItem[] = [];\n\tspec.series.forEach((s, i) => {\n\t\tconst type = chartSeriesType(spec, s);\n\t\titems.push({\n\t\t\tlabel: s.name,\n\t\t\tcolor: chartSeriesColor(spec, i),\n\t\t\ttarget: { kind: 'series', seriesId: s.id },\n\t\t\tfaded: !!s.hidden,\n\t\t\tselected: ctx.seriesSelected(s.id),\n\t\t\tshape: family === 'cartesian' || family === 'xy' || family === 'radar' ? (type === 'line' ? 'line' : 'square') : 'square'\n\t\t});\n\t\tfor (const t of trends.filter((t) => t.after === s.id)) items.push({ label: t.label, color: t.color, target: t.target, faded: false, selected: ctx.isSelected(t.target), shape: 'dash' });\n\t});\n\treturn items;\n}\n\n/**\n * Lays a chart out as drawing primitives (rects, paths, circles, lines and text) in a\n * `width × height` box. Pure and DOM-free: the React and Svelte canvases render the marks\n * as SVG, `chartSceneToSvg` writes them as a string, and anything else (canvas, PDF) can\n * draw them too. Clickable marks carry the `target` they select; what had to be left out\n * (hidden labels, thinned lines) is listed in `warnings`.\n */\nexport function layoutChart(spec: ChartSpec, options: ChartLayoutOptions): ChartScene {\n\tconst width = Math.max(1, Number.isFinite(options.width) ? options.width : 1);\n\tconst height = Math.max(1, Number.isFinite(options.height) ? options.height : 1);\n\tconst theme: ChartTheme = { ...DEFAULT_CHART_THEME, ...options.theme };\n\tconst selection = options.selection ?? null;\n\tconst marks: ChartMark[] = [];\n\tconst warnings: ChartWarning[] = [];\n\tconst ctx: LayoutCtx = {\n\t\tspec,\n\t\toptions,\n\t\ttheme,\n\t\tmeasure: options.measureText ?? estimateChartTextWidth,\n\t\tfs: theme.fontSize,\n\t\twidth,\n\t\theight,\n\t\tmarks,\n\t\twarnings,\n\t\tselection,\n\t\tisSelected: (target) => sameChartSelection(selection, target),\n\t\tseriesSelected: (id) => selection?.kind === 'series' && selection.seriesId === id,\n\t\toutline: (key, box) => marks.push({ kind: 'rect', key, role: 'selection', ...box, fill: 'none', stroke: theme.selection, strokeWidth: 1, radius: 3 }),\n\t\twarn: (code, message) => {\n\t\t\tif (!warnings.some((w) => w.code === code && w.message === message)) warnings.push({ code, message });\n\t\t}\n\t};\n\tconst pad = 12;\n\tconst fs = theme.fontSize;\n\n\tmarks.push({ kind: 'rect', key: 'background', role: 'background', x: 0, y: 0, width, height, fill: theme.background, target: { kind: 'chart' } });\n\n\t// Titles ----------------------------------------------------------------\n\tlet top = pad;\n\tconst titleMark = (kind: 'title' | 'subtitle', label: string, size: number, weight: number, fill: string) => {\n\t\tconst target: ChartSelection = { kind };\n\t\tconst selected = ctx.isSelected(target);\n\t\tmarks.push({ kind: 'text', key: kind, role: kind, x: width / 2, y: top, text: label, anchor: 'middle', baseline: 'top', size, weight, fill, target, selected });\n\t\tif (selected) {\n\t\t\tconst w = ctx.measure(label, size, weight);\n\t\t\tctx.outline(`${kind}-selection`, { x: width / 2 - w / 2 - 4, y: top - 3, width: w + 8, height: size * 1.25 + 6 });\n\t\t}\n\t\ttop += size * 1.35;\n\t};\n\tif (spec.title) titleMark('title', spec.title, theme.titleSize, 600, theme.text);\n\tif (spec.subtitle) titleMark('subtitle', spec.subtitle, fs + 1, 400, theme.mutedText);\n\tif (spec.title || spec.subtitle) top += 6;\n\n\tconst family = chartFamily(spec);\n\tconst visible = spec.series.filter((s) => !s.hidden);\n\n\t// Legend ----------------------------------------------------------------\n\tconst items = legendItems(spec, ctx);\n\tlet area: ChartRect = { x: pad, y: top, width: width - pad * 2, height: height - top - pad };\n\tconst seriesLegend = family === 'cartesian' || family === 'xy' || family === 'radar' || family === 'histogram';\n\tconst hasTrend = items.some((i) => i.shape === 'dash');\n\tconst showLegend = spec.legend.show && items.length > 0 && (!seriesLegend || spec.series.length > 1 || spec.series.some((s) => s.hidden) || hasTrend);\n\tif (showLegend) area = layoutLegend(items, spec.legend.position, area, ctx, ctx.isSelected({ kind: 'legend' }));\n\n\tconst finish = (result: FamilyResult): ChartScene => {\n\t\tif (options.readout) addReadout(ctx, options.readout, result.plot);\n\t\tconst kept = finalizeMarks(marks, width, height);\n\t\tif (kept.length < marks.length) ctx.warn('values-skipped', `${marks.length - kept.length} mark${marks.length - kept.length === 1 ? ' was' : 's were'} left out: their values are not finite numbers.`);\n\t\treturn {\n\t\t\twidth,\n\t\t\theight,\n\t\t\tplot: result.plot,\n\t\t\tmarks: kept,\n\t\t\tpercent: result.percent ?? false,\n\t\t\ttheme,\n\t\t\twarnings,\n\t\t\t...(result.valueScale ? { valueScale: result.valueScale } : {}),\n\t\t\t...(result.valueScale2 ? { valueScale2: result.valueScale2 } : {}),\n\t\t\t...(result.xScale ? { xScale: result.xScale } : {})\n\t\t};\n\t};\n\tconst empty = (message: string) => {\n\t\tmarks.push({ kind: 'text', key: 'empty', role: 'empty', x: area.x + area.width / 2, y: area.y + area.height / 2, text: message, anchor: 'middle', baseline: 'middle', size: fs + 1, fill: theme.mutedText });\n\t\treturn finish({ plot: { x: area.x, y: area.y, width: 0, height: 0 } });\n\t};\n\tconst hasData = visible.some((s) => s.values.some((v) => v !== null && Number.isFinite(v)));\n\tconst needsCategories = family !== 'histogram' && family !== 'box' && family !== 'xy';\n\tif ((needsCategories && !spec.categories.length) || !visible.length || !hasData) return empty(options.emptyMessage ?? 'No data');\n\n\tswitch (family) {\n\t\tcase 'pie':\n\t\t\treturn finish(layoutPie(ctx, area, visible[0]));\n\t\tcase 'radar':\n\t\t\tif (spec.categories.length < 3) {\n\t\t\t\tctx.warn('type-fallback', 'A radar chart needs at least 3 categories; it is drawn as a line chart until then.');\n\t\t\t\treturn finish(layoutCartesian(ctx, area, visible));\n\t\t\t}\n\t\t\treturn finish(layoutRadar(ctx, area, visible));\n\t\tcase 'waterfall':\n\t\t\treturn finish(layoutWaterfall(ctx, area, visible[0]));\n\t\tcase 'histogram':\n\t\t\treturn finish(layoutHistogram(ctx, area, visible));\n\t\tcase 'box':\n\t\t\treturn finish(layoutBox(ctx, area, visible));\n\t\tcase 'funnel':\n\t\t\treturn finish(layoutFunnel(ctx, area, visible[0]));\n\t\tcase 'treemap':\n\t\t\treturn finish(layoutTreemap(ctx, area, visible[0]));\n\t\tcase 'xy':\n\t\t\treturn finish(layoutXy(ctx, area, visible));\n\t\tdefault:\n\t\t\treturn finish(layoutCartesian(ctx, area, visible));\n\t}\n}\n\nfunction layoutLegend(items: LegendItem[], position: ChartSpec['legend']['position'], area: ChartRect, ctx: LayoutCtx, legendSelected: boolean): ChartRect {\n\tconst { theme, measure, marks } = ctx;\n\tconst fs = theme.fontSize;\n\tconst swatch = 10;\n\tconst rowH = fs * 1.7;\n\tconst itemW = (item: LegendItem) => swatch + 6 + measure(item.label, fs) + 14;\n\tconst placed: { item: LegendItem; x: number; y: number }[] = [];\n\tlet box: ChartRect;\n\tlet rest: ChartRect;\n\tif (position === 'left' || position === 'right') {\n\t\tconst w = Math.min(area.width * 0.35, Math.max(...items.map(itemW)));\n\t\tconst fit = Math.max(1, Math.floor((area.height + 0.5) / rowH));\n\t\tconst shown = items.slice(0, fit);\n\t\tconst h = Math.min(shown.length * rowH, area.height);\n\t\tconst x = position === 'left' ? area.x : area.x + area.width - w;\n\t\tconst y = area.y + (area.height - h) / 2;\n\t\tshown.forEach((item, i) => placed.push({ item, x, y: y + i * rowH + rowH / 2 }));\n\t\tif (shown.length < items.length) ctx.warn('legend-truncated', `The legend shows ${shown.length} of ${items.length} entries; the chart is too short for the rest.`);\n\t\tbox = { x, y, width: w, height: h };\n\t\trest = position === 'left' ? { ...area, x: area.x + w + 8, width: area.width - w - 8 } : { ...area, width: area.width - w - 8 };\n\t} else {\n\t\t// Wrap into rows, each centred; at most 30% of the chart height.\n\t\tconst rows: LegendItem[][] = [[]];\n\t\tlet used = 0;\n\t\tfor (const item of items) {\n\t\t\tconst w = itemW(item);\n\t\t\tif (used + w > area.width && rows[rows.length - 1].length) {\n\t\t\t\trows.push([]);\n\t\t\t\tused = 0;\n\t\t\t}\n\t\t\trows[rows.length - 1].push(item);\n\t\t\tused += w;\n\t\t}\n\t\tconst maxRows = Math.max(1, Math.floor((ctx.height * 0.3) / rowH));\n\t\tconst shown = rows.slice(0, maxRows);\n\t\tconst hiddenCount = rows.slice(maxRows).reduce((t, r) => t + r.length, 0);\n\t\tif (hiddenCount) ctx.warn('legend-truncated', `The legend shows ${items.length - hiddenCount} of ${items.length} entries; the rest do not fit.`);\n\t\tconst h = shown.length * rowH;\n\t\tconst y0 = position === 'top' ? area.y : area.y + area.height - h;\n\t\tshown.forEach((row, r) => {\n\t\t\tconst total = row.reduce((t, item) => t + itemW(item), 0) - 14;\n\t\t\tlet x = area.x + (area.width - total) / 2;\n\t\t\tfor (const item of row) {\n\t\t\t\tplaced.push({ item, x, y: y0 + r * rowH + rowH / 2 });\n\t\t\t\tx += itemW(item);\n\t\t\t}\n\t\t});\n\t\tbox = { x: area.x, y: y0, width: area.width, height: h };\n\t\trest = position === 'top' ? { ...area, y: area.y + h + 6, height: area.height - h - 6 } : { ...area, height: area.height - h - 6 };\n\t}\n\tplaced.forEach(({ item, x, y }, i) => {\n\t\tconst opacity = item.faded ? 0.4 : undefined;\n\t\tif (item.shape === 'line' || item.shape === 'dash')\n\t\t\tmarks.push({ kind: 'line', key: `legend-swatch-${i}`, role: 'legend-swatch', x1: x, y1: y, x2: x + swatch, y2: y, stroke: item.color, strokeWidth: item.shape === 'dash' ? 1.5 : 2.5, ...(item.shape === 'dash' ? { dash: '3 2' } : {}), opacity, target: item.target, selected: item.selected });\n\t\telse marks.push({ kind: 'rect', key: `legend-swatch-${i}`, role: 'legend-swatch', x, y: y - swatch / 2, width: swatch, height: swatch, radius: 2, fill: item.color, opacity, target: item.target, selected: item.selected });\n\t\tmarks.push({\n\t\t\tkind: 'text',\n\t\t\tkey: `legend-label-${i}`,\n\t\t\trole: 'legend-label',\n\t\t\tx: x + swatch + 6,\n\t\t\ty,\n\t\t\ttext: item.label,\n\t\t\tanchor: 'start',\n\t\t\tbaseline: 'middle',\n\t\t\tsize: fs,\n\t\t\tfill: item.selected ? theme.selection : theme.text,\n\t\t\tweight: item.selected ? 600 : undefined,\n\t\t\topacity,\n\t\t\ttarget: item.target,\n\t\t\tselected: item.selected\n\t\t});\n\t});\n\tif (legendSelected) marks.push({ kind: 'rect', key: 'legend-selection', role: 'selection', x: box.x - 4, y: box.y - 2, width: box.width + 8, height: box.height + 4, fill: 'none', stroke: theme.selection, strokeWidth: 1, radius: 3 });\n\treturn rest;\n}\n\n/** Writes the value of a point next to it while it is dragged, kept inside the plot. */\nfunction addReadout(ctx: LayoutCtx, readout: { seriesId: string; index: number }, plot: ChartRect) {\n\tconst { spec, marks, theme, fs, measure } = ctx;\n\tconst series = spec.series.find((s) => s.id === readout.seriesId);\n\tconst value = series?.values[readout.index];\n\tif (!series || value === null || value === undefined) return;\n\tconst mark = marks.find((m) => (m.role === 'point' || m.role === 'bar') && m.target?.kind === 'point' && m.target.seriesId === readout.seriesId && m.target.index === readout.index);\n\tif (!mark) return;\n\tlet ax: number;\n\tlet ay: number;\n\tif (mark.kind === 'rect') {\n\t\tax = mark.x + mark.width / 2;\n\t\tay = mark.y;\n\t} else if (mark.kind === 'circle') {\n\t\tax = mark.cx;\n\t\tay = mark.cy - mark.r;\n\t} else return;\n\tconst axis = series.axis === 'secondary' ? spec.yAxis2 : spec.yAxis;\n\tconst x = series.x?.[readout.index];\n\tconst text =\n\t\tx !== null && x !== undefined && chartFamily(spec) === 'xy'\n\t\t\t? `${formatChartNumber(x, spec.xAxis.format)}, ${formatChartNumber(value, axis.format)}`\n\t\t\t: `${spec.categories[readout.index] ?? ''}: ${formatChartNumber(value, series.format ?? axis.format)}`;\n\tconst w = measure(text, fs, 600) + 10;\n\tconst h = fs + 8;\n\tconst bx = Math.max(plot.x, Math.min(plot.x + plot.width - w, ax - w / 2));\n\tconst by = Math.max(plot.y, Math.min(plot.y + plot.height - h, ay - h - 6));\n\tmarks.push({ kind: 'rect', key: 'drag-readout-box', role: 'drag-readout', x: bx, y: by, width: w, height: h, fill: theme.text, radius: 3, opacity: 0.9 });\n\tmarks.push({ kind: 'text', key: 'drag-readout', role: 'drag-readout', x: bx + w / 2, y: by + h / 2, text, anchor: 'middle', baseline: 'middle', size: fs, weight: 600, fill: theme.surface });\n}\n\nconst finite = (...n: (number | undefined)[]) => n.every((v) => v === undefined || Number.isFinite(v));\n\n/** Drops marks with non-finite coordinates (junk data never reaches a renderer) and clamps the rest near the box. */\nfunction finalizeMarks(marks: ChartMark[], width: number, height: number): ChartMark[] {\n\tconst lim = (v: number, size: number) => Math.max(-size * 4, Math.min(size * 5, v));\n\tconst out: ChartMark[] = [];\n\tfor (const m of marks) {\n\t\tswitch (m.kind) {\n\t\t\tcase 'rect':\n\t\t\t\tif (!finite(m.x, m.y, m.width, m.height)) continue;\n\t\t\t\tout.push({ ...m, x: lim(m.x, width), y: lim(m.y, height), width: Math.max(0, Math.min(m.width, width * 9)), height: Math.max(0, Math.min(m.height, height * 9)) });\n\t\t\t\tbreak;\n\t\t\tcase 'circle':\n\t\t\t\tif (!finite(m.cx, m.cy, m.r)) continue;\n\t\t\t\tout.push({ ...m, cx: lim(m.cx, width), cy: lim(m.cy, height) });\n\t\t\t\tbreak;\n\t\t\tcase 'line':\n\t\t\t\tif (!finite(m.x1, m.y1, m.x2, m.y2)) continue;\n\t\t\t\tout.push({ ...m, x1: lim(m.x1, width), y1: lim(m.y1, height), x2: lim(m.x2, width), y2: lim(m.y2, height) });\n\t\t\t\tbreak;\n\t\t\tcase 'text':\n\t\t\t\tif (!finite(m.x, m.y, m.size)) continue;\n\t\t\t\tout.push(m);\n\t\t\t\tbreak;\n\t\t\tcase 'path':\n\t\t\t\tif (/NaN|Infinity/.test(m.d)) continue;\n\t\t\t\tout.push(m);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\treturn out;\n}\n\n/**\n * The data value at a scene point along the value axis (for dragging a bar or point), or\n * `undefined` when the scene has no value scale or is percent-stacked. `secondary` reads the\n * secondary axis.\n */\nexport function chartValueAtPoint(scene: ChartScene, point: { x: number; y: number }, secondary = false): number | undefined {\n\tconst scale = secondary ? (scene.valueScale2 ?? scene.valueScale) : scene.valueScale;\n\tif (!scale || scene.percent) return undefined;\n\tconst px = scale.axis === 'y' ? point.y : point.x;\n\treturn linearMap(px, scale.range, scale.domain);\n}\n\n/** The x value at a scene point on a scatter or bubble chart, or `undefined` without a numeric x axis. */\nexport function chartXAtPoint(scene: ChartScene, point: { x: number; y: number }): number | undefined {\n\tconst scale = scene.xScale;\n\tif (!scale) return undefined;\n\treturn linearMap(point.x, scale.range, scale.domain);\n}\n\n/** The value a drag should store: snapped to a tenth of the tick step (or `step`) and kept finite. */\nexport function snapChartValue(scene: ChartScene, value: number, step?: number, ticks: readonly number[] = scene.valueScale?.ticks ?? []): number {\n\tconst unit = step ?? (ticks.length > 1 ? (ticks[1] - ticks[0]) / 10 : 1);\n\tif (!(unit > 0)) return value;\n\tconst snapped = Math.round(value / unit) * unit;\n\treturn Number(snapped.toPrecision(12));\n}\n","// Numeric summaries for agents that check visuals without looking follow\n// filmcraft-main/crates/scopes/src/summary.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See\n// THIRD_PARTY_NOTICES.md.\nimport { checkChartColors, type ChartColorCheck } from './color.js';\nimport { layoutChart } from './layout.js';\nimport type { ChartLayoutOptions } from './layout-shared.js';\nimport { formatChartNumber } from './scale.js';\nimport { chartColorsByCategory, chartFamily, chartSeriesColor, chartSliceColor, CHART_TYPE_LABELS } from './spec.js';\nimport { summarizeChartSeries, type ChartSeriesSummary } from './stats.js';\nimport type { ChartScene, ChartSpec, ChartWarning } from './types.js';\n\nexport interface ChartSummary {\n\ttype: ChartSpec['type'];\n\ttitle: string;\n\tcategories: number;\n\tseries: ChartSeriesSummary[];\n\t/** The value axis as drawn: range and ticks (absent for pies and treemaps). */\n\tvalueAxis?: { min: number; max: number; ticks: number[] };\n\tsecondaryAxis?: { min: number; max: number; ticks: number[] };\n\txAxis?: { min: number; max: number; ticks: number[] };\n\t/** What the layout left out at this size (hidden labels, thinned lines, …). */\n\twarnings: ChartWarning[];\n\t/** Whether the series colours can be told apart (also with colour-blindness). */\n\tcolors: Pick<ChartColorCheck, 'ok' | 'issues'>;\n\t/** A plain-language description (the SVG `<desc>`). */\n\tdescription: string;\n}\n\n/**\n * Numbers about a chart for agents and screen readers: per-series min / max / mean / trend,\n * the axes as drawn, layout warnings at `size`, a colour check and a one-paragraph\n * description. Lets an agent verify a chart without rendering it.\n */\nexport function summarizeChart(spec: ChartSpec, size: Pick<ChartLayoutOptions, 'width' | 'height' | 'measureText'> = { width: 640, height: 400 }, scene: ChartScene = layoutChart(spec, { ...size, selection: null })): ChartSummary {\n\tconst visible = spec.series.filter((s) => !s.hidden);\n\tconst series = visible.map((s) => summarizeChartSeries(s, spec.categories));\n\tconst colors = chartColorsByCategory(spec.type) ? spec.categories.map((_, i) => visible[0]?.pointColors?.[i] || chartSliceColor(spec, i)) : visible.map((s) => chartSeriesColor(spec, spec.series.indexOf(s)));\n\tconst check = checkChartColors(colors, { surface: scene.theme.surface.startsWith('var(') ? '#ffffff' : scene.theme.surface });\n\tconst axis = (s: ChartScene['valueScale']) => (s ? { min: s.domain[0], max: s.domain[1], ticks: [...s.ticks] } : undefined);\n\treturn {\n\t\ttype: spec.type,\n\t\ttitle: spec.title,\n\t\tcategories: spec.categories.length,\n\t\tseries,\n\t\t...(scene.valueScale ? { valueAxis: axis(scene.valueScale) } : {}),\n\t\t...(scene.valueScale2 ? { secondaryAxis: axis(scene.valueScale2) } : {}),\n\t\t...(scene.xScale ? { xAxis: axis(scene.xScale) } : {}),\n\t\twarnings: scene.warnings,\n\t\tcolors: { ok: check.ok, issues: check.issues },\n\t\tdescription: describeChart(spec, series)\n\t};\n}\n\n/** A plain-language description of a chart: type, what it compares, ranges and notable extremes. */\nexport function describeChart(spec: ChartSpec, series = spec.series.filter((s) => !s.hidden).map((s) => summarizeChartSeries(s, spec.categories))): string {\n\tconst kind = CHART_TYPE_LABELS[spec.type].toLowerCase();\n\tconst parts: string[] = [];\n\tconst family = chartFamily(spec);\n\tconst format = spec.yAxis.format;\n\tconst fmt = (v: number | null) => (v === null ? 'blank' : formatChartNumber(v, format));\n\tconst head = spec.title ? `${spec.title}: a ${kind} chart` : `A ${kind} chart`;\n\tif (!series.length || series.every((s) => s.count === 0)) return `${head} with no data.`;\n\tif (family === 'pie' || family === 'funnel' || family === 'treemap') {\n\t\tconst s = series[0];\n\t\tconst total = s.sum ?? 0;\n\t\tconst share = (v: number | null) => (total && v !== null ? ` (${Math.round((v / total) * 100)}%)` : '');\n\t\tparts.push(`${head} of ${s.name} across ${spec.categories.length} categories, totalling ${fmt(s.sum)}.`);\n\t\tparts.push(`The largest is ${s.maxAt} at ${fmt(s.max)}${share(s.max)}; the smallest is ${s.minAt} at ${fmt(s.min)}${share(s.min)}.`);\n\t\treturn parts.join(' ');\n\t}\n\tif (family === 'waterfall') {\n\t\tconst s = spec.series.find((x) => !x.hidden)!;\n\t\tconst steps = s.values.filter((v, i) => v !== null && Number.isFinite(v) && !/\\b(sub)?total\\b/i.test(spec.categories[i] ?? '')) as number[];\n\t\tconst total = steps.reduce((t, v) => t + v, 0);\n\t\tconst up = steps.filter((v) => v > 0).length;\n\t\tconst down = steps.filter((v) => v < 0).length;\n\t\treturn `${head} of ${s.name}: ${up} increase${up === 1 ? '' : 's'} and ${down} decrease${down === 1 ? '' : 's'} over ${spec.categories.length} steps, ending at a total of ${fmt(total)}.`;\n\t}\n\tif (family === 'histogram' || family === 'box') {\n\t\tparts.push(`${head} of ${series.map((s) => s.name).join(', ')}.`);\n\t\tfor (const s of series) parts.push(`${s.name}: ${s.count} values from ${fmt(s.min)} to ${fmt(s.max)}, mean ${fmt(s.mean)}.`);\n\t\treturn parts.join(' ');\n\t}\n\tconst what = family === 'xy' ? `${series.length} series of points` : `${series.length} series over ${spec.categories.length} categories${spec.categories.length ? ` (${spec.categories[0]} to ${spec.categories[spec.categories.length - 1]})` : ''}`;\n\tparts.push(`${head} showing ${what}.`);\n\tfor (const s of series.slice(0, 8)) {\n\t\tif (!s.count) {\n\t\t\tparts.push(`${s.name} has no values.`);\n\t\t\tcontinue;\n\t\t}\n\t\tconst pct = s.change !== null ? Math.round(Math.abs(s.change) * 100) : 0;\n\t\tconst trend = s.trend && s.trend !== 'flat' ? `, ${s.trend}${pct ? `, ${s.change! > 0 ? 'up' : 'down'} ${pct}% from first to last` : ''}` : s.trend === 'flat' ? ', roughly flat' : '';\n\t\tparts.push(`${s.name} ranges from ${fmt(s.min)} (${s.minAt}) to ${fmt(s.max)} (${s.maxAt})${trend}.`);\n\t}\n\tif (series.length > 8) parts.push(`${series.length - 8} more series are not described.`);\n\treturn parts.join(' ');\n}\n","// `<title>` and `<desc>` on exported SVG follow vectorcraft-main/crates/svg/src/export.rs (MIT OR Apache-2.0,\n// © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\nimport { layoutChart, type ChartLayoutOptions } from './layout.js';\nimport { describeChart } from './summary.js';\nimport type { ChartMark, ChartScene, ChartSpec, ChartTextMark } from './types.js';\n\n/** SVG `dominant-baseline` for a text mark's baseline (shared by every renderer). */\nexport function chartTextBaseline(baseline: ChartTextMark['baseline']): 'hanging' | 'central' | 'text-after-edge' {\n\treturn baseline === 'top' ? 'hanging' : baseline === 'middle' ? 'central' : 'text-after-edge';\n}\n\n/** SVG `transform` for a rotated text mark, if any. */\nexport function chartTextTransform(mark: ChartTextMark): string | undefined {\n\treturn mark.rotate ? `rotate(${mark.rotate} ${mark.x} ${mark.y})` : undefined;\n}\n\nconst escape = (text: string) => text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n\n/** A mark as an SVG tag name, its attributes (unset ones dropped) and its text. */\nfunction markParts(mark: ChartMark, fontFamily: string): { tag: string; attrs: Record<string, string>; text?: string } {\n\tconst common = { opacity: mark.opacity, 'data-role': mark.role };\n\tconst parts = (tag: string, values: Record<string, string | number | undefined>, text?: string) => ({\n\t\ttag,\n\t\tattrs: Object.fromEntries(Object.entries(values).filter(([, v]) => v !== undefined && v !== '').map(([k, v]) => [k, String(v)])),\n\t\ttext\n\t});\n\tswitch (mark.kind) {\n\t\tcase 'rect':\n\t\t\treturn parts('rect', { x: mark.x, y: mark.y, width: mark.width, height: mark.height, rx: mark.radius, fill: mark.fill, stroke: mark.stroke, 'stroke-width': mark.strokeWidth, ...common });\n\t\tcase 'path':\n\t\t\treturn parts('path', { d: mark.d, fill: mark.fill ?? 'none', stroke: mark.stroke, 'stroke-width': mark.strokeWidth, 'stroke-dasharray': mark.dash, 'stroke-linejoin': 'round', 'stroke-linecap': 'round', ...common });\n\t\tcase 'circle':\n\t\t\treturn parts('circle', { cx: mark.cx, cy: mark.cy, r: mark.r, fill: mark.fill, stroke: mark.stroke, 'stroke-width': mark.strokeWidth, ...common });\n\t\tcase 'line':\n\t\t\treturn parts('line', { x1: mark.x1, y1: mark.y1, x2: mark.x2, y2: mark.y2, stroke: mark.stroke, 'stroke-width': mark.strokeWidth, 'stroke-dasharray': mark.dash, ...common });\n\t\tcase 'text':\n\t\t\treturn parts(\n\t\t\t\t'text',\n\t\t\t\t{\n\t\t\t\t\tx: mark.x,\n\t\t\t\t\ty: mark.y,\n\t\t\t\t\t'text-anchor': mark.anchor,\n\t\t\t\t\t'dominant-baseline': chartTextBaseline(mark.baseline),\n\t\t\t\t\t'font-size': mark.size,\n\t\t\t\t\t'font-weight': mark.weight,\n\t\t\t\t\t'font-family': fontFamily,\n\t\t\t\t\tfill: mark.fill,\n\t\t\t\t\ttransform: chartTextTransform(mark),\n\t\t\t\t\t...common\n\t\t\t\t},\n\t\t\t\tmark.text\n\t\t\t);\n\t}\n}\n\nfunction markToSvg(mark: ChartMark, fontFamily: string): string {\n\tconst { tag, attrs, text } = markParts(mark, fontFamily);\n\tconst attrText = Object.entries(attrs)\n\t\t.map(([k, v]) => ` ${k}=\"${escape(v)}\"`)\n\t\t.join('');\n\treturn text === undefined ? `<${tag}${attrText}/>` : `<${tag}${attrText}>${escape(text)}</${tag}>`;\n}\n\nexport interface ChartSvgOptions {\n\t/** Leave out selection outlines (default `true`: exports show the chart only). */\n\thideSelection?: boolean;\n\t/** Accessible title (default: the chart title). */\n\tariaLabel?: string;\n\t/** A longer description written as `<desc>` (`chartToSvg` defaults to `describeChart`). */\n\tdescription?: string;\n}\n\n/** Writes a laid-out scene as a standalone SVG document string. */\nexport function chartSceneToSvg(scene: ChartScene, options: ChartSvgOptions = {}): string {\n\tconst hide = options.hideSelection ?? true;\n\tconst font = scene.theme.fontFamily;\n\tconst body = scene.marks\n\t\t.filter((m) => !(hide && m.role === 'selection'))\n\t\t.map((m) => markToSvg(m, font))\n\t\t.join('');\n\tconst label = options.ariaLabel;\n\tconst desc = options.description;\n\tconst labelled = [label ? 'chart-title' : '', desc ? 'chart-desc' : ''].filter(Boolean).join(' ');\n\tconst head = `${label ? `<title id=\"chart-title\">${escape(label)}</title>` : ''}${desc ? `<desc id=\"chart-desc\">${escape(desc)}</desc>` : ''}`;\n\treturn `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${scene.width}\" height=\"${scene.height}\" viewBox=\"0 0 ${scene.width} ${scene.height}\" role=\"img\"${label ? ` aria-label=\"${escape(label)}\"` : ''}${labelled ? ` aria-labelledby=\"${labelled}\"` : ''}>${head}${body}</svg>`;\n}\n\n/** Lays out and writes a chart as SVG, e.g. for export or server rendering. */\nexport function chartToSvg(spec: ChartSpec, options: Omit<ChartLayoutOptions, 'selection'> & ChartSvgOptions): string {\n\tconst scene = layoutChart(spec, { ...options, selection: null });\n\treturn chartSceneToSvg(scene, { ...options, ariaLabel: options.ariaLabel ?? (spec.title || undefined), description: options.description ?? describeChart(spec) });\n}\n\nconst SVG_NS = 'http://www.w3.org/2000/svg';\n\n/** A ProseMirror / TipTap `DOMOutputSpec` array (kept structural so this module needs no TipTap types). */\nexport type ChartDomSpec = [string, Record<string, string>, ...(ChartDomSpec | string)[]];\n\n/**\n * Writes a laid-out scene as a ProseMirror `DOMOutputSpec` (SVG-namespaced tags), so a\n * TipTap node's `renderHTML` can draw a chart without parsing an SVG string. Same output\n * as `chartSceneToSvg`.\n */\nexport function chartSceneToDomSpec(scene: ChartScene, options: ChartSvgOptions & { attrs?: Record<string, string> } = {}): ChartDomSpec {\n\tconst hide = options.hideSelection ?? true;\n\tconst font = scene.theme.fontFamily;\n\tconst tag = (name: string) => `${SVG_NS} ${name}`;\n\tconst children: ChartDomSpec[] = [];\n\tif (options.ariaLabel) children.push([tag('title'), {}, options.ariaLabel]);\n\tif (options.description) children.push([tag('desc'), {}, options.description]);\n\tfor (const mark of scene.marks) {\n\t\tif (hide && mark.role === 'selection') continue;\n\t\tconst { tag: name, attrs, text } = markParts(mark, font);\n\t\tchildren.push(text === undefined ? [tag(name), attrs] : [tag(name), attrs, text]);\n\t}\n\tconst attrs: Record<string, string> = {\n\t\twidth: String(scene.width),\n\t\theight: String(scene.height),\n\t\tviewBox: `0 0 ${scene.width} ${scene.height}`,\n\t\trole: 'img',\n\t\t...(options.ariaLabel ? { 'aria-label': options.ariaLabel } : {}),\n\t\t...options.attrs\n\t};\n\treturn [tag('svg'), attrs, ...children];\n}\n","// Quick layouts and colour schemes, as GridCraft's `chart.quickLayout` and `chart.changeColors` offer them\n// (gridcraft-main/crates/engine/src/cmd/extra.rs; MIT OR Apache-2.0, © ArtCraft Team and contributors).\n// See THIRD_PARTY_NOTICES.md.\nimport { shadeChartColor } from './color.js';\nimport { DEFAULT_CHART_PALETTE } from './spec.js';\nimport type { ChartAxis, ChartLegend, ChartSpec } from './types.js';\n\n/** A partial change several fields at once (what `ChartController.update` takes). */\nexport type ChartPresetPatch = Partial<Omit<ChartSpec, 'version' | 'xAxis' | 'yAxis' | 'yAxis2' | 'legend' | 'series' | 'categories'>> & {\n\txAxis?: Partial<ChartAxis>;\n\tyAxis?: Partial<ChartAxis>;\n\tyAxis2?: Partial<ChartAxis>;\n\tlegend?: Partial<ChartLegend>;\n};\n\nexport interface ChartLayoutPreset {\n\tid: string;\n\tlabel: string;\n\tdescription: string;\n\tpatch: ChartPresetPatch;\n}\n\n/** Quick layouts: legend, labels and grid lines set together. */\nexport const CHART_LAYOUT_PRESETS: readonly ChartLayoutPreset[] = [\n\t{ id: 'standard', label: 'Standard', description: 'Legend at the bottom, value grid lines, no data labels.', patch: { legend: { show: true, position: 'bottom' }, dataLabels: false, xAxis: { gridlines: false, show: true }, yAxis: { gridlines: true, show: true } } },\n\t{ id: 'minimal', label: 'Minimal', description: 'No legend, grid lines or axis titles; the data speaks.', patch: { legend: { show: false }, dataLabels: false, xAxis: { gridlines: false, title: '' }, yAxis: { gridlines: false, title: '' } } },\n\t{ id: 'labelled', label: 'Labelled', description: 'Values written on the marks, no value axis or grid lines.', patch: { dataLabels: true, labelPosition: 'auto', yAxis: { show: false, gridlines: false }, xAxis: { gridlines: false } } },\n\t{ id: 'legend-right', label: 'Legend on the right', description: 'Legend beside the plot, value grid lines.', patch: { legend: { show: true, position: 'right' }, yAxis: { gridlines: true } } },\n\t{ id: 'presentation', label: 'Presentation', description: 'Legend on top, data labels, light grid.', patch: { legend: { show: true, position: 'top' }, dataLabels: true, yAxis: { gridlines: true }, xAxis: { gridlines: false } } },\n\t{ id: 'analysis', label: 'Analysis', description: 'Grid lines on both axes, point markers, legend at the bottom.', patch: { legend: { show: true, position: 'bottom' }, points: true, dataLabels: false, xAxis: { gridlines: true }, yAxis: { gridlines: true } } }\n];\n\nexport interface ChartColorScheme {\n\tid: string;\n\tlabel: string;\n\tdescription: string;\n\tpalette: readonly string[];\n}\n\n/** Shades of one hue, dark to light (for monochrome schemes). */\nexport function monochromeChartPalette(base: string, count = 6): string[] {\n\tconst steps = count <= 1 ? [0] : Array.from({ length: count }, (_, i) => -0.45 + (1.05 * i) / (count - 1));\n\treturn steps.map((t) => shadeChartColor(base, t));\n}\n\n/** Named palettes. `default` and `okabe-ito` are colour-blind friendly. */\nexport const CHART_COLOR_SCHEMES: readonly ChartColorScheme[] = [\n\t{ id: 'default', label: 'Default', description: 'Ten distinct colours, readable on light and dark backgrounds.', palette: DEFAULT_CHART_PALETTE },\n\t{ id: 'okabe-ito', label: 'Okabe–Ito', description: 'Eight colours chosen to stay distinct with colour-blindness.', palette: ['#e69f00', '#56b4e9', '#009e73', '#f0e442', '#0072b2', '#d55e00', '#cc79a7', '#000000'] },\n\t{ id: 'office', label: 'Office', description: 'The six accent colours of the default Office theme.', palette: ['#4472c4', '#ed7d31', '#a5a5a5', '#ffc000', '#5b9bd5', '#70ad47'] },\n\t{ id: 'warm', label: 'Warm', description: 'Reds, oranges and yellows.', palette: ['#b2182b', '#d6604d', '#f4a582', '#e08214', '#fdb863', '#8c510a'] },\n\t{ id: 'cool', label: 'Cool', description: 'Blues, teals and purples.', palette: ['#2166ac', '#4393c3', '#35978f', '#80cdc1', '#762a83', '#9970ab'] },\n\t{ id: 'monochrome-blue', label: 'Monochrome blue', description: 'Shades of one blue.', palette: monochromeChartPalette('#4269d0') },\n\t{ id: 'monochrome-green', label: 'Monochrome green', description: 'Shades of one green.', palette: monochromeChartPalette('#3ca951') },\n\t{ id: 'monochrome-orange', label: 'Monochrome orange', description: 'Shades of one orange.', palette: monochromeChartPalette('#e8710a') },\n\t{ id: 'monochrome-gray', label: 'Monochrome grey', description: 'Shades of grey.', palette: monochromeChartPalette('#6b7280') }\n];\n\n/** The change a named layout and / or colour scheme makes, or a sentence saying which names exist. */\nexport function chartPresetPatch(options: { layout?: string; colors?: string }): { ok: true; patch: ChartPresetPatch } | { ok: false; message: string } {\n\tconst patch: ChartPresetPatch = {};\n\tif (options.layout !== undefined) {\n\t\tconst preset = CHART_LAYOUT_PRESETS.find((p) => p.id === options.layout);\n\t\tif (!preset) return { ok: false, message: `There is no layout \"${options.layout}\"; expected one of ${CHART_LAYOUT_PRESETS.map((p) => `\"${p.id}\"`).join(', ')}.` };\n\t\tObject.assign(patch, preset.patch);\n\t}\n\tif (options.colors !== undefined) {\n\t\tconst scheme = CHART_COLOR_SCHEMES.find((p) => p.id === options.colors);\n\t\tif (!scheme) return { ok: false, message: `There is no colour scheme \"${options.colors}\"; expected one of ${CHART_COLOR_SCHEMES.map((p) => `\"${p.id}\"`).join(', ')}.` };\n\t\tpatch.palette = [...scheme.palette];\n\t}\n\tif (options.layout === undefined && options.colors === undefined) return { ok: false, message: 'Name a layout, a colour scheme or both.' };\n\treturn { ok: true, patch };\n}\n","import { HistoryManager } from '../history/HistoryManager.js';\nimport type { Command, HistoryMark } from '../history/types.js';\nimport { parseCsv, parseTsv } from '../spreadsheet/csv.js';\nimport { sameChartSelection } from './layout.js';\nimport { chartPresetPatch } from './presets.js';\nimport { chartFamily } from './spec.js';\nimport { chartMovingAverage, chartSupportsTrendlines, fitChartTrendline, type ChartTrendFit } from './trend.js';\nimport {\n\tchartDataFromTable,\n\tnormalizeChartTrendline,\n\tcloneChartSeries,\n\tcloneChartSpec,\n\tnormalizeChartSpec,\n\tparseChartValue,\n\tuniqueChartSeriesId,\n\ttype ChartTableOptions\n} from './spec.js';\nimport type {\n\tChartAxis,\n\tChartIssue,\n\tChartCurve,\n\tChartLegend,\n\tChartMarker,\n\tChartNumberFormat,\n\tChartSelection,\n\tChartSeries,\n\tChartSeriesType,\n\tChartSpec,\n\tChartTrendline,\n\tChartType,\n\tChartValue\n} from './types.js';\n\n/** Structural equality of JSON-like values (specs are plain data; key order does not matter). */\nfunction sameChartJson(a: unknown, b: unknown): boolean {\n\tif (a === b) return true;\n\tif (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return Number.isNaN(a) && Number.isNaN(b);\n\tif (Array.isArray(a) !== Array.isArray(b)) return false;\n\tif (Array.isArray(a)) {\n\t\tconst bb = b as unknown[];\n\t\treturn a.length === bb.length && a.every((v, i) => sameChartJson(v, bb[i]));\n\t}\n\tconst ao = a as Record<string, unknown>;\n\tconst bo = b as Record<string, unknown>;\n\tconst keys = Object.keys(ao).filter((k) => ao[k] !== undefined);\n\tif (keys.length !== Object.keys(bo).filter((k) => bo[k] !== undefined).length) return false;\n\treturn keys.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && sameChartJson(ao[k], bo[k]));\n}\n\nexport interface ChartControllerOptions {\n\t/** Starting chart (any partial or saved spec; problems are reported in `state.issues`). */\n\tspec?: Partial<ChartSpec> | unknown;\n\t/** History to record into, e.g. one shared with the rest of the app (default: a new one). */\n\thistory?: HistoryManager;\n\t/** Called after the spec changes (edit, undo, redo, load). */\n\tonChange?: (spec: Readonly<ChartSpec>) => void;\n}\n\n/** A snapshot of the controller. The same object is returned until something changes. */\nexport interface ChartState {\n\tspec: Readonly<ChartSpec>;\n\tselection: ChartSelection | null;\n\t/** The series the selection belongs to (a series, one of its points or one of its trendlines). */\n\tselectedSeries: ChartSeries | null;\n\t/** The category the selection belongs to (a category or a point). */\n\tselectedIndex: number | null;\n\t/** Problems from the last load or rejected edit, as sentences. */\n\tissues: readonly ChartIssue[];\n\t/** The gesture in progress (e.g. a bar drag), if any. */\n\tgesture: string | null;\n\tcanUndo: boolean;\n\tcanRedo: boolean;\n\tundoLabel: string;\n\tredoLabel: string;\n\t/** Bumped on every change. */\n\trevision: number;\n}\n\nexport type ChartActionResult = { ok: true } | { ok: false; message: string; issues?: ChartIssue[] };\n\n/** Top-level fields with partial axes and legend. */\nexport type ChartSpecPatch = Partial<Omit<ChartSpec, 'version' | 'xAxis' | 'yAxis' | 'yAxis2' | 'legend'>> & {\n\txAxis?: Partial<ChartAxis>;\n\tyAxis?: Partial<ChartAxis>;\n\tyAxis2?: Partial<ChartAxis>;\n\tlegend?: Partial<ChartLegend>;\n};\n\nexport interface ChartSeriesInput {\n\tid?: string;\n\tname?: string;\n\tvalues?: readonly unknown[];\n\tcolor?: string;\n\ttype?: ChartSeriesType;\n\thidden?: boolean;\n\taxis?: 'primary' | 'secondary';\n\tx?: readonly unknown[];\n\tsizes?: readonly unknown[];\n\tpointColors?: readonly (string | null)[];\n\tmarker?: ChartMarker;\n\tcurve?: ChartCurve;\n\tdataLabels?: boolean;\n\tformat?: ChartNumberFormat;\n\ttrendlines?: readonly ChartTrendline[];\n}\n\n/** Changes to one series; `null` resets a field to the chart's default. */\nexport interface ChartSeriesPatch {\n\tname?: string;\n\t/** `null` goes back to the palette colour. */\n\tcolor?: string | null;\n\t/** `null` follows the chart type. */\n\ttype?: ChartSeriesType | null;\n\thidden?: boolean;\n\taxis?: 'primary' | 'secondary' | null;\n\tx?: readonly unknown[] | null;\n\tsizes?: readonly unknown[] | null;\n\tpointColors?: readonly (string | null)[] | null;\n\tmarker?: ChartMarker | null;\n\tcurve?: ChartCurve | null;\n\tdataLabels?: boolean | null;\n\tformat?: ChartNumberFormat | null;\n}\n\nexport interface ChartEditOptions {\n\t/** Undo step label. */\n\tlabel?: string;\n}\n\nconst fail = (message: string, issues?: ChartIssue[]): ChartActionResult => ({ ok: false, message, ...(issues ? { issues } : {}) });\n\n/**\n * Headless state for a chart editor: a validated `ChartSpec`, a selection, and undoable\n * edits to data (values, series, categories, pasted grids, CSV) and presentation (type,\n * titles, axes, legend, stacking, …). Drags merge into one undo step (`beginGesture`).\n * `getState()` returns the same object until something changes, so it works with\n * `useSyncExternalStore` and Svelte runes alike.\n */\nexport class ChartController {\n\treadonly history: HistoryManager;\n\tprivate spec: ChartSpec;\n\tprivate selection: ChartSelection | null = null;\n\tprivate issues: ChartIssue[] = [];\n\tprivate gestureKey: string | null = null;\n\tprivate gestureCount = 0;\n\tprivate gestureStart: { spec: ChartSpec; mark: HistoryMark } | null = null;\n\tprivate revision = 0;\n\tprivate applying = false;\n\tprivate readonly onChange?: ChartControllerOptions['onChange'];\n\tprivate readonly listeners = new Set<(state: ChartState) => void>();\n\tprivate unsubscribeHistory?: () => void;\n\tprivate state!: ChartState;\n\n\tconstructor(options: ChartControllerOptions = {}) {\n\t\tthis.history = options.history ?? new HistoryManager();\n\t\tthis.onChange = options.onChange;\n\t\tconst { spec, issues } = normalizeChartSpec(options.spec ?? {});\n\t\tthis.spec = spec;\n\t\tthis.issues = issues;\n\t\tthis.state = this.computeState();\n\t\tthis.connect();\n\t}\n\n\tprivate connect() {\n\t\tthis.unsubscribeHistory?.();\n\t\tthis.unsubscribeHistory = this.history.subscribe((h) => {\n\t\t\t// Edits notify as they apply; only re-notify when the undo/redo state moved on.\n\t\t\tconst s = this.state;\n\t\t\tif (this.applying || (s.canUndo === h.canUndo && s.canRedo === h.canRedo && s.undoLabel === h.undoName && s.redoLabel === h.redoName)) return;\n\t\t\tthis.notify(false);\n\t\t});\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// State\n\t// -------------------------------------------------------------------------\n\n\t/** The current snapshot (stable until the next change). */\n\tgetState = (): ChartState => this.state;\n\n\t/** Calls `listener` after every change. Returns an unsubscribe function. */\n\tsubscribe = (listener: (state: ChartState) => void): (() => void) => {\n\t\t// Reconnect after `destroy()` so a remount (React StrictMode) keeps undo working.\n\t\tif (!this.unsubscribeHistory) this.connect();\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t};\n\n\t/** The current spec (do not mutate; edit through the controller). */\n\tgetSpec(): Readonly<ChartSpec> {\n\t\treturn this.spec;\n\t}\n\n\tgetSeries(id: string): ChartSeries | undefined {\n\t\treturn this.spec.series.find((s) => s.id === id);\n\t}\n\n\tprivate computeState(): ChartState {\n\t\tconst h = this.history.getState();\n\t\tconst sel = this.selection;\n\t\tconst seriesId = sel?.kind === 'series' || sel?.kind === 'point' || sel?.kind === 'trendline' ? sel.seriesId : null;\n\t\treturn {\n\t\t\tspec: this.spec,\n\t\t\tselection: sel,\n\t\t\tselectedSeries: seriesId ? (this.spec.series.find((s) => s.id === seriesId) ?? null) : null,\n\t\t\tselectedIndex: sel?.kind === 'point' || sel?.kind === 'category' ? sel.index : null,\n\t\t\tissues: this.issues,\n\t\t\tgesture: this.gestureKey,\n\t\t\tcanUndo: h.canUndo,\n\t\t\tcanRedo: h.canRedo,\n\t\t\tundoLabel: h.undoName,\n\t\t\tredoLabel: h.redoName,\n\t\t\trevision: this.revision\n\t\t};\n\t}\n\n\tprivate notify(specChanged: boolean) {\n\t\tthis.revision++;\n\t\tthis.state = this.computeState();\n\t\tif (specChanged) this.onChange?.(this.spec);\n\t\tconst state = this.state;\n\t\tthis.listeners.forEach((listener) => {\n\t\t\ttry {\n\t\t\t\tlistener(state);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('[ChartController] Error in state listener:', error);\n\t\t\t}\n\t\t});\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Changes: every edit records a before / after snapshot of the spec\n\t// -------------------------------------------------------------------------\n\n\t/** Drops a selection that no longer points at anything. */\n\tprivate validSelection(sel: ChartSelection | null, spec: ChartSpec): ChartSelection | null {\n\t\tif (!sel) return null;\n\t\tswitch (sel.kind) {\n\t\t\tcase 'series':\n\t\t\t\treturn spec.series.some((s) => s.id === sel.seriesId) ? sel : null;\n\t\t\tcase 'point':\n\t\t\t\treturn spec.series.some((s) => s.id === sel.seriesId) && sel.index < spec.categories.length ? sel : null;\n\t\t\tcase 'category':\n\t\t\t\treturn sel.index < spec.categories.length ? sel : null;\n\t\t\tcase 'trendline':\n\t\t\t\treturn spec.series.find((s) => s.id === sel.seriesId)?.trendlines?.[sel.index] ? sel : null;\n\t\t\tcase 'title':\n\t\t\t\treturn spec.title ? sel : null;\n\t\t\tcase 'subtitle':\n\t\t\t\treturn spec.subtitle ? sel : null;\n\t\t\tdefault:\n\t\t\t\treturn sel;\n\t\t}\n\t}\n\n\tprivate applySpec(spec: ChartSpec) {\n\t\tthis.applying = true;\n\t\ttry {\n\t\t\tthis.spec = spec;\n\t\t\tthis.selection = this.validSelection(this.selection, spec);\n\t\t} finally {\n\t\t\tthis.applying = false;\n\t\t}\n\t\tthis.notify(true);\n\t}\n\n\tprivate commit(label: string, next: ChartSpec, select?: ChartSelection | null) {\n\t\tconst before = this.spec;\n\t\tconst hadIssues = this.issues.length > 0;\n\t\tthis.issues = [];\n\t\t// A no-op edit (same value, same title, …) records no undo step.\n\t\tif (sameChartJson(before, next)) {\n\t\t\tconst reselect = select !== undefined && !sameChartSelection(this.selection, select) && !(select === null && this.selection === null);\n\t\t\tif (reselect) this.selection = select;\n\t\t\tif (reselect || hadIssues) this.notify(false);\n\t\t\treturn;\n\t\t}\n\t\tconst command: Command = {\n\t\t\tname: label,\n\t\t\ttag: 'chart',\n\t\t\texecute: () => this.applySpec(next),\n\t\t\tundo: () => this.applySpec(before),\n\t\t\tredo: () => this.applySpec(next)\n\t\t};\n\t\tif (select !== undefined) this.selection = select;\n\t\tif (this.gestureKey !== null) this.history.executeMerged(command, { key: `chart.gesture:${this.gestureKey}:${this.gestureCount}` });\n\t\telse this.history.execute(command);\n\t}\n\n\tprivate reject(message: string, issues: ChartIssue[] = []): ChartActionResult {\n\t\tthis.issues = issues.length ? issues : [{ path: '', message }];\n\t\tthis.notify(false);\n\t\treturn fail(message, issues.length ? issues : undefined);\n\t}\n\n\t/** A copy of the spec with `edit` applied (series and categories copied as needed). */\n\tprivate edited(edit: (draft: ChartSpec) => void): ChartSpec {\n\t\tconst draft = cloneChartSpec(this.spec);\n\t\tedit(draft);\n\t\treturn draft;\n\t}\n\n\tprivate unknownSeries(id: string): ChartActionResult {\n\t\tconst ids = this.spec.series.map((s) => `\"${s.id}\"`);\n\t\treturn fail(`There is no series \"${id}\"; ${ids.length ? `expected one of ${ids.join(', ')}` : 'the chart has no series yet'}.`);\n\t}\n\n\tprivate badIndex(index: number): ChartActionResult {\n\t\tconst n = this.spec.categories.length;\n\t\treturn fail(n ? `Category index ${index} is out of range; expected 0 to ${n - 1}.` : 'The chart has no categories yet.');\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Presentation\n\t// -------------------------------------------------------------------------\n\n\t/**\n\t * Changes any presentation or data fields as one undo step. All-or-nothing: if anything\n\t * is invalid, nothing changes and every problem is returned (and shown in `state.issues`).\n\t */\n\tupdate(patch: ChartSpecPatch, options: ChartEditOptions = {}): ChartActionResult {\n\t\tconst s = this.spec;\n\t\tconst merged = {\n\t\t\t...s,\n\t\t\t...patch,\n\t\t\txAxis: patch.xAxis ? { ...s.xAxis, ...patch.xAxis } : s.xAxis,\n\t\t\tyAxis: patch.yAxis ? { ...s.yAxis, ...patch.yAxis } : s.yAxis,\n\t\t\tyAxis2: patch.yAxis2 ? { ...s.yAxis2, ...patch.yAxis2 } : s.yAxis2,\n\t\t\tlegend: patch.legend ? { ...s.legend, ...patch.legend } : s.legend,\n\t\t\tversion: 1\n\t\t};\n\t\tconst { spec, issues } = normalizeChartSpec(merged);\n\t\tif (issues.length) return this.reject(issues.map((i) => i.message).join(' '), issues);\n\t\tif (JSON.stringify(spec) === JSON.stringify(s)) return { ok: true };\n\t\tthis.commit(options.label ?? updateLabel(patch), spec);\n\t\treturn { ok: true };\n\t}\n\n\tsetType(type: ChartType): ChartActionResult {\n\t\treturn this.update({ type }, { label: `Change chart type` });\n\t}\n\n\tsetTitle(title: string): ChartActionResult {\n\t\treturn this.update({ title }, { label: 'Edit title' });\n\t}\n\n\tsetSubtitle(subtitle: string): ChartActionResult {\n\t\treturn this.update({ subtitle }, { label: 'Edit subtitle' });\n\t}\n\n\t/** Changes the category (`x`), value (`y`) or secondary value (`y2`) axis. */\n\tsetAxis(axis: 'x' | 'y' | 'y2', patch: Partial<ChartAxis>): ChartActionResult {\n\t\tconst key = axis === 'x' ? 'xAxis' : axis === 'y' ? 'yAxis' : 'yAxis2';\n\t\treturn this.update({ [key]: patch }, { label: `Change ${axis === 'x' ? 'category' : axis === 'y' ? 'value' : 'secondary value'} axis` });\n\t}\n\n\t/**\n\t * Applies a named quick layout and / or colour scheme (see `CHART_LAYOUT_PRESETS` and\n\t * `CHART_COLOR_SCHEMES`) as one undo step.\n\t */\n\tapplyPreset(options: { layout?: string; colors?: string }): ChartActionResult {\n\t\tconst preset = chartPresetPatch(options);\n\t\tif (!preset.ok) return this.reject(preset.message);\n\t\tconst label = options.layout && options.colors ? 'Apply chart style' : options.layout ? 'Apply quick layout' : 'Change colours';\n\t\treturn this.update(preset.patch, { label });\n\t}\n\n\tsetLegend(patch: Partial<ChartLegend>): ChartActionResult {\n\t\treturn this.update({ legend: patch }, { label: 'Change legend' });\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Data\n\t// -------------------------------------------------------------------------\n\n\t/**\n\t * Replaces categories and / or series as one undo step. Values are validated and padded\n\t * to the categories; all-or-nothing like `update`.\n\t */\n\tsetData(data: { categories?: readonly unknown[]; series?: readonly ChartSeriesInput[] }, options: ChartEditOptions = {}): ChartActionResult {\n\t\treturn this.update({ ...(data.categories ? { categories: data.categories as string[] } : {}), ...(data.series ? { series: data.series as ChartSeries[] } : {}) }, { label: options.label ?? 'Edit chart data' });\n\t}\n\n\t/**\n\t * Sets one value (numbers, numeric text such as `1,200` or `12%`, or blank for a gap).\n\t * One undo step, or part of the current gesture.\n\t */\n\tsetValue(seriesId: string, index: number, value: unknown, options: ChartEditOptions = {}): ChartActionResult {\n\t\tconst series = this.getSeries(seriesId);\n\t\tif (!series) return this.unknownSeries(seriesId);\n\t\tif (!Number.isInteger(index) || index < 0 || index >= this.spec.categories.length) return this.badIndex(index);\n\t\tconst parsed = parseChartValue(value);\n\t\tif (parsed === undefined) return this.reject(`\"${String(value)}\" is not a number; enter a number or leave the cell blank.`, [{ path: `${seriesId}[${index}]`, message: `\"${String(value)}\" is not a number.` }]);\n\t\tif (series.values[index] === parsed) {\n\t\t\tif (this.issues.length) {\n\t\t\t\tthis.issues = [];\n\t\t\t\tthis.notify(false);\n\t\t\t}\n\t\t\treturn { ok: true };\n\t\t}\n\t\tconst next = this.edited((d) => {\n\t\t\td.series.find((s) => s.id === seriesId)!.values[index] = parsed;\n\t\t});\n\t\tthis.commit(options.label ?? `Edit ${series.name} · ${this.spec.categories[index]}`, next);\n\t\treturn { ok: true };\n\t}\n\n\t/** Several values as one undo step (all-or-nothing). */\n\tsetValues(cells: readonly { seriesId: string; index: number; value: unknown }[], options: ChartEditOptions = {}): ChartActionResult {\n\t\tconst issues: ChartIssue[] = [];\n\t\tconst parsed: { seriesId: string; index: number; value: ChartValue }[] = [];\n\t\tfor (const cell of cells) {\n\t\t\tif (!this.getSeries(cell.seriesId)) issues.push({ path: cell.seriesId, message: (this.unknownSeries(cell.seriesId) as { message: string }).message });\n\t\t\telse if (!Number.isInteger(cell.index) || cell.index < 0 || cell.index >= this.spec.categories.length) issues.push({ path: `${cell.seriesId}[${cell.index}]`, message: (this.badIndex(cell.index) as { message: string }).message });\n\t\t\telse {\n\t\t\t\tconst value = parseChartValue(cell.value);\n\t\t\t\tif (value === undefined) issues.push({ path: `${cell.seriesId}[${cell.index}]`, message: `\"${String(cell.value)}\" is not a number.` });\n\t\t\t\telse parsed.push({ seriesId: cell.seriesId, index: cell.index, value });\n\t\t\t}\n\t\t}\n\t\tif (issues.length) return this.reject(issues.map((i) => i.message).join(' '), issues);\n\t\tconst next = this.edited((d) => {\n\t\t\tfor (const cell of parsed) d.series.find((s) => s.id === cell.seriesId)!.values[cell.index] = cell.value;\n\t\t});\n\t\tif (JSON.stringify(next.series) !== JSON.stringify(this.spec.series)) this.commit(options.label ?? 'Edit values', next);\n\t\treturn { ok: true };\n\t}\n\n\t/** Adds a series (values padded to the categories) and selects it. Returns its id. */\n\taddSeries(input: ChartSeriesInput = {}, options: { at?: number } = {}): { ok: true; id: string } | { ok: false; message: string; issues?: ChartIssue[] } {\n\t\tconst id = input.id && !this.getSeries(input.id) ? input.id : uniqueChartSeriesId(this.spec.series);\n\t\tconst name = input.name ?? `Series ${this.spec.series.length + 1}`;\n\t\tconst at = Math.max(0, Math.min(this.spec.series.length, options.at ?? this.spec.series.length));\n\t\tconst series = [...this.spec.series];\n\t\tseries.splice(at, 0, { ...input, id, name, values: (input.values ?? []) as ChartValue[] } as unknown as ChartSeries);\n\t\tconst { spec, issues } = normalizeChartSpec({ ...this.spec, series });\n\t\tif (issues.length) return this.reject(issues.map((i) => i.message).join(' '), issues) as { ok: false; message: string };\n\t\tthis.commit(`Add series \"${name}\"`, spec, { kind: 'series', seriesId: id });\n\t\treturn { ok: true, id };\n\t}\n\n\tremoveSeries(id: string): ChartActionResult {\n\t\tconst series = this.getSeries(id);\n\t\tif (!series) return this.unknownSeries(id);\n\t\tthis.commit(\n\t\t\t`Delete series \"${series.name}\"`,\n\t\t\tthis.edited((d) => {\n\t\t\t\td.series = d.series.filter((s) => s.id !== id);\n\t\t\t})\n\t\t);\n\t\treturn { ok: true };\n\t}\n\n\tupdateSeries(id: string, patch: ChartSeriesPatch): ChartActionResult {\n\t\tconst series = this.getSeries(id);\n\t\tif (!series) return this.unknownSeries(id);\n\t\tconst next = cloneChartSeries(series);\n\t\tif (patch.name !== undefined) next.name = String(patch.name);\n\t\tif (patch.color === null || patch.color === '') delete next.color;\n\t\telse if (patch.color !== undefined) next.color = patch.color;\n\t\tif (patch.type === null) delete next.type;\n\t\telse if (patch.type !== undefined) next.type = patch.type;\n\t\tif (patch.hidden !== undefined) {\n\t\t\tif (patch.hidden) next.hidden = true;\n\t\t\telse delete next.hidden;\n\t\t}\n\t\tconst resettable = ['axis', 'x', 'sizes', 'pointColors', 'marker', 'curve', 'dataLabels', 'format'] as const;\n\t\tfor (const key of resettable) {\n\t\t\tif (!(key in patch) || patch[key] === undefined) continue;\n\t\t\tif (patch[key] === null) delete (next as any)[key];\n\t\t\telse (next as any)[key] = patch[key];\n\t\t}\n\t\tif (next.axis === 'primary') delete next.axis;\n\t\tconst series2 = this.spec.series.map((s) => (s.id === id ? next : s));\n\t\tconst { spec, issues } = normalizeChartSpec({ ...this.spec, series: series2 });\n\t\tif (issues.length) return this.reject(issues.map((i) => i.message).join(' '), issues);\n\t\tif (JSON.stringify(spec.series) === JSON.stringify(this.spec.series)) return { ok: true };\n\t\tconst label =\n\t\t\tpatch.hidden !== undefined && Object.keys(patch).length === 1\n\t\t\t\t? `${patch.hidden ? 'Hide' : 'Show'} \"${series.name}\"`\n\t\t\t\t: patch.name !== undefined && Object.keys(patch).length === 1\n\t\t\t\t\t? 'Rename series'\n\t\t\t\t\t: `Change series \"${series.name}\"`;\n\t\tthis.commit(label, spec);\n\t\treturn { ok: true };\n\t}\n\n\ttoggleSeries(id: string): ChartActionResult {\n\t\tconst series = this.getSeries(id);\n\t\tif (!series) return this.unknownSeries(id);\n\t\treturn this.updateSeries(id, { hidden: !series.hidden });\n\t}\n\n\t/** Moves a series to position `to` (drawing and legend order). */\n\tmoveSeries(id: string, to: number): ChartActionResult {\n\t\tconst from = this.spec.series.findIndex((s) => s.id === id);\n\t\tif (from < 0) return this.unknownSeries(id);\n\t\tconst target = Math.max(0, Math.min(this.spec.series.length - 1, to));\n\t\tif (target === from) return { ok: true };\n\t\tthis.commit(\n\t\t\t'Reorder series',\n\t\t\tthis.edited((d) => {\n\t\t\t\tconst [s] = d.series.splice(from, 1);\n\t\t\t\td.series.splice(target, 0, s);\n\t\t\t})\n\t\t);\n\t\treturn { ok: true };\n\t}\n\n\t/** Inserts a category (with gaps in every series) at `at` (default: the end). Returns its index. */\n\taddCategory(label?: string, options: { at?: number } = {}): { ok: true; index: number } {\n\t\tconst at = Math.max(0, Math.min(this.spec.categories.length, options.at ?? this.spec.categories.length));\n\t\tconst name = label ?? `${this.spec.categories.length + 1}`;\n\t\tthis.commit(\n\t\t\t`Add category \"${name}\"`,\n\t\t\tthis.edited((d) => {\n\t\t\t\td.categories.splice(at, 0, name);\n\t\t\t\tfor (const s of d.series) s.values.splice(at, 0, null);\n\t\t\t}),\n\t\t\t{ kind: 'category', index: at }\n\t\t);\n\t\treturn { ok: true, index: at };\n\t}\n\n\tremoveCategory(index: number): ChartActionResult {\n\t\tif (!Number.isInteger(index) || index < 0 || index >= this.spec.categories.length) return this.badIndex(index);\n\t\tthis.commit(\n\t\t\t`Delete category \"${this.spec.categories[index]}\"`,\n\t\t\tthis.edited((d) => {\n\t\t\t\td.categories.splice(index, 1);\n\t\t\t\tfor (const s of d.series) s.values.splice(index, 1);\n\t\t\t})\n\t\t);\n\t\treturn { ok: true };\n\t}\n\n\trenameCategory(index: number, label: string): ChartActionResult {\n\t\tif (!Number.isInteger(index) || index < 0 || index >= this.spec.categories.length) return this.badIndex(index);\n\t\tif (this.spec.categories[index] === label) return { ok: true };\n\t\tthis.commit(\n\t\t\t'Rename category',\n\t\t\tthis.edited((d) => {\n\t\t\t\td.categories[index] = String(label);\n\t\t\t})\n\t\t);\n\t\treturn { ok: true };\n\t}\n\n\tmoveCategory(from: number, to: number): ChartActionResult {\n\t\tif (!Number.isInteger(from) || from < 0 || from >= this.spec.categories.length) return this.badIndex(from);\n\t\tconst target = Math.max(0, Math.min(this.spec.categories.length - 1, to));\n\t\tif (target === from) return { ok: true };\n\t\tconst move = <T>(list: T[]) => {\n\t\t\tconst [item] = list.splice(from, 1);\n\t\t\tlist.splice(target, 0, item);\n\t\t};\n\t\tthis.commit(\n\t\t\t'Reorder categories',\n\t\t\tthis.edited((d) => {\n\t\t\t\tmove(d.categories);\n\t\t\t\tfor (const s of d.series) move(s.values);\n\t\t\t})\n\t\t);\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Pastes a block of cells (TSV / CSV text or rows) into the data grid with its top-left at\n\t * `row` (category index) and `column` (series index; `-1` is the category column). The\n\t * grid grows to fit. Non-numeric value cells become gaps and are reported in `issues`.\n\t */\n\tpasteCells(row: number, column: number, cells: string | ReadonlyArray<ReadonlyArray<unknown>>): ChartActionResult & { issues?: ChartIssue[] } {\n\t\tconst rows: unknown[][] = typeof cells === 'string' ? (cells.includes('\\t') ? parseTsv(cells) : parseCsv(cells)) : cells.map((r) => [...r]);\n\t\tif (!rows.length) return { ok: true };\n\t\tconst issues: ChartIssue[] = [];\n\t\tconst next = this.edited((d) => {\n\t\t\tconst width = Math.max(...rows.map((r) => r.length));\n\t\t\tconst lastSeries = column + width - 1;\n\t\t\twhile (d.series.length <= lastSeries) {\n\t\t\t\tconst id = uniqueChartSeriesId(d.series);\n\t\t\t\td.series.push({ id, name: `Series ${d.series.length + 1}`, values: d.categories.map(() => null) });\n\t\t\t}\n\t\t\tconst lastRow = row + rows.length - 1;\n\t\t\twhile (d.categories.length <= lastRow) {\n\t\t\t\td.categories.push(`${d.categories.length + 1}`);\n\t\t\t\tfor (const s of d.series) s.values.push(null);\n\t\t\t}\n\t\t\trows.forEach((cellsInRow, r) => {\n\t\t\t\tcellsInRow.forEach((cell, c) => {\n\t\t\t\t\tconst col = column + c;\n\t\t\t\t\tconst index = row + r;\n\t\t\t\t\tif (col < 0) d.categories[index] = cell === null || cell === undefined ? '' : String(cell).trim();\n\t\t\t\t\telse {\n\t\t\t\t\t\tconst parsed = parseChartValue(cell);\n\t\t\t\t\t\tif (parsed === undefined) issues.push({ path: `${d.series[col].id}[${index}]`, message: `\"${String(cell)}\" is not a number; it was left blank.` });\n\t\t\t\t\t\td.series[col].values[index] = parsed ?? null;\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t});\n\t\t});\n\t\tthis.commit('Paste cells', next);\n\t\tif (issues.length) {\n\t\t\tthis.issues = issues;\n\t\t\tthis.notify(false);\n\t\t}\n\t\treturn issues.length ? { ok: true, issues } : { ok: true };\n\t}\n\n\t/**\n\t * Replaces the data with a grid of cells (see `chartDataFromTable`): one undo step. Series\n\t * colours and types are kept by name.\n\t */\n\timportTable(rows: ReadonlyArray<ReadonlyArray<unknown>>, options: ChartTableOptions = {}): ChartActionResult & { issues?: ChartIssue[] } {\n\t\tconst data = chartDataFromTable(rows, options);\n\t\tif (!data.categories.length || !data.series.length) return this.reject('There is no data to import: expected a header row and at least one row of values.');\n\t\tconst byName = new Map(this.spec.series.map((s) => [s.name, s]));\n\t\tconst series = data.series.map((s) => {\n\t\t\tconst old = byName.get(s.name);\n\t\t\treturn { ...s, ...(old?.color ? { color: old.color } : {}), ...(old?.type ? { type: old.type } : {}), ...(old ? { id: old.id } : {}) };\n\t\t});\n\t\tconst { spec } = normalizeChartSpec({ ...this.spec, categories: data.categories, series });\n\t\tthis.commit('Import data', spec);\n\t\tif (data.issues.length) {\n\t\t\tthis.issues = data.issues;\n\t\t\tthis.notify(false);\n\t\t\treturn { ok: true, issues: data.issues };\n\t\t}\n\t\treturn { ok: true };\n\t}\n\n\t/** Imports CSV (or TSV, detected by tabs) text; see `importTable`. */\n\timportCsv(text: string, options: ChartTableOptions & { delimiter?: string } = {}): ChartActionResult & { issues?: ChartIssue[] } {\n\t\tconst delimiter = options.delimiter ?? (text.includes('\\t') ? '\\t' : ',');\n\t\treturn this.importTable(parseCsv(text, { delimiter }), options);\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Trendlines\n\t// -------------------------------------------------------------------------\n\n\t/** Adds a trendline to a series and selects it. Returns its index (and the fit, when it can be computed). */\n\taddTrendline(seriesId: string, input: ChartTrendline): { ok: true; index: number; fit: ChartTrendFit | null } | { ok: false; message: string; issues?: ChartIssue[] } {\n\t\tconst series = this.getSeries(seriesId);\n\t\tif (!series) return this.unknownSeries(seriesId) as { ok: false; message: string };\n\t\tif (!chartSupportsTrendlines(this.spec.type, this.spec.stacking))\n\t\t\treturn this.reject(`Trendlines need an unstacked bar, line, area, scatter or bubble chart; this is a ${this.spec.stacking !== 'none' ? `${this.spec.stacking} ` : ''}${this.spec.type} chart.`) as { ok: false; message: string };\n\t\tconst issues: ChartIssue[] = [];\n\t\tconst line = normalizeChartTrendline(input, 'trendline', issues);\n\t\tif (!line || issues.length) return this.reject(issues.map((i) => i.message).join(' ') || 'The trendline is not valid.', issues) as { ok: false; message: string };\n\t\tconst index = series.trendlines?.length ?? 0;\n\t\tconst next = this.edited((d) => {\n\t\t\tconst s = d.series.find((x) => x.id === seriesId)!;\n\t\t\ts.trendlines = [...(s.trendlines ?? []), line];\n\t\t});\n\t\tthis.commit(`Add trendline to \"${series.name}\"`, next, { kind: 'trendline', seriesId, index });\n\t\treturn { ok: true, index, fit: this.trendlineFit(seriesId, index) };\n\t}\n\n\t/** Changes a trendline (all-or-nothing). */\n\tupdateTrendline(seriesId: string, index: number, patch: Partial<ChartTrendline>): ChartActionResult {\n\t\tconst series = this.getSeries(seriesId);\n\t\tif (!series) return this.unknownSeries(seriesId);\n\t\tconst current = series.trendlines?.[index];\n\t\tif (!current) return fail(`\"${series.name}\" has no trendline ${index}; ${series.trendlines?.length ? `expected 0 to ${series.trendlines.length - 1}` : 'it has none yet'}.`);\n\t\tconst issues: ChartIssue[] = [];\n\t\tconst merged: Record<string, unknown> = { ...current, ...patch };\n\t\tfor (const [k, v] of Object.entries(patch)) if (v === null) delete merged[k];\n\t\tconst line = normalizeChartTrendline(merged, 'trendline', issues);\n\t\tif (!line || issues.length) return this.reject(issues.map((i) => i.message).join(' '), issues);\n\t\tthis.commit(\n\t\t\t'Change trendline',\n\t\t\tthis.edited((d) => {\n\t\t\t\td.series.find((x) => x.id === seriesId)!.trendlines![index] = line;\n\t\t\t})\n\t\t);\n\t\treturn { ok: true };\n\t}\n\n\tremoveTrendline(seriesId: string, index: number): ChartActionResult {\n\t\tconst series = this.getSeries(seriesId);\n\t\tif (!series) return this.unknownSeries(seriesId);\n\t\tif (!series.trendlines?.[index]) return fail(`\"${series.name}\" has no trendline ${index}.`);\n\t\tthis.commit(\n\t\t\t'Delete trendline',\n\t\t\tthis.edited((d) => {\n\t\t\t\tconst s = d.series.find((x) => x.id === seriesId)!;\n\t\t\t\ts.trendlines = s.trendlines!.filter((_, i) => i !== index);\n\t\t\t\tif (!s.trendlines.length) delete s.trendlines;\n\t\t\t})\n\t\t);\n\t\treturn { ok: true };\n\t}\n\n\t/** The points a series' trendlines are fitted to: (x, y) with x = 1, 2, 3 … on category charts. */\n\ttrendlinePoints(seriesId: string): [number, number][] {\n\t\tconst series = this.getSeries(seriesId);\n\t\tif (!series) return [];\n\t\tconst xy = chartFamily(this.spec) === 'xy';\n\t\tconst out: [number, number][] = [];\n\t\tseries.values.forEach((v, i) => {\n\t\t\tconst x = xy ? (series.x?.[i] ?? (this.spec.type === 'bubble' ? i + 1 : null)) : i + 1;\n\t\t\tif (v !== null && x !== null && Number.isFinite(v) && Number.isFinite(x)) out.push([x, v]);\n\t\t});\n\t\treturn out;\n\t}\n\n\t/** The fitted coefficients, R² and equation of a trendline (null for moving averages or unfittable data). */\n\ttrendlineFit(seriesId: string, index: number): ChartTrendFit | null {\n\t\tconst line = this.getSeries(seriesId)?.trendlines?.[index];\n\t\tif (!line) return null;\n\t\treturn fitChartTrendline(line, this.trendlinePoints(seriesId));\n\t}\n\n\t/** The moving-average points of a trendline (empty for other kinds). */\n\ttrendlineMovingAverage(seriesId: string, index: number): [number, number][] {\n\t\tconst line = this.getSeries(seriesId)?.trendlines?.[index];\n\t\tif (!line || line.type !== 'moving-average') return [];\n\t\treturn chartMovingAverage(this.trendlinePoints(seriesId), line.period ?? 2);\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Reshaping\n\t// -------------------------------------------------------------------------\n\n\t/**\n\t * Switches rows and columns: series become categories and categories become series (one\n\t * undo step). Series colours, types and other per-series settings are dropped.\n\t */\n\ttranspose(): ChartActionResult {\n\t\tconst s = this.spec;\n\t\tif (!s.series.length || !s.categories.length) return this.reject('There is no data to switch: the chart needs at least one series and one category.');\n\t\tconst used = new Set<string>();\n\t\tconst series: ChartSeries[] = s.categories.map((name, i) => {\n\t\t\tlet id = `series-${i + 1}`;\n\t\t\tfor (let k = i + 2; used.has(id); k++) id = `series-${k}`;\n\t\t\tused.add(id);\n\t\t\treturn { id, name, values: s.series.map((x) => x.values[i] ?? null) };\n\t\t});\n\t\tconst { spec } = normalizeChartSpec({ ...s, categories: s.series.map((x) => x.name), series });\n\t\tthis.commit('Switch rows and columns', spec, null);\n\t\treturn { ok: true };\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Selection and gestures\n\t// -------------------------------------------------------------------------\n\n\t/** Selects a chart element (view state; not an undo step). */\n\tselect(selection: ChartSelection | null): void {\n\t\tconst next = this.validSelection(selection, this.spec);\n\t\tif (sameChartSelection(next, this.selection) || (next === null && this.selection === null)) return;\n\t\tthis.selection = next;\n\t\tthis.notify(false);\n\t}\n\n\t/**\n\t * Deletes what is selected: a series or category is removed, a point becomes a gap, a\n\t * title is cleared, the legend or an axis is hidden.\n\t */\n\tdeleteSelection(): ChartActionResult {\n\t\tconst sel = this.selection;\n\t\tif (!sel) return fail('Nothing is selected.');\n\t\tswitch (sel.kind) {\n\t\t\tcase 'series':\n\t\t\t\treturn this.removeSeries(sel.seriesId);\n\t\t\tcase 'category':\n\t\t\t\treturn this.removeCategory(sel.index);\n\t\t\tcase 'point':\n\t\t\t\treturn this.setValue(sel.seriesId, sel.index, null, { label: 'Clear value' });\n\t\t\tcase 'title':\n\t\t\t\treturn this.setTitle('');\n\t\t\tcase 'subtitle':\n\t\t\t\treturn this.setSubtitle('');\n\t\t\tcase 'legend':\n\t\t\t\treturn this.setLegend({ show: false });\n\t\t\tcase 'axis':\n\t\t\t\treturn this.setAxis(sel.axis, { show: false });\n\t\t\tcase 'trendline':\n\t\t\t\treturn this.removeTrendline(sel.seriesId, sel.index);\n\t\t\tdefault:\n\t\t\t\treturn fail('The chart itself cannot be deleted.');\n\t\t}\n\t}\n\n\t/** Adds `delta` to a value (a gap counts as 0). */\n\tnudgeValue(seriesId: string, index: number, delta: number): ChartActionResult {\n\t\tconst series = this.getSeries(seriesId);\n\t\tif (!series) return this.unknownSeries(seriesId);\n\t\tconst current = series.values[index] ?? 0;\n\t\treturn this.setValue(seriesId, index, Number((current + delta).toPrecision(12)), { label: `Edit ${series.name} · ${this.spec.categories[index] ?? index}` });\n\t}\n\n\t/** Starts a continuous edit (a drag): every change until `endGesture()` is one undo step. */\n\tbeginGesture(key: string): void {\n\t\tif (this.gestureKey === key) return;\n\t\tthis.history.endGesture();\n\t\tthis.gestureKey = key;\n\t\tthis.gestureCount++;\n\t\tthis.gestureStart = { spec: this.spec, mark: this.history.mark() };\n\t\tthis.notify(false);\n\t}\n\n\t/**\n\t * Cancels the gesture in progress (Escape during a drag): everything it changed is taken\n\t * back and leaves no undo or redo step.\n\t */\n\tcancelGesture(): void {\n\t\tif (this.gestureKey === null) return;\n\t\tconst start = this.gestureStart;\n\t\tthis.gestureKey = null;\n\t\tthis.gestureStart = null;\n\t\tthis.history.endGesture();\n\t\tif (start) this.history.revertTo(start.mark);\n\t\tthis.notify(false);\n\t}\n\n\tendGesture(): void {\n\t\tif (this.gestureKey === null) return;\n\t\tthis.gestureKey = null;\n\t\tthis.history.endGesture();\n\t\t// A drag that ends where it started leaves no undo step behind.\n\t\tconst start = this.gestureStart;\n\t\tthis.gestureStart = null;\n\t\tif (start && sameChartJson(start.spec, this.spec)) this.history.revertTo(start.mark);\n\t\tthis.notify(false);\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Persistence and history\n\t// -------------------------------------------------------------------------\n\n\t/** The spec, for saving (a copy). */\n\ttoJSON(): ChartSpec {\n\t\treturn cloneChartSpec(this.spec);\n\t}\n\n\t/**\n\t * Replaces the chart without an undo step (e.g. when opening a document). Problems are\n\t * fixed with defaults and reported in `state.issues` (and returned).\n\t */\n\tload(input: unknown, options: { clearHistory?: boolean } = {}): ChartIssue[] {\n\t\tconst { spec, issues } = normalizeChartSpec(input);\n\t\tthis.spec = spec;\n\t\tthis.issues = issues;\n\t\tthis.selection = this.validSelection(this.selection, spec);\n\t\tif (options.clearHistory) {\n\t\t\tthis.applying = true;\n\t\t\ttry {\n\t\t\t\tthis.history.clear();\n\t\t\t} finally {\n\t\t\t\tthis.applying = false;\n\t\t\t}\n\t\t}\n\t\tthis.notify(true);\n\t\treturn issues;\n\t}\n\n\tundo(): boolean {\n\t\tthis.endGesture();\n\t\treturn this.history.undo();\n\t}\n\n\tredo(): boolean {\n\t\tthis.endGesture();\n\t\treturn this.history.redo();\n\t}\n\n\t/** Stops listening to the history (reconnected by the next `subscribe`). */\n\tdestroy(): void {\n\t\tthis.unsubscribeHistory?.();\n\t\tthis.unsubscribeHistory = undefined;\n\t\tthis.listeners.clear();\n\t}\n}\n\nexport function createChartController(options: ChartControllerOptions = {}): ChartController {\n\treturn new ChartController(options);\n}\n\nfunction updateLabel(patch: ChartSpecPatch): string {\n\tconst keys = Object.keys(patch);\n\tif (keys.length !== 1) return 'Change chart';\n\tconst labels: Record<string, string> = {\n\t\ttype: 'Change chart type',\n\t\ttitle: 'Edit title',\n\t\tsubtitle: 'Edit subtitle',\n\t\tcategories: 'Edit categories',\n\t\tseries: 'Edit series',\n\t\txAxis: 'Change category axis',\n\t\tyAxis: 'Change value axis',\n\t\tyAxis2: 'Change secondary value axis',\n\t\tlabelPosition: 'Change label position',\n\t\tgapWidth: 'Change bar spacing',\n\t\toverlap: 'Change bar overlap',\n\t\tbins: 'Change histogram bins',\n\t\tshowTotal: 'Toggle waterfall total',\n\t\tlegend: 'Change legend',\n\t\tstacking: 'Change stacking',\n\t\torientation: 'Change orientation',\n\t\tcurve: 'Change line style',\n\t\tpoints: 'Toggle points',\n\t\tdataLabels: 'Toggle data labels',\n\t\tpalette: 'Change colours',\n\t\tinnerRadius: 'Change donut hole'\n\t};\n\treturn labels[keys[0]] ?? 'Change chart';\n}\n","// Ported from gridcraft-main/crates/chart/src/sparkline.rs (MIT OR Apache-2.0, © ArtCraft Team and\n// contributors). See THIRD_PARTY_NOTICES.md.\nimport { r2 } from './layout-shared.js';\nimport { chartSceneToSvg } from './svg.js';\nimport { DEFAULT_CHART_THEME } from './spec.js';\nimport type { ChartMark, ChartScene, ChartValue } from './types.js';\n\nexport type ChartSparklineKind = 'line' | 'column' | 'win-loss';\n\nexport interface ChartSparklineOptions {\n\tkind?: ChartSparklineKind;\n\twidth: number;\n\theight: number;\n\t/** Line, positive column and win colour (default the first palette colour). */\n\tcolor?: string;\n\t/** Negative column and loss colour (default red). */\n\tnegativeColor?: string;\n\t/** Dots on every line point (thinned past 500). */\n\tmarkers?: boolean;\n\t/** Highlight the highest and lowest point of a line (or column). */\n\thighLow?: boolean;\n\t/** Fixed value range (default: the data, including 0 for columns). */\n\tmin?: number;\n\tmax?: number;\n}\n\nconst MAX_POINTS = 100_000;\n\n/**\n * Lays out a word-sized chart (a sparkline) as marks: a line broken at gaps, columns\n * (negatives in `negativeColor`) or win/loss bars of equal height. Long lines are thinned past\n * 4000 points. Returns a scene with no axes, legend or targets.\n */\nexport function layoutSparkline(values: readonly ChartValue[], options: ChartSparklineOptions): ChartScene {\n\tconst kind = options.kind ?? 'line';\n\tconst w = Number.isFinite(options.width) ? Math.max(1, options.width) : 1;\n\tconst h = Number.isFinite(options.height) ? Math.max(1, options.height) : 1;\n\tconst color = options.color ?? '#4269d0';\n\tconst negative = options.negativeColor ?? '#e5484d';\n\tconst theme = { ...DEFAULT_CHART_THEME, background: 'transparent' };\n\tconst marks: ChartMark[] = [];\n\tconst scene: ChartScene = { width: w, height: h, plot: { x: 0, y: 0, width: w, height: h }, marks, percent: false, theme, warnings: [] };\n\tconst vals = values.slice(0, MAX_POINTS).map((v) => (v !== null && Number.isFinite(v) ? Math.max(-1e300, Math.min(1e300, v)) : null));\n\tconst n = vals.length;\n\tif (!n) return scene;\n\tconst pad = Math.max(0.5, Math.min(2, h * 0.1));\n\tconst x0 = pad;\n\tconst y0 = pad;\n\tconst iw = Math.max(0.5, w - 2 * pad);\n\tconst ih = Math.max(0.5, h - 2 * pad);\n\tlet lo = Infinity;\n\tlet hi = -Infinity;\n\tfor (const v of vals) if (v !== null) (lo = Math.min(lo, v)), (hi = Math.max(hi, v));\n\tif (!Number.isFinite(lo)) return scene;\n\tif (options.min !== undefined && Number.isFinite(options.min)) lo = options.min;\n\tif (options.max !== undefined && Number.isFinite(options.max)) hi = options.max;\n\tlet hiAt = -1;\n\tlet loAt = -1;\n\tvals.forEach((v, i) => {\n\t\tif (v === null) return;\n\t\tif (hiAt < 0 || v > vals[hiAt]!) hiAt = i;\n\t\tif (loAt < 0 || v < vals[loAt]!) loAt = i;\n\t});\n\tif (kind === 'line') {\n\t\tconst span = hi - lo;\n\t\tconst fy = (v: number) => y0 + ih - (span > 0 ? (v - lo) / span : 0.5) * ih;\n\t\tconst fx = (j: number) => (n > 1 ? x0 + (j / (n - 1)) * iw : x0 + iw / 2);\n\t\tlet d = '';\n\t\tlet run: [number, number][] = [];\n\t\tconst flush = () => {\n\t\t\tif (run.length >= 2) {\n\t\t\t\tconst pts = run.length > 4000 ? run.filter((_, i) => i % (Math.floor(run.length / 2000) + 1) === 0) : run;\n\t\t\t\td += pts.map(([x, y], i) => `${i ? 'L' : 'M'}${r2(x)},${r2(y)}`).join('');\n\t\t\t}\n\t\t\trun = [];\n\t\t};\n\t\tvals.forEach((v, j) => (v === null ? flush() : run.push([fx(j), fy(v)])));\n\t\tflush();\n\t\tif (d) marks.push({ kind: 'path', key: 'sparkline', role: 'line', d, fill: 'none', stroke: color, strokeWidth: 1.25 });\n\t\tconst r = Math.max(1, Math.min(3, h * 0.08));\n\t\tif (options.markers || n === 1) {\n\t\t\tconst step = Math.max(1, Math.floor(n / 500));\n\t\t\tvals.forEach((v, j) => {\n\t\t\t\tif (v !== null && j % step === 0) marks.push({ kind: 'circle', key: `point-${j}`, role: 'point', cx: fx(j), cy: fy(v), r, fill: color });\n\t\t\t});\n\t\t}\n\t\tif (options.highLow && n > 1) {\n\t\t\tif (hiAt >= 0) marks.push({ kind: 'circle', key: 'high', role: 'point', cx: fx(hiAt), cy: fy(vals[hiAt]!), r: r + 0.5, fill: color });\n\t\t\tif (loAt >= 0 && loAt !== hiAt) marks.push({ kind: 'circle', key: 'low', role: 'point', cx: fx(loAt), cy: fy(vals[loAt]!), r: r + 0.5, fill: negative });\n\t\t}\n\t\treturn scene;\n\t}\n\tconst slot = iw / n;\n\tconst bw = Math.max(0.25, slot * 0.8);\n\tconst win = kind === 'win-loss';\n\tconst lo0 = win ? -1 : Math.min(lo, 0);\n\tconst hi0 = win ? 1 : Math.max(hi, 0);\n\tconst span = hi0 - lo0;\n\tconst fy = (v: number) => (span > 0 ? y0 + ih - ((v - lo0) / span) * ih : y0 + ih);\n\tconst base = fy(0);\n\tvals.forEach((raw, j) => {\n\t\tif (raw === null) return;\n\t\tconst v = win ? Math.sign(raw) : raw;\n\t\tif (win && v === 0) return;\n\t\tconst top = fy(v);\n\t\tconst highlight = options.highLow && !win && (j === hiAt || j === loAt);\n\t\tmarks.push({ kind: 'rect', key: `bar-${j}`, role: 'bar', x: x0 + j * slot + (slot - bw) / 2, y: Math.min(top, base), width: bw, height: Math.max(0.5, Math.abs(top - base)), fill: v < 0 ? negative : color, opacity: options.highLow && !win && !highlight ? 0.6 : undefined });\n\t});\n\treturn scene;\n}\n\n/** A sparkline as a standalone SVG string (e.g. for an inline image in rich text). */\nexport function sparklineToSvg(values: readonly ChartValue[], options: ChartSparklineOptions & { ariaLabel?: string }): string {\n\treturn chartSceneToSvg(layoutSparkline(values, options), { ariaLabel: options.ariaLabel });\n}\n"]}