@pixerate/editor 0.162.1 → 0.164.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/README.md +1 -1
  2. package/dist/agent/index.cjs +34 -34
  3. package/dist/agent/index.js +5 -5
  4. package/dist/canvas/displacement.d.cts +34 -2
  5. package/dist/canvas/displacement.d.ts +34 -2
  6. package/dist/canvas/explosion-tools.d.cts +43 -0
  7. package/dist/canvas/explosion-tools.d.ts +43 -0
  8. package/dist/canvas/explosionLayout.d.cts +50 -0
  9. package/dist/canvas/explosionLayout.d.ts +50 -0
  10. package/dist/canvas/explosionState.d.cts +52 -0
  11. package/dist/canvas/explosionState.d.ts +52 -0
  12. package/dist/canvas/index.cjs +430 -88
  13. package/dist/canvas/index.d.cts +3 -0
  14. package/dist/canvas/index.d.ts +3 -0
  15. package/dist/canvas/index.js +425 -83
  16. package/dist/canvas/types.d.cts +27 -2
  17. package/dist/canvas/types.d.ts +27 -2
  18. package/dist/chart/commands.d.cts +2 -1
  19. package/dist/chart/commands.d.ts +2 -1
  20. package/dist/chart/controller.d.cts +109 -1
  21. package/dist/chart/controller.d.ts +109 -1
  22. package/dist/chart/index.cjs +605 -130
  23. package/dist/chart/index.d.cts +1 -0
  24. package/dist/chart/index.d.ts +1 -0
  25. package/dist/chart/index.js +490 -15
  26. package/dist/chart/layout-annotations.d.cts +29 -0
  27. package/dist/chart/layout-annotations.d.ts +29 -0
  28. package/dist/chart/layout-families.d.cts +23 -0
  29. package/dist/chart/layout-families.d.ts +23 -0
  30. package/dist/chart/mermaid.d.cts +2 -1
  31. package/dist/chart/mermaid.d.ts +2 -1
  32. package/dist/chart/schemas.d.cts +13 -0
  33. package/dist/chart/schemas.d.ts +13 -0
  34. package/dist/chart/spec.d.cts +30 -5
  35. package/dist/chart/spec.d.ts +30 -5
  36. package/dist/chart/stats.d.cts +9 -1
  37. package/dist/chart/stats.d.ts +9 -1
  38. package/dist/chart/tools.d.cts +1 -1
  39. package/dist/chart/tools.d.ts +1 -1
  40. package/dist/chart/tree.d.cts +62 -0
  41. package/dist/chart/tree.d.ts +62 -0
  42. package/dist/chart/types.d.cts +77 -3
  43. package/dist/chart/types.d.ts +77 -3
  44. package/dist/{chunk-7YBDRJXT.cjs → chunk-22KLZZGH.cjs} +8 -8
  45. package/dist/{chunk-HLOKB7WW.js → chunk-3NEZJCBY.js} +366 -6
  46. package/dist/{chunk-EVPZ5IKN.js → chunk-5FNGLV2D.js} +65 -3
  47. package/dist/{chunk-EQ46F75O.cjs → chunk-D54JHQK2.cjs} +51 -51
  48. package/dist/{chunk-NO5QQDHZ.js → chunk-DAPQGA55.js} +2 -2
  49. package/dist/{chunk-3IHNXRUV.cjs → chunk-GKNEJ6EN.cjs} +399 -67
  50. package/dist/{chunk-4NGVHHJB.cjs → chunk-NZD2MZJC.cjs} +80 -18
  51. package/dist/{chunk-CX6JLDFC.js → chunk-OXERWBKE.js} +3 -3
  52. package/dist/{chunk-7CKPUUN4.js → chunk-POR54B55.js} +61 -10
  53. package/dist/{chunk-LL3H5XUH.cjs → chunk-R3P2M7HU.cjs} +367 -7
  54. package/dist/{chunk-HDGBM7EA.cjs → chunk-SBQVEBSU.cjs} +580 -53
  55. package/dist/{chunk-XDCFJEU7.js → chunk-SZA42R4N.js} +346 -14
  56. package/dist/{chunk-GLMP22FT.js → chunk-TALQONZ5.js} +3 -3
  57. package/dist/{chunk-NA4UVVJ4.cjs → chunk-YGG7WDWT.cjs} +76 -25
  58. package/dist/{chunk-EY22AY6R.cjs → chunk-YSAGBIRW.cjs} +16 -16
  59. package/dist/{chunk-DTSTNRPH.js → chunk-Z4P2RBMJ.js} +536 -9
  60. package/dist/export/index.cjs +4 -4
  61. package/dist/export/index.js +3 -3
  62. package/dist/extensions/index.cjs +7 -7
  63. package/dist/extensions/index.js +6 -6
  64. package/dist/index.cjs +13 -13
  65. package/dist/index.js +7 -7
  66. package/dist/paint-editor/index.cjs +0 -20
  67. package/dist/paint-editor/index.js +0 -20
  68. package/package.json +1 -1
  69. package/skills/README.md +12 -0
  70. package/skills/pixerate-agent-tools/SKILL.md +34 -1
  71. package/skills/pixerate-canvas/SKILL.md +16 -0
  72. package/skills/pixerate-chart/SKILL.md +41 -9
  73. package/skills/pixerate-image-editor/SKILL.md +2 -0
  74. package/skills/pixerate-paint-editor/SKILL.md +2 -0
  75. package/skills/pixerate-tokens/SKILL.md +5 -0
