@pixerate/editor 0.164.0 → 0.166.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/index.cjs +54 -39
- package/dist/agent/index.js +24 -9
- package/dist/animation/index.cjs +6 -5
- package/dist/animation/index.js +5 -4
- package/dist/animation-editor/index.cjs +65 -64
- package/dist/animation-editor/index.js +8 -7
- package/dist/bridge/cli.cjs +3 -3
- package/dist/bridge/cli.js +2 -2
- package/dist/bridge/index.cjs +3 -3
- package/dist/bridge/index.js +2 -2
- package/dist/canvas/index.cjs +89 -87
- package/dist/canvas/index.js +11 -9
- package/dist/chart/controller.d.cts +60 -2
- package/dist/chart/controller.d.ts +60 -2
- package/dist/chart/index.cjs +657 -163
- package/dist/chart/index.d.cts +3 -0
- package/dist/chart/index.d.ts +3 -0
- package/dist/chart/index.js +512 -18
- package/dist/chart/layout-families.d.cts +14 -0
- package/dist/chart/layout-families.d.ts +14 -0
- package/dist/chart/layout-shared.d.cts +5 -0
- package/dist/chart/layout-shared.d.ts +5 -0
- package/dist/chart/links.d.cts +34 -0
- package/dist/chart/links.d.ts +34 -0
- package/dist/chart/mermaid.d.cts +9 -6
- package/dist/chart/mermaid.d.ts +9 -6
- package/dist/chart/race.d.cts +26 -0
- package/dist/chart/race.d.ts +26 -0
- package/dist/chart/schemas.d.cts +5 -0
- package/dist/chart/schemas.d.ts +5 -0
- package/dist/chart/spec.d.cts +3 -2
- package/dist/chart/spec.d.ts +3 -2
- package/dist/chart/style.d.cts +33 -0
- package/dist/chart/style.d.ts +33 -0
- package/dist/chart/types.d.cts +28 -2
- package/dist/chart/types.d.ts +28 -2
- package/dist/{chunk-ZVNAMTFO.cjs → chunk-3FSUJIXN.cjs} +18 -174
- package/dist/{chunk-RQCMSEM5.cjs → chunk-4QUUHTGJ.cjs} +2 -2
- package/dist/{chunk-CIPIESX4.cjs → chunk-4ZC5QJE2.cjs} +3 -3
- package/dist/{chunk-S6X2QRVU.cjs → chunk-5UEFBM2M.cjs} +1 -138
- package/dist/{chunk-5FNGLV2D.js → chunk-5UNQDC3Q.js} +18 -3
- package/dist/{chunk-YSAGBIRW.cjs → chunk-6FYIFGBW.cjs} +16 -16
- package/dist/{chunk-QYZGBVNC.js → chunk-7272D3K2.js} +10 -8
- package/dist/{chunk-462WGGXX.cjs → chunk-AMX23CSN.cjs} +27 -16
- package/dist/{chunk-3VLOOO3G.js → chunk-BDIM6NVW.js} +1 -1
- package/dist/{chunk-R3P2M7HU.cjs → chunk-BPX4Z3N3.cjs} +355 -207
- package/dist/{chunk-D54JHQK2.cjs → chunk-CHLV7UFP.cjs} +51 -51
- package/dist/{chunk-OXERWBKE.js → chunk-DFCAR7AW.js} +3 -3
- package/dist/{chunk-GZ2FM3ZB.js → chunk-DKNEQX7T.js} +6 -162
- package/dist/{chunk-JCXMJUQO.cjs → chunk-EGZM52EO.cjs} +3 -3
- package/dist/{chunk-ES3UEVUT.cjs → chunk-EUIDTZNQ.cjs} +112 -110
- package/dist/{chunk-H3OEM3Q2.js → chunk-F6R67I3A.js} +1 -1
- package/dist/{chunk-3NEZJCBY.js → chunk-FR432ROA.js} +349 -201
- package/dist/chunk-HIDFL5LQ.js +484 -0
- package/dist/{chunk-22KLZZGH.cjs → chunk-HXOBOHH2.cjs} +8 -8
- package/dist/{chunk-MKTDELCD.js → chunk-I5MRJW7B.js} +1 -1
- package/dist/{chunk-SDHZG5TF.js → chunk-ILP77UD6.js} +160 -2
- package/dist/{chunk-Z4P2RBMJ.js → chunk-ITANXMZD.js} +177 -3
- package/dist/chunk-KGRZMHZB.js +204 -0
- package/dist/{chunk-SET6KJOP.js → chunk-LPIKXPBM.js} +1 -1
- package/dist/{chunk-BQWGN5WH.js → chunk-M2TPMYJE.js} +1 -1
- package/dist/{chunk-GTELFJRE.cjs → chunk-M5GLQQWG.cjs} +2 -2
- package/dist/{chunk-4VU4CP7R.js → chunk-MHLU6GRJ.js} +1 -138
- package/dist/{chunk-NZD2MZJC.cjs → chunk-MZFZSVUJ.cjs} +38 -23
- package/dist/{chunk-DAPQGA55.js → chunk-NQQDGGXY.js} +2 -2
- package/dist/chunk-O6OGNDND.js +164 -0
- package/dist/{chunk-PA3IMPPS.cjs → chunk-OVYQAYGH.cjs} +81 -81
- package/dist/{chunk-YXOW456B.js → chunk-PPEDZ5WR.js} +1 -1
- package/dist/{chunk-24M7VD6M.cjs → chunk-S4EIAIEW.cjs} +7 -7
- package/dist/{chunk-M4MIZPQI.cjs → chunk-SJ2CPTQC.cjs} +189 -15
- package/dist/{chunk-YGG7WDWT.cjs → chunk-SZWVMB3B.cjs} +24 -18
- package/dist/chunk-TFF2GTGN.cjs +153 -0
- package/dist/chunk-TTCJPX4U.cjs +204 -0
- package/dist/{chunk-SBQVEBSU.cjs → chunk-UUUTEVX6.cjs} +242 -68
- package/dist/{chunk-WI7QJA7A.cjs → chunk-UXETV2NC.cjs} +161 -3
- package/dist/{chunk-GKNEJ6EN.cjs → chunk-V5L2G22R.cjs} +321 -107
- package/dist/{chunk-TALQONZ5.js → chunk-VB2CY6EG.js} +3 -3
- package/dist/{chunk-QY4KUDMY.js → chunk-VRXXAXUT.js} +2 -2
- package/dist/chunk-VU6WQSMB.cjs +164 -0
- package/dist/{chunk-NHLYK3AX.js → chunk-XBQX76CT.js} +182 -8
- package/dist/{chunk-5AARIS3R.cjs → chunk-XCDGM7TD.cjs} +37 -37
- package/dist/{chunk-POR54B55.js → chunk-XNU45TYA.js} +7 -1
- package/dist/{chunk-EVXQNQND.js → chunk-XZB3TJ6K.js} +16 -5
- package/dist/chunk-YBAPT7NL.js +153 -0
- package/dist/{chunk-SZA42R4N.js → chunk-YUFTVNLX.js} +230 -16
- package/dist/chunk-Z2UQYMPS.cjs +484 -0
- package/dist/color/index.cjs +5 -3
- package/dist/color/index.js +12 -10
- package/dist/diagram/index.cjs +55 -243
- package/dist/diagram/index.js +12 -200
- package/dist/export/index.cjs +7 -6
- package/dist/export/index.js +6 -5
- package/dist/extensions/index.cjs +12 -9
- package/dist/extensions/index.js +11 -8
- package/dist/gradient/index.cjs +29 -27
- package/dist/gradient/index.js +6 -4
- package/dist/image-editor/index.cjs +644 -643
- package/dist/image-editor/index.js +7 -6
- package/dist/index.cjs +21 -18
- package/dist/index.js +14 -11
- package/dist/paint-editor/index.cjs +19 -18
- package/dist/paint-editor/index.js +7 -6
- package/dist/parameters/index.cjs +2 -2
- package/dist/parameters/index.js +1 -1
- package/dist/persistence/assetWrites.d.cts +22 -0
- package/dist/persistence/assetWrites.d.ts +22 -0
- package/dist/persistence/assets.d.cts +71 -0
- package/dist/persistence/assets.d.ts +71 -0
- package/dist/persistence/documentStore.d.cts +17 -1
- package/dist/persistence/documentStore.d.ts +17 -1
- package/dist/persistence/documentTools.d.cts +5 -1
- package/dist/persistence/documentTools.d.ts +5 -1
- package/dist/persistence/editorAutosave.d.cts +7 -0
- package/dist/persistence/editorAutosave.d.ts +7 -0
- package/dist/persistence/index.cjs +141 -21
- package/dist/persistence/index.js +130 -10
- package/dist/persistence/storage.d.cts +13 -1
- package/dist/persistence/storage.d.ts +13 -1
- package/dist/spreadsheet/index.cjs +3 -3
- package/dist/spreadsheet/index.js +2 -2
- package/dist/tokens/index.cjs +11 -3
- package/dist/tokens/index.d.cts +1 -0
- package/dist/tokens/index.d.ts +1 -0
- package/dist/tokens/index.js +10 -2
- package/dist/tokens/style.d.cts +36 -0
- package/dist/tokens/style.d.ts +36 -0
- package/dist/tools/index.cjs +15 -3
- package/dist/tools/index.d.cts +2 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +14 -2
- package/dist/tools/style-tools.d.cts +19 -0
- package/dist/tools/style-tools.d.ts +19 -0
- package/dist/tools/style.d.cts +212 -0
- package/dist/tools/style.d.ts +212 -0
- package/dist/tools/view.d.cts +6 -0
- package/dist/tools/view.d.ts +6 -0
- package/dist/typography/index.cjs +8 -6
- package/dist/typography/index.js +5 -3
- package/dist/vector/index.cjs +953 -952
- package/dist/vector/index.js +7 -6
- package/dist/video-editor/index.cjs +8 -7
- package/dist/video-editor/index.js +6 -5
- package/package.json +1 -1
- package/skills/pixerate-chart/SKILL.md +20 -2
- package/dist/chunk-ERAGQL2N.cjs +0 -68
- package/dist/chunk-KMU6QJB7.js +0 -68
|
@@ -253,6 +253,12 @@ var ToolRegistryView = class {
|
|
|
253
253
|
return set.tools.some((t) => this.enabledTools.has(t)) ? "some" : "none";
|
|
254
254
|
};
|
|
255
255
|
const listedCount = () => this.listed(this.catalogue()).length;
|
|
256
|
+
const styleOf = async (set) => {
|
|
257
|
+
const domain = set.skill?.replace(/^pixerate-/, "");
|
|
258
|
+
if (!discovery.styles || !domain || !discovery.styles.domains.some((d) => d.id === domain)) return void 0;
|
|
259
|
+
const style = await discovery.styles.resolve(domain);
|
|
260
|
+
return style.skills.length ? { domain, skills: style.skills.length, layers: [...new Set(style.skills.map((s) => s.layer))] } : void 0;
|
|
261
|
+
};
|
|
256
262
|
const tools = [
|
|
257
263
|
{
|
|
258
264
|
name: "list_toolsets",
|
|
@@ -260,16 +266,18 @@ var ToolRegistryView = class {
|
|
|
260
266
|
description: "Lists the groups of editor tools available here (one per open editor or feature): name, title, what it covers, its agent skill, how many tools it has and whether they are in your tool list (`enabled`: all, some or none). Start here, then call search_tools or enable_tools.",
|
|
261
267
|
annotations: READ_ONLY,
|
|
262
268
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
263
|
-
handler: () => {
|
|
269
|
+
handler: async () => {
|
|
264
270
|
const cat = this.catalogue();
|
|
271
|
+
const styles = await Promise.all(cat.sets.map(styleOf));
|
|
265
272
|
return {
|
|
266
|
-
toolsets: cat.sets.map((set) => ({
|
|
273
|
+
toolsets: cat.sets.map((set, i) => ({
|
|
267
274
|
name: set.name,
|
|
268
275
|
...set.title ? { title: set.title } : {},
|
|
269
276
|
...set.description ? { description: set.description } : {},
|
|
270
277
|
...set.skill ? { skill: set.skill } : {},
|
|
271
278
|
tools: set.tools.length,
|
|
272
|
-
enabled: enabledState(set)
|
|
279
|
+
enabled: enabledState(set),
|
|
280
|
+
...styles[i] ? { style: styles[i] } : {}
|
|
273
281
|
})),
|
|
274
282
|
...skills.length > 0 ? { skills: skills.map((s) => s.name) } : {}
|
|
275
283
|
};
|
|
@@ -312,9 +320,12 @@ var ToolRegistryView = class {
|
|
|
312
320
|
description: "Adds tools to your tool list: single tools by name (from search_tools) and/or whole toolsets (from list_toolsets). Returns the names added and how many tools are listed now. Prefer single tools over large toolsets. Unknown names fail and change nothing.",
|
|
313
321
|
annotations: LISTING,
|
|
314
322
|
inputSchema: SELECTION_SCHEMA,
|
|
315
|
-
handler: (args) => {
|
|
323
|
+
handler: async (args) => {
|
|
316
324
|
const added = this.enableTools(args);
|
|
317
|
-
|
|
325
|
+
const sets = this.catalogue().sets.filter((set) => set.tools.some((t) => added.includes(t)));
|
|
326
|
+
const domains = (await Promise.all(sets.map(styleOf))).filter((s) => s !== void 0).map((s) => s.domain);
|
|
327
|
+
const style = [...new Set(domains)].map((d) => `House style applies: call get_style with domain "${d}" before making changes.`);
|
|
328
|
+
return { added, listed: listedCount(), ...style.length ? { style } : {} };
|
|
318
329
|
}
|
|
319
330
|
},
|
|
320
331
|
{
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {
|
|
2
|
+
contrastLevel,
|
|
3
|
+
contrastRatio,
|
|
4
|
+
linearSrgbToDisplayP3,
|
|
5
|
+
linearToSrgb,
|
|
6
|
+
oklabToOklch,
|
|
7
|
+
oklabToRgb,
|
|
8
|
+
oklchToOklab,
|
|
9
|
+
parseColor,
|
|
10
|
+
rgbToHsl,
|
|
11
|
+
rgbToOklab,
|
|
12
|
+
srgbToLinear
|
|
13
|
+
} from "./chunk-MHLU6GRJ.js";
|
|
14
|
+
|
|
15
|
+
// src/color/gamut.ts
|
|
16
|
+
var clamp01 = (v) => Math.min(1, Math.max(0, v));
|
|
17
|
+
function inSrgbGamut(c, epsilon = 1e-5) {
|
|
18
|
+
return c.r >= -epsilon && c.r <= 1 + epsilon && c.g >= -epsilon && c.g <= 1 + epsilon && c.b >= -epsilon && c.b <= 1 + epsilon;
|
|
19
|
+
}
|
|
20
|
+
function deltaEOK(a, b) {
|
|
21
|
+
const la = rgbToOklab(a);
|
|
22
|
+
const lb = rgbToOklab(b);
|
|
23
|
+
return Math.hypot(la.l - lb.l, la.a - lb.a, la.b - lb.b);
|
|
24
|
+
}
|
|
25
|
+
function toSrgbGamut(c) {
|
|
26
|
+
const lch = oklabToOklch(rgbToOklab(c));
|
|
27
|
+
if (lch.l >= 1) return { r: 1, g: 1, b: 1, a: c.a };
|
|
28
|
+
if (lch.l <= 0) return { r: 0, g: 0, b: 0, a: c.a };
|
|
29
|
+
if (inSrgbGamut(c)) {
|
|
30
|
+
return { r: clamp01(c.r), g: clamp01(c.g), b: clamp01(c.b), a: c.a };
|
|
31
|
+
}
|
|
32
|
+
let low = 0;
|
|
33
|
+
let high = lch.c;
|
|
34
|
+
let clippedCandidate = { r: clamp01(c.r), g: clamp01(c.g), b: clamp01(c.b), a: c.a };
|
|
35
|
+
const JND = 0.02;
|
|
36
|
+
const EPS = 1e-4;
|
|
37
|
+
for (let i = 0; i < 30; i++) {
|
|
38
|
+
const mid = (low + high) / 2;
|
|
39
|
+
const candidate = oklabToRgb(oklchToOklab({ l: lch.l, c: mid, h: lch.h, alpha: c.a }));
|
|
40
|
+
const clipped = {
|
|
41
|
+
r: clamp01(candidate.r),
|
|
42
|
+
g: clamp01(candidate.g),
|
|
43
|
+
b: clamp01(candidate.b),
|
|
44
|
+
a: c.a
|
|
45
|
+
};
|
|
46
|
+
clippedCandidate = clipped;
|
|
47
|
+
const dE = deltaEOK(candidate, clipped);
|
|
48
|
+
if (dE < JND) {
|
|
49
|
+
if (JND - dE < EPS) {
|
|
50
|
+
return clipped;
|
|
51
|
+
}
|
|
52
|
+
low = mid;
|
|
53
|
+
} else {
|
|
54
|
+
high = mid;
|
|
55
|
+
}
|
|
56
|
+
if (high - low < 1e-4) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return clippedCandidate;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/color/format.ts
|
|
64
|
+
var clamp012 = (v) => Math.min(1, Math.max(0, v));
|
|
65
|
+
var byte = (v) => Math.round(clamp012(v) * 255);
|
|
66
|
+
var hex2 = (n) => n.toString(16).padStart(2, "0");
|
|
67
|
+
var round2 = (n) => (Math.round(n * 100) / 100).toString();
|
|
68
|
+
function formatHex(c) {
|
|
69
|
+
const mapped = toSrgbGamut(c);
|
|
70
|
+
const head = `#${hex2(byte(mapped.r))}${hex2(byte(mapped.g))}${hex2(byte(mapped.b))}`;
|
|
71
|
+
const alpha = byte(mapped.a);
|
|
72
|
+
return alpha === 255 ? head : head + hex2(alpha);
|
|
73
|
+
}
|
|
74
|
+
function formatColor(c, format) {
|
|
75
|
+
switch (format) {
|
|
76
|
+
case "hex":
|
|
77
|
+
return formatHex(c);
|
|
78
|
+
case "rgb": {
|
|
79
|
+
const inGamut = toSrgbGamut(c);
|
|
80
|
+
const r = byte(inGamut.r);
|
|
81
|
+
const g = byte(inGamut.g);
|
|
82
|
+
const b = byte(inGamut.b);
|
|
83
|
+
return c.a < 1 ? `rgb(${r} ${g} ${b} / ${round2(c.a)})` : `rgb(${r} ${g} ${b})`;
|
|
84
|
+
}
|
|
85
|
+
case "hsl": {
|
|
86
|
+
const hsl = rgbToHsl(toSrgbGamut(c));
|
|
87
|
+
const h = (Math.round(hsl.h * 100) / 100).toString();
|
|
88
|
+
const s = (Math.round(hsl.s * 1e4) / 100).toString();
|
|
89
|
+
const l = (Math.round(hsl.l * 1e4) / 100).toString();
|
|
90
|
+
return c.a < 1 ? `hsl(${h} ${s}% ${l}% / ${round2(c.a)})` : `hsl(${h} ${s}% ${l}%)`;
|
|
91
|
+
}
|
|
92
|
+
case "oklch": {
|
|
93
|
+
const lch = oklabToOklch(rgbToOklab(c));
|
|
94
|
+
const l = (lch.l * 100).toFixed(2);
|
|
95
|
+
const ch = lch.c.toFixed(4);
|
|
96
|
+
const h = lch.h.toFixed(2);
|
|
97
|
+
return c.a < 1 ? `oklch(${l}% ${ch} ${h} / ${round2(c.a)})` : `oklch(${l}% ${ch} ${h})`;
|
|
98
|
+
}
|
|
99
|
+
case "oklab": {
|
|
100
|
+
const lab = rgbToOklab(c);
|
|
101
|
+
const l = (lab.l * 100).toFixed(2);
|
|
102
|
+
const a = lab.a.toFixed(4);
|
|
103
|
+
const b = lab.b.toFixed(4);
|
|
104
|
+
return c.a < 1 ? `oklab(${l}% ${a} ${b} / ${round2(c.a)})` : `oklab(${l}% ${a} ${b})`;
|
|
105
|
+
}
|
|
106
|
+
case "p3": {
|
|
107
|
+
const linSrgb = {
|
|
108
|
+
r: srgbToLinear(c.r),
|
|
109
|
+
g: srgbToLinear(c.g),
|
|
110
|
+
b: srgbToLinear(c.b),
|
|
111
|
+
a: c.a
|
|
112
|
+
};
|
|
113
|
+
const linP3 = linearSrgbToDisplayP3(linSrgb);
|
|
114
|
+
const r = linearToSrgb(linP3.r).toFixed(4);
|
|
115
|
+
const g = linearToSrgb(linP3.g).toFixed(4);
|
|
116
|
+
const b = linearToSrgb(linP3.b).toFixed(4);
|
|
117
|
+
return c.a < 1 ? `color(display-p3 ${r} ${g} ${b} / ${round2(c.a)})` : `color(display-p3 ${r} ${g} ${b})`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function normalizeColor(input) {
|
|
122
|
+
const parsed = parseColor(input);
|
|
123
|
+
return parsed ? formatHex(parsed) : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/color/suggest.ts
|
|
127
|
+
function suggestAccessibleForeground(foreground, background, options = {}) {
|
|
128
|
+
const large = options.large ?? false;
|
|
129
|
+
const lch = oklabToOklch(rgbToOklab(foreground));
|
|
130
|
+
const asHex = (l) => parseColor(formatHex(toSrgbGamut(oklabToRgb(oklchToOklab({ ...lch, l })))));
|
|
131
|
+
for (let d = 0.01; d <= 1; d += 0.01) {
|
|
132
|
+
const downCand = asHex(Math.max(0, lch.l - d));
|
|
133
|
+
const upCand = asHex(Math.min(1, lch.l + d));
|
|
134
|
+
const downRatio = contrastRatio(downCand, background);
|
|
135
|
+
const upRatio = contrastRatio(upCand, background);
|
|
136
|
+
const downPass = contrastLevel(downRatio, { large }) !== "fail";
|
|
137
|
+
const upPass = contrastLevel(upRatio, { large }) !== "fail";
|
|
138
|
+
if (downPass && upPass) return formatHex(downRatio >= upRatio ? downCand : upCand);
|
|
139
|
+
if (downPass) return formatHex(downCand);
|
|
140
|
+
if (upPass) return formatHex(upCand);
|
|
141
|
+
}
|
|
142
|
+
return void 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export {
|
|
146
|
+
inSrgbGamut,
|
|
147
|
+
deltaEOK,
|
|
148
|
+
toSrgbGamut,
|
|
149
|
+
formatHex,
|
|
150
|
+
formatColor,
|
|
151
|
+
normalizeColor,
|
|
152
|
+
suggestAccessibleForeground
|
|
153
|
+
};
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseSankey,
|
|
3
|
+
parseTreemap
|
|
4
|
+
} from "./chunk-KGRZMHZB.js";
|
|
1
5
|
import {
|
|
2
6
|
chartMovingAverage,
|
|
7
|
+
chartRaceRanking,
|
|
3
8
|
chartSupportsTrendlines,
|
|
4
9
|
fitChartTrendline,
|
|
5
10
|
formatChartNumber,
|
|
@@ -7,20 +12,23 @@ import {
|
|
|
7
12
|
r2,
|
|
8
13
|
sameChartSelection,
|
|
9
14
|
summarizeChartSeries
|
|
10
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-ITANXMZD.js";
|
|
11
16
|
import {
|
|
12
17
|
CHART_TYPE_LABELS,
|
|
13
18
|
DEFAULT_CHART_PALETTE,
|
|
14
19
|
DEFAULT_CHART_THEME,
|
|
15
20
|
chartDataFromTable,
|
|
16
21
|
chartFamily,
|
|
22
|
+
chartLinksFromTable,
|
|
17
23
|
chartSeriesColor,
|
|
18
24
|
chartSliceColor,
|
|
19
25
|
chartSpecColorsByCategory,
|
|
20
26
|
chartTreeDepth,
|
|
21
27
|
chartTreeFromRows,
|
|
28
|
+
chartTreeFromSeries,
|
|
22
29
|
chartTreeFromTable,
|
|
23
30
|
chartTreeToRows,
|
|
31
|
+
chartTreeTotal,
|
|
24
32
|
checkChartColors,
|
|
25
33
|
cloneChartSeries,
|
|
26
34
|
cloneChartSpec,
|
|
@@ -28,6 +36,7 @@ import {
|
|
|
28
36
|
defaultChartAxis,
|
|
29
37
|
describeChartErrorBars,
|
|
30
38
|
findChartTreeNode,
|
|
39
|
+
normalizeChartLinks,
|
|
31
40
|
normalizeChartReferenceBand,
|
|
32
41
|
normalizeChartReferenceLine,
|
|
33
42
|
normalizeChartSpec,
|
|
@@ -36,7 +45,7 @@ import {
|
|
|
36
45
|
parseChartValue,
|
|
37
46
|
shadeChartColor,
|
|
38
47
|
uniqueChartSeriesId
|
|
39
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-FR432ROA.js";
|
|
40
49
|
import {
|
|
41
50
|
parseCsv,
|
|
42
51
|
parseTsv
|
|
@@ -71,9 +80,14 @@ function splitOutside(text, sep) {
|
|
|
71
80
|
out.push(cur);
|
|
72
81
|
return out;
|
|
73
82
|
}
|
|
83
|
+
var csvField = (text) => /[",\n]|^\s|\s$/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
84
|
+
var treemapName = (name) => !name.includes('"') ? `"${name}"` : !name.includes("'") ? `'${name}'` : `"${name.replace(/"/g, "'")}"`;
|
|
85
|
+
var yamlText = (text) => /^[\w][\w .,!?()/&+-]*$/.test(text) && !/:\s/.test(text) ? text : JSON.stringify(text);
|
|
74
86
|
function chartMermaidFamily(spec) {
|
|
75
87
|
const family = chartFamily(spec);
|
|
76
|
-
if (family === "
|
|
88
|
+
if (family === "sankey" && spec.links?.length) return "sankey";
|
|
89
|
+
if (family === "treemap" || family === "sunburst") return "treemap";
|
|
90
|
+
if (family === "pie" || family === "funnel" || family === "parliament") return "pie";
|
|
77
91
|
if (family === "radar") return "radar";
|
|
78
92
|
return "xychart";
|
|
79
93
|
}
|
|
@@ -114,7 +128,27 @@ function chartToMermaid(spec, options = {}) {
|
|
|
114
128
|
const visible = spec.series.filter((s) => !s.hidden);
|
|
115
129
|
const lines = [];
|
|
116
130
|
const v = (x) => x === null || !Number.isFinite(x) ? "0" : num(x);
|
|
117
|
-
|
|
131
|
+
const front = [];
|
|
132
|
+
if (family === "sankey") {
|
|
133
|
+
if (spec.title) front.push("---", `title: ${yamlText(spec.title)}`, "---");
|
|
134
|
+
lines.push("sankey-beta");
|
|
135
|
+
for (const l of spec.links) lines.push(`${csvField(l.source)},${csvField(l.target)},${num(l.value)}`);
|
|
136
|
+
} else if (family === "treemap") {
|
|
137
|
+
lines.push("treemap-beta");
|
|
138
|
+
if (spec.title) lines.push(`title ${spec.title}`);
|
|
139
|
+
const first = visible[0] ?? spec.series[0];
|
|
140
|
+
const tree = spec.tree.length ? spec.tree : chartTreeFromSeries(spec.categories, first?.values ?? []);
|
|
141
|
+
const write = (nodes, depth) => {
|
|
142
|
+
for (const n of nodes) {
|
|
143
|
+
const pad = ind.repeat(depth);
|
|
144
|
+
if (n.children) {
|
|
145
|
+
lines.push(`${pad}${treemapName(n.name)}`);
|
|
146
|
+
write(n.children, depth + 1);
|
|
147
|
+
} else lines.push(`${pad}${treemapName(n.name)}: ${num(n.value ?? 0)}`);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
write(tree, 1);
|
|
151
|
+
} else if (family === "pie") {
|
|
118
152
|
const s = visible[0] ?? spec.series[0];
|
|
119
153
|
lines.push(`pie${spec.dataLabels ? " showData" : ""}`);
|
|
120
154
|
if (spec.title) lines.push(`${ind}title ${spec.title}`);
|
|
@@ -160,13 +194,13 @@ function chartToMermaid(spec, options = {}) {
|
|
|
160
194
|
lines.push(`${ind}${kind} ${quote(s.name)} [${s.values.map(v).join(", ")}]`);
|
|
161
195
|
}
|
|
162
196
|
}
|
|
163
|
-
let text = `${lines.join("\n")}
|
|
197
|
+
let text = `${[...front, ...lines].join("\n")}
|
|
164
198
|
`;
|
|
165
199
|
if (options.meta !== false) {
|
|
166
200
|
const meta = metaFor(spec, text);
|
|
167
201
|
if (meta) {
|
|
168
202
|
const [header, ...rest] = lines;
|
|
169
|
-
text = `${[header, `${CHART_MERMAID_META_PREFIX}${JSON.stringify({ v: 1, ...meta })}`, ...rest].join("\n")}
|
|
203
|
+
text = `${[...front, header, `${CHART_MERMAID_META_PREFIX}${JSON.stringify({ v: 1, ...meta })}`, ...rest].join("\n")}
|
|
170
204
|
`;
|
|
171
205
|
}
|
|
172
206
|
}
|
|
@@ -229,7 +263,9 @@ function chartFromMermaid(text, options = {}) {
|
|
|
229
263
|
showData = !!m[1];
|
|
230
264
|
if (m[2]) frontTitle = m[2].trim();
|
|
231
265
|
} else if (/^radar(?:-beta)?\s*$/i.test(t)) family = "radar";
|
|
232
|
-
else
|
|
266
|
+
else if (/^sankey(?:-beta)?\s*$/i.test(t)) family = "sankey";
|
|
267
|
+
else if (/^treemap(?:-beta)?\s*$/i.test(t)) family = "treemap";
|
|
268
|
+
else issues.push({ path: "header", line: i + 1, message: `Line ${i + 1}: expected a chart header (xychart-beta, pie, radar-beta, sankey-beta or treemap-beta), got "${t.slice(0, 40)}".` });
|
|
233
269
|
headerLine = i;
|
|
234
270
|
break;
|
|
235
271
|
}
|
|
@@ -245,6 +281,7 @@ function chartFromMermaid(text, options = {}) {
|
|
|
245
281
|
let graticule = "circle";
|
|
246
282
|
const counts = { bar: 0, line: 0 };
|
|
247
283
|
const lineIssue = (i, message) => issues.push({ path: `line ${i + 1}`, line: i + 1, message: `Line ${i + 1}: ${message}` });
|
|
284
|
+
const whole = family === "sankey" || family === "treemap";
|
|
248
285
|
for (let i = headerLine + 1; family && i < all.length; i++) {
|
|
249
286
|
const raw = all[i];
|
|
250
287
|
const t = raw.trim();
|
|
@@ -260,7 +297,7 @@ function chartFromMermaid(text, options = {}) {
|
|
|
260
297
|
}
|
|
261
298
|
continue;
|
|
262
299
|
}
|
|
263
|
-
if (t.startsWith("%%")) continue;
|
|
300
|
+
if (t.startsWith("%%") || whole) continue;
|
|
264
301
|
const isLast = i === all.length - 1 || all.slice(i + 1).every((l) => !l.trim());
|
|
265
302
|
let m;
|
|
266
303
|
if (m = /^title\s+(.+)$/i.exec(t)) {
|
|
@@ -362,7 +399,8 @@ function chartFromMermaid(text, options = {}) {
|
|
|
362
399
|
if (!(partial && isLast)) lineIssue(i, `could not read "${t.slice(0, 60)}"; it was skipped.`);
|
|
363
400
|
}
|
|
364
401
|
let input;
|
|
365
|
-
if (family
|
|
402
|
+
if (whole) input = readWholeMermaid(family, all, headerLine, partial, title, issues);
|
|
403
|
+
else if (family === "pie") {
|
|
366
404
|
input = {
|
|
367
405
|
type: "pie",
|
|
368
406
|
title,
|
|
@@ -398,18 +436,54 @@ function chartFromMermaid(text, options = {}) {
|
|
|
398
436
|
}
|
|
399
437
|
}
|
|
400
438
|
const { spec, issues: specIssues } = normalizeChartSpec(input);
|
|
401
|
-
if (!family && !partial && !issues.length) issues.push({ path: "header", message: "The text is empty: expected a chart header (xychart-beta, pie or
|
|
439
|
+
if (!family && !partial && !issues.length) issues.push({ path: "header", message: "The text is empty: expected a chart header (xychart-beta, pie, radar-beta, sankey-beta or treemap-beta)." });
|
|
402
440
|
return { spec, issues: [...issues, ...specIssues], family, hasMeta: !!meta };
|
|
403
441
|
}
|
|
442
|
+
function readWholeMermaid(family, all, headerLine, partial, title, issues) {
|
|
443
|
+
const body = all.slice(headerLine).map((l) => l.trim().startsWith(CHART_MERMAID_META_PREFIX.trim()) ? "%%" : l);
|
|
444
|
+
const parse = (lines) => family === "sankey" ? parseSankey(lines.join("\n")) : parseTreemap(lines.join("\n"));
|
|
445
|
+
let read = parse(body);
|
|
446
|
+
if ("error" in read && partial && body.length > 1) read = parse(body.slice(0, -1));
|
|
447
|
+
const lineOf = (n) => n === void 0 ? void 0 : n + headerLine;
|
|
448
|
+
if ("error" in read) {
|
|
449
|
+
const line = lineOf(read.line);
|
|
450
|
+
if (!(partial && body.length <= 2)) issues.push({ path: family, ...line ? { line } : {}, message: line ? `Line ${line}: ${read.error}` : read.error });
|
|
451
|
+
return { type: family === "sankey" ? "sankey" : "treemap", title };
|
|
452
|
+
}
|
|
453
|
+
if (family === "sankey") {
|
|
454
|
+
const data2 = read.data;
|
|
455
|
+
return { type: "sankey", title, links: data2.links, sankeyNodes: data2.nodes, series: [{ id: "series-1", name: "Values", values: [] }] };
|
|
456
|
+
}
|
|
457
|
+
const data = read.data;
|
|
458
|
+
let items = data.items;
|
|
459
|
+
let name = data.title ?? title;
|
|
460
|
+
if (items.length === 1 && items[0].children?.length) {
|
|
461
|
+
name ||= items[0].name;
|
|
462
|
+
items = items[0].children;
|
|
463
|
+
}
|
|
464
|
+
const toNode = (item) => item.children?.length ? { name: item.name, children: item.children.map(toNode) } : { name: item.name, value: item.value ?? 0 };
|
|
465
|
+
const tree = items.map(toNode);
|
|
466
|
+
const leaf = (n) => !n.children;
|
|
467
|
+
const flatOrOneLevel = tree.every(leaf) || tree.every((n) => n.children?.every(leaf));
|
|
468
|
+
return { type: flatOrOneLevel ? "treemap" : "sunburst", title: name, tree, categories: tree.map((n) => n.name), series: [{ id: "series-1", name: "Values", values: tree.map(chartTreeTotal) }] };
|
|
469
|
+
}
|
|
404
470
|
function findMermaidCharts(text) {
|
|
405
471
|
const lines = text.replace(/^/, "").split(/\r?\n/);
|
|
406
472
|
const found = [];
|
|
407
473
|
const familyOf = (body) => {
|
|
408
|
-
|
|
474
|
+
let rows = body.split(/\r?\n/);
|
|
475
|
+
const firstRow = rows.findIndex((l) => l.trim());
|
|
476
|
+
if (rows[firstRow]?.trim() === "---") {
|
|
477
|
+
const end = rows.findIndex((l, i) => i > firstRow && l.trim() === "---");
|
|
478
|
+
if (end > 0) rows = rows.slice(end + 1);
|
|
479
|
+
}
|
|
480
|
+
const header = rows.find((l) => l.trim() && !l.trim().startsWith("%%"));
|
|
409
481
|
if (!header) return null;
|
|
410
482
|
if (/^\s*xychart(-beta)?\b/.test(header)) return "xychart";
|
|
411
483
|
if (/^\s*pie\b/.test(header)) return "pie";
|
|
412
484
|
if (/^\s*radar(-beta)?\b/.test(header)) return "radar";
|
|
485
|
+
if (/^\s*sankey(-beta)?\s*$/.test(header)) return "sankey";
|
|
486
|
+
if (/^\s*treemap(-beta)?\s*$/.test(header)) return "treemap";
|
|
413
487
|
return null;
|
|
414
488
|
};
|
|
415
489
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -476,6 +550,21 @@ function describeChart(spec, series = spec.series.filter((s) => !s.hidden).map((
|
|
|
476
550
|
}
|
|
477
551
|
return parts.join(" ");
|
|
478
552
|
}
|
|
553
|
+
if (family === "barrace") {
|
|
554
|
+
const n = spec.categories.length;
|
|
555
|
+
const first = chartRaceRanking(spec, 0)[0];
|
|
556
|
+
const last = chartRaceRanking(spec, n - 1)[0];
|
|
557
|
+
const lead = (e, at) => e ? `${e.name} leads at ${at} with ${fmt(e.value)}` : "";
|
|
558
|
+
return `${head} of ${series.length} names over ${n} time points (${spec.categories[0]} to ${spec.categories[n - 1]}), showing the top ${Math.min(spec.raceTopN, series.length)}. ${lead(first, spec.categories[0])}; ${lead(last, spec.categories[n - 1])}.`;
|
|
559
|
+
}
|
|
560
|
+
if (family === "sankey") {
|
|
561
|
+
if (!spec.links.length) return `${head} with no flows yet.`;
|
|
562
|
+
const total = spec.links.reduce((t, l) => t + l.value, 0);
|
|
563
|
+
const largest = [...spec.links].sort((a, b) => b.value - a.value)[0];
|
|
564
|
+
const sources = spec.sankeyNodes.filter((n) => !spec.links.some((l) => l.target === n));
|
|
565
|
+
const sinks = spec.sankeyNodes.filter((n) => !spec.links.some((l) => l.source === n));
|
|
566
|
+
return `${head} of ${spec.links.length} flows between ${spec.sankeyNodes.length} nodes, from ${sources.join(", ")} to ${sinks.join(", ")}. The largest flow is ${largest.source} \u2192 ${largest.target} (${fmt(largest.value)}); all flows add up to ${fmt(total)}.`;
|
|
567
|
+
}
|
|
479
568
|
if (family === "parliament") {
|
|
480
569
|
const s = spec.series.find((x) => !x.hidden);
|
|
481
570
|
const seats = spec.categories.map((c, i) => ({ c, n: Math.max(0, Math.round(s.values[i] ?? 0)) }));
|
|
@@ -695,6 +784,17 @@ function sameChartJson(a, b) {
|
|
|
695
784
|
if (keys.length !== Object.keys(bo).filter((k) => bo[k] !== void 0).length) return false;
|
|
696
785
|
return keys.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && sameChartJson(ao[k], bo[k]));
|
|
697
786
|
}
|
|
787
|
+
var defaultRaceTicker = {
|
|
788
|
+
now: () => typeof performance !== "undefined" ? performance.now() : Date.now(),
|
|
789
|
+
next: (step) => {
|
|
790
|
+
if (typeof requestAnimationFrame === "function") {
|
|
791
|
+
const id2 = requestAnimationFrame(() => step());
|
|
792
|
+
return () => cancelAnimationFrame(id2);
|
|
793
|
+
}
|
|
794
|
+
const id = setTimeout(step, 16);
|
|
795
|
+
return () => clearTimeout(id);
|
|
796
|
+
}
|
|
797
|
+
};
|
|
698
798
|
var fail = (message, issues) => ({ ok: false, message, ...issues ? { issues } : {} });
|
|
699
799
|
var ChartController = class {
|
|
700
800
|
history;
|
|
@@ -707,12 +807,16 @@ var ChartController = class {
|
|
|
707
807
|
revision = 0;
|
|
708
808
|
applying = false;
|
|
709
809
|
onChange;
|
|
810
|
+
raceTicker;
|
|
811
|
+
raceFrame = null;
|
|
812
|
+
raceStop = null;
|
|
710
813
|
listeners = /* @__PURE__ */ new Set();
|
|
711
814
|
unsubscribeHistory;
|
|
712
815
|
state;
|
|
713
816
|
constructor(options = {}) {
|
|
714
817
|
this.history = options.history ?? new HistoryManager();
|
|
715
818
|
this.onChange = options.onChange;
|
|
819
|
+
this.raceTicker = options.raceTicker ?? defaultRaceTicker;
|
|
716
820
|
const { spec, issues } = normalizeChartSpec(options.spec ?? {});
|
|
717
821
|
this.spec = spec;
|
|
718
822
|
this.issues = issues;
|
|
@@ -762,6 +866,8 @@ var ChartController = class {
|
|
|
762
866
|
canRedo: h.canRedo,
|
|
763
867
|
undoLabel: h.undoName,
|
|
764
868
|
redoLabel: h.redoName,
|
|
869
|
+
raceFrame: this.raceFrame === null ? null : Math.min(this.raceFrame, Math.max(0, this.spec.categories.length - 1)),
|
|
870
|
+
racePlaying: this.raceStop !== null,
|
|
765
871
|
revision: this.revision
|
|
766
872
|
};
|
|
767
873
|
}
|
|
@@ -910,7 +1016,7 @@ var ChartController = class {
|
|
|
910
1016
|
* to the categories; all-or-nothing like `update`.
|
|
911
1017
|
*/
|
|
912
1018
|
setData(data, options = {}) {
|
|
913
|
-
const clear = (data.categories || data.series) && this.spec.tree.length ? { tree: [] } : {};
|
|
1019
|
+
const clear = (data.categories || data.series) && (this.spec.tree.length || this.spec.links.length) ? { tree: [], links: [] } : {};
|
|
914
1020
|
return this.update({ ...data.categories ? { categories: data.categories } : {}, ...data.series ? { series: data.series } : {}, ...clear }, { label: options.label ?? "Edit chart data" });
|
|
915
1021
|
}
|
|
916
1022
|
/**
|
|
@@ -1153,7 +1259,7 @@ var ChartController = class {
|
|
|
1153
1259
|
const old = byName.get(s.name);
|
|
1154
1260
|
return { ...s, ...old?.color ? { color: old.color } : {}, ...old?.type ? { type: old.type } : {}, ...old ? { id: old.id } : {} };
|
|
1155
1261
|
});
|
|
1156
|
-
const { spec } = normalizeChartSpec({ ...this.spec, categories: data.categories, series, tree: [] });
|
|
1262
|
+
const { spec } = normalizeChartSpec({ ...this.spec, categories: data.categories, series, tree: [], links: [] });
|
|
1157
1263
|
this.commit("Import data", spec);
|
|
1158
1264
|
if (data.issues.length) {
|
|
1159
1265
|
this.issues = data.issues;
|
|
@@ -1170,8 +1276,12 @@ var ChartController = class {
|
|
|
1170
1276
|
// -------------------------------------------------------------------------
|
|
1171
1277
|
// Grouped data (tree)
|
|
1172
1278
|
// -------------------------------------------------------------------------
|
|
1173
|
-
/** A refusal while the data is a tree (cell edits would be overwritten by
|
|
1279
|
+
/** A refusal while the data is a tree or flows (cell edits would be overwritten by their totals), else null. */
|
|
1174
1280
|
treeLocked() {
|
|
1281
|
+
if (this.spec.links.length)
|
|
1282
|
+
return this.reject(
|
|
1283
|
+
"This chart's data is flows (links): its categories are the nodes and its values how much passes through each. Change it with setLinks / the flow edits (set_chart_links, add_chart_link, update_chart_link, remove_chart_link), or replace it with new data."
|
|
1284
|
+
);
|
|
1175
1285
|
if (!this.spec.tree.length) return null;
|
|
1176
1286
|
return this.reject(
|
|
1177
1287
|
"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."
|
|
@@ -1182,7 +1292,7 @@ var ChartController = class {
|
|
|
1182
1292
|
const issues = [];
|
|
1183
1293
|
const next = normalizeChartTree(tree, issues);
|
|
1184
1294
|
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") });
|
|
1295
|
+
return this.update({ tree: next, links: [] }, { label: options.label ?? (next.length ? "Replace grouped data" : "Ungroup data") });
|
|
1186
1296
|
}
|
|
1187
1297
|
/**
|
|
1188
1298
|
* Replaces the grouped data from rows of names and a value (group, …, item, value), as the
|
|
@@ -1208,7 +1318,7 @@ var ChartController = class {
|
|
|
1208
1318
|
const next = chartTreeFromTable(rows, options, issues);
|
|
1209
1319
|
if (issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
|
|
1210
1320
|
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" });
|
|
1321
|
+
return this.update({ tree: next, links: [] }, { label: "Import grouped data" });
|
|
1212
1322
|
}
|
|
1213
1323
|
nodeAt(path) {
|
|
1214
1324
|
const node = findChartTreeNode(this.spec.tree, path);
|
|
@@ -1281,6 +1391,56 @@ var ChartController = class {
|
|
|
1281
1391
|
return this.update({ tree }, { label: `Delete "${node.name}"` });
|
|
1282
1392
|
}
|
|
1283
1393
|
// -------------------------------------------------------------------------
|
|
1394
|
+
// Flows (links)
|
|
1395
|
+
// -------------------------------------------------------------------------
|
|
1396
|
+
/**
|
|
1397
|
+
* Replaces the flows (and, when given, the node order). An empty list goes back to the
|
|
1398
|
+
* table, keeping how much passed through each node as values. One undo step, all-or-nothing.
|
|
1399
|
+
*/
|
|
1400
|
+
setLinks(links, options = {}) {
|
|
1401
|
+
const issues = [];
|
|
1402
|
+
const next = normalizeChartLinks(links, issues);
|
|
1403
|
+
if (issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
|
|
1404
|
+
const used = new Set(next.flatMap((l) => [l.source, l.target]));
|
|
1405
|
+
const sankeyNodes = options.nodes ? [...options.nodes] : this.spec.sankeyNodes.filter((n) => used.has(n));
|
|
1406
|
+
return this.update({ links: next, tree: [], sankeyNodes }, { label: options.label ?? (next.length ? "Replace flows" : "Remove flows") });
|
|
1407
|
+
}
|
|
1408
|
+
/** Replaces the data with flows from a table of source, target and value columns. */
|
|
1409
|
+
importLinksTable(rows, options = {}) {
|
|
1410
|
+
const raw = chartLinksFromTable(rows, options);
|
|
1411
|
+
if (!raw.length) return this.reject('There are no flows to import: expected rows of source, target and value, e.g. "Website,Signup,120".');
|
|
1412
|
+
return this.setLinks(raw, { label: "Import flows" });
|
|
1413
|
+
}
|
|
1414
|
+
badLink(index) {
|
|
1415
|
+
const n = this.spec.links.length;
|
|
1416
|
+
return fail(`There is no flow ${index}; ${n ? `expected 0 to ${n - 1}` : "the chart has none yet"}.`);
|
|
1417
|
+
}
|
|
1418
|
+
/** Adds a flow and returns its index. */
|
|
1419
|
+
addLink(link) {
|
|
1420
|
+
const result = this.setLinks([...this.spec.links, link], { label: `Add flow ${String(link?.source ?? "")} \u2192 ${String(link?.target ?? "")}` });
|
|
1421
|
+
if (!result.ok) return result;
|
|
1422
|
+
return { ok: true, index: this.spec.links.length - 1 };
|
|
1423
|
+
}
|
|
1424
|
+
/** Changes a flow's source, target, value or colour (`color: null` goes back to the default). */
|
|
1425
|
+
updateLink(index, patch) {
|
|
1426
|
+
const current = this.spec.links[index];
|
|
1427
|
+
if (!current) return this.badLink(index);
|
|
1428
|
+
const merged = { ...current, ...patch };
|
|
1429
|
+
if (patch.color === null) delete merged.color;
|
|
1430
|
+
return this.setLinks(
|
|
1431
|
+
this.spec.links.map((l, i) => i === index ? merged : l),
|
|
1432
|
+
{ label: "Change flow" }
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
removeLink(index) {
|
|
1436
|
+
const current = this.spec.links[index];
|
|
1437
|
+
if (!current) return this.badLink(index);
|
|
1438
|
+
return this.setLinks(
|
|
1439
|
+
this.spec.links.filter((_, i) => i !== index),
|
|
1440
|
+
{ label: `Delete flow ${current.source} \u2192 ${current.target}` }
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
// -------------------------------------------------------------------------
|
|
1284
1444
|
// Annotations
|
|
1285
1445
|
// -------------------------------------------------------------------------
|
|
1286
1446
|
/** Sets the note written under the chart ("" removes it). */
|
|
@@ -1516,6 +1676,55 @@ var ChartController = class {
|
|
|
1516
1676
|
// -------------------------------------------------------------------------
|
|
1517
1677
|
// Selection and gestures
|
|
1518
1678
|
// -------------------------------------------------------------------------
|
|
1679
|
+
/**
|
|
1680
|
+
* Shows a bar race at a moment: a category index, fractional between time points (`null`:
|
|
1681
|
+
* the last one). View state like the selection, so not an undo step. Stops playback.
|
|
1682
|
+
*/
|
|
1683
|
+
setRaceFrame(frame) {
|
|
1684
|
+
const n = this.spec.categories.length;
|
|
1685
|
+
if (frame !== null && !(Number.isFinite(frame) && frame >= 0 && frame <= Math.max(0, n - 1)))
|
|
1686
|
+
return fail(n ? `The race has time points 0 to ${n - 1}${n > 1 ? ` ("${this.spec.categories[0]}" to "${this.spec.categories[n - 1]}")` : ""}; got ${frame}.` : "The chart has no time points (categories) yet.");
|
|
1687
|
+
this.stopRace();
|
|
1688
|
+
if (frame === this.raceFrame) return { ok: true };
|
|
1689
|
+
this.raceFrame = frame;
|
|
1690
|
+
this.notify(false);
|
|
1691
|
+
return { ok: true };
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Plays the bar race from the moment shown (from the start when it is at the end), moving
|
|
1695
|
+
* one time point every `msPerStep` (default 1000) until the last. Not an undo step.
|
|
1696
|
+
*/
|
|
1697
|
+
playRace(options = {}) {
|
|
1698
|
+
const last = this.spec.categories.length - 1;
|
|
1699
|
+
if (last < 1) return fail("A race needs at least two time points (categories) to play.");
|
|
1700
|
+
this.stopRace();
|
|
1701
|
+
const per = Math.max(50, options.msPerStep ?? 1e3);
|
|
1702
|
+
const from = this.raceFrame === null || this.raceFrame >= last ? 0 : this.raceFrame;
|
|
1703
|
+
const start = this.raceTicker.now();
|
|
1704
|
+
const step = () => {
|
|
1705
|
+
const at = Math.min(last, from + (this.raceTicker.now() - start) / per);
|
|
1706
|
+
this.raceFrame = at;
|
|
1707
|
+
if (at >= last) {
|
|
1708
|
+
this.raceStop = null;
|
|
1709
|
+
this.raceFrame = null;
|
|
1710
|
+
} else this.raceStop = this.raceTicker.next(step);
|
|
1711
|
+
this.notify(false);
|
|
1712
|
+
};
|
|
1713
|
+
this.raceFrame = from;
|
|
1714
|
+
this.raceStop = this.raceTicker.next(step);
|
|
1715
|
+
this.notify(false);
|
|
1716
|
+
return { ok: true };
|
|
1717
|
+
}
|
|
1718
|
+
/** Pauses bar race playback where it is. */
|
|
1719
|
+
pauseRace() {
|
|
1720
|
+
if (!this.raceStop) return;
|
|
1721
|
+
this.stopRace();
|
|
1722
|
+
this.notify(false);
|
|
1723
|
+
}
|
|
1724
|
+
stopRace() {
|
|
1725
|
+
this.raceStop?.();
|
|
1726
|
+
this.raceStop = null;
|
|
1727
|
+
}
|
|
1519
1728
|
/** Selects a chart element (view state; not an undo step). */
|
|
1520
1729
|
select(selection) {
|
|
1521
1730
|
const next = this.validSelection(selection, this.spec);
|
|
@@ -1638,6 +1847,7 @@ var ChartController = class {
|
|
|
1638
1847
|
}
|
|
1639
1848
|
/** Stops listening to the history (reconnected by the next `subscribe`). */
|
|
1640
1849
|
destroy() {
|
|
1850
|
+
this.stopRace();
|
|
1641
1851
|
this.unsubscribeHistory?.();
|
|
1642
1852
|
this.unsubscribeHistory = void 0;
|
|
1643
1853
|
this.listeners.clear();
|
|
@@ -1673,8 +1883,12 @@ function updateLabel(patch) {
|
|
|
1673
1883
|
parliamentArc: "Change hemicycle sweep",
|
|
1674
1884
|
parliamentMajority: "Change majority",
|
|
1675
1885
|
parliamentLabel: "Edit seat caption",
|
|
1886
|
+
raceTopN: "Change bars shown",
|
|
1676
1887
|
sunburstLabel: "Edit centre caption",
|
|
1677
1888
|
tree: "Replace grouped data",
|
|
1889
|
+
links: "Replace flows",
|
|
1890
|
+
sankeyNodes: "Reorder nodes",
|
|
1891
|
+
sankeyLinkColor: "Change flow colours",
|
|
1678
1892
|
radarTicks: "Change radar rings",
|
|
1679
1893
|
radarGraticule: "Change radar grid",
|
|
1680
1894
|
legend: "Change legend",
|