@pixerate/editor 0.47.0 → 0.49.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 +248 -29
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +18 -6
- package/dist/agent/index.d.ts +18 -6
- package/dist/agent/index.js +228 -9
- package/dist/agent/index.js.map +1 -1
- package/dist/{builtins-xs7rK6ys.d.cts → builtins-1kqwLUnb.d.cts} +1 -1
- package/dist/{builtins-D2nMtw2W.d.ts → builtins-BKYlHH1s.d.ts} +1 -1
- package/dist/chart/index.cjs +216 -3541
- package/dist/chart/index.cjs.map +1 -1
- package/dist/chart/index.d.cts +4 -494
- package/dist/chart/index.d.ts +4 -494
- package/dist/chart/index.js +155 -3480
- package/dist/chart/index.js.map +1 -1
- package/dist/{chunk-5VXI5GED.js → chunk-2CJ3LPYQ.js} +3 -3
- package/dist/{chunk-TXXNBDQX.cjs → chunk-2Q46FKZK.cjs} +113 -34
- package/dist/chunk-2Q46FKZK.cjs.map +1 -0
- package/dist/chunk-3JNRQVTS.cjs +521 -0
- package/dist/chunk-3JNRQVTS.cjs.map +1 -0
- package/dist/chunk-5TWIN7ZJ.js +113 -0
- package/dist/chunk-5TWIN7ZJ.js.map +1 -0
- package/dist/{chunk-PQFAHT5O.js → chunk-DQPBQXKG.js} +27 -94
- package/dist/chunk-DQPBQXKG.js.map +1 -0
- package/dist/{chunk-ZFBBVGCI.js → chunk-EKYHKQPG.js} +243 -243
- package/dist/{chunk-ZFBBVGCI.js.map → chunk-EKYHKQPG.js.map} +1 -1
- package/dist/{chunk-J3R2OWOE.cjs → chunk-FEMIVHN3.cjs} +610 -15
- package/dist/chunk-FEMIVHN3.cjs.map +1 -0
- package/dist/{chunk-C3NWJE3K.js → chunk-GJBIVWB2.js} +11 -672
- package/dist/chunk-GJBIVWB2.js.map +1 -0
- package/dist/chunk-GTZXF4BI.cjs +3424 -0
- package/dist/chunk-GTZXF4BI.cjs.map +1 -0
- package/dist/chunk-JOX6MJW4.cjs +674 -0
- package/dist/chunk-JOX6MJW4.cjs.map +1 -0
- package/dist/{chunk-REAGGLXS.js → chunk-JQ3BB3E6.js} +2 -2
- package/dist/{chunk-H4PICYNL.cjs → chunk-LNKZPIQD.cjs} +113 -180
- package/dist/chunk-LNKZPIQD.cjs.map +1 -0
- package/dist/chunk-OV7UAKIY.js +674 -0
- package/dist/chunk-OV7UAKIY.js.map +1 -0
- package/dist/{chunk-TK2XZK6Z.js → chunk-QFTVLEVN.js} +596 -1
- package/dist/chunk-QFTVLEVN.js.map +1 -0
- package/dist/{chunk-KRGXUQ2Y.js → chunk-T6XU4B54.js} +82 -3
- package/dist/chunk-T6XU4B54.js.map +1 -0
- package/dist/chunk-TGBCBQFV.cjs +113 -0
- package/dist/chunk-TGBCBQFV.cjs.map +1 -0
- package/dist/{chunk-SQC26VQV.cjs → chunk-UN33YOXW.cjs} +191 -191
- package/dist/{chunk-SQC26VQV.cjs.map → chunk-UN33YOXW.cjs.map} +1 -1
- package/dist/chunk-WBOAXDKD.js +3424 -0
- package/dist/chunk-WBOAXDKD.js.map +1 -0
- package/dist/{chunk-LYGEEDNR.cjs → chunk-XP5IUZUO.cjs} +242 -242
- package/dist/chunk-XP5IUZUO.cjs.map +1 -0
- package/dist/{chunk-BBRK6MOI.cjs → chunk-YU4WQE5M.cjs} +100 -100
- package/dist/{chunk-BBRK6MOI.cjs.map → chunk-YU4WQE5M.cjs.map} +1 -1
- package/dist/commands/index.d.cts +1 -1
- package/dist/commands/index.d.ts +1 -1
- package/dist/controller-ChjCCMLl.d.cts +822 -0
- package/dist/controller-DtbEo3om.d.ts +822 -0
- package/dist/export/index.cjs +4 -2
- package/dist/export/index.cjs.map +1 -1
- package/dist/export/index.d.cts +5 -1
- package/dist/export/index.d.ts +5 -1
- package/dist/export/index.js +3 -1
- package/dist/extensions/index.cjs +33 -4
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.d.cts +4 -2
- package/dist/extensions/index.d.ts +4 -2
- package/dist/extensions/index.js +32 -3
- package/dist/{index-QrqMmZ75.d.cts → index-DoHqT1mq.d.cts} +203 -7
- package/dist/{index-CSUCQYAH.d.ts → index-YLZ0fKqE.d.ts} +203 -7
- package/dist/index.cjs +39 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -7
- package/dist/index.d.ts +9 -7
- package/dist/index.js +33 -7
- package/dist/index.js.map +1 -1
- package/dist/spreadsheet/index.cjs +4 -4
- package/dist/spreadsheet/index.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-C3NWJE3K.js.map +0 -1
- package/dist/chunk-H4PICYNL.cjs.map +0 -1
- package/dist/chunk-J3R2OWOE.cjs.map +0 -1
- package/dist/chunk-KRGXUQ2Y.js.map +0 -1
- package/dist/chunk-LYGEEDNR.cjs.map +0 -1
- package/dist/chunk-NEOO6QJD.cjs +0 -1182
- package/dist/chunk-NEOO6QJD.cjs.map +0 -1
- package/dist/chunk-PQFAHT5O.js.map +0 -1
- package/dist/chunk-TK2XZK6Z.js.map +0 -1
- package/dist/chunk-TXXNBDQX.cjs.map +0 -1
- package/dist/types-BVqWm3d1.d.cts +0 -320
- package/dist/types-BVqWm3d1.d.ts +0 -320
- /package/dist/{chunk-5VXI5GED.js.map → chunk-2CJ3LPYQ.js.map} +0 -0
- /package/dist/{chunk-REAGGLXS.js.map → chunk-JQ3BB3E6.js.map} +0 -0
|
@@ -0,0 +1,3424 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class;
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
var _chunkJOX6MJW4cjs = require('./chunk-JOX6MJW4.cjs');
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
var _chunkXP5IUZUOcjs = require('./chunk-XP5IUZUO.cjs');
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
var _chunkTNNJXF46cjs = require('./chunk-TNNJXF46.cjs');
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
var _chunkQKLRAA4Tcjs = require('./chunk-QKLRAA4T.cjs');
|
|
32
|
+
|
|
33
|
+
// src/chart/mermaid.ts
|
|
34
|
+
var CHART_MERMAID_META_PREFIX = "%% @pixerate ";
|
|
35
|
+
var quote = (text) => `"${text.replace(/"/g, "#quot;")}"`;
|
|
36
|
+
var unquote = (text) => {
|
|
37
|
+
const t = text.trim();
|
|
38
|
+
return (t.length >= 2 && t.startsWith('"') && t.endsWith('"') ? t.slice(1, -1) : t).replace(/#quot;/g, '"');
|
|
39
|
+
};
|
|
40
|
+
var word = (text) => /^[A-Za-z0-9_]+$/.test(text) ? text : quote(text);
|
|
41
|
+
var num = (value) => Number.isInteger(value) ? String(value) : String(Number(value.toPrecision(10)));
|
|
42
|
+
function splitOutside(text, sep) {
|
|
43
|
+
const out = [];
|
|
44
|
+
let cur = "";
|
|
45
|
+
let quoted = false;
|
|
46
|
+
let depth = 0;
|
|
47
|
+
for (const ch of text) {
|
|
48
|
+
if (ch === '"') quoted = !quoted;
|
|
49
|
+
else if (!quoted && (ch === "[" || ch === "{")) depth++;
|
|
50
|
+
else if (!quoted && (ch === "]" || ch === "}")) depth--;
|
|
51
|
+
if (ch === sep && !quoted && depth === 0) {
|
|
52
|
+
out.push(cur);
|
|
53
|
+
cur = "";
|
|
54
|
+
} else cur += ch;
|
|
55
|
+
}
|
|
56
|
+
out.push(cur);
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
function chartMermaidFamily(spec) {
|
|
60
|
+
const family = _chunkJOX6MJW4cjs.chartFamily.call(void 0, spec);
|
|
61
|
+
if (family === "pie" || family === "funnel" || family === "treemap") return "pie";
|
|
62
|
+
if (family === "radar") return "radar";
|
|
63
|
+
return "xychart";
|
|
64
|
+
}
|
|
65
|
+
function slug(label, i, taken) {
|
|
66
|
+
let base = label.normalize("NFKD").replace(/[^\w-]+/g, "_").replace(/^_+|_+$/g, "") || `a${i + 1}`;
|
|
67
|
+
if (!/^[A-Za-z_]/.test(base)) base = `a_${base}`;
|
|
68
|
+
let id = base;
|
|
69
|
+
for (let k = 2; taken.has(id); k++) id = `${base}_${k}`;
|
|
70
|
+
taken.add(id);
|
|
71
|
+
return id;
|
|
72
|
+
}
|
|
73
|
+
function metaFor(spec, text) {
|
|
74
|
+
const base = chartFromMermaid(text, { ignoreMeta: true }).spec;
|
|
75
|
+
const meta = {};
|
|
76
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
77
|
+
for (const key of Object.keys(spec)) {
|
|
78
|
+
if (key === "series" || key === "version") continue;
|
|
79
|
+
if (!same(spec[key], base[key])) meta[key] = spec[key];
|
|
80
|
+
}
|
|
81
|
+
const series = spec.series.map((s, i) => {
|
|
82
|
+
const b = base.series[i];
|
|
83
|
+
const out = {};
|
|
84
|
+
for (const [k, v] of Object.entries(s)) {
|
|
85
|
+
if (k === "values") {
|
|
86
|
+
if (b && !same(v, b.values)) out.values = v;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!b || !same(v, b[k])) out[k] = v;
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
});
|
|
93
|
+
if (series.some((s) => Object.keys(s).length) || spec.series.length !== base.series.length) meta.series = series;
|
|
94
|
+
return Object.keys(meta).length ? meta : null;
|
|
95
|
+
}
|
|
96
|
+
function chartToMermaid(spec, options = {}) {
|
|
97
|
+
const ind = _nullishCoalesce(options.indent, () => ( " "));
|
|
98
|
+
const family = chartMermaidFamily(spec);
|
|
99
|
+
const visible = spec.series.filter((s) => !s.hidden);
|
|
100
|
+
const lines = [];
|
|
101
|
+
const v = (x) => x === null || !Number.isFinite(x) ? "0" : num(x);
|
|
102
|
+
if (family === "pie") {
|
|
103
|
+
const s = _nullishCoalesce(visible[0], () => ( spec.series[0]));
|
|
104
|
+
lines.push(`pie${spec.dataLabels ? " showData" : ""}`);
|
|
105
|
+
if (spec.title) lines.push(`${ind}title ${spec.title}`);
|
|
106
|
+
spec.categories.forEach((c, i) => {
|
|
107
|
+
const value = _optionalChain([s, 'optionalAccess', _2 => _2.values, 'access', _3 => _3[i]]);
|
|
108
|
+
if (value !== null && value !== void 0 && Number.isFinite(value) && value > 0) lines.push(`${ind}${quote(c)} : ${num(value)}`);
|
|
109
|
+
});
|
|
110
|
+
} else if (family === "radar") {
|
|
111
|
+
lines.push("radar-beta");
|
|
112
|
+
if (spec.title) lines.push(`${ind}title ${spec.title}`);
|
|
113
|
+
const taken = /* @__PURE__ */ new Set();
|
|
114
|
+
const ids = spec.categories.map((c, i) => slug(c, i, taken));
|
|
115
|
+
lines.push(`${ind}axis ${ids.map((id, i) => id === spec.categories[i] ? id : `${id}["${spec.categories[i].replace(/"/g, "'")}"]`).join(", ")}`);
|
|
116
|
+
const seriesTaken = new Set(ids);
|
|
117
|
+
for (const [i, s] of spec.series.entries()) {
|
|
118
|
+
const id = slug(s.id, i, seriesTaken);
|
|
119
|
+
lines.push(`${ind}curve ${id}["${s.name.replace(/"/g, "'")}"]{${s.values.map(v).join(", ")}}`);
|
|
120
|
+
}
|
|
121
|
+
if (spec.yAxis.max !== null) lines.push(`${ind}max ${num(spec.yAxis.max)}`);
|
|
122
|
+
if (spec.yAxis.min !== null) lines.push(`${ind}min ${num(spec.yAxis.min)}`);
|
|
123
|
+
if (!spec.legend.show) lines.push(`${ind}showLegend false`);
|
|
124
|
+
} else {
|
|
125
|
+
lines.push(`xychart-beta${spec.orientation === "horizontal" ? " horizontal" : ""}`);
|
|
126
|
+
if (spec.title) lines.push(`${ind}title ${quote(spec.title)}`);
|
|
127
|
+
const xTitle = spec.xAxis.title ? ` ${quote(spec.xAxis.title)}` : "";
|
|
128
|
+
lines.push(`${ind}x-axis${xTitle} [${spec.categories.map(word).join(", ")}]`);
|
|
129
|
+
const y = spec.yAxis;
|
|
130
|
+
if (y.title || y.min !== null || y.max !== null) {
|
|
131
|
+
let out = `${ind}y-axis`;
|
|
132
|
+
if (y.title) out += ` ${quote(y.title)}`;
|
|
133
|
+
if (y.min !== null || y.max !== null) {
|
|
134
|
+
const all = spec.series.flatMap((s) => s.values).filter((x) => x !== null && Number.isFinite(x));
|
|
135
|
+
const lo = _nullishCoalesce(y.min, () => ( Math.min(0, ...all)));
|
|
136
|
+
const hi = _nullishCoalesce(y.max, () => ( Math.max(lo + 1, ...all)));
|
|
137
|
+
out += ` ${num(lo)} --> ${num(hi)}`;
|
|
138
|
+
}
|
|
139
|
+
lines.push(out);
|
|
140
|
+
}
|
|
141
|
+
for (const s of spec.series) {
|
|
142
|
+
const kind = (_nullishCoalesce(s.type, () => ( (spec.type === "bar" ? "bar" : "line")))) === "bar" ? "bar" : "line";
|
|
143
|
+
lines.push(`${ind}${kind} ${quote(s.name)} [${s.values.map(v).join(", ")}]`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
let text = `${lines.join("\n")}
|
|
147
|
+
`;
|
|
148
|
+
if (options.meta !== false) {
|
|
149
|
+
const meta = metaFor(spec, text);
|
|
150
|
+
if (meta) {
|
|
151
|
+
const [header, ...rest] = lines;
|
|
152
|
+
text = `${[header, `${CHART_MERMAID_META_PREFIX}${JSON.stringify({ v: 1, ...meta })}`, ...rest].join("\n")}
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return text;
|
|
157
|
+
}
|
|
158
|
+
var RANGE = /^(-?\d+(?:\.\d+)?)\s*-->\s*(-?\d+(?:\.\d+)?)$/;
|
|
159
|
+
function takeTitle(text) {
|
|
160
|
+
const t = text.trim();
|
|
161
|
+
if (t.startsWith('"')) {
|
|
162
|
+
const end = t.indexOf('"', 1);
|
|
163
|
+
if (end > 0) return { title: unquote(t.slice(0, end + 1)), rest: t.slice(end + 1).trim() };
|
|
164
|
+
}
|
|
165
|
+
if (!t || t.startsWith("[") || RANGE.test(t) || /^-?\d/.test(t)) return { rest: t };
|
|
166
|
+
const m = /^(\S+)\s*(.*)$/.exec(t);
|
|
167
|
+
return { title: m[1], rest: m[2].trim() };
|
|
168
|
+
}
|
|
169
|
+
function parseList(text, partial) {
|
|
170
|
+
const t = text.trim();
|
|
171
|
+
let m = /^\[(.*)\]$/s.exec(t);
|
|
172
|
+
if (!m && partial) m = /^\[(.*)$/s.exec(t);
|
|
173
|
+
if (!m) return void 0;
|
|
174
|
+
if (!m[1].trim()) return [];
|
|
175
|
+
const items = splitOutside(m[1], ",").map((x) => x.trim());
|
|
176
|
+
if (partial && !/\]$/.test(t)) {
|
|
177
|
+
const last = items[items.length - 1];
|
|
178
|
+
if (!last || last.startsWith('"') && !/"$/.test(last.slice(1))) items.pop();
|
|
179
|
+
}
|
|
180
|
+
return items.map(unquote);
|
|
181
|
+
}
|
|
182
|
+
function chartFromMermaid(text, options = {}) {
|
|
183
|
+
const issues = [];
|
|
184
|
+
const partial = !!options.partial;
|
|
185
|
+
const all = text.replace(/^/, "").split(/\r?\n/);
|
|
186
|
+
let start = 0;
|
|
187
|
+
let frontTitle;
|
|
188
|
+
if (_optionalChain([all, 'access', _4 => _4[0], 'optionalAccess', _5 => _5.trim, 'call', _6 => _6()]) === "---") {
|
|
189
|
+
const end = all.findIndex((l, i) => i > 0 && l.trim() === "---");
|
|
190
|
+
if (end > 0) {
|
|
191
|
+
for (const l of all.slice(1, end)) {
|
|
192
|
+
const m = /^title:\s*(.+)$/.exec(l.trim());
|
|
193
|
+
if (m) frontTitle = unquote(m[1]);
|
|
194
|
+
}
|
|
195
|
+
start = end + 1;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
let family = null;
|
|
199
|
+
let headerLine = -1;
|
|
200
|
+
let horizontal = false;
|
|
201
|
+
let showData = false;
|
|
202
|
+
let meta = null;
|
|
203
|
+
for (let i = start; i < all.length; i++) {
|
|
204
|
+
const t = all[i].trim();
|
|
205
|
+
if (!t || t.startsWith("%%")) continue;
|
|
206
|
+
let m;
|
|
207
|
+
if (m = /^xychart(?:-beta)?(?:\s+(horizontal|vertical))?\s*$/i.exec(t)) {
|
|
208
|
+
family = "xychart";
|
|
209
|
+
horizontal = _optionalChain([m, 'access', _7 => _7[1], 'optionalAccess', _8 => _8.toLowerCase, 'call', _9 => _9()]) === "horizontal";
|
|
210
|
+
} else if (m = /^pie(\s+showData)?(?:\s+title\s+(.*))?\s*$/i.exec(t)) {
|
|
211
|
+
family = "pie";
|
|
212
|
+
showData = !!m[1];
|
|
213
|
+
if (m[2]) frontTitle = m[2].trim();
|
|
214
|
+
} else if (/^radar(?:-beta)?\s*$/i.test(t)) family = "radar";
|
|
215
|
+
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)}".` });
|
|
216
|
+
headerLine = i;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
let title = _nullishCoalesce(frontTitle, () => ( ""));
|
|
220
|
+
const categories = [];
|
|
221
|
+
const series = [];
|
|
222
|
+
const xAxis = _chunkJOX6MJW4cjs.defaultChartAxis.call(void 0, false);
|
|
223
|
+
const yAxis = _chunkJOX6MJW4cjs.defaultChartAxis.call(void 0, true);
|
|
224
|
+
const radarAxes = [];
|
|
225
|
+
const pieSlices = [];
|
|
226
|
+
let legendShow = true;
|
|
227
|
+
const counts = { bar: 0, line: 0 };
|
|
228
|
+
const lineIssue = (i, message) => issues.push({ path: `line ${i + 1}`, line: i + 1, message: `Line ${i + 1}: ${message}` });
|
|
229
|
+
for (let i = headerLine + 1; family && i < all.length; i++) {
|
|
230
|
+
const raw = all[i];
|
|
231
|
+
const t = raw.trim();
|
|
232
|
+
if (!t) continue;
|
|
233
|
+
if (t.startsWith(CHART_MERMAID_META_PREFIX.trim())) {
|
|
234
|
+
if (options.ignoreMeta) continue;
|
|
235
|
+
try {
|
|
236
|
+
const parsed = JSON.parse(t.slice(CHART_MERMAID_META_PREFIX.trim().length).trim());
|
|
237
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) meta = parsed;
|
|
238
|
+
else lineIssue(i, "the @pixerate metadata is not a JSON object; it was ignored.");
|
|
239
|
+
} catch (e2) {
|
|
240
|
+
if (!partial) lineIssue(i, "the @pixerate metadata is not valid JSON; it was ignored.");
|
|
241
|
+
}
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (t.startsWith("%%")) continue;
|
|
245
|
+
const isLast = i === all.length - 1 || all.slice(i + 1).every((l) => !l.trim());
|
|
246
|
+
let m;
|
|
247
|
+
if (m = /^title\s+(.+)$/i.exec(t)) {
|
|
248
|
+
title = unquote(m[1]);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (family === "xychart") {
|
|
252
|
+
if (m = /^x-axis\b(.*)$/i.exec(t)) {
|
|
253
|
+
const { title: xt, rest } = takeTitle(m[1]);
|
|
254
|
+
if (xt) xAxis.title = xt;
|
|
255
|
+
const list = parseList(rest, partial && isLast);
|
|
256
|
+
if (list) categories.splice(0, categories.length, ...list);
|
|
257
|
+
else if (RANGE.test(rest)) {
|
|
258
|
+
const r = RANGE.exec(rest);
|
|
259
|
+
xAxis.min = Number(r[1]);
|
|
260
|
+
xAxis.max = Number(r[2]);
|
|
261
|
+
} else if (rest) lineIssue(i, `expected x-axis [a, b, \u2026] or a range "min --> max", got "${rest.slice(0, 40)}".`);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (m = /^y-axis\b(.*)$/i.exec(t)) {
|
|
265
|
+
const { title: yt, rest } = takeTitle(m[1]);
|
|
266
|
+
if (yt) yAxis.title = yt;
|
|
267
|
+
const r = RANGE.exec(rest);
|
|
268
|
+
if (r) {
|
|
269
|
+
yAxis.min = Number(r[1]);
|
|
270
|
+
yAxis.max = Number(r[2]);
|
|
271
|
+
} else if (rest) lineIssue(i, `expected y-axis "min --> max", got "${rest.slice(0, 40)}".`);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (m = /^(bar|line)\b(.*)$/i.exec(t)) {
|
|
275
|
+
const kind = m[1].toLowerCase();
|
|
276
|
+
const { title: st, rest } = takeTitle(m[2]);
|
|
277
|
+
const list = parseList(rest, partial && isLast);
|
|
278
|
+
if (!list) {
|
|
279
|
+
if (!(partial && isLast)) lineIssue(i, `expected ${kind} [1, 2, 3], got "${rest.slice(0, 40)}".`);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const values = list.map((x) => x.trim() === "" ? null : Number(x));
|
|
283
|
+
if (values.some((x) => x !== null && !Number.isFinite(x))) {
|
|
284
|
+
lineIssue(i, `${kind} values must be numbers.`);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
counts[kind]++;
|
|
288
|
+
series.push({ id: `series-${series.length + 1}`, name: _nullishCoalesce(st, () => ( `${kind === "bar" ? "Bar" : "Line"} ${counts[kind]}`)), values, type: kind });
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
} else if (family === "pie") {
|
|
292
|
+
if (m = /^"((?:[^"\\]|\\.)*)"\s*:\s*(-?\d+(?:\.\d+)?)\s*$/.exec(t)) {
|
|
293
|
+
pieSlices.push({ label: m[1].replace(/#quot;/g, '"'), value: Number(m[2]) });
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (/^showData\s*$/i.test(t)) {
|
|
297
|
+
showData = true;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
} else if (family === "radar") {
|
|
301
|
+
if (m = /^axis\s+(.+)$/.exec(t)) {
|
|
302
|
+
for (const part of splitOutside(m[1], ",")) {
|
|
303
|
+
const a = /^([A-Za-z_][\w-]*)\s*(?:\[\s*"((?:[^"\\]|\\.)*)"\s*\])?$/.exec(part.trim());
|
|
304
|
+
if (a) radarAxes.push({ id: a[1], label: _nullishCoalesce(a[2], () => ( a[1])) });
|
|
305
|
+
else lineIssue(i, `could not read the axis "${part.trim()}".`);
|
|
306
|
+
}
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (m = /^curve\s+([A-Za-z_][\w-]*)\s*(?:\[\s*"((?:[^"\\]|\\.)*)"\s*\])?\s*\{(.*)\}?\s*$/.exec(t)) {
|
|
310
|
+
const body = m[3].replace(/\}\s*$/, "");
|
|
311
|
+
const entries = body.trim() ? splitOutside(body, ",").map((x) => x.trim()).filter(Boolean) : [];
|
|
312
|
+
const ids = radarAxes.map((a) => a.id);
|
|
313
|
+
const values = ids.map(() => null);
|
|
314
|
+
let k = 0;
|
|
315
|
+
for (const e of entries) {
|
|
316
|
+
const keyed = /^([A-Za-z_][\w-]*)\s*:\s*(-?\d+(?:\.\d+)?)$/.exec(e);
|
|
317
|
+
if (keyed) {
|
|
318
|
+
const at = ids.indexOf(keyed[1]);
|
|
319
|
+
if (at >= 0) values[at] = Number(keyed[2]);
|
|
320
|
+
else lineIssue(i, `curve ${m[1]} names an unknown axis "${keyed[1]}".`);
|
|
321
|
+
} else if (/^-?\d+(?:\.\d+)?$/.test(e)) {
|
|
322
|
+
if (k < values.length) values[k] = Number(e);
|
|
323
|
+
else values.push(Number(e));
|
|
324
|
+
k++;
|
|
325
|
+
} else lineIssue(i, `could not read "${e}" in curve ${m[1]}.`);
|
|
326
|
+
}
|
|
327
|
+
series.push({ id: m[1], name: _nullishCoalesce(m[2], () => ( m[1])), values });
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (m = /^(max|min|ticks|graticule|showLegend)\s+(\S+)$/.exec(t)) {
|
|
331
|
+
if (m[1] === "max") yAxis.max = Number(m[2]);
|
|
332
|
+
else if (m[1] === "min") yAxis.min = Number(m[2]);
|
|
333
|
+
else if (m[1] === "showLegend") legendShow = m[2] !== "false";
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (!(partial && isLast)) lineIssue(i, `could not read "${t.slice(0, 60)}"; it was skipped.`);
|
|
338
|
+
}
|
|
339
|
+
let input;
|
|
340
|
+
if (family === "pie") {
|
|
341
|
+
input = {
|
|
342
|
+
type: "pie",
|
|
343
|
+
title,
|
|
344
|
+
dataLabels: showData,
|
|
345
|
+
categories: pieSlices.map((s) => s.label),
|
|
346
|
+
series: [{ id: "series-1", name: title || "Values", values: pieSlices.map((s) => s.value) }]
|
|
347
|
+
};
|
|
348
|
+
} else if (family === "radar") {
|
|
349
|
+
input = { type: "radar", title, categories: radarAxes.map((a) => a.label), series, yAxis, legend: { show: legendShow, position: "bottom" } };
|
|
350
|
+
} else {
|
|
351
|
+
const allBar = series.length > 0 && series.every((s) => s.type === "bar");
|
|
352
|
+
const allLine = series.length > 0 && series.every((s) => s.type === "line");
|
|
353
|
+
const type = allLine ? "line" : "bar";
|
|
354
|
+
const longest = Math.max(0, ...series.map((s) => s.values.length));
|
|
355
|
+
const cats = categories.length ? categories : Array.from({ length: longest }, (_, i) => `${i + 1}`);
|
|
356
|
+
input = {
|
|
357
|
+
type,
|
|
358
|
+
title,
|
|
359
|
+
orientation: horizontal ? "horizontal" : "vertical",
|
|
360
|
+
categories: cats,
|
|
361
|
+
xAxis,
|
|
362
|
+
yAxis,
|
|
363
|
+
// Series that match the chart type do not need their own type.
|
|
364
|
+
series: series.map((s) => allBar || allLine || s.type === type ? { ...s, type: void 0 } : s)
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
if (meta) {
|
|
368
|
+
const { v: _v, series: metaSeries, ...rest } = meta;
|
|
369
|
+
Object.assign(input, rest);
|
|
370
|
+
if (Array.isArray(metaSeries)) {
|
|
371
|
+
const base = _nullishCoalesce(input.series, () => ( []));
|
|
372
|
+
input.series = metaSeries.map((extra, i) => ({ ..._nullishCoalesce(base[i], () => ( {})), ..._nullishCoalesce(extra, () => ( {})) }));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const { spec, issues: specIssues } = _chunkJOX6MJW4cjs.normalizeChartSpec.call(void 0, input);
|
|
376
|
+
if (!family && !partial && !issues.length) issues.push({ path: "header", message: "The text is empty: expected a chart header (xychart-beta, pie or radar-beta)." });
|
|
377
|
+
return { spec, issues: [...issues, ...specIssues], family, hasMeta: !!meta };
|
|
378
|
+
}
|
|
379
|
+
function findMermaidCharts(text) {
|
|
380
|
+
const lines = text.replace(/^/, "").split(/\r?\n/);
|
|
381
|
+
const found = [];
|
|
382
|
+
const familyOf = (body) => {
|
|
383
|
+
const header = body.split(/\r?\n/).find((l) => l.trim() && !l.trim().startsWith("%%") && l.trim() !== "---");
|
|
384
|
+
if (!header) return null;
|
|
385
|
+
if (/^\s*xychart(-beta)?\b/.test(header)) return "xychart";
|
|
386
|
+
if (/^\s*pie\b/.test(header)) return "pie";
|
|
387
|
+
if (/^\s*radar(-beta)?\b/.test(header)) return "radar";
|
|
388
|
+
return null;
|
|
389
|
+
};
|
|
390
|
+
for (let i = 0; i < lines.length; i++) {
|
|
391
|
+
const open = /^ {0,3}(`{3,}|~{3,})\s*([\w-]*)/.exec(lines[i]);
|
|
392
|
+
if (!open) continue;
|
|
393
|
+
const fence = open[1];
|
|
394
|
+
const info = open[2].toLowerCase();
|
|
395
|
+
let end = i + 1;
|
|
396
|
+
while (end < lines.length && !new RegExp(`^ {0,3}${fence[0]}{${fence.length},}\\s*$`).test(lines[end])) end++;
|
|
397
|
+
const body = lines.slice(i + 1, end).join("\n");
|
|
398
|
+
if (!info || info === "mermaid") {
|
|
399
|
+
const family = familyOf(body);
|
|
400
|
+
if (family) found.push({ source: `${body.trimEnd()}
|
|
401
|
+
`, family, line: i + 2 });
|
|
402
|
+
}
|
|
403
|
+
i = end;
|
|
404
|
+
}
|
|
405
|
+
if (!found.length) {
|
|
406
|
+
const family = familyOf(text);
|
|
407
|
+
if (family) found.push({ source: `${text.trim()}
|
|
408
|
+
`, family, line: 1 });
|
|
409
|
+
}
|
|
410
|
+
return found;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// src/chart/date.ts
|
|
414
|
+
var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
415
|
+
function parseChartDate(label) {
|
|
416
|
+
const text = label.trim();
|
|
417
|
+
let m = /^(\d{4})$/.exec(text);
|
|
418
|
+
if (m) return Date.UTC(Number(m[1]), 0, 1);
|
|
419
|
+
m = /^(\d{4})[-/](\d{1,2})$/.exec(text);
|
|
420
|
+
if (m) return valid(Number(m[1]), Number(m[2]) - 1, 1);
|
|
421
|
+
m = /^(\d{4})[-/](\d{1,2})[-/](\d{1,2})(?:[T ](\d{1,2}):(\d{2})(?::(\d{2}))?(?:\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$/.exec(text);
|
|
422
|
+
if (m) {
|
|
423
|
+
if (m[4] !== void 0) {
|
|
424
|
+
const t = Date.parse(text.replace(" ", "T").replace(/\//g, "-") + (m[7] ? "" : "Z"));
|
|
425
|
+
return Number.isFinite(t) ? t : null;
|
|
426
|
+
}
|
|
427
|
+
return valid(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
428
|
+
}
|
|
429
|
+
m = /^(?:(\d{1,2})\s+)?([A-Za-z]{3})[a-z]*\.?,?\s+(\d{4})$/.exec(text);
|
|
430
|
+
if (m) {
|
|
431
|
+
const month = MONTHS.findIndex((x) => x.toLowerCase() === m[2].toLowerCase());
|
|
432
|
+
if (month >= 0) return valid(Number(m[3]), month, m[1] ? Number(m[1]) : 1);
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
function valid(y, mo, d) {
|
|
437
|
+
const t = Date.UTC(y, mo, d);
|
|
438
|
+
const date = new Date(t);
|
|
439
|
+
return date.getUTCFullYear() === y && date.getUTCMonth() === mo && date.getUTCDate() === d ? t : null;
|
|
440
|
+
}
|
|
441
|
+
var CHART_DATE_STEPS = [
|
|
442
|
+
{ unit: "day", count: 1 },
|
|
443
|
+
{ unit: "day", count: 2 },
|
|
444
|
+
{ unit: "week", count: 1 },
|
|
445
|
+
{ unit: "week", count: 2 },
|
|
446
|
+
{ unit: "month", count: 1 },
|
|
447
|
+
{ unit: "month", count: 3 },
|
|
448
|
+
{ unit: "month", count: 6 },
|
|
449
|
+
{ unit: "year", count: 1 },
|
|
450
|
+
{ unit: "year", count: 2 },
|
|
451
|
+
{ unit: "year", count: 5 },
|
|
452
|
+
{ unit: "year", count: 10 },
|
|
453
|
+
{ unit: "year", count: 25 },
|
|
454
|
+
{ unit: "year", count: 50 },
|
|
455
|
+
{ unit: "year", count: 100 }
|
|
456
|
+
];
|
|
457
|
+
function bucket(t, step) {
|
|
458
|
+
const d = new Date(t);
|
|
459
|
+
const y = d.getUTCFullYear();
|
|
460
|
+
switch (step.unit) {
|
|
461
|
+
case "day":
|
|
462
|
+
return Math.floor(Math.floor(t / 864e5) / step.count);
|
|
463
|
+
case "week":
|
|
464
|
+
return Math.floor(Math.floor((t / 864e5 - 4) / 7) / step.count);
|
|
465
|
+
case "month":
|
|
466
|
+
return Math.floor((y * 12 + d.getUTCMonth()) / step.count);
|
|
467
|
+
case "year":
|
|
468
|
+
return Math.floor(y / step.count);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function formatChartDate(t, step, previous = null) {
|
|
472
|
+
const d = new Date(t);
|
|
473
|
+
const y = d.getUTCFullYear();
|
|
474
|
+
const mo = MONTHS[d.getUTCMonth()];
|
|
475
|
+
const sameYear = previous !== null && new Date(previous).getUTCFullYear() === y;
|
|
476
|
+
if (step.unit === "year") return String(y);
|
|
477
|
+
if (step.unit === "month") return sameYear ? mo : `${mo} ${y}`;
|
|
478
|
+
return sameYear ? `${d.getUTCDate()} ${mo}` : `${d.getUTCDate()} ${mo} ${y}`;
|
|
479
|
+
}
|
|
480
|
+
function chartDateLabels(categories, band, measure, gap = 10) {
|
|
481
|
+
const times = categories.map(parseChartDate);
|
|
482
|
+
if (times.filter((t) => t !== null).length < Math.max(1, categories.length / 2)) return null;
|
|
483
|
+
const present = times.filter((t) => t !== null).sort((x, y) => x - y);
|
|
484
|
+
const gaps = present.slice(1).map((t, i) => t - present[i]).filter((g) => g > 0).sort((x, y) => x - y);
|
|
485
|
+
const spacing = gaps.length ? gaps[Math.floor(gaps.length / 2)] : 0;
|
|
486
|
+
const DAY = 864e5;
|
|
487
|
+
const length = (step) => step.count * (step.unit === "day" ? DAY : step.unit === "week" ? 7 * DAY : step.unit === "month" ? 30 * DAY : 365 * DAY);
|
|
488
|
+
for (const step of CHART_DATE_STEPS) {
|
|
489
|
+
if (length(step) < spacing * 0.5 && step !== CHART_DATE_STEPS[CHART_DATE_STEPS.length - 1]) continue;
|
|
490
|
+
const indexes = [];
|
|
491
|
+
const labels = /* @__PURE__ */ new Map();
|
|
492
|
+
let last = null;
|
|
493
|
+
let lastTime = null;
|
|
494
|
+
times.forEach((t, i) => {
|
|
495
|
+
if (t === null) return;
|
|
496
|
+
const b = bucket(t, step);
|
|
497
|
+
if (b === last) return;
|
|
498
|
+
last = b;
|
|
499
|
+
indexes.push(i);
|
|
500
|
+
labels.set(i, formatChartDate(t, step, lastTime));
|
|
501
|
+
lastTime = t;
|
|
502
|
+
});
|
|
503
|
+
let fits = true;
|
|
504
|
+
for (let k = 1; k < indexes.length && fits; k++) {
|
|
505
|
+
const a = labels.get(indexes[k - 1]);
|
|
506
|
+
const b = labels.get(indexes[k]);
|
|
507
|
+
if ((indexes[k] - indexes[k - 1]) * band < (measure(a) + measure(b)) / 2 + gap) fits = false;
|
|
508
|
+
}
|
|
509
|
+
if (fits || step === CHART_DATE_STEPS[CHART_DATE_STEPS.length - 1]) return { step, indexes, labels };
|
|
510
|
+
}
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/chart/numfmt.ts
|
|
515
|
+
function compileChartFormatCode(code) {
|
|
516
|
+
const fmt = _chunkXP5IUZUOcjs.NumberFormat.parseCached(code);
|
|
517
|
+
return {
|
|
518
|
+
isDate: fmt.isDate(),
|
|
519
|
+
isPercent: fmt.isPercent(),
|
|
520
|
+
format: (value) => Number.isFinite(value) ? fmt.formatNumber(value).text : ""
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function formatChartValueWithCode(value, code) {
|
|
524
|
+
return compileChartFormatCode(code).format(value);
|
|
525
|
+
}
|
|
526
|
+
function chartSerialToDate(serial) {
|
|
527
|
+
const origin = serial < 61 ? Date.UTC(1899, 11, 31) : Date.UTC(1899, 11, 30);
|
|
528
|
+
return new Date(origin + Math.round(serial * 864e5));
|
|
529
|
+
}
|
|
530
|
+
function chartDateToSerial(date) {
|
|
531
|
+
const serial = (date.getTime() - Date.UTC(1899, 11, 30)) / 864e5;
|
|
532
|
+
return serial < 61 ? serial - 1 : serial;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/chart/scale.ts
|
|
536
|
+
function niceStep(span, count) {
|
|
537
|
+
if (!(span > 0) || !(count > 0)) return 1;
|
|
538
|
+
const raw = span / count;
|
|
539
|
+
const power = Math.pow(10, Math.floor(Math.log10(raw)));
|
|
540
|
+
const fraction = raw / power;
|
|
541
|
+
const nice = fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 2.5 ? 2.5 : fraction <= 5 ? 5 : 10;
|
|
542
|
+
return nice * power;
|
|
543
|
+
}
|
|
544
|
+
var clean = (n) => Math.abs(n) < 1e-12 ? 0 : Number(n.toPrecision(12));
|
|
545
|
+
function niceScale(min, max, count = 5, fixedMin = null, fixedMax = null) {
|
|
546
|
+
if (fixedMin !== null && !Number.isFinite(fixedMin)) fixedMin = null;
|
|
547
|
+
if (fixedMax !== null && !Number.isFinite(fixedMax)) fixedMax = null;
|
|
548
|
+
let lo = _nullishCoalesce(fixedMin, () => ( min));
|
|
549
|
+
let hi = _nullishCoalesce(fixedMax, () => ( max));
|
|
550
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi)) {
|
|
551
|
+
fixedMin = fixedMax = null;
|
|
552
|
+
lo = 0;
|
|
553
|
+
hi = 1;
|
|
554
|
+
}
|
|
555
|
+
if (lo > hi) [lo, hi] = [hi, lo];
|
|
556
|
+
if (lo === hi) {
|
|
557
|
+
const pad = lo === 0 ? 1 : Math.abs(lo) * 0.1;
|
|
558
|
+
if (fixedMin === null && lo !== 0) lo -= pad;
|
|
559
|
+
if (fixedMax === null || lo === hi) hi += pad;
|
|
560
|
+
}
|
|
561
|
+
const span = hi - lo;
|
|
562
|
+
const step = Number.isFinite(span) ? niceStep(span, count) : niceStep(hi / 2 - lo / 2, count) * 2;
|
|
563
|
+
const finiteOr = (n2, fallback) => Number.isFinite(n2) ? n2 : fallback;
|
|
564
|
+
const niceMin = _nullishCoalesce(fixedMin, () => ( finiteOr(snap(Math.floor(lo / step + 1e-9) * step, step), lo)));
|
|
565
|
+
const niceMax = _nullishCoalesce(fixedMax, () => ( finiteOr(snap(Math.ceil(hi / step - 1e-9) * step, step), hi)));
|
|
566
|
+
const firstIndex = Math.ceil(niceMin / step - 1e-9);
|
|
567
|
+
const lastIndex = Math.floor(niceMax / step + 1e-9);
|
|
568
|
+
const n = Math.min(999, Math.max(-1, lastIndex - firstIndex));
|
|
569
|
+
const ticks = [];
|
|
570
|
+
for (let i = 0; i <= n; i++) {
|
|
571
|
+
const t = snap((firstIndex + i) * step, step);
|
|
572
|
+
if (Number.isFinite(t) && t !== ticks[ticks.length - 1]) ticks.push(t);
|
|
573
|
+
}
|
|
574
|
+
return { min: niceMin, max: niceMax, step, ticks };
|
|
575
|
+
}
|
|
576
|
+
function snap(n, step) {
|
|
577
|
+
const c = clean(n);
|
|
578
|
+
return Math.abs(c - n) <= step * 1e-6 ? c : n;
|
|
579
|
+
}
|
|
580
|
+
function stepDecimals(step) {
|
|
581
|
+
if (!(step > 0) || !Number.isFinite(step)) return 0;
|
|
582
|
+
for (let d = 0; d < 6; d++) {
|
|
583
|
+
const scaled = step * Math.pow(10, d);
|
|
584
|
+
if (Math.abs(scaled - Math.round(scaled)) < 1e-9) return d;
|
|
585
|
+
}
|
|
586
|
+
return 6;
|
|
587
|
+
}
|
|
588
|
+
function magnitudeDecimals(value) {
|
|
589
|
+
if (!(value > 0) || value >= 1) return 0;
|
|
590
|
+
return Math.min(6, Math.max(0, Math.ceil(-Math.log10(value) - 1e-9)));
|
|
591
|
+
}
|
|
592
|
+
var COMPACT = [
|
|
593
|
+
{ value: 1e12, suffix: "T" },
|
|
594
|
+
{ value: 1e9, suffix: "B" },
|
|
595
|
+
{ value: 1e6, suffix: "M" },
|
|
596
|
+
{ value: 1e3, suffix: "K" }
|
|
597
|
+
];
|
|
598
|
+
function grouped(value, decimals) {
|
|
599
|
+
const fixed = Math.abs(value).toFixed(decimals);
|
|
600
|
+
const [int, frac] = fixed.split(".");
|
|
601
|
+
const withCommas = int.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
602
|
+
return `${value < 0 && Number(fixed) !== 0 ? "-" : ""}${withCommas}${frac ? `.${frac}` : ""}`;
|
|
603
|
+
}
|
|
604
|
+
var CURRENCY_SYMBOLS = { USD: "$", EUR: "\u20AC", GBP: "\xA3", JPY: "\xA5", CNY: "\xA5", INR: "\u20B9", AUD: "A$", CAD: "C$" };
|
|
605
|
+
function formatChartNumber(value, format = {}, fallbackDecimals) {
|
|
606
|
+
if (value === null || value === void 0 || !Number.isFinite(value)) return "";
|
|
607
|
+
if (format.code) return `${_nullishCoalesce(format.prefix, () => ( ""))}${formatChartValueWithCode(value, format.code)}${_nullishCoalesce(format.suffix, () => ( ""))}`;
|
|
608
|
+
const style = _nullishCoalesce(format.style, () => ( "number"));
|
|
609
|
+
let body;
|
|
610
|
+
const decimalsFor = (v) => {
|
|
611
|
+
if (format.decimals !== void 0) return format.decimals;
|
|
612
|
+
if (fallbackDecimals !== void 0) return fallbackDecimals;
|
|
613
|
+
return Number.isInteger(v) ? 0 : Math.abs(v) < 1 ? Math.min(4, Math.max(2, magnitudeDecimals(Math.abs(v)) + 1)) : 2;
|
|
614
|
+
};
|
|
615
|
+
const trim = (s) => format.decimals === void 0 && fallbackDecimals === void 0 && s.includes(".") ? s.replace(/\.?0+$/, "") : s;
|
|
616
|
+
if (style === "percent") {
|
|
617
|
+
const v = value * 100;
|
|
618
|
+
const d = _nullishCoalesce(format.decimals, () => ( (fallbackDecimals !== void 0 ? Math.max(0, fallbackDecimals - 2) : Number.isInteger(clean(v)) ? 0 : 1)));
|
|
619
|
+
body = `${grouped(clean(v), d)}%`;
|
|
620
|
+
} else if (style === "compact") {
|
|
621
|
+
const unit = COMPACT.find((u) => Math.abs(value) >= u.value);
|
|
622
|
+
if (unit) {
|
|
623
|
+
const v = value / unit.value;
|
|
624
|
+
const d = _nullishCoalesce(format.decimals, () => ( (Math.abs(v) >= 100 || Number.isInteger(clean(v)) ? 0 : 1)));
|
|
625
|
+
body = `${trim(grouped(v, d))}${unit.suffix}`;
|
|
626
|
+
} else body = trim(grouped(value, decimalsFor(value)));
|
|
627
|
+
} else if (style === "currency") {
|
|
628
|
+
const code = (format.currency || "USD").toUpperCase();
|
|
629
|
+
const symbol = _nullishCoalesce(CURRENCY_SYMBOLS[code], () => ( `${code} `));
|
|
630
|
+
const d = _nullishCoalesce(format.decimals, () => ( (fallbackDecimals !== void 0 ? fallbackDecimals : Number.isInteger(value) ? 0 : 2)));
|
|
631
|
+
const s = grouped(Math.abs(value), d);
|
|
632
|
+
body = `${value < 0 ? "-" : ""}${symbol}${s}`;
|
|
633
|
+
} else body = trim(grouped(value, decimalsFor(value)));
|
|
634
|
+
return `${_nullishCoalesce(format.prefix, () => ( ""))}${body}${_nullishCoalesce(format.suffix, () => ( ""))}`;
|
|
635
|
+
}
|
|
636
|
+
function linearMap(value, domain, range) {
|
|
637
|
+
const [d0, d1] = domain;
|
|
638
|
+
const [r0, r1] = range;
|
|
639
|
+
if (d1 === d0) return (r0 + r1) / 2;
|
|
640
|
+
return r0 + (value - d0) / (d1 - d0) * (r1 - r0);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/chart/layout-shared.ts
|
|
644
|
+
var estimateChartTextWidth = (text, size, weight = 400) => text.length * size * (weight >= 600 ? 0.6 : 0.56);
|
|
645
|
+
function sameChartSelection(a, b) {
|
|
646
|
+
if (!a || !b || a.kind !== b.kind) return false;
|
|
647
|
+
switch (a.kind) {
|
|
648
|
+
case "axis":
|
|
649
|
+
return a.axis === b.axis;
|
|
650
|
+
case "series":
|
|
651
|
+
return a.seriesId === b.seriesId;
|
|
652
|
+
case "point":
|
|
653
|
+
case "trendline":
|
|
654
|
+
return a.seriesId === b.seriesId && a.index === b.index;
|
|
655
|
+
case "category":
|
|
656
|
+
return a.index === b.index;
|
|
657
|
+
default:
|
|
658
|
+
return true;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
function contrastingTextColor(fill) {
|
|
662
|
+
let rgb = null;
|
|
663
|
+
const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(fill.trim());
|
|
664
|
+
if (hex) {
|
|
665
|
+
const h = hex[1].length === 3 ? [...hex[1]].map((c) => c + c).join("") : hex[1];
|
|
666
|
+
rgb = [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
|
|
667
|
+
} else {
|
|
668
|
+
const fn = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i.exec(fill.trim());
|
|
669
|
+
if (fn) rgb = fn.slice(1, 4).map(Number);
|
|
670
|
+
}
|
|
671
|
+
if (!rgb) return "#ffffff";
|
|
672
|
+
const luminance = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;
|
|
673
|
+
return luminance > 0.5 ? "#0f172a" : "#ffffff";
|
|
674
|
+
}
|
|
675
|
+
var r2 = (n) => Math.round(n * 100) / 100;
|
|
676
|
+
function curvePath(points, curve, map, moveFirst = true) {
|
|
677
|
+
if (!points.length) return "";
|
|
678
|
+
const xy = points.map(map);
|
|
679
|
+
let d = `${moveFirst ? "M" : "L"}${r2(xy[0][0])},${r2(xy[0][1])}`;
|
|
680
|
+
if (points.length === 1) return d;
|
|
681
|
+
if (curve === "step") {
|
|
682
|
+
for (let i = 1; i < points.length; i++) {
|
|
683
|
+
const mid = (points[i - 1].c + points[i].c) / 2;
|
|
684
|
+
const a = map({ c: mid, v: points[i - 1].v });
|
|
685
|
+
const b = map({ c: mid, v: points[i].v });
|
|
686
|
+
d += `L${r2(a[0])},${r2(a[1])}L${r2(b[0])},${r2(b[1])}L${r2(xy[i][0])},${r2(xy[i][1])}`;
|
|
687
|
+
}
|
|
688
|
+
return d;
|
|
689
|
+
}
|
|
690
|
+
if (curve === "smooth" && points.length > 2) {
|
|
691
|
+
const n = points.length;
|
|
692
|
+
const dc = points.slice(1).map((p, i) => p.c - points[i].c);
|
|
693
|
+
const slope = points.slice(1).map((p, i) => dc[i] === 0 ? 0 : (p.v - points[i].v) / dc[i]);
|
|
694
|
+
const tangent = points.map((_, i) => {
|
|
695
|
+
if (i === 0) return slope[0];
|
|
696
|
+
if (i === n - 1) return slope[n - 2];
|
|
697
|
+
const a = slope[i - 1];
|
|
698
|
+
const b = slope[i];
|
|
699
|
+
if (a * b <= 0) return 0;
|
|
700
|
+
const wa = 2 * dc[i] + dc[i - 1];
|
|
701
|
+
const wb = dc[i] + 2 * dc[i - 1];
|
|
702
|
+
return (wa + wb) / (wa / a + wb / b);
|
|
703
|
+
});
|
|
704
|
+
for (let i = 1; i < n; i++) {
|
|
705
|
+
const h = dc[i - 1] / 3;
|
|
706
|
+
const p0 = points[i - 1];
|
|
707
|
+
const p1 = points[i];
|
|
708
|
+
const a = map({ c: p0.c + h, v: p0.v + tangent[i - 1] * h });
|
|
709
|
+
const b = map({ c: p1.c - h, v: p1.v - tangent[i] * h });
|
|
710
|
+
d += `C${r2(a[0])},${r2(a[1])} ${r2(b[0])},${r2(b[1])} ${r2(xy[i][0])},${r2(xy[i][1])}`;
|
|
711
|
+
}
|
|
712
|
+
return d;
|
|
713
|
+
}
|
|
714
|
+
for (let i = 1; i < xy.length; i++) d += `L${r2(xy[i][0])},${r2(xy[i][1])}`;
|
|
715
|
+
return d;
|
|
716
|
+
}
|
|
717
|
+
function arcPath(cx, cy, outer, inner, start, end) {
|
|
718
|
+
const full = end - start >= Math.PI * 2 - 1e-9;
|
|
719
|
+
if (full) end = start + Math.PI * 2 - 1e-4;
|
|
720
|
+
const p = (r, a) => `${r2(cx + r * Math.sin(a))},${r2(cy - r * Math.cos(a))}`;
|
|
721
|
+
const large = end - start > Math.PI ? 1 : 0;
|
|
722
|
+
let d = `M${p(outer, start)}A${r2(outer)},${r2(outer)} 0 ${large} 1 ${p(outer, end)}`;
|
|
723
|
+
if (inner > 0) d += `L${p(inner, end)}A${r2(inner)},${r2(inner)} 0 ${large} 0 ${p(inner, start)}Z`;
|
|
724
|
+
else d += `L${r2(cx)},${r2(cy)}Z`;
|
|
725
|
+
return d;
|
|
726
|
+
}
|
|
727
|
+
function truncate(text, maxWidth, size, measure) {
|
|
728
|
+
if (measure(text, size) <= maxWidth) return text;
|
|
729
|
+
let out = text;
|
|
730
|
+
while (out.length > 1 && measure(`${out}\u2026`, size) > maxWidth) out = out.slice(0, -1);
|
|
731
|
+
return `${out}\u2026`;
|
|
732
|
+
}
|
|
733
|
+
function markerPath(shape, cx, cy, r) {
|
|
734
|
+
switch (shape) {
|
|
735
|
+
case "square":
|
|
736
|
+
return `M${r2(cx - r)},${r2(cy - r)}H${r2(cx + r)}V${r2(cy + r)}H${r2(cx - r)}Z`;
|
|
737
|
+
case "diamond": {
|
|
738
|
+
const d = r * 1.3;
|
|
739
|
+
return `M${r2(cx)},${r2(cy - d)}L${r2(cx + d)},${r2(cy)}L${r2(cx)},${r2(cy + d)}L${r2(cx - d)},${r2(cy)}Z`;
|
|
740
|
+
}
|
|
741
|
+
case "triangle": {
|
|
742
|
+
const d = r * 1.3;
|
|
743
|
+
return `M${r2(cx)},${r2(cy - d)}L${r2(cx + d * 0.87)},${r2(cy + d * 0.5)}L${r2(cx - d * 0.87)},${r2(cy + d * 0.5)}Z`;
|
|
744
|
+
}
|
|
745
|
+
case "cross": {
|
|
746
|
+
const d = r;
|
|
747
|
+
return `M${r2(cx - d)},${r2(cy - d)}L${r2(cx + d)},${r2(cy + d)}M${r2(cx - d)},${r2(cy + d)}L${r2(cx + d)},${r2(cy - d)}`;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
var tickCount = (length) => Math.max(2, Math.min(10, Math.round(length / 56)));
|
|
752
|
+
function axisScale(lo, hi, length, axis, frozen, headroom = false) {
|
|
753
|
+
const fixedMin = frozen && Number.isFinite(frozen.min) ? frozen.min : axis.min;
|
|
754
|
+
const fixedMax = frozen && Number.isFinite(frozen.max) ? frozen.max : axis.max;
|
|
755
|
+
const step = axis.step;
|
|
756
|
+
if (step && step > 0 && Number.isFinite(lo) && Number.isFinite(hi)) {
|
|
757
|
+
let min = _nullishCoalesce(fixedMin, () => ( Math.floor(lo / step + 1e-9) * step));
|
|
758
|
+
let max = _nullishCoalesce(fixedMax, () => ( Math.ceil(hi / step - 1e-9) * step));
|
|
759
|
+
if (!(max > min)) max = min + step;
|
|
760
|
+
const first = Math.ceil(min / step - 1e-9);
|
|
761
|
+
const last = Math.floor(max / step + 1e-9);
|
|
762
|
+
if (last - first <= 200) {
|
|
763
|
+
const clean3 = (n) => Math.abs(n) < 1e-12 ? 0 : Number(n.toPrecision(12));
|
|
764
|
+
min = clean3(min);
|
|
765
|
+
max = clean3(max);
|
|
766
|
+
const ticks = [];
|
|
767
|
+
for (let i = first; i <= last; i++) ticks.push(clean3(i * step));
|
|
768
|
+
return { min, max, step, ticks };
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
let scale = niceScale(lo, hi, tickCount(length), fixedMin, fixedMax);
|
|
772
|
+
const roomy = hi + (hi - lo) * 0.05;
|
|
773
|
+
if (headroom && fixedMax === null && hi !== 0 && hi > lo && scale.max === hi && Number.isFinite(roomy)) scale = niceScale(lo, roomy, tickCount(length), fixedMin, fixedMax);
|
|
774
|
+
return scale;
|
|
775
|
+
}
|
|
776
|
+
function clipSegment(a, b, rect) {
|
|
777
|
+
let t0 = 0;
|
|
778
|
+
let t1 = 1;
|
|
779
|
+
const dx = b[0] - a[0];
|
|
780
|
+
const dy = b[1] - a[1];
|
|
781
|
+
const checks = [
|
|
782
|
+
[-dx, a[0] - rect.x],
|
|
783
|
+
[dx, rect.x + rect.width - a[0]],
|
|
784
|
+
[-dy, a[1] - rect.y],
|
|
785
|
+
[dy, rect.y + rect.height - a[1]]
|
|
786
|
+
];
|
|
787
|
+
for (const [p, q] of checks) {
|
|
788
|
+
if (p === 0) {
|
|
789
|
+
if (q < 0) return null;
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
const r = q / p;
|
|
793
|
+
if (p < 0) {
|
|
794
|
+
if (r > t1) return null;
|
|
795
|
+
if (r > t0) t0 = r;
|
|
796
|
+
} else {
|
|
797
|
+
if (r < t0) return null;
|
|
798
|
+
if (r < t1) t1 = r;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return [
|
|
802
|
+
[a[0] + t0 * dx, a[1] + t0 * dy],
|
|
803
|
+
[a[0] + t1 * dx, a[1] + t1 * dy]
|
|
804
|
+
];
|
|
805
|
+
}
|
|
806
|
+
function clippedPolyline(points, rect) {
|
|
807
|
+
let d = "";
|
|
808
|
+
let open = false;
|
|
809
|
+
let last = null;
|
|
810
|
+
for (let i = 1; i < points.length; i++) {
|
|
811
|
+
const seg = clipSegment(points[i - 1], points[i], rect);
|
|
812
|
+
if (!seg) {
|
|
813
|
+
open = false;
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
const [p, q] = seg;
|
|
817
|
+
if (!open || !last || Math.abs(last[0] - p[0]) > 0.01 || Math.abs(last[1] - p[1]) > 0.01) d += `M${r2(p[0])},${r2(p[1])}`;
|
|
818
|
+
d += `L${r2(q[0])},${r2(q[1])}`;
|
|
819
|
+
last = q;
|
|
820
|
+
open = true;
|
|
821
|
+
}
|
|
822
|
+
return d;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// src/chart/stats.ts
|
|
826
|
+
function chartHistogram(values, bins = null) {
|
|
827
|
+
const vals = values.filter((v) => v !== null && Number.isFinite(v));
|
|
828
|
+
if (!vals.length) return [];
|
|
829
|
+
let lo = Infinity;
|
|
830
|
+
let hi = -Infinity;
|
|
831
|
+
for (const v of vals) {
|
|
832
|
+
lo = Math.min(lo, v);
|
|
833
|
+
hi = Math.max(hi, v);
|
|
834
|
+
}
|
|
835
|
+
const span = hi - lo;
|
|
836
|
+
let count = _nullishCoalesce(bins, () => ( Math.max(1, Math.min(100, Math.ceil(Math.sqrt(vals.length))))));
|
|
837
|
+
if (!(span > 0)) count = 1;
|
|
838
|
+
const width = count === 1 && !(span > 0) ? 1 : span / count;
|
|
839
|
+
const edges = Array.from({ length: count }, (_, b) => b + 1 === count ? Math.max(hi, lo + b * width) : lo + (b + 1) * width);
|
|
840
|
+
const counts = Array(count).fill(0);
|
|
841
|
+
for (const v of vals) {
|
|
842
|
+
let a = 0;
|
|
843
|
+
let z = count;
|
|
844
|
+
while (a < z) {
|
|
845
|
+
const mid = a + z >> 1;
|
|
846
|
+
if (edges[mid] < v) a = mid + 1;
|
|
847
|
+
else z = mid;
|
|
848
|
+
}
|
|
849
|
+
counts[Math.min(a, count - 1)]++;
|
|
850
|
+
}
|
|
851
|
+
return counts.map((c, b) => ({ from: clean2(lo + b * width), to: clean2(edges[b]), count: c }));
|
|
852
|
+
}
|
|
853
|
+
var clean2 = (n) => Math.abs(n) < 1e-12 ? 0 : Number(n.toPrecision(12));
|
|
854
|
+
function chartQuantile(sorted, q) {
|
|
855
|
+
const n = sorted.length;
|
|
856
|
+
if (!n) return 0;
|
|
857
|
+
const pos = q * (n - 1);
|
|
858
|
+
const i = Math.floor(pos);
|
|
859
|
+
const a = sorted[i];
|
|
860
|
+
const b = _nullishCoalesce(sorted[i + 1], () => ( a));
|
|
861
|
+
return a + (b - a) * (pos - i);
|
|
862
|
+
}
|
|
863
|
+
function chartBoxStats(values) {
|
|
864
|
+
const v = values.filter((x) => x !== null && Number.isFinite(x)).sort((a, b) => a - b);
|
|
865
|
+
if (!v.length) return null;
|
|
866
|
+
const q1 = chartQuantile(v, 0.25);
|
|
867
|
+
const median = chartQuantile(v, 0.5);
|
|
868
|
+
const q3 = chartQuantile(v, 0.75);
|
|
869
|
+
const iqr = q3 - q1;
|
|
870
|
+
const lo = q1 - 1.5 * iqr;
|
|
871
|
+
const hi = q3 + 1.5 * iqr;
|
|
872
|
+
return {
|
|
873
|
+
min: v[0],
|
|
874
|
+
max: v[v.length - 1],
|
|
875
|
+
q1,
|
|
876
|
+
median,
|
|
877
|
+
q3,
|
|
878
|
+
mean: v.reduce((t, x) => t + x, 0) / v.length,
|
|
879
|
+
lowerWhisker: _nullishCoalesce(v.find((x) => x >= lo), () => ( q1)),
|
|
880
|
+
upperWhisker: _nullishCoalesce([...v].reverse().find((x) => x <= hi), () => ( q3)),
|
|
881
|
+
outliers: v.filter((x) => x < lo || x > hi),
|
|
882
|
+
count: v.length
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function chartSquarify(values, rect) {
|
|
886
|
+
const out = values.map(() => ({ x: rect.x, y: rect.y, width: 0, height: 0 }));
|
|
887
|
+
const items = values.map((v, i2) => [i2, v]).filter(([, v]) => v > 0 && Number.isFinite(v)).sort((a, b) => b[1] - a[1]);
|
|
888
|
+
const total = items.reduce((t, [, v]) => t + v, 0);
|
|
889
|
+
const area = rect.width * rect.height;
|
|
890
|
+
if (!(total > 0 && area > 0)) return out;
|
|
891
|
+
const scale = area / total;
|
|
892
|
+
const rem = { ...rect };
|
|
893
|
+
let row = [];
|
|
894
|
+
const worst = (r, side) => {
|
|
895
|
+
const s = r.reduce((t, v) => t + v, 0);
|
|
896
|
+
if (s <= 0 || side <= 0) return Infinity;
|
|
897
|
+
const mn = Math.min(...r);
|
|
898
|
+
const mx = Math.max(...r);
|
|
899
|
+
return Math.max(side * side * mx / (s * s), s * s / (side * side * mn));
|
|
900
|
+
};
|
|
901
|
+
const layoutRow = (r) => {
|
|
902
|
+
const s = r.reduce((t, [, a]) => t + a, 0);
|
|
903
|
+
if (s <= 0) return;
|
|
904
|
+
if (rem.width >= rem.height) {
|
|
905
|
+
const cw = s / Math.max(1e-6, rem.height);
|
|
906
|
+
let y = rem.y;
|
|
907
|
+
for (const [j, a] of r) {
|
|
908
|
+
const h = a / Math.max(1e-6, cw);
|
|
909
|
+
out[j] = { x: rem.x, y, width: cw, height: h };
|
|
910
|
+
y += h;
|
|
911
|
+
}
|
|
912
|
+
rem.x += cw;
|
|
913
|
+
rem.width = Math.max(0, rem.width - cw);
|
|
914
|
+
} else {
|
|
915
|
+
const rh = s / Math.max(1e-6, rem.width);
|
|
916
|
+
let x = rem.x;
|
|
917
|
+
for (const [j, a] of r) {
|
|
918
|
+
const w = a / Math.max(1e-6, rh);
|
|
919
|
+
out[j] = { x, y: rem.y, width: w, height: rh };
|
|
920
|
+
x += w;
|
|
921
|
+
}
|
|
922
|
+
rem.y += rh;
|
|
923
|
+
rem.height = Math.max(0, rem.height - rh);
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
let i = 0;
|
|
927
|
+
while (i < items.length) {
|
|
928
|
+
const [j, v] = items[i];
|
|
929
|
+
const a = v * scale;
|
|
930
|
+
const side = Math.min(rem.width, rem.height);
|
|
931
|
+
const cur = row.map(([, x]) => x);
|
|
932
|
+
if (!row.length || worst([...cur, a], side) <= worst(cur, side)) {
|
|
933
|
+
row.push([j, a]);
|
|
934
|
+
i++;
|
|
935
|
+
} else {
|
|
936
|
+
layoutRow(row);
|
|
937
|
+
row = [];
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
if (row.length) layoutRow(row);
|
|
941
|
+
return out;
|
|
942
|
+
}
|
|
943
|
+
function decimateChartPoints(points, max = 2e3) {
|
|
944
|
+
if (points.length <= max) return [...points];
|
|
945
|
+
const out = [];
|
|
946
|
+
let i = 0;
|
|
947
|
+
while (i < points.length) {
|
|
948
|
+
const col = Math.floor(points[i].x);
|
|
949
|
+
const start = i;
|
|
950
|
+
let mn = i;
|
|
951
|
+
let mx = i;
|
|
952
|
+
let k = i + 1;
|
|
953
|
+
while (k < points.length && Math.floor(points[k].x) === col) {
|
|
954
|
+
if (points[k].y < points[mn].y) mn = k;
|
|
955
|
+
if (points[k].y > points[mx].y) mx = k;
|
|
956
|
+
k++;
|
|
957
|
+
}
|
|
958
|
+
const end = k - 1;
|
|
959
|
+
for (const idx of [.../* @__PURE__ */ new Set([start, mn, mx, end])].sort((a, b) => a - b)) out.push(points[idx]);
|
|
960
|
+
i = k;
|
|
961
|
+
}
|
|
962
|
+
return out;
|
|
963
|
+
}
|
|
964
|
+
var round = (n) => Number(n.toPrecision(6));
|
|
965
|
+
function summarizeChartSeries(series, categories) {
|
|
966
|
+
const present = series.values.map((v, i) => [i, v]).filter((p) => p[1] !== null && Number.isFinite(p[1]));
|
|
967
|
+
const base = { id: series.id, name: series.name, count: present.length, gaps: series.values.length - present.length, min: null, max: null, mean: null, sum: null, minAt: null, maxAt: null, trend: null, change: null };
|
|
968
|
+
if (!present.length) return base;
|
|
969
|
+
let lo = present[0];
|
|
970
|
+
let hi = present[0];
|
|
971
|
+
let sum = 0;
|
|
972
|
+
for (const p of present) {
|
|
973
|
+
if (p[1] < lo[1]) lo = p;
|
|
974
|
+
if (p[1] > hi[1]) hi = p;
|
|
975
|
+
sum += p[1];
|
|
976
|
+
}
|
|
977
|
+
const mean = sum / present.length;
|
|
978
|
+
let trend = null;
|
|
979
|
+
if (present.length >= 2) {
|
|
980
|
+
const mx = present.reduce((t, p) => t + p[0], 0) / present.length;
|
|
981
|
+
let sxy = 0;
|
|
982
|
+
let sxx = 0;
|
|
983
|
+
for (const [i, v] of present) {
|
|
984
|
+
sxy += (i - mx) * (v - mean);
|
|
985
|
+
sxx += (i - mx) ** 2;
|
|
986
|
+
}
|
|
987
|
+
const slope = sxx ? sxy / sxx : 0;
|
|
988
|
+
const spread = hi[1] - lo[1];
|
|
989
|
+
const travel = Math.abs(slope * (present[present.length - 1][0] - present[0][0]));
|
|
990
|
+
trend = !spread || travel < spread * 0.05 ? "flat" : slope > 0 ? "rising" : "falling";
|
|
991
|
+
}
|
|
992
|
+
const first = present[0][1];
|
|
993
|
+
const last = present[present.length - 1][1];
|
|
994
|
+
return {
|
|
995
|
+
...base,
|
|
996
|
+
min: round(lo[1]),
|
|
997
|
+
max: round(hi[1]),
|
|
998
|
+
mean: round(mean),
|
|
999
|
+
sum: round(sum),
|
|
1000
|
+
minAt: _nullishCoalesce(categories[lo[0]], () => ( String(lo[0]))),
|
|
1001
|
+
maxAt: _nullishCoalesce(categories[hi[0]], () => ( String(hi[0]))),
|
|
1002
|
+
trend,
|
|
1003
|
+
change: present.length >= 2 && first !== 0 ? round((last - first) / Math.abs(first)) : null
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// src/chart/trend.ts
|
|
1008
|
+
var degree = (t) => t.type === "polynomial" ? Math.max(2, Math.min(6, _nullishCoalesce(t.order, () => ( 2)))) : 1;
|
|
1009
|
+
var logX = (type) => type === "logarithmic" || type === "power";
|
|
1010
|
+
var logY = (type) => type === "exponential" || type === "power";
|
|
1011
|
+
function fitChartTrendline(trendline, points) {
|
|
1012
|
+
const type = trendline.type;
|
|
1013
|
+
if (type === "moving-average") return null;
|
|
1014
|
+
const d = degree(trendline);
|
|
1015
|
+
const data = points.filter(([x, y]) => Number.isFinite(x) && Number.isFinite(y) && (!logX(type) || x > 0) && (!logY(type) || y > 0)).map(([x, y]) => [logX(type) ? Math.log(x) : x, logY(type) ? Math.log(y) : y]);
|
|
1016
|
+
if (data.length < 2 || data.length <= d) return null;
|
|
1017
|
+
let fixed = null;
|
|
1018
|
+
const b = trendline.intercept;
|
|
1019
|
+
if (b !== void 0 && b !== null && Number.isFinite(b)) {
|
|
1020
|
+
if (type === "linear" || type === "polynomial") fixed = b;
|
|
1021
|
+
else if (type === "exponential" && b > 0) fixed = Math.log(b);
|
|
1022
|
+
}
|
|
1023
|
+
const n = data.length;
|
|
1024
|
+
const center = fixed !== null ? 0 : data.reduce((t, p) => t + p[0], 0) / n;
|
|
1025
|
+
const scale = data.reduce((m2, p) => Math.max(m2, Math.abs(p[0] - center)), 0);
|
|
1026
|
+
if (!(Number.isFinite(scale) && scale > 0)) return null;
|
|
1027
|
+
const first = fixed !== null ? 1 : 0;
|
|
1028
|
+
const b0 = _nullishCoalesce(fixed, () => ( 0));
|
|
1029
|
+
const m = d + 1 - first;
|
|
1030
|
+
const ata = Array.from({ length: m }, () => Array(m).fill(0));
|
|
1031
|
+
const aty = Array(m).fill(0);
|
|
1032
|
+
for (const [x, y] of data) {
|
|
1033
|
+
const u = (x - center) / scale;
|
|
1034
|
+
const pows = Array.from({ length: m }, (_, k) => Math.pow(u, k + first));
|
|
1035
|
+
for (let i = 0; i < m; i++) {
|
|
1036
|
+
aty[i] += pows[i] * (y - b0);
|
|
1037
|
+
for (let j = 0; j < m; j++) ata[i][j] += pows[i] * pows[j];
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
const sol = solve(ata, aty);
|
|
1041
|
+
if (!sol) return null;
|
|
1042
|
+
const a = Array(d + 1).fill(b0);
|
|
1043
|
+
sol.forEach((v, k) => a[k + first] = v);
|
|
1044
|
+
const coefficients = expand(a, center, scale);
|
|
1045
|
+
if (!coefficients) return null;
|
|
1046
|
+
const poly = (x) => {
|
|
1047
|
+
const u = (x - center) / scale;
|
|
1048
|
+
return a.reduceRight((acc, c) => acc * u + c, 0);
|
|
1049
|
+
};
|
|
1050
|
+
const mean = data.reduce((t, p) => t + p[1], 0) / n;
|
|
1051
|
+
let ssRes = 0;
|
|
1052
|
+
let ssTot = 0;
|
|
1053
|
+
for (const [x, y] of data) {
|
|
1054
|
+
ssRes += (y - poly(x)) ** 2;
|
|
1055
|
+
ssTot += fixed !== null ? (y - b0) ** 2 : (y - mean) ** 2;
|
|
1056
|
+
}
|
|
1057
|
+
const r22 = ssTot > 0 && Number.isFinite(ssTot) && Number.isFinite(ssRes) ? 1 - ssRes / ssTot : null;
|
|
1058
|
+
return {
|
|
1059
|
+
type,
|
|
1060
|
+
coefficients,
|
|
1061
|
+
r2: r22,
|
|
1062
|
+
equation: equationText(type, coefficients),
|
|
1063
|
+
points: n,
|
|
1064
|
+
evaluate(x) {
|
|
1065
|
+
let xx = x;
|
|
1066
|
+
if (logX(type)) {
|
|
1067
|
+
if (!(x > 0)) return null;
|
|
1068
|
+
xx = Math.log(x);
|
|
1069
|
+
}
|
|
1070
|
+
const p = poly(xx);
|
|
1071
|
+
const y = logY(type) ? Math.exp(p) : p;
|
|
1072
|
+
return Number.isFinite(y) ? y : null;
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
function solve(a, b) {
|
|
1077
|
+
const n = b.length;
|
|
1078
|
+
const big = a.flat().reduce((m, v) => Math.max(m, Math.abs(v)), 0);
|
|
1079
|
+
if (!(Number.isFinite(big) && big > 0)) return null;
|
|
1080
|
+
for (let col = 0; col < n; col++) {
|
|
1081
|
+
let piv = col;
|
|
1082
|
+
for (let r = col + 1; r < n; r++) if (Math.abs(a[r][col]) > Math.abs(a[piv][col])) piv = r;
|
|
1083
|
+
[a[col], a[piv]] = [a[piv], a[col]];
|
|
1084
|
+
[b[col], b[piv]] = [b[piv], b[col]];
|
|
1085
|
+
const p = a[col][col];
|
|
1086
|
+
if (!Number.isFinite(p) || Math.abs(p) <= big * 1e-13) return null;
|
|
1087
|
+
for (let r = col + 1; r < n; r++) {
|
|
1088
|
+
const f = a[r][col] / p;
|
|
1089
|
+
for (let c = col; c < n; c++) a[r][c] -= f * a[col][c];
|
|
1090
|
+
b[r] -= f * b[col];
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
const x = Array(n).fill(0);
|
|
1094
|
+
for (let r = n - 1; r >= 0; r--) {
|
|
1095
|
+
let s = 0;
|
|
1096
|
+
for (let c = r + 1; c < n; c++) s += a[r][c] * x[c];
|
|
1097
|
+
x[r] = (b[r] - s) / a[r][r];
|
|
1098
|
+
}
|
|
1099
|
+
return x.every(Number.isFinite) ? x : null;
|
|
1100
|
+
}
|
|
1101
|
+
function expand(a, c, s) {
|
|
1102
|
+
const out = Array(a.length).fill(0);
|
|
1103
|
+
a.forEach((ak, k) => {
|
|
1104
|
+
const lead = ak / Math.pow(s, k);
|
|
1105
|
+
let binom = 1;
|
|
1106
|
+
for (let j = 0; j <= k; j++) {
|
|
1107
|
+
out[j] += lead * binom * Math.pow(-c, k - j);
|
|
1108
|
+
binom = binom * (k - j) / (j + 1);
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
return out.every(Number.isFinite) ? out : null;
|
|
1112
|
+
}
|
|
1113
|
+
var POWERS = ["", "x", "x\xB2", "x\xB3", "x\u2074", "x\u2075", "x\u2076"];
|
|
1114
|
+
function equationText(type, coef) {
|
|
1115
|
+
const c0 = _nullishCoalesce(coef[0], () => ( 0));
|
|
1116
|
+
const c1 = _nullishCoalesce(coef[1], () => ( 0));
|
|
1117
|
+
if (type === "exponential") return `y = ${trendNumberText(Math.exp(c0))}e^(${trendNumberText(c1)}x)`;
|
|
1118
|
+
if (type === "power") return `y = ${trendNumberText(Math.exp(c0))}x^${trendNumberText(c1)}`;
|
|
1119
|
+
if (type === "logarithmic") return terms([[c1, "ln(x)"], [c0, ""]]);
|
|
1120
|
+
return terms(coef.map((c, k) => [c, _nullishCoalesce(POWERS[k], () => ( "x"))]).reverse());
|
|
1121
|
+
}
|
|
1122
|
+
function terms(parts) {
|
|
1123
|
+
let s = "y = ";
|
|
1124
|
+
let first = true;
|
|
1125
|
+
for (const [c, t] of parts) {
|
|
1126
|
+
const txt = trendNumberText(Math.abs(c));
|
|
1127
|
+
if (txt === "0") continue;
|
|
1128
|
+
const neg = c < 0;
|
|
1129
|
+
if (first) s += neg ? "-" : "";
|
|
1130
|
+
else s += neg ? " - " : " + ";
|
|
1131
|
+
if (txt !== "1" || !t) s += txt;
|
|
1132
|
+
s += t;
|
|
1133
|
+
first = false;
|
|
1134
|
+
}
|
|
1135
|
+
return first ? `${s}0` : s;
|
|
1136
|
+
}
|
|
1137
|
+
function trendNumberText(v) {
|
|
1138
|
+
if (!Number.isFinite(v)) return "#NUM!";
|
|
1139
|
+
const a = Math.abs(v);
|
|
1140
|
+
if (a === 0) return "0";
|
|
1141
|
+
if (a < 1e-4 || a >= 1e11) {
|
|
1142
|
+
const [mant, exp] = v.toExponential(4).split("e");
|
|
1143
|
+
const e = Number(exp);
|
|
1144
|
+
return `${trimZeros(mant)}E${e < 0 ? "-" : "+"}${String(Math.abs(e)).padStart(2, "0")}`;
|
|
1145
|
+
}
|
|
1146
|
+
const s = trimZeros(v.toFixed(4));
|
|
1147
|
+
return s === "-0" ? "0" : s;
|
|
1148
|
+
}
|
|
1149
|
+
var trimZeros = (s) => s.includes(".") ? s.replace(/0+$/, "").replace(/\.$/, "") : s;
|
|
1150
|
+
function chartMovingAverage(points, period) {
|
|
1151
|
+
const p = Math.max(2, Math.min(1e3, Math.round(period) || 2));
|
|
1152
|
+
const pts = points.filter(([x, y]) => Number.isFinite(x) && Number.isFinite(y));
|
|
1153
|
+
const out = [];
|
|
1154
|
+
let sum = 0;
|
|
1155
|
+
pts.forEach(([x, y], i) => {
|
|
1156
|
+
sum += y;
|
|
1157
|
+
if (i >= p) sum -= pts[i - p][1];
|
|
1158
|
+
if (i >= p - 1) out.push([x, sum / p]);
|
|
1159
|
+
});
|
|
1160
|
+
return out;
|
|
1161
|
+
}
|
|
1162
|
+
function chartTrendlineName(trendline, seriesName) {
|
|
1163
|
+
if (trendline.name) return trendline.name;
|
|
1164
|
+
const kind = {
|
|
1165
|
+
linear: "Linear",
|
|
1166
|
+
exponential: "Expon.",
|
|
1167
|
+
logarithmic: "Log.",
|
|
1168
|
+
polynomial: "Poly.",
|
|
1169
|
+
power: "Power",
|
|
1170
|
+
"moving-average": `${Math.max(2, _nullishCoalesce(trendline.period, () => ( 2)))} per. Mov. Avg.`
|
|
1171
|
+
};
|
|
1172
|
+
return `${kind[trendline.type]} (${seriesName})`;
|
|
1173
|
+
}
|
|
1174
|
+
function chartSupportsTrendlines(type, stacking) {
|
|
1175
|
+
return (type === "bar" || type === "line" || type === "area" || type === "scatter" || type === "bubble") && stacking === "none";
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// src/chart/layout-cartesian.ts
|
|
1179
|
+
var MAX_CHART_DATA_LABELS = 200;
|
|
1180
|
+
var MAX_CHART_LINE_POINTS = 2e3;
|
|
1181
|
+
var MAX_CHART_MARKERS = 1e3;
|
|
1182
|
+
function scaleInfo(scale, range, format) {
|
|
1183
|
+
const domain = [scale.min, scale.max];
|
|
1184
|
+
const raw = (v) => linearMap(v, domain, range);
|
|
1185
|
+
const px = (v) => raw(Math.min(scale.max, Math.max(scale.min, v)));
|
|
1186
|
+
return { scale, range, px, raw, zero: px(0), format, tickText: (t) => formatChartNumber(t, format, _nullishCoalesce(format.decimals, () => ( stepDecimals(scale.step)))) };
|
|
1187
|
+
}
|
|
1188
|
+
function categoryFrame(ctx, area, input) {
|
|
1189
|
+
const { spec, theme, measure, fs, marks } = ctx;
|
|
1190
|
+
const { labels, horizontal, primary, secondary } = input;
|
|
1191
|
+
const xAxis = _nullishCoalesce(input.categoryAxis, () => ( spec.xAxis));
|
|
1192
|
+
const n = Math.max(1, labels.length);
|
|
1193
|
+
const titleBand = fs * 1.3 + 6;
|
|
1194
|
+
const labelTarget = _nullishCoalesce(input.labelTarget, () => ( ((i) => ({ kind: "category", index: i }))));
|
|
1195
|
+
const estimate = (v, length) => {
|
|
1196
|
+
const s = axisScale(v.lo, v.hi, length, v.axis, v.frozen, v.headroom);
|
|
1197
|
+
const info = scaleInfo(s, [0, 1], v.format);
|
|
1198
|
+
return Math.max(0, ...s.ticks.map((t) => measure(info.tickText(t), fs)));
|
|
1199
|
+
};
|
|
1200
|
+
const valueLen0 = horizontal ? area.width : area.height;
|
|
1201
|
+
const valueLabelWidth = primary.axis.show ? estimate(primary, valueLen0) : 0;
|
|
1202
|
+
const valueLabelWidth2 = _optionalChain([secondary, 'optionalAccess', _10 => _10.axis, 'access', _11 => _11.show]) ? estimate(secondary, valueLen0) : 0;
|
|
1203
|
+
const catLabelWidth = Math.max(0, ...labels.map((c) => measure(c, fs)));
|
|
1204
|
+
let left;
|
|
1205
|
+
let bottom;
|
|
1206
|
+
let right = 8;
|
|
1207
|
+
let top = horizontal ? 4 : fs / 2 + 2;
|
|
1208
|
+
let rotate = 0;
|
|
1209
|
+
let dateLabels = null;
|
|
1210
|
+
let rotatedMax = 0;
|
|
1211
|
+
if (horizontal) {
|
|
1212
|
+
left = (xAxis.show ? Math.min(catLabelWidth, area.width * 0.35) + 8 : 2) + (xAxis.title ? titleBand : 0);
|
|
1213
|
+
bottom = (primary.axis.show ? fs * 1.6 : 2) + (primary.axis.title ? titleBand : 0);
|
|
1214
|
+
right = Math.max(8, valueLabelWidth / 2);
|
|
1215
|
+
if (secondary) top = (secondary.axis.show ? fs * 1.6 : 4) + (secondary.axis.title ? titleBand : 0);
|
|
1216
|
+
} else {
|
|
1217
|
+
left = (primary.axis.show ? valueLabelWidth + 8 : 2) + (primary.axis.title ? titleBand : 0);
|
|
1218
|
+
if (secondary) right = (secondary.axis.show ? valueLabelWidth2 + 8 : 8) + (secondary.axis.title ? titleBand : 0);
|
|
1219
|
+
const band2 = (area.width - left - right) / n;
|
|
1220
|
+
if (xAxis.show && xAxis.kind === "date") dateLabels = chartDateLabels(labels, band2, (t) => measure(t, fs));
|
|
1221
|
+
if (xAxis.show && !dateLabels && catLabelWidth > band2 * 0.92) rotate = -40;
|
|
1222
|
+
const sin = Math.sin(Math.PI / 4.5);
|
|
1223
|
+
const catHeight = rotate ? Math.min(catLabelWidth * sin + fs, area.height * 0.4) : fs * 1.6;
|
|
1224
|
+
if (rotate) rotatedMax = (catHeight - fs) / sin;
|
|
1225
|
+
bottom = (xAxis.show ? catHeight : 2) + (xAxis.title ? titleBand : 0);
|
|
1226
|
+
}
|
|
1227
|
+
const plot = {
|
|
1228
|
+
x: area.x + left,
|
|
1229
|
+
y: area.y + top,
|
|
1230
|
+
width: Math.max(1, area.width - left - right),
|
|
1231
|
+
height: Math.max(1, area.height - bottom - top)
|
|
1232
|
+
};
|
|
1233
|
+
const valueLen = horizontal ? plot.width : plot.height;
|
|
1234
|
+
const range = horizontal ? [plot.x, plot.x + plot.width] : [plot.y + plot.height, plot.y];
|
|
1235
|
+
const value = scaleInfo(axisScale(primary.lo, primary.hi, valueLen, primary.axis, primary.frozen, primary.headroom), range, primary.format);
|
|
1236
|
+
const value2 = secondary ? scaleInfo(axisScale(secondary.lo, secondary.hi, valueLen, secondary.axis, secondary.frozen, secondary.headroom), range, secondary.format) : void 0;
|
|
1237
|
+
const catStart = horizontal ? plot.y : plot.x;
|
|
1238
|
+
const catLen = horizontal ? plot.height : plot.width;
|
|
1239
|
+
const band = catLen / n;
|
|
1240
|
+
const catCenter = (i) => catStart + band * (i + 0.5);
|
|
1241
|
+
const map = (p) => horizontal ? [p.v, p.c] : [p.c, p.v];
|
|
1242
|
+
const gridAt = (info, key) => info.scale.ticks.forEach((t, i) => {
|
|
1243
|
+
const p = info.px(t);
|
|
1244
|
+
const [x1, y1] = map({ c: catStart, v: p });
|
|
1245
|
+
const [x2, y2] = map({ c: catStart + catLen, v: p });
|
|
1246
|
+
marks.push({ kind: "line", key: `grid-${key}-${i}`, role: "grid", x1, y1, x2, y2, stroke: theme.grid, strokeWidth: 1 });
|
|
1247
|
+
});
|
|
1248
|
+
if (primary.axis.gridlines) gridAt(value, "y");
|
|
1249
|
+
if (value2 && secondary.axis.gridlines) gridAt(value2, "y2");
|
|
1250
|
+
if (xAxis.gridlines) {
|
|
1251
|
+
for (let i = 1; i < n; i++) {
|
|
1252
|
+
const c = catStart + band * i;
|
|
1253
|
+
const [x1, y1] = map({ c, v: range[0] });
|
|
1254
|
+
const [x2, y2] = map({ c, v: range[1] });
|
|
1255
|
+
marks.push({ kind: "line", key: `grid-x-${i}`, role: "grid", x1, y1, x2, y2, stroke: theme.grid, strokeWidth: 1 });
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
const ticksFor = (info, v, far) => {
|
|
1259
|
+
if (!v.axis.show) return;
|
|
1260
|
+
const target = { kind: "axis", axis: v.key };
|
|
1261
|
+
info.scale.ticks.forEach((t, i) => {
|
|
1262
|
+
const p = info.px(t);
|
|
1263
|
+
const label = info.tickText(t);
|
|
1264
|
+
const key = `tick-${v.key}-${i}`;
|
|
1265
|
+
if (horizontal) {
|
|
1266
|
+
if (far) marks.push({ kind: "text", key, role: "tick-label", x: p, y: plot.y - 6, text: label, anchor: "middle", baseline: "bottom", size: fs, fill: theme.mutedText, target });
|
|
1267
|
+
else marks.push({ kind: "text", key, role: "tick-label", x: p, y: plot.y + plot.height + 6, text: label, anchor: "middle", baseline: "top", size: fs, fill: theme.mutedText, target });
|
|
1268
|
+
} else if (far) marks.push({ kind: "text", key, role: "tick-label", x: plot.x + plot.width + 8, y: p, text: label, anchor: "start", baseline: "middle", size: fs, fill: theme.mutedText, target });
|
|
1269
|
+
else marks.push({ kind: "text", key, role: "tick-label", x: plot.x - 8, y: p, text: label, anchor: "end", baseline: "middle", size: fs, fill: theme.mutedText, target });
|
|
1270
|
+
});
|
|
1271
|
+
};
|
|
1272
|
+
ticksFor(value, primary, false);
|
|
1273
|
+
if (value2) ticksFor(value2, secondary, true);
|
|
1274
|
+
const xTarget = { kind: "axis", axis: "x" };
|
|
1275
|
+
if (xAxis.show) {
|
|
1276
|
+
const [x1, y1] = map({ c: catStart, v: value.zero });
|
|
1277
|
+
const [x2, y2] = map({ c: catStart + catLen, v: value.zero });
|
|
1278
|
+
marks.push({ kind: "line", key: "axis-x", role: "axis", x1, y1, x2, y2, stroke: theme.axis, strokeWidth: 1, target: xTarget });
|
|
1279
|
+
const every = dateLabels ? 1 : horizontal ? Math.max(1, Math.ceil(fs * 1.3 / band)) : rotate ? Math.max(1, Math.ceil(fs * 1.4 / band)) : 1;
|
|
1280
|
+
let hidden = 0;
|
|
1281
|
+
let truncated = 0;
|
|
1282
|
+
labels.forEach((c, i) => {
|
|
1283
|
+
const text0 = dateLabels ? dateLabels.labels.get(i) : i % every ? void 0 : c;
|
|
1284
|
+
if (text0 === void 0) {
|
|
1285
|
+
hidden++;
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
const target = labelTarget(i);
|
|
1289
|
+
const selected = target ? ctx.isSelected(target) : false;
|
|
1290
|
+
const fill = selected ? theme.selection : theme.mutedText;
|
|
1291
|
+
const extra = target ? { target, selected } : {};
|
|
1292
|
+
if (horizontal) {
|
|
1293
|
+
const maxW = left - (xAxis.title ? titleBand : 0) - 8;
|
|
1294
|
+
const text = truncate(text0, maxW, fs, measure);
|
|
1295
|
+
if (text !== text0) truncated++;
|
|
1296
|
+
marks.push({ kind: "text", key: `tick-x-${i}`, role: "tick-label", x: plot.x - 8, y: catCenter(i), text, anchor: "end", baseline: "middle", size: fs, fill, ...extra });
|
|
1297
|
+
} else if (rotate) {
|
|
1298
|
+
const room = (catCenter(i) - 2) / Math.cos(Math.PI / 4.5);
|
|
1299
|
+
const text = truncate(text0, Math.max(fs, Math.min(rotatedMax, room)), fs, measure);
|
|
1300
|
+
if (text !== text0) truncated++;
|
|
1301
|
+
marks.push({ kind: "text", key: `tick-x-${i}`, role: "tick-label", x: catCenter(i), y: plot.y + plot.height + 8, text, anchor: "end", baseline: "middle", size: fs, fill, rotate, ...extra });
|
|
1302
|
+
} else {
|
|
1303
|
+
const text = dateLabels ? text0 : truncate(text0, Math.max(band - 4, fs * 2), fs, measure);
|
|
1304
|
+
if (text !== text0) truncated++;
|
|
1305
|
+
marks.push({ kind: "text", key: `tick-x-${i}`, role: "tick-label", x: catCenter(i), y: plot.y + plot.height + 6, text, anchor: dateLabels ? "start" : "middle", baseline: "top", size: fs, fill, ...extra });
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
if (dateLabels) {
|
|
1309
|
+
for (const i of dateLabels.indexes) {
|
|
1310
|
+
const x = catCenter(i);
|
|
1311
|
+
marks.push({ kind: "line", key: `tick-mark-x-${i}`, role: "tick", x1: x, y1: plot.y + plot.height, x2: x, y2: plot.y + plot.height + 4, stroke: theme.axis, strokeWidth: 1 });
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
if (hidden && !dateLabels) ctx.warn("labels-hidden", `${hidden} of ${labels.length} category labels are hidden so the rest do not overlap.`);
|
|
1315
|
+
if (truncated) ctx.warn("labels-truncated", `${truncated} category label${truncated === 1 ? " was" : "s were"} shortened to fit.`);
|
|
1316
|
+
}
|
|
1317
|
+
const yTarget = { kind: "axis", axis: "y" };
|
|
1318
|
+
const valueTitle = primary.axis.title;
|
|
1319
|
+
const catTitle = xAxis.title;
|
|
1320
|
+
const bottomTitle = horizontal ? valueTitle : catTitle;
|
|
1321
|
+
const leftTitle = horizontal ? catTitle : valueTitle;
|
|
1322
|
+
if (bottomTitle)
|
|
1323
|
+
marks.push({ kind: "text", key: `axis-title-${horizontal ? "y" : "x"}`, role: "axis-title", x: plot.x + plot.width / 2, y: area.y + area.height, text: bottomTitle, anchor: "middle", baseline: "bottom", size: fs + 1, weight: 500, fill: theme.text, target: horizontal ? yTarget : xTarget });
|
|
1324
|
+
if (leftTitle)
|
|
1325
|
+
marks.push({ kind: "text", key: `axis-title-${horizontal ? "x" : "y"}`, role: "axis-title", x: area.x, y: plot.y + plot.height / 2, text: leftTitle, anchor: "middle", baseline: "top", size: fs + 1, weight: 500, fill: theme.text, rotate: -90, target: horizontal ? xTarget : yTarget });
|
|
1326
|
+
if (_optionalChain([secondary, 'optionalAccess', _12 => _12.axis, 'access', _13 => _13.title])) {
|
|
1327
|
+
const target = { kind: "axis", axis: "y2" };
|
|
1328
|
+
if (horizontal) marks.push({ kind: "text", key: "axis-title-y2", role: "axis-title", x: plot.x + plot.width / 2, y: area.y, text: secondary.axis.title, anchor: "middle", baseline: "top", size: fs + 1, weight: 500, fill: theme.text, target });
|
|
1329
|
+
else marks.push({ kind: "text", key: "axis-title-y2", role: "axis-title", x: area.x + area.width, y: plot.y + plot.height / 2, text: secondary.axis.title, anchor: "middle", baseline: "top", size: fs + 1, weight: 500, fill: theme.text, rotate: 90, target });
|
|
1330
|
+
}
|
|
1331
|
+
for (const axis of ["x", "y", "y2"]) {
|
|
1332
|
+
if (!ctx.isSelected({ kind: "axis", axis })) continue;
|
|
1333
|
+
if (axis === "y2" && !secondary) continue;
|
|
1334
|
+
let box;
|
|
1335
|
+
if (axis === "y2") box = horizontal ? { x: plot.x - 4, y: area.y, width: plot.width + 8, height: plot.y - area.y - 2 } : { x: plot.x + plot.width + 2, y: plot.y - fs / 2, width: area.x + area.width - plot.x - plot.width - 2, height: plot.height + fs };
|
|
1336
|
+
else {
|
|
1337
|
+
const onLeft = axis === "y" !== horizontal;
|
|
1338
|
+
box = onLeft ? { x: area.x, y: plot.y - fs / 2, width: left - 2, height: plot.height + fs } : { x: plot.x - 4, y: plot.y + plot.height + 2, width: plot.width + 8, height: area.y + area.height - plot.y - plot.height - 2 };
|
|
1339
|
+
}
|
|
1340
|
+
ctx.outline(`axis-${axis}-selection`, box);
|
|
1341
|
+
}
|
|
1342
|
+
return { plot, horizontal, n, band, catStart, catLen, catCenter, map, value, value2 };
|
|
1343
|
+
}
|
|
1344
|
+
var toValueScale = (info, axis) => ({ axis, domain: [info.scale.min, info.scale.max], range: info.range, ticks: info.scale.ticks });
|
|
1345
|
+
var order = (t) => t === "area" ? 0 : t === "bar" ? 1 : t === "line" ? 2 : 3;
|
|
1346
|
+
function pointMark(base, shape, cx, cy, r, fill, stroke, strokeWidth) {
|
|
1347
|
+
if (shape === "circle" || shape === "hit" || shape === "none") return { kind: "circle", role: "point", ...base, cx, cy, r, fill, ...stroke ? { stroke, strokeWidth } : {} };
|
|
1348
|
+
const closed = shape !== "cross";
|
|
1349
|
+
return { kind: "path", role: "point", ...base, d: markerPath(shape, cx, cy, r), fill: closed ? fill : "none", stroke: _nullishCoalesce(stroke, () => ( fill)), strokeWidth: _nullishCoalesce(strokeWidth, () => ( 2)) };
|
|
1350
|
+
}
|
|
1351
|
+
function drawTrendlines(ctx, plot, series, color, points, toPx, labels) {
|
|
1352
|
+
(_nullishCoalesce(series.trendlines, () => ( []))).forEach((t, ti) => {
|
|
1353
|
+
const target = { kind: "trendline", seriesId: series.id, index: ti };
|
|
1354
|
+
const selected = ctx.isSelected(target);
|
|
1355
|
+
const stroke = _nullishCoalesce(t.color, () => ( color));
|
|
1356
|
+
let px = [];
|
|
1357
|
+
if (t.type === "moving-average") px = chartMovingAverage(points, _nullishCoalesce(t.period, () => ( 2))).map(([x, y]) => toPx(x, y));
|
|
1358
|
+
else {
|
|
1359
|
+
const fit = fitChartTrendline(t, points);
|
|
1360
|
+
if (!fit) {
|
|
1361
|
+
ctx.warn("values-skipped", `The ${t.type} trendline on "${series.name}" could not be fitted (too few usable points).`);
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
const xs = points.map((p) => p[0]);
|
|
1365
|
+
const lo = Math.min(...xs) - (_nullishCoalesce(t.backward, () => ( 0)));
|
|
1366
|
+
const hi = Math.max(...xs) + (_nullishCoalesce(t.forward, () => ( 0)));
|
|
1367
|
+
const steps = t.type === "linear" ? 1 : 200;
|
|
1368
|
+
for (let k = 0; k <= steps; k++) {
|
|
1369
|
+
const x = lo + (hi - lo) * k / steps;
|
|
1370
|
+
const y = fit.evaluate(x);
|
|
1371
|
+
if (y !== null) px.push(toPx(x, y));
|
|
1372
|
+
}
|
|
1373
|
+
if (t.showEquation) labels.push({ text: fit.equation, color: stroke, target });
|
|
1374
|
+
if (t.showR2) labels.push({ text: fit.r2 === null ? "R\xB2 = #N/A" : `R\xB2 = ${trendNumberText(fit.r2)}`, color: stroke, target });
|
|
1375
|
+
}
|
|
1376
|
+
const d = clippedPolyline(px, plot);
|
|
1377
|
+
if (!d) return;
|
|
1378
|
+
ctx.marks.push({ kind: "path", key: `trendline-${series.id}-${ti}`, role: "trendline", d, fill: "none", stroke, strokeWidth: selected ? 3 : 1.5, dash: "6 4", target, selected });
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
function drawTrendLabels(ctx, plot, labels) {
|
|
1382
|
+
labels.forEach(
|
|
1383
|
+
(l, i) => ctx.marks.push({ kind: "text", key: `trendline-label-${i}`, role: "trendline-label", x: plot.x + plot.width - 4, y: plot.y + 4 + i * (ctx.fs + 3), text: l.text, anchor: "end", baseline: "top", size: ctx.fs - 1, fill: l.color, target: l.target })
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
function barLabel(ctx, text, rect, horizontal, positive, stacked, fill, plot) {
|
|
1387
|
+
const { measure, fs, spec, theme } = ctx;
|
|
1388
|
+
const size = fs - 1;
|
|
1389
|
+
const tw = measure(text, size);
|
|
1390
|
+
const th = size + 2;
|
|
1391
|
+
const along = horizontal ? rect.width : rect.height;
|
|
1392
|
+
const across = horizontal ? rect.height : rect.width;
|
|
1393
|
+
const need = horizontal ? tw + 6 : th + 2;
|
|
1394
|
+
const fitsInside = along >= need && across >= (horizontal ? th : Math.min(tw, across + 1));
|
|
1395
|
+
let position = spec.labelPosition;
|
|
1396
|
+
if (position === "auto") position = stacked ? "center" : "outside";
|
|
1397
|
+
const cx = rect.x + rect.width / 2;
|
|
1398
|
+
const cy = rect.y + rect.height / 2;
|
|
1399
|
+
const inner = contrastingTextColor(fill);
|
|
1400
|
+
const centre = () => ({ x: cx, y: cy, anchor: "middle", baseline: "middle", fill: inner });
|
|
1401
|
+
const insideEnd = () => horizontal ? { x: positive ? rect.x + rect.width - 4 : rect.x + 4, y: cy, anchor: positive ? "end" : "start", baseline: "middle", fill: inner } : { x: cx, y: positive ? rect.y + 3 : rect.y + rect.height - 3, anchor: "middle", baseline: positive ? "top" : "bottom", fill: inner };
|
|
1402
|
+
const outside = () => horizontal ? { x: positive ? rect.x + rect.width + 4 : rect.x - 4, y: cy, anchor: positive ? "start" : "end", baseline: "middle", fill: theme.text } : { x: cx, y: positive ? rect.y - 4 : rect.y + rect.height + 4, anchor: "middle", baseline: positive ? "bottom" : "top", fill: theme.text };
|
|
1403
|
+
const outsideFits = () => {
|
|
1404
|
+
if (horizontal) return positive ? rect.x + rect.width + 4 + tw <= plot.x + plot.width + 8 : rect.x - 4 - tw >= plot.x - 8;
|
|
1405
|
+
return positive ? rect.y - 4 - th >= plot.y - fs : rect.y + rect.height + 4 + th <= plot.y + plot.height + fs;
|
|
1406
|
+
};
|
|
1407
|
+
if (position === "center") return fitsInside ? centre() : null;
|
|
1408
|
+
if (position === "inside") return fitsInside ? insideEnd() : stacked ? null : outside();
|
|
1409
|
+
if (stacked) return fitsInside ? centre() : null;
|
|
1410
|
+
if (outsideFits()) return outside();
|
|
1411
|
+
return fitsInside ? insideEnd() : null;
|
|
1412
|
+
}
|
|
1413
|
+
function layoutCartesian(ctx, area, visible) {
|
|
1414
|
+
const { spec, theme, marks, measure, fs } = ctx;
|
|
1415
|
+
const horizontal = spec.orientation === "horizontal";
|
|
1416
|
+
const n = spec.categories.length;
|
|
1417
|
+
const types = new Map(visible.map((s) => [s.id, _chunkJOX6MJW4cjs.chartSeriesType.call(void 0, spec, s)]));
|
|
1418
|
+
const stacked = spec.stacking !== "none";
|
|
1419
|
+
const percent = spec.stacking === "percent";
|
|
1420
|
+
const onSecondary = (s) => s.axis === "secondary";
|
|
1421
|
+
const useSecondary = visible.some(onSecondary);
|
|
1422
|
+
const stacks = /* @__PURE__ */ new Map();
|
|
1423
|
+
if (stacked) {
|
|
1424
|
+
for (const kind of ["bar", "area"]) {
|
|
1425
|
+
for (const secondary of [false, true]) {
|
|
1426
|
+
const group = visible.filter((s) => types.get(s.id) === kind && onSecondary(s) === secondary);
|
|
1427
|
+
if (!group.length) continue;
|
|
1428
|
+
const totals = Array.from({ length: n }, (_, i) => group.reduce((t, s) => t + Math.abs(_nullishCoalesce(s.values[i], () => ( 0))), 0));
|
|
1429
|
+
const pos = Array(n).fill(0);
|
|
1430
|
+
const neg = Array(n).fill(0);
|
|
1431
|
+
for (const s of group) {
|
|
1432
|
+
const segments = s.values.map((raw, i) => {
|
|
1433
|
+
if (raw === null && kind === "bar") return null;
|
|
1434
|
+
let v = _nullishCoalesce(raw, () => ( 0));
|
|
1435
|
+
if (percent) v = totals[i] ? v / totals[i] : 0;
|
|
1436
|
+
const base = v >= 0 ? pos[i] : neg[i];
|
|
1437
|
+
const seg = [base, base + v];
|
|
1438
|
+
if (v >= 0) pos[i] += v;
|
|
1439
|
+
else neg[i] += v;
|
|
1440
|
+
return seg;
|
|
1441
|
+
});
|
|
1442
|
+
stacks.set(s.id, { segments });
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
const range = (secondary) => {
|
|
1448
|
+
let lo = Infinity;
|
|
1449
|
+
let hi = -Infinity;
|
|
1450
|
+
const see = (v) => {
|
|
1451
|
+
lo = Math.min(lo, v);
|
|
1452
|
+
hi = Math.max(hi, v);
|
|
1453
|
+
};
|
|
1454
|
+
const group = visible.filter((s) => onSecondary(s) === secondary);
|
|
1455
|
+
for (const s of group) {
|
|
1456
|
+
const stack = stacks.get(s.id);
|
|
1457
|
+
if (stack) stack.segments.forEach((seg) => seg && (see(seg[0]), see(seg[1])));
|
|
1458
|
+
else s.values.forEach((v) => v !== null && Number.isFinite(v) && see(v));
|
|
1459
|
+
}
|
|
1460
|
+
if (!Number.isFinite(lo)) {
|
|
1461
|
+
lo = 0;
|
|
1462
|
+
hi = 1;
|
|
1463
|
+
}
|
|
1464
|
+
if (group.some((s) => types.get(s.id) === "bar" || types.get(s.id) === "area")) {
|
|
1465
|
+
lo = Math.min(lo, 0);
|
|
1466
|
+
hi = Math.max(hi, 0);
|
|
1467
|
+
}
|
|
1468
|
+
const lineLike = group.some((s) => types.get(s.id) === "line" || types.get(s.id) === "scatter");
|
|
1469
|
+
return { lo, hi, lineLike };
|
|
1470
|
+
};
|
|
1471
|
+
const pctFormat = (f) => percent && !f.style && !f.code ? { ...f, style: "percent" } : f;
|
|
1472
|
+
const p = range(false);
|
|
1473
|
+
const s2 = useSecondary ? range(true) : null;
|
|
1474
|
+
const frame = categoryFrame(ctx, area, {
|
|
1475
|
+
labels: spec.categories,
|
|
1476
|
+
horizontal,
|
|
1477
|
+
primary: { lo: p.lo, hi: p.hi, axis: spec.yAxis, format: pctFormat(spec.yAxis.format), frozen: ctx.options.valueDomain, headroom: p.lineLike, key: "y" },
|
|
1478
|
+
secondary: s2 ? { lo: s2.lo, hi: s2.hi, axis: spec.yAxis2, format: pctFormat(spec.yAxis2.format), frozen: ctx.options.valueDomain2, headroom: s2.lineLike, key: "y2" } : void 0
|
|
1479
|
+
});
|
|
1480
|
+
const { plot, band, catCenter, map, catStart } = frame;
|
|
1481
|
+
const infoOf = (s) => onSecondary(s) && frame.value2 ? frame.value2 : frame.value;
|
|
1482
|
+
const bars = visible.filter((s) => types.get(s.id) === "bar");
|
|
1483
|
+
const barAxes = [...new Set(bars.map((s) => onSecondary(s)))];
|
|
1484
|
+
const slots = stacked ? barAxes.length : bars.length;
|
|
1485
|
+
const groupWidth = band * (1 - spec.gapWidth);
|
|
1486
|
+
const o = Math.max(-1, Math.min(0.99, spec.overlap));
|
|
1487
|
+
const barWidth = slots ? groupWidth / (slots - (slots - 1) * o) : 0;
|
|
1488
|
+
const slotOf = (s) => stacked ? barAxes.indexOf(onSecondary(s)) : bars.indexOf(s);
|
|
1489
|
+
const labelMarks = [];
|
|
1490
|
+
const trendLabels = [];
|
|
1491
|
+
let hiddenLabels = 0;
|
|
1492
|
+
const trendsAllowed = chartSupportsTrendlines(spec.type, spec.stacking);
|
|
1493
|
+
const drawOrder = [...visible].sort((a, b) => order(types.get(a.id)) - order(types.get(b.id)));
|
|
1494
|
+
for (const s of drawOrder) {
|
|
1495
|
+
const si = spec.series.indexOf(s);
|
|
1496
|
+
const color = _chunkJOX6MJW4cjs.chartSeriesColor.call(void 0, spec, si);
|
|
1497
|
+
const type = types.get(s.id);
|
|
1498
|
+
const stack = stacks.get(s.id);
|
|
1499
|
+
const selSeries = ctx.seriesSelected(s.id);
|
|
1500
|
+
const seriesTarget = { kind: "series", seriesId: s.id };
|
|
1501
|
+
const info = infoOf(s);
|
|
1502
|
+
const valuePx = info.px;
|
|
1503
|
+
const labelsOn = (_nullishCoalesce(s.dataLabels, () => ( spec.dataLabels))) && s.values.length <= MAX_CHART_DATA_LABELS;
|
|
1504
|
+
if ((_nullishCoalesce(s.dataLabels, () => ( spec.dataLabels))) && !labelsOn) ctx.warn("data-labels-hidden", `Data labels are off for "${s.name}": it has more than ${MAX_CHART_DATA_LABELS} points.`);
|
|
1505
|
+
const labelFormat = _nullishCoalesce(s.format, () => ( info.format));
|
|
1506
|
+
const dataLabel = (v) => formatChartNumber(v, labelFormat, _nullishCoalesce(labelFormat.decimals, () => ( void 0)));
|
|
1507
|
+
if (type === "bar") {
|
|
1508
|
+
const slot = slotOf(s);
|
|
1509
|
+
s.values.forEach((raw, i) => {
|
|
1510
|
+
const seg = stack ? stack.segments[i] : raw === null ? null : [0, raw];
|
|
1511
|
+
if (!seg || raw === null || !Number.isFinite(raw)) return;
|
|
1512
|
+
const a = valuePx(seg[0]);
|
|
1513
|
+
const b = valuePx(seg[1]);
|
|
1514
|
+
const c0 = catCenter(i) - groupWidth / 2 + slot * barWidth * (1 - o);
|
|
1515
|
+
const cw = barWidth;
|
|
1516
|
+
const target = { kind: "point", seriesId: s.id, index: i };
|
|
1517
|
+
const selected = ctx.isSelected(target);
|
|
1518
|
+
const [x, y] = map({ c: c0, v: Math.min(a, b) });
|
|
1519
|
+
const rect = horizontal ? { x: Math.min(a, b), y: c0, width: Math.abs(b - a), height: cw } : { x, y, width: cw, height: Math.abs(b - a) };
|
|
1520
|
+
const fill = _chunkJOX6MJW4cjs.chartPointColor.call(void 0, spec, si, i);
|
|
1521
|
+
marks.push({
|
|
1522
|
+
kind: "rect",
|
|
1523
|
+
key: `bar-${s.id}-${i}`,
|
|
1524
|
+
role: "bar",
|
|
1525
|
+
...rect,
|
|
1526
|
+
fill,
|
|
1527
|
+
radius: Math.min(2, cw / 4),
|
|
1528
|
+
target,
|
|
1529
|
+
selected: selected || selSeries,
|
|
1530
|
+
...selSeries ? { stroke: theme.selection, strokeWidth: 1.5 } : {}
|
|
1531
|
+
});
|
|
1532
|
+
if (selected) ctx.outline(`bar-${s.id}-${i}-selection`, { x: rect.x - 2, y: rect.y - 2, width: rect.width + 4, height: rect.height + 4 });
|
|
1533
|
+
if (labelsOn) {
|
|
1534
|
+
const v = percent ? seg[1] - seg[0] : raw;
|
|
1535
|
+
const text = dataLabel(v);
|
|
1536
|
+
const at = barLabel(ctx, text, rect, horizontal, seg[1] >= seg[0], stacked, fill, plot);
|
|
1537
|
+
if (!at) {
|
|
1538
|
+
hiddenLabels++;
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
labelMarks.push({ kind: "text", key: `label-${s.id}-${i}`, role: "data-label", ...at, text, size: fs - 1, target });
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
} else {
|
|
1545
|
+
const curve = _nullishCoalesce(s.curve, () => ( spec.curve));
|
|
1546
|
+
const pointValue = (i) => {
|
|
1547
|
+
const v = stack ? _nullishCoalesce(_optionalChain([stack, 'access', _14 => _14.segments, 'access', _15 => _15[i], 'optionalAccess', _16 => _16[1]]), () => ( null)) : s.values[i];
|
|
1548
|
+
return v !== null && Number.isFinite(v) ? v : null;
|
|
1549
|
+
};
|
|
1550
|
+
const runs = [];
|
|
1551
|
+
let run = [];
|
|
1552
|
+
for (let i = 0; i < n; i++) {
|
|
1553
|
+
const v = pointValue(i);
|
|
1554
|
+
if (v === null) {
|
|
1555
|
+
if (run.length) runs.push(run);
|
|
1556
|
+
run = [];
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
run.push({ c: catCenter(i), v: valuePx(v) });
|
|
1560
|
+
}
|
|
1561
|
+
if (run.length) runs.push(run);
|
|
1562
|
+
const thin = (r) => {
|
|
1563
|
+
if (r.length <= MAX_CHART_LINE_POINTS) return r;
|
|
1564
|
+
const pts = decimateChartPoints(r.map((p2) => ({ x: p2.c, y: p2.v, p: p2 })), MAX_CHART_LINE_POINTS).map((q) => q.p);
|
|
1565
|
+
ctx.warn("points-decimated", `"${s.name}" has ${r.length} points; its line is drawn with ${pts.length} (one column of pixels each).`);
|
|
1566
|
+
return pts;
|
|
1567
|
+
};
|
|
1568
|
+
const drawn = runs.map(thin);
|
|
1569
|
+
if (type === "area") {
|
|
1570
|
+
const parts = drawn.map((r) => {
|
|
1571
|
+
const baseline = stack ? r.map((pt) => ({ c: pt.c, v: valuePx(_nullishCoalesce(_optionalChain([stack, 'access', _17 => _17.segments, 'access', _18 => _18[Math.round((pt.c - catStart) / band - 0.5)], 'optionalAccess', _19 => _19[0]]), () => ( 0))) })).reverse() : [
|
|
1572
|
+
{ c: r[r.length - 1].c, v: info.zero },
|
|
1573
|
+
{ c: r[0].c, v: info.zero }
|
|
1574
|
+
];
|
|
1575
|
+
return `${curvePath(r, curve, map)}${stack ? curvePath(baseline, curve, map, false) : baseline.map((pt) => `L${map(pt).map(r2).join(",")}`).join("")}Z`;
|
|
1576
|
+
});
|
|
1577
|
+
marks.push({ kind: "path", key: `area-${s.id}`, role: "area", d: parts.join(""), fill: color, opacity: 0.22, target: seriesTarget, selected: selSeries });
|
|
1578
|
+
}
|
|
1579
|
+
if (type === "line" || type === "area") {
|
|
1580
|
+
marks.push({ kind: "path", key: `line-${s.id}`, role: "line", d: drawn.map((r) => curvePath(r, curve, map)).join(""), fill: "none", stroke: color, strokeWidth: selSeries ? 3.5 : 2, target: seriesTarget, selected: selSeries });
|
|
1581
|
+
}
|
|
1582
|
+
const marker = _nullishCoalesce(s.marker, () => ( (type === "scatter" || spec.points ? "circle" : "none")));
|
|
1583
|
+
const showDots = marker !== "none";
|
|
1584
|
+
const thinMarkers = n > MAX_CHART_MARKERS;
|
|
1585
|
+
const cells = /* @__PURE__ */ new Set();
|
|
1586
|
+
for (let i = 0; i < n; i++) {
|
|
1587
|
+
const v = pointValue(i);
|
|
1588
|
+
if (v === null || s.values[i] === null) continue;
|
|
1589
|
+
const [cx, cy] = map({ c: catCenter(i), v: valuePx(v) });
|
|
1590
|
+
if (thinMarkers) {
|
|
1591
|
+
const cell = `${Math.round(cx / 2)},${Math.round(cy / 2)}`;
|
|
1592
|
+
if (cells.has(cell)) continue;
|
|
1593
|
+
cells.add(cell);
|
|
1594
|
+
}
|
|
1595
|
+
const target = { kind: "point", seriesId: s.id, index: i };
|
|
1596
|
+
const selected = ctx.isSelected(target);
|
|
1597
|
+
const r = type === "scatter" ? 4.5 : 3.5;
|
|
1598
|
+
const pointColor = _chunkJOX6MJW4cjs.chartPointColor.call(void 0, spec, si, i);
|
|
1599
|
+
const fill = showDots ? type === "scatter" ? pointColor : theme.surface : "transparent";
|
|
1600
|
+
marks.push(
|
|
1601
|
+
pointMark({ key: `point-${s.id}-${i}`, target, selected: selected || selSeries, opacity: type === "scatter" ? 0.85 : void 0 }, showDots ? marker : "hit", cx, cy, showDots ? r : 6, fill, showDots ? pointColor : void 0, 2)
|
|
1602
|
+
);
|
|
1603
|
+
if (selected) marks.push({ kind: "circle", key: `point-${s.id}-${i}-selection`, role: "selection", cx, cy, r: r + 3.5, fill: "none", stroke: theme.selection, strokeWidth: 1.5 });
|
|
1604
|
+
if (labelsOn) {
|
|
1605
|
+
const raw = s.values[i];
|
|
1606
|
+
const shown = percent && stack ? stack.segments[i][1] - stack.segments[i][0] : raw;
|
|
1607
|
+
labelMarks.push({
|
|
1608
|
+
kind: "text",
|
|
1609
|
+
key: `label-${s.id}-${i}`,
|
|
1610
|
+
role: "data-label",
|
|
1611
|
+
x: horizontal ? cx + 8 : cx,
|
|
1612
|
+
y: horizontal ? cy : cy - 8,
|
|
1613
|
+
text: dataLabel(shown),
|
|
1614
|
+
anchor: horizontal ? "start" : "middle",
|
|
1615
|
+
baseline: horizontal ? "middle" : "bottom",
|
|
1616
|
+
size: fs - 1,
|
|
1617
|
+
fill: theme.text,
|
|
1618
|
+
target
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
if (trendsAllowed && _optionalChain([s, 'access', _20 => _20.trendlines, 'optionalAccess', _21 => _21.length])) {
|
|
1624
|
+
const points = [];
|
|
1625
|
+
s.values.forEach((v, i) => v !== null && Number.isFinite(v) && points.push([i + 1, v]));
|
|
1626
|
+
const toPx = (x, y) => map({ c: catStart + band * (x - 0.5), v: info.raw(y) });
|
|
1627
|
+
drawTrendlines(ctx, plot, s, color, points, toPx, trendLabels);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
marks.push(...labelMarks);
|
|
1631
|
+
drawTrendLabels(ctx, plot, trendLabels);
|
|
1632
|
+
if (hiddenLabels) ctx.warn("data-labels-hidden", `${hiddenLabels} data label${hiddenLabels === 1 ? "" : "s"} did not fit and ${hiddenLabels === 1 ? "is" : "are"} hidden.`);
|
|
1633
|
+
return {
|
|
1634
|
+
plot,
|
|
1635
|
+
valueScale: toValueScale(frame.value, horizontal ? "x" : "y"),
|
|
1636
|
+
...frame.value2 ? { valueScale2: toValueScale(frame.value2, horizontal ? "x" : "y") } : {},
|
|
1637
|
+
percent
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
function layoutXy(ctx, area, visible) {
|
|
1641
|
+
const { spec, theme, marks, measure, fs } = ctx;
|
|
1642
|
+
const bubble = spec.type === "bubble";
|
|
1643
|
+
const pointsOf = (s) => s.values.map((y, i) => ({ i, x: _nullishCoalesce(_optionalChain([s, 'access', _22 => _22.x, 'optionalAccess', _23 => _23[i]]), () => ( (bubble || !s.x ? i + 1 : null))), y, size: _nullishCoalesce(_optionalChain([s, 'access', _24 => _24.sizes, 'optionalAccess', _25 => _25[i]]), () => ( null)) })).filter((p) => p.x !== null && p.y !== null && Number.isFinite(p.x) && Number.isFinite(p.y));
|
|
1644
|
+
const all = visible.map((s) => ({ s, pts: pointsOf(s) }));
|
|
1645
|
+
let xlo = Infinity;
|
|
1646
|
+
let xhi = -Infinity;
|
|
1647
|
+
let ylo = Infinity;
|
|
1648
|
+
let yhi = -Infinity;
|
|
1649
|
+
let maxSize = 0;
|
|
1650
|
+
for (const { s, pts } of all) {
|
|
1651
|
+
for (const p of pts) {
|
|
1652
|
+
xlo = Math.min(xlo, p.x);
|
|
1653
|
+
xhi = Math.max(xhi, p.x);
|
|
1654
|
+
ylo = Math.min(ylo, p.y);
|
|
1655
|
+
yhi = Math.max(yhi, p.y);
|
|
1656
|
+
if (p.size !== null && p.size > maxSize) maxSize = p.size;
|
|
1657
|
+
}
|
|
1658
|
+
for (const t of _nullishCoalesce(s.trendlines, () => ( []))) {
|
|
1659
|
+
if (!pts.length) continue;
|
|
1660
|
+
xlo = Math.min(xlo, Math.min(...pts.map((p) => p.x)) - (_nullishCoalesce(t.backward, () => ( 0))));
|
|
1661
|
+
xhi = Math.max(xhi, Math.max(...pts.map((p) => p.x)) + (_nullishCoalesce(t.forward, () => ( 0))));
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
if (!Number.isFinite(xlo)) [xlo, xhi, ylo, yhi] = [0, 1, 0, 1];
|
|
1665
|
+
const titleBand = fs * 1.3 + 6;
|
|
1666
|
+
const yAxis = spec.yAxis;
|
|
1667
|
+
const xAxis = spec.xAxis;
|
|
1668
|
+
const yEst = axisScale(ylo, yhi, area.height, yAxis, ctx.options.valueDomain, true);
|
|
1669
|
+
const yText = (t, step) => formatChartNumber(t, yAxis.format, _nullishCoalesce(yAxis.format.decimals, () => ( stepDecimals(step))));
|
|
1670
|
+
const left = (yAxis.show ? Math.max(0, ...yEst.ticks.map((t) => measure(yText(t, yEst.step), fs))) + 8 : 2) + (yAxis.title ? titleBand : 0);
|
|
1671
|
+
const bottom = (xAxis.show ? fs * 1.6 : 2) + (xAxis.title ? titleBand : 0);
|
|
1672
|
+
const plot = { x: area.x + left, y: area.y + fs / 2 + 2, width: Math.max(1, area.width - left - 12), height: Math.max(1, area.height - bottom - fs / 2 - 2) };
|
|
1673
|
+
const maxR = bubble ? Math.max(4, Math.min(plot.width, plot.height) * 0.08) : 0;
|
|
1674
|
+
const inset = bubble ? maxR : 0;
|
|
1675
|
+
const yScale = axisScale(ylo, yhi, plot.height, yAxis, ctx.options.valueDomain, true);
|
|
1676
|
+
const xScale = axisScale(xlo, xhi, plot.width, xAxis, ctx.options.xDomain, true);
|
|
1677
|
+
const xRange = [plot.x + inset, plot.x + plot.width - inset];
|
|
1678
|
+
const yRange = [plot.y + plot.height - inset, plot.y + inset];
|
|
1679
|
+
const xRaw = (x) => linearMap(x, [xScale.min, xScale.max], xRange);
|
|
1680
|
+
const yRaw = (y) => linearMap(y, [yScale.min, yScale.max], yRange);
|
|
1681
|
+
const xPx = (x) => xRaw(Math.min(xScale.max, Math.max(xScale.min, x)));
|
|
1682
|
+
const yPx = (y) => yRaw(Math.min(yScale.max, Math.max(yScale.min, y)));
|
|
1683
|
+
const xTarget = { kind: "axis", axis: "x" };
|
|
1684
|
+
const yTarget = { kind: "axis", axis: "y" };
|
|
1685
|
+
if (yAxis.gridlines) yScale.ticks.forEach((t, i) => marks.push({ kind: "line", key: `grid-y-${i}`, role: "grid", x1: plot.x, y1: yPx(t), x2: plot.x + plot.width, y2: yPx(t), stroke: theme.grid, strokeWidth: 1 }));
|
|
1686
|
+
if (xAxis.gridlines) xScale.ticks.forEach((t, i) => marks.push({ kind: "line", key: `grid-x-${i}`, role: "grid", x1: xPx(t), y1: plot.y, x2: xPx(t), y2: plot.y + plot.height, stroke: theme.grid, strokeWidth: 1 }));
|
|
1687
|
+
if (yAxis.show) yScale.ticks.forEach((t, i) => marks.push({ kind: "text", key: `tick-y-${i}`, role: "tick-label", x: plot.x - 8, y: yPx(t), text: yText(t, yScale.step), anchor: "end", baseline: "middle", size: fs, fill: theme.mutedText, target: yTarget }));
|
|
1688
|
+
if (xAxis.show) {
|
|
1689
|
+
const y0 = yScale.min <= 0 && yScale.max >= 0 ? yPx(0) : plot.y + plot.height;
|
|
1690
|
+
marks.push({ kind: "line", key: "axis-x", role: "axis", x1: plot.x, y1: y0, x2: plot.x + plot.width, y2: y0, stroke: theme.axis, strokeWidth: 1, target: xTarget });
|
|
1691
|
+
const texts = xScale.ticks.map((t) => formatChartNumber(t, xAxis.format, _nullishCoalesce(xAxis.format.decimals, () => ( stepDecimals(xScale.step)))));
|
|
1692
|
+
const widest = Math.max(0, ...texts.map((t) => measure(t, fs)));
|
|
1693
|
+
const spacing = xScale.ticks.length > 1 ? Math.abs(xPx(xScale.ticks[1]) - xPx(xScale.ticks[0])) : Infinity;
|
|
1694
|
+
const every = Math.max(1, Math.ceil((widest + 8) / spacing));
|
|
1695
|
+
xScale.ticks.forEach((t, i) => {
|
|
1696
|
+
if (i % every) return;
|
|
1697
|
+
marks.push({ kind: "text", key: `tick-x-${i}`, role: "tick-label", x: xPx(t), y: plot.y + plot.height + 6, text: texts[i], anchor: "middle", baseline: "top", size: fs, fill: theme.mutedText, target: xTarget });
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
if (xAxis.title) marks.push({ kind: "text", key: "axis-title-x", role: "axis-title", x: plot.x + plot.width / 2, y: area.y + area.height, text: xAxis.title, anchor: "middle", baseline: "bottom", size: fs + 1, weight: 500, fill: theme.text, target: xTarget });
|
|
1701
|
+
if (yAxis.title) marks.push({ kind: "text", key: "axis-title-y", role: "axis-title", x: area.x, y: plot.y + plot.height / 2, text: yAxis.title, anchor: "middle", baseline: "top", size: fs + 1, weight: 500, fill: theme.text, rotate: -90, target: yTarget });
|
|
1702
|
+
for (const axis of ["x", "y"]) {
|
|
1703
|
+
if (!ctx.isSelected({ kind: "axis", axis })) continue;
|
|
1704
|
+
ctx.outline(`axis-${axis}-selection`, axis === "y" ? { x: area.x, y: plot.y - fs / 2, width: left - 2, height: plot.height + fs } : { x: plot.x - 4, y: plot.y + plot.height + 2, width: plot.width + 8, height: area.y + area.height - plot.y - plot.height - 2 });
|
|
1705
|
+
}
|
|
1706
|
+
const labelMarks = [];
|
|
1707
|
+
const trendLabels = [];
|
|
1708
|
+
for (const { s, pts } of all) {
|
|
1709
|
+
const si = spec.series.indexOf(s);
|
|
1710
|
+
const color = _chunkJOX6MJW4cjs.chartSeriesColor.call(void 0, spec, si);
|
|
1711
|
+
const type = _chunkJOX6MJW4cjs.chartSeriesType.call(void 0, spec, s);
|
|
1712
|
+
const selSeries = ctx.seriesSelected(s.id);
|
|
1713
|
+
const seriesTarget = { kind: "series", seriesId: s.id };
|
|
1714
|
+
if (type === "line" || type === "area") {
|
|
1715
|
+
const xy = pts.map((p) => ({ x: xPx(p.x), y: yPx(p.y) }));
|
|
1716
|
+
const thinned = xy.length > MAX_CHART_LINE_POINTS ? decimateChartPoints(xy, MAX_CHART_LINE_POINTS) : xy;
|
|
1717
|
+
if (thinned.length < xy.length) ctx.warn("points-decimated", `"${s.name}" has ${xy.length} points; its line is drawn with ${thinned.length}.`);
|
|
1718
|
+
const d = curvePath(thinned.map((p) => ({ c: p.x, v: p.y })), _nullishCoalesce(s.curve, () => ( spec.curve)), (p) => [p.c, p.v]);
|
|
1719
|
+
if (d) marks.push({ kind: "path", key: `line-${s.id}`, role: "line", d, fill: "none", stroke: color, strokeWidth: selSeries ? 3.5 : 2, target: seriesTarget, selected: selSeries });
|
|
1720
|
+
}
|
|
1721
|
+
const marker = bubble ? "circle" : _nullishCoalesce(s.marker, () => ( (type === "scatter" || spec.points ? "circle" : "none")));
|
|
1722
|
+
const ordered = bubble ? [...pts].sort((a, b) => (_nullishCoalesce(b.size, () => ( 0))) - (_nullishCoalesce(a.size, () => ( 0)))) : pts;
|
|
1723
|
+
const labelsOn = (_nullishCoalesce(s.dataLabels, () => ( spec.dataLabels))) && pts.length <= MAX_CHART_DATA_LABELS;
|
|
1724
|
+
const labelFormat = _nullishCoalesce(s.format, () => ( yAxis.format));
|
|
1725
|
+
const cells = /* @__PURE__ */ new Set();
|
|
1726
|
+
for (const p of ordered) {
|
|
1727
|
+
const cx = xPx(p.x);
|
|
1728
|
+
const cy = yPx(p.y);
|
|
1729
|
+
if (pts.length > MAX_CHART_MARKERS) {
|
|
1730
|
+
const cell = `${Math.round(cx / 2)},${Math.round(cy / 2)}`;
|
|
1731
|
+
if (cells.has(cell)) continue;
|
|
1732
|
+
cells.add(cell);
|
|
1733
|
+
}
|
|
1734
|
+
const target = { kind: "point", seriesId: s.id, index: p.i };
|
|
1735
|
+
const selected = ctx.isSelected(target);
|
|
1736
|
+
const pointColor = _chunkJOX6MJW4cjs.chartPointColor.call(void 0, spec, si, p.i);
|
|
1737
|
+
const r = bubble ? maxSize > 0 && p.size !== null && p.size > 0 ? Math.max(2, Math.sqrt(p.size / maxSize) * maxR) : 2 : 4.5;
|
|
1738
|
+
if (bubble) marks.push({ kind: "circle", key: `point-${s.id}-${p.i}`, role: "point", cx, cy, r, fill: pointColor, stroke: pointColor, strokeWidth: 1, opacity: 0.6, target, selected: selected || selSeries });
|
|
1739
|
+
else marks.push(pointMark({ key: `point-${s.id}-${p.i}`, target, selected: selected || selSeries, opacity: 0.85 }, marker === "none" ? "hit" : marker, cx, cy, marker === "none" ? 6 : r, marker === "none" ? "transparent" : pointColor, marker === "none" ? void 0 : pointColor, 2));
|
|
1740
|
+
if (selected) marks.push({ kind: "circle", key: `point-${s.id}-${p.i}-selection`, role: "selection", cx, cy, r: r + 3.5, fill: "none", stroke: theme.selection, strokeWidth: 1.5 });
|
|
1741
|
+
if (labelsOn) labelMarks.push({ kind: "text", key: `label-${s.id}-${p.i}`, role: "data-label", x: cx, y: cy - r - 4, text: formatChartNumber(p.y, labelFormat), anchor: "middle", baseline: "bottom", size: fs - 1, fill: theme.text, target });
|
|
1742
|
+
}
|
|
1743
|
+
if (_optionalChain([s, 'access', _26 => _26.trendlines, 'optionalAccess', _27 => _27.length])) drawTrendlines(ctx, plot, s, color, pts.map((p) => [p.x, p.y]), (x, y) => [xRaw(x), yRaw(y)], trendLabels);
|
|
1744
|
+
}
|
|
1745
|
+
marks.push(...labelMarks);
|
|
1746
|
+
drawTrendLabels(ctx, plot, trendLabels);
|
|
1747
|
+
return {
|
|
1748
|
+
plot,
|
|
1749
|
+
valueScale: { axis: "y", domain: [yScale.min, yScale.max], range: yRange, ticks: yScale.ticks },
|
|
1750
|
+
xScale: { axis: "x", domain: [xScale.min, xScale.max], range: xRange, ticks: xScale.ticks }
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
function trendlineLegendItems(spec, color) {
|
|
1754
|
+
if (!chartSupportsTrendlines(spec.type, spec.stacking)) return [];
|
|
1755
|
+
return spec.series.flatMap(
|
|
1756
|
+
(s, si) => s.hidden ? [] : (_nullishCoalesce(s.trendlines, () => ( []))).map((t, ti) => ({ label: chartTrendlineName(t, s.name), color: _nullishCoalesce(t.color, () => ( color(si))), target: { kind: "trendline", seriesId: s.id, index: ti }, after: s.id }))
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// src/chart/layout-families.ts
|
|
1761
|
+
var sliceColor = (ctx, series, i) => _optionalChain([series, 'access', _28 => _28.pointColors, 'optionalAccess', _29 => _29[i]]) || _chunkJOX6MJW4cjs.chartSliceColor.call(void 0, ctx.spec, i);
|
|
1762
|
+
function layoutPie(ctx, area, series) {
|
|
1763
|
+
const { spec, theme, marks, fs, selection } = ctx;
|
|
1764
|
+
const values = spec.categories.map((_, i) => {
|
|
1765
|
+
const v = series.values[i];
|
|
1766
|
+
return v !== null && Number.isFinite(v) ? Math.max(0, v) : 0;
|
|
1767
|
+
});
|
|
1768
|
+
const total = values.reduce((a, b) => a + b, 0);
|
|
1769
|
+
const labelsOn = _nullishCoalesce(series.dataLabels, () => ( spec.dataLabels));
|
|
1770
|
+
const labelRoom = labelsOn ? fs * 2.2 : 4;
|
|
1771
|
+
const outer = Math.max(4, Math.min(area.width, area.height) / 2 - labelRoom);
|
|
1772
|
+
const inner = spec.type === "donut" ? outer * spec.innerRadius : 0;
|
|
1773
|
+
const cx = area.x + area.width / 2;
|
|
1774
|
+
const cy = area.y + area.height / 2;
|
|
1775
|
+
const plot = { x: cx - outer, y: cy - outer, width: outer * 2, height: outer * 2 };
|
|
1776
|
+
if (total <= 0) {
|
|
1777
|
+
marks.push({ kind: "text", key: "empty", role: "empty", x: cx, y: cy, text: "No positive values", anchor: "middle", baseline: "middle", size: fs + 1, fill: theme.mutedText });
|
|
1778
|
+
return { plot };
|
|
1779
|
+
}
|
|
1780
|
+
let angle = 0;
|
|
1781
|
+
const format = _nullishCoalesce(series.format, () => ( spec.yAxis.format));
|
|
1782
|
+
let hidden = 0;
|
|
1783
|
+
values.forEach((v, i) => {
|
|
1784
|
+
if (v <= 0) return;
|
|
1785
|
+
const sweep = v / total * Math.PI * 2;
|
|
1786
|
+
const start = angle;
|
|
1787
|
+
const end = angle + sweep;
|
|
1788
|
+
angle = end;
|
|
1789
|
+
const target = { kind: "point", seriesId: series.id, index: i };
|
|
1790
|
+
const selected = ctx.isSelected(target) || ctx.isSelected({ kind: "category", index: i });
|
|
1791
|
+
const seriesSel = _optionalChain([selection, 'optionalAccess', _30 => _30.kind]) === "series" && selection.seriesId === series.id;
|
|
1792
|
+
const mid = (start + end) / 2;
|
|
1793
|
+
const off = selected ? 6 : 0;
|
|
1794
|
+
const ox = cx + off * Math.sin(mid);
|
|
1795
|
+
const oy = cy - off * Math.cos(mid);
|
|
1796
|
+
marks.push({
|
|
1797
|
+
kind: "path",
|
|
1798
|
+
key: `slice-${i}`,
|
|
1799
|
+
role: "slice",
|
|
1800
|
+
d: arcPath(ox, oy, outer, inner, start, end),
|
|
1801
|
+
fill: sliceColor(ctx, series, i),
|
|
1802
|
+
stroke: selected || seriesSel ? theme.selection : theme.surface,
|
|
1803
|
+
strokeWidth: selected || seriesSel ? 2 : 1,
|
|
1804
|
+
target,
|
|
1805
|
+
selected: selected || seriesSel
|
|
1806
|
+
});
|
|
1807
|
+
if (!labelsOn) return;
|
|
1808
|
+
if (sweep <= 0.12) {
|
|
1809
|
+
hidden++;
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
const share = v / total;
|
|
1813
|
+
const text = format.style || format.code ? formatChartNumber(v, format) : `${Math.round(share * 100)}%`;
|
|
1814
|
+
const lr = outer + fs * 1.1;
|
|
1815
|
+
const lx = ox + lr * Math.sin(mid);
|
|
1816
|
+
const ly = oy - lr * Math.cos(mid);
|
|
1817
|
+
const anchor = Math.abs(Math.sin(mid)) < 0.2 ? "middle" : Math.sin(mid) > 0 ? "start" : "end";
|
|
1818
|
+
marks.push({ kind: "text", key: `label-slice-${i}`, role: "data-label", x: lx, y: ly, text, anchor, baseline: "middle", size: fs, fill: theme.text, target });
|
|
1819
|
+
});
|
|
1820
|
+
if (hidden) ctx.warn("data-labels-hidden", `${hidden} slice label${hidden === 1 ? " is" : "s are"} hidden: the slices are too thin.`);
|
|
1821
|
+
if (spec.type === "donut" && inner > fs * 2) {
|
|
1822
|
+
marks.push({ kind: "text", key: "donut-total", role: "data-label", x: cx, y: cy, text: formatChartNumber(total, format), anchor: "middle", baseline: "middle", size: Math.min(theme.titleSize + 4, inner / 2), weight: 600, fill: theme.text, target: { kind: "series", seriesId: series.id } });
|
|
1823
|
+
}
|
|
1824
|
+
return { plot };
|
|
1825
|
+
}
|
|
1826
|
+
function layoutRadar(ctx, area, visible) {
|
|
1827
|
+
const { spec, theme, marks, fs, measure } = ctx;
|
|
1828
|
+
const n = spec.categories.length;
|
|
1829
|
+
const labelW = Math.min(area.width * 0.25, Math.max(0, ...spec.categories.map((c) => measure(c, fs))));
|
|
1830
|
+
const radius = Math.max(8, Math.min(area.width / 2 - labelW - 8, area.height / 2 - fs * 1.6));
|
|
1831
|
+
const cx = area.x + area.width / 2;
|
|
1832
|
+
const cy = area.y + area.height / 2;
|
|
1833
|
+
let lo = 0;
|
|
1834
|
+
let hi = -Infinity;
|
|
1835
|
+
for (const s of visible) for (const v of s.values) if (v !== null && Number.isFinite(v)) lo = Math.min(lo, v), hi = Math.max(hi, v);
|
|
1836
|
+
if (!Number.isFinite(hi)) hi = 1;
|
|
1837
|
+
const scale = axisScale(lo, hi, radius, spec.yAxis, ctx.options.valueDomain);
|
|
1838
|
+
const rOf = (v) => linearMap(Math.min(scale.max, Math.max(scale.min, v)), [scale.min, scale.max], [0, radius]);
|
|
1839
|
+
const angle = (i) => i / n * Math.PI * 2;
|
|
1840
|
+
const at = (i, r) => [cx + r * Math.sin(angle(i)), cy - r * Math.cos(angle(i))];
|
|
1841
|
+
const yTarget = { kind: "axis", axis: "y" };
|
|
1842
|
+
if (spec.yAxis.gridlines)
|
|
1843
|
+
scale.ticks.forEach((t, k) => {
|
|
1844
|
+
const r = rOf(t);
|
|
1845
|
+
if (r <= 0) return;
|
|
1846
|
+
const d = Array.from({ length: n }, (_, i) => at(i, r)).map(([x, y], i) => `${i ? "L" : "M"}${r2(x)},${r2(y)}`).join("") + "Z";
|
|
1847
|
+
marks.push({ kind: "path", key: `grid-ring-${k}`, role: "grid", d, fill: "none", stroke: theme.grid, strokeWidth: 1 });
|
|
1848
|
+
});
|
|
1849
|
+
for (let i = 0; i < n; i++) {
|
|
1850
|
+
const [x, y] = at(i, radius);
|
|
1851
|
+
marks.push({ kind: "line", key: `spoke-${i}`, role: "axis", x1: cx, y1: cy, x2: x, y2: y, stroke: theme.grid, strokeWidth: 1, target: { kind: "axis", axis: "x" } });
|
|
1852
|
+
if (spec.xAxis.show) {
|
|
1853
|
+
const [lx, ly] = at(i, radius + 8);
|
|
1854
|
+
const s = Math.sin(angle(i));
|
|
1855
|
+
const c = Math.cos(angle(i));
|
|
1856
|
+
const target = { kind: "category", index: i };
|
|
1857
|
+
const selected = ctx.isSelected(target);
|
|
1858
|
+
marks.push({ kind: "text", key: `tick-x-${i}`, role: "tick-label", x: lx, y: ly, text: truncate(spec.categories[i], labelW, fs, measure), anchor: Math.abs(s) < 0.2 ? "middle" : s > 0 ? "start" : "end", baseline: Math.abs(c) < 0.2 ? "middle" : c > 0 ? "bottom" : "top", size: fs, fill: selected ? theme.selection : theme.mutedText, target, selected });
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
if (spec.yAxis.show)
|
|
1862
|
+
scale.ticks.forEach((t, k) => {
|
|
1863
|
+
const r = rOf(t);
|
|
1864
|
+
marks.push({ kind: "text", key: `tick-y-${k}`, role: "tick-label", x: cx + 4, y: cy - r, text: formatChartNumber(t, spec.yAxis.format, _nullishCoalesce(spec.yAxis.format.decimals, () => ( stepDecimals(scale.step)))), anchor: "start", baseline: "bottom", size: fs - 1, fill: theme.mutedText, target: yTarget });
|
|
1865
|
+
});
|
|
1866
|
+
for (const s of visible) {
|
|
1867
|
+
const si = spec.series.indexOf(s);
|
|
1868
|
+
const color = _chunkJOX6MJW4cjs.chartSeriesColor.call(void 0, spec, si);
|
|
1869
|
+
const selSeries = ctx.seriesSelected(s.id);
|
|
1870
|
+
const pts = s.values.map((v, i) => v !== null && Number.isFinite(v) ? at(i, rOf(v)) : null);
|
|
1871
|
+
const present = pts.filter((p) => p !== null);
|
|
1872
|
+
if (!present.length) continue;
|
|
1873
|
+
const d = present.map(([x, y], i) => `${i ? "L" : "M"}${r2(x)},${r2(y)}`).join("") + "Z";
|
|
1874
|
+
marks.push({ kind: "path", key: `area-${s.id}`, role: "area", d, fill: color, opacity: s.type === "line" ? 0 : 0.15, target: { kind: "series", seriesId: s.id }, selected: selSeries });
|
|
1875
|
+
marks.push({ kind: "path", key: `line-${s.id}`, role: "line", d, fill: "none", stroke: color, strokeWidth: selSeries ? 3.5 : 2, target: { kind: "series", seriesId: s.id }, selected: selSeries });
|
|
1876
|
+
pts.forEach((p, i) => {
|
|
1877
|
+
if (!p) return;
|
|
1878
|
+
const target = { kind: "point", seriesId: s.id, index: i };
|
|
1879
|
+
const selected = ctx.isSelected(target);
|
|
1880
|
+
marks.push({ kind: "circle", key: `point-${s.id}-${i}`, role: "point", cx: p[0], cy: p[1], r: spec.points ? 3.5 : 6, fill: spec.points ? theme.surface : "transparent", ...spec.points ? { stroke: color, strokeWidth: 2 } : {}, target, selected: selected || selSeries });
|
|
1881
|
+
if (selected) marks.push({ kind: "circle", key: `point-${s.id}-${i}-selection`, role: "selection", cx: p[0], cy: p[1], r: 7, fill: "none", stroke: theme.selection, strokeWidth: 1.5 });
|
|
1882
|
+
if (_nullishCoalesce(s.dataLabels, () => ( spec.dataLabels))) marks.push({ kind: "text", key: `label-${s.id}-${i}`, role: "data-label", x: p[0], y: p[1] - 6, text: formatChartNumber(s.values[i], _nullishCoalesce(s.format, () => ( spec.yAxis.format))), anchor: "middle", baseline: "bottom", size: fs - 1, fill: theme.text, target });
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
return { plot: { x: cx - radius, y: cy - radius, width: radius * 2, height: radius * 2 } };
|
|
1886
|
+
}
|
|
1887
|
+
function layoutWaterfall(ctx, area, series) {
|
|
1888
|
+
const { spec, theme, marks, fs } = ctx;
|
|
1889
|
+
const isTotal = (label) => /\b(sub)?total\b/i.test(label);
|
|
1890
|
+
const steps = [];
|
|
1891
|
+
let running = 0;
|
|
1892
|
+
spec.categories.forEach((label, i) => {
|
|
1893
|
+
const v = series.values[i];
|
|
1894
|
+
if (isTotal(label)) {
|
|
1895
|
+
steps.push({ label, index: i, from: 0, to: running, total: true });
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
const delta = v !== null && Number.isFinite(v) ? v : 0;
|
|
1899
|
+
steps.push({ label, index: i, from: running, to: running + delta, total: false });
|
|
1900
|
+
running += delta;
|
|
1901
|
+
});
|
|
1902
|
+
if (spec.showTotal && !(steps.length && steps[steps.length - 1].total)) steps.push({ label: "Total", index: null, from: 0, to: running, total: true });
|
|
1903
|
+
let lo = 0;
|
|
1904
|
+
let hi = 0;
|
|
1905
|
+
for (const s of steps) {
|
|
1906
|
+
lo = Math.min(lo, s.from, s.to);
|
|
1907
|
+
hi = Math.max(hi, s.from, s.to);
|
|
1908
|
+
}
|
|
1909
|
+
const horizontal = spec.orientation === "horizontal";
|
|
1910
|
+
const frame = categoryFrame(ctx, area, {
|
|
1911
|
+
labels: steps.map((s) => s.label),
|
|
1912
|
+
horizontal,
|
|
1913
|
+
primary: { lo, hi, axis: spec.yAxis, format: spec.yAxis.format, frozen: ctx.options.valueDomain, key: "y" },
|
|
1914
|
+
labelTarget: (k) => steps[k].index !== null ? { kind: "category", index: steps[k].index } : void 0
|
|
1915
|
+
});
|
|
1916
|
+
const { band, catCenter, map, value } = frame;
|
|
1917
|
+
const w = band * (1 - spec.gapWidth);
|
|
1918
|
+
const color = _chunkJOX6MJW4cjs.chartSeriesColor.call(void 0, spec, spec.series.indexOf(series));
|
|
1919
|
+
const format = _nullishCoalesce(series.format, () => ( spec.yAxis.format));
|
|
1920
|
+
const labelsOn = _nullishCoalesce(series.dataLabels, () => ( spec.dataLabels));
|
|
1921
|
+
let prevEnd = null;
|
|
1922
|
+
steps.forEach((s, k) => {
|
|
1923
|
+
const a = value.px(s.from);
|
|
1924
|
+
const b = value.px(s.to);
|
|
1925
|
+
const c0 = catCenter(k) - w / 2;
|
|
1926
|
+
const [x, y] = map({ c: c0, v: Math.min(a, b) });
|
|
1927
|
+
const rect = horizontal ? { x: Math.min(a, b), y: c0, width: Math.max(1, Math.abs(b - a)), height: w } : { x, y, width: w, height: Math.max(1, Math.abs(b - a)) };
|
|
1928
|
+
const fill = s.total ? color : s.to >= s.from ? theme.positive : theme.negative;
|
|
1929
|
+
const target = s.index !== null ? { kind: "point", seriesId: series.id, index: s.index } : { kind: "series", seriesId: series.id };
|
|
1930
|
+
const selected = ctx.isSelected(target);
|
|
1931
|
+
marks.push({ kind: "rect", key: `bar-${series.id}-${_nullishCoalesce(s.index, () => ( "total"))}`, role: "bar", ...rect, fill, radius: Math.min(2, w / 4), target, selected });
|
|
1932
|
+
if (selected) ctx.outline(`bar-${series.id}-${_nullishCoalesce(s.index, () => ( "total"))}-selection`, { x: rect.x - 2, y: rect.y - 2, width: rect.width + 4, height: rect.height + 4 });
|
|
1933
|
+
if (prevEnd !== null) {
|
|
1934
|
+
const [x1, y1] = map({ c: catCenter(k - 1) + w / 2, v: value.px(prevEnd) });
|
|
1935
|
+
const [x2, y2] = map({ c: c0, v: value.px(prevEnd) });
|
|
1936
|
+
marks.push({ kind: "line", key: `connector-${k}`, role: "connector", x1, y1, x2, y2, stroke: theme.axis, strokeWidth: 1, dash: "3 3" });
|
|
1937
|
+
}
|
|
1938
|
+
prevEnd = s.to;
|
|
1939
|
+
if (labelsOn) {
|
|
1940
|
+
const text = formatChartNumber(s.total ? s.to : s.to - s.from, format);
|
|
1941
|
+
const up = s.to >= s.from;
|
|
1942
|
+
const [lx, ly] = map({ c: catCenter(k), v: horizontal ? Math.max(a, b) + 4 : up ? Math.min(a, b) - 4 : Math.max(a, b) + 4 });
|
|
1943
|
+
marks.push({ kind: "text", key: `label-${series.id}-${_nullishCoalesce(s.index, () => ( "total"))}`, role: "data-label", x: lx, y: ly, text, anchor: horizontal ? "start" : "middle", baseline: horizontal ? "middle" : up ? "bottom" : "top", size: fs - 1, fill: theme.text, target });
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
return { plot: frame.plot, valueScale: toValueScale(value, horizontal ? "x" : "y") };
|
|
1947
|
+
}
|
|
1948
|
+
function layoutHistogram(ctx, area, visible) {
|
|
1949
|
+
const { spec, marks, fs, theme } = ctx;
|
|
1950
|
+
const pooled = visible.flatMap((s) => s.values);
|
|
1951
|
+
const bins = chartHistogram(pooled, spec.bins);
|
|
1952
|
+
if (!bins.length) return { plot: area };
|
|
1953
|
+
const edgeFormat = spec.xAxis.format;
|
|
1954
|
+
const edges = (b, k2) => `${k2 === 0 ? "[" : "("}${formatChartNumber(b.from, edgeFormat)}, ${formatChartNumber(b.to, edgeFormat)}]`;
|
|
1955
|
+
const counts = visible.map((s) => {
|
|
1956
|
+
const c = bins.map(() => 0);
|
|
1957
|
+
for (const v of s.values) {
|
|
1958
|
+
if (v === null || !Number.isFinite(v)) continue;
|
|
1959
|
+
let k2 = bins.findIndex((b) => v <= b.to);
|
|
1960
|
+
if (k2 < 0) k2 = bins.length - 1;
|
|
1961
|
+
c[k2]++;
|
|
1962
|
+
}
|
|
1963
|
+
return c;
|
|
1964
|
+
});
|
|
1965
|
+
const maxCount = Math.max(1, ...counts.flat());
|
|
1966
|
+
const frame = categoryFrame(ctx, area, {
|
|
1967
|
+
labels: bins.map(edges),
|
|
1968
|
+
horizontal: false,
|
|
1969
|
+
primary: { lo: 0, hi: maxCount, axis: { ...spec.yAxis, format: spec.yAxis.format.code || spec.yAxis.format.style ? spec.yAxis.format : { decimals: 0 } }, format: spec.yAxis.format.code || spec.yAxis.format.style ? spec.yAxis.format : { decimals: 0 }, frozen: null, key: "y" },
|
|
1970
|
+
labelTarget: () => void 0
|
|
1971
|
+
});
|
|
1972
|
+
const { band, catCenter, value, plot } = frame;
|
|
1973
|
+
const k = visible.length;
|
|
1974
|
+
const gap = k > 1 ? band * spec.gapWidth * 0.5 : 0;
|
|
1975
|
+
const w = (band - gap) / k;
|
|
1976
|
+
visible.forEach((s, si) => {
|
|
1977
|
+
const color = _chunkJOX6MJW4cjs.chartSeriesColor.call(void 0, spec, spec.series.indexOf(s));
|
|
1978
|
+
const target = { kind: "series", seriesId: s.id };
|
|
1979
|
+
const selected = ctx.seriesSelected(s.id);
|
|
1980
|
+
counts[si].forEach((c, b) => {
|
|
1981
|
+
if (!c) return;
|
|
1982
|
+
const top = value.px(c);
|
|
1983
|
+
const x = catCenter(b) - band / 2 + gap / 2 + si * w;
|
|
1984
|
+
marks.push({ kind: "rect", key: `bar-${s.id}-${b}`, role: "bar", x, y: top, width: w, height: value.zero - top, fill: color, stroke: theme.surface, strokeWidth: 0.75, opacity: k > 1 ? 0.85 : void 0, target, selected });
|
|
1985
|
+
if (_nullishCoalesce(s.dataLabels, () => ( spec.dataLabels))) marks.push({ kind: "text", key: `label-${s.id}-${b}`, role: "data-label", x: x + w / 2, y: top - 3, text: String(c), anchor: "middle", baseline: "bottom", size: fs - 1, fill: theme.text, target });
|
|
1986
|
+
});
|
|
1987
|
+
});
|
|
1988
|
+
return { plot };
|
|
1989
|
+
}
|
|
1990
|
+
function layoutBox(ctx, area, visible) {
|
|
1991
|
+
const { spec, marks, theme, fs } = ctx;
|
|
1992
|
+
const stats = visible.map((s) => chartBoxStats(s.values));
|
|
1993
|
+
let lo = Infinity;
|
|
1994
|
+
let hi = -Infinity;
|
|
1995
|
+
for (const st of stats) if (st) lo = Math.min(lo, st.min), hi = Math.max(hi, st.max);
|
|
1996
|
+
if (!Number.isFinite(lo)) [lo, hi] = [0, 1];
|
|
1997
|
+
const frame = categoryFrame(ctx, area, {
|
|
1998
|
+
labels: visible.map((s) => s.name),
|
|
1999
|
+
horizontal: false,
|
|
2000
|
+
primary: { lo, hi, axis: spec.yAxis, format: spec.yAxis.format, frozen: ctx.options.valueDomain, headroom: true, key: "y" },
|
|
2001
|
+
labelTarget: (i) => ({ kind: "series", seriesId: visible[i].id })
|
|
2002
|
+
});
|
|
2003
|
+
const { band, catCenter, value, plot } = frame;
|
|
2004
|
+
const bw = Math.min(80, band * (1 - spec.gapWidth) * 0.6);
|
|
2005
|
+
visible.forEach((s, i) => {
|
|
2006
|
+
const st = stats[i];
|
|
2007
|
+
if (!st) return;
|
|
2008
|
+
const color = _chunkJOX6MJW4cjs.chartSeriesColor.call(void 0, spec, spec.series.indexOf(s));
|
|
2009
|
+
const target = { kind: "series", seriesId: s.id };
|
|
2010
|
+
const selected = ctx.seriesSelected(s.id);
|
|
2011
|
+
const c = catCenter(i);
|
|
2012
|
+
const y = value.px;
|
|
2013
|
+
const line = (key, x1, y1, x2, y2, width = 1.25, stroke = color) => marks.push({ kind: "line", key, role: "whisker", x1, y1, x2, y2, stroke, strokeWidth: width, target });
|
|
2014
|
+
line(`whisker-hi-${s.id}`, c, y(st.upperWhisker), c, y(st.q3));
|
|
2015
|
+
line(`whisker-lo-${s.id}`, c, y(st.q1), c, y(st.lowerWhisker));
|
|
2016
|
+
line(`cap-hi-${s.id}`, c - bw / 4, y(st.upperWhisker), c + bw / 4, y(st.upperWhisker));
|
|
2017
|
+
line(`cap-lo-${s.id}`, c - bw / 4, y(st.lowerWhisker), c + bw / 4, y(st.lowerWhisker));
|
|
2018
|
+
const top = y(st.q3);
|
|
2019
|
+
const bottom = y(st.q1);
|
|
2020
|
+
marks.push({ kind: "rect", key: `box-${s.id}`, role: "box", x: c - bw / 2, y: top, width: bw, height: Math.max(0.5, bottom - top), fill: color, opacity: 0.3, stroke: selected ? theme.selection : color, strokeWidth: selected ? 2.5 : 1.5, target, selected });
|
|
2021
|
+
line(`median-${s.id}`, c - bw / 2, y(st.median), c + bw / 2, y(st.median), 2.5);
|
|
2022
|
+
const my = y(st.mean);
|
|
2023
|
+
marks.push({ kind: "path", key: `mean-${s.id}`, role: "whisker", d: `M${r2(c - 3)},${r2(my - 3)}L${r2(c + 3)},${r2(my + 3)}M${r2(c - 3)},${r2(my + 3)}L${r2(c + 3)},${r2(my - 3)}`, stroke: color, strokeWidth: 1.25, fill: "none", target });
|
|
2024
|
+
st.outliers.slice(0, 500).forEach((o, k) => marks.push({ kind: "circle", key: `outlier-${s.id}-${k}`, role: "outlier", cx: c, cy: y(o), r: 2.5, fill: "none", stroke: color, strokeWidth: 1, target }));
|
|
2025
|
+
if (_nullishCoalesce(s.dataLabels, () => ( spec.dataLabels))) marks.push({ kind: "text", key: `label-${s.id}-median`, role: "data-label", x: c + bw / 2 + 4, y: y(st.median), text: formatChartNumber(st.median, _nullishCoalesce(s.format, () => ( spec.yAxis.format))), anchor: "start", baseline: "middle", size: fs - 1, fill: theme.text, target });
|
|
2026
|
+
});
|
|
2027
|
+
return { plot, valueScale: toValueScale(value, "y") };
|
|
2028
|
+
}
|
|
2029
|
+
function layoutFunnel(ctx, area, series) {
|
|
2030
|
+
const { spec, marks, theme, fs, measure } = ctx;
|
|
2031
|
+
const n = spec.categories.length;
|
|
2032
|
+
const labelW = spec.xAxis.show ? Math.min(area.width * 0.3, Math.max(0, ...spec.categories.map((c) => measure(c, fs)))) + 10 : 0;
|
|
2033
|
+
const plot = { x: area.x + labelW, y: area.y, width: Math.max(1, area.width - labelW), height: area.height };
|
|
2034
|
+
const max = Math.max(0, ...series.values.map((v) => v !== null && Number.isFinite(v) ? Math.abs(v) : 0));
|
|
2035
|
+
const band = plot.height / Math.max(1, n);
|
|
2036
|
+
const h = band * (1 - Math.min(0.5, spec.gapWidth * 0.5));
|
|
2037
|
+
const format = _nullishCoalesce(series.format, () => ( spec.yAxis.format));
|
|
2038
|
+
const labelsOn = _nullishCoalesce(series.dataLabels, () => ( true));
|
|
2039
|
+
const cx = plot.x + plot.width / 2;
|
|
2040
|
+
spec.categories.forEach((label, i) => {
|
|
2041
|
+
const v = series.values[i];
|
|
2042
|
+
const cy = plot.y + band * (i + 0.5);
|
|
2043
|
+
const target = { kind: "point", seriesId: series.id, index: i };
|
|
2044
|
+
const selected = ctx.isSelected(target);
|
|
2045
|
+
if (spec.xAxis.show) {
|
|
2046
|
+
const catTarget = { kind: "category", index: i };
|
|
2047
|
+
marks.push({ kind: "text", key: `tick-x-${i}`, role: "tick-label", x: plot.x - 8, y: cy, text: truncate(label, labelW - 10, fs, measure), anchor: "end", baseline: "middle", size: fs, fill: ctx.isSelected(catTarget) ? theme.selection : theme.mutedText, target: catTarget });
|
|
2048
|
+
}
|
|
2049
|
+
if (v === null || !Number.isFinite(v) || max <= 0) return;
|
|
2050
|
+
const w = Math.max(1, Math.abs(v) / max * plot.width);
|
|
2051
|
+
const fill = sliceColor(ctx, series, i);
|
|
2052
|
+
marks.push({ kind: "rect", key: `bar-${series.id}-${i}`, role: "bar", x: cx - w / 2, y: cy - h / 2, width: w, height: h, fill, radius: 2, target, selected });
|
|
2053
|
+
if (selected) ctx.outline(`bar-${series.id}-${i}-selection`, { x: cx - w / 2 - 2, y: cy - h / 2 - 2, width: w + 4, height: h + 4 });
|
|
2054
|
+
if (labelsOn) {
|
|
2055
|
+
const text = formatChartNumber(v, format);
|
|
2056
|
+
const fits = measure(text, fs) + 8 <= w && fs + 2 <= h;
|
|
2057
|
+
marks.push({ kind: "text", key: `label-${series.id}-${i}`, role: "data-label", x: fits ? cx : cx + w / 2 + 4, y: cy, text, anchor: fits ? "middle" : "start", baseline: "middle", size: fs, fill: fits ? contrastingTextColor(fill) : theme.text, target });
|
|
2058
|
+
}
|
|
2059
|
+
});
|
|
2060
|
+
return { plot };
|
|
2061
|
+
}
|
|
2062
|
+
function layoutTreemap(ctx, area, series) {
|
|
2063
|
+
const { spec, marks, theme, fs, measure } = ctx;
|
|
2064
|
+
const values = spec.categories.map((_, i) => {
|
|
2065
|
+
const v = series.values[i];
|
|
2066
|
+
return v !== null && Number.isFinite(v) ? v : 0;
|
|
2067
|
+
});
|
|
2068
|
+
const rects = chartSquarify(values, area);
|
|
2069
|
+
const format = _nullishCoalesce(series.format, () => ( spec.yAxis.format));
|
|
2070
|
+
const labelsOn = _nullishCoalesce(series.dataLabels, () => ( true));
|
|
2071
|
+
let unlabelled = 0;
|
|
2072
|
+
rects.forEach((r, i) => {
|
|
2073
|
+
if (r.width <= 0 || r.height <= 0) return;
|
|
2074
|
+
const target = { kind: "point", seriesId: series.id, index: i };
|
|
2075
|
+
const selected = ctx.isSelected(target) || ctx.isSelected({ kind: "category", index: i });
|
|
2076
|
+
const fill = sliceColor(ctx, series, i);
|
|
2077
|
+
marks.push({ kind: "rect", key: `cell-${i}`, role: "cell", ...r, fill, stroke: selected ? theme.selection : theme.surface, strokeWidth: selected ? 2.5 : 1.5, target, selected });
|
|
2078
|
+
if (!labelsOn) return;
|
|
2079
|
+
const room = r.width - 8;
|
|
2080
|
+
if (room < fs * 2 || r.height < fs * 1.5) {
|
|
2081
|
+
unlabelled++;
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
const color = contrastingTextColor(fill);
|
|
2085
|
+
marks.push({ kind: "text", key: `label-cell-${i}`, role: "data-label", x: r.x + 4, y: r.y + 4, text: truncate(spec.categories[i], room, fs, measure), anchor: "start", baseline: "top", size: fs, weight: 600, fill: color, target });
|
|
2086
|
+
if (r.height >= fs * 3) marks.push({ kind: "text", key: `label-value-${i}`, role: "data-label", x: r.x + 4, y: r.y + 6 + fs * 1.2, text: truncate(formatChartNumber(values[i], format), room, fs - 1, measure), anchor: "start", baseline: "top", size: fs - 1, fill: color, target });
|
|
2087
|
+
});
|
|
2088
|
+
if (unlabelled) ctx.warn("data-labels-hidden", `${unlabelled} treemap cell${unlabelled === 1 ? " is" : "s are"} too small for a label.`);
|
|
2089
|
+
return { plot: area };
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// src/chart/layout.ts
|
|
2093
|
+
function legendItems(spec, ctx) {
|
|
2094
|
+
const family = _chunkJOX6MJW4cjs.chartFamily.call(void 0, spec);
|
|
2095
|
+
if (spec.type === "funnel") return [];
|
|
2096
|
+
if (_chunkJOX6MJW4cjs.chartColorsByCategory.call(void 0, spec.type)) {
|
|
2097
|
+
const first = spec.series.find((s) => !s.hidden);
|
|
2098
|
+
return spec.categories.map((c, i) => ({
|
|
2099
|
+
label: c,
|
|
2100
|
+
color: _optionalChain([first, 'optionalAccess', _31 => _31.pointColors, 'optionalAccess', _32 => _32[i]]) || _chunkJOX6MJW4cjs.chartSliceColor.call(void 0, spec, i),
|
|
2101
|
+
target: { kind: "category", index: i },
|
|
2102
|
+
faded: false,
|
|
2103
|
+
selected: ctx.isSelected({ kind: "category", index: i }),
|
|
2104
|
+
shape: "square"
|
|
2105
|
+
}));
|
|
2106
|
+
}
|
|
2107
|
+
if (family === "waterfall") {
|
|
2108
|
+
const first = spec.series.find((s) => !s.hidden);
|
|
2109
|
+
if (!first) return [];
|
|
2110
|
+
const target = { kind: "series", seriesId: first.id };
|
|
2111
|
+
return [
|
|
2112
|
+
{ label: "Increase", color: ctx.theme.positive, target, faded: false, selected: false, shape: "square" },
|
|
2113
|
+
{ label: "Decrease", color: ctx.theme.negative, target, faded: false, selected: false, shape: "square" },
|
|
2114
|
+
{ label: "Total", color: _chunkJOX6MJW4cjs.chartSeriesColor.call(void 0, spec, spec.series.indexOf(first)), target, faded: false, selected: false, shape: "square" }
|
|
2115
|
+
];
|
|
2116
|
+
}
|
|
2117
|
+
if (family === "box") return [];
|
|
2118
|
+
const trends = trendlineLegendItems(spec, (i) => _chunkJOX6MJW4cjs.chartSeriesColor.call(void 0, spec, i));
|
|
2119
|
+
const items = [];
|
|
2120
|
+
spec.series.forEach((s, i) => {
|
|
2121
|
+
const type = _chunkJOX6MJW4cjs.chartSeriesType.call(void 0, spec, s);
|
|
2122
|
+
items.push({
|
|
2123
|
+
label: s.name,
|
|
2124
|
+
color: _chunkJOX6MJW4cjs.chartSeriesColor.call(void 0, spec, i),
|
|
2125
|
+
target: { kind: "series", seriesId: s.id },
|
|
2126
|
+
faded: !!s.hidden,
|
|
2127
|
+
selected: ctx.seriesSelected(s.id),
|
|
2128
|
+
shape: family === "cartesian" || family === "xy" || family === "radar" ? type === "line" ? "line" : "square" : "square"
|
|
2129
|
+
});
|
|
2130
|
+
for (const t of trends.filter((t2) => t2.after === s.id)) items.push({ label: t.label, color: t.color, target: t.target, faded: false, selected: ctx.isSelected(t.target), shape: "dash" });
|
|
2131
|
+
});
|
|
2132
|
+
return items;
|
|
2133
|
+
}
|
|
2134
|
+
function layoutChart(spec, options) {
|
|
2135
|
+
const width = Math.max(1, Number.isFinite(options.width) ? options.width : 1);
|
|
2136
|
+
const height = Math.max(1, Number.isFinite(options.height) ? options.height : 1);
|
|
2137
|
+
const theme = { ..._chunkJOX6MJW4cjs.DEFAULT_CHART_THEME, ...options.theme };
|
|
2138
|
+
const selection = _nullishCoalesce(options.selection, () => ( null));
|
|
2139
|
+
const marks = [];
|
|
2140
|
+
const warnings = [];
|
|
2141
|
+
const ctx = {
|
|
2142
|
+
spec,
|
|
2143
|
+
options,
|
|
2144
|
+
theme,
|
|
2145
|
+
measure: _nullishCoalesce(options.measureText, () => ( estimateChartTextWidth)),
|
|
2146
|
+
fs: theme.fontSize,
|
|
2147
|
+
width,
|
|
2148
|
+
height,
|
|
2149
|
+
marks,
|
|
2150
|
+
warnings,
|
|
2151
|
+
selection,
|
|
2152
|
+
isSelected: (target) => sameChartSelection(selection, target),
|
|
2153
|
+
seriesSelected: (id) => _optionalChain([selection, 'optionalAccess', _33 => _33.kind]) === "series" && selection.seriesId === id,
|
|
2154
|
+
outline: (key, box) => marks.push({ kind: "rect", key, role: "selection", ...box, fill: "none", stroke: theme.selection, strokeWidth: 1, radius: 3 }),
|
|
2155
|
+
warn: (code, message) => {
|
|
2156
|
+
if (!warnings.some((w) => w.code === code && w.message === message)) warnings.push({ code, message });
|
|
2157
|
+
}
|
|
2158
|
+
};
|
|
2159
|
+
const pad = 12;
|
|
2160
|
+
const fs = theme.fontSize;
|
|
2161
|
+
marks.push({ kind: "rect", key: "background", role: "background", x: 0, y: 0, width, height, fill: theme.background, target: { kind: "chart" } });
|
|
2162
|
+
let top = pad;
|
|
2163
|
+
const titleMark = (kind, label, size, weight, fill) => {
|
|
2164
|
+
const target = { kind };
|
|
2165
|
+
const selected = ctx.isSelected(target);
|
|
2166
|
+
marks.push({ kind: "text", key: kind, role: kind, x: width / 2, y: top, text: label, anchor: "middle", baseline: "top", size, weight, fill, target, selected });
|
|
2167
|
+
if (selected) {
|
|
2168
|
+
const w = ctx.measure(label, size, weight);
|
|
2169
|
+
ctx.outline(`${kind}-selection`, { x: width / 2 - w / 2 - 4, y: top - 3, width: w + 8, height: size * 1.25 + 6 });
|
|
2170
|
+
}
|
|
2171
|
+
top += size * 1.35;
|
|
2172
|
+
};
|
|
2173
|
+
if (spec.title) titleMark("title", spec.title, theme.titleSize, 600, theme.text);
|
|
2174
|
+
if (spec.subtitle) titleMark("subtitle", spec.subtitle, fs + 1, 400, theme.mutedText);
|
|
2175
|
+
if (spec.title || spec.subtitle) top += 6;
|
|
2176
|
+
const family = _chunkJOX6MJW4cjs.chartFamily.call(void 0, spec);
|
|
2177
|
+
const visible = spec.series.filter((s) => !s.hidden);
|
|
2178
|
+
const items = legendItems(spec, ctx);
|
|
2179
|
+
let area = { x: pad, y: top, width: width - pad * 2, height: height - top - pad };
|
|
2180
|
+
const seriesLegend = family === "cartesian" || family === "xy" || family === "radar" || family === "histogram";
|
|
2181
|
+
const hasTrend = items.some((i) => i.shape === "dash");
|
|
2182
|
+
const showLegend = spec.legend.show && items.length > 0 && (!seriesLegend || spec.series.length > 1 || spec.series.some((s) => s.hidden) || hasTrend);
|
|
2183
|
+
if (showLegend) area = layoutLegend(items, spec.legend.position, area, ctx, ctx.isSelected({ kind: "legend" }));
|
|
2184
|
+
const finish = (result) => {
|
|
2185
|
+
if (options.readout) addReadout(ctx, options.readout, result.plot);
|
|
2186
|
+
const kept = finalizeMarks(marks, width, height);
|
|
2187
|
+
if (kept.length < marks.length) ctx.warn("values-skipped", `${marks.length - kept.length} mark${marks.length - kept.length === 1 ? " was" : "s were"} left out: their values are not finite numbers.`);
|
|
2188
|
+
return {
|
|
2189
|
+
width,
|
|
2190
|
+
height,
|
|
2191
|
+
plot: result.plot,
|
|
2192
|
+
marks: kept,
|
|
2193
|
+
percent: _nullishCoalesce(result.percent, () => ( false)),
|
|
2194
|
+
theme,
|
|
2195
|
+
warnings,
|
|
2196
|
+
...result.valueScale ? { valueScale: result.valueScale } : {},
|
|
2197
|
+
...result.valueScale2 ? { valueScale2: result.valueScale2 } : {},
|
|
2198
|
+
...result.xScale ? { xScale: result.xScale } : {}
|
|
2199
|
+
};
|
|
2200
|
+
};
|
|
2201
|
+
const empty = (message) => {
|
|
2202
|
+
marks.push({ kind: "text", key: "empty", role: "empty", x: area.x + area.width / 2, y: area.y + area.height / 2, text: message, anchor: "middle", baseline: "middle", size: fs + 1, fill: theme.mutedText });
|
|
2203
|
+
return finish({ plot: { x: area.x, y: area.y, width: 0, height: 0 } });
|
|
2204
|
+
};
|
|
2205
|
+
const hasData = visible.some((s) => s.values.some((v) => v !== null && Number.isFinite(v)));
|
|
2206
|
+
const needsCategories = family !== "histogram" && family !== "box" && family !== "xy";
|
|
2207
|
+
if (needsCategories && !spec.categories.length || !visible.length || !hasData) return empty(_nullishCoalesce(options.emptyMessage, () => ( "No data")));
|
|
2208
|
+
switch (family) {
|
|
2209
|
+
case "pie":
|
|
2210
|
+
return finish(layoutPie(ctx, area, visible[0]));
|
|
2211
|
+
case "radar":
|
|
2212
|
+
if (spec.categories.length < 3) {
|
|
2213
|
+
ctx.warn("type-fallback", "A radar chart needs at least 3 categories; it is drawn as a line chart until then.");
|
|
2214
|
+
return finish(layoutCartesian(ctx, area, visible));
|
|
2215
|
+
}
|
|
2216
|
+
return finish(layoutRadar(ctx, area, visible));
|
|
2217
|
+
case "waterfall":
|
|
2218
|
+
return finish(layoutWaterfall(ctx, area, visible[0]));
|
|
2219
|
+
case "histogram":
|
|
2220
|
+
return finish(layoutHistogram(ctx, area, visible));
|
|
2221
|
+
case "box":
|
|
2222
|
+
return finish(layoutBox(ctx, area, visible));
|
|
2223
|
+
case "funnel":
|
|
2224
|
+
return finish(layoutFunnel(ctx, area, visible[0]));
|
|
2225
|
+
case "treemap":
|
|
2226
|
+
return finish(layoutTreemap(ctx, area, visible[0]));
|
|
2227
|
+
case "xy":
|
|
2228
|
+
return finish(layoutXy(ctx, area, visible));
|
|
2229
|
+
default:
|
|
2230
|
+
return finish(layoutCartesian(ctx, area, visible));
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
function layoutLegend(items, position, area, ctx, legendSelected) {
|
|
2234
|
+
const { theme, measure, marks } = ctx;
|
|
2235
|
+
const fs = theme.fontSize;
|
|
2236
|
+
const swatch = 10;
|
|
2237
|
+
const rowH = fs * 1.7;
|
|
2238
|
+
const itemW = (item) => swatch + 6 + measure(item.label, fs) + 14;
|
|
2239
|
+
const placed = [];
|
|
2240
|
+
let box;
|
|
2241
|
+
let rest;
|
|
2242
|
+
if (position === "left" || position === "right") {
|
|
2243
|
+
const w = Math.min(area.width * 0.35, Math.max(...items.map(itemW)));
|
|
2244
|
+
const fit = Math.max(1, Math.floor((area.height + 0.5) / rowH));
|
|
2245
|
+
const shown = items.slice(0, fit);
|
|
2246
|
+
const h = Math.min(shown.length * rowH, area.height);
|
|
2247
|
+
const x = position === "left" ? area.x : area.x + area.width - w;
|
|
2248
|
+
const y = area.y + (area.height - h) / 2;
|
|
2249
|
+
shown.forEach((item, i) => placed.push({ item, x, y: y + i * rowH + rowH / 2 }));
|
|
2250
|
+
if (shown.length < items.length) ctx.warn("legend-truncated", `The legend shows ${shown.length} of ${items.length} entries; the chart is too short for the rest.`);
|
|
2251
|
+
box = { x, y, width: w, height: h };
|
|
2252
|
+
rest = position === "left" ? { ...area, x: area.x + w + 8, width: area.width - w - 8 } : { ...area, width: area.width - w - 8 };
|
|
2253
|
+
} else {
|
|
2254
|
+
const rows = [[]];
|
|
2255
|
+
let used = 0;
|
|
2256
|
+
for (const item of items) {
|
|
2257
|
+
const w = itemW(item);
|
|
2258
|
+
if (used + w > area.width && rows[rows.length - 1].length) {
|
|
2259
|
+
rows.push([]);
|
|
2260
|
+
used = 0;
|
|
2261
|
+
}
|
|
2262
|
+
rows[rows.length - 1].push(item);
|
|
2263
|
+
used += w;
|
|
2264
|
+
}
|
|
2265
|
+
const maxRows = Math.max(1, Math.floor(ctx.height * 0.3 / rowH));
|
|
2266
|
+
const shown = rows.slice(0, maxRows);
|
|
2267
|
+
const hiddenCount = rows.slice(maxRows).reduce((t, r) => t + r.length, 0);
|
|
2268
|
+
if (hiddenCount) ctx.warn("legend-truncated", `The legend shows ${items.length - hiddenCount} of ${items.length} entries; the rest do not fit.`);
|
|
2269
|
+
const h = shown.length * rowH;
|
|
2270
|
+
const y0 = position === "top" ? area.y : area.y + area.height - h;
|
|
2271
|
+
shown.forEach((row, r) => {
|
|
2272
|
+
const total = row.reduce((t, item) => t + itemW(item), 0) - 14;
|
|
2273
|
+
let x = area.x + (area.width - total) / 2;
|
|
2274
|
+
for (const item of row) {
|
|
2275
|
+
placed.push({ item, x, y: y0 + r * rowH + rowH / 2 });
|
|
2276
|
+
x += itemW(item);
|
|
2277
|
+
}
|
|
2278
|
+
});
|
|
2279
|
+
box = { x: area.x, y: y0, width: area.width, height: h };
|
|
2280
|
+
rest = position === "top" ? { ...area, y: area.y + h + 6, height: area.height - h - 6 } : { ...area, height: area.height - h - 6 };
|
|
2281
|
+
}
|
|
2282
|
+
placed.forEach(({ item, x, y }, i) => {
|
|
2283
|
+
const opacity = item.faded ? 0.4 : void 0;
|
|
2284
|
+
if (item.shape === "line" || item.shape === "dash")
|
|
2285
|
+
marks.push({ kind: "line", key: `legend-swatch-${i}`, role: "legend-swatch", x1: x, y1: y, x2: x + swatch, y2: y, stroke: item.color, strokeWidth: item.shape === "dash" ? 1.5 : 2.5, ...item.shape === "dash" ? { dash: "3 2" } : {}, opacity, target: item.target, selected: item.selected });
|
|
2286
|
+
else marks.push({ kind: "rect", key: `legend-swatch-${i}`, role: "legend-swatch", x, y: y - swatch / 2, width: swatch, height: swatch, radius: 2, fill: item.color, opacity, target: item.target, selected: item.selected });
|
|
2287
|
+
marks.push({
|
|
2288
|
+
kind: "text",
|
|
2289
|
+
key: `legend-label-${i}`,
|
|
2290
|
+
role: "legend-label",
|
|
2291
|
+
x: x + swatch + 6,
|
|
2292
|
+
y,
|
|
2293
|
+
text: item.label,
|
|
2294
|
+
anchor: "start",
|
|
2295
|
+
baseline: "middle",
|
|
2296
|
+
size: fs,
|
|
2297
|
+
fill: item.selected ? theme.selection : theme.text,
|
|
2298
|
+
weight: item.selected ? 600 : void 0,
|
|
2299
|
+
opacity,
|
|
2300
|
+
target: item.target,
|
|
2301
|
+
selected: item.selected
|
|
2302
|
+
});
|
|
2303
|
+
});
|
|
2304
|
+
if (legendSelected) marks.push({ kind: "rect", key: "legend-selection", role: "selection", x: box.x - 4, y: box.y - 2, width: box.width + 8, height: box.height + 4, fill: "none", stroke: theme.selection, strokeWidth: 1, radius: 3 });
|
|
2305
|
+
return rest;
|
|
2306
|
+
}
|
|
2307
|
+
function addReadout(ctx, readout, plot) {
|
|
2308
|
+
const { spec, marks, theme, fs, measure } = ctx;
|
|
2309
|
+
const series = spec.series.find((s) => s.id === readout.seriesId);
|
|
2310
|
+
const value = _optionalChain([series, 'optionalAccess', _34 => _34.values, 'access', _35 => _35[readout.index]]);
|
|
2311
|
+
if (!series || value === null || value === void 0) return;
|
|
2312
|
+
const mark = marks.find((m) => (m.role === "point" || m.role === "bar") && _optionalChain([m, 'access', _36 => _36.target, 'optionalAccess', _37 => _37.kind]) === "point" && m.target.seriesId === readout.seriesId && m.target.index === readout.index);
|
|
2313
|
+
if (!mark) return;
|
|
2314
|
+
let ax;
|
|
2315
|
+
let ay;
|
|
2316
|
+
if (mark.kind === "rect") {
|
|
2317
|
+
ax = mark.x + mark.width / 2;
|
|
2318
|
+
ay = mark.y;
|
|
2319
|
+
} else if (mark.kind === "circle") {
|
|
2320
|
+
ax = mark.cx;
|
|
2321
|
+
ay = mark.cy - mark.r;
|
|
2322
|
+
} else return;
|
|
2323
|
+
const axis = series.axis === "secondary" ? spec.yAxis2 : spec.yAxis;
|
|
2324
|
+
const x = _optionalChain([series, 'access', _38 => _38.x, 'optionalAccess', _39 => _39[readout.index]]);
|
|
2325
|
+
const text = x !== null && x !== void 0 && _chunkJOX6MJW4cjs.chartFamily.call(void 0, spec) === "xy" ? `${formatChartNumber(x, spec.xAxis.format)}, ${formatChartNumber(value, axis.format)}` : `${_nullishCoalesce(spec.categories[readout.index], () => ( ""))}: ${formatChartNumber(value, _nullishCoalesce(series.format, () => ( axis.format)))}`;
|
|
2326
|
+
const w = measure(text, fs, 600) + 10;
|
|
2327
|
+
const h = fs + 8;
|
|
2328
|
+
const bx = Math.max(plot.x, Math.min(plot.x + plot.width - w, ax - w / 2));
|
|
2329
|
+
const by = Math.max(plot.y, Math.min(plot.y + plot.height - h, ay - h - 6));
|
|
2330
|
+
marks.push({ kind: "rect", key: "drag-readout-box", role: "drag-readout", x: bx, y: by, width: w, height: h, fill: theme.text, radius: 3, opacity: 0.9 });
|
|
2331
|
+
marks.push({ kind: "text", key: "drag-readout", role: "drag-readout", x: bx + w / 2, y: by + h / 2, text, anchor: "middle", baseline: "middle", size: fs, weight: 600, fill: theme.surface });
|
|
2332
|
+
}
|
|
2333
|
+
var finite = (...n) => n.every((v) => v === void 0 || Number.isFinite(v));
|
|
2334
|
+
function finalizeMarks(marks, width, height) {
|
|
2335
|
+
const lim = (v, size) => Math.max(-size * 4, Math.min(size * 5, v));
|
|
2336
|
+
const out = [];
|
|
2337
|
+
for (const m of marks) {
|
|
2338
|
+
switch (m.kind) {
|
|
2339
|
+
case "rect":
|
|
2340
|
+
if (!finite(m.x, m.y, m.width, m.height)) continue;
|
|
2341
|
+
out.push({ ...m, x: lim(m.x, width), y: lim(m.y, height), width: Math.max(0, Math.min(m.width, width * 9)), height: Math.max(0, Math.min(m.height, height * 9)) });
|
|
2342
|
+
break;
|
|
2343
|
+
case "circle":
|
|
2344
|
+
if (!finite(m.cx, m.cy, m.r)) continue;
|
|
2345
|
+
out.push({ ...m, cx: lim(m.cx, width), cy: lim(m.cy, height) });
|
|
2346
|
+
break;
|
|
2347
|
+
case "line":
|
|
2348
|
+
if (!finite(m.x1, m.y1, m.x2, m.y2)) continue;
|
|
2349
|
+
out.push({ ...m, x1: lim(m.x1, width), y1: lim(m.y1, height), x2: lim(m.x2, width), y2: lim(m.y2, height) });
|
|
2350
|
+
break;
|
|
2351
|
+
case "text":
|
|
2352
|
+
if (!finite(m.x, m.y, m.size)) continue;
|
|
2353
|
+
out.push(m);
|
|
2354
|
+
break;
|
|
2355
|
+
case "path":
|
|
2356
|
+
if (/NaN|Infinity/.test(m.d)) continue;
|
|
2357
|
+
out.push(m);
|
|
2358
|
+
break;
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
return out;
|
|
2362
|
+
}
|
|
2363
|
+
function chartValueAtPoint(scene, point, secondary = false) {
|
|
2364
|
+
const scale = secondary ? _nullishCoalesce(scene.valueScale2, () => ( scene.valueScale)) : scene.valueScale;
|
|
2365
|
+
if (!scale || scene.percent) return void 0;
|
|
2366
|
+
const px = scale.axis === "y" ? point.y : point.x;
|
|
2367
|
+
return linearMap(px, scale.range, scale.domain);
|
|
2368
|
+
}
|
|
2369
|
+
function chartXAtPoint(scene, point) {
|
|
2370
|
+
const scale = scene.xScale;
|
|
2371
|
+
if (!scale) return void 0;
|
|
2372
|
+
return linearMap(point.x, scale.range, scale.domain);
|
|
2373
|
+
}
|
|
2374
|
+
function snapChartValue(scene, value, step, ticks = _nullishCoalesce(_optionalChain([scene, 'access', _40 => _40.valueScale, 'optionalAccess', _41 => _41.ticks]), () => ( []))) {
|
|
2375
|
+
const unit = _nullishCoalesce(step, () => ( (ticks.length > 1 ? (ticks[1] - ticks[0]) / 10 : 1)));
|
|
2376
|
+
if (!(unit > 0)) return value;
|
|
2377
|
+
const snapped = Math.round(value / unit) * unit;
|
|
2378
|
+
return Number(snapped.toPrecision(12));
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
// src/chart/summary.ts
|
|
2382
|
+
function summarizeChart(spec, size = { width: 640, height: 400 }, scene = layoutChart(spec, { ...size, selection: null })) {
|
|
2383
|
+
const visible = spec.series.filter((s) => !s.hidden);
|
|
2384
|
+
const series = visible.map((s) => summarizeChartSeries(s, spec.categories));
|
|
2385
|
+
const colors = _chunkJOX6MJW4cjs.chartColorsByCategory.call(void 0, spec.type) ? spec.categories.map((_, i) => _optionalChain([visible, 'access', _42 => _42[0], 'optionalAccess', _43 => _43.pointColors, 'optionalAccess', _44 => _44[i]]) || _chunkJOX6MJW4cjs.chartSliceColor.call(void 0, spec, i)) : visible.map((s) => _chunkJOX6MJW4cjs.chartSeriesColor.call(void 0, spec, spec.series.indexOf(s)));
|
|
2386
|
+
const check = _chunkJOX6MJW4cjs.checkChartColors.call(void 0, colors, { surface: scene.theme.surface.startsWith("var(") ? "#ffffff" : scene.theme.surface });
|
|
2387
|
+
const axis = (s) => s ? { min: s.domain[0], max: s.domain[1], ticks: [...s.ticks] } : void 0;
|
|
2388
|
+
return {
|
|
2389
|
+
type: spec.type,
|
|
2390
|
+
title: spec.title,
|
|
2391
|
+
categories: spec.categories.length,
|
|
2392
|
+
series,
|
|
2393
|
+
...scene.valueScale ? { valueAxis: axis(scene.valueScale) } : {},
|
|
2394
|
+
...scene.valueScale2 ? { secondaryAxis: axis(scene.valueScale2) } : {},
|
|
2395
|
+
...scene.xScale ? { xAxis: axis(scene.xScale) } : {},
|
|
2396
|
+
warnings: scene.warnings,
|
|
2397
|
+
colors: { ok: check.ok, issues: check.issues },
|
|
2398
|
+
description: describeChart(spec, series)
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
function describeChart(spec, series = spec.series.filter((s) => !s.hidden).map((s) => summarizeChartSeries(s, spec.categories))) {
|
|
2402
|
+
const kind = _chunkJOX6MJW4cjs.CHART_TYPE_LABELS[spec.type].toLowerCase();
|
|
2403
|
+
const parts = [];
|
|
2404
|
+
const family = _chunkJOX6MJW4cjs.chartFamily.call(void 0, spec);
|
|
2405
|
+
const format = spec.yAxis.format;
|
|
2406
|
+
const fmt = (v) => v === null ? "blank" : formatChartNumber(v, format);
|
|
2407
|
+
const head = spec.title ? `${spec.title}: a ${kind} chart` : `A ${kind} chart`;
|
|
2408
|
+
if (!series.length || series.every((s) => s.count === 0)) return `${head} with no data.`;
|
|
2409
|
+
if (family === "pie" || family === "funnel" || family === "treemap") {
|
|
2410
|
+
const s = series[0];
|
|
2411
|
+
const total = _nullishCoalesce(s.sum, () => ( 0));
|
|
2412
|
+
const share = (v) => total && v !== null ? ` (${Math.round(v / total * 100)}%)` : "";
|
|
2413
|
+
parts.push(`${head} of ${s.name} across ${spec.categories.length} categories, totalling ${fmt(s.sum)}.`);
|
|
2414
|
+
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)}.`);
|
|
2415
|
+
return parts.join(" ");
|
|
2416
|
+
}
|
|
2417
|
+
if (family === "waterfall") {
|
|
2418
|
+
const s = spec.series.find((x) => !x.hidden);
|
|
2419
|
+
const steps = s.values.filter((v, i) => v !== null && Number.isFinite(v) && !/\b(sub)?total\b/i.test(_nullishCoalesce(spec.categories[i], () => ( ""))));
|
|
2420
|
+
const total = steps.reduce((t, v) => t + v, 0);
|
|
2421
|
+
const up = steps.filter((v) => v > 0).length;
|
|
2422
|
+
const down = steps.filter((v) => v < 0).length;
|
|
2423
|
+
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)}.`;
|
|
2424
|
+
}
|
|
2425
|
+
if (family === "histogram" || family === "box") {
|
|
2426
|
+
parts.push(`${head} of ${series.map((s) => s.name).join(", ")}.`);
|
|
2427
|
+
for (const s of series) parts.push(`${s.name}: ${s.count} values from ${fmt(s.min)} to ${fmt(s.max)}, mean ${fmt(s.mean)}.`);
|
|
2428
|
+
return parts.join(" ");
|
|
2429
|
+
}
|
|
2430
|
+
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]})` : ""}`;
|
|
2431
|
+
parts.push(`${head} showing ${what}.`);
|
|
2432
|
+
for (const s of series.slice(0, 8)) {
|
|
2433
|
+
if (!s.count) {
|
|
2434
|
+
parts.push(`${s.name} has no values.`);
|
|
2435
|
+
continue;
|
|
2436
|
+
}
|
|
2437
|
+
const pct = s.change !== null ? Math.round(Math.abs(s.change) * 100) : 0;
|
|
2438
|
+
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" : "";
|
|
2439
|
+
parts.push(`${s.name} ranges from ${fmt(s.min)} (${s.minAt}) to ${fmt(s.max)} (${s.maxAt})${trend}.`);
|
|
2440
|
+
}
|
|
2441
|
+
if (series.length > 8) parts.push(`${series.length - 8} more series are not described.`);
|
|
2442
|
+
return parts.join(" ");
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
// src/chart/svg.ts
|
|
2446
|
+
function chartTextBaseline(baseline) {
|
|
2447
|
+
return baseline === "top" ? "hanging" : baseline === "middle" ? "central" : "text-after-edge";
|
|
2448
|
+
}
|
|
2449
|
+
function chartTextTransform(mark) {
|
|
2450
|
+
return mark.rotate ? `rotate(${mark.rotate} ${mark.x} ${mark.y})` : void 0;
|
|
2451
|
+
}
|
|
2452
|
+
var escape = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2453
|
+
function markParts(mark, fontFamily) {
|
|
2454
|
+
const common = { opacity: mark.opacity, "data-role": mark.role };
|
|
2455
|
+
const parts = (tag, values, text) => ({
|
|
2456
|
+
tag,
|
|
2457
|
+
attrs: Object.fromEntries(Object.entries(values).filter(([, v]) => v !== void 0 && v !== "").map(([k, v]) => [k, String(v)])),
|
|
2458
|
+
text
|
|
2459
|
+
});
|
|
2460
|
+
switch (mark.kind) {
|
|
2461
|
+
case "rect":
|
|
2462
|
+
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 });
|
|
2463
|
+
case "path":
|
|
2464
|
+
return parts("path", { d: mark.d, fill: _nullishCoalesce(mark.fill, () => ( "none")), stroke: mark.stroke, "stroke-width": mark.strokeWidth, "stroke-dasharray": mark.dash, "stroke-linejoin": "round", "stroke-linecap": "round", ...common });
|
|
2465
|
+
case "circle":
|
|
2466
|
+
return parts("circle", { cx: mark.cx, cy: mark.cy, r: mark.r, fill: mark.fill, stroke: mark.stroke, "stroke-width": mark.strokeWidth, ...common });
|
|
2467
|
+
case "line":
|
|
2468
|
+
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 });
|
|
2469
|
+
case "text":
|
|
2470
|
+
return parts(
|
|
2471
|
+
"text",
|
|
2472
|
+
{
|
|
2473
|
+
x: mark.x,
|
|
2474
|
+
y: mark.y,
|
|
2475
|
+
"text-anchor": mark.anchor,
|
|
2476
|
+
"dominant-baseline": chartTextBaseline(mark.baseline),
|
|
2477
|
+
"font-size": mark.size,
|
|
2478
|
+
"font-weight": mark.weight,
|
|
2479
|
+
"font-family": fontFamily,
|
|
2480
|
+
fill: mark.fill,
|
|
2481
|
+
transform: chartTextTransform(mark),
|
|
2482
|
+
...common
|
|
2483
|
+
},
|
|
2484
|
+
mark.text
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
function markToSvg(mark, fontFamily) {
|
|
2489
|
+
const { tag, attrs, text } = markParts(mark, fontFamily);
|
|
2490
|
+
const attrText = Object.entries(attrs).map(([k, v]) => ` ${k}="${escape(v)}"`).join("");
|
|
2491
|
+
return text === void 0 ? `<${tag}${attrText}/>` : `<${tag}${attrText}>${escape(text)}</${tag}>`;
|
|
2492
|
+
}
|
|
2493
|
+
function chartSceneToSvg(scene, options = {}) {
|
|
2494
|
+
const hide = _nullishCoalesce(options.hideSelection, () => ( true));
|
|
2495
|
+
const font = scene.theme.fontFamily;
|
|
2496
|
+
const body = scene.marks.filter((m) => !(hide && m.role === "selection")).map((m) => markToSvg(m, font)).join("");
|
|
2497
|
+
const label = options.ariaLabel;
|
|
2498
|
+
const desc = options.description;
|
|
2499
|
+
const labelled = [label ? "chart-title" : "", desc ? "chart-desc" : ""].filter(Boolean).join(" ");
|
|
2500
|
+
const head = `${label ? `<title id="chart-title">${escape(label)}</title>` : ""}${desc ? `<desc id="chart-desc">${escape(desc)}</desc>` : ""}`;
|
|
2501
|
+
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>`;
|
|
2502
|
+
}
|
|
2503
|
+
function chartToSvg(spec, options) {
|
|
2504
|
+
const scene = layoutChart(spec, { ...options, selection: null });
|
|
2505
|
+
return chartSceneToSvg(scene, { ...options, ariaLabel: _nullishCoalesce(options.ariaLabel, () => ( (spec.title || void 0))), description: _nullishCoalesce(options.description, () => ( describeChart(spec))) });
|
|
2506
|
+
}
|
|
2507
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
2508
|
+
function chartSceneToDomSpec(scene, options = {}) {
|
|
2509
|
+
const hide = _nullishCoalesce(options.hideSelection, () => ( true));
|
|
2510
|
+
const font = scene.theme.fontFamily;
|
|
2511
|
+
const tag = (name) => `${SVG_NS} ${name}`;
|
|
2512
|
+
const children = [];
|
|
2513
|
+
if (options.ariaLabel) children.push([tag("title"), {}, options.ariaLabel]);
|
|
2514
|
+
if (options.description) children.push([tag("desc"), {}, options.description]);
|
|
2515
|
+
for (const mark of scene.marks) {
|
|
2516
|
+
if (hide && mark.role === "selection") continue;
|
|
2517
|
+
const { tag: name, attrs: attrs2, text } = markParts(mark, font);
|
|
2518
|
+
children.push(text === void 0 ? [tag(name), attrs2] : [tag(name), attrs2, text]);
|
|
2519
|
+
}
|
|
2520
|
+
const attrs = {
|
|
2521
|
+
width: String(scene.width),
|
|
2522
|
+
height: String(scene.height),
|
|
2523
|
+
viewBox: `0 0 ${scene.width} ${scene.height}`,
|
|
2524
|
+
role: "img",
|
|
2525
|
+
...options.ariaLabel ? { "aria-label": options.ariaLabel } : {},
|
|
2526
|
+
...options.attrs
|
|
2527
|
+
};
|
|
2528
|
+
return [tag("svg"), attrs, ...children];
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
// src/chart/presets.ts
|
|
2532
|
+
var CHART_LAYOUT_PRESETS = [
|
|
2533
|
+
{ 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 } } },
|
|
2534
|
+
{ 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: "" } } },
|
|
2535
|
+
{ 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 } } },
|
|
2536
|
+
{ 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 } } },
|
|
2537
|
+
{ 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 } } },
|
|
2538
|
+
{ 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 } } }
|
|
2539
|
+
];
|
|
2540
|
+
function monochromeChartPalette(base, count = 6) {
|
|
2541
|
+
const steps = count <= 1 ? [0] : Array.from({ length: count }, (_, i) => -0.45 + 1.05 * i / (count - 1));
|
|
2542
|
+
return steps.map((t) => _chunkJOX6MJW4cjs.shadeChartColor.call(void 0, base, t));
|
|
2543
|
+
}
|
|
2544
|
+
var CHART_COLOR_SCHEMES = [
|
|
2545
|
+
{ id: "default", label: "Default", description: "Ten distinct colours, readable on light and dark backgrounds.", palette: _chunkJOX6MJW4cjs.DEFAULT_CHART_PALETTE },
|
|
2546
|
+
{ 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"] },
|
|
2547
|
+
{ id: "office", label: "Office", description: "The six accent colours of the default Office theme.", palette: ["#4472c4", "#ed7d31", "#a5a5a5", "#ffc000", "#5b9bd5", "#70ad47"] },
|
|
2548
|
+
{ id: "warm", label: "Warm", description: "Reds, oranges and yellows.", palette: ["#b2182b", "#d6604d", "#f4a582", "#e08214", "#fdb863", "#8c510a"] },
|
|
2549
|
+
{ id: "cool", label: "Cool", description: "Blues, teals and purples.", palette: ["#2166ac", "#4393c3", "#35978f", "#80cdc1", "#762a83", "#9970ab"] },
|
|
2550
|
+
{ id: "monochrome-blue", label: "Monochrome blue", description: "Shades of one blue.", palette: monochromeChartPalette("#4269d0") },
|
|
2551
|
+
{ id: "monochrome-green", label: "Monochrome green", description: "Shades of one green.", palette: monochromeChartPalette("#3ca951") },
|
|
2552
|
+
{ id: "monochrome-orange", label: "Monochrome orange", description: "Shades of one orange.", palette: monochromeChartPalette("#e8710a") },
|
|
2553
|
+
{ id: "monochrome-gray", label: "Monochrome grey", description: "Shades of grey.", palette: monochromeChartPalette("#6b7280") }
|
|
2554
|
+
];
|
|
2555
|
+
function chartPresetPatch(options) {
|
|
2556
|
+
const patch = {};
|
|
2557
|
+
if (options.layout !== void 0) {
|
|
2558
|
+
const preset = CHART_LAYOUT_PRESETS.find((p) => p.id === options.layout);
|
|
2559
|
+
if (!preset) return { ok: false, message: `There is no layout "${options.layout}"; expected one of ${CHART_LAYOUT_PRESETS.map((p) => `"${p.id}"`).join(", ")}.` };
|
|
2560
|
+
Object.assign(patch, preset.patch);
|
|
2561
|
+
}
|
|
2562
|
+
if (options.colors !== void 0) {
|
|
2563
|
+
const scheme = CHART_COLOR_SCHEMES.find((p) => p.id === options.colors);
|
|
2564
|
+
if (!scheme) return { ok: false, message: `There is no colour scheme "${options.colors}"; expected one of ${CHART_COLOR_SCHEMES.map((p) => `"${p.id}"`).join(", ")}.` };
|
|
2565
|
+
patch.palette = [...scheme.palette];
|
|
2566
|
+
}
|
|
2567
|
+
if (options.layout === void 0 && options.colors === void 0) return { ok: false, message: "Name a layout, a colour scheme or both." };
|
|
2568
|
+
return { ok: true, patch };
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
// src/chart/controller.ts
|
|
2572
|
+
function sameChartJson(a, b) {
|
|
2573
|
+
if (a === b) return true;
|
|
2574
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return Number.isNaN(a) && Number.isNaN(b);
|
|
2575
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
2576
|
+
if (Array.isArray(a)) {
|
|
2577
|
+
const bb = b;
|
|
2578
|
+
return a.length === bb.length && a.every((v, i) => sameChartJson(v, bb[i]));
|
|
2579
|
+
}
|
|
2580
|
+
const ao = a;
|
|
2581
|
+
const bo = b;
|
|
2582
|
+
const keys = Object.keys(ao).filter((k) => ao[k] !== void 0);
|
|
2583
|
+
if (keys.length !== Object.keys(bo).filter((k) => bo[k] !== void 0).length) return false;
|
|
2584
|
+
return keys.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && sameChartJson(ao[k], bo[k]));
|
|
2585
|
+
}
|
|
2586
|
+
var fail = (message, issues) => ({ ok: false, message, ...issues ? { issues } : {} });
|
|
2587
|
+
var ChartController = (_class = class {
|
|
2588
|
+
|
|
2589
|
+
|
|
2590
|
+
__init() {this.selection = null}
|
|
2591
|
+
__init2() {this.issues = []}
|
|
2592
|
+
__init3() {this.gestureKey = null}
|
|
2593
|
+
__init4() {this.gestureCount = 0}
|
|
2594
|
+
__init5() {this.gestureStart = null}
|
|
2595
|
+
__init6() {this.revision = 0}
|
|
2596
|
+
__init7() {this.applying = false}
|
|
2597
|
+
|
|
2598
|
+
__init8() {this.listeners = /* @__PURE__ */ new Set()}
|
|
2599
|
+
|
|
2600
|
+
|
|
2601
|
+
constructor(options = {}) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);_class.prototype.__init5.call(this);_class.prototype.__init6.call(this);_class.prototype.__init7.call(this);_class.prototype.__init8.call(this);_class.prototype.__init9.call(this);_class.prototype.__init10.call(this);
|
|
2602
|
+
this.history = _nullishCoalesce(options.history, () => ( new (0, _chunkQKLRAA4Tcjs.HistoryManager)()));
|
|
2603
|
+
this.onChange = options.onChange;
|
|
2604
|
+
const { spec, issues } = _chunkJOX6MJW4cjs.normalizeChartSpec.call(void 0, _nullishCoalesce(options.spec, () => ( {})));
|
|
2605
|
+
this.spec = spec;
|
|
2606
|
+
this.issues = issues;
|
|
2607
|
+
this.state = this.computeState();
|
|
2608
|
+
this.connect();
|
|
2609
|
+
}
|
|
2610
|
+
connect() {
|
|
2611
|
+
_optionalChain([this, 'access', _45 => _45.unsubscribeHistory, 'optionalCall', _46 => _46()]);
|
|
2612
|
+
this.unsubscribeHistory = this.history.subscribe((h) => {
|
|
2613
|
+
const s = this.state;
|
|
2614
|
+
if (this.applying || s.canUndo === h.canUndo && s.canRedo === h.canRedo && s.undoLabel === h.undoName && s.redoLabel === h.redoName) return;
|
|
2615
|
+
this.notify(false);
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
// -------------------------------------------------------------------------
|
|
2619
|
+
// State
|
|
2620
|
+
// -------------------------------------------------------------------------
|
|
2621
|
+
/** The current snapshot (stable until the next change). */
|
|
2622
|
+
__init9() {this.getState = () => this.state}
|
|
2623
|
+
/** Calls `listener` after every change. Returns an unsubscribe function. */
|
|
2624
|
+
__init10() {this.subscribe = (listener) => {
|
|
2625
|
+
if (!this.unsubscribeHistory) this.connect();
|
|
2626
|
+
this.listeners.add(listener);
|
|
2627
|
+
return () => {
|
|
2628
|
+
this.listeners.delete(listener);
|
|
2629
|
+
};
|
|
2630
|
+
}}
|
|
2631
|
+
/** The current spec (do not mutate; edit through the controller). */
|
|
2632
|
+
getSpec() {
|
|
2633
|
+
return this.spec;
|
|
2634
|
+
}
|
|
2635
|
+
getSeries(id) {
|
|
2636
|
+
return this.spec.series.find((s) => s.id === id);
|
|
2637
|
+
}
|
|
2638
|
+
computeState() {
|
|
2639
|
+
const h = this.history.getState();
|
|
2640
|
+
const sel = this.selection;
|
|
2641
|
+
const seriesId = _optionalChain([sel, 'optionalAccess', _47 => _47.kind]) === "series" || _optionalChain([sel, 'optionalAccess', _48 => _48.kind]) === "point" || _optionalChain([sel, 'optionalAccess', _49 => _49.kind]) === "trendline" ? sel.seriesId : null;
|
|
2642
|
+
return {
|
|
2643
|
+
spec: this.spec,
|
|
2644
|
+
selection: sel,
|
|
2645
|
+
selectedSeries: seriesId ? _nullishCoalesce(this.spec.series.find((s) => s.id === seriesId), () => ( null)) : null,
|
|
2646
|
+
selectedIndex: _optionalChain([sel, 'optionalAccess', _50 => _50.kind]) === "point" || _optionalChain([sel, 'optionalAccess', _51 => _51.kind]) === "category" ? sel.index : null,
|
|
2647
|
+
issues: this.issues,
|
|
2648
|
+
gesture: this.gestureKey,
|
|
2649
|
+
canUndo: h.canUndo,
|
|
2650
|
+
canRedo: h.canRedo,
|
|
2651
|
+
undoLabel: h.undoName,
|
|
2652
|
+
redoLabel: h.redoName,
|
|
2653
|
+
revision: this.revision
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2656
|
+
notify(specChanged) {
|
|
2657
|
+
this.revision++;
|
|
2658
|
+
this.state = this.computeState();
|
|
2659
|
+
if (specChanged) _optionalChain([this, 'access', _52 => _52.onChange, 'optionalCall', _53 => _53(this.spec)]);
|
|
2660
|
+
const state = this.state;
|
|
2661
|
+
this.listeners.forEach((listener) => {
|
|
2662
|
+
try {
|
|
2663
|
+
listener(state);
|
|
2664
|
+
} catch (error) {
|
|
2665
|
+
console.error("[ChartController] Error in state listener:", error);
|
|
2666
|
+
}
|
|
2667
|
+
});
|
|
2668
|
+
}
|
|
2669
|
+
// -------------------------------------------------------------------------
|
|
2670
|
+
// Changes: every edit records a before / after snapshot of the spec
|
|
2671
|
+
// -------------------------------------------------------------------------
|
|
2672
|
+
/** Drops a selection that no longer points at anything. */
|
|
2673
|
+
validSelection(sel, spec) {
|
|
2674
|
+
if (!sel) return null;
|
|
2675
|
+
switch (sel.kind) {
|
|
2676
|
+
case "series":
|
|
2677
|
+
return spec.series.some((s) => s.id === sel.seriesId) ? sel : null;
|
|
2678
|
+
case "point":
|
|
2679
|
+
return spec.series.some((s) => s.id === sel.seriesId) && sel.index < spec.categories.length ? sel : null;
|
|
2680
|
+
case "category":
|
|
2681
|
+
return sel.index < spec.categories.length ? sel : null;
|
|
2682
|
+
case "trendline":
|
|
2683
|
+
return _optionalChain([spec, 'access', _54 => _54.series, 'access', _55 => _55.find, 'call', _56 => _56((s) => s.id === sel.seriesId), 'optionalAccess', _57 => _57.trendlines, 'optionalAccess', _58 => _58[sel.index]]) ? sel : null;
|
|
2684
|
+
case "title":
|
|
2685
|
+
return spec.title ? sel : null;
|
|
2686
|
+
case "subtitle":
|
|
2687
|
+
return spec.subtitle ? sel : null;
|
|
2688
|
+
default:
|
|
2689
|
+
return sel;
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
applySpec(spec) {
|
|
2693
|
+
this.applying = true;
|
|
2694
|
+
try {
|
|
2695
|
+
this.spec = spec;
|
|
2696
|
+
this.selection = this.validSelection(this.selection, spec);
|
|
2697
|
+
} finally {
|
|
2698
|
+
this.applying = false;
|
|
2699
|
+
}
|
|
2700
|
+
this.notify(true);
|
|
2701
|
+
}
|
|
2702
|
+
commit(label, next, select) {
|
|
2703
|
+
const before = this.spec;
|
|
2704
|
+
const hadIssues = this.issues.length > 0;
|
|
2705
|
+
this.issues = [];
|
|
2706
|
+
if (sameChartJson(before, next)) {
|
|
2707
|
+
const reselect = select !== void 0 && !sameChartSelection(this.selection, select) && !(select === null && this.selection === null);
|
|
2708
|
+
if (reselect) this.selection = select;
|
|
2709
|
+
if (reselect || hadIssues) this.notify(false);
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
const command = {
|
|
2713
|
+
name: label,
|
|
2714
|
+
tag: "chart",
|
|
2715
|
+
execute: () => this.applySpec(next),
|
|
2716
|
+
undo: () => this.applySpec(before),
|
|
2717
|
+
redo: () => this.applySpec(next)
|
|
2718
|
+
};
|
|
2719
|
+
if (select !== void 0) this.selection = select;
|
|
2720
|
+
if (this.gestureKey !== null) this.history.executeMerged(command, { key: `chart.gesture:${this.gestureKey}:${this.gestureCount}` });
|
|
2721
|
+
else this.history.execute(command);
|
|
2722
|
+
}
|
|
2723
|
+
reject(message, issues = []) {
|
|
2724
|
+
this.issues = issues.length ? issues : [{ path: "", message }];
|
|
2725
|
+
this.notify(false);
|
|
2726
|
+
return fail(message, issues.length ? issues : void 0);
|
|
2727
|
+
}
|
|
2728
|
+
/** A copy of the spec with `edit` applied (series and categories copied as needed). */
|
|
2729
|
+
edited(edit) {
|
|
2730
|
+
const draft = _chunkJOX6MJW4cjs.cloneChartSpec.call(void 0, this.spec);
|
|
2731
|
+
edit(draft);
|
|
2732
|
+
return draft;
|
|
2733
|
+
}
|
|
2734
|
+
unknownSeries(id) {
|
|
2735
|
+
const ids = this.spec.series.map((s) => `"${s.id}"`);
|
|
2736
|
+
return fail(`There is no series "${id}"; ${ids.length ? `expected one of ${ids.join(", ")}` : "the chart has no series yet"}.`);
|
|
2737
|
+
}
|
|
2738
|
+
badIndex(index) {
|
|
2739
|
+
const n = this.spec.categories.length;
|
|
2740
|
+
return fail(n ? `Category index ${index} is out of range; expected 0 to ${n - 1}.` : "The chart has no categories yet.");
|
|
2741
|
+
}
|
|
2742
|
+
// -------------------------------------------------------------------------
|
|
2743
|
+
// Presentation
|
|
2744
|
+
// -------------------------------------------------------------------------
|
|
2745
|
+
/**
|
|
2746
|
+
* Changes any presentation or data fields as one undo step. All-or-nothing: if anything
|
|
2747
|
+
* is invalid, nothing changes and every problem is returned (and shown in `state.issues`).
|
|
2748
|
+
*/
|
|
2749
|
+
update(patch, options = {}) {
|
|
2750
|
+
const s = this.spec;
|
|
2751
|
+
const merged = {
|
|
2752
|
+
...s,
|
|
2753
|
+
...patch,
|
|
2754
|
+
xAxis: patch.xAxis ? { ...s.xAxis, ...patch.xAxis } : s.xAxis,
|
|
2755
|
+
yAxis: patch.yAxis ? { ...s.yAxis, ...patch.yAxis } : s.yAxis,
|
|
2756
|
+
yAxis2: patch.yAxis2 ? { ...s.yAxis2, ...patch.yAxis2 } : s.yAxis2,
|
|
2757
|
+
legend: patch.legend ? { ...s.legend, ...patch.legend } : s.legend,
|
|
2758
|
+
version: 1
|
|
2759
|
+
};
|
|
2760
|
+
const { spec, issues } = _chunkJOX6MJW4cjs.normalizeChartSpec.call(void 0, merged);
|
|
2761
|
+
if (issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
|
|
2762
|
+
if (JSON.stringify(spec) === JSON.stringify(s)) return { ok: true };
|
|
2763
|
+
this.commit(_nullishCoalesce(options.label, () => ( updateLabel(patch))), spec);
|
|
2764
|
+
return { ok: true };
|
|
2765
|
+
}
|
|
2766
|
+
setType(type) {
|
|
2767
|
+
return this.update({ type }, { label: `Change chart type` });
|
|
2768
|
+
}
|
|
2769
|
+
setTitle(title) {
|
|
2770
|
+
return this.update({ title }, { label: "Edit title" });
|
|
2771
|
+
}
|
|
2772
|
+
setSubtitle(subtitle) {
|
|
2773
|
+
return this.update({ subtitle }, { label: "Edit subtitle" });
|
|
2774
|
+
}
|
|
2775
|
+
/** Changes the category (`x`), value (`y`) or secondary value (`y2`) axis. */
|
|
2776
|
+
setAxis(axis, patch) {
|
|
2777
|
+
const key = axis === "x" ? "xAxis" : axis === "y" ? "yAxis" : "yAxis2";
|
|
2778
|
+
return this.update({ [key]: patch }, { label: `Change ${axis === "x" ? "category" : axis === "y" ? "value" : "secondary value"} axis` });
|
|
2779
|
+
}
|
|
2780
|
+
/**
|
|
2781
|
+
* Applies a named quick layout and / or colour scheme (see `CHART_LAYOUT_PRESETS` and
|
|
2782
|
+
* `CHART_COLOR_SCHEMES`) as one undo step.
|
|
2783
|
+
*/
|
|
2784
|
+
applyPreset(options) {
|
|
2785
|
+
const preset = chartPresetPatch(options);
|
|
2786
|
+
if (!preset.ok) return this.reject(preset.message);
|
|
2787
|
+
const label = options.layout && options.colors ? "Apply chart style" : options.layout ? "Apply quick layout" : "Change colours";
|
|
2788
|
+
return this.update(preset.patch, { label });
|
|
2789
|
+
}
|
|
2790
|
+
setLegend(patch) {
|
|
2791
|
+
return this.update({ legend: patch }, { label: "Change legend" });
|
|
2792
|
+
}
|
|
2793
|
+
// -------------------------------------------------------------------------
|
|
2794
|
+
// Data
|
|
2795
|
+
// -------------------------------------------------------------------------
|
|
2796
|
+
/**
|
|
2797
|
+
* Replaces categories and / or series as one undo step. Values are validated and padded
|
|
2798
|
+
* to the categories; all-or-nothing like `update`.
|
|
2799
|
+
*/
|
|
2800
|
+
setData(data, options = {}) {
|
|
2801
|
+
return this.update({ ...data.categories ? { categories: data.categories } : {}, ...data.series ? { series: data.series } : {} }, { label: _nullishCoalesce(options.label, () => ( "Edit chart data")) });
|
|
2802
|
+
}
|
|
2803
|
+
/**
|
|
2804
|
+
* Sets one value (numbers, numeric text such as `1,200` or `12%`, or blank for a gap).
|
|
2805
|
+
* One undo step, or part of the current gesture.
|
|
2806
|
+
*/
|
|
2807
|
+
setValue(seriesId, index, value, options = {}) {
|
|
2808
|
+
const series = this.getSeries(seriesId);
|
|
2809
|
+
if (!series) return this.unknownSeries(seriesId);
|
|
2810
|
+
if (!Number.isInteger(index) || index < 0 || index >= this.spec.categories.length) return this.badIndex(index);
|
|
2811
|
+
const parsed = _chunkJOX6MJW4cjs.parseChartValue.call(void 0, value);
|
|
2812
|
+
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.` }]);
|
|
2813
|
+
if (series.values[index] === parsed) {
|
|
2814
|
+
if (this.issues.length) {
|
|
2815
|
+
this.issues = [];
|
|
2816
|
+
this.notify(false);
|
|
2817
|
+
}
|
|
2818
|
+
return { ok: true };
|
|
2819
|
+
}
|
|
2820
|
+
const next = this.edited((d) => {
|
|
2821
|
+
d.series.find((s) => s.id === seriesId).values[index] = parsed;
|
|
2822
|
+
});
|
|
2823
|
+
this.commit(_nullishCoalesce(options.label, () => ( `Edit ${series.name} \xB7 ${this.spec.categories[index]}`)), next);
|
|
2824
|
+
return { ok: true };
|
|
2825
|
+
}
|
|
2826
|
+
/** Several values as one undo step (all-or-nothing). */
|
|
2827
|
+
setValues(cells, options = {}) {
|
|
2828
|
+
const issues = [];
|
|
2829
|
+
const parsed = [];
|
|
2830
|
+
for (const cell of cells) {
|
|
2831
|
+
if (!this.getSeries(cell.seriesId)) issues.push({ path: cell.seriesId, message: this.unknownSeries(cell.seriesId).message });
|
|
2832
|
+
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 });
|
|
2833
|
+
else {
|
|
2834
|
+
const value = _chunkJOX6MJW4cjs.parseChartValue.call(void 0, cell.value);
|
|
2835
|
+
if (value === void 0) issues.push({ path: `${cell.seriesId}[${cell.index}]`, message: `"${String(cell.value)}" is not a number.` });
|
|
2836
|
+
else parsed.push({ seriesId: cell.seriesId, index: cell.index, value });
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
if (issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
|
|
2840
|
+
const next = this.edited((d) => {
|
|
2841
|
+
for (const cell of parsed) d.series.find((s) => s.id === cell.seriesId).values[cell.index] = cell.value;
|
|
2842
|
+
});
|
|
2843
|
+
if (JSON.stringify(next.series) !== JSON.stringify(this.spec.series)) this.commit(_nullishCoalesce(options.label, () => ( "Edit values")), next);
|
|
2844
|
+
return { ok: true };
|
|
2845
|
+
}
|
|
2846
|
+
/** Adds a series (values padded to the categories) and selects it. Returns its id. */
|
|
2847
|
+
addSeries(input = {}, options = {}) {
|
|
2848
|
+
const id = input.id && !this.getSeries(input.id) ? input.id : _chunkJOX6MJW4cjs.uniqueChartSeriesId.call(void 0, this.spec.series);
|
|
2849
|
+
const name = _nullishCoalesce(input.name, () => ( `Series ${this.spec.series.length + 1}`));
|
|
2850
|
+
const at = Math.max(0, Math.min(this.spec.series.length, _nullishCoalesce(options.at, () => ( this.spec.series.length))));
|
|
2851
|
+
const series = [...this.spec.series];
|
|
2852
|
+
series.splice(at, 0, { ...input, id, name, values: _nullishCoalesce(input.values, () => ( [])) });
|
|
2853
|
+
const { spec, issues } = _chunkJOX6MJW4cjs.normalizeChartSpec.call(void 0, { ...this.spec, series });
|
|
2854
|
+
if (issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
|
|
2855
|
+
this.commit(`Add series "${name}"`, spec, { kind: "series", seriesId: id });
|
|
2856
|
+
return { ok: true, id };
|
|
2857
|
+
}
|
|
2858
|
+
removeSeries(id) {
|
|
2859
|
+
const series = this.getSeries(id);
|
|
2860
|
+
if (!series) return this.unknownSeries(id);
|
|
2861
|
+
this.commit(
|
|
2862
|
+
`Delete series "${series.name}"`,
|
|
2863
|
+
this.edited((d) => {
|
|
2864
|
+
d.series = d.series.filter((s) => s.id !== id);
|
|
2865
|
+
})
|
|
2866
|
+
);
|
|
2867
|
+
return { ok: true };
|
|
2868
|
+
}
|
|
2869
|
+
updateSeries(id, patch) {
|
|
2870
|
+
const series = this.getSeries(id);
|
|
2871
|
+
if (!series) return this.unknownSeries(id);
|
|
2872
|
+
const next = _chunkJOX6MJW4cjs.cloneChartSeries.call(void 0, series);
|
|
2873
|
+
if (patch.name !== void 0) next.name = String(patch.name);
|
|
2874
|
+
if (patch.color === null || patch.color === "") delete next.color;
|
|
2875
|
+
else if (patch.color !== void 0) next.color = patch.color;
|
|
2876
|
+
if (patch.type === null) delete next.type;
|
|
2877
|
+
else if (patch.type !== void 0) next.type = patch.type;
|
|
2878
|
+
if (patch.hidden !== void 0) {
|
|
2879
|
+
if (patch.hidden) next.hidden = true;
|
|
2880
|
+
else delete next.hidden;
|
|
2881
|
+
}
|
|
2882
|
+
const resettable = ["axis", "x", "sizes", "pointColors", "marker", "curve", "dataLabels", "format"];
|
|
2883
|
+
for (const key of resettable) {
|
|
2884
|
+
if (!(key in patch) || patch[key] === void 0) continue;
|
|
2885
|
+
if (patch[key] === null) delete next[key];
|
|
2886
|
+
else next[key] = patch[key];
|
|
2887
|
+
}
|
|
2888
|
+
if (next.axis === "primary") delete next.axis;
|
|
2889
|
+
const series2 = this.spec.series.map((s) => s.id === id ? next : s);
|
|
2890
|
+
const { spec, issues } = _chunkJOX6MJW4cjs.normalizeChartSpec.call(void 0, { ...this.spec, series: series2 });
|
|
2891
|
+
if (issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
|
|
2892
|
+
if (JSON.stringify(spec.series) === JSON.stringify(this.spec.series)) return { ok: true };
|
|
2893
|
+
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}"`;
|
|
2894
|
+
this.commit(label, spec);
|
|
2895
|
+
return { ok: true };
|
|
2896
|
+
}
|
|
2897
|
+
toggleSeries(id) {
|
|
2898
|
+
const series = this.getSeries(id);
|
|
2899
|
+
if (!series) return this.unknownSeries(id);
|
|
2900
|
+
return this.updateSeries(id, { hidden: !series.hidden });
|
|
2901
|
+
}
|
|
2902
|
+
/** Moves a series to position `to` (drawing and legend order). */
|
|
2903
|
+
moveSeries(id, to) {
|
|
2904
|
+
const from = this.spec.series.findIndex((s) => s.id === id);
|
|
2905
|
+
if (from < 0) return this.unknownSeries(id);
|
|
2906
|
+
const target = Math.max(0, Math.min(this.spec.series.length - 1, to));
|
|
2907
|
+
if (target === from) return { ok: true };
|
|
2908
|
+
this.commit(
|
|
2909
|
+
"Reorder series",
|
|
2910
|
+
this.edited((d) => {
|
|
2911
|
+
const [s] = d.series.splice(from, 1);
|
|
2912
|
+
d.series.splice(target, 0, s);
|
|
2913
|
+
})
|
|
2914
|
+
);
|
|
2915
|
+
return { ok: true };
|
|
2916
|
+
}
|
|
2917
|
+
/** Inserts a category (with gaps in every series) at `at` (default: the end). Returns its index. */
|
|
2918
|
+
addCategory(label, options = {}) {
|
|
2919
|
+
const at = Math.max(0, Math.min(this.spec.categories.length, _nullishCoalesce(options.at, () => ( this.spec.categories.length))));
|
|
2920
|
+
const name = _nullishCoalesce(label, () => ( `${this.spec.categories.length + 1}`));
|
|
2921
|
+
this.commit(
|
|
2922
|
+
`Add category "${name}"`,
|
|
2923
|
+
this.edited((d) => {
|
|
2924
|
+
d.categories.splice(at, 0, name);
|
|
2925
|
+
for (const s of d.series) s.values.splice(at, 0, null);
|
|
2926
|
+
}),
|
|
2927
|
+
{ kind: "category", index: at }
|
|
2928
|
+
);
|
|
2929
|
+
return { ok: true, index: at };
|
|
2930
|
+
}
|
|
2931
|
+
removeCategory(index) {
|
|
2932
|
+
if (!Number.isInteger(index) || index < 0 || index >= this.spec.categories.length) return this.badIndex(index);
|
|
2933
|
+
this.commit(
|
|
2934
|
+
`Delete category "${this.spec.categories[index]}"`,
|
|
2935
|
+
this.edited((d) => {
|
|
2936
|
+
d.categories.splice(index, 1);
|
|
2937
|
+
for (const s of d.series) s.values.splice(index, 1);
|
|
2938
|
+
})
|
|
2939
|
+
);
|
|
2940
|
+
return { ok: true };
|
|
2941
|
+
}
|
|
2942
|
+
renameCategory(index, label) {
|
|
2943
|
+
if (!Number.isInteger(index) || index < 0 || index >= this.spec.categories.length) return this.badIndex(index);
|
|
2944
|
+
if (this.spec.categories[index] === label) return { ok: true };
|
|
2945
|
+
this.commit(
|
|
2946
|
+
"Rename category",
|
|
2947
|
+
this.edited((d) => {
|
|
2948
|
+
d.categories[index] = String(label);
|
|
2949
|
+
})
|
|
2950
|
+
);
|
|
2951
|
+
return { ok: true };
|
|
2952
|
+
}
|
|
2953
|
+
moveCategory(from, to) {
|
|
2954
|
+
if (!Number.isInteger(from) || from < 0 || from >= this.spec.categories.length) return this.badIndex(from);
|
|
2955
|
+
const target = Math.max(0, Math.min(this.spec.categories.length - 1, to));
|
|
2956
|
+
if (target === from) return { ok: true };
|
|
2957
|
+
const move = (list) => {
|
|
2958
|
+
const [item] = list.splice(from, 1);
|
|
2959
|
+
list.splice(target, 0, item);
|
|
2960
|
+
};
|
|
2961
|
+
this.commit(
|
|
2962
|
+
"Reorder categories",
|
|
2963
|
+
this.edited((d) => {
|
|
2964
|
+
move(d.categories);
|
|
2965
|
+
for (const s of d.series) move(s.values);
|
|
2966
|
+
})
|
|
2967
|
+
);
|
|
2968
|
+
return { ok: true };
|
|
2969
|
+
}
|
|
2970
|
+
/**
|
|
2971
|
+
* Pastes a block of cells (TSV / CSV text or rows) into the data grid with its top-left at
|
|
2972
|
+
* `row` (category index) and `column` (series index; `-1` is the category column). The
|
|
2973
|
+
* grid grows to fit. Non-numeric value cells become gaps and are reported in `issues`.
|
|
2974
|
+
*/
|
|
2975
|
+
pasteCells(row, column, cells) {
|
|
2976
|
+
const rows = typeof cells === "string" ? cells.includes(" ") ? _chunkTNNJXF46cjs.parseTsv.call(void 0, cells) : _chunkTNNJXF46cjs.parseCsv.call(void 0, cells) : cells.map((r) => [...r]);
|
|
2977
|
+
if (!rows.length) return { ok: true };
|
|
2978
|
+
const issues = [];
|
|
2979
|
+
const next = this.edited((d) => {
|
|
2980
|
+
const width = Math.max(...rows.map((r) => r.length));
|
|
2981
|
+
const lastSeries = column + width - 1;
|
|
2982
|
+
while (d.series.length <= lastSeries) {
|
|
2983
|
+
const id = _chunkJOX6MJW4cjs.uniqueChartSeriesId.call(void 0, d.series);
|
|
2984
|
+
d.series.push({ id, name: `Series ${d.series.length + 1}`, values: d.categories.map(() => null) });
|
|
2985
|
+
}
|
|
2986
|
+
const lastRow = row + rows.length - 1;
|
|
2987
|
+
while (d.categories.length <= lastRow) {
|
|
2988
|
+
d.categories.push(`${d.categories.length + 1}`);
|
|
2989
|
+
for (const s of d.series) s.values.push(null);
|
|
2990
|
+
}
|
|
2991
|
+
rows.forEach((cellsInRow, r) => {
|
|
2992
|
+
cellsInRow.forEach((cell, c) => {
|
|
2993
|
+
const col = column + c;
|
|
2994
|
+
const index = row + r;
|
|
2995
|
+
if (col < 0) d.categories[index] = cell === null || cell === void 0 ? "" : String(cell).trim();
|
|
2996
|
+
else {
|
|
2997
|
+
const parsed = _chunkJOX6MJW4cjs.parseChartValue.call(void 0, cell);
|
|
2998
|
+
if (parsed === void 0) issues.push({ path: `${d.series[col].id}[${index}]`, message: `"${String(cell)}" is not a number; it was left blank.` });
|
|
2999
|
+
d.series[col].values[index] = _nullishCoalesce(parsed, () => ( null));
|
|
3000
|
+
}
|
|
3001
|
+
});
|
|
3002
|
+
});
|
|
3003
|
+
});
|
|
3004
|
+
this.commit("Paste cells", next);
|
|
3005
|
+
if (issues.length) {
|
|
3006
|
+
this.issues = issues;
|
|
3007
|
+
this.notify(false);
|
|
3008
|
+
}
|
|
3009
|
+
return issues.length ? { ok: true, issues } : { ok: true };
|
|
3010
|
+
}
|
|
3011
|
+
/**
|
|
3012
|
+
* Replaces the data with a grid of cells (see `chartDataFromTable`): one undo step. Series
|
|
3013
|
+
* colours and types are kept by name.
|
|
3014
|
+
*/
|
|
3015
|
+
importTable(rows, options = {}) {
|
|
3016
|
+
const data = _chunkJOX6MJW4cjs.chartDataFromTable.call(void 0, rows, options);
|
|
3017
|
+
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.");
|
|
3018
|
+
const byName = new Map(this.spec.series.map((s) => [s.name, s]));
|
|
3019
|
+
const series = data.series.map((s) => {
|
|
3020
|
+
const old = byName.get(s.name);
|
|
3021
|
+
return { ...s, ..._optionalChain([old, 'optionalAccess', _59 => _59.color]) ? { color: old.color } : {}, ..._optionalChain([old, 'optionalAccess', _60 => _60.type]) ? { type: old.type } : {}, ...old ? { id: old.id } : {} };
|
|
3022
|
+
});
|
|
3023
|
+
const { spec } = _chunkJOX6MJW4cjs.normalizeChartSpec.call(void 0, { ...this.spec, categories: data.categories, series });
|
|
3024
|
+
this.commit("Import data", spec);
|
|
3025
|
+
if (data.issues.length) {
|
|
3026
|
+
this.issues = data.issues;
|
|
3027
|
+
this.notify(false);
|
|
3028
|
+
return { ok: true, issues: data.issues };
|
|
3029
|
+
}
|
|
3030
|
+
return { ok: true };
|
|
3031
|
+
}
|
|
3032
|
+
/** Imports CSV (or TSV, detected by tabs) text; see `importTable`. */
|
|
3033
|
+
importCsv(text, options = {}) {
|
|
3034
|
+
const delimiter = _nullishCoalesce(options.delimiter, () => ( (text.includes(" ") ? " " : ",")));
|
|
3035
|
+
return this.importTable(_chunkTNNJXF46cjs.parseCsv.call(void 0, text, { delimiter }), options);
|
|
3036
|
+
}
|
|
3037
|
+
// -------------------------------------------------------------------------
|
|
3038
|
+
// Trendlines
|
|
3039
|
+
// -------------------------------------------------------------------------
|
|
3040
|
+
/** Adds a trendline to a series and selects it. Returns its index (and the fit, when it can be computed). */
|
|
3041
|
+
addTrendline(seriesId, input) {
|
|
3042
|
+
const series = this.getSeries(seriesId);
|
|
3043
|
+
if (!series) return this.unknownSeries(seriesId);
|
|
3044
|
+
if (!chartSupportsTrendlines(this.spec.type, this.spec.stacking))
|
|
3045
|
+
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.`);
|
|
3046
|
+
const issues = [];
|
|
3047
|
+
const line = _chunkJOX6MJW4cjs.normalizeChartTrendline.call(void 0, input, "trendline", issues);
|
|
3048
|
+
if (!line || issues.length) return this.reject(issues.map((i) => i.message).join(" ") || "The trendline is not valid.", issues);
|
|
3049
|
+
const index = _nullishCoalesce(_optionalChain([series, 'access', _61 => _61.trendlines, 'optionalAccess', _62 => _62.length]), () => ( 0));
|
|
3050
|
+
const next = this.edited((d) => {
|
|
3051
|
+
const s = d.series.find((x) => x.id === seriesId);
|
|
3052
|
+
s.trendlines = [..._nullishCoalesce(s.trendlines, () => ( [])), line];
|
|
3053
|
+
});
|
|
3054
|
+
this.commit(`Add trendline to "${series.name}"`, next, { kind: "trendline", seriesId, index });
|
|
3055
|
+
return { ok: true, index, fit: this.trendlineFit(seriesId, index) };
|
|
3056
|
+
}
|
|
3057
|
+
/** Changes a trendline (all-or-nothing). */
|
|
3058
|
+
updateTrendline(seriesId, index, patch) {
|
|
3059
|
+
const series = this.getSeries(seriesId);
|
|
3060
|
+
if (!series) return this.unknownSeries(seriesId);
|
|
3061
|
+
const current = _optionalChain([series, 'access', _63 => _63.trendlines, 'optionalAccess', _64 => _64[index]]);
|
|
3062
|
+
if (!current) return fail(`"${series.name}" has no trendline ${index}; ${_optionalChain([series, 'access', _65 => _65.trendlines, 'optionalAccess', _66 => _66.length]) ? `expected 0 to ${series.trendlines.length - 1}` : "it has none yet"}.`);
|
|
3063
|
+
const issues = [];
|
|
3064
|
+
const merged = { ...current, ...patch };
|
|
3065
|
+
for (const [k, v] of Object.entries(patch)) if (v === null) delete merged[k];
|
|
3066
|
+
const line = _chunkJOX6MJW4cjs.normalizeChartTrendline.call(void 0, merged, "trendline", issues);
|
|
3067
|
+
if (!line || issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
|
|
3068
|
+
this.commit(
|
|
3069
|
+
"Change trendline",
|
|
3070
|
+
this.edited((d) => {
|
|
3071
|
+
d.series.find((x) => x.id === seriesId).trendlines[index] = line;
|
|
3072
|
+
})
|
|
3073
|
+
);
|
|
3074
|
+
return { ok: true };
|
|
3075
|
+
}
|
|
3076
|
+
removeTrendline(seriesId, index) {
|
|
3077
|
+
const series = this.getSeries(seriesId);
|
|
3078
|
+
if (!series) return this.unknownSeries(seriesId);
|
|
3079
|
+
if (!_optionalChain([series, 'access', _67 => _67.trendlines, 'optionalAccess', _68 => _68[index]])) return fail(`"${series.name}" has no trendline ${index}.`);
|
|
3080
|
+
this.commit(
|
|
3081
|
+
"Delete trendline",
|
|
3082
|
+
this.edited((d) => {
|
|
3083
|
+
const s = d.series.find((x) => x.id === seriesId);
|
|
3084
|
+
s.trendlines = s.trendlines.filter((_, i) => i !== index);
|
|
3085
|
+
if (!s.trendlines.length) delete s.trendlines;
|
|
3086
|
+
})
|
|
3087
|
+
);
|
|
3088
|
+
return { ok: true };
|
|
3089
|
+
}
|
|
3090
|
+
/** The points a series' trendlines are fitted to: (x, y) with x = 1, 2, 3 … on category charts. */
|
|
3091
|
+
trendlinePoints(seriesId) {
|
|
3092
|
+
const series = this.getSeries(seriesId);
|
|
3093
|
+
if (!series) return [];
|
|
3094
|
+
const xy = _chunkJOX6MJW4cjs.chartFamily.call(void 0, this.spec) === "xy";
|
|
3095
|
+
const out = [];
|
|
3096
|
+
series.values.forEach((v, i) => {
|
|
3097
|
+
const x = xy ? _nullishCoalesce(_optionalChain([series, 'access', _69 => _69.x, 'optionalAccess', _70 => _70[i]]), () => ( (this.spec.type === "bubble" ? i + 1 : null))) : i + 1;
|
|
3098
|
+
if (v !== null && x !== null && Number.isFinite(v) && Number.isFinite(x)) out.push([x, v]);
|
|
3099
|
+
});
|
|
3100
|
+
return out;
|
|
3101
|
+
}
|
|
3102
|
+
/** The fitted coefficients, R² and equation of a trendline (null for moving averages or unfittable data). */
|
|
3103
|
+
trendlineFit(seriesId, index) {
|
|
3104
|
+
const line = _optionalChain([this, 'access', _71 => _71.getSeries, 'call', _72 => _72(seriesId), 'optionalAccess', _73 => _73.trendlines, 'optionalAccess', _74 => _74[index]]);
|
|
3105
|
+
if (!line) return null;
|
|
3106
|
+
return fitChartTrendline(line, this.trendlinePoints(seriesId));
|
|
3107
|
+
}
|
|
3108
|
+
/** The moving-average points of a trendline (empty for other kinds). */
|
|
3109
|
+
trendlineMovingAverage(seriesId, index) {
|
|
3110
|
+
const line = _optionalChain([this, 'access', _75 => _75.getSeries, 'call', _76 => _76(seriesId), 'optionalAccess', _77 => _77.trendlines, 'optionalAccess', _78 => _78[index]]);
|
|
3111
|
+
if (!line || line.type !== "moving-average") return [];
|
|
3112
|
+
return chartMovingAverage(this.trendlinePoints(seriesId), _nullishCoalesce(line.period, () => ( 2)));
|
|
3113
|
+
}
|
|
3114
|
+
// -------------------------------------------------------------------------
|
|
3115
|
+
// Reshaping
|
|
3116
|
+
// -------------------------------------------------------------------------
|
|
3117
|
+
/**
|
|
3118
|
+
* Switches rows and columns: series become categories and categories become series (one
|
|
3119
|
+
* undo step). Series colours, types and other per-series settings are dropped.
|
|
3120
|
+
*/
|
|
3121
|
+
transpose() {
|
|
3122
|
+
const s = this.spec;
|
|
3123
|
+
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.");
|
|
3124
|
+
const used = /* @__PURE__ */ new Set();
|
|
3125
|
+
const series = s.categories.map((name, i) => {
|
|
3126
|
+
let id = `series-${i + 1}`;
|
|
3127
|
+
for (let k = i + 2; used.has(id); k++) id = `series-${k}`;
|
|
3128
|
+
used.add(id);
|
|
3129
|
+
return { id, name, values: s.series.map((x) => _nullishCoalesce(x.values[i], () => ( null))) };
|
|
3130
|
+
});
|
|
3131
|
+
const { spec } = _chunkJOX6MJW4cjs.normalizeChartSpec.call(void 0, { ...s, categories: s.series.map((x) => x.name), series });
|
|
3132
|
+
this.commit("Switch rows and columns", spec, null);
|
|
3133
|
+
return { ok: true };
|
|
3134
|
+
}
|
|
3135
|
+
// -------------------------------------------------------------------------
|
|
3136
|
+
// Selection and gestures
|
|
3137
|
+
// -------------------------------------------------------------------------
|
|
3138
|
+
/** Selects a chart element (view state; not an undo step). */
|
|
3139
|
+
select(selection) {
|
|
3140
|
+
const next = this.validSelection(selection, this.spec);
|
|
3141
|
+
if (sameChartSelection(next, this.selection) || next === null && this.selection === null) return;
|
|
3142
|
+
this.selection = next;
|
|
3143
|
+
this.notify(false);
|
|
3144
|
+
}
|
|
3145
|
+
/**
|
|
3146
|
+
* Deletes what is selected: a series or category is removed, a point becomes a gap, a
|
|
3147
|
+
* title is cleared, the legend or an axis is hidden.
|
|
3148
|
+
*/
|
|
3149
|
+
deleteSelection() {
|
|
3150
|
+
const sel = this.selection;
|
|
3151
|
+
if (!sel) return fail("Nothing is selected.");
|
|
3152
|
+
switch (sel.kind) {
|
|
3153
|
+
case "series":
|
|
3154
|
+
return this.removeSeries(sel.seriesId);
|
|
3155
|
+
case "category":
|
|
3156
|
+
return this.removeCategory(sel.index);
|
|
3157
|
+
case "point":
|
|
3158
|
+
return this.setValue(sel.seriesId, sel.index, null, { label: "Clear value" });
|
|
3159
|
+
case "title":
|
|
3160
|
+
return this.setTitle("");
|
|
3161
|
+
case "subtitle":
|
|
3162
|
+
return this.setSubtitle("");
|
|
3163
|
+
case "legend":
|
|
3164
|
+
return this.setLegend({ show: false });
|
|
3165
|
+
case "axis":
|
|
3166
|
+
return this.setAxis(sel.axis, { show: false });
|
|
3167
|
+
case "trendline":
|
|
3168
|
+
return this.removeTrendline(sel.seriesId, sel.index);
|
|
3169
|
+
default:
|
|
3170
|
+
return fail("The chart itself cannot be deleted.");
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
/** Adds `delta` to a value (a gap counts as 0). */
|
|
3174
|
+
nudgeValue(seriesId, index, delta) {
|
|
3175
|
+
const series = this.getSeries(seriesId);
|
|
3176
|
+
if (!series) return this.unknownSeries(seriesId);
|
|
3177
|
+
const current = _nullishCoalesce(series.values[index], () => ( 0));
|
|
3178
|
+
return this.setValue(seriesId, index, Number((current + delta).toPrecision(12)), { label: `Edit ${series.name} \xB7 ${_nullishCoalesce(this.spec.categories[index], () => ( index))}` });
|
|
3179
|
+
}
|
|
3180
|
+
/** Starts a continuous edit (a drag): every change until `endGesture()` is one undo step. */
|
|
3181
|
+
beginGesture(key) {
|
|
3182
|
+
if (this.gestureKey === key) return;
|
|
3183
|
+
this.history.endGesture();
|
|
3184
|
+
this.gestureKey = key;
|
|
3185
|
+
this.gestureCount++;
|
|
3186
|
+
this.gestureStart = { spec: this.spec, mark: this.history.mark() };
|
|
3187
|
+
this.notify(false);
|
|
3188
|
+
}
|
|
3189
|
+
/**
|
|
3190
|
+
* Cancels the gesture in progress (Escape during a drag): everything it changed is taken
|
|
3191
|
+
* back and leaves no undo or redo step.
|
|
3192
|
+
*/
|
|
3193
|
+
cancelGesture() {
|
|
3194
|
+
if (this.gestureKey === null) return;
|
|
3195
|
+
const start = this.gestureStart;
|
|
3196
|
+
this.gestureKey = null;
|
|
3197
|
+
this.gestureStart = null;
|
|
3198
|
+
this.history.endGesture();
|
|
3199
|
+
if (start) this.history.revertTo(start.mark);
|
|
3200
|
+
this.notify(false);
|
|
3201
|
+
}
|
|
3202
|
+
endGesture() {
|
|
3203
|
+
if (this.gestureKey === null) return;
|
|
3204
|
+
this.gestureKey = null;
|
|
3205
|
+
this.history.endGesture();
|
|
3206
|
+
const start = this.gestureStart;
|
|
3207
|
+
this.gestureStart = null;
|
|
3208
|
+
if (start && sameChartJson(start.spec, this.spec)) this.history.revertTo(start.mark);
|
|
3209
|
+
this.notify(false);
|
|
3210
|
+
}
|
|
3211
|
+
// -------------------------------------------------------------------------
|
|
3212
|
+
// Persistence and history
|
|
3213
|
+
// -------------------------------------------------------------------------
|
|
3214
|
+
/** The spec, for saving (a copy). */
|
|
3215
|
+
toJSON() {
|
|
3216
|
+
return _chunkJOX6MJW4cjs.cloneChartSpec.call(void 0, this.spec);
|
|
3217
|
+
}
|
|
3218
|
+
/**
|
|
3219
|
+
* Replaces the chart without an undo step (e.g. when opening a document). Problems are
|
|
3220
|
+
* fixed with defaults and reported in `state.issues` (and returned).
|
|
3221
|
+
*/
|
|
3222
|
+
load(input, options = {}) {
|
|
3223
|
+
const { spec, issues } = _chunkJOX6MJW4cjs.normalizeChartSpec.call(void 0, input);
|
|
3224
|
+
this.spec = spec;
|
|
3225
|
+
this.issues = issues;
|
|
3226
|
+
this.selection = this.validSelection(this.selection, spec);
|
|
3227
|
+
if (options.clearHistory) {
|
|
3228
|
+
this.applying = true;
|
|
3229
|
+
try {
|
|
3230
|
+
this.history.clear();
|
|
3231
|
+
} finally {
|
|
3232
|
+
this.applying = false;
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
this.notify(true);
|
|
3236
|
+
return issues;
|
|
3237
|
+
}
|
|
3238
|
+
undo() {
|
|
3239
|
+
this.endGesture();
|
|
3240
|
+
return this.history.undo();
|
|
3241
|
+
}
|
|
3242
|
+
redo() {
|
|
3243
|
+
this.endGesture();
|
|
3244
|
+
return this.history.redo();
|
|
3245
|
+
}
|
|
3246
|
+
/** Stops listening to the history (reconnected by the next `subscribe`). */
|
|
3247
|
+
destroy() {
|
|
3248
|
+
_optionalChain([this, 'access', _79 => _79.unsubscribeHistory, 'optionalCall', _80 => _80()]);
|
|
3249
|
+
this.unsubscribeHistory = void 0;
|
|
3250
|
+
this.listeners.clear();
|
|
3251
|
+
}
|
|
3252
|
+
}, _class);
|
|
3253
|
+
function createChartController(options = {}) {
|
|
3254
|
+
return new ChartController(options);
|
|
3255
|
+
}
|
|
3256
|
+
function updateLabel(patch) {
|
|
3257
|
+
const keys = Object.keys(patch);
|
|
3258
|
+
if (keys.length !== 1) return "Change chart";
|
|
3259
|
+
const labels = {
|
|
3260
|
+
type: "Change chart type",
|
|
3261
|
+
title: "Edit title",
|
|
3262
|
+
subtitle: "Edit subtitle",
|
|
3263
|
+
categories: "Edit categories",
|
|
3264
|
+
series: "Edit series",
|
|
3265
|
+
xAxis: "Change category axis",
|
|
3266
|
+
yAxis: "Change value axis",
|
|
3267
|
+
yAxis2: "Change secondary value axis",
|
|
3268
|
+
labelPosition: "Change label position",
|
|
3269
|
+
gapWidth: "Change bar spacing",
|
|
3270
|
+
overlap: "Change bar overlap",
|
|
3271
|
+
bins: "Change histogram bins",
|
|
3272
|
+
showTotal: "Toggle waterfall total",
|
|
3273
|
+
legend: "Change legend",
|
|
3274
|
+
stacking: "Change stacking",
|
|
3275
|
+
orientation: "Change orientation",
|
|
3276
|
+
curve: "Change line style",
|
|
3277
|
+
points: "Toggle points",
|
|
3278
|
+
dataLabels: "Toggle data labels",
|
|
3279
|
+
palette: "Change colours",
|
|
3280
|
+
innerRadius: "Change donut hole"
|
|
3281
|
+
};
|
|
3282
|
+
return _nullishCoalesce(labels[keys[0]], () => ( "Change chart"));
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
// src/chart/sparkline.ts
|
|
3286
|
+
var MAX_POINTS = 1e5;
|
|
3287
|
+
function layoutSparkline(values, options) {
|
|
3288
|
+
const kind = _nullishCoalesce(options.kind, () => ( "line"));
|
|
3289
|
+
const w = Number.isFinite(options.width) ? Math.max(1, options.width) : 1;
|
|
3290
|
+
const h = Number.isFinite(options.height) ? Math.max(1, options.height) : 1;
|
|
3291
|
+
const color = _nullishCoalesce(options.color, () => ( "#4269d0"));
|
|
3292
|
+
const negative = _nullishCoalesce(options.negativeColor, () => ( "#e5484d"));
|
|
3293
|
+
const theme = { ..._chunkJOX6MJW4cjs.DEFAULT_CHART_THEME, background: "transparent" };
|
|
3294
|
+
const marks = [];
|
|
3295
|
+
const scene = { width: w, height: h, plot: { x: 0, y: 0, width: w, height: h }, marks, percent: false, theme, warnings: [] };
|
|
3296
|
+
const vals = values.slice(0, MAX_POINTS).map((v) => v !== null && Number.isFinite(v) ? Math.max(-1e300, Math.min(1e300, v)) : null);
|
|
3297
|
+
const n = vals.length;
|
|
3298
|
+
if (!n) return scene;
|
|
3299
|
+
const pad = Math.max(0.5, Math.min(2, h * 0.1));
|
|
3300
|
+
const x0 = pad;
|
|
3301
|
+
const y0 = pad;
|
|
3302
|
+
const iw = Math.max(0.5, w - 2 * pad);
|
|
3303
|
+
const ih = Math.max(0.5, h - 2 * pad);
|
|
3304
|
+
let lo = Infinity;
|
|
3305
|
+
let hi = -Infinity;
|
|
3306
|
+
for (const v of vals) if (v !== null) lo = Math.min(lo, v), hi = Math.max(hi, v);
|
|
3307
|
+
if (!Number.isFinite(lo)) return scene;
|
|
3308
|
+
if (options.min !== void 0 && Number.isFinite(options.min)) lo = options.min;
|
|
3309
|
+
if (options.max !== void 0 && Number.isFinite(options.max)) hi = options.max;
|
|
3310
|
+
let hiAt = -1;
|
|
3311
|
+
let loAt = -1;
|
|
3312
|
+
vals.forEach((v, i) => {
|
|
3313
|
+
if (v === null) return;
|
|
3314
|
+
if (hiAt < 0 || v > vals[hiAt]) hiAt = i;
|
|
3315
|
+
if (loAt < 0 || v < vals[loAt]) loAt = i;
|
|
3316
|
+
});
|
|
3317
|
+
if (kind === "line") {
|
|
3318
|
+
const span2 = hi - lo;
|
|
3319
|
+
const fy2 = (v) => y0 + ih - (span2 > 0 ? (v - lo) / span2 : 0.5) * ih;
|
|
3320
|
+
const fx = (j) => n > 1 ? x0 + j / (n - 1) * iw : x0 + iw / 2;
|
|
3321
|
+
let d = "";
|
|
3322
|
+
let run = [];
|
|
3323
|
+
const flush = () => {
|
|
3324
|
+
if (run.length >= 2) {
|
|
3325
|
+
const pts = run.length > 4e3 ? run.filter((_, i) => i % (Math.floor(run.length / 2e3) + 1) === 0) : run;
|
|
3326
|
+
d += pts.map(([x, y], i) => `${i ? "L" : "M"}${r2(x)},${r2(y)}`).join("");
|
|
3327
|
+
}
|
|
3328
|
+
run = [];
|
|
3329
|
+
};
|
|
3330
|
+
vals.forEach((v, j) => v === null ? flush() : run.push([fx(j), fy2(v)]));
|
|
3331
|
+
flush();
|
|
3332
|
+
if (d) marks.push({ kind: "path", key: "sparkline", role: "line", d, fill: "none", stroke: color, strokeWidth: 1.25 });
|
|
3333
|
+
const r = Math.max(1, Math.min(3, h * 0.08));
|
|
3334
|
+
if (options.markers || n === 1) {
|
|
3335
|
+
const step = Math.max(1, Math.floor(n / 500));
|
|
3336
|
+
vals.forEach((v, j) => {
|
|
3337
|
+
if (v !== null && j % step === 0) marks.push({ kind: "circle", key: `point-${j}`, role: "point", cx: fx(j), cy: fy2(v), r, fill: color });
|
|
3338
|
+
});
|
|
3339
|
+
}
|
|
3340
|
+
if (options.highLow && n > 1) {
|
|
3341
|
+
if (hiAt >= 0) marks.push({ kind: "circle", key: "high", role: "point", cx: fx(hiAt), cy: fy2(vals[hiAt]), r: r + 0.5, fill: color });
|
|
3342
|
+
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 });
|
|
3343
|
+
}
|
|
3344
|
+
return scene;
|
|
3345
|
+
}
|
|
3346
|
+
const slot = iw / n;
|
|
3347
|
+
const bw = Math.max(0.25, slot * 0.8);
|
|
3348
|
+
const win = kind === "win-loss";
|
|
3349
|
+
const lo0 = win ? -1 : Math.min(lo, 0);
|
|
3350
|
+
const hi0 = win ? 1 : Math.max(hi, 0);
|
|
3351
|
+
const span = hi0 - lo0;
|
|
3352
|
+
const fy = (v) => span > 0 ? y0 + ih - (v - lo0) / span * ih : y0 + ih;
|
|
3353
|
+
const base = fy(0);
|
|
3354
|
+
vals.forEach((raw, j) => {
|
|
3355
|
+
if (raw === null) return;
|
|
3356
|
+
const v = win ? Math.sign(raw) : raw;
|
|
3357
|
+
if (win && v === 0) return;
|
|
3358
|
+
const top = fy(v);
|
|
3359
|
+
const highlight = options.highLow && !win && (j === hiAt || j === loAt);
|
|
3360
|
+
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 });
|
|
3361
|
+
});
|
|
3362
|
+
return scene;
|
|
3363
|
+
}
|
|
3364
|
+
function sparklineToSvg(values, options) {
|
|
3365
|
+
return chartSceneToSvg(layoutSparkline(values, options), { ariaLabel: options.ariaLabel });
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
|
|
3369
|
+
|
|
3370
|
+
|
|
3371
|
+
|
|
3372
|
+
|
|
3373
|
+
|
|
3374
|
+
|
|
3375
|
+
|
|
3376
|
+
|
|
3377
|
+
|
|
3378
|
+
|
|
3379
|
+
|
|
3380
|
+
|
|
3381
|
+
|
|
3382
|
+
|
|
3383
|
+
|
|
3384
|
+
|
|
3385
|
+
|
|
3386
|
+
|
|
3387
|
+
|
|
3388
|
+
|
|
3389
|
+
|
|
3390
|
+
|
|
3391
|
+
|
|
3392
|
+
|
|
3393
|
+
|
|
3394
|
+
|
|
3395
|
+
|
|
3396
|
+
|
|
3397
|
+
|
|
3398
|
+
|
|
3399
|
+
|
|
3400
|
+
|
|
3401
|
+
|
|
3402
|
+
|
|
3403
|
+
|
|
3404
|
+
|
|
3405
|
+
|
|
3406
|
+
|
|
3407
|
+
|
|
3408
|
+
|
|
3409
|
+
|
|
3410
|
+
|
|
3411
|
+
|
|
3412
|
+
|
|
3413
|
+
|
|
3414
|
+
|
|
3415
|
+
|
|
3416
|
+
|
|
3417
|
+
|
|
3418
|
+
|
|
3419
|
+
|
|
3420
|
+
|
|
3421
|
+
|
|
3422
|
+
|
|
3423
|
+
exports.CHART_MERMAID_META_PREFIX = CHART_MERMAID_META_PREFIX; exports.chartMermaidFamily = chartMermaidFamily; exports.chartToMermaid = chartToMermaid; exports.chartFromMermaid = chartFromMermaid; exports.findMermaidCharts = findMermaidCharts; exports.parseChartDate = parseChartDate; exports.CHART_DATE_STEPS = CHART_DATE_STEPS; exports.formatChartDate = formatChartDate; exports.chartDateLabels = chartDateLabels; exports.compileChartFormatCode = compileChartFormatCode; exports.formatChartValueWithCode = formatChartValueWithCode; exports.chartSerialToDate = chartSerialToDate; exports.chartDateToSerial = chartDateToSerial; exports.niceStep = niceStep; exports.niceScale = niceScale; exports.stepDecimals = stepDecimals; exports.formatChartNumber = formatChartNumber; exports.linearMap = linearMap; exports.estimateChartTextWidth = estimateChartTextWidth; exports.sameChartSelection = sameChartSelection; exports.contrastingTextColor = contrastingTextColor; exports.chartHistogram = chartHistogram; exports.chartQuantile = chartQuantile; exports.chartBoxStats = chartBoxStats; exports.chartSquarify = chartSquarify; exports.decimateChartPoints = decimateChartPoints; exports.summarizeChartSeries = summarizeChartSeries; exports.fitChartTrendline = fitChartTrendline; exports.trendNumberText = trendNumberText; exports.chartMovingAverage = chartMovingAverage; exports.chartTrendlineName = chartTrendlineName; exports.chartSupportsTrendlines = chartSupportsTrendlines; exports.MAX_CHART_DATA_LABELS = MAX_CHART_DATA_LABELS; exports.MAX_CHART_LINE_POINTS = MAX_CHART_LINE_POINTS; exports.MAX_CHART_MARKERS = MAX_CHART_MARKERS; exports.layoutChart = layoutChart; exports.chartValueAtPoint = chartValueAtPoint; exports.chartXAtPoint = chartXAtPoint; exports.snapChartValue = snapChartValue; exports.summarizeChart = summarizeChart; exports.describeChart = describeChart; exports.chartTextBaseline = chartTextBaseline; exports.chartTextTransform = chartTextTransform; exports.chartSceneToSvg = chartSceneToSvg; exports.chartToSvg = chartToSvg; exports.chartSceneToDomSpec = chartSceneToDomSpec; exports.CHART_LAYOUT_PRESETS = CHART_LAYOUT_PRESETS; exports.monochromeChartPalette = monochromeChartPalette; exports.CHART_COLOR_SCHEMES = CHART_COLOR_SCHEMES; exports.chartPresetPatch = chartPresetPatch; exports.ChartController = ChartController; exports.createChartController = createChartController; exports.layoutSparkline = layoutSparkline; exports.sparklineToSvg = sparklineToSvg;
|
|
3424
|
+
//# sourceMappingURL=chunk-GTZXF4BI.cjs.map
|