@pixerate/editor 0.55.0 → 0.56.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.
- package/dist/agent/index.cjs +18 -17
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.js +3 -2
- package/dist/agent/index.js.map +1 -1
- package/dist/canvas/index.cjs +2105 -172
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +319 -23
- package/dist/canvas/index.d.ts +319 -23
- package/dist/canvas/index.js +2081 -148
- package/dist/canvas/index.js.map +1 -1
- package/dist/chart/index.cjs +32 -30
- package/dist/chart/index.cjs.map +1 -1
- package/dist/chart/index.d.cts +4 -2
- package/dist/chart/index.d.ts +4 -2
- package/dist/chart/index.js +20 -18
- package/dist/chart/index.js.map +1 -1
- package/dist/{chunk-C3JHII3M.cjs → chunk-3I6V7PI4.cjs} +45 -45
- package/dist/{chunk-C3JHII3M.cjs.map → chunk-3I6V7PI4.cjs.map} +1 -1
- package/dist/{chunk-P43HUQRX.cjs → chunk-AHCENCU4.cjs} +12 -10
- package/dist/chunk-AHCENCU4.cjs.map +1 -0
- package/dist/chunk-II77MLHN.cjs +11106 -0
- package/dist/chunk-II77MLHN.cjs.map +1 -0
- package/dist/{chunk-OIKYP7XC.js → chunk-LMUDHBWR.js} +2 -2
- package/dist/{chunk-VODBAO32.js → chunk-OEFCLWZ3.js} +4 -1425
- package/dist/chunk-OEFCLWZ3.js.map +1 -0
- package/dist/{chunk-NK3WL5TJ.cjs → chunk-P2CTDJNF.cjs} +27 -1448
- package/dist/chunk-P2CTDJNF.cjs.map +1 -0
- package/dist/chunk-TKPPIVW7.cjs +1443 -0
- package/dist/chunk-TKPPIVW7.cjs.map +1 -0
- package/dist/chunk-WJSCGBJ2.js +11106 -0
- package/dist/chunk-WJSCGBJ2.js.map +1 -0
- package/dist/{chunk-RIXTW3C4.js → chunk-WLVZ235X.js} +5 -3
- package/dist/{chunk-RIXTW3C4.js.map → chunk-WLVZ235X.js.map} +1 -1
- package/dist/chunk-ZOH6X3VR.js +1443 -0
- package/dist/chunk-ZOH6X3VR.js.map +1 -0
- package/dist/{controller-DscWQbjO.d.ts → controller-D2rj5umx.d.ts} +2 -362
- package/dist/{controller-CX0QfSv0.d.cts → controller-R2biKk_E.d.cts} +2 -362
- package/dist/diagram/index.cjs +1457 -47
- package/dist/diagram/index.cjs.map +1 -1
- package/dist/diagram/index.d.cts +1734 -15
- package/dist/diagram/index.d.ts +1734 -15
- package/dist/diagram/index.js +1436 -26
- package/dist/diagram/index.js.map +1 -1
- package/dist/extensions/index.cjs +5 -4
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.d.cts +4 -3
- package/dist/extensions/index.d.ts +4 -3
- package/dist/extensions/index.js +4 -3
- package/dist/{index-DIWB6xMf.d.cts → index-BcHyG0Wq.d.cts} +2 -1
- package/dist/{index-CALDD35B.d.ts → index-CTCc3m3C.d.ts} +2 -1
- package/dist/index.cjs +10 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/types-BRp-6zkY.d.cts +362 -0
- package/dist/types-BRp-6zkY.d.ts +362 -0
- package/dist/{types-BULrFyuQ.d.cts → types-DYkjhzdz.d.cts} +1 -1
- package/dist/{types-BULrFyuQ.d.ts → types-DYkjhzdz.d.ts} +1 -1
- package/package.json +1 -1
- package/dist/chunk-H5K77C7Y.cjs +0 -3848
- package/dist/chunk-H5K77C7Y.cjs.map +0 -1
- package/dist/chunk-NK3WL5TJ.cjs.map +0 -1
- package/dist/chunk-P43HUQRX.cjs.map +0 -1
- package/dist/chunk-VODBAO32.js.map +0 -1
- package/dist/chunk-WWJNC7SX.js +0 -3848
- package/dist/chunk-WWJNC7SX.js.map +0 -1
- /package/dist/{chunk-OIKYP7XC.js.map → chunk-LMUDHBWR.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/chunk-P2CTDJNF.cjs","../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"],"names":["n","clean","r2","band","p"],"mappings":"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EAAA;AACI,EAAA;AACN,EAAA;AACY,EAAA;AACX,EAAA;AAChC,EAAA;AACP,EAAA;AACA,EAAA;AACsB,EAAA;AACE,IAAA;AACjB,IAAA;AACwB,EAAA;AACxB,IAAA;AACU,IAAA;AACd,EAAA;AACyC,EAAA;AACd,EAAA;AAEA,EAAA;AAGC,EAAA;AACpB,EAAA;AACoC,EAAA;AACA,EAAA;AACnB,EAAA;AACgB,EAAA;AACjD;AAEsE;AAGiB;AACpB,EAAA;AACxC,EAAA;AACH,EAAA;AACN,IAAA;AACV,MAAA;AACsC,QAAA;AACE,QAAA;AAC5C,QAAA;AACI,MAAA;AAC0B,QAAA;AACa,QAAA;AAC3C,QAAA;AACI,MAAA;AACiC,QAAA;AACM,QAAA;AAC3C,QAAA;AACI,MAAA;AAC2B,QAAA;AACrB,QAAA;AACV,QAAA;AACI,MAAA;AAC0B,QAAA;AACpB,QAAA;AACV,QAAA;AACF,IAAA;AACD,EAAA;AACO,EAAA;AACR;AAOkG;AACjD,EAAA;AACZ,EAAA;AACY,EAAA;AACZ,EAAA;AACrC;AAGsG;AACjF,EAAA;AACD,EAAA;AACsB,EAAA;AAC1C;AAO2G;AACxD,EAAA;AACD,EAAA;AACzB,EAAA;AACmB,EAAA;AACN,EAAA;AACtC;ATkvDqD;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-P2CTDJNF.cjs","sourcesContent":[null,"// 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, ChartValueScale } 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\n/** Rounds float noise off a tick without flushing tiny values to 0 (log ticks such as 1e-15). */\nconst tidy = (n: number) => (n === 0 ? 0 : Number(n.toPrecision(12)));\n\n/**\n * A logarithmic scale over `[lo, hi]` (only positive values count): the ends extend to whole\n * powers of `base` unless fixed, and ticks sit on the powers, thinned to about `count`, or, in\n * base 10 over a few decades, on the 1-2-5 steps between them. `step` is the base. Non-positive\n * or non-finite ends fall back to the data, and no positive data at all gives 1 to `base`.\n */\nexport function niceLogScale(lo: number, hi: number, count = 5, base = 10, fixedMin: number | null = null, fixedMax: number | null = null): NiceScale {\n\tif (!(base > 1) || !Number.isFinite(base)) base = 10;\n\tif (fixedMin !== null && !(fixedMin > 0 && Number.isFinite(fixedMin))) fixedMin = null;\n\tif (fixedMax !== null && !(fixedMax > 0 && Number.isFinite(fixedMax))) fixedMax = null;\n\tif (!(lo > 0) || !Number.isFinite(lo)) lo = hi > 0 && Number.isFinite(hi) ? hi : (fixedMin ?? 1);\n\tif (!(hi > 0) || !Number.isFinite(hi)) hi = Math.max(lo, fixedMax ?? lo * base);\n\tif (lo > hi) [lo, hi] = [hi, lo];\n\tconst log = (v: number) => Math.log(v) / Math.log(base);\n\tconst e0 = Math.floor(log(fixedMin ?? lo) + 1e-9);\n\tlet e1 = Math.ceil(log(fixedMax ?? hi) - 1e-9);\n\tif (e1 <= e0) e1 = e0 + 1;\n\tlet min = fixedMin ?? tidy(Math.pow(base, e0));\n\tlet max = fixedMax ?? tidy(Math.pow(base, e1));\n\tif (!(max > min)) {\n\t\tif (fixedMax === null) max = tidy(min * base);\n\t\telse min = tidy(max / base);\n\t}\n\tif (!Number.isFinite(max)) max = Number.MAX_VALUE;\n\tif (!(min > 0)) min = Number.MIN_VALUE;\n\tconst inside = (t: number) => t >= min * (1 - 1e-9) && t <= max * (1 + 1e-9);\n\tconst decades = Math.max(1, log(max) - log(min));\n\tlet ticks: number[] = [];\n\tconst first = Math.ceil(log(min) - 1e-9);\n\tconst last = Math.floor(log(max) + 1e-9);\n\tif (base === 10 && decades * 3 <= count + 1) {\n\t\tfor (let e = first - 1; e <= last; e++) for (const m of [1, 2, 5]) ticks.push(tidy(m * Math.pow(10, e)));\n\t} else {\n\t\tconst every = Math.max(1, Math.ceil((last - first + 1) / (count + 2)));\n\t\tfor (let e = Math.ceil(first / every) * every; e <= last && ticks.length < 1000; e += every) ticks.push(tidy(Math.pow(base, e)));\n\t}\n\tticks = ticks.filter((t) => Number.isFinite(t) && t > 0 && inside(t));\n\t// A fixed range inside one power: fall back to evenly spaced ticks between the ends.\n\tif (ticks.length < 2) ticks = niceScale(min, max, count, min, max).ticks.filter((t) => t > 0);\n\treturn { min, max, step: base, ticks: [...new Set(ticks)].sort((a, b) => a - b) };\n}\n\n/**\n * A data value's pixel on a value scale: linear, or by logarithm when `scale.logBase` is set\n * (values of 0 or less have no pixel there and give NaN).\n */\nexport function chartScaleToPixel(value: number, scale: Pick<ChartValueScale, 'domain' | 'range' | 'logBase'>): number {\n\tif (!scale.logBase) return linearMap(value, scale.domain, scale.range);\n\tif (!(value > 0)) return NaN;\n\treturn linearMap(Math.log(value), [Math.log(scale.domain[0]), Math.log(scale.domain[1])], scale.range);\n}\n\n/** The data value at a pixel on a value scale (the inverse of `chartScaleToPixel`). */\nexport function chartScaleFromPixel(px: number, scale: Pick<ChartValueScale, 'domain' | 'range' | 'logBase'>): number {\n\tif (!scale.logBase) return linearMap(px, scale.range, scale.domain);\n\treturn Math.exp(linearMap(px, scale.range, [Math.log(scale.domain[0]), Math.log(scale.domain[1])]));\n}\n","import { niceLogScale, 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\t/** The category index the point belongs to (when it has one). */\n\ti?: 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\t// Log axes end on powers of the base (the step does not apply); `lo` / `hi` are the positive data range.\n\tif (axis.scale === 'log') return niceLogScale(lo, hi, tickCount(length), axis.logBase ?? 10, fixedMin, fixedMax);\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 { chartErrorAmounts } from './errorbars.js';\nimport { chartScaleToPixel, formatChartNumber, 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, ChartErrorBars, ChartMark, ChartMarker, ChartNumberFormat, ChartRect, ChartSelection, ChartSeries, ChartSeriesType, ChartValue, 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; the lower end on a log axis). */\n\tzero: number;\n\tformat: ChartNumberFormat;\n\ttickText(t: number): string;\n\t/** Set when the axis is logarithmic. */\n\tlogBase?: number;\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\n/**\n * Value ↔ pixel for one axis: logarithmic when `axis.scale` is `log` (values of 0 or less\n * clamp to the lower end), and running the other way when `axis.reverse` is set.\n */\nexport function scaleInfo(scale: NiceScale, range: [number, number], format: ChartNumberFormat, axis?: Pick<ChartAxis, 'scale' | 'logBase' | 'reverse'>): ScaleInfo {\n\tconst logBase = axis?.scale === 'log' ? (axis.logBase ?? 10) : undefined;\n\tconst r: [number, number] = axis?.reverse ? [range[1], range[0]] : range;\n\tconst vs = { domain: [scale.min, scale.max] as [number, number], range: r, logBase };\n\tconst raw = (v: number) => chartScaleToPixel(v, vs);\n\tconst px = (v: number) => (logBase && !(v > 0) ? r[0] : raw(Math.min(scale.max, Math.max(scale.min, v))));\n\t// Log ticks span powers, so each is written with the decimals it needs.\n\tconst tickText = (t: number) => formatChartNumber(t, format, logBase ? format.decimals : (format.decimals ?? stepDecimals(scale.step)));\n\treturn { scale, range: r, px, raw, zero: px(0), format, tickText, ...(logBase ? { logBase } : {}) };\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, v.axis);\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, primary.axis);\n\tconst value2 = secondary ? scaleInfo(axisScale(secondary.lo, secondary.hi, valueLen, secondary.axis, secondary.frozen, secondary.headroom), range, secondary.format, secondary.axis) : undefined;\n\n\tconst catStart = horizontal ? plot.y : plot.x;\n\tconst catLen = horizontal ? plot.height : plot.width;\n\tconst band = catLen / n;\n\t// A reversed category axis runs from the far end; `i` may be fractional (trendlines).\n\tconst catCenter = (i: number) => catStart + band * (xAxis.reverse ? n - i - 0.5 : 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, ...(info.logBase ? { logBase: info.logBase } : {}) });\n\ntype ChartErrorAmountLike = ReturnType<typeof chartErrorAmounts>[number];\n\n/** An error bar in value units: its centre and ends. */\ninterface ErrorSpan {\n\tcenter: number;\n\tlo: number;\n\thi: number;\n}\n\n/**\n * One error bar as path data: a stem from pixel `a` to `b` along the value direction at `c`\n * across it, with caps `width` wide at the ends that have them. `map` turns (across, along)\n * into x / y.\n */\nfunction errorBarPath(map: (p: Pt) => [number, number], c: number, a: number, b: number, width: number, capA: boolean, capB: boolean): string {\n\tif (!Number.isFinite(a) || !Number.isFinite(b) || !Number.isFinite(c) || (Math.abs(a - b) < 0.01 && !capA && !capB)) return '';\n\tconst at = (across: number, along: number) => map({ c: across, v: along }).map(r2).join(',');\n\tlet d = `M${at(c, a)}L${at(c, b)}`;\n\tif (capA) d += `M${at(c - width / 2, a)}L${at(c + width / 2, a)}`;\n\tif (capB) d += `M${at(c - width / 2, b)}L${at(c + width / 2, b)}`;\n\treturn d;\n}\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[], logAxis = false) {\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\tconst finitePx = (q: [number, number]) => Number.isFinite(q[0]) && Number.isFinite(q[1]);\n\t\tif (t.type === 'moving-average') px = chartMovingAverage(points, t.period ?? 2).map(([x, y]) => toPx(x, y)).filter(finitePx);\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\t// A straight fit is a curve on a log axis: sample it like the others.\n\t\t\tconst steps = t.type === 'linear' && !logAxis ? 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\tconst q = y === null ? null : toPx(x, y);\n\t\t\t\t// Log axes have no pixel for values of 0 or less.\n\t\t\t\tif (q && finitePx(q)) px.push(q);\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 logOf = (secondary: boolean) => (secondary ? spec.yAxis2 : spec.yAxis).scale === 'log';\n\t// Error bars in value units where they are drawn: on stack tops, scaled to shares when percent-stacked.\n\tconst errors = new Map<string, (ErrorSpan | null)[]>();\n\tfor (const s of visible) {\n\t\tif (!s.errorBars) continue;\n\t\tconst stack = stacks.get(s.id);\n\t\terrors.set(\n\t\t\ts.id,\n\t\t\tchartErrorAmounts(s.errorBars, s.values).map((e, i) => {\n\t\t\t\tconst raw = s.values[i];\n\t\t\t\tconst seg = stack?.segments[i];\n\t\t\t\tif (!e || raw === null || (stack && !seg)) return null;\n\t\t\t\tconst k = seg && percent ? (raw ? (seg[1] - seg[0]) / raw : 0) : 1;\n\t\t\t\tconst center = (seg ? seg[1] : raw) + (e.center - raw) * k;\n\t\t\t\treturn { center, lo: center - e.minus * k, hi: center + e.plus * k };\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 log = logOf(secondary);\n\t\tconst see = (v: number) => {\n\t\t\tif (!Number.isFinite(v) || (log && !(v > 0))) return;\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 && see(v));\n\t\t\terrors.get(s.id)?.forEach((e) => e && (see(e.lo), see(e.hi)));\n\t\t}\n\t\tif (log) {\n\t\t\tconst skipped = group.reduce((t, s) => t + s.values.filter((v) => v !== null && Number.isFinite(v) && v <= 0).length, 0);\n\t\t\tif (skipped) ctx.warn('values-skipped', `${skipped} value${skipped === 1 ? '' : 's'} of 0 or less cannot be drawn on the logarithmic ${secondary ? 'secondary ' : ''}value axis and ${skipped === 1 ? 'was' : 'were'} skipped.`);\n\t\t}\n\t\tif (!Number.isFinite(lo)) {\n\t\t\tlo = log ? 1 : 0;\n\t\t\thi = log ? 10 : 1;\n\t\t}\n\t\tif (!log && 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 errorMarks: 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 log = logOf(onSecondary(s));\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\tif (log && (raw <= 0 || seg[1] <= 0)) 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\t// Which way the bar grows on screen (reversed and log axes included).\n\t\t\t\t\tconst grows = horizontal ? b >= a : b <= a;\n\t\t\t\t\tconst at = barLabel(ctx, text, rect, horizontal, grows, 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) && !(log && (v <= 0 || (s.values[i] ?? 0) <= 0)) ? 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), i });\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[pt.i!]?.[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\tconst errs = errors.get(s.id);\n\t\tif (errs) {\n\t\t\tconst slot = type === 'bar' ? slotOf(s) : 0;\n\t\t\tconst capWidth = type === 'bar' ? Math.min(10, barWidth * 0.5) : 8;\n\t\t\tconst cap = s.errorBars!.cap !== false;\n\t\t\terrs.forEach((e, i) => {\n\t\t\t\tif (!e || (log && !((s.values[i] ?? 0) > 0))) return;\n\t\t\t\tconst c = type === 'bar' ? catCenter(i) - groupWidth / 2 + slot * barWidth * (1 - o) + barWidth / 2 : catCenter(i);\n\t\t\t\tconst d = errorBarPath(map, c, valuePx(e.lo), valuePx(e.hi), capWidth, cap && e.lo < e.center, cap && e.hi > e.center);\n\t\t\t\tif (d) errorMarks.push({ kind: 'path', key: `error-bar-${s.id}-${i}`, role: 'error-bar', d, fill: 'none', stroke: type === 'bar' ? theme.text : color, strokeWidth: selSeries ? 2 : 1.25, target: seriesTarget, selected: selSeries });\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: catCenter(x - 1), v: info.raw(y) });\n\t\t\tdrawTrendlines(ctx, plot, s, color, points, toPx, trendLabels, !!info.logBase);\n\t\t}\n\t}\n\tmarks.push(...errorMarks, ...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 yAxis = spec.yAxis;\n\tconst xAxis = spec.xAxis;\n\tconst xLog = xAxis.scale === 'log';\n\tconst yLog = yAxis.scale === 'log';\n\tlet skipped = 0;\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 } => {\n\t\t\t\tif (p.x === null || p.y === null || !Number.isFinite(p.x) || !Number.isFinite(p.y)) return false;\n\t\t\t\t// Log axes cannot place 0 or less.\n\t\t\t\tif ((xLog && p.x <= 0) || (yLog && p.y <= 0)) return (skipped++, false);\n\t\t\t\treturn true;\n\t\t\t});\n\tconst all = visible.map((s) => ({ s, pts: pointsOf(s) }));\n\tif (skipped) ctx.warn('values-skipped', `${skipped} point${skipped === 1 ? '' : 's'} with a value of 0 or less cannot be drawn on the logarithmic ${xLog && yLog ? 'axes' : xLog ? 'x axis' : 'value axis'} and ${skipped === 1 ? 'was' : 'were'} skipped.`);\n\t// Error bars per point, keyed by series id and point index.\n\tconst yErrors = new Map<string, (ChartErrorAmountLike | null)[]>();\n\tconst xErrors = new Map<string, (ChartErrorAmountLike | null)[]>();\n\tfor (const { s } of all) {\n\t\tif (s.errorBars) yErrors.set(s.id, chartErrorAmounts(s.errorBars, s.values));\n\t\tif (s.xErrorBars) xErrors.set(s.id, chartErrorAmounts(s.xErrorBars, s.values.map((_, i) => (s.x?.[i] ?? (bubble || !s.x ? i + 1 : null)) as ChartValue)));\n\t}\n\tlet xlo = Infinity;\n\tlet xhi = -Infinity;\n\tlet ylo = Infinity;\n\tlet yhi = -Infinity;\n\tlet maxSize = 0;\n\tconst seeX = (v: number) => {\n\t\tif (!Number.isFinite(v) || (xLog && !(v > 0))) return;\n\t\txlo = Math.min(xlo, v);\n\t\txhi = Math.max(xhi, v);\n\t};\n\tconst seeY = (v: number) => {\n\t\tif (!Number.isFinite(v) || (yLog && !(v > 0))) return;\n\t\tylo = Math.min(ylo, v);\n\t\tyhi = Math.max(yhi, v);\n\t};\n\tfor (const { s, pts } of all) {\n\t\tfor (const p of pts) {\n\t\t\tseeX(p.x);\n\t\t\tseeY(p.y);\n\t\t\tif (p.size !== null && p.size > maxSize) maxSize = p.size;\n\t\t\tconst ey = yErrors.get(s.id)?.[p.i];\n\t\t\tif (ey) (seeY(ey.center - ey.minus), seeY(ey.center + ey.plus));\n\t\t\tconst ex = xErrors.get(s.id)?.[p.i];\n\t\t\tif (ex) (seeX(ex.center - ex.minus), seeX(ex.center + ex.plus));\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\tseeX(Math.min(...pts.map((p) => p.x)) - (t.backward ?? 0));\n\t\t\tseeX(Math.max(...pts.map((p) => p.x)) + (t.forward ?? 0));\n\t\t}\n\t}\n\tif (!Number.isFinite(xlo)) [xlo, xhi] = xLog ? [1, 10] : [0, 1];\n\tif (!Number.isFinite(ylo)) [ylo, yhi] = yLog ? [1, 10] : [0, 1];\n\tconst titleBand = fs * 1.3 + 6;\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 ?? (yLog ? undefined : 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 xInfo = scaleInfo(xScale, xRange, xAxis.format, xAxis);\n\tconst yInfo = scaleInfo(yScale, yRange, yAxis.format, yAxis);\n\tconst { raw: xRaw, px: xPx } = xInfo;\n\tconst { raw: yRaw, px: yPx } = yInfo;\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\t// The x axis crosses at y = 0, else along the lower end of the y axis (the top when reversed).\n\t\tconst y0 = !yLog && yScale.min <= 0 && yScale.max >= 0 ? yPx(0) : yAxis.reverse ? plot.y : 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 ?? (xLog ? undefined : stepDecimals(xScale.step))));\n\t\tconst widest = Math.max(0, ...texts.map((t) => measure(t, fs)));\n\t\t// Log ticks are unevenly spaced: thin by the closest pair.\n\t\tconst spacing = Math.min(Infinity, ...xScale.ticks.slice(1).map((t, i) => Math.abs(xPx(t) - xPx(xScale.ticks[i]))));\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 errorMarks: 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\t// Error bars: y bars vertical through the point, x bars horizontal.\n\t\tconst stroke = color;\n\t\tconst errorBar = (key: string, d: string) => d && errorMarks.push({ kind: 'path', key, role: 'error-bar', d, fill: 'none', stroke, strokeWidth: selSeries ? 2 : 1.25, target: seriesTarget, selected: selSeries });\n\t\tfor (const p of pts) {\n\t\t\tconst ey = yErrors.get(s.id)?.[p.i];\n\t\t\tif (ey) {\n\t\t\t\tconst lo = ey.center - ey.minus;\n\t\t\t\tconst hi = ey.center + ey.plus;\n\t\t\t\tconst cap = s.errorBars!.cap !== false;\n\t\t\t\terrorBar(`error-bar-${s.id}-${p.i}`, errorBarPath((q) => [q.c, q.v], xPx(p.x), yPx(lo), yPx(hi), 8, cap && lo < ey.center, cap && hi > ey.center));\n\t\t\t}\n\t\t\tconst ex = xErrors.get(s.id)?.[p.i];\n\t\t\tif (ex) {\n\t\t\t\tconst lo = ex.center - ex.minus;\n\t\t\t\tconst hi = ex.center + ex.plus;\n\t\t\t\tconst cap = s.xErrorBars!.cap !== false;\n\t\t\t\terrorBar(`error-bar-x-${s.id}-${p.i}`, errorBarPath((q) => [q.v, q.c], yPx(p.y), xPx(lo), xPx(hi), 8, cap && lo < ex.center, cap && hi > ex.center));\n\t\t\t}\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, xLog || yLog);\n\t}\n\tmarks.push(...errorMarks, ...labelMarks);\n\tdrawTrendLabels(ctx, plot, trendLabels);\n\treturn {\n\t\tplot,\n\t\tvalueScale: toValueScale(yInfo, 'y'),\n\t\txScale: toValueScale(xInfo, 'x')\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 { chartScaleFromPixel, formatChartNumber } 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. Reversed and logarithmic axes are mapped as drawn.\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 chartScaleFromPixel(px, scale);\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 chartScaleFromPixel(point.x, scale);\n}\n\n/**\n * The value a drag should store: snapped to a tenth of the tick step (or `step`) and kept\n * finite. On a logarithmic axis (`logBase`, without a `step`) ticks are not evenly spaced, so\n * the value is rounded to two significant digits instead.\n */\nexport function snapChartValue(scene: ChartScene, value: number, step?: number, ticks: readonly number[] = scene.valueScale?.ticks ?? [], logBase?: number): number {\n\tif (logBase && step === undefined) return value > 0 && Number.isFinite(value) ? Number(value.toPrecision(2)) : value;\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"]}
|