@aparte/plugin-chartjs 0.0.0-stage → 0.29.1
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/LICENSE +21 -0
- package/README.md +223 -2
- package/dist/core.d.ts +73 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +869 -0
- package/dist/core.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/palette.d.ts +41 -0
- package/dist/palette.d.ts.map +1 -0
- package/dist/source.d.ts +64 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/theme.d.ts +50 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/words.d.ts +81 -0
- package/dist/words.d.ts.map +1 -0
- package/package.json +65 -3
package/dist/core.js
ADDED
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
import { escapeHtml, escapeAttr, aparteGlobalConfig } from "@aparte/core";
|
|
2
|
+
const styles = "/* A Chart.js chart in a reply: the bubble's width up to a reading measure, at a set\n height (`setupChartjs({ height })`, else 18rem) — or at the config's own aspect ratio,\n when it asks for one. The canvas is sized by the plugin from its frame, so the frame\n alone carries the size and the canvas is its only child (what Chart.js measures). */\n.aparte-chartjs {\n display: block;\n position: relative;\n inline-size: 100%;\n max-inline-size: var(--aparte-chartjs-max-width, 40rem);\n margin-block: var(--aparte-space-2);\n}\n\n.aparte-chartjs__frame {\n position: relative;\n inline-size: 100%;\n block-size: var(--aparte-chartjs-height, 18rem);\n}\n\n.aparte-chartjs[data-aparte-chartjs-ratio] .aparte-chartjs__frame {\n block-size: auto;\n}\n\n.aparte-chartjs__canvas {\n display: block;\n}\n\n/* While the chart is on its way: a quiet box the size the chart will be. */\n.aparte-chartjs__frame > .aparte-skeleton {\n display: block;\n block-size: 100%;\n}\n\n/* A tool whose chart cannot be shown: one quiet line, where the chart would have been. */\n.aparte-chartjs-note {\n color: var(--aparte-text-muted);\n font-size: var(--aparte-font-size-sm);\n}\n";
|
|
3
|
+
const MAX_POINTS = 5e3;
|
|
4
|
+
const MAX_TICKS = 100;
|
|
5
|
+
const MAX_COLUMNS = 50;
|
|
6
|
+
const MAX_CELLS = 1e4;
|
|
7
|
+
const TOOLTIP_POSITIONS = /* @__PURE__ */ new Set(["average", "nearest"]);
|
|
8
|
+
const MAX_DEPTH = 32;
|
|
9
|
+
const DROPPED_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor", "toString", "valueOf", "toJSON"]);
|
|
10
|
+
function plain(value, depth = 0, animating = false) {
|
|
11
|
+
if (depth > MAX_DEPTH) throw new RangeError("too deep");
|
|
12
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
13
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
14
|
+
if (Array.isArray(value)) return value.map((item) => plain(item, depth + 1, animating));
|
|
15
|
+
if (typeof value === "object") {
|
|
16
|
+
const out = {};
|
|
17
|
+
for (const [key, item] of Object.entries(value)) {
|
|
18
|
+
if (DROPPED_KEYS.has(key) || animating && key === "loop") continue;
|
|
19
|
+
const copy = plain(item, depth + 1, animating || key === "animation" || key === "animations");
|
|
20
|
+
if (copy !== void 0) out[key] = copy;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
return void 0;
|
|
25
|
+
}
|
|
26
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
|
+
const child$1 = (parent, key) => {
|
|
28
|
+
const value = isObject(parent) ? parent[key] : void 0;
|
|
29
|
+
return isObject(value) ? value : void 0;
|
|
30
|
+
};
|
|
31
|
+
function dropCalls(target, keys) {
|
|
32
|
+
if (target) for (const key of keys) delete target[key];
|
|
33
|
+
}
|
|
34
|
+
function isLocale(tag) {
|
|
35
|
+
if (typeof tag !== "string") return false;
|
|
36
|
+
try {
|
|
37
|
+
return Intl.NumberFormat.supportedLocalesOf(tag).length > 0;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function readChartConfig(value, maxPoints = MAX_POINTS) {
|
|
43
|
+
let copy;
|
|
44
|
+
try {
|
|
45
|
+
copy = plain(value);
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (!isObject(copy) || typeof copy["type"] !== "string" || !/^[A-Za-z][\w-]*$/.test(copy["type"])) return null;
|
|
50
|
+
const data = copy["data"];
|
|
51
|
+
if (!isObject(data) || !Array.isArray(data["datasets"])) return null;
|
|
52
|
+
const datasets = data["datasets"];
|
|
53
|
+
if (!datasets.every((d) => isObject(d) && Array.isArray(d["data"]))) return null;
|
|
54
|
+
if (data["labels"] !== void 0 && !Array.isArray(data["labels"])) return null;
|
|
55
|
+
const points = datasets.reduce((sum, d) => sum + d["data"].length, 0);
|
|
56
|
+
const labels = Array.isArray(data["labels"]) ? data["labels"].length : 0;
|
|
57
|
+
if (points > maxPoints || labels > maxPoints || datasets.length > maxPoints) return null;
|
|
58
|
+
delete copy["plugins"];
|
|
59
|
+
if (copy["options"] !== void 0 && !isObject(copy["options"])) delete copy["options"];
|
|
60
|
+
const options = copy["options"];
|
|
61
|
+
if (options) {
|
|
62
|
+
delete options["devicePixelRatio"];
|
|
63
|
+
if (options["locale"] !== void 0 && !isLocale(options["locale"])) delete options["locale"];
|
|
64
|
+
const ratio = options["aspectRatio"];
|
|
65
|
+
if (ratio !== void 0) {
|
|
66
|
+
if (typeof ratio !== "number" || ratio <= 0) delete options["aspectRatio"];
|
|
67
|
+
else options["aspectRatio"] = Math.min(4, Math.max(0.25, ratio));
|
|
68
|
+
}
|
|
69
|
+
const scales = child$1(options, "scales");
|
|
70
|
+
for (const id of scales ? Object.keys(scales) : []) {
|
|
71
|
+
const ticks = child$1(scales[id], "ticks");
|
|
72
|
+
if (!ticks) continue;
|
|
73
|
+
for (const key of ["count", "maxTicksLimit"]) {
|
|
74
|
+
const n = ticks[key];
|
|
75
|
+
if (n === void 0) continue;
|
|
76
|
+
if (typeof n !== "number" || n < 1) delete ticks[key];
|
|
77
|
+
else ticks[key] = Math.min(MAX_TICKS, Math.floor(n));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const plugins = child$1(options, "plugins");
|
|
81
|
+
const tooltip = child$1(plugins, "tooltip");
|
|
82
|
+
dropCalls(tooltip, ["external", "filter", "itemSort", "callbacks"]);
|
|
83
|
+
if (tooltip && tooltip["position"] !== void 0 && !TOOLTIP_POSITIONS.has(tooltip["position"])) delete tooltip["position"];
|
|
84
|
+
dropCalls(child$1(child$1(plugins, "legend"), "labels"), ["generateLabels", "filter", "sort"]);
|
|
85
|
+
}
|
|
86
|
+
return copy;
|
|
87
|
+
}
|
|
88
|
+
function hasPoints(config) {
|
|
89
|
+
return config.data.datasets.some((d) => d["data"].length > 0);
|
|
90
|
+
}
|
|
91
|
+
function closeJson(text) {
|
|
92
|
+
const stack = [];
|
|
93
|
+
let inString = false;
|
|
94
|
+
let escaped = false;
|
|
95
|
+
let cut = -1;
|
|
96
|
+
let closers = "";
|
|
97
|
+
for (let i = 0; i < text.length; i++) {
|
|
98
|
+
const c = text[i];
|
|
99
|
+
if (inString) {
|
|
100
|
+
if (escaped) escaped = false;
|
|
101
|
+
else if (c === "\\") escaped = true;
|
|
102
|
+
else if (c === '"') inString = false;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (c === '"') inString = true;
|
|
106
|
+
else if (c === "{" || c === "[") {
|
|
107
|
+
stack.push(c === "{" ? "}" : "]");
|
|
108
|
+
cut = i + 1;
|
|
109
|
+
closers = stack.slice().reverse().join("");
|
|
110
|
+
} else if (c === "}" || c === "]") {
|
|
111
|
+
if (stack.pop() !== c) return null;
|
|
112
|
+
cut = i + 1;
|
|
113
|
+
closers = stack.slice().reverse().join("");
|
|
114
|
+
if (stack.length === 0) return text.slice(0, cut);
|
|
115
|
+
} else if (c === ",") {
|
|
116
|
+
cut = i;
|
|
117
|
+
closers = stack.slice().reverse().join("");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return cut < 0 ? null : text.slice(0, cut) + closers;
|
|
121
|
+
}
|
|
122
|
+
function parseFence(source, open, maxPoints = MAX_POINTS) {
|
|
123
|
+
const text = open ? closeJson(source) : source;
|
|
124
|
+
if (text === null) return null;
|
|
125
|
+
try {
|
|
126
|
+
return readChartConfig(JSON.parse(text), maxPoints);
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function titleOf(config) {
|
|
132
|
+
const title = config.options?.["plugins"]?.["title"]?.["text"];
|
|
133
|
+
const text = Array.isArray(title) ? title.filter((t) => typeof t === "string").join(" ") : typeof title === "string" ? title : "";
|
|
134
|
+
return text.trim() || void 0;
|
|
135
|
+
}
|
|
136
|
+
function cell(value, format) {
|
|
137
|
+
if (typeof value === "number") return format(value);
|
|
138
|
+
if (typeof value === "string") return value;
|
|
139
|
+
if (isObject(value)) {
|
|
140
|
+
const y = value["y"];
|
|
141
|
+
const shown = typeof y === "number" ? format(y) : typeof y === "string" ? y : "";
|
|
142
|
+
return typeof value["r"] === "number" ? `${shown} (${format(value["r"])})` : shown;
|
|
143
|
+
}
|
|
144
|
+
return "";
|
|
145
|
+
}
|
|
146
|
+
function rowName(config, datasets, index, format) {
|
|
147
|
+
const label = config.data.labels?.[index];
|
|
148
|
+
if (typeof label === "string" || typeof label === "number") return String(label);
|
|
149
|
+
if (Array.isArray(label)) return label.join(" ");
|
|
150
|
+
for (const dataset of datasets) {
|
|
151
|
+
const point = dataset["data"][index];
|
|
152
|
+
if (isObject(point) && (typeof point["x"] === "number" || typeof point["x"] === "string")) {
|
|
153
|
+
return typeof point["x"] === "number" ? format(point["x"]) : point["x"];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return String(index + 1);
|
|
157
|
+
}
|
|
158
|
+
function dataTable(config, locale) {
|
|
159
|
+
let formatter;
|
|
160
|
+
try {
|
|
161
|
+
formatter = new Intl.NumberFormat(locale);
|
|
162
|
+
} catch {
|
|
163
|
+
formatter = new Intl.NumberFormat();
|
|
164
|
+
}
|
|
165
|
+
const format = (n) => formatter.format(n);
|
|
166
|
+
const all = config.data.datasets.map((d, i) => ({ d, i })).filter(({ d }) => d["data"].length > 0);
|
|
167
|
+
const datasets = all.slice(0, MAX_COLUMNS);
|
|
168
|
+
const withData = all.map(({ d }) => d);
|
|
169
|
+
const height = Math.max(config.data.labels?.length ?? 0, ...datasets.map(({ d }) => d["data"].length));
|
|
170
|
+
const rows = Math.min(height, Math.floor(MAX_CELLS / (datasets.length + 1)));
|
|
171
|
+
const head = datasets.map(({ d, i }) => {
|
|
172
|
+
const label = typeof d["label"] === "string" && d["label"].trim() ? d["label"] : "";
|
|
173
|
+
return label ? `<th scope="col">${escapeHtml(label)}</th>` : `<th scope="col" data-aparte-chartjs-series="${escapeAttr(String(i + 1))}"></th>`;
|
|
174
|
+
}).join("");
|
|
175
|
+
const body = [];
|
|
176
|
+
for (let r = 0; r < rows; r++) {
|
|
177
|
+
const cells = datasets.map(({ d }) => `<td>${escapeHtml(cell(d["data"][r], format))}</td>`).join("");
|
|
178
|
+
body.push(`<tr><th scope="row">${escapeHtml(rowName(config, withData, r, format))}</th>${cells}</tr>`);
|
|
179
|
+
}
|
|
180
|
+
const cut = datasets.length < all.length || rows < height ? `<tfoot><tr><td colspan="${datasets.length + 1}" data-aparte-chartjs-cut></td></tr></tfoot>` : "";
|
|
181
|
+
return `<table><caption data-aparte-chartjs-name></caption><thead><tr><td></td>${head}</tr></thead>${cut}<tbody>${body.join("")}</tbody></table>`;
|
|
182
|
+
}
|
|
183
|
+
const SERIES_HUES = ["#2a78d6", "#d95926", "#199e70", "#c98500", "#d55181", "#008300", "#9085e9", "#e34948"];
|
|
184
|
+
const NORMAL_MIN = 15;
|
|
185
|
+
const CVD_MIN = 8;
|
|
186
|
+
const MAX_SERIES = 8;
|
|
187
|
+
function parseColor(color) {
|
|
188
|
+
const value = color.trim().toLowerCase();
|
|
189
|
+
const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/.exec(value)?.[1];
|
|
190
|
+
if (hex) {
|
|
191
|
+
const full = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
|
|
192
|
+
return [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16));
|
|
193
|
+
}
|
|
194
|
+
const rgb = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/.exec(value);
|
|
195
|
+
if (rgb) return [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])];
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
function withAlpha(color, alpha) {
|
|
199
|
+
const rgb = parseColor(color);
|
|
200
|
+
return rgb ? `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha})` : color;
|
|
201
|
+
}
|
|
202
|
+
const toLinear = (c) => {
|
|
203
|
+
const s = c / 255;
|
|
204
|
+
return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
205
|
+
};
|
|
206
|
+
const CVD = {
|
|
207
|
+
protan: [[0.152286, 1.052583, -0.204868], [0.114503, 0.786281, 0.099216], [-3882e-6, -0.048116, 1.051998]],
|
|
208
|
+
deutan: [[0.367322, 0.860646, -0.227968], [0.280085, 0.672501, 0.047413], [-0.01182, 0.04294, 0.968881]]
|
|
209
|
+
};
|
|
210
|
+
function oklab([r, g, b]) {
|
|
211
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
212
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
213
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
214
|
+
return [
|
|
215
|
+
0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
|
|
216
|
+
1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
|
|
217
|
+
0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
|
|
218
|
+
];
|
|
219
|
+
}
|
|
220
|
+
function distance(a, b, kind) {
|
|
221
|
+
const see = (c) => {
|
|
222
|
+
const lin = c.map(toLinear);
|
|
223
|
+
if (!kind) return oklab(lin);
|
|
224
|
+
return oklab(CVD[kind].map((row) => Math.min(1, Math.max(0, row[0] * lin[0] + row[1] * lin[1] + row[2] * lin[2]))));
|
|
225
|
+
};
|
|
226
|
+
const [x, y] = [see(a), see(b)];
|
|
227
|
+
return 100 * Math.hypot(x[0] - y[0], x[1] - y[1], x[2] - y[2]);
|
|
228
|
+
}
|
|
229
|
+
const cache = /* @__PURE__ */ new Map();
|
|
230
|
+
function seriesPalette(primary) {
|
|
231
|
+
const hit = cache.get(primary);
|
|
232
|
+
if (hit) return hit;
|
|
233
|
+
const brand = parseColor(primary);
|
|
234
|
+
let palette;
|
|
235
|
+
if (!brand) {
|
|
236
|
+
palette = [primary, ...SERIES_HUES].slice(0, MAX_SERIES);
|
|
237
|
+
} else {
|
|
238
|
+
palette = [primary];
|
|
239
|
+
let previous = brand;
|
|
240
|
+
const left = SERIES_HUES.map((hex) => ({ hex, rgb: parseColor(hex) })).filter(({ rgb }) => distance(rgb, brand) >= NORMAL_MIN);
|
|
241
|
+
while (palette.length < MAX_SERIES) {
|
|
242
|
+
const i = left.findIndex(({ rgb }) => distance(rgb, previous) >= NORMAL_MIN && Math.min(distance(rgb, previous, "protan"), distance(rgb, previous, "deutan")) >= CVD_MIN);
|
|
243
|
+
if (i < 0) break;
|
|
244
|
+
const [next] = left.splice(i, 1);
|
|
245
|
+
palette.push(next.hex);
|
|
246
|
+
previous = next.rgb;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (cache.size > 32) cache.clear();
|
|
250
|
+
cache.set(primary, palette);
|
|
251
|
+
return palette;
|
|
252
|
+
}
|
|
253
|
+
function child(parent, key) {
|
|
254
|
+
const value = parent[key];
|
|
255
|
+
if (value === void 0) return parent[key] = {};
|
|
256
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
257
|
+
}
|
|
258
|
+
function fill(target, values) {
|
|
259
|
+
if (!target) return;
|
|
260
|
+
for (const [key, value] of Object.entries(values)) if (target[key] === void 0) target[key] = value;
|
|
261
|
+
}
|
|
262
|
+
function defaultScales(type) {
|
|
263
|
+
if (type === "radar" || type === "polarArea") return ["r"];
|
|
264
|
+
if (type === "pie" || type === "doughnut") return [];
|
|
265
|
+
return ["x", "y"];
|
|
266
|
+
}
|
|
267
|
+
const ARCS = /* @__PURE__ */ new Set(["pie", "doughnut", "polarArea"]);
|
|
268
|
+
function colourDataset(dataset, type, colour, palette, tokens) {
|
|
269
|
+
if (dataset["backgroundColor"] !== void 0 || dataset["borderColor"] !== void 0) return;
|
|
270
|
+
if (ARCS.has(type)) {
|
|
271
|
+
const count = dataset["data"].length;
|
|
272
|
+
const alpha = type === "polarArea" ? 0.7 : 1;
|
|
273
|
+
dataset["backgroundColor"] = Array.from({ length: count }, (_, i) => withAlpha(palette(i), alpha));
|
|
274
|
+
dataset["borderColor"] = tokens.background;
|
|
275
|
+
fill(dataset, { borderWidth: 2 });
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (type === "line" || type === "radar") {
|
|
279
|
+
dataset["borderColor"] = colour;
|
|
280
|
+
dataset["backgroundColor"] = withAlpha(colour, 0.2);
|
|
281
|
+
fill(dataset, { pointBackgroundColor: colour, borderWidth: 2 });
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
dataset["borderColor"] = colour;
|
|
285
|
+
dataset["backgroundColor"] = withAlpha(colour, type === "bar" ? 0.8 : 0.6);
|
|
286
|
+
if (type === "bar") fill(dataset, { borderWidth: 1, borderRadius: 4 });
|
|
287
|
+
}
|
|
288
|
+
function themeChartjsConfig(config, tokens) {
|
|
289
|
+
const out = JSON.parse(JSON.stringify(config));
|
|
290
|
+
const options = child(out, "options");
|
|
291
|
+
fill(options, { color: tokens.text, borderColor: tokens.border });
|
|
292
|
+
fill(child(options, "font"), { family: tokens.fontFamily });
|
|
293
|
+
const plugins = child(options, "plugins");
|
|
294
|
+
if (plugins) {
|
|
295
|
+
const legend = child(plugins, "legend");
|
|
296
|
+
if (legend) fill(child(legend, "labels"), { color: tokens.text });
|
|
297
|
+
fill(child(plugins, "title"), { color: tokens.text });
|
|
298
|
+
fill(child(plugins, "subtitle"), { color: tokens.muted });
|
|
299
|
+
fill(child(plugins, "tooltip"), {
|
|
300
|
+
backgroundColor: tokens.surface,
|
|
301
|
+
titleColor: tokens.text,
|
|
302
|
+
bodyColor: tokens.text,
|
|
303
|
+
footerColor: tokens.muted,
|
|
304
|
+
borderColor: tokens.border,
|
|
305
|
+
borderWidth: 1
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
const scales = child(options, "scales");
|
|
309
|
+
if (scales) {
|
|
310
|
+
for (const id of defaultScales(out.type)) if (scales[id] === void 0) scales[id] = {};
|
|
311
|
+
for (const id of Object.keys(scales)) {
|
|
312
|
+
const scale = child(scales, id);
|
|
313
|
+
if (!scale) continue;
|
|
314
|
+
fill(child(scale, "grid"), { color: tokens.border });
|
|
315
|
+
fill(child(scale, "border"), { color: tokens.border });
|
|
316
|
+
fill(child(scale, "ticks"), { color: tokens.muted, backdropColor: tokens.background });
|
|
317
|
+
fill(child(scale, "title"), { color: tokens.muted });
|
|
318
|
+
if (id === "r" || scale["type"] === "radialLinear") {
|
|
319
|
+
fill(child(scale, "angleLines"), { color: tokens.border });
|
|
320
|
+
fill(child(scale, "pointLabels"), { color: tokens.text });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (Object.keys(scales).length === 0) delete options["scales"];
|
|
324
|
+
}
|
|
325
|
+
const palette = seriesPalette(tokens.primary);
|
|
326
|
+
const colourAt = (i) => palette[i] ?? tokens.muted;
|
|
327
|
+
out.data.datasets.forEach((dataset, i) => {
|
|
328
|
+
const type = typeof dataset["type"] === "string" ? dataset["type"] : out.type;
|
|
329
|
+
colourDataset(dataset, type, colourAt(i), colourAt, tokens);
|
|
330
|
+
});
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
const TOKENS = {
|
|
334
|
+
text: ["--aparte-text", "#1a1a1a"],
|
|
335
|
+
muted: ["--aparte-text-muted", "#666666"],
|
|
336
|
+
border: ["--aparte-border", "#d0d0d0"],
|
|
337
|
+
surface: ["--aparte-surface-1", "#ffffff"],
|
|
338
|
+
background: ["--aparte-bg", "#ffffff"],
|
|
339
|
+
primary: ["--aparte-primary", "#3b5bdb"]
|
|
340
|
+
};
|
|
341
|
+
let scratch;
|
|
342
|
+
function readTokens(host) {
|
|
343
|
+
const fontFamily = (typeof getComputedStyle === "function" ? getComputedStyle(host).fontFamily : "") || "system-ui, sans-serif";
|
|
344
|
+
const fallback = Object.fromEntries(Object.entries(TOKENS).map(([k, [, v]]) => [k, v]));
|
|
345
|
+
if (typeof getComputedStyle !== "function") return { ...fallback, fontFamily };
|
|
346
|
+
const probe = document.createElement("span");
|
|
347
|
+
probe.style.display = "none";
|
|
348
|
+
host.appendChild(probe);
|
|
349
|
+
scratch ??= document.createElement("canvas").getContext?.("2d") ?? null;
|
|
350
|
+
const read = ([token, fallbackColour]) => {
|
|
351
|
+
probe.style.color = `var(${token}, ${fallbackColour})`;
|
|
352
|
+
const computed = getComputedStyle(probe).color || fallbackColour;
|
|
353
|
+
if (!scratch) return computed;
|
|
354
|
+
scratch.fillStyle = "#000";
|
|
355
|
+
scratch.fillStyle = computed;
|
|
356
|
+
return String(scratch.fillStyle);
|
|
357
|
+
};
|
|
358
|
+
try {
|
|
359
|
+
return {
|
|
360
|
+
text: read(TOKENS.text),
|
|
361
|
+
muted: read(TOKENS.muted),
|
|
362
|
+
border: read(TOKENS.border),
|
|
363
|
+
surface: read(TOKENS.surface),
|
|
364
|
+
background: read(TOKENS.background),
|
|
365
|
+
primary: read(TOKENS.primary),
|
|
366
|
+
fontFamily
|
|
367
|
+
};
|
|
368
|
+
} finally {
|
|
369
|
+
probe.remove();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const WORDS = {
|
|
373
|
+
/** A chart's accessible name when its config gives it no title. */
|
|
374
|
+
chartjsChart: {
|
|
375
|
+
en: "Chart",
|
|
376
|
+
fr: "Graphique",
|
|
377
|
+
de: "Diagramm",
|
|
378
|
+
es: "Gráfico",
|
|
379
|
+
it: "Grafico",
|
|
380
|
+
ja: "グラフ",
|
|
381
|
+
ko: "차트",
|
|
382
|
+
pt: "Gráfico",
|
|
383
|
+
ru: "Диаграмма",
|
|
384
|
+
zh: "图表"
|
|
385
|
+
},
|
|
386
|
+
/** The note over the source of a chart that could not be drawn. */
|
|
387
|
+
chartjsError: {
|
|
388
|
+
en: "This chart could not be drawn.",
|
|
389
|
+
fr: "Ce graphique n’a pas pu être tracé.",
|
|
390
|
+
de: "Dieses Diagramm konnte nicht gezeichnet werden.",
|
|
391
|
+
es: "No se pudo dibujar este gráfico.",
|
|
392
|
+
it: "Impossibile disegnare questo grafico.",
|
|
393
|
+
ja: "このグラフは描画できませんでした。",
|
|
394
|
+
ko: "이 차트를 그릴 수 없습니다.",
|
|
395
|
+
pt: "Não foi possível desenhar este gráfico.",
|
|
396
|
+
ru: "Не удалось построить эту диаграмму.",
|
|
397
|
+
zh: "无法绘制此图表。"
|
|
398
|
+
},
|
|
399
|
+
/** A tool that should have returned a chart and did not run, or failed. */
|
|
400
|
+
chartjsUnavailable: {
|
|
401
|
+
en: "Chart not available",
|
|
402
|
+
fr: "Graphique indisponible",
|
|
403
|
+
de: "Diagramm nicht verfügbar",
|
|
404
|
+
es: "Gráfico no disponible",
|
|
405
|
+
it: "Grafico non disponibile",
|
|
406
|
+
ja: "グラフは利用できません",
|
|
407
|
+
ko: "차트를 사용할 수 없음",
|
|
408
|
+
pt: "Gráfico indisponível",
|
|
409
|
+
ru: "Диаграмма недоступна",
|
|
410
|
+
zh: "图表不可用"
|
|
411
|
+
},
|
|
412
|
+
/** A column of the data table for a dataset with no label. */
|
|
413
|
+
chartjsSeries: {
|
|
414
|
+
en: "Series {n}",
|
|
415
|
+
fr: "Série {n}",
|
|
416
|
+
de: "Reihe {n}",
|
|
417
|
+
es: "Serie {n}",
|
|
418
|
+
it: "Serie {n}",
|
|
419
|
+
ja: "系列 {n}",
|
|
420
|
+
ko: "계열 {n}",
|
|
421
|
+
pt: "Série {n}",
|
|
422
|
+
ru: "Ряд {n}",
|
|
423
|
+
zh: "系列 {n}"
|
|
424
|
+
},
|
|
425
|
+
/** The data table's last line, when it lists only part of the data. */
|
|
426
|
+
chartjsTableCut: {
|
|
427
|
+
en: "Only part of the data is listed.",
|
|
428
|
+
fr: "Seule une partie des données est listée.",
|
|
429
|
+
de: "Nur ein Teil der Daten ist aufgeführt.",
|
|
430
|
+
es: "Solo se muestra una parte de los datos.",
|
|
431
|
+
it: "È elencata solo una parte dei dati.",
|
|
432
|
+
ja: "データの一部のみを表示しています。",
|
|
433
|
+
ko: "데이터의 일부만 표시됩니다.",
|
|
434
|
+
pt: "Apenas parte dos dados está listada.",
|
|
435
|
+
ru: "Показана только часть данных.",
|
|
436
|
+
zh: "仅列出部分数据。"
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
function languageOf(config) {
|
|
440
|
+
const tag = config.getLocale().tag;
|
|
441
|
+
return typeof tag === "string" && tag ? tag.split("-")[0].toLowerCase() : "en";
|
|
442
|
+
}
|
|
443
|
+
function word(config, key, vars = {}) {
|
|
444
|
+
const fromApp = config.getLocale()[key];
|
|
445
|
+
const table = WORDS[key];
|
|
446
|
+
const text = typeof fromApp === "string" && fromApp || table[languageOf(config)] || table.en;
|
|
447
|
+
return text.replace(/\{(\w+)\}/g, (all, name) => vars[name] ?? all);
|
|
448
|
+
}
|
|
449
|
+
const CHARTJS_LANGUAGES = ["chartjs", "chart.js"];
|
|
450
|
+
const PROGRESSIVE_MS = 300;
|
|
451
|
+
const CONFIG_ATTR = "data-aparte-chartjs-config";
|
|
452
|
+
const PARKED = 32;
|
|
453
|
+
const STYLE_ATTR = "data-aparte-chartjs-styles";
|
|
454
|
+
const warned = /* @__PURE__ */ new WeakSet();
|
|
455
|
+
function warnOnce(key, message, error) {
|
|
456
|
+
if (warned.has(key)) return;
|
|
457
|
+
warned.add(key);
|
|
458
|
+
console.warn(`[aparte-chartjs] ${message}`, error);
|
|
459
|
+
}
|
|
460
|
+
const isChartClass = (value) => typeof value === "function" && typeof value.prototype?.update === "function";
|
|
461
|
+
const reducedMotion = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
462
|
+
function ensureStyles(node) {
|
|
463
|
+
const root = node.getRootNode();
|
|
464
|
+
const target = typeof ShadowRoot !== "undefined" && root instanceof ShadowRoot ? root : root === document ? document.head : null;
|
|
465
|
+
if (!target || target.querySelector(`style[${STYLE_ATTR}]`)) return;
|
|
466
|
+
const sheet = document.createElement("style");
|
|
467
|
+
sheet.setAttribute(STYLE_ATTR, "");
|
|
468
|
+
sheet.textContent = styles;
|
|
469
|
+
target.appendChild(sheet);
|
|
470
|
+
}
|
|
471
|
+
function setupChartjsWith(chartjs, options = {}, config = aparteGlobalConfig) {
|
|
472
|
+
const charts = createCharts(chartjs, config);
|
|
473
|
+
const unregister = config.registerCodeLanguageRenderer(createFenceRenderer(charts, options));
|
|
474
|
+
const matchers = options.tool === void 0 ? [] : Array.isArray(options.tool) ? options.tool : [options.tool];
|
|
475
|
+
const toolRenderer = createToolRenderer(charts, options, config);
|
|
476
|
+
for (const match of matchers) config.registerToolRenderer(match, toolRenderer);
|
|
477
|
+
return () => {
|
|
478
|
+
unregister();
|
|
479
|
+
for (const match of matchers) config.unregisterToolRenderer(match);
|
|
480
|
+
charts.stop();
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function createCharts(chartjs, config) {
|
|
484
|
+
const load = isChartClass(chartjs) ? () => Promise.resolve(chartjs) : chartjs;
|
|
485
|
+
let loading;
|
|
486
|
+
let Chart = isChartClass(chartjs) ? chartjs : void 0;
|
|
487
|
+
const mounted = /* @__PURE__ */ new Map();
|
|
488
|
+
const parked = /* @__PURE__ */ new Map();
|
|
489
|
+
const shadows = /* @__PURE__ */ new WeakSet();
|
|
490
|
+
let sweep;
|
|
491
|
+
let stopped = false;
|
|
492
|
+
const chartOf = () => {
|
|
493
|
+
loading ??= load().then((m) => {
|
|
494
|
+
const found = isChartClass(m) ? m : m.default ?? m.Chart;
|
|
495
|
+
if (!isChartClass(found)) throw new TypeError("the loader returned no Chart class");
|
|
496
|
+
Chart = found;
|
|
497
|
+
return found;
|
|
498
|
+
}).catch((error) => {
|
|
499
|
+
loading = void 0;
|
|
500
|
+
throw error;
|
|
501
|
+
});
|
|
502
|
+
return loading;
|
|
503
|
+
};
|
|
504
|
+
const frameOf = (wrapper) => wrapper.querySelector(".aparte-chartjs__frame");
|
|
505
|
+
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver((entries) => {
|
|
506
|
+
for (const entry of entries) {
|
|
507
|
+
const wrapper = entry.target.parentElement;
|
|
508
|
+
if (!wrapper) continue;
|
|
509
|
+
if (!wrapper.isConnected) {
|
|
510
|
+
release.later();
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (entry.contentRect.width === 0 && entry.contentRect.height === 0) continue;
|
|
514
|
+
const chart = mounted.get(wrapper);
|
|
515
|
+
try {
|
|
516
|
+
if (!chart) mount(wrapper);
|
|
517
|
+
else if (chart.responsive) chart.chart.resize();
|
|
518
|
+
} catch (error) {
|
|
519
|
+
release(wrapper);
|
|
520
|
+
fail(wrapper, error);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
const unwatch = (wrapper) => {
|
|
525
|
+
const frame = parked.get(wrapper) ?? frameOf(wrapper);
|
|
526
|
+
parked.delete(wrapper);
|
|
527
|
+
if (frame) observer?.unobserve(frame);
|
|
528
|
+
};
|
|
529
|
+
const release = Object.assign((wrapper, park = false) => {
|
|
530
|
+
const chart = mounted.get(wrapper);
|
|
531
|
+
if (chart) {
|
|
532
|
+
mounted.delete(wrapper);
|
|
533
|
+
try {
|
|
534
|
+
chart.chart.destroy();
|
|
535
|
+
} catch {
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const frame = frameOf(wrapper);
|
|
539
|
+
if (!park || !frame || !observer) {
|
|
540
|
+
unwatch(wrapper);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
parked.delete(wrapper);
|
|
544
|
+
parked.set(wrapper, frame);
|
|
545
|
+
for (const [oldest] of parked) {
|
|
546
|
+
if (parked.size <= PARKED) break;
|
|
547
|
+
unwatch(oldest);
|
|
548
|
+
}
|
|
549
|
+
}, {
|
|
550
|
+
/** Release every chart off the page, after the task: a move puts its nodes back before then. */
|
|
551
|
+
later() {
|
|
552
|
+
if (sweep !== void 0) return;
|
|
553
|
+
sweep = setTimeout(() => {
|
|
554
|
+
sweep = void 0;
|
|
555
|
+
for (const wrapper of [...mounted.keys()]) if (!wrapper.isConnected) release(wrapper, true);
|
|
556
|
+
}, 0);
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
const drawn = (chart, wrapper, animate) => {
|
|
560
|
+
const themed = themeChartjsConfig(chart, readTokens(wrapper));
|
|
561
|
+
const o = themed.options ??= {};
|
|
562
|
+
const responsive = o["responsive"] !== false;
|
|
563
|
+
o["maintainAspectRatio"] = o["aspectRatio"] !== void 0 || o["maintainAspectRatio"] === true;
|
|
564
|
+
o["responsive"] = false;
|
|
565
|
+
if (!animate) o["animation"] = false;
|
|
566
|
+
return { config: themed, responsive };
|
|
567
|
+
};
|
|
568
|
+
const fail = (wrapper, error) => {
|
|
569
|
+
warnOnce(config, "a chart could not be drawn; its note is shown instead. Is its type registered on your Chart?", error);
|
|
570
|
+
wrapper.setAttribute("data-aparte-chartjs-failed", "");
|
|
571
|
+
const note = document.createElement("span");
|
|
572
|
+
note.className = "aparte-code-error";
|
|
573
|
+
note.setAttribute("role", "note");
|
|
574
|
+
note.setAttribute("data-aparte-chartjs-error", "");
|
|
575
|
+
note.textContent = word(config, "chartjsError");
|
|
576
|
+
unwatch(wrapper);
|
|
577
|
+
frameOf(wrapper)?.replaceWith(note);
|
|
578
|
+
};
|
|
579
|
+
function mount(wrapper) {
|
|
580
|
+
if (stopped || mounted.has(wrapper) || !wrapper.isConnected || wrapper.hasAttribute("data-aparte-chartjs-failed")) return;
|
|
581
|
+
if (!Chart) {
|
|
582
|
+
void chartOf().then(() => mount(wrapper), (error) => warnOnce(load, "Chart.js could not be loaded.", error));
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const canvas = wrapper.querySelector("canvas");
|
|
586
|
+
const frame = frameOf(wrapper);
|
|
587
|
+
if (!canvas || !frame) return;
|
|
588
|
+
let source;
|
|
589
|
+
try {
|
|
590
|
+
source = readChartConfig(JSON.parse(wrapper.getAttribute(CONFIG_ATTR) ?? ""), Infinity);
|
|
591
|
+
} catch {
|
|
592
|
+
source = null;
|
|
593
|
+
}
|
|
594
|
+
if (!source) return;
|
|
595
|
+
ensureStyles(wrapper);
|
|
596
|
+
watchRoot(wrapper);
|
|
597
|
+
const animate = !reducedMotion() && !wrapper.hasAttribute("data-aparte-chartjs-open") && (!wrapper.hasAttribute("data-aparte-chartjs-still") || source.options?.["animation"] !== void 0);
|
|
598
|
+
const { config: final, responsive } = drawn(source, wrapper, animate);
|
|
599
|
+
let chart;
|
|
600
|
+
try {
|
|
601
|
+
chart = new Chart(canvas, final);
|
|
602
|
+
if (responsive) chart.resize();
|
|
603
|
+
} catch (error) {
|
|
604
|
+
try {
|
|
605
|
+
chart?.destroy();
|
|
606
|
+
} catch {
|
|
607
|
+
}
|
|
608
|
+
fail(wrapper, error);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
mounted.set(wrapper, { chart, config: source, responsive });
|
|
612
|
+
parked.delete(wrapper);
|
|
613
|
+
release.later();
|
|
614
|
+
}
|
|
615
|
+
const retheme = () => {
|
|
616
|
+
for (const [wrapper, chart] of [...mounted]) {
|
|
617
|
+
if (!wrapper.isConnected) {
|
|
618
|
+
release.later();
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
const { config: final } = drawn(chart.config, wrapper, false);
|
|
623
|
+
chart.chart.options = final.options;
|
|
624
|
+
chart.chart.data = final.data;
|
|
625
|
+
chart.chart.update("none");
|
|
626
|
+
} catch (error) {
|
|
627
|
+
release(wrapper);
|
|
628
|
+
fail(wrapper, error);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
const watch = watchTheme(retheme);
|
|
633
|
+
const watchRoot = (node) => {
|
|
634
|
+
const root = node.getRootNode();
|
|
635
|
+
if (typeof ShadowRoot === "undefined" || !(root instanceof ShadowRoot) || shadows.has(root)) return;
|
|
636
|
+
shadows.add(root);
|
|
637
|
+
watch.add(root);
|
|
638
|
+
};
|
|
639
|
+
return {
|
|
640
|
+
chartOf,
|
|
641
|
+
warm() {
|
|
642
|
+
void chartOf().catch(() => void 0);
|
|
643
|
+
},
|
|
644
|
+
/** Mount a chart on every drawn canvas in `root` that has none. */
|
|
645
|
+
activate(root) {
|
|
646
|
+
const wrappers = root.matches(`.aparte-chartjs[${CONFIG_ATTR}]`) ? [root] : Array.from(root.querySelectorAll(`.aparte-chartjs[${CONFIG_ATTR}]`));
|
|
647
|
+
for (const wrapper of wrappers) {
|
|
648
|
+
const frame = frameOf(wrapper);
|
|
649
|
+
if (frame && !wrapper.hasAttribute("data-aparte-chartjs-failed")) observer?.observe(frame);
|
|
650
|
+
mount(wrapper);
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
/** Release the charts in `root` now: it is gone for good. */
|
|
654
|
+
releaseIn(root) {
|
|
655
|
+
for (const wrapper of [...mounted.keys(), ...parked.keys()]) if (root === wrapper || root.contains(wrapper)) release(wrapper);
|
|
656
|
+
},
|
|
657
|
+
stop() {
|
|
658
|
+
stopped = true;
|
|
659
|
+
watch.stop();
|
|
660
|
+
if (sweep !== void 0) clearTimeout(sweep);
|
|
661
|
+
sweep = void 0;
|
|
662
|
+
for (const wrapper of [...mounted.keys()]) release(wrapper);
|
|
663
|
+
parked.clear();
|
|
664
|
+
observer?.disconnect();
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
function chartMarkup(chart, locale, flags = {}) {
|
|
669
|
+
const o = chart.options ?? {};
|
|
670
|
+
const ratio = o["aspectRatio"] !== void 0 || o["maintainAspectRatio"] === true;
|
|
671
|
+
const title = titleOf(chart);
|
|
672
|
+
const named = title ? ` data-aparte-chartjs-title="${escapeAttr(title)}"` : "";
|
|
673
|
+
const marks = `${ratio ? " data-aparte-chartjs-ratio" : ""}${flags.open ? " data-aparte-chartjs-open" : ""}${flags.still ? " data-aparte-chartjs-still" : ""}`;
|
|
674
|
+
return `<div class="aparte-chartjs" role="figure"${named}${marks} ${CONFIG_ATTR}="${escapeAttr(JSON.stringify(chart))}"><div class="aparte-chartjs__frame"><canvas class="aparte-chartjs__canvas" role="img"></canvas></div><div class="aparte-sr-only">${dataTable(chart, locale)}</div></div>`;
|
|
675
|
+
}
|
|
676
|
+
function pendingMarkup() {
|
|
677
|
+
return '<div class="aparte-chartjs"><div class="aparte-chartjs__frame"><span class="aparte-skeleton aparte-skeleton--rect" aria-hidden="true"></span></div></div>';
|
|
678
|
+
}
|
|
679
|
+
function relabelCharts(root, config, options) {
|
|
680
|
+
const wrappers = root.matches(".aparte-chartjs") ? [root] : Array.from(root.querySelectorAll(".aparte-chartjs"));
|
|
681
|
+
for (const wrapper of wrappers) {
|
|
682
|
+
if (options.height) wrapper.style.setProperty("--aparte-chartjs-height", options.height);
|
|
683
|
+
const name = wrapper.getAttribute("data-aparte-chartjs-title") || word(config, "chartjsChart");
|
|
684
|
+
if (wrapper.getAttribute("role") === "figure") wrapper.setAttribute("aria-label", name);
|
|
685
|
+
wrapper.querySelector("canvas")?.setAttribute("aria-label", name);
|
|
686
|
+
const caption = wrapper.querySelector("[data-aparte-chartjs-name]");
|
|
687
|
+
if (caption) caption.textContent = name;
|
|
688
|
+
for (const th of Array.from(wrapper.querySelectorAll("[data-aparte-chartjs-series]"))) {
|
|
689
|
+
th.textContent = word(config, "chartjsSeries", { n: th.getAttribute("data-aparte-chartjs-series") ?? "" });
|
|
690
|
+
}
|
|
691
|
+
for (const cut of Array.from(wrapper.querySelectorAll("[data-aparte-chartjs-cut]"))) cut.textContent = word(config, "chartjsTableCut");
|
|
692
|
+
}
|
|
693
|
+
for (const note of Array.from(root.querySelectorAll("[data-aparte-chartjs-error]"))) note.textContent = word(config, "chartjsError");
|
|
694
|
+
for (const note of Array.from(root.querySelectorAll("[data-aparte-chartjs-unavailable]"))) note.textContent = word(config, "chartjsUnavailable");
|
|
695
|
+
for (const note of Array.from(root.querySelectorAll("[data-aparte-chartjs-waiting]"))) note.textContent = config.t("approvalWaiting");
|
|
696
|
+
}
|
|
697
|
+
const localeOf = (config) => {
|
|
698
|
+
const tag = config.getLocale().tag;
|
|
699
|
+
return typeof tag === "string" && tag ? tag : void 0;
|
|
700
|
+
};
|
|
701
|
+
function failed(ctx) {
|
|
702
|
+
return `<span class="aparte-code-error" role="note" data-aparte-chartjs-error></span><pre class="aparte-code-error">${escapeHtml(ctx.source)}</pre>`;
|
|
703
|
+
}
|
|
704
|
+
function createFenceRenderer(charts, options) {
|
|
705
|
+
const progressive = options.progressive !== false;
|
|
706
|
+
const maxPoints = options.maxPoints ?? MAX_POINTS;
|
|
707
|
+
const arriving = /* @__PURE__ */ new WeakMap();
|
|
708
|
+
const stateOf = (host) => {
|
|
709
|
+
const part = host.closest("[data-segment-id]") ?? host.closest("pre")?.parentElement ?? host.parentElement ?? host;
|
|
710
|
+
let state = arriving.get(part);
|
|
711
|
+
if (!state) {
|
|
712
|
+
state = { waiting: null, timer: void 0, at: 0, started: null, drawn: null };
|
|
713
|
+
arriving.set(part, state);
|
|
714
|
+
}
|
|
715
|
+
return state;
|
|
716
|
+
};
|
|
717
|
+
const frame = async (ctx, state) => {
|
|
718
|
+
const chart = parseFence(ctx.source, true, maxPoints);
|
|
719
|
+
if (!chart || !hasPoints(chart)) return null;
|
|
720
|
+
const key = JSON.stringify(chart);
|
|
721
|
+
if (key === state.drawn) return null;
|
|
722
|
+
await charts.chartOf();
|
|
723
|
+
state.drawn = key;
|
|
724
|
+
return chartMarkup(chart, localeOf(ctx.config), { open: true });
|
|
725
|
+
};
|
|
726
|
+
const schedule = (state) => {
|
|
727
|
+
if (state.timer !== void 0 || !state.waiting) return;
|
|
728
|
+
const wait = Math.max(0, state.at + PROGRESSIVE_MS - performance.now());
|
|
729
|
+
state.timer = setTimeout(() => {
|
|
730
|
+
state.timer = void 0;
|
|
731
|
+
const job = state.waiting;
|
|
732
|
+
state.waiting = null;
|
|
733
|
+
if (!job) return;
|
|
734
|
+
state.at = performance.now();
|
|
735
|
+
void frame(job.ctx, state).then(job.answer, () => job.answer(null));
|
|
736
|
+
}, wait);
|
|
737
|
+
};
|
|
738
|
+
return {
|
|
739
|
+
languages: CHARTJS_LANGUAGES,
|
|
740
|
+
lazy: true,
|
|
741
|
+
render(ctx) {
|
|
742
|
+
if (!ctx.display) return null;
|
|
743
|
+
const state = stateOf(ctx.host);
|
|
744
|
+
const waiting = state.waiting;
|
|
745
|
+
if (!ctx.open) {
|
|
746
|
+
const streamed = state.started !== null && ctx.source.startsWith(state.started);
|
|
747
|
+
if (streamed) {
|
|
748
|
+
waiting?.answer(null);
|
|
749
|
+
state.waiting = null;
|
|
750
|
+
state.started = null;
|
|
751
|
+
state.drawn = null;
|
|
752
|
+
}
|
|
753
|
+
const chart = parseFence(ctx.source, false, maxPoints);
|
|
754
|
+
if (!chart) return failed(ctx);
|
|
755
|
+
return charts.chartOf().then(() => chartMarkup(chart, localeOf(ctx.config), { still: streamed }));
|
|
756
|
+
}
|
|
757
|
+
waiting?.answer(null);
|
|
758
|
+
state.waiting = null;
|
|
759
|
+
if (!progressive) return null;
|
|
760
|
+
if (state.started === null || !ctx.source.startsWith(state.started)) {
|
|
761
|
+
state.started = ctx.source;
|
|
762
|
+
state.drawn = null;
|
|
763
|
+
charts.warm();
|
|
764
|
+
return pendingMarkup();
|
|
765
|
+
}
|
|
766
|
+
return new Promise((answer) => {
|
|
767
|
+
state.waiting = { ctx, answer };
|
|
768
|
+
schedule(state);
|
|
769
|
+
});
|
|
770
|
+
},
|
|
771
|
+
relabel(drawing, ctx) {
|
|
772
|
+
relabelCharts(drawing, ctx.config, options);
|
|
773
|
+
charts.activate(drawing);
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function resultOf(segment) {
|
|
778
|
+
if (segment.structuredResult !== void 0) return segment.structuredResult;
|
|
779
|
+
if (typeof segment.result !== "string") return void 0;
|
|
780
|
+
try {
|
|
781
|
+
return JSON.parse(segment.result);
|
|
782
|
+
} catch {
|
|
783
|
+
return void 0;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function createToolRenderer(charts, options, config) {
|
|
787
|
+
const maxPoints = options.maxPoints ?? MAX_POINTS;
|
|
788
|
+
const drawnFrom = /* @__PURE__ */ new WeakMap();
|
|
789
|
+
const note = (attr) => `<span class="aparte-chartjs-note" role="note" ${attr}></span>`;
|
|
790
|
+
const draw = (element, segment) => {
|
|
791
|
+
const status = segment.status;
|
|
792
|
+
const signature = status === "resolved" ? `resolved|${segment.result ?? ""}|${JSON.stringify(segment.structuredResult ?? null)}` : status;
|
|
793
|
+
if (drawnFrom.get(element) === signature) return false;
|
|
794
|
+
drawnFrom.set(element, signature);
|
|
795
|
+
charts.releaseIn(element);
|
|
796
|
+
element.removeAttribute("aria-busy");
|
|
797
|
+
let html;
|
|
798
|
+
if (status === "pending") {
|
|
799
|
+
element.setAttribute("aria-busy", "true");
|
|
800
|
+
charts.warm();
|
|
801
|
+
html = pendingMarkup();
|
|
802
|
+
} else if (status === "awaiting-approval") {
|
|
803
|
+
html = note("data-aparte-chartjs-waiting");
|
|
804
|
+
} else if (status === "resolved") {
|
|
805
|
+
let chart = null;
|
|
806
|
+
try {
|
|
807
|
+
const result = resultOf(segment);
|
|
808
|
+
chart = readChartConfig(options.map ? options.map(result, segment.toolCall?.input ?? {}) : result, maxPoints);
|
|
809
|
+
} catch (error) {
|
|
810
|
+
warnOnce(options, "the `map` given for a chart tool threw.", error);
|
|
811
|
+
}
|
|
812
|
+
html = chart ? chartMarkup(chart, localeOf(config)) : `<span class="aparte-code-error" role="note" data-aparte-chartjs-error></span>`;
|
|
813
|
+
} else {
|
|
814
|
+
html = note("data-aparte-chartjs-unavailable");
|
|
815
|
+
}
|
|
816
|
+
element.innerHTML = html;
|
|
817
|
+
relabelCharts(element, config, options);
|
|
818
|
+
return true;
|
|
819
|
+
};
|
|
820
|
+
return {
|
|
821
|
+
render(segment) {
|
|
822
|
+
const element = document.createElement("div");
|
|
823
|
+
element.className = "aparte-chartjs-tool";
|
|
824
|
+
draw(element, segment);
|
|
825
|
+
return element;
|
|
826
|
+
},
|
|
827
|
+
setup(element) {
|
|
828
|
+
ensureStyles(element);
|
|
829
|
+
charts.activate(element);
|
|
830
|
+
},
|
|
831
|
+
update(element, segment) {
|
|
832
|
+
if (!draw(element, segment)) return;
|
|
833
|
+
ensureStyles(element);
|
|
834
|
+
charts.activate(element);
|
|
835
|
+
},
|
|
836
|
+
relabel(element) {
|
|
837
|
+
relabelCharts(element, config, options);
|
|
838
|
+
},
|
|
839
|
+
cleanup(element) {
|
|
840
|
+
charts.releaseIn(element);
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
function watchTheme(changed) {
|
|
845
|
+
if (typeof MutationObserver === "undefined" || typeof document === "undefined") return { add: () => {
|
|
846
|
+
}, stop: () => {
|
|
847
|
+
} };
|
|
848
|
+
const observer = new MutationObserver(changed);
|
|
849
|
+
const init = { attributes: true, subtree: true, attributeFilter: ["data-aparte-theme"] };
|
|
850
|
+
observer.observe(document.documentElement, init);
|
|
851
|
+
const media = typeof matchMedia === "function" ? matchMedia("(prefers-color-scheme: dark)") : null;
|
|
852
|
+
media?.addEventListener("change", changed);
|
|
853
|
+
return {
|
|
854
|
+
add: (root) => observer.observe(root, init),
|
|
855
|
+
stop: () => {
|
|
856
|
+
observer.disconnect();
|
|
857
|
+
media?.removeEventListener("change", changed);
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
export {
|
|
862
|
+
CHARTJS_LANGUAGES,
|
|
863
|
+
SERIES_HUES,
|
|
864
|
+
readChartConfig,
|
|
865
|
+
seriesPalette,
|
|
866
|
+
setupChartjsWith,
|
|
867
|
+
themeChartjsConfig
|
|
868
|
+
};
|
|
869
|
+
//# sourceMappingURL=core.js.map
|