@@ -7,27 +7,36 @@ import {
7
7
  r2,
8
8
  sameChartSelection,
9
9
  summarizeChartSeries
10
- } from "./chunk-DTSTNRPH.js";
10
+ } from "./chunk-Z4P2RBMJ.js";
11
11
  import {
12
12
  CHART_TYPE_LABELS,
13
13
  DEFAULT_CHART_PALETTE,
14
14
  DEFAULT_CHART_THEME,
15
- chartColorsByCategory,
16
15
  chartDataFromTable,
17
16
  chartFamily,
18
17
  chartSeriesColor,
19
18
  chartSliceColor,
19
+ chartSpecColorsByCategory,
20
+ chartTreeDepth,
21
+ chartTreeFromRows,
22
+ chartTreeFromTable,
23
+ chartTreeToRows,
20
24
  checkChartColors,
21
25
  cloneChartSeries,
22
26
  cloneChartSpec,
27
+ cloneChartTree,
23
28
  defaultChartAxis,
24
29
  describeChartErrorBars,
30
+ findChartTreeNode,
31
+ normalizeChartReferenceBand,
32
+ normalizeChartReferenceLine,
25
33
  normalizeChartSpec,
34
+ normalizeChartTree,
26
35
  normalizeChartTrendline,
27
36
  parseChartValue,
28
37
  shadeChartColor,
29
38
  uniqueChartSeriesId
30
- } from "./chunk-HLOKB7WW.js";
39
+ } from "./chunk-3NEZJCBY.js";
31
40
  import {
32
41
  parseCsv,
33
42
  parseTsv
@@ -64,7 +73,7 @@ function splitOutside(text, sep) {
64
73
  }
65
74
  function chartMermaidFamily(spec) {
66
75
  const family = chartFamily(spec);
67
- if (family === "pie" || family === "funnel" || family === "treemap") return "pie";
76
+ if (family === "pie" || family === "funnel" || family === "treemap" || family === "parliament" || family === "sunburst") return "pie";
68
77
  if (family === "radar") return "radar";
69
78
  return "xychart";
70
79
  }
@@ -430,7 +439,7 @@ function findMermaidCharts(text) {
430
439
  function summarizeChart(spec, size = { width: 640, height: 400 }, scene = layoutChart(spec, { ...size, selection: null })) {
431
440
  const visible = spec.series.filter((s) => !s.hidden);
432
441
  const series = visible.map((s) => summarizeChartSeries(s, spec.categories));
433
- 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)));
442
+ const colors = chartSpecColorsByCategory(spec) ? spec.categories.map((_, i) => visible[0]?.pointColors?.[i] || chartSliceColor(spec, i)) : visible.map((s) => chartSeriesColor(spec, spec.series.indexOf(s)));
434
443
  const check = checkChartColors(colors, { surface: scene.theme.surface.startsWith("var(") ? "#ffffff" : scene.theme.surface });
435
444
  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;
