@pixerate/editor 0.55.0 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/agent/index.cjs +18 -17
  2. package/dist/agent/index.cjs.map +1 -1
  3. package/dist/agent/index.js +3 -2
  4. package/dist/agent/index.js.map +1 -1
  5. package/dist/animation-editor/index.cjs +25 -7
  6. package/dist/animation-editor/index.cjs.map +1 -1
  7. package/dist/animation-editor/index.d.cts +21 -5
  8. package/dist/animation-editor/index.d.ts +21 -5
  9. package/dist/animation-editor/index.js +25 -7
  10. package/dist/animation-editor/index.js.map +1 -1
  11. package/dist/canvas/index.cjs +2105 -172
  12. package/dist/canvas/index.cjs.map +1 -1
  13. package/dist/canvas/index.d.cts +319 -23
  14. package/dist/canvas/index.d.ts +319 -23
  15. package/dist/canvas/index.js +2081 -148
  16. package/dist/canvas/index.js.map +1 -1
  17. package/dist/chart/index.cjs +32 -30
  18. package/dist/chart/index.cjs.map +1 -1
  19. package/dist/chart/index.d.cts +4 -2
  20. package/dist/chart/index.d.ts +4 -2
  21. package/dist/chart/index.js +20 -18
  22. package/dist/chart/index.js.map +1 -1
  23. package/dist/{chunk-C3JHII3M.cjs → chunk-3I6V7PI4.cjs} +45 -45
  24. package/dist/{chunk-C3JHII3M.cjs.map → chunk-3I6V7PI4.cjs.map} +1 -1
  25. package/dist/{chunk-P43HUQRX.cjs → chunk-AHCENCU4.cjs} +12 -10
  26. package/dist/chunk-AHCENCU4.cjs.map +1 -0
  27. package/dist/chunk-II77MLHN.cjs +11106 -0
  28. package/dist/chunk-II77MLHN.cjs.map +1 -0
  29. package/dist/{chunk-OIKYP7XC.js → chunk-LMUDHBWR.js} +2 -2
  30. package/dist/{chunk-VODBAO32.js → chunk-OEFCLWZ3.js} +4 -1425
  31. package/dist/chunk-OEFCLWZ3.js.map +1 -0
  32. package/dist/{chunk-NK3WL5TJ.cjs → chunk-P2CTDJNF.cjs} +27 -1448
  33. package/dist/chunk-P2CTDJNF.cjs.map +1 -0
  34. package/dist/chunk-TKPPIVW7.cjs +1443 -0
  35. package/dist/chunk-TKPPIVW7.cjs.map +1 -0
  36. package/dist/chunk-WJSCGBJ2.js +11106 -0
  37. package/dist/chunk-WJSCGBJ2.js.map +1 -0
  38. package/dist/{chunk-RIXTW3C4.js → chunk-WLVZ235X.js} +5 -3
  39. package/dist/{chunk-RIXTW3C4.js.map → chunk-WLVZ235X.js.map} +1 -1
  40. package/dist/chunk-ZOH6X3VR.js +1443 -0
  41. package/dist/chunk-ZOH6X3VR.js.map +1 -0
  42. package/dist/{controller-DscWQbjO.d.ts → controller-D2rj5umx.d.ts} +2 -362
  43. package/dist/{controller-CX0QfSv0.d.cts → controller-R2biKk_E.d.cts} +2 -362
  44. package/dist/diagram/index.cjs +1457 -47
  45. package/dist/diagram/index.cjs.map +1 -1
  46. package/dist/diagram/index.d.cts +1734 -15
  47. package/dist/diagram/index.d.ts +1734 -15
  48. package/dist/diagram/index.js +1436 -26
  49. package/dist/diagram/index.js.map +1 -1
  50. package/dist/extensions/index.cjs +5 -4
  51. package/dist/extensions/index.cjs.map +1 -1
  52. package/dist/extensions/index.d.cts +4 -3
  53. package/dist/extensions/index.d.ts +4 -3
  54. package/dist/extensions/index.js +4 -3
  55. package/dist/{index-DIWB6xMf.d.cts → index-BcHyG0Wq.d.cts} +2 -1
  56. package/dist/{index-CALDD35B.d.ts → index-CTCc3m3C.d.ts} +2 -1
  57. package/dist/index.cjs +10 -9
  58. package/dist/index.cjs.map +1 -1
  59. package/dist/index.d.cts +4 -3
  60. package/dist/index.d.ts +4 -3
  61. package/dist/index.js +4 -3
  62. package/dist/index.js.map +1 -1
  63. package/dist/types-BRp-6zkY.d.cts +362 -0
  64. package/dist/types-BRp-6zkY.d.ts +362 -0
  65. package/dist/{types-BULrFyuQ.d.cts → types-DYkjhzdz.d.cts} +1 -1
  66. package/dist/{types-BULrFyuQ.d.ts → types-DYkjhzdz.d.ts} +1 -1
  67. package/package.json +1 -1
  68. package/dist/chunk-H5K77C7Y.cjs +0 -3848
  69. package/dist/chunk-H5K77C7Y.cjs.map +0 -1
  70. package/dist/chunk-NK3WL5TJ.cjs.map +0 -1
  71. package/dist/chunk-P43HUQRX.cjs.map +0 -1
  72. package/dist/chunk-VODBAO32.js.map +0 -1
  73. package/dist/chunk-WWJNC7SX.js +0 -3848
  74. package/dist/chunk-WWJNC7SX.js.map +0 -1
  75. /package/dist/{chunk-OIKYP7XC.js.map → chunk-LMUDHBWR.js.map} +0 -0
@@ -1,416 +1,16 @@
1
1
  import {
2
- CHART_TYPE_LABELS,
3
- DEFAULT_CHART_PALETTE,
4
2
  DEFAULT_CHART_THEME,
5
3
  chartColorsByCategory,
6
- chartDataFromTable,
7
4
  chartErrorAmounts,
8
5
  chartFamily,
9
6
  chartPointColor,
10
7
  chartSeriesColor,
11
8
  chartSeriesType,
12
- chartSliceColor,
13
- checkChartColors,
14
- cloneChartSeries,
15
- cloneChartSpec,
16
- defaultChartAxis,
17
- describeChartErrorBars,
18
- normalizeChartSpec,
19
- normalizeChartTrendline,
20
- parseChartValue,
21
- shadeChartColor,
22
- uniqueChartSeriesId
9
+ chartSliceColor
23
10
  } from "./chunk-633QC34G.js";
24
11
  import {
25
12
  NumberFormat
26
13
  } from "./chunk-EKYHKQPG.js";