436
445
  return {
@@ -454,14 +463,50 @@ function describeChart(spec, series = spec.series.filter((s) => !s.hidden).map((
454
463
  const fmt = (v) => v === null ? "blank" : formatChartNumber(v, format);
455
464
  const head = spec.title ? `${spec.title}: a ${kind} chart` : `A ${kind} chart`;
456
465
  if (!series.length || series.every((s) => s.count === 0)) return `${head} with no data.`;
457
- if (family === "pie" || family === "funnel" || family === "treemap") {
466
+ if (family === "pie" || family === "funnel" || family === "treemap" || family === "sunburst") {
458
467
  const s = series[0];
459
468
  const total = s.sum ?? 0;
460
469
  const share = (v) => total && v !== null ? ` (${Math.round(v / total * 100)}%)` : "";
461
470
  parts.push(`${head} of ${s.name} across ${spec.categories.length} categories, totalling ${fmt(s.sum)}.`);
462
471
  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)}.`);
472
+ if (spec.tree.length) {
473
+ const depth = chartTreeDepth(spec.tree);
474
+ const items = chartTreeToRows(spec.tree).length;
475
+ parts.push(`The data is grouped ${depth} level${depth === 1 ? "" : "s"} deep with ${items} item${items === 1 ? "" : "s"}.`);
476
+ }
463
477
  return parts.join(" ");
464
478
  }
479
+ if (family === "parliament") {
480
+ const s = spec.series.find((x) => !x.hidden);
481
+ const seats = spec.categories.map((c, i) => ({ c, n: Math.max(0, Math.round(s.values[i] ?? 0)) }));
482
+ const total = seats.reduce((t, x) => t + x.n, 0);
483
+ const majority = spec.parliamentMajority ?? Math.floor(total / 2) + 1;
484
+ const largest = [...seats].sort((a, b) => b.n - a.n)[0];
485
+ const verdict = largest && largest.n >= majority ? `${largest.c} holds a majority with ${largest.n}` : `no group reaches the majority of ${majority}; the largest is ${largest?.c} with ${largest?.n}`;
486
+ return `${head} of ${total} seats across ${seats.length} groups: ${verdict}.`;
487
+ }
488
+ if (family === "slope") {
489
+ const [a, b] = spec.series.filter((x) => !x.hidden);
490
+ if (!b) return `${head} that needs a second series to compare against.`;
491
+ const moves = spec.categories.map((c, i) => ({ c, d: a.values[i] !== null && b.values[i] !== null ? b.values[i] - a.values[i] : null })).filter((m) => m.d !== null);
492
+ const up = moves.filter((m) => m.d > 0).length;
493
+ const down = moves.filter((m) => m.d < 0).length;
494
+ const top = [...moves].sort((x, y) => y.d - x.d);
495
+ const extremes = top.length ? ` The biggest rise is ${top[0].c} (${fmt(top[0].d)}); the biggest fall is ${top[top.length - 1].c} (${fmt(top[top.length - 1].d)}).` : "";
496
+ return `${head} from ${a.name} to ${b.name}: ${up} of ${moves.length} rose and ${down} fell.${extremes}`;
497
+ }
498
+ if (family === "likert") {
499
+ const answers = spec.series.filter((x) => !x.hidden);
500
+ const side = Math.floor(answers.length / 2);
501
+ const agree = spec.categories.map((c, i) => {
502
+ const vals = answers.map((x) => Math.max(0, x.values[i] ?? 0));
503
+ const total = vals.reduce((t, v) => t + v, 0);
504
+ return { c, share: total ? vals.slice(answers.length - side).reduce((t, v) => t + v, 0) / total : 0 };
505
+ });
506
+ const best = [...agree].sort((x, y) => y.share - x.share);
507
+ const pct = (v) => `${Math.round(v * 100)}%`;
508
+ return `${head} of ${spec.categories.length} questions with ${answers.length} answers from ${answers[0]?.name} to ${answers[answers.length - 1]?.name}. Most positive: ${best[0]?.c} (${pct(best[0]?.share ?? 0)}); least: ${best[best.length - 1]?.c} (${pct(best[best.length - 1]?.share ?? 0)}).`;
509
+ }
465
510
  if (family === "waterfall") {
466
511
  const s = spec.series.find((x) => !x.hidden);
467
512
  const steps = s.values.filter((v, i) => v !== null && Number.isFinite(v) && !/\b(sub)?total\b/i.test(spec.categories[i] ?? ""));
@@ -632,6 +677,10 @@ function chartPresetPatch(options) {
632
677
  }
633
678
 
634
679
  // src/chart/controller.ts
680
+ function pointLists(s) {
681
+ const lists = [s.values, s.x, s.sizes, s.pointColors, s.callouts, s.errorBars?.plus, s.errorBars?.minus, s.xErrorBars?.plus, s.xErrorBars?.minus];
682
+ return lists.filter((l) => Array.isArray(l));
683
+ }
635
684
  function sameChartJson(a, b) {
636
685
  if (a === b) return true;
637
686
  if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return Number.isNaN(a) && Number.isNaN(b);
@@ -861,13 +910,16 @@ var ChartController = class {
861
910
  * to the categories; all-or-nothing like `update`.
862
911
  */
863
912
  setData(data, options = {}) {
864
- return this.update({ ...data.categories ? { categories: data.categories } : {}, ...data.series ? { series: data.series } : {} }, { label: options.label ?? "Edit chart data" });
913
+ const clear = (data.categories || data.series) && this.spec.tree.length ? { tree: [] } : {};
914
+ return this.update({ ...data.categories ? { categories: data.categories } : {}, ...data.series ? { series: data.series } : {}, ...clear }, { label: options.label ?? "Edit chart data" });
865
915
  }
866
916
  /**
867
917
  * Sets one value (numbers, numeric text such as `1,200` or `12%`, or blank for a gap).
868
918
  * One undo step, or part of the current gesture.
869
919
  */
870
920
  setValue(seriesId, index, value, options = {}) {
921
+ const grouped = this.treeLocked();
922
+ if (grouped) return grouped;
871
923
  const series = this.getSeries(seriesId);
872
924
  if (!series) return this.unknownSeries(seriesId);
873
925
  if (!Number.isInteger(index) || index < 0 || index >= this.spec.categories.length) return this.badIndex(index);
@@ -888,6 +940,8 @@ var ChartController = class {
888
940
  }
889
941
  /** Several values as one undo step (all-or-nothing). */
890
942
  setValues(cells, options = {}) {
943
+ const grouped = this.treeLocked();
944
+ if (grouped) return grouped;
891
945
  const issues = [];
892
946
  const parsed = [];
893
947
  for (const cell of cells) {
@@ -925,6 +979,7 @@ var ChartController = class {
925
979
  `Delete series "${series.name}"`,
926
980
  this.edited((d) => {
927
981
  d.series = d.series.filter((s) => s.id !== id);
982
+ d.highlight = d.highlight.filter((h) => h !== id && (h !== series.name || d.series.some((s) => s.name === h) || d.categories.includes(h)));
928
983
  })
929
984
  );
930
985
  return { ok: true };
@@ -942,7 +997,7 @@ var ChartController = class {
942
997
  if (patch.hidden) next.hidden = true;
943
998
  else delete next.hidden;
944
999
  }
945
- const resettable = ["axis", "x", "sizes", "pointColors", "marker", "curve", "dataLabels", "format", "errorBars", "xErrorBars"];
1000
+ const resettable = ["axis", "x", "sizes", "pointColors", "callouts", "marker", "curve", "dataLabels", "format", "errorBars", "xErrorBars"];
946
1001
  for (const key of resettable) {
947
1002
  if (!(key in patch) || patch[key] === void 0) continue;
948
1003
  if (patch[key] === null) delete next[key];
@@ -950,7 +1005,9 @@ var ChartController = class {
950
1005
  }
951
1006
  if (next.axis === "primary") delete next.axis;
952
1007
  const series2 = this.spec.series.map((s) => s.id === id ? next : s);
953
- const { spec, issues } = normalizeChartSpec({ ...this.spec, series: series2 });
1008
+ const renamed = next.name !== series.name && this.spec.highlight.includes(series.name) && !this.spec.categories.includes(series.name);
1009
+ const highlight = renamed ? this.spec.highlight.map((h) => h === series.name ? next.name : h) : this.spec.highlight;
1010
+ const { spec, issues } = normalizeChartSpec({ ...this.spec, series: series2, highlight });
954
1011
  if (issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
955
1012
  if (JSON.stringify(spec.series) === JSON.stringify(this.spec.series)) return { ok: true };
956
1013
  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}"`;
@@ -979,41 +1036,52 @@ var ChartController = class {
979
1036
  }
980
1037
  /** Inserts a category (with gaps in every series) at `at` (default: the end). Returns its index. */
981
1038
  addCategory(label, options = {}) {
1039
+ const grouped = this.treeLocked();
1040
+ if (grouped) return grouped;
982
1041
  const at = Math.max(0, Math.min(this.spec.categories.length, options.at ?? this.spec.categories.length));
983
1042
  const name = label ?? `${this.spec.categories.length + 1}`;
984
1043
  this.commit(
985
1044
  `Add category "${name}"`,
986
1045
  this.edited((d) => {
987
1046
  d.categories.splice(at, 0, name);
988
- for (const s of d.series) s.values.splice(at, 0, null);
1047
+ for (const s of d.series) for (const list of pointLists(s)) list.splice(at, 0, null);
989
1048
  }),
990
1049
  { kind: "category", index: at }
991
1050
  );
992
1051
  return { ok: true, index: at };
993
1052
  }
994
1053
  removeCategory(index) {
1054
+ const grouped = this.treeLocked();
1055
+ if (grouped) return grouped;
995
1056
  if (!Number.isInteger(index) || index < 0 || index >= this.spec.categories.length) return this.badIndex(index);
996
1057
  this.commit(
997
1058
  `Delete category "${this.spec.categories[index]}"`,
998
1059
  this.edited((d) => {
999
- d.categories.splice(index, 1);
1000
- for (const s of d.series) s.values.splice(index, 1);
1060
+ const [label] = d.categories.splice(index, 1);
1061
+ for (const s of d.series) for (const list of pointLists(s)) list.splice(index, 1);
1062
+ if (!d.categories.includes(label)) d.highlight = d.highlight.filter((h) => h !== label);
1001
1063
  })
1002
1064
  );
1003
1065
  return { ok: true };
1004
1066
  }
1005
1067
  renameCategory(index, label) {
1068
+ const grouped = this.treeLocked();
1069
+ if (grouped) return grouped;
1006
1070
  if (!Number.isInteger(index) || index < 0 || index >= this.spec.categories.length) return this.badIndex(index);
1007
1071
  if (this.spec.categories[index] === label) return { ok: true };
1008
1072
  this.commit(
1009
1073
  "Rename category",
1010
1074
  this.edited((d) => {
1075
+ const before = d.categories[index];
1011
1076
  d.categories[index] = String(label);
1077
+ if (d.highlight.includes(before) && !d.categories.includes(before)) d.highlight = d.highlight.map((h) => h === before ? String(label) : h);
1012
1078
  })
1013
1079
  );
1014
1080
  return { ok: true };
1015
1081
  }
1016
1082
  moveCategory(from, to) {
1083
+ const grouped = this.treeLocked();
1084
+ if (grouped) return grouped;
1017
1085
  if (!Number.isInteger(from) || from < 0 || from >= this.spec.categories.length) return this.badIndex(from);
1018
1086
  const target = Math.max(0, Math.min(this.spec.categories.length - 1, to));
1019
1087
  if (target === from) return { ok: true };
@@ -1025,7 +1093,7 @@ var ChartController = class {
1025
1093
  "Reorder categories",
1026
1094
  this.edited((d) => {
1027
1095
  move(d.categories);
1028
- for (const s of d.series) move(s.values);
1096
+ for (const s of d.series) for (const list of pointLists(s)) move(list);
1029
1097
  })
1030
1098
  );
1031
1099
  return { ok: true };
@@ -1036,6 +1104,8 @@ var ChartController = class {
1036
1104
  * grid grows to fit. Non-numeric value cells become gaps and are reported in `issues`.
1037
1105
  */
1038
1106
  pasteCells(row, column, cells) {
1107
+ const grouped = this.treeLocked();
1108
+ if (grouped) return grouped;
1039
1109
  const rows = typeof cells === "string" ? cells.includes(" ") ? parseTsv(cells) : parseCsv(cells) : cells.map((r) => [...r]);
1040
1110
  if (!rows.length) return { ok: true };
1041
1111
  const issues = [];
@@ -1083,7 +1153,7 @@ var ChartController = class {
1083
1153
  const old = byName.get(s.name);
1084
1154
  return { ...s, ...old?.color ? { color: old.color } : {}, ...old?.type ? { type: old.type } : {}, ...old ? { id: old.id } : {} };
1085
1155
  });
1086
- const { spec } = normalizeChartSpec({ ...this.spec, categories: data.categories, series });
1156
+ const { spec } = normalizeChartSpec({ ...this.spec, categories: data.categories, series, tree: [] });
1087
1157
  this.commit("Import data", spec);
1088
1158
  if (data.issues.length) {
1089
1159
  this.issues = data.issues;
@@ -1098,6 +1168,252 @@ var ChartController = class {
1098
1168
  return this.importTable(parseCsv(text, { delimiter }), options);
1099
1169
  }
1100
1170
  // -------------------------------------------------------------------------
1171
+ // Grouped data (tree)
1172
+ // -------------------------------------------------------------------------
1173
+ /** A refusal while the data is a tree (cell edits would be overwritten by its totals), else null. */
1174
+ treeLocked() {
1175
+ if (!this.spec.tree.length) return null;
1176
+ return this.reject(
1177
+ "This chart's data is grouped (a tree): its categories and values are the top-level groups and their totals. Change it with setTree / the tree node edits (set_chart_tree, add_chart_tree_node, update_chart_tree_node, remove_chart_tree_node), or replace it with new data."
1178
+ );
1179
+ }
1180
+ /** Replaces the grouped data (an empty list goes back to the table, keeping the totals as values). One undo step. */
1181
+ setTree(tree, options = {}) {
1182
+ const issues = [];
1183
+ const next = normalizeChartTree(tree, issues);
1184
+ if (issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
1185
+ return this.update({ tree: next }, { label: options.label ?? (next.length ? "Replace grouped data" : "Ungroup data") });
1186
+ }
1187
+ /**
1188
+ * Replaces the grouped data from rows of names and a value (group, …, item, value), as the
1189
+ * data grid edits it; colours of nodes whose path is unchanged are kept. All-or-nothing.
1190
+ */
1191
+ setTreeRows(rows, options = {}) {
1192
+ const issues = [];
1193
+ const next = chartTreeFromRows(rows, issues);
1194
+ if (issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
1195
+ const keepColors = (list, prefix) => {
1196
+ for (const n of list) {
1197
+ const old = findChartTreeNode(this.spec.tree, [...prefix, n.name]);
1198
+ if (old?.color) n.color = old.color;
1199
+ if (n.children) keepColors(n.children, [...prefix, n.name]);
1200
+ }
1201
+ };
1202
+ keepColors(next, []);
1203
+ return this.update({ tree: next }, { label: options.label ?? "Edit grouped data" });
1204
+ }
1205
+ /** Replaces the data with a table of names and values (group, …, item, value); see `chartTreeFromTable`. */
1206
+ importTreeTable(rows, options = {}) {
1207
+ const issues = [];
1208
+ const next = chartTreeFromTable(rows, options, issues);
1209
+ if (issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
1210
+ if (!next.length) return this.reject('There is no grouped data to import: expected rows of names followed by a value, e.g. "Europe,France,40".');
1211
+ return this.update({ tree: next }, { label: "Import grouped data" });
1212
+ }
1213
+ nodeAt(path) {
1214
+ const node = findChartTreeNode(this.spec.tree, path);
1215
+ if (node) return node;
1216
+ const known = path.length > 1 ? findChartTreeNode(this.spec.tree, path.slice(0, -1)) : null;
1217
+ const siblings = (path.length > 1 ? known?.children : this.spec.tree) ?? [];
1218
+ return fail(
1219
+ `There is no node ${JSON.stringify(path)}. ${siblings.length ? `${path.length > 1 && known ? `"${known.name}" contains` : "The top level has"} ${siblings.map((n) => `"${n.name}"`).slice(0, 20).join(", ")}.` : "The chart has no grouped data yet; add nodes with add_chart_tree_node or set_chart_tree."}`
1220
+ );
1221
+ }
1222
+ /** Adds a node under the group at `parent` ([] for the top level) at `at` (default: the end). Returns its path. */
1223
+ addTreeNode(parent, input, options = {}) {
1224
+ let siblings = this.spec.tree;
1225
+ if (parent.length) {
1226
+ const group = this.nodeAt(parent);
1227
+ if ("ok" in group) return group;
1228
+ if (!group.children) return this.reject(`"${group.name}" is an item with its own value, not a group; add the node next to it, or turn it into a group by replacing the tree with set_chart_tree.`);
1229
+ siblings = group.children;
1230
+ }
1231
+ const issues = [];
1232
+ const [node] = normalizeChartTree([input], issues, "node");
1233
+ if (!node || issues.length) return this.reject(issues.map((i) => i.message).join(" ") || "The node is not valid.", issues);
1234
+ if (siblings.some((n) => n.name === node.name)) return this.reject(`"${node.name}" is already there; names under one group must be unique.`);
1235
+ const at = Math.max(0, Math.min(siblings.length, options.at ?? siblings.length));
1236
+ const tree = cloneChartTree(this.spec.tree);
1237
+ const list = parent.length ? findChartTreeNode(tree, parent).children : tree;
1238
+ list.splice(at, 0, node);
1239
+ const result = this.update({ tree }, { label: `Add "${node.name}"` });
1240
+ if (!result.ok) return result;
1241
+ return { ok: true, path: [...parent, node.name] };
1242
+ }
1243
+ /** Renames, revalues (items only) or recolours a node; `color: null` goes back to the palette. Returns its path. */
1244
+ updateTreeNode(path, patch) {
1245
+ const node = this.nodeAt(path);
1246
+ if ("ok" in node) return node;
1247
+ if (patch.value !== void 0 && node.children) return this.reject(`"${node.name}" is a group: its value is the sum of its items. Change the items instead.`);
1248
+ const name = patch.name !== void 0 ? String(patch.name).trim() : node.name;
1249
+ const siblings = (path.length > 1 ? findChartTreeNode(this.spec.tree, path.slice(0, -1)).children : this.spec.tree).filter((n) => n !== node);
1250
+ if (siblings.some((n) => n.name === name)) return this.reject(`There is already a "${name}" next to "${node.name}"; names under one group must be unique.`);
1251
+ const tree = cloneChartTree(this.spec.tree);
1252
+ const target = findChartTreeNode(tree, path);
1253
+ const merged = { ...target, name };
1254
+ if (patch.value !== void 0) merged.value = patch.value;
1255
+ if (patch.color === null) delete merged.color;
1256
+ else if (patch.color !== void 0) merged.color = patch.color;
1257
+ const issues = [];
1258
+ const [checked] = normalizeChartTree([merged], issues, "node");
1259
+ if (!checked || issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
1260
+ Object.assign(target, checked);
1261
+ if (patch.color === null) delete target.color;
1262
+ const result = this.update({ tree }, { label: patch.name !== void 0 && patch.name !== node.name ? "Rename group" : `Change "${node.name}"` });
1263
+ if (!result.ok) return result;
1264
+ return { ok: true, path: [...path.slice(0, -1), name] };
1265
+ }
1266
+ /** Removes a node (with everything under it). A group left empty becomes an item with value 0. */
1267
+ removeTreeNode(path) {
1268
+ const node = this.nodeAt(path);
1269
+ if ("ok" in node) return node;
1270
+ const tree = cloneChartTree(this.spec.tree);
1271
+ const parent = path.length > 1 ? findChartTreeNode(tree, path.slice(0, -1)) : null;
1272
+ const list = parent ? parent.children : tree;
1273
+ list.splice(
1274
+ list.findIndex((n) => n.name === node.name),
1275
+ 1
1276
+ );
1277
+ if (parent && !parent.children.length) {
1278
+ delete parent.children;
1279
+ parent.value = 0;
1280
+ }
1281
+ return this.update({ tree }, { label: `Delete "${node.name}"` });
1282
+ }
1283
+ // -------------------------------------------------------------------------
1284
+ // Annotations
1285
+ // -------------------------------------------------------------------------
1286
+ /** Sets the note written under the chart ("" removes it). */
1287
+ setNote(note) {
1288
+ return this.update({ note }, { label: note ? "Edit note" : "Remove note" });
1289
+ }
1290
+ /**
1291
+ * Draws the named categories (labels) and series (names or ids) normally and mutes the
1292
+ * rest; an empty list shows everything. Unknown names change nothing. Returns the category
1293
+ * indexes and series ids that matched.
1294
+ */
1295
+ setHighlight(names) {
1296
+ const s = this.spec;
1297
+ const list = [...new Set(names.map(String))];
1298
+ const unknown = list.filter((n) => !s.categories.includes(n) && !s.series.some((x) => x.id === n || x.name === n));
1299
+ if (unknown.length)
1300
+ return this.reject(
1301
+ `${unknown.map((u) => `"${u}"`).join(", ")} ${unknown.length === 1 ? "is" : "are"} neither a category nor a series. Categories: ${s.categories.slice(0, 20).map((c) => `"${c}"`).join(", ") || "(none)"}; series: ${s.series.map((x) => `"${x.name}"`).join(", ") || "(none)"}.`
1302
+ );
1303
+ const result = this.update({ highlight: list }, { label: list.length ? "Highlight" : "Clear highlight" });
1304
+ if (!result.ok) return result;
1305
+ return {
1306
+ ok: true,
1307
+ categories: s.categories.flatMap((c, i) => list.includes(c) ? [i] : []),
1308
+ series: s.series.filter((x) => list.includes(x.id) || list.includes(x.name)).map((x) => x.id)
1309
+ };
1310
+ }
1311
+ /** Why a reference on `axis` at `values` cannot be drawn on this chart, or null. */
1312
+ referenceProblem(axis, values) {
1313
+ const s = this.spec;
1314
+ const family = chartFamily(s);
1315
+ if (family !== "cartesian" && family !== "xy") return `Reference lines and bands are drawn on bar, line, area, scatter and bubble charts; this is a ${s.type} chart.`;
1316
+ if (axis === "x" && family !== "xy") return 'axis "x" needs a scatter or bubble chart with numeric x values; use "y" for the value axis.';
1317
+ if (axis === "y2" && (family !== "cartesian" || !s.series.some((x) => !x.hidden && x.axis === "secondary")))
1318
+ return 'axis "y2" needs a visible series on the secondary axis (set one with update_chart_series axis "secondary"), or use "y".';
1319
+ const scale = (axis === "x" ? s.xAxis : axis === "y2" ? s.yAxis2 : s.yAxis).scale;
1320
+ if (scale === "log" && values.some((v) => !(v > 0))) return "That axis is logarithmic, so the values must be above 0.";
1321
+ return null;
1322
+ }
1323
+ editReferences(kind, label, edit, select) {
1324
+ this.commit(
1325
+ label,
1326
+ this.edited((d) => edit(d[kind])),
1327
+ select
1328
+ );
1329
+ }
1330
+ /** Adds a reference line (a target, floor or average). Returns its index. */
1331
+ addReferenceLine(input) {
1332
+ const issues = [];
1333
+ const line = normalizeChartReferenceLine(input, "referenceLine", issues);
1334
+ if (!line || issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
1335
+ const problem = this.referenceProblem(line.axis, [line.value]);
1336
+ if (problem) return this.reject(problem);
1337
+ const index = this.spec.referenceLines.length;
1338
+ this.editReferences("referenceLines", `Add reference line${line.label ? ` "${line.label}"` : ""}`, (list) => list.push(line));
1339
+ return { ok: true, index };
1340
+ }
1341
+ /** Changes a reference line (all-or-nothing); `null` removes its label, colour or axis. */
1342
+ updateReferenceLine(index, patch) {
1343
+ const current = this.spec.referenceLines[index];
1344
+ if (!current) return fail(this.noReference("reference line", this.spec.referenceLines.length, index));
1345
+ const merged = { ...current, ...patch };
1346
+ for (const [k, v] of Object.entries(patch)) if (v === null) delete merged[k];
1347
+ const issues = [];
1348
+ const line = normalizeChartReferenceLine(merged, "referenceLine", issues);
1349
+ if (!line || issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
1350
+ const problem = this.referenceProblem(line.axis, [line.value]);
1351
+ if (problem) return this.reject(problem);
1352
+ this.editReferences("referenceLines", "Change reference line", (list) => list[index] = line);
1353
+ return { ok: true };
1354
+ }
1355
+ removeReferenceLine(index) {
1356
+ if (!this.spec.referenceLines[index]) return fail(this.noReference("reference line", this.spec.referenceLines.length, index));
1357
+ this.editReferences("referenceLines", "Delete reference line", (list) => list.splice(index, 1));
1358
+ return { ok: true };
1359
+ }
1360
+ /** Adds a reference band (a target range). Returns its index. */
1361
+ addReferenceBand(input) {
1362
+ const issues = [];
1363
+ const band = normalizeChartReferenceBand(input, "referenceBand", issues);
1364
+ if (!band || issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
1365
+ const problem = this.referenceProblem(band.axis, [band.from, band.to]);
1366
+ if (problem) return this.reject(problem);
1367
+ const index = this.spec.referenceBands.length;
1368
+ this.editReferences("referenceBands", `Add reference band${band.label ? ` "${band.label}"` : ""}`, (list) => list.push(band));
1369
+ return { ok: true, index };
1370
+ }
1371
+ /** Changes a reference band (all-or-nothing); `null` removes its label, colour or axis. */
1372
+ updateReferenceBand(index, patch) {
1373
+ const current = this.spec.referenceBands[index];
1374
+ if (!current) return fail(this.noReference("reference band", this.spec.referenceBands.length, index));
1375
+ const merged = { ...current, ...patch };
1376
+ for (const [k, v] of Object.entries(patch)) if (v === null) delete merged[k];
1377
+ const issues = [];
1378
+ const band = normalizeChartReferenceBand(merged, "referenceBand", issues);
1379
+ if (!band || issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
1380
+ const problem = this.referenceProblem(band.axis, [band.from, band.to]);
1381
+ if (problem) return this.reject(problem);
1382
+ this.editReferences("referenceBands", "Change reference band", (list) => list[index] = band);
1383
+ return { ok: true };
1384
+ }
1385
+ removeReferenceBand(index) {
1386
+ if (!this.spec.referenceBands[index]) return fail(this.noReference("reference band", this.spec.referenceBands.length, index));
1387
+ this.editReferences("referenceBands", "Delete reference band", (list) => list.splice(index, 1));
1388
+ return { ok: true };
1389
+ }
1390
+ noReference(what, count, index) {
1391
+ return `There is no ${what} ${index}; ${count ? `expected 0 to ${count - 1}` : `the chart has none yet`}.`;
1392
+ }
1393
+ /**
1394
+ * Pins a label to one bar or point (`null` or "" removes it) and selects the point. The
1395
+ * point must have a value, on a bar, line, area, scatter or bubble chart.
1396
+ */
1397
+ setCallout(seriesId, index, label) {
1398
+ const series = this.getSeries(seriesId);
1399
+ if (!series) return this.unknownSeries(seriesId);
1400
+ if (!Number.isInteger(index) || index < 0 || index >= this.spec.categories.length) return this.badIndex(index);
1401
+ const family = chartFamily(this.spec);
1402
+ const text = label === null ? "" : String(label).trim();
1403
+ if (text && family !== "cartesian" && family !== "xy") return this.reject(`Callouts are drawn on bar, line, area, scatter and bubble charts; this is a ${this.spec.type} chart.`);
1404
+ if (text && (series.values[index] === null || series.values[index] === void 0))
1405
+ return this.reject(`"${series.name}" has no value at "${this.spec.categories[index]}", so there is nothing to point at; set a value first.`);
1406
+ const next = this.edited((d) => {
1407
+ const s = d.series.find((x) => x.id === seriesId);
1408
+ const labels = s.callouts ? [...s.callouts] : d.categories.map(() => null);
1409
+ labels[index] = text || null;
1410
+ if (labels.some((l) => l)) s.callouts = labels;
1411
+ else delete s.callouts;
1412
+ });
1413
+ this.commit(text ? "Add callout" : "Remove callout", next, { kind: "point", seriesId, index });
1414
+ return { ok: true };
1415
+ }
1416
+ // -------------------------------------------------------------------------
1101
1417
  // Trendlines
1102
1418
  // -------------------------------------------------------------------------
1103
1419
  /** Adds a trendline to a series and selects it. Returns its index (and the fit, when it can be computed). */
@@ -1182,6 +1498,8 @@ var ChartController = class {
1182
1498
  * undo step). Series colours, types and other per-series settings are dropped.
1183
1499
  */
1184
1500
  transpose() {
1501
+ const grouped = this.treeLocked();
1502
+ if (grouped) return grouped;
1185
1503
  const s = this.spec;
1186
1504
  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.");
1187
1505
  const used = /* @__PURE__ */ new Set();
@@ -1235,6 +1553,8 @@ var ChartController = class {
1235
1553
  }
1236
1554
  /** Adds `delta` to a value (a gap counts as 0). */
1237
1555
  nudgeValue(seriesId, index, delta) {
1556
+ const grouped = this.treeLocked();
1557
+ if (grouped) return grouped;
1238
1558
  const series = this.getSeries(seriesId);
1239
1559
  if (!series) return this.unknownSeries(seriesId);
1240
1560
  const current = series.values[index] ?? 0;
@@ -1343,6 +1663,18 @@ function updateLabel(patch) {
1343
1663
  overlap: "Change bar overlap",
1344
1664
  bins: "Change histogram bins",
1345
1665
  showTotal: "Toggle waterfall total",
1666
+ note: "Edit note",
1667
+ highlight: "Change highlight",
1668
+ referenceLines: "Change reference lines",
1669
+ referenceBands: "Change reference bands",
1670
+ varyColors: "Toggle colour by category",
1671
+ likertNeutral: "Change neutral answer",
1672
+ likertSort: "Sort questions",
1673
+ parliamentArc: "Change hemicycle sweep",
1674
+ parliamentMajority: "Change majority",
1675
+ parliamentLabel: "Edit seat caption",
1676
+ sunburstLabel: "Edit centre caption",
1677
+ tree: "Replace grouped data",
1346
1678
  radarTicks: "Change radar rings",
1347
1679
  radarGraticule: "Change radar grid",
1348
1680
  legend: "Change legend",
@@ -7,7 +7,7 @@ import {
7
7
  import {
8
8
  CHART_OOXML_NS,
9
9
  chartFromOoxml
10
- } from "./chunk-7CKPUUN4.js";
10
+ } from "./chunk-POR54B55.js";
11
11
  import {
12
12
  compareDocuments
13
13
  } from "./chunk-V5OOP74P.js";
@@ -34,10 +34,10 @@ import {
34
34
  searchDocument,
35
35
  sliceContainsTable,
36
36
  syncInitialContent
37
- } from "./chunk-CX6JLDFC.js";
37
+ } from "./chunk-OXERWBKE.js";
38
38
  import {
39
39
  chartDataToTable
40
- } from "./chunk-HLOKB7WW.js";
40
+ } from "./chunk-3NEZJCBY.js";
41
41
  import {
42
42
  PIXERATE_DOCUMENT_MIME,
43
43
  getClipboardData,