27
- import {
28
- parseCsv,
29
- parseTsv
30
- } from "./chunk-664LQYFJ.js";
31
- import {
32
- HistoryManager
33
- } from "./chunk-QVZZGT4X.js";
34
-
35
- // src/chart/mermaid.ts
36
- var CHART_MERMAID_META_PREFIX = "%% @pixerate ";
37
- var quote = (text) => `"${text.replace(/"/g, "#quot;")}"`;
38
- var unquote = (text) => {
39
- const t = text.trim();
40
- return (t.length >= 2 && t.startsWith('"') && t.endsWith('"') ? t.slice(1, -1) : t).replace(/#quot;/g, '"');
41
- };
42
- var word = (text) => /^[A-Za-z0-9_]+$/.test(text) ? text : quote(text);
43
- var num = (value) => Number.isInteger(value) ? String(value) : String(Number(value.toPrecision(10)));
44
- function splitOutside(text, sep) {
45
- const out = [];
46
- let cur = "";
47
- let quoted = false;
48
- let depth = 0;
49
- for (const ch of text) {
50
- if (ch === '"') quoted = !quoted;
51
- else if (!quoted && (ch === "[" || ch === "{")) depth++;
52
- else if (!quoted && (ch === "]" || ch === "}")) depth--;
53
- if (ch === sep && !quoted && depth === 0) {
54
- out.push(cur);
55
- cur = "";
56
- } else cur += ch;
57
- }
58
- out.push(cur);
59
- return out;
60
- }
61
- function chartMermaidFamily(spec) {
62
- const family = chartFamily(spec);
63
- if (family === "pie" || family === "funnel" || family === "treemap") return "pie";
64
- if (family === "radar") return "radar";
65
- return "xychart";
66
- }
67
- function slug(label, i, taken) {
68
- let base = label.normalize("NFKD").replace(/[^\w-]+/g, "_").replace(/^_+|_+$/g, "") || `a${i + 1}`;
69
- if (!/^[A-Za-z_]/.test(base)) base = `a_${base}`;
70
- let id = base;
71
- for (let k = 2; taken.has(id); k++) id = `${base}_${k}`;
72
- taken.add(id);
73
- return id;
74
- }
75
- function metaFor(spec, text) {
76
- const base = chartFromMermaid(text, { ignoreMeta: true }).spec;
77
- const meta = {};
78
- const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
79
- for (const key of Object.keys(spec)) {
80
- if (key === "series" || key === "version") continue;
81
- if (!same(spec[key], base[key])) meta[key] = spec[key];
82
- }
83
- const series = spec.series.map((s, i) => {
84
- const b = base.series[i];
85
- const out = {};
86
- for (const [k, v] of Object.entries(s)) {
87
- if (k === "values") {
88
- if (b && !same(v, b.values)) out.values = v;
89
- continue;
90
- }
91
- if (!b || !same(v, b[k])) out[k] = v;
92
- }
93
- return out;
94
- });
95
- if (series.some((s) => Object.keys(s).length) || spec.series.length !== base.series.length) meta.series = series;
96
- return Object.keys(meta).length ? meta : null;
97
- }
98
- function chartToMermaid(spec, options = {}) {
99
- const ind = options.indent ?? " ";
100
- const family = chartMermaidFamily(spec);
101
- const visible = spec.series.filter((s) => !s.hidden);
102
- const lines = [];
103
- const v = (x) => x === null || !Number.isFinite(x) ? "0" : num(x);
104
- if (family === "pie") {
105
- const s = visible[0] ?? spec.series[0];
106
- lines.push(`pie${spec.dataLabels ? " showData" : ""}`);
107
- if (spec.title) lines.push(`${ind}title ${spec.title}`);
108
- spec.categories.forEach((c, i) => {
109
- const value = s?.values[i];
110
- if (value !== null && value !== void 0 && Number.isFinite(value) && value > 0) lines.push(`${ind}${quote(c)} : ${num(value)}`);
111
- });
112
- } else if (family === "radar") {
113
- lines.push("radar-beta");
114
- if (spec.title) lines.push(`${ind}title ${spec.title}`);
115
- const taken = /* @__PURE__ */ new Set();
116
- const ids = spec.categories.map((c, i) => slug(c, i, taken));
117
- lines.push(`${ind}axis ${ids.map((id, i) => id === spec.categories[i] ? id : `${id}["${spec.categories[i].replace(/"/g, "'")}"]`).join(", ")}`);
118
- const seriesTaken = new Set(ids);
119
- for (const [i, s] of spec.series.entries()) {
120
- const id = slug(s.id, i, seriesTaken);
121
- lines.push(`${ind}curve ${id}["${s.name.replace(/"/g, "'")}"]{${s.values.map(v).join(", ")}}`);
122
- }
123
- if (spec.yAxis.max !== null) lines.push(`${ind}max ${num(spec.yAxis.max)}`);
124
- if (spec.yAxis.min !== null) lines.push(`${ind}min ${num(spec.yAxis.min)}`);
125
- if (!spec.legend.show) lines.push(`${ind}showLegend false`);
126
- } else {
127
- lines.push(`xychart-beta${spec.orientation === "horizontal" ? " horizontal" : ""}`);
128
- if (spec.title) lines.push(`${ind}title ${quote(spec.title)}`);
129
- const xTitle = spec.xAxis.title ? ` ${quote(spec.xAxis.title)}` : "";
130
- lines.push(`${ind}x-axis${xTitle} [${spec.categories.map(word).join(", ")}]`);
131
- const y = spec.yAxis;
132
- if (y.title || y.min !== null || y.max !== null) {
133
- let out = `${ind}y-axis`;
134
- if (y.title) out += ` ${quote(y.title)}`;
135
- if (y.min !== null || y.max !== null) {
136
- const all = spec.series.flatMap((s) => s.values).filter((x) => x !== null && Number.isFinite(x));
137
- const lo = y.min ?? Math.min(0, ...all);
138
- const hi = y.max ?? Math.max(lo + 1, ...all);
139
- out += ` ${num(lo)} --> ${num(hi)}`;
140
- }
141
- lines.push(out);
142
- }
143
- for (const s of spec.series) {
144
- const kind = (s.type ?? (spec.type === "bar" ? "bar" : "line")) === "bar" ? "bar" : "line";
145
- lines.push(`${ind}${kind} ${quote(s.name)} [${s.values.map(v).join(", ")}]`);
146
- }
147
- }
148
- let text = `${lines.join("\n")}
149
- `;
150
- if (options.meta !== false) {
151
- const meta = metaFor(spec, text);
152
- if (meta) {
153
- const [header, ...rest] = lines;
154
- text = `${[header, `${CHART_MERMAID_META_PREFIX}${JSON.stringify({ v: 1, ...meta })}`, ...rest].join("\n")}
155
- `;
156
- }
157
- }
158
- return text;
159
- }
160
- var RANGE = /^(-?\d+(?:\.\d+)?)\s*-->\s*(-?\d+(?:\.\d+)?)$/;
161
- function takeTitle(text) {
162
- const t = text.trim();
163
- if (t.startsWith('"')) {
164
- const end = t.indexOf('"', 1);
165
- if (end > 0) return { title: unquote(t.slice(0, end + 1)), rest: t.slice(end + 1).trim() };
166
- }
167
- if (!t || t.startsWith("[") || RANGE.test(t) || /^-?\d/.test(t)) return { rest: t };
168
- const m = /^(\S+)\s*(.*)$/.exec(t);
169
- return { title: m[1], rest: m[2].trim() };
170
- }
171
- function parseList(text, partial) {
172
- const t = text.trim();
173
- let m = /^\[(.*)\]$/s.exec(t);
174
- if (!m && partial) m = /^\[(.*)$/s.exec(t);
175
- if (!m) return void 0;
176
- if (!m[1].trim()) return [];
177
- const items = splitOutside(m[1], ",").map((x) => x.trim());
178
- if (partial && !/\]$/.test(t)) {
179
- const last = items[items.length - 1];
180
- if (!last || last.startsWith('"') && !/"$/.test(last.slice(1))) items.pop();
181
- }
182
- return items.map(unquote);
183
- }
184
- function chartFromMermaid(text, options = {}) {
185
- const issues = [];
186
- const partial = !!options.partial;
187
- const all = text.replace(/^/, "").split(/\r?\n/);
188
- let start = 0;
189
- let frontTitle;
190
- if (all[0]?.trim() === "---") {
191
- const end = all.findIndex((l, i) => i > 0 && l.trim() === "---");
192
- if (end > 0) {
193
- for (const l of all.slice(1, end)) {
194
- const m = /^title:\s*(.+)$/.exec(l.trim());
195
- if (m) frontTitle = unquote(m[1]);
196
- }
197
- start = end + 1;
198
- }
199
- }
200
- let family = null;
201
- let headerLine = -1;
202
- let horizontal = false;
203
- let showData = false;
204
- let meta = null;
205
- for (let i = start; i < all.length; i++) {
206
- const t = all[i].trim();
207
- if (!t || t.startsWith("%%")) continue;
208
- let m;
209
- if (m = /^xychart(?:-beta)?(?:\s+(horizontal|vertical))?\s*$/i.exec(t)) {
210
- family = "xychart";
211
- horizontal = m[1]?.toLowerCase() === "horizontal";
212
- } else if (m = /^pie(\s+showData)?(?:\s+title\s+(.*))?\s*$/i.exec(t)) {
213
- family = "pie";
214
- showData = !!m[1];
215
- if (m[2]) frontTitle = m[2].trim();
216
- } else if (/^radar(?:-beta)?\s*$/i.test(t)) family = "radar";
217
- else issues.push({ path: "header", line: i + 1, message: `Line ${i + 1}: expected a chart header (xychart-beta, pie or radar-beta), got "${t.slice(0, 40)}".` });
218
- headerLine = i;
219
- break;
220
- }
221
- let title = frontTitle ?? "";
222
- const categories = [];
223
- const series = [];
224
- const xAxis = defaultChartAxis(false);
225
- const yAxis = defaultChartAxis(true);
226
- const radarAxes = [];
227
- const pieSlices = [];
228
- let legendShow = true;
229
- const counts = { bar: 0, line: 0 };
230
- const lineIssue = (i, message) => issues.push({ path: `line ${i + 1}`, line: i + 1, message: `Line ${i + 1}: ${message}` });
231
- for (let i = headerLine + 1; family && i < all.length; i++) {
232
- const raw = all[i];
233
- const t = raw.trim();
234
- if (!t) continue;
235
- if (t.startsWith(CHART_MERMAID_META_PREFIX.trim())) {
236
- if (options.ignoreMeta) continue;
237
- try {
238
- const parsed = JSON.parse(t.slice(CHART_MERMAID_META_PREFIX.trim().length).trim());
239
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) meta = parsed;
240
- else lineIssue(i, "the @pixerate metadata is not a JSON object; it was ignored.");
241
- } catch {
242
- if (!partial) lineIssue(i, "the @pixerate metadata is not valid JSON; it was ignored.");
243
- }
244
- continue;
245
- }
246
- if (t.startsWith("%%")) continue;
247
- const isLast = i === all.length - 1 || all.slice(i + 1).every((l) => !l.trim());
248
- let m;
249
- if (m = /^title\s+(.+)$/i.exec(t)) {
250
- title = unquote(m[1]);
251
- continue;
252
- }
253
- if (family === "xychart") {
254
- if (m = /^x-axis\b(.*)$/i.exec(t)) {
255
- const { title: xt, rest } = takeTitle(m[1]);
256
- if (xt) xAxis.title = xt;
257
- const list = parseList(rest, partial && isLast);
258
- if (list) categories.splice(0, categories.length, ...list);
259
- else if (RANGE.test(rest)) {
260
- const r = RANGE.exec(rest);
261
- xAxis.min = Number(r[1]);
262
- xAxis.max = Number(r[2]);
263
- } else if (rest) lineIssue(i, `expected x-axis [a, b, \u2026] or a range "min --> max", got "${rest.slice(0, 40)}".`);
264
- continue;
265
- }
266
- if (m = /^y-axis\b(.*)$/i.exec(t)) {
267
- const { title: yt, rest } = takeTitle(m[1]);
268
- if (yt) yAxis.title = yt;
269
- const r = RANGE.exec(rest);
270
- if (r) {
271
- yAxis.min = Number(r[1]);
272
- yAxis.max = Number(r[2]);
273
- } else if (rest) lineIssue(i, `expected y-axis "min --> max", got "${rest.slice(0, 40)}".`);
274
- continue;
275
- }
276
- if (m = /^(bar|line)\b(.*)$/i.exec(t)) {
277
- const kind = m[1].toLowerCase();
278
- const { title: st, rest } = takeTitle(m[2]);
279
- const list = parseList(rest, partial && isLast);
280
- if (!list) {
281
- if (!(partial && isLast)) lineIssue(i, `expected ${kind} [1, 2, 3], got "${rest.slice(0, 40)}".`);
282
- continue;
283
- }
284
- const values = list.map((x) => x.trim() === "" ? null : Number(x));
285
- if (values.some((x) => x !== null && !Number.isFinite(x))) {
286
- lineIssue(i, `${kind} values must be numbers.`);
287
- continue;
288
- }
289
- counts[kind]++;
290
- series.push({ id: `series-${series.length + 1}`, name: st ?? `${kind === "bar" ? "Bar" : "Line"} ${counts[kind]}`, values, type: kind });
291
- continue;
292
- }
293
- } else if (family === "pie") {
294
- if (m = /^"((?:[^"\\]|\\.)*)"\s*:\s*(-?\d+(?:\.\d+)?)\s*$/.exec(t)) {
295
- pieSlices.push({ label: m[1].replace(/#quot;/g, '"'), value: Number(m[2]) });
296
- continue;
297
- }
298
- if (/^showData\s*$/i.test(t)) {
299
- showData = true;
300
- continue;
301
- }
302
- } else if (family === "radar") {
303
- if (m = /^axis\s+(.+)$/.exec(t)) {
304
- for (const part of splitOutside(m[1], ",")) {
305
- const a = /^([A-Za-z_][\w-]*)\s*(?:\[\s*"((?:[^"\\]|\\.)*)"\s*\])?$/.exec(part.trim());
306
- if (a) radarAxes.push({ id: a[1], label: a[2] ?? a[1] });
307
- else lineIssue(i, `could not read the axis "${part.trim()}".`);
308
- }
309
- continue;
310
- }
311
- if (m = /^curve\s+([A-Za-z_][\w-]*)\s*(?:\[\s*"((?:[^"\\]|\\.)*)"\s*\])?\s*\{(.*)\}?\s*$/.exec(t)) {
312
- const body = m[3].replace(/\}\s*$/, "");
313
- const entries = body.trim() ? splitOutside(body, ",").map((x) => x.trim()).filter(Boolean) : [];
314
- const ids = radarAxes.map((a) => a.id);
315
- const values = ids.map(() => null);
316
- let k = 0;
317
- for (const e of entries) {
318
- const keyed = /^([A-Za-z_][\w-]*)\s*:\s*(-?\d+(?:\.\d+)?)$/.exec(e);
319
- if (keyed) {
320
- const at = ids.indexOf(keyed[1]);
321
- if (at >= 0) values[at] = Number(keyed[2]);
322
- else lineIssue(i, `curve ${m[1]} names an unknown axis "${keyed[1]}".`);
323
- } else if (/^-?\d+(?:\.\d+)?$/.test(e)) {
324
- if (k < values.length) values[k] = Number(e);
325
- else values.push(Number(e));
326
- k++;
327
- } else lineIssue(i, `could not read "${e}" in curve ${m[1]}.`);
328
- }
329
- series.push({ id: m[1], name: m[2] ?? m[1], values });
330
- continue;
331
- }
332
- if (m = /^(max|min|ticks|graticule|showLegend)\s+(\S+)$/.exec(t)) {
333
- if (m[1] === "max") yAxis.max = Number(m[2]);
334
- else if (m[1] === "min") yAxis.min = Number(m[2]);
335
- else if (m[1] === "showLegend") legendShow = m[2] !== "false";
336
- continue;
337
- }
338
- }
339
- if (!(partial && isLast)) lineIssue(i, `could not read "${t.slice(0, 60)}"; it was skipped.`);
340
- }
341
- let input;
342
- if (family === "pie") {
343
- input = {
344
- type: "pie",
345
- title,
346
- dataLabels: showData,
347
- categories: pieSlices.map((s) => s.label),
348
- series: [{ id: "series-1", name: title || "Values", values: pieSlices.map((s) => s.value) }]
349
- };
350
- } else if (family === "radar") {
351
- input = { type: "radar", title, categories: radarAxes.map((a) => a.label), series, yAxis, legend: { show: legendShow, position: "bottom" } };
352
- } else {
353
- const allBar = series.length > 0 && series.every((s) => s.type === "bar");
354
- const allLine = series.length > 0 && series.every((s) => s.type === "line");
355
- const type = allLine ? "line" : "bar";
356
- const longest = Math.max(0, ...series.map((s) => s.values.length));
357
- const cats = categories.length ? categories : Array.from({ length: longest }, (_, i) => `${i + 1}`);
358
- input = {
359
- type,
360
- title,
361
- orientation: horizontal ? "horizontal" : "vertical",
362
- categories: cats,
363
- xAxis,
364
- yAxis,
365
- // Series that match the chart type do not need their own type.
366
- series: series.map((s) => allBar || allLine || s.type === type ? { ...s, type: void 0 } : s)
367
- };
368
- }
369
- if (meta) {
370
- const { v: _v, series: metaSeries, ...rest } = meta;
371
- Object.assign(input, rest);
372
- if (Array.isArray(metaSeries)) {
373
- const base = input.series ?? [];
374
- input.series = metaSeries.map((extra, i) => ({ ...base[i] ?? {}, ...extra ?? {} }));
375
- }
376
- }
377
- const { spec, issues: specIssues } = normalizeChartSpec(input);
378
- if (!family && !partial && !issues.length) issues.push({ path: "header", message: "The text is empty: expected a chart header (xychart-beta, pie or radar-beta)." });
379
- return { spec, issues: [...issues, ...specIssues], family, hasMeta: !!meta };
380
- }
381
- function findMermaidCharts(text) {
382
- const lines = text.replace(/^/, "").split(/\r?\n/);
383
- const found = [];
384
- const familyOf = (body) => {
385
- const header = body.split(/\r?\n/).find((l) => l.trim() && !l.trim().startsWith("%%") && l.trim() !== "---");
386
- if (!header) return null;
387
- if (/^\s*xychart(-beta)?\b/.test(header)) return "xychart";
388
- if (/^\s*pie\b/.test(header)) return "pie";
389
- if (/^\s*radar(-beta)?\b/.test(header)) return "radar";
390
- return null;
391
- };
392
- for (let i = 0; i < lines.length; i++) {
393
- const open = /^ {0,3}(`{3,}|~{3,})\s*([\w-]*)/.exec(lines[i]);
394
- if (!open) continue;
395
- const fence = open[1];
396
- const info = open[2].toLowerCase();
397
- let end = i + 1;
398
- while (end < lines.length && !new RegExp(`^ {0,3}${fence[0]}{${fence.length},}\\s*$`).test(lines[end])) end++;
399
- const body = lines.slice(i + 1, end).join("\n");
400
- if (!info || info === "mermaid") {
401
- const family = familyOf(body);
402
- if (family) found.push({ source: `${body.trimEnd()}
403
- `, family, line: i + 2 });
404
- }
405
- i = end;
406
- }
407
- if (!found.length) {
408
- const family = familyOf(text);
409
- if (family) found.push({ source: `${text.trim()}
410
- `, family, line: 1 });
411
- }
412
- return found;
413
- }
414
14
 
415
15
  // src/chart/date.ts
416
16
  var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
@@ -2525,1014 +2125,7 @@ function snapChartValue(scene, value, step, ticks = scene.valueScale?.ticks ?? [
2525
2125
  return Number(snapped.toPrecision(12));
2526
2126
  }
2527
2127
 
2528
- // src/chart/summary.ts
2529
- function summarizeChart(spec, size = { width: 640, height: 400 }, scene = layoutChart(spec, { ...size, selection: null })) {
2530
- const visible = spec.series.filter((s) => !s.hidden);
2531
- const series = visible.map((s) => summarizeChartSeries(s, spec.categories));
2532
- const colors = chartColorsByCategory(spec.type) ? spec.categories.map((_, i) => visible[0]?.pointColors?.[i] || chartSliceColor(spec, i)) : visible.map((s) => chartSeriesColor(spec, spec.series.indexOf(s)));
2533
- const check = checkChartColors(colors, { surface: scene.theme.surface.startsWith("var(") ? "#ffffff" : scene.theme.surface });
2534
- const axis = (s, a) => s ? { min: s.domain[0], max: s.domain[1], ticks: [...s.ticks], ...s.logBase ? { logBase: s.logBase } : {}, ...a.reverse ? { reversed: true } : {} } : void 0;
2535
- return {
2536
- type: spec.type,
2537
- title: spec.title,
2538
- categories: spec.categories.length,
2539
- series,
2540
- ...scene.valueScale ? { valueAxis: axis(scene.valueScale, spec.yAxis) } : {},
2541
- ...scene.valueScale2 ? { secondaryAxis: axis(scene.valueScale2, spec.yAxis2) } : {},
2542
- ...scene.xScale ? { xAxis: axis(scene.xScale, spec.xAxis) } : {},
2543
- warnings: scene.warnings,
2544
- colors: { ok: check.ok, issues: check.issues },
2545
- description: describeChart(spec, series)
2546
- };
2547
- }
2548
- function describeChart(spec, series = spec.series.filter((s) => !s.hidden).map((s) => summarizeChartSeries(s, spec.categories))) {
2549
- const kind = CHART_TYPE_LABELS[spec.type].toLowerCase();
2550
- const parts = [];
2551
- const family = chartFamily(spec);
2552
- const format = spec.yAxis.format;
2553
- const fmt = (v) => v === null ? "blank" : formatChartNumber(v, format);
2554
- const head = spec.title ? `${spec.title}: a ${kind} chart` : `A ${kind} chart`;
2555
- if (!series.length || series.every((s) => s.count === 0)) return `${head} with no data.`;
2556
- if (family === "pie" || family === "funnel" || family === "treemap") {
2557
- const s = series[0];
2558
- const total = s.sum ?? 0;
2559
- const share = (v) => total && v !== null ? ` (${Math.round(v / total * 100)}%)` : "";
2560
- parts.push(`${head} of ${s.name} across ${spec.categories.length} categories, totalling ${fmt(s.sum)}.`);
2561
- parts.push(`The largest is ${s.maxAt} at ${fmt(s.max)}${share(s.max)}; the smallest is ${s.minAt} at ${fmt(s.min)}${share(s.min)}.`);
2562
- return parts.join(" ");
2563
- }
2564
- if (family === "waterfall") {
2565
- const s = spec.series.find((x) => !x.hidden);
2566
- const steps = s.values.filter((v, i) => v !== null && Number.isFinite(v) && !/\b(sub)?total\b/i.test(spec.categories[i] ?? ""));
2567
- const total = steps.reduce((t, v) => t + v, 0);
2568
- const up = steps.filter((v) => v > 0).length;
2569
- const down = steps.filter((v) => v < 0).length;
2570
- return `${head} of ${s.name}: ${up} increase${up === 1 ? "" : "s"} and ${down} decrease${down === 1 ? "" : "s"} over ${spec.categories.length} steps, ending at a total of ${fmt(total)}.`;
2571
- }
2572
- if (family === "histogram" || family === "box") {
2573
- parts.push(`${head} of ${series.map((s) => s.name).join(", ")}.`);
2574
- for (const s of series) parts.push(`${s.name}: ${s.count} values from ${fmt(s.min)} to ${fmt(s.max)}, mean ${fmt(s.mean)}.`);
2575
- return parts.join(" ");
2576
- }
2577
- const what = family === "xy" ? `${series.length} series of points` : `${series.length} series over ${spec.categories.length} categories${spec.categories.length ? ` (${spec.categories[0]} to ${spec.categories[spec.categories.length - 1]})` : ""}`;
2578
- parts.push(`${head} showing ${what}.`);
2579
- for (const s of series.slice(0, 8)) {
2580
- if (!s.count) {
2581
- parts.push(`${s.name} has no values.`);
2582
- continue;
2583
- }
2584
- const pct = s.change !== null ? Math.round(Math.abs(s.change) * 100) : 0;
2585
- const trend = s.trend && s.trend !== "flat" ? `, ${s.trend}${pct ? `, ${s.change > 0 ? "up" : "down"} ${pct}% from first to last` : ""}` : s.trend === "flat" ? ", roughly flat" : "";
2586
- parts.push(`${s.name} ranges from ${fmt(s.min)} (${s.minAt}) to ${fmt(s.max)} (${s.maxAt})${trend}.`);
2587
- }
2588
- if (series.length > 8) parts.push(`${series.length - 8} more series are not described.`);
2589
- const axes = [];
2590
- for (const [a, name] of [
2591
- [spec.yAxis, "value axis"],
2592
- [spec.yAxis2, "secondary value axis"],
2593
- [spec.xAxis, family === "xy" ? "x axis" : "category axis"]
2594
- ]) {
2595
- if (name === "secondary value axis" && !spec.series.some((s) => !s.hidden && s.axis === "secondary")) continue;
2596
- const log = a.scale === "log" && name !== "category axis";
2597
- if (log || a.reverse) axes.push(`The ${name} is ${[log ? `logarithmic (base ${a.logBase ?? 10})` : "", a.reverse ? "reversed" : ""].filter(Boolean).join(" and ")}.`);
2598
- }
2599
- parts.push(...axes);
2600
- for (const s of spec.series.filter((x) => !x.hidden && (x.errorBars || x.xErrorBars)).slice(0, 8)) {
2601
- const bars = [s.errorBars ? describeChartErrorBars(s.errorBars) : "", s.xErrorBars ? `x ${describeChartErrorBars(s.xErrorBars)}` : ""].filter(Boolean).join(" and ");
2602
- parts.push(`${s.name} has ${bars}.`);
2603
- }
2604
- return parts.join(" ");
2605
- }
2606
-
2607
- // src/chart/svg.ts
2608
- function chartTextBaseline(baseline) {
2609
- return baseline === "top" ? "hanging" : baseline === "middle" ? "central" : "text-after-edge";
2610
- }
2611
- function chartTextTransform(mark) {
2612
- return mark.rotate ? `rotate(${mark.rotate} ${mark.x} ${mark.y})` : void 0;
2613
- }
2614
- var escape = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2615
- function markParts(mark, fontFamily) {
2616
- const common = { opacity: mark.opacity, "data-role": mark.role };
2617
- const parts = (tag, values, text) => ({
2618
- tag,
2619
- attrs: Object.fromEntries(Object.entries(values).filter(([, v]) => v !== void 0 && v !== "").map(([k, v]) => [k, String(v)])),
2620
- text
2621
- });
2622
- switch (mark.kind) {
2623
- case "rect":
2624
- return parts("rect", { x: mark.x, y: mark.y, width: mark.width, height: mark.height, rx: mark.radius, fill: mark.fill, stroke: mark.stroke, "stroke-width": mark.strokeWidth, ...common });
2625
- case "path":
2626
- return parts("path", { d: mark.d, fill: mark.fill ?? "none", stroke: mark.stroke, "stroke-width": mark.strokeWidth, "stroke-dasharray": mark.dash, "stroke-linejoin": "round", "stroke-linecap": "round", ...common });
2627
- case "circle":
2628
- return parts("circle", { cx: mark.cx, cy: mark.cy, r: mark.r, fill: mark.fill, stroke: mark.stroke, "stroke-width": mark.strokeWidth, ...common });
2629
- case "line":
2630
- return parts("line", { x1: mark.x1, y1: mark.y1, x2: mark.x2, y2: mark.y2, stroke: mark.stroke, "stroke-width": mark.strokeWidth, "stroke-dasharray": mark.dash, ...common });
2631
- case "text":
2632
- return parts(
2633
- "text",
2634
- {
2635
- x: mark.x,
2636
- y: mark.y,
2637
- "text-anchor": mark.anchor,
2638
- "dominant-baseline": chartTextBaseline(mark.baseline),
2639
- "font-size": mark.size,
2640
- "font-weight": mark.weight,
2641
- "font-family": fontFamily,
2642
- fill: mark.fill,
2643
- transform: chartTextTransform(mark),
2644
- ...common
2645
- },
2646
- mark.text
2647
- );
2648
- }
2649
- }
2650
- function markToSvg(mark, fontFamily) {
2651
- const { tag, attrs, text } = markParts(mark, fontFamily);
2652
- const attrText = Object.entries(attrs).map(([k, v]) => ` ${k}="${escape(v)}"`).join("");
2653
- return text === void 0 ? `<${tag}${attrText}/>` : `<${tag}${attrText}>${escape(text)}</${tag}>`;
2654
- }
2655
- function chartSceneToSvg(scene, options = {}) {
2656
- const hide = options.hideSelection ?? true;
2657
- const font = scene.theme.fontFamily;
2658
- const body = scene.marks.filter((m) => !(hide && m.role === "selection")).map((m) => markToSvg(m, font)).join("");
2659
- const label = options.ariaLabel;
2660
- const desc = options.description;
2661
- const labelled = [label ? "chart-title" : "", desc ? "chart-desc" : ""].filter(Boolean).join(" ");
2662
- const head = `${label ? `<title id="chart-title">${escape(label)}</title>` : ""}${desc ? `<desc id="chart-desc">${escape(desc)}</desc>` : ""}`;
2663
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${scene.width}" height="${scene.height}" viewBox="0 0 ${scene.width} ${scene.height}" role="img"${label ? ` aria-label="${escape(label)}"` : ""}${labelled ? ` aria-labelledby="${labelled}"` : ""}>${head}${body}</svg>`;
2664
- }
2665
- function chartToSvg(spec, options) {
2666
- const scene = layoutChart(spec, { ...options, selection: null });
2667
- return chartSceneToSvg(scene, { ...options, ariaLabel: options.ariaLabel ?? (spec.title || void 0), description: options.description ?? describeChart(spec) });
2668
- }
2669
- var SVG_NS = "http://www.w3.org/2000/svg";
2670
- function chartSceneToDomSpec(scene, options = {}) {
2671
- const hide = options.hideSelection ?? true;
2672
- const font = scene.theme.fontFamily;
2673
- const tag = (name) => `${SVG_NS} ${name}`;
2674
- const children = [];
2675
- if (options.ariaLabel) children.push([tag("title"), {}, options.ariaLabel]);
2676
- if (options.description) children.push([tag("desc"), {}, options.description]);
2677
- for (const mark of scene.marks) {
2678
- if (hide && mark.role === "selection") continue;
2679
- const { tag: name, attrs: attrs2, text } = markParts(mark, font);
2680
- children.push(text === void 0 ? [tag(name), attrs2] : [tag(name), attrs2, text]);
2681
- }
2682
- const attrs = {
2683
- width: String(scene.width),
2684
- height: String(scene.height),
2685
- viewBox: `0 0 ${scene.width} ${scene.height}`,
2686
- role: "img",
2687
- ...options.ariaLabel ? { "aria-label": options.ariaLabel } : {},
2688
- ...options.attrs
2689
- };
2690
- return [tag("svg"), attrs, ...children];
2691
- }
2692
-
2693
- // src/chart/presets.ts
2694
- var CHART_LAYOUT_PRESETS = [
2695
- { id: "standard", label: "Standard", description: "Legend at the bottom, value grid lines, no data labels.", patch: { legend: { show: true, position: "bottom" }, dataLabels: false, xAxis: { gridlines: false, show: true }, yAxis: { gridlines: true, show: true } } },
2696
- { id: "minimal", label: "Minimal", description: "No legend, grid lines or axis titles; the data speaks.", patch: { legend: { show: false }, dataLabels: false, xAxis: { gridlines: false, title: "" }, yAxis: { gridlines: false, title: "" } } },
2697
- { id: "labelled", label: "Labelled", description: "Values written on the marks, no value axis or grid lines.", patch: { dataLabels: true, labelPosition: "auto", yAxis: { show: false, gridlines: false }, xAxis: { gridlines: false } } },
2698
- { id: "legend-right", label: "Legend on the right", description: "Legend beside the plot, value grid lines.", patch: { legend: { show: true, position: "right" }, yAxis: { gridlines: true } } },
2699
- { id: "presentation", label: "Presentation", description: "Legend on top, data labels, light grid.", patch: { legend: { show: true, position: "top" }, dataLabels: true, yAxis: { gridlines: true }, xAxis: { gridlines: false } } },
2700
- { id: "analysis", label: "Analysis", description: "Grid lines on both axes, point markers, legend at the bottom.", patch: { legend: { show: true, position: "bottom" }, points: true, dataLabels: false, xAxis: { gridlines: true }, yAxis: { gridlines: true } } }
2701
- ];
2702
- function monochromeChartPalette(base, count = 6) {
2703
- const steps = count <= 1 ? [0] : Array.from({ length: count }, (_, i) => -0.45 + 1.05 * i / (count - 1));
2704
- return steps.map((t) => shadeChartColor(base, t));
2705
- }
2706
- var CHART_COLOR_SCHEMES = [
2707
- { id: "default", label: "Default", description: "Ten distinct colours, readable on light and dark backgrounds.", palette: DEFAULT_CHART_PALETTE },
2708
- { id: "okabe-ito", label: "Okabe\u2013Ito", description: "Eight colours chosen to stay distinct with colour-blindness.", palette: ["#e69f00", "#56b4e9", "#009e73", "#f0e442", "#0072b2", "#d55e00", "#cc79a7", "#000000"] },
2709
- { id: "office", label: "Office", description: "The six accent colours of the default Office theme.", palette: ["#4472c4", "#ed7d31", "#a5a5a5", "#ffc000", "#5b9bd5", "#70ad47"] },
2710
- { id: "warm", label: "Warm", description: "Reds, oranges and yellows.", palette: ["#b2182b", "#d6604d", "#f4a582", "#e08214", "#fdb863", "#8c510a"] },
2711
- { id: "cool", label: "Cool", description: "Blues, teals and purples.", palette: ["#2166ac", "#4393c3", "#35978f", "#80cdc1", "#762a83", "#9970ab"] },
2712
- { id: "monochrome-blue", label: "Monochrome blue", description: "Shades of one blue.", palette: monochromeChartPalette("#4269d0") },
2713
- { id: "monochrome-green", label: "Monochrome green", description: "Shades of one green.", palette: monochromeChartPalette("#3ca951") },
2714
- { id: "monochrome-orange", label: "Monochrome orange", description: "Shades of one orange.", palette: monochromeChartPalette("#e8710a") },
2715
- { id: "monochrome-gray", label: "Monochrome grey", description: "Shades of grey.", palette: monochromeChartPalette("#6b7280") }
2716
- ];
2717
- function chartPresetPatch(options) {
2718
- const patch = {};
2719
- if (options.layout !== void 0) {
2720
- const preset = CHART_LAYOUT_PRESETS.find((p) => p.id === options.layout);
2721
- if (!preset) return { ok: false, message: `There is no layout "${options.layout}"; expected one of ${CHART_LAYOUT_PRESETS.map((p) => `"${p.id}"`).join(", ")}.` };
2722
- Object.assign(patch, preset.patch);
2723
- }
2724
- if (options.colors !== void 0) {
2725
- const scheme = CHART_COLOR_SCHEMES.find((p) => p.id === options.colors);
2726
- if (!scheme) return { ok: false, message: `There is no colour scheme "${options.colors}"; expected one of ${CHART_COLOR_SCHEMES.map((p) => `"${p.id}"`).join(", ")}.` };
2727
- patch.palette = [...scheme.palette];
2728
- }
2729
- if (options.layout === void 0 && options.colors === void 0) return { ok: false, message: "Name a layout, a colour scheme or both." };
2730
- return { ok: true, patch };
2731
- }
2732
-
2733
- // src/chart/controller.ts
2734
- function sameChartJson(a, b) {
2735
- if (a === b) return true;
2736
- if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return Number.isNaN(a) && Number.isNaN(b);
2737
- if (Array.isArray(a) !== Array.isArray(b)) return false;
2738
- if (Array.isArray(a)) {
2739
- const bb = b;
2740
- return a.length === bb.length && a.every((v, i) => sameChartJson(v, bb[i]));
2741
- }
2742
- const ao = a;
2743
- const bo = b;
2744
- const keys = Object.keys(ao).filter((k) => ao[k] !== void 0);
2745
- if (keys.length !== Object.keys(bo).filter((k) => bo[k] !== void 0).length) return false;
2746
- return keys.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && sameChartJson(ao[k], bo[k]));
2747
- }
2748
- var fail = (message, issues) => ({ ok: false, message, ...issues ? { issues } : {} });
2749
- var ChartController = class {
2750
- history;
2751
- spec;
2752
- selection = null;
2753
- issues = [];
2754
- gestureKey = null;
2755
- gestureCount = 0;
2756
- gestureStart = null;
2757
- revision = 0;
2758
- applying = false;
2759
- onChange;
2760
- listeners = /* @__PURE__ */ new Set();
2761
- unsubscribeHistory;
2762
- state;
2763
- constructor(options = {}) {
2764
- this.history = options.history ?? new HistoryManager();
2765
- this.onChange = options.onChange;
2766
- const { spec, issues } = normalizeChartSpec(options.spec ?? {});
2767
- this.spec = spec;
2768
- this.issues = issues;
2769
- this.state = this.computeState();
2770
- this.connect();
2771
- }
2772
- connect() {
2773
- this.unsubscribeHistory?.();
2774
- this.unsubscribeHistory = this.history.subscribe((h) => {
2775
- const s = this.state;
2776
- if (this.applying || s.canUndo === h.canUndo && s.canRedo === h.canRedo && s.undoLabel === h.undoName && s.redoLabel === h.redoName) return;
2777
- this.notify(false);
2778
- });
2779
- }
2780
- // -------------------------------------------------------------------------
2781
- // State
2782
- // -------------------------------------------------------------------------
2783
- /** The current snapshot (stable until the next change). */
2784
- getState = () => this.state;
2785
- /** Calls `listener` after every change. Returns an unsubscribe function. */
2786
- subscribe = (listener) => {
2787
- if (!this.unsubscribeHistory) this.connect();
2788
- this.listeners.add(listener);
2789
- return () => {
2790
- this.listeners.delete(listener);
2791
- };
2792
- };
2793
- /** The current spec (do not mutate; edit through the controller). */
2794
- getSpec() {
2795
- return this.spec;
2796
- }
2797
- getSeries(id) {
2798
- return this.spec.series.find((s) => s.id === id);
2799
- }
2800
- computeState() {
2801
- const h = this.history.getState();
2802
- const sel = this.selection;
2803
- const seriesId = sel?.kind === "series" || sel?.kind === "point" || sel?.kind === "trendline" ? sel.seriesId : null;
2804
- return {
2805
- spec: this.spec,
2806
- selection: sel,
2807
- selectedSeries: seriesId ? this.spec.series.find((s) => s.id === seriesId) ?? null : null,
2808
- selectedIndex: sel?.kind === "point" || sel?.kind === "category" ? sel.index : null,
2809
- issues: this.issues,
2810
- gesture: this.gestureKey,
2811
- canUndo: h.canUndo,
2812
- canRedo: h.canRedo,
2813
- undoLabel: h.undoName,
2814
- redoLabel: h.redoName,
2815
- revision: this.revision
2816
- };
2817
- }
2818
- notify(specChanged) {
2819
- this.revision++;
2820
- this.state = this.computeState();
2821
- if (specChanged) this.onChange?.(this.spec);
2822
- const state = this.state;
2823
- this.listeners.forEach((listener) => {
2824
- try {
2825
- listener(state);
2826
- } catch (error) {
2827
- console.error("[ChartController] Error in state listener:", error);
2828
- }
2829
- });
2830
- }
2831
- // -------------------------------------------------------------------------
2832
- // Changes: every edit records a before / after snapshot of the spec
2833
- // -------------------------------------------------------------------------
2834
- /** Drops a selection that no longer points at anything. */
2835
- validSelection(sel, spec) {
2836
- if (!sel) return null;
2837
- switch (sel.kind) {
2838
- case "series":
2839
- return spec.series.some((s) => s.id === sel.seriesId) ? sel : null;
2840
- case "point":
2841
- return spec.series.some((s) => s.id === sel.seriesId) && sel.index < spec.categories.length ? sel : null;
2842
- case "category":
2843
- return sel.index < spec.categories.length ? sel : null;
2844
- case "trendline":
2845
- return spec.series.find((s) => s.id === sel.seriesId)?.trendlines?.[sel.index] ? sel : null;
2846
- case "title":
2847
- return spec.title ? sel : null;
2848
- case "subtitle":
2849
- return spec.subtitle ? sel : null;
2850
- default:
2851
- return sel;
2852
- }
2853
- }
2854
- applySpec(spec) {
2855
- this.applying = true;
2856
- try {
2857
- this.spec = spec;
2858
- this.selection = this.validSelection(this.selection, spec);
2859
- } finally {
2860
- this.applying = false;
2861
- }
2862
- this.notify(true);
2863
- }
2864
- commit(label, next, select) {
2865
- const before = this.spec;
2866
- const hadIssues = this.issues.length > 0;
2867
- this.issues = [];
2868
- if (sameChartJson(before, next)) {
2869
- const reselect = select !== void 0 && !sameChartSelection(this.selection, select) && !(select === null && this.selection === null);
2870
- if (reselect) this.selection = select;
2871
- if (reselect || hadIssues) this.notify(false);
2872
- return;
2873
- }
2874
- const command = {
2875
- name: label,
2876
- tag: "chart",
2877
- execute: () => this.applySpec(next),
2878
- undo: () => this.applySpec(before),
2879
- redo: () => this.applySpec(next)
2880
- };
2881
- if (select !== void 0) this.selection = select;
2882
- if (this.gestureKey !== null) this.history.executeMerged(command, { key: `chart.gesture:${this.gestureKey}:${this.gestureCount}` });
2883
- else this.history.execute(command);
2884
- }
2885
- reject(message, issues = []) {
2886
- this.issues = issues.length ? issues : [{ path: "", message }];
2887
- this.notify(false);
2888
- return fail(message, issues.length ? issues : void 0);
2889
- }
2890
- /** A copy of the spec with `edit` applied (series and categories copied as needed). */
2891
- edited(edit) {
2892
- const draft = cloneChartSpec(this.spec);
2893
- edit(draft);
2894
- return draft;
2895
- }
2896
- unknownSeries(id) {
2897
- const ids = this.spec.series.map((s) => `"${s.id}"`);
2898
- return fail(`There is no series "${id}"; ${ids.length ? `expected one of ${ids.join(", ")}` : "the chart has no series yet"}.`);
2899
- }
2900
- badIndex(index) {
2901
- const n = this.spec.categories.length;
2902
- return fail(n ? `Category index ${index} is out of range; expected 0 to ${n - 1}.` : "The chart has no categories yet.");
2903
- }
2904
- // -------------------------------------------------------------------------
2905
- // Presentation
2906
- // -------------------------------------------------------------------------
2907
- /**
2908
- * Changes any presentation or data fields as one undo step. All-or-nothing: if anything
2909
- * is invalid, nothing changes and every problem is returned (and shown in `state.issues`).
2910
- */
2911
- update(patch, options = {}) {
2912
- const s = this.spec;
2913
- const merged = {
2914
- ...s,
2915
- ...patch,
2916
- xAxis: patch.xAxis ? { ...s.xAxis, ...patch.xAxis } : s.xAxis,
2917
- yAxis: patch.yAxis ? { ...s.yAxis, ...patch.yAxis } : s.yAxis,
2918
- yAxis2: patch.yAxis2 ? { ...s.yAxis2, ...patch.yAxis2 } : s.yAxis2,
2919
- legend: patch.legend ? { ...s.legend, ...patch.legend } : s.legend,
2920
- version: 1
2921
- };
2922
- const { spec, issues } = normalizeChartSpec(merged);
2923
- if (issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
2924
- if (JSON.stringify(spec) === JSON.stringify(s)) return { ok: true };
2925
- this.commit(options.label ?? updateLabel(patch), spec);
2926
- return { ok: true };
2927
- }
2928
- setType(type) {
2929
- return this.update({ type }, { label: `Change chart type` });
2930
- }
2931
- setTitle(title) {
2932
- return this.update({ title }, { label: "Edit title" });
2933
- }
2934
- setSubtitle(subtitle) {
2935
- return this.update({ subtitle }, { label: "Edit subtitle" });
2936
- }
2937
- /** Changes the category (`x`), value (`y`) or secondary value (`y2`) axis. */
2938
- setAxis(axis, patch) {
2939
- const key = axis === "x" ? "xAxis" : axis === "y" ? "yAxis" : "yAxis2";
2940
- return this.update({ [key]: patch }, { label: `Change ${axis === "x" ? "category" : axis === "y" ? "value" : "secondary value"} axis` });
2941
- }
2942
- /**
2943
- * Applies a named quick layout and / or colour scheme (see `CHART_LAYOUT_PRESETS` and
2944
- * `CHART_COLOR_SCHEMES`) as one undo step.
2945
- */
2946
- applyPreset(options) {
2947
- const preset = chartPresetPatch(options);
2948
- if (!preset.ok) return this.reject(preset.message);
2949
- const label = options.layout && options.colors ? "Apply chart style" : options.layout ? "Apply quick layout" : "Change colours";
2950
- return this.update(preset.patch, { label });
2951
- }
2952
- setLegend(patch) {
2953
- return this.update({ legend: patch }, { label: "Change legend" });
2954
- }
2955
- // -------------------------------------------------------------------------
2956
- // Data
2957
- // -------------------------------------------------------------------------
2958
- /**
2959
- * Replaces categories and / or series as one undo step. Values are validated and padded
2960
- * to the categories; all-or-nothing like `update`.
2961
- */
2962
- setData(data, options = {}) {
2963
- return this.update({ ...data.categories ? { categories: data.categories } : {}, ...data.series ? { series: data.series } : {} }, { label: options.label ?? "Edit chart data" });
2964
- }
2965
- /**
2966
- * Sets one value (numbers, numeric text such as `1,200` or `12%`, or blank for a gap).
2967
- * One undo step, or part of the current gesture.
2968
- */
2969
- setValue(seriesId, index, value, options = {}) {
2970
- const series = this.getSeries(seriesId);
2971
- if (!series) return this.unknownSeries(seriesId);
2972
- if (!Number.isInteger(index) || index < 0 || index >= this.spec.categories.length) return this.badIndex(index);
2973
- const parsed = parseChartValue(value);
2974
- if (parsed === void 0) return this.reject(`"${String(value)}" is not a number; enter a number or leave the cell blank.`, [{ path: `${seriesId}[${index}]`, message: `"${String(value)}" is not a number.` }]);
2975
- if (series.values[index] === parsed) {
2976
- if (this.issues.length) {
2977
- this.issues = [];
2978
- this.notify(false);
2979
- }
2980
- return { ok: true };
2981
- }
2982
- const next = this.edited((d) => {
2983
- d.series.find((s) => s.id === seriesId).values[index] = parsed;
2984
- });
2985
- this.commit(options.label ?? `Edit ${series.name} \xB7 ${this.spec.categories[index]}`, next);
2986
- return { ok: true };
2987
- }
2988
- /** Several values as one undo step (all-or-nothing). */
2989
- setValues(cells, options = {}) {
2990
- const issues = [];
2991
- const parsed = [];
2992
- for (const cell of cells) {
2993
- if (!this.getSeries(cell.seriesId)) issues.push({ path: cell.seriesId, message: this.unknownSeries(cell.seriesId).message });
2994
- else if (!Number.isInteger(cell.index) || cell.index < 0 || cell.index >= this.spec.categories.length) issues.push({ path: `${cell.seriesId}[${cell.index}]`, message: this.badIndex(cell.index).message });
2995
- else {
2996
- const value = parseChartValue(cell.value);
2997
- if (value === void 0) issues.push({ path: `${cell.seriesId}[${cell.index}]`, message: `"${String(cell.value)}" is not a number.` });
2998
- else parsed.push({ seriesId: cell.seriesId, index: cell.index, value });
2999
- }
3000
- }
3001
- if (issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
3002
- const next = this.edited((d) => {
3003
- for (const cell of parsed) d.series.find((s) => s.id === cell.seriesId).values[cell.index] = cell.value;
3004
- });
3005
- if (JSON.stringify(next.series) !== JSON.stringify(this.spec.series)) this.commit(options.label ?? "Edit values", next);
3006
- return { ok: true };
3007
- }
3008
- /** Adds a series (values padded to the categories) and selects it. Returns its id. */
3009
- addSeries(input = {}, options = {}) {
3010
- const id = input.id && !this.getSeries(input.id) ? input.id : uniqueChartSeriesId(this.spec.series);
3011
- const name = input.name ?? `Series ${this.spec.series.length + 1}`;
3012
- const at = Math.max(0, Math.min(this.spec.series.length, options.at ?? this.spec.series.length));
3013
- const series = [...this.spec.series];
3014
- series.splice(at, 0, { ...input, id, name, values: input.values ?? [] });
3015
- const { spec, issues } = normalizeChartSpec({ ...this.spec, series });
3016
- if (issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
3017
- this.commit(`Add series "${name}"`, spec, { kind: "series", seriesId: id });
3018
- return { ok: true, id };
3019
- }
3020
- removeSeries(id) {
3021
- const series = this.getSeries(id);
3022
- if (!series) return this.unknownSeries(id);
3023
- this.commit(
3024
- `Delete series "${series.name}"`,
3025
- this.edited((d) => {
3026
- d.series = d.series.filter((s) => s.id !== id);
3027
- })
3028
- );
3029
- return { ok: true };
3030
- }
3031
- updateSeries(id, patch) {
3032
- const series = this.getSeries(id);
3033
- if (!series) return this.unknownSeries(id);
3034
- const next = cloneChartSeries(series);
3035
- if (patch.name !== void 0) next.name = String(patch.name);
3036
- if (patch.color === null || patch.color === "") delete next.color;
3037
- else if (patch.color !== void 0) next.color = patch.color;
3038
- if (patch.type === null) delete next.type;
3039
- else if (patch.type !== void 0) next.type = patch.type;
3040
- if (patch.hidden !== void 0) {
3041
- if (patch.hidden) next.hidden = true;
3042
- else delete next.hidden;
3043
- }
3044
- const resettable = ["axis", "x", "sizes", "pointColors", "marker", "curve", "dataLabels", "format", "errorBars", "xErrorBars"];
3045
- for (const key of resettable) {
3046
- if (!(key in patch) || patch[key] === void 0) continue;
3047
- if (patch[key] === null) delete next[key];
3048
- else next[key] = patch[key];
3049
- }
3050
- if (next.axis === "primary") delete next.axis;
3051
- const series2 = this.spec.series.map((s) => s.id === id ? next : s);
3052
- const { spec, issues } = normalizeChartSpec({ ...this.spec, series: series2 });
3053
- if (issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
3054
- if (JSON.stringify(spec.series) === JSON.stringify(this.spec.series)) return { ok: true };
3055
- const label = patch.hidden !== void 0 && Object.keys(patch).length === 1 ? `${patch.hidden ? "Hide" : "Show"} "${series.name}"` : patch.name !== void 0 && Object.keys(patch).length === 1 ? "Rename series" : `Change series "${series.name}"`;
3056
- this.commit(label, spec);
3057
- return { ok: true };
3058
- }
3059
- toggleSeries(id) {
3060
- const series = this.getSeries(id);
3061
- if (!series) return this.unknownSeries(id);
3062
- return this.updateSeries(id, { hidden: !series.hidden });
3063
- }
3064
- /** Moves a series to position `to` (drawing and legend order). */
3065
- moveSeries(id, to) {
3066
- const from = this.spec.series.findIndex((s) => s.id === id);
3067
- if (from < 0) return this.unknownSeries(id);
3068
- const target = Math.max(0, Math.min(this.spec.series.length - 1, to));
3069
- if (target === from) return { ok: true };
3070
- this.commit(
3071
- "Reorder series",
3072
- this.edited((d) => {
3073
- const [s] = d.series.splice(from, 1);
3074
- d.series.splice(target, 0, s);
3075
- })
3076
- );
3077
- return { ok: true };
3078
- }
3079
- /** Inserts a category (with gaps in every series) at `at` (default: the end). Returns its index. */
3080
- addCategory(label, options = {}) {
3081
- const at = Math.max(0, Math.min(this.spec.categories.length, options.at ?? this.spec.categories.length));
3082
- const name = label ?? `${this.spec.categories.length + 1}`;
3083
- this.commit(
3084
- `Add category "${name}"`,
3085
- this.edited((d) => {
3086
- d.categories.splice(at, 0, name);
3087
- for (const s of d.series) s.values.splice(at, 0, null);
3088
- }),
3089
- { kind: "category", index: at }
3090
- );
3091
- return { ok: true, index: at };
3092
- }
3093
- removeCategory(index) {
3094
- if (!Number.isInteger(index) || index < 0 || index >= this.spec.categories.length) return this.badIndex(index);
3095
- this.commit(
3096
- `Delete category "${this.spec.categories[index]}"`,
3097
- this.edited((d) => {
3098
- d.categories.splice(index, 1);
3099
- for (const s of d.series) s.values.splice(index, 1);
3100
- })
3101
- );
3102
- return { ok: true };
3103
- }
3104
- renameCategory(index, label) {
3105
- if (!Number.isInteger(index) || index < 0 || index >= this.spec.categories.length) return this.badIndex(index);
3106
- if (this.spec.categories[index] === label) return { ok: true };
3107
- this.commit(
3108
- "Rename category",
3109
- this.edited((d) => {
3110
- d.categories[index] = String(label);
3111
- })
3112
- );
3113
- return { ok: true };
3114
- }
3115
- moveCategory(from, to) {
3116
- if (!Number.isInteger(from) || from < 0 || from >= this.spec.categories.length) return this.badIndex(from);
3117
- const target = Math.max(0, Math.min(this.spec.categories.length - 1, to));
3118
- if (target === from) return { ok: true };
3119
- const move = (list) => {
3120
- const [item] = list.splice(from, 1);
3121
- list.splice(target, 0, item);
3122
- };
3123
- this.commit(
3124
- "Reorder categories",
3125
- this.edited((d) => {
3126
- move(d.categories);
3127
- for (const s of d.series) move(s.values);
3128
- })
3129
- );
3130
- return { ok: true };
3131
- }
3132
- /**
3133
- * Pastes a block of cells (TSV / CSV text or rows) into the data grid with its top-left at
3134
- * `row` (category index) and `column` (series index; `-1` is the category column). The
3135
- * grid grows to fit. Non-numeric value cells become gaps and are reported in `issues`.
3136
- */
3137
- pasteCells(row, column, cells) {
3138
- const rows = typeof cells === "string" ? cells.includes(" ") ? parseTsv(cells) : parseCsv(cells) : cells.map((r) => [...r]);
3139
- if (!rows.length) return { ok: true };
3140
- const issues = [];
3141
- const next = this.edited((d) => {
3142
- const width = Math.max(...rows.map((r) => r.length));
3143
- const lastSeries = column + width - 1;
3144
- while (d.series.length <= lastSeries) {
3145
- const id = uniqueChartSeriesId(d.series);
3146
- d.series.push({ id, name: `Series ${d.series.length + 1}`, values: d.categories.map(() => null) });
3147
- }
3148
- const lastRow = row + rows.length - 1;
3149
- while (d.categories.length <= lastRow) {
3150
- d.categories.push(`${d.categories.length + 1}`);
3151
- for (const s of d.series) s.values.push(null);
3152
- }
3153
- rows.forEach((cellsInRow, r) => {
3154
- cellsInRow.forEach((cell, c) => {
3155
- const col = column + c;
3156
- const index = row + r;
3157
- if (col < 0) d.categories[index] = cell === null || cell === void 0 ? "" : String(cell).trim();
3158
- else {
3159
- const parsed = parseChartValue(cell);
3160
- if (parsed === void 0) issues.push({ path: `${d.series[col].id}[${index}]`, message: `"${String(cell)}" is not a number; it was left blank.` });
3161
- d.series[col].values[index] = parsed ?? null;
3162
- }
3163
- });
3164
- });
3165
- });
3166
- this.commit("Paste cells", next);
3167
- if (issues.length) {
3168
- this.issues = issues;
3169
- this.notify(false);
3170
- }
3171
- return issues.length ? { ok: true, issues } : { ok: true };
3172
- }
3173
- /**
3174
- * Replaces the data with a grid of cells (see `chartDataFromTable`): one undo step. Series
3175
- * colours and types are kept by name.
3176
- */
3177
- importTable(rows, options = {}) {
3178
- const data = chartDataFromTable(rows, options);
3179
- if (!data.categories.length || !data.series.length) return this.reject("There is no data to import: expected a header row and at least one row of values.");
3180
- const byName = new Map(this.spec.series.map((s) => [s.name, s]));
3181
- const series = data.series.map((s) => {
3182
- const old = byName.get(s.name);
3183
- return { ...s, ...old?.color ? { color: old.color } : {}, ...old?.type ? { type: old.type } : {}, ...old ? { id: old.id } : {} };
3184
- });
3185
- const { spec } = normalizeChartSpec({ ...this.spec, categories: data.categories, series });
3186
- this.commit("Import data", spec);
3187
- if (data.issues.length) {
3188
- this.issues = data.issues;
3189
- this.notify(false);
3190
- return { ok: true, issues: data.issues };
3191
- }
3192
- return { ok: true };
3193
- }
3194
- /** Imports CSV (or TSV, detected by tabs) text; see `importTable`. */
3195
- importCsv(text, options = {}) {
3196
- const delimiter = options.delimiter ?? (text.includes(" ") ? " " : ",");
3197
- return this.importTable(parseCsv(text, { delimiter }), options);
3198
- }
3199
- // -------------------------------------------------------------------------
3200
- // Trendlines
3201
- // -------------------------------------------------------------------------
3202
- /** Adds a trendline to a series and selects it. Returns its index (and the fit, when it can be computed). */
3203
- addTrendline(seriesId, input) {
3204
- const series = this.getSeries(seriesId);
3205
- if (!series) return this.unknownSeries(seriesId);
3206
- if (!chartSupportsTrendlines(this.spec.type, this.spec.stacking))
3207
- return this.reject(`Trendlines need an unstacked bar, line, area, scatter or bubble chart; this is a ${this.spec.stacking !== "none" ? `${this.spec.stacking} ` : ""}${this.spec.type} chart.`);
3208
- const issues = [];
3209
- const line = normalizeChartTrendline(input, "trendline", issues);
3210
- if (!line || issues.length) return this.reject(issues.map((i) => i.message).join(" ") || "The trendline is not valid.", issues);
3211
- const index = series.trendlines?.length ?? 0;
3212
- const next = this.edited((d) => {
3213
- const s = d.series.find((x) => x.id === seriesId);
3214
- s.trendlines = [...s.trendlines ?? [], line];
3215
- });
3216
- this.commit(`Add trendline to "${series.name}"`, next, { kind: "trendline", seriesId, index });
3217
- return { ok: true, index, fit: this.trendlineFit(seriesId, index) };
3218
- }
3219
- /** Changes a trendline (all-or-nothing). */
3220
- updateTrendline(seriesId, index, patch) {
3221
- const series = this.getSeries(seriesId);
3222
- if (!series) return this.unknownSeries(seriesId);
3223
- const current = series.trendlines?.[index];
3224
- if (!current) return fail(`"${series.name}" has no trendline ${index}; ${series.trendlines?.length ? `expected 0 to ${series.trendlines.length - 1}` : "it has none yet"}.`);
3225
- const issues = [];
3226
- const merged = { ...current, ...patch };
3227
- for (const [k, v] of Object.entries(patch)) if (v === null) delete merged[k];
3228
- const line = normalizeChartTrendline(merged, "trendline", issues);
3229
- if (!line || issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
3230
- this.commit(
3231
- "Change trendline",
3232
- this.edited((d) => {
3233
- d.series.find((x) => x.id === seriesId).trendlines[index] = line;
3234
- })
3235
- );
3236
- return { ok: true };
3237
- }
3238
- removeTrendline(seriesId, index) {
3239
- const series = this.getSeries(seriesId);
3240
- if (!series) return this.unknownSeries(seriesId);
3241
- if (!series.trendlines?.[index]) return fail(`"${series.name}" has no trendline ${index}.`);
3242
- this.commit(
3243
- "Delete trendline",
3244
- this.edited((d) => {
3245
- const s = d.series.find((x) => x.id === seriesId);
3246
- s.trendlines = s.trendlines.filter((_, i) => i !== index);
3247
- if (!s.trendlines.length) delete s.trendlines;
3248
- })
3249
- );
3250
- return { ok: true };
3251
- }
3252
- /** The points a series' trendlines are fitted to: (x, y) with x = 1, 2, 3 … on category charts. */
3253
- trendlinePoints(seriesId) {
3254
- const series = this.getSeries(seriesId);
3255
- if (!series) return [];
3256
- const xy = chartFamily(this.spec) === "xy";
3257
- const out = [];
3258
- series.values.forEach((v, i) => {
3259
- const x = xy ? series.x?.[i] ?? (this.spec.type === "bubble" ? i + 1 : null) : i + 1;
3260
- if (v !== null && x !== null && Number.isFinite(v) && Number.isFinite(x)) out.push([x, v]);
3261
- });
3262
- return out;
3263
- }
3264
- /** The fitted coefficients, R² and equation of a trendline (null for moving averages or unfittable data). */
3265
- trendlineFit(seriesId, index) {
3266
- const line = this.getSeries(seriesId)?.trendlines?.[index];
3267
- if (!line) return null;
3268
- return fitChartTrendline(line, this.trendlinePoints(seriesId));
3269
- }
3270
- /** The moving-average points of a trendline (empty for other kinds). */
3271
- trendlineMovingAverage(seriesId, index) {
3272
- const line = this.getSeries(seriesId)?.trendlines?.[index];
3273
- if (!line || line.type !== "moving-average") return [];
3274
- return chartMovingAverage(this.trendlinePoints(seriesId), line.period ?? 2);
3275
- }
3276
- // -------------------------------------------------------------------------
3277
- // Reshaping
3278
- // -------------------------------------------------------------------------
3279
- /**
3280
- * Switches rows and columns: series become categories and categories become series (one
3281
- * undo step). Series colours, types and other per-series settings are dropped.
3282
- */
3283
- transpose() {
3284
- const s = this.spec;
3285
- if (!s.series.length || !s.categories.length) return this.reject("There is no data to switch: the chart needs at least one series and one category.");
3286
- const used = /* @__PURE__ */ new Set();
3287
- const series = s.categories.map((name, i) => {
3288
- let id = `series-${i + 1}`;
3289
- for (let k = i + 2; used.has(id); k++) id = `series-${k}`;
3290
- used.add(id);
3291
- return { id, name, values: s.series.map((x) => x.values[i] ?? null) };
3292
- });
3293
- const { spec } = normalizeChartSpec({ ...s, categories: s.series.map((x) => x.name), series });
3294
- this.commit("Switch rows and columns", spec, null);
3295
- return { ok: true };
3296
- }
3297
- // -------------------------------------------------------------------------
3298
- // Selection and gestures
3299
- // -------------------------------------------------------------------------
3300
- /** Selects a chart element (view state; not an undo step). */
3301
- select(selection) {
3302
- const next = this.validSelection(selection, this.spec);
3303
- if (sameChartSelection(next, this.selection) || next === null && this.selection === null) return;
3304
- this.selection = next;
3305
- this.notify(false);
3306
- }
3307
- /**
3308
- * Deletes what is selected: a series or category is removed, a point becomes a gap, a
3309
- * title is cleared, the legend or an axis is hidden.
3310
- */
3311
- deleteSelection() {
3312
- const sel = this.selection;
3313
- if (!sel) return fail("Nothing is selected.");
3314
- switch (sel.kind) {
3315
- case "series":
3316
- return this.removeSeries(sel.seriesId);
3317
- case "category":
3318
- return this.removeCategory(sel.index);
3319
- case "point":
3320
- return this.setValue(sel.seriesId, sel.index, null, { label: "Clear value" });
3321
- case "title":
3322
- return this.setTitle("");
3323
- case "subtitle":
3324
- return this.setSubtitle("");
3325
- case "legend":
3326
- return this.setLegend({ show: false });
3327
- case "axis":
3328
- return this.setAxis(sel.axis, { show: false });
3329
- case "trendline":
3330
- return this.removeTrendline(sel.seriesId, sel.index);
3331
- default:
3332
- return fail("The chart itself cannot be deleted.");
3333
- }
3334
- }
3335
- /** Adds `delta` to a value (a gap counts as 0). */
3336
- nudgeValue(seriesId, index, delta) {
3337
- const series = this.getSeries(seriesId);
3338
- if (!series) return this.unknownSeries(seriesId);
3339
- const current = series.values[index] ?? 0;
3340
- return this.setValue(seriesId, index, Number((current + delta).toPrecision(12)), { label: `Edit ${series.name} \xB7 ${this.spec.categories[index] ?? index}` });
3341
- }
3342
- /** Starts a continuous edit (a drag): every change until `endGesture()` is one undo step. */
3343
- beginGesture(key) {
3344
- if (this.gestureKey === key) return;
3345
- this.history.endGesture();
3346
- this.gestureKey = key;
3347
- this.gestureCount++;
3348
- this.gestureStart = { spec: this.spec, mark: this.history.mark() };
3349
- this.notify(false);
3350
- }
3351
- /**
3352
- * Cancels the gesture in progress (Escape during a drag): everything it changed is taken
3353
- * back and leaves no undo or redo step.
3354
- */
3355
- cancelGesture() {
3356
- if (this.gestureKey === null) return;
3357
- const start = this.gestureStart;
3358
- this.gestureKey = null;
3359
- this.gestureStart = null;
3360
- this.history.endGesture();
3361
- if (start) this.history.revertTo(start.mark);
3362
- this.notify(false);
3363
- }
3364
- endGesture() {
3365
- if (this.gestureKey === null) return;
3366
- this.gestureKey = null;
3367
- this.history.endGesture();
3368
- const start = this.gestureStart;
3369
- this.gestureStart = null;
3370
- if (start && sameChartJson(start.spec, this.spec)) this.history.revertTo(start.mark);
3371
- this.notify(false);
3372
- }
3373
- // -------------------------------------------------------------------------
3374
- // Persistence and history
3375
- // -------------------------------------------------------------------------
3376
- /** The spec, for saving (a copy). */
3377
- toJSON() {
3378
- return cloneChartSpec(this.spec);
3379
- }
3380
- /**
3381
- * Replaces the chart without an undo step (e.g. when opening a document). Problems are
3382
- * fixed with defaults and reported in `state.issues` (and returned).
3383
- */
3384
- load(input, options = {}) {
3385
- const { spec, issues } = normalizeChartSpec(input);
3386
- this.spec = spec;
3387
- this.issues = issues;
3388
- this.selection = this.validSelection(this.selection, spec);
3389
- if (options.clearHistory) {
3390
- this.applying = true;
3391
- try {
3392
- this.history.clear();
3393
- } finally {
3394
- this.applying = false;
3395
- }
3396
- }
3397
- this.notify(true);
3398
- return issues;
3399
- }
3400
- undo() {
3401
- this.endGesture();
3402
- return this.history.undo();
3403
- }
3404
- redo() {
3405
- this.endGesture();
3406
- return this.history.redo();
3407
- }
3408
- /** Stops listening to the history (reconnected by the next `subscribe`). */
3409
- destroy() {
3410
- this.unsubscribeHistory?.();
3411
- this.unsubscribeHistory = void 0;
3412
- this.listeners.clear();
3413
- }
3414
- };
3415
- function createChartController(options = {}) {
3416
- return new ChartController(options);
3417
- }
3418
- function updateLabel(patch) {
3419
- const keys = Object.keys(patch);
3420
- if (keys.length !== 1) return "Change chart";
3421
- const labels = {
3422
- type: "Change chart type",
3423
- title: "Edit title",
3424
- subtitle: "Edit subtitle",
3425
- categories: "Edit categories",
3426
- series: "Edit series",
3427
- xAxis: "Change category axis",
3428
- yAxis: "Change value axis",
3429
- yAxis2: "Change secondary value axis",
3430
- labelPosition: "Change label position",
3431
- gapWidth: "Change bar spacing",
3432
- overlap: "Change bar overlap",
3433
- bins: "Change histogram bins",
3434
- showTotal: "Toggle waterfall total",
3435
- legend: "Change legend",
3436
- stacking: "Change stacking",
3437
- orientation: "Change orientation",
3438
- curve: "Change line style",
3439
- points: "Toggle points",
3440
- dataLabels: "Toggle data labels",
3441
- palette: "Change colours",
3442
- innerRadius: "Change donut hole"
3443
- };
3444
- return labels[keys[0]] ?? "Change chart";
3445
- }
3446
-
3447
- // src/chart/sparkline.ts
3448
- var MAX_POINTS = 1e5;
3449
- function layoutSparkline(values, options) {
3450
- const kind = options.kind ?? "line";
3451
- const w = Number.isFinite(options.width) ? Math.max(1, options.width) : 1;
3452
- const h = Number.isFinite(options.height) ? Math.max(1, options.height) : 1;
3453
- const color = options.color ?? "#4269d0";
3454
- const negative = options.negativeColor ?? "#e5484d";
3455
- const theme = { ...DEFAULT_CHART_THEME, background: "transparent" };
3456
- const marks = [];
3457
- const scene = { width: w, height: h, plot: { x: 0, y: 0, width: w, height: h }, marks, percent: false, theme, warnings: [] };
3458
- const vals = values.slice(0, MAX_POINTS).map((v) => v !== null && Number.isFinite(v) ? Math.max(-1e300, Math.min(1e300, v)) : null);
3459
- const n = vals.length;
3460
- if (!n) return scene;
3461
- const pad = Math.max(0.5, Math.min(2, h * 0.1));
3462
- const x0 = pad;
3463
- const y0 = pad;
3464
- const iw = Math.max(0.5, w - 2 * pad);
3465
- const ih = Math.max(0.5, h - 2 * pad);
3466
- let lo = Infinity;
3467
- let hi = -Infinity;
3468
- for (const v of vals) if (v !== null) lo = Math.min(lo, v), hi = Math.max(hi, v);
3469
- if (!Number.isFinite(lo)) return scene;
3470
- if (options.min !== void 0 && Number.isFinite(options.min)) lo = options.min;
3471
- if (options.max !== void 0 && Number.isFinite(options.max)) hi = options.max;
3472
- let hiAt = -1;
3473
- let loAt = -1;
3474
- vals.forEach((v, i) => {
3475
- if (v === null) return;
3476
- if (hiAt < 0 || v > vals[hiAt]) hiAt = i;
3477
- if (loAt < 0 || v < vals[loAt]) loAt = i;
3478
- });
3479
- if (kind === "line") {
3480
- const span2 = hi - lo;
3481
- const fy2 = (v) => y0 + ih - (span2 > 0 ? (v - lo) / span2 : 0.5) * ih;
3482
- const fx = (j) => n > 1 ? x0 + j / (n - 1) * iw : x0 + iw / 2;
3483
- let d = "";
3484
- let run = [];
3485
- const flush = () => {
3486
- if (run.length >= 2) {
3487
- const pts = run.length > 4e3 ? run.filter((_, i) => i % (Math.floor(run.length / 2e3) + 1) === 0) : run;
3488
- d += pts.map(([x, y], i) => `${i ? "L" : "M"}${r2(x)},${r2(y)}`).join("");
3489
- }
3490
- run = [];
3491
- };
3492
- vals.forEach((v, j) => v === null ? flush() : run.push([fx(j), fy2(v)]));
3493
- flush();
3494
- if (d) marks.push({ kind: "path", key: "sparkline", role: "line", d, fill: "none", stroke: color, strokeWidth: 1.25 });
3495
- const r = Math.max(1, Math.min(3, h * 0.08));
3496
- if (options.markers || n === 1) {
3497
- const step = Math.max(1, Math.floor(n / 500));
3498
- vals.forEach((v, j) => {
3499
- if (v !== null && j % step === 0) marks.push({ kind: "circle", key: `point-${j}`, role: "point", cx: fx(j), cy: fy2(v), r, fill: color });
3500
- });
3501
- }
3502
- if (options.highLow && n > 1) {
3503
- if (hiAt >= 0) marks.push({ kind: "circle", key: "high", role: "point", cx: fx(hiAt), cy: fy2(vals[hiAt]), r: r + 0.5, fill: color });
3504
- if (loAt >= 0 && loAt !== hiAt) marks.push({ kind: "circle", key: "low", role: "point", cx: fx(loAt), cy: fy2(vals[loAt]), r: r + 0.5, fill: negative });
3505
- }
3506
- return scene;
3507
- }
3508
- const slot = iw / n;
3509
- const bw = Math.max(0.25, slot * 0.8);
3510
- const win = kind === "win-loss";
3511
- const lo0 = win ? -1 : Math.min(lo, 0);
3512
- const hi0 = win ? 1 : Math.max(hi, 0);
3513
- const span = hi0 - lo0;
3514
- const fy = (v) => span > 0 ? y0 + ih - (v - lo0) / span * ih : y0 + ih;
3515
- const base = fy(0);
3516
- vals.forEach((raw, j) => {
3517
- if (raw === null) return;
3518
- const v = win ? Math.sign(raw) : raw;
3519
- if (win && v === 0) return;
3520
- const top = fy(v);
3521
- const highlight = options.highLow && !win && (j === hiAt || j === loAt);
3522
- marks.push({ kind: "rect", key: `bar-${j}`, role: "bar", x: x0 + j * slot + (slot - bw) / 2, y: Math.min(top, base), width: bw, height: Math.max(0.5, Math.abs(top - base)), fill: v < 0 ? negative : color, opacity: options.highLow && !win && !highlight ? 0.6 : void 0 });
3523
- });
3524
- return scene;
3525
- }
3526
- function sparklineToSvg(values, options) {
3527
- return chartSceneToSvg(layoutSparkline(values, options), { ariaLabel: options.ariaLabel });
3528
- }
3529
-
3530
2128
  export {
3531
- CHART_MERMAID_META_PREFIX,
3532
- chartMermaidFamily,
3533
- chartToMermaid,
3534
- chartFromMermaid,
3535
- findMermaidCharts,
3536
2129
  parseChartDate,
3537
2130
  CHART_DATE_STEPS,
3538
2131
  formatChartDate,
@@ -3552,6 +2145,7 @@ export {
3552
2145
  estimateChartTextWidth,
3553
2146
  sameChartSelection,
3554
2147
  contrastingTextColor,
2148
+ r2,
3555
2149
  chartHistogram,
3556
2150
  chartQuantile,
3557
2151
  chartBoxStats,
@@ -3569,21 +2163,6 @@ export {
3569
2163
  layoutChart,
3570
2164
  chartValueAtPoint,
3571
2165
  chartXAtPoint,
3572
- snapChartValue,
3573
- summarizeChart,
3574
- describeChart,
3575
- chartTextBaseline,
3576
- chartTextTransform,
3577
- chartSceneToSvg,
3578
- chartToSvg,
3579
- chartSceneToDomSpec,
3580
- CHART_LAYOUT_PRESETS,
3581
- monochromeChartPalette,
3582
- CHART_COLOR_SCHEMES,
3583
- chartPresetPatch,
3584
- ChartController,
3585
- createChartController,
3586
- layoutSparkline,
3587
- sparklineToSvg
2166
+ snapChartValue
3588
2167
  };
3589
- //# sourceMappingURL=chunk-VODBAO32.js.map
2168
+ //# sourceMappingURL=chunk-OEFCLWZ3.js.map