@ai-gui/plugin-bigscreen 0.36.3 → 0.37.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/CHANGELOG.md +57 -0
- package/README.md +20 -3
- package/dist/index.cjs +478 -14
- package/dist/index.d.cts +631 -4
- package/dist/index.d.ts +631 -4
- package/dist/index.js +416 -6
- package/dist/{mount-DPyPP9Ha.cjs → mount-BTQc08Gl.cjs} +164 -27
- package/dist/{mount-CbzNLMoI.js → mount-jK0A6BBY.js} +147 -10
- package/dist/positions-BArCP-1k.js +1621 -0
- package/dist/positions-BWG0m0U9.cjs +1933 -0
- package/package.json +2 -2
- package/dist/options-BpHnWRpI.cjs +0 -499
- package/dist/options-C087Peeh.js +0 -415
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
const
|
|
3
|
-
const echarts_charts =
|
|
4
|
-
const echarts_components =
|
|
5
|
-
const echarts_core =
|
|
6
|
-
const echarts_renderers =
|
|
2
|
+
const require_positions = require('./positions-BWG0m0U9.cjs');
|
|
3
|
+
const echarts_charts = require_positions.__toESM(require("echarts/charts"));
|
|
4
|
+
const echarts_components = require_positions.__toESM(require("echarts/components"));
|
|
5
|
+
const echarts_core = require_positions.__toESM(require("echarts/core"));
|
|
6
|
+
const echarts_renderers = require_positions.__toESM(require("echarts/renderers"));
|
|
7
7
|
|
|
8
8
|
//#region src/mount.ts
|
|
9
9
|
(0, echarts_core.use)([
|
|
@@ -36,7 +36,9 @@ const DEFAULT_HEIGHT = {
|
|
|
36
36
|
rank: 220,
|
|
37
37
|
chart: 240,
|
|
38
38
|
chart3d: 280,
|
|
39
|
-
globe: 300
|
|
39
|
+
globe: 300,
|
|
40
|
+
timeline: 320,
|
|
41
|
+
graph3d: 320
|
|
40
42
|
};
|
|
41
43
|
const easeOut = (t) => 1 - Math.pow(1 - t, 3);
|
|
42
44
|
function el(tag, attrs = {}, text) {
|
|
@@ -49,7 +51,7 @@ function el(tag, attrs = {}, text) {
|
|
|
49
51
|
function countUp(node, panel, animate) {
|
|
50
52
|
const decimals = panel.decimals ?? 0;
|
|
51
53
|
const show = (value) => {
|
|
52
|
-
node.textContent = `${panel.prefix ?? ""}${
|
|
54
|
+
node.textContent = `${panel.prefix ?? ""}${require_positions.formatNumber(value, decimals)}`;
|
|
53
55
|
};
|
|
54
56
|
if (!animate || typeof requestAnimationFrame !== "function") {
|
|
55
57
|
show(panel.value);
|
|
@@ -81,7 +83,7 @@ function sparkline(values, c, width, height) {
|
|
|
81
83
|
svg.setAttribute("aria-hidden", "true");
|
|
82
84
|
const fill = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
|
|
83
85
|
fill.setAttribute("points", `0,${height} ${points.join(" ")} ${width},${height}`);
|
|
84
|
-
fill.setAttribute("fill",
|
|
86
|
+
fill.setAttribute("fill", require_positions.withAlpha(c.accent, .18));
|
|
85
87
|
const line = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
|
|
86
88
|
line.setAttribute("points", points.join(" "));
|
|
87
89
|
line.setAttribute("fill", "none");
|
|
@@ -97,7 +99,7 @@ function mountKpi(body, panel, c, animate) {
|
|
|
97
99
|
const row = el("div", { class: "aigui-bs-kpi-row" });
|
|
98
100
|
const value = el("span", { class: "aigui-bs-kpi-value" });
|
|
99
101
|
value.style.color = c.accent;
|
|
100
|
-
value.style.textShadow = `0 0 18px ${
|
|
102
|
+
value.style.textShadow = `0 0 18px ${require_positions.withAlpha(c.accent, .45)}`;
|
|
101
103
|
row.appendChild(value);
|
|
102
104
|
if (panel.unit) row.appendChild(el("span", { class: "aigui-bs-kpi-unit" }, panel.unit));
|
|
103
105
|
body.appendChild(row);
|
|
@@ -105,7 +107,7 @@ function mountKpi(body, panel, c, animate) {
|
|
|
105
107
|
if (panel.delta !== void 0) {
|
|
106
108
|
const up = panel.delta >= 0;
|
|
107
109
|
const good = (panel.upIsGood ?? true) === up;
|
|
108
|
-
const delta = el("span", { class: "aigui-bs-kpi-delta" }, `${up ? "▲" : "▼"} ${
|
|
110
|
+
const delta = el("span", { class: "aigui-bs-kpi-delta" }, `${up ? "▲" : "▼"} ${require_positions.formatNumber(Math.abs(panel.delta) * 100, 1)}%`);
|
|
109
111
|
delta.style.color = good ? c.good : c.bad;
|
|
110
112
|
meta.appendChild(delta);
|
|
111
113
|
}
|
|
@@ -132,12 +134,12 @@ function mountRank(body, panel, c, animate) {
|
|
|
132
134
|
track.style.background = c.track;
|
|
133
135
|
const fill = el("span", { class: "aigui-bs-rank-fill" });
|
|
134
136
|
const colour = index === 0 ? c.accent : c.series[index % (c.series.length - 1) + 1];
|
|
135
|
-
fill.style.background = `linear-gradient(90deg, ${
|
|
136
|
-
fill.style.boxShadow = `0 0 10px ${
|
|
137
|
+
fill.style.background = `linear-gradient(90deg, ${require_positions.withAlpha(colour, .55)}, ${colour})`;
|
|
138
|
+
fill.style.boxShadow = `0 0 10px ${require_positions.withAlpha(colour, .5)}`;
|
|
137
139
|
const width = `${Math.max(2, Math.max(item.value, 0) / max * 100)}%`;
|
|
138
140
|
fill.style.width = animate ? "0%" : width;
|
|
139
141
|
track.appendChild(fill);
|
|
140
|
-
const value = el("span", { class: "aigui-bs-rank-value" }, `${
|
|
142
|
+
const value = el("span", { class: "aigui-bs-rank-value" }, `${require_positions.formatNumber(item.value, Number.isInteger(item.value) ? 0 : 1)}${panel.unit ?? ""}`);
|
|
141
143
|
row.append(rank, name, track, value);
|
|
142
144
|
body.appendChild(row);
|
|
143
145
|
bars.push({
|
|
@@ -171,12 +173,13 @@ function whenSized(body, fn) {
|
|
|
171
173
|
const handle = requestAnimationFrame(fn);
|
|
172
174
|
return () => cancelAnimationFrame(handle);
|
|
173
175
|
}
|
|
174
|
-
function mountChart(body, option, theme) {
|
|
176
|
+
function mountChart(body, option, theme, ready) {
|
|
175
177
|
let chart;
|
|
176
178
|
let unfollow;
|
|
177
179
|
const cancel = whenSized(body, () => {
|
|
178
180
|
chart = (0, echarts_core.init)(body, theme === "dark" ? "dark" : void 0, { renderer: "canvas" });
|
|
179
181
|
chart.setOption(option);
|
|
182
|
+
ready?.(chart);
|
|
180
183
|
unfollow = follow(body, chart);
|
|
181
184
|
});
|
|
182
185
|
return () => {
|
|
@@ -185,10 +188,17 @@ function mountChart(body, option, theme) {
|
|
|
185
188
|
chart?.dispose();
|
|
186
189
|
};
|
|
187
190
|
}
|
|
188
|
-
|
|
191
|
+
/**
|
|
192
|
+
* A chart that needs WebGL, with an optional loop running against it.
|
|
193
|
+
*
|
|
194
|
+
* `ready` may hand back a teardown of its own — the settling loop does — and it is run before the
|
|
195
|
+
* chart is disposed, because a frame that lands on a disposed chart throws.
|
|
196
|
+
*/
|
|
197
|
+
function mountGl(body, build, theme, fallback, ready) {
|
|
189
198
|
let chart;
|
|
190
199
|
let unfollow;
|
|
191
200
|
let cancel;
|
|
201
|
+
let stop;
|
|
192
202
|
let disposed = false;
|
|
193
203
|
loadGl().then(() => {
|
|
194
204
|
if (disposed) return;
|
|
@@ -196,6 +206,7 @@ function mountGl(body, build, theme, fallback) {
|
|
|
196
206
|
if (disposed) return;
|
|
197
207
|
chart = (0, echarts_core.init)(body, theme === "dark" ? "dark" : void 0, { renderer: "canvas" });
|
|
198
208
|
chart.setOption(build());
|
|
209
|
+
stop = ready?.(chart) ?? void 0;
|
|
199
210
|
unfollow = follow(body, chart);
|
|
200
211
|
});
|
|
201
212
|
}).catch(() => {
|
|
@@ -207,6 +218,7 @@ function mountGl(body, build, theme, fallback) {
|
|
|
207
218
|
return () => {
|
|
208
219
|
disposed = true;
|
|
209
220
|
cancel?.();
|
|
221
|
+
stop?.();
|
|
210
222
|
unfollow?.();
|
|
211
223
|
chart?.dispose();
|
|
212
224
|
};
|
|
@@ -214,7 +226,103 @@ function mountGl(body, build, theme, fallback) {
|
|
|
214
226
|
function note(body, text) {
|
|
215
227
|
body.replaceChildren(el("div", { class: "aigui-bs-note" }, text));
|
|
216
228
|
}
|
|
217
|
-
|
|
229
|
+
/**
|
|
230
|
+
* What a click on a claim does.
|
|
231
|
+
*
|
|
232
|
+
* The host wins. A page that passed `onItemClick` has its own idea of what a claim is — a drawer,
|
|
233
|
+
* a route, a second panel — and must not also get a tab it never asked for. With no handler the
|
|
234
|
+
* claim's own `url` opens in a new tab with the opener cut off; the parser has already refused
|
|
235
|
+
* anything that is not `http` or `https`, because this argument reaches `window.open`.
|
|
236
|
+
*/
|
|
237
|
+
function openTimelineItem(item, events) {
|
|
238
|
+
if (events?.onItemClick) {
|
|
239
|
+
events.onItemClick(item);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (item.url && typeof window !== "undefined") window.open(item.url, "_blank", "noopener,noreferrer");
|
|
243
|
+
}
|
|
244
|
+
function bindTimeline(chart, events) {
|
|
245
|
+
chart.on("click", (params) => {
|
|
246
|
+
const item = params.data?.item;
|
|
247
|
+
if (item) openTimelineItem(item, events);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
function bindGraph(chart, events) {
|
|
251
|
+
chart.on("click", (params) => {
|
|
252
|
+
const event = params;
|
|
253
|
+
if (event.dataType === "edge") return;
|
|
254
|
+
const node = event.data?.node;
|
|
255
|
+
if (node) events?.onNodeClick?.(node);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Step the layout in front of the reader, and stop.
|
|
260
|
+
*
|
|
261
|
+
* A knowledge graph pulling itself apart is the panel telling you what it found: the clusters
|
|
262
|
+
* arrive one after another and you watch which entities go with which. So the layout is stepped a
|
|
263
|
+
* few steps per frame rather than run to convergence behind a promise, and only the two series'
|
|
264
|
+
* data is pushed each time — merged, not replaced, so the camera and the lights survive the
|
|
265
|
+
* update and the graph does not jump back to its opening pose sixty times a second.
|
|
266
|
+
*
|
|
267
|
+
* When it has settled the loop ends for good and the positions are remembered, which is what lets
|
|
268
|
+
* the next render of the same fence resume rather than reshuffle. The camera keeps turning after:
|
|
269
|
+
* that is echarts-gl's own animation, not this one.
|
|
270
|
+
*/
|
|
271
|
+
function settleGraph(chart, panel, c, layout, key) {
|
|
272
|
+
const ids = panel.nodes.map((node) => node.id);
|
|
273
|
+
const remember = () => require_positions.rememberPositions(key, ids, layout.positions());
|
|
274
|
+
if (layout.done || typeof requestAnimationFrame !== "function") {
|
|
275
|
+
remember();
|
|
276
|
+
return () => {};
|
|
277
|
+
}
|
|
278
|
+
const chunk = require_positions.layoutChunk(panel.nodes.length);
|
|
279
|
+
const tick = () => {
|
|
280
|
+
layout.step(chunk);
|
|
281
|
+
const data = require_positions.graphOrbitData(panel, c, layout.positions());
|
|
282
|
+
chart.setOption({ series: [{ data: data.edges }, { data: data.nodes }] }, {
|
|
283
|
+
notMerge: false,
|
|
284
|
+
lazyUpdate: false
|
|
285
|
+
});
|
|
286
|
+
if (layout.done) {
|
|
287
|
+
remember();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
handle = requestAnimationFrame(tick);
|
|
291
|
+
};
|
|
292
|
+
let handle = requestAnimationFrame(tick);
|
|
293
|
+
return () => cancelAnimationFrame(handle);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* The key to a graph's colours, in HTML over the canvas.
|
|
297
|
+
*
|
|
298
|
+
* HTML rather than an ECharts legend: the graph is a WebGL series with no categories for a legend
|
|
299
|
+
* component to read, and a corner of text a reader can select beats a row of chart furniture. It
|
|
300
|
+
* is `pointer-events:none` so it never eats a click meant for the graph behind it, and an untyped
|
|
301
|
+
* graph gets nothing at all rather than an empty box.
|
|
302
|
+
*
|
|
303
|
+
* It hangs off the panel rather than off the chart's own element, because ECharts empties the
|
|
304
|
+
* element it is initialised on — a legend appended there is gone the moment the graph appears,
|
|
305
|
+
* which is exactly what happened the first time this was drawn.
|
|
306
|
+
*/
|
|
307
|
+
function graphLegendNode(panel, c) {
|
|
308
|
+
const entries = require_positions.graphLegend(panel, c);
|
|
309
|
+
if (!entries.length) return void 0;
|
|
310
|
+
const box = el("div", { class: "aigui-bs-graph-legend" });
|
|
311
|
+
for (const entry of entries) {
|
|
312
|
+
const row = el("div", { class: "aigui-bs-graph-legend-row" });
|
|
313
|
+
const swatch = el("i", { class: `aigui-bs-graph-legend-${entry.shape === "node" ? "dot" : "line"}` });
|
|
314
|
+
swatch.style.background = entry.colour;
|
|
315
|
+
row.append(swatch, document.createTextNode(entry.label));
|
|
316
|
+
box.appendChild(row);
|
|
317
|
+
}
|
|
318
|
+
return box;
|
|
319
|
+
}
|
|
320
|
+
/** The body height a panel gets when it did not ask for one. */
|
|
321
|
+
function bodyHeight(panel) {
|
|
322
|
+
if (panel.kind === "timeline") return require_positions.timelineHeight(panel.lanes.length);
|
|
323
|
+
return DEFAULT_HEIGHT[panel.kind];
|
|
324
|
+
}
|
|
325
|
+
function mountPanel(panel, definition, c, animate, earth, events) {
|
|
218
326
|
const node = el("section", {
|
|
219
327
|
class: "aigui-bs-panel",
|
|
220
328
|
"data-aigui-bigscreen-panel": panel.kind
|
|
@@ -222,7 +330,7 @@ function mountPanel(panel, definition, c, animate) {
|
|
|
222
330
|
const span = Math.min(panel.span ?? 4, definition.columns);
|
|
223
331
|
node.style.gridColumn = `span ${span}`;
|
|
224
332
|
if (span * 2 > definition.columns) node.setAttribute("data-aigui-bigscreen-wide", "");
|
|
225
|
-
node.style.borderColor =
|
|
333
|
+
node.style.borderColor = require_positions.withAlpha(c.accent, definition.theme === "dark" ? .22 : .18);
|
|
226
334
|
if (panel.title) {
|
|
227
335
|
const head = el("header", { class: "aigui-bs-panel-title" }, panel.title);
|
|
228
336
|
const mark = el("i", { class: "aigui-bs-panel-mark" });
|
|
@@ -232,7 +340,7 @@ function mountPanel(panel, definition, c, animate) {
|
|
|
232
340
|
node.appendChild(head);
|
|
233
341
|
}
|
|
234
342
|
const body = el("div", { class: "aigui-bs-panel-body" });
|
|
235
|
-
const height = panel.height ??
|
|
343
|
+
const height = panel.height ?? bodyHeight(panel);
|
|
236
344
|
if (height) body.style.height = `${height}px`;
|
|
237
345
|
node.appendChild(body);
|
|
238
346
|
let destroy = () => {};
|
|
@@ -245,17 +353,40 @@ function mountPanel(panel, definition, c, animate) {
|
|
|
245
353
|
destroy = mountRank(body, panel, c, animate);
|
|
246
354
|
break;
|
|
247
355
|
case "gauge":
|
|
248
|
-
destroy = mountChart(body,
|
|
356
|
+
destroy = mountChart(body, require_positions.gaugeOption(panel, c, animate), definition.theme);
|
|
249
357
|
break;
|
|
250
358
|
case "chart":
|
|
251
|
-
destroy = mountChart(body,
|
|
359
|
+
destroy = mountChart(body, require_positions.chartOption(panel, c, animate), definition.theme);
|
|
252
360
|
break;
|
|
253
361
|
case "chart3d":
|
|
254
|
-
destroy = mountGl(body, () =>
|
|
362
|
+
destroy = mountGl(body, () => require_positions.chart3dOption(panel, c, animate), definition.theme, () => note(body, "3D panels need echarts-gl and WebGL."));
|
|
255
363
|
break;
|
|
256
364
|
case "globe":
|
|
257
|
-
destroy = mountGl(body, () =>
|
|
365
|
+
destroy = mountGl(body, () => require_positions.globeOption(panel, c, animate, require_positions.earthTexture(c, definition.theme, earth), earth), definition.theme, () => note(body, "Globe panels need echarts-gl and WebGL."));
|
|
366
|
+
break;
|
|
367
|
+
case "timeline":
|
|
368
|
+
destroy = mountChart(body, require_positions.timelineOption(panel, c, animate), definition.theme, (chart) => bindTimeline(chart, events));
|
|
258
369
|
break;
|
|
370
|
+
case "graph3d": {
|
|
371
|
+
const legend = graphLegendNode(panel, c);
|
|
372
|
+
if (legend) node.appendChild(legend);
|
|
373
|
+
const missing = () => {
|
|
374
|
+
legend?.remove();
|
|
375
|
+
note(body, "Knowledge graph panels need echarts-gl and WebGL.");
|
|
376
|
+
};
|
|
377
|
+
if (panel.mode === "flat") {
|
|
378
|
+
destroy = mountGl(body, () => require_positions.graph3dOption(panel, c, animate), definition.theme, missing, (chart) => bindGraph(chart, events));
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
const key = require_positions.graphKey(panel);
|
|
382
|
+
const layout = require_positions.createLayout(panel.nodes, panel.edges, require_positions.recallPositions(key));
|
|
383
|
+
if (!animate) layout.step(layout.steps);
|
|
384
|
+
destroy = mountGl(body, () => require_positions.graphOrbitOption(panel, c, animate, layout.positions()), definition.theme, missing, (chart) => {
|
|
385
|
+
bindGraph(chart, events);
|
|
386
|
+
return settleGraph(chart, panel, c, layout, key);
|
|
387
|
+
});
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
259
390
|
}
|
|
260
391
|
} catch {
|
|
261
392
|
note(body, "Panel could not be drawn.");
|
|
@@ -265,9 +396,15 @@ function mountPanel(panel, definition, c, animate) {
|
|
|
265
396
|
destroy
|
|
266
397
|
};
|
|
267
398
|
}
|
|
268
|
-
/**
|
|
269
|
-
|
|
270
|
-
|
|
399
|
+
/**
|
|
400
|
+
* Build the whole screen into `host`, returning the teardown the reconciler will call.
|
|
401
|
+
*
|
|
402
|
+
* `earth` is the host's globe configuration, passed through untouched: what the planet looks like
|
|
403
|
+
* is the page's decision, not the fence's. `events` is the same bargain for clicks: what a claim
|
|
404
|
+
* or an entity does when it is clicked is the page's decision too.
|
|
405
|
+
*/
|
|
406
|
+
function mountScreen(host, definition, animate, earth, events) {
|
|
407
|
+
const c = require_positions.palette(definition);
|
|
271
408
|
host.setAttribute("data-aigui-bigscreen", definition.theme);
|
|
272
409
|
host.style.setProperty("--aigui-bs-accent", c.accent);
|
|
273
410
|
host.style.setProperty("--aigui-bs-text", c.text);
|
|
@@ -276,7 +413,7 @@ function mountScreen(host, definition, animate) {
|
|
|
276
413
|
const head = el("header", { class: "aigui-bs-head" });
|
|
277
414
|
if (definition.title) {
|
|
278
415
|
const title = el("h2", { class: "aigui-bs-title" }, definition.title);
|
|
279
|
-
title.style.textShadow = definition.theme === "dark" ? `0 0 24px ${
|
|
416
|
+
title.style.textShadow = definition.theme === "dark" ? `0 0 24px ${require_positions.withAlpha(c.accent, .35)}` : "none";
|
|
280
417
|
head.appendChild(title);
|
|
281
418
|
}
|
|
282
419
|
if (definition.subtitle) head.appendChild(el("p", { class: "aigui-bs-subtitle" }, definition.subtitle));
|
|
@@ -288,7 +425,7 @@ function mountScreen(host, definition, animate) {
|
|
|
288
425
|
const grid = el("div", { class: "aigui-bs-grid" });
|
|
289
426
|
grid.style.gridTemplateColumns = `repeat(${definition.columns}, minmax(0, 1fr))`;
|
|
290
427
|
host.appendChild(grid);
|
|
291
|
-
const mounted = definition.panels.map((panel) => mountPanel(panel, definition, c, animate));
|
|
428
|
+
const mounted = definition.panels.map((panel) => mountPanel(panel, definition, c, animate, earth, events));
|
|
292
429
|
for (const { node } of mounted) grid.appendChild(node);
|
|
293
430
|
return () => {
|
|
294
431
|
for (const { destroy } of mounted) destroy();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chart3dOption, chartOption, formatNumber, gaugeOption, globeOption,
|
|
1
|
+
import { chart3dOption, chartOption, createLayout, earthTexture, formatNumber, gaugeOption, globeOption, graph3dOption, graphKey, graphLegend, graphOrbitData, graphOrbitOption, layoutChunk, palette, recallPositions, rememberPositions, timelineHeight, timelineOption, withAlpha } from "./positions-BArCP-1k.js";
|
|
2
2
|
import { BarChart, EffectScatterChart, FunnelChart, GaugeChart, HeatmapChart, LineChart, LinesChart, PieChart, RadarChart, ScatterChart } from "echarts/charts";
|
|
3
3
|
import { DatasetComponent, GridComponent, LegendComponent, PolarComponent, RadarComponent, TitleComponent, TooltipComponent, VisualMapComponent } from "echarts/components";
|
|
4
4
|
import { init, use } from "echarts/core";
|
|
@@ -35,7 +35,9 @@ const DEFAULT_HEIGHT = {
|
|
|
35
35
|
rank: 220,
|
|
36
36
|
chart: 240,
|
|
37
37
|
chart3d: 280,
|
|
38
|
-
globe: 300
|
|
38
|
+
globe: 300,
|
|
39
|
+
timeline: 320,
|
|
40
|
+
graph3d: 320
|
|
39
41
|
};
|
|
40
42
|
const easeOut = (t) => 1 - Math.pow(1 - t, 3);
|
|
41
43
|
function el(tag, attrs = {}, text) {
|
|
@@ -170,12 +172,13 @@ function whenSized(body, fn) {
|
|
|
170
172
|
const handle = requestAnimationFrame(fn);
|
|
171
173
|
return () => cancelAnimationFrame(handle);
|
|
172
174
|
}
|
|
173
|
-
function mountChart(body, option, theme) {
|
|
175
|
+
function mountChart(body, option, theme, ready) {
|
|
174
176
|
let chart;
|
|
175
177
|
let unfollow;
|
|
176
178
|
const cancel = whenSized(body, () => {
|
|
177
179
|
chart = init(body, theme === "dark" ? "dark" : void 0, { renderer: "canvas" });
|
|
178
180
|
chart.setOption(option);
|
|
181
|
+
ready?.(chart);
|
|
179
182
|
unfollow = follow(body, chart);
|
|
180
183
|
});
|
|
181
184
|
return () => {
|
|
@@ -184,10 +187,17 @@ function mountChart(body, option, theme) {
|
|
|
184
187
|
chart?.dispose();
|
|
185
188
|
};
|
|
186
189
|
}
|
|
187
|
-
|
|
190
|
+
/**
|
|
191
|
+
* A chart that needs WebGL, with an optional loop running against it.
|
|
192
|
+
*
|
|
193
|
+
* `ready` may hand back a teardown of its own — the settling loop does — and it is run before the
|
|
194
|
+
* chart is disposed, because a frame that lands on a disposed chart throws.
|
|
195
|
+
*/
|
|
196
|
+
function mountGl(body, build, theme, fallback, ready) {
|
|
188
197
|
let chart;
|
|
189
198
|
let unfollow;
|
|
190
199
|
let cancel;
|
|
200
|
+
let stop;
|
|
191
201
|
let disposed = false;
|
|
192
202
|
loadGl().then(() => {
|
|
193
203
|
if (disposed) return;
|
|
@@ -195,6 +205,7 @@ function mountGl(body, build, theme, fallback) {
|
|
|
195
205
|
if (disposed) return;
|
|
196
206
|
chart = init(body, theme === "dark" ? "dark" : void 0, { renderer: "canvas" });
|
|
197
207
|
chart.setOption(build());
|
|
208
|
+
stop = ready?.(chart) ?? void 0;
|
|
198
209
|
unfollow = follow(body, chart);
|
|
199
210
|
});
|
|
200
211
|
}).catch(() => {
|
|
@@ -206,6 +217,7 @@ function mountGl(body, build, theme, fallback) {
|
|
|
206
217
|
return () => {
|
|
207
218
|
disposed = true;
|
|
208
219
|
cancel?.();
|
|
220
|
+
stop?.();
|
|
209
221
|
unfollow?.();
|
|
210
222
|
chart?.dispose();
|
|
211
223
|
};
|
|
@@ -213,7 +225,103 @@ function mountGl(body, build, theme, fallback) {
|
|
|
213
225
|
function note(body, text) {
|
|
214
226
|
body.replaceChildren(el("div", { class: "aigui-bs-note" }, text));
|
|
215
227
|
}
|
|
216
|
-
|
|
228
|
+
/**
|
|
229
|
+
* What a click on a claim does.
|
|
230
|
+
*
|
|
231
|
+
* The host wins. A page that passed `onItemClick` has its own idea of what a claim is — a drawer,
|
|
232
|
+
* a route, a second panel — and must not also get a tab it never asked for. With no handler the
|
|
233
|
+
* claim's own `url` opens in a new tab with the opener cut off; the parser has already refused
|
|
234
|
+
* anything that is not `http` or `https`, because this argument reaches `window.open`.
|
|
235
|
+
*/
|
|
236
|
+
function openTimelineItem(item, events) {
|
|
237
|
+
if (events?.onItemClick) {
|
|
238
|
+
events.onItemClick(item);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (item.url && typeof window !== "undefined") window.open(item.url, "_blank", "noopener,noreferrer");
|
|
242
|
+
}
|
|
243
|
+
function bindTimeline(chart, events) {
|
|
244
|
+
chart.on("click", (params) => {
|
|
245
|
+
const item = params.data?.item;
|
|
246
|
+
if (item) openTimelineItem(item, events);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
function bindGraph(chart, events) {
|
|
250
|
+
chart.on("click", (params) => {
|
|
251
|
+
const event = params;
|
|
252
|
+
if (event.dataType === "edge") return;
|
|
253
|
+
const node = event.data?.node;
|
|
254
|
+
if (node) events?.onNodeClick?.(node);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Step the layout in front of the reader, and stop.
|
|
259
|
+
*
|
|
260
|
+
* A knowledge graph pulling itself apart is the panel telling you what it found: the clusters
|
|
261
|
+
* arrive one after another and you watch which entities go with which. So the layout is stepped a
|
|
262
|
+
* few steps per frame rather than run to convergence behind a promise, and only the two series'
|
|
263
|
+
* data is pushed each time — merged, not replaced, so the camera and the lights survive the
|
|
264
|
+
* update and the graph does not jump back to its opening pose sixty times a second.
|
|
265
|
+
*
|
|
266
|
+
* When it has settled the loop ends for good and the positions are remembered, which is what lets
|
|
267
|
+
* the next render of the same fence resume rather than reshuffle. The camera keeps turning after:
|
|
268
|
+
* that is echarts-gl's own animation, not this one.
|
|
269
|
+
*/
|
|
270
|
+
function settleGraph(chart, panel, c, layout, key) {
|
|
271
|
+
const ids = panel.nodes.map((node) => node.id);
|
|
272
|
+
const remember = () => rememberPositions(key, ids, layout.positions());
|
|
273
|
+
if (layout.done || typeof requestAnimationFrame !== "function") {
|
|
274
|
+
remember();
|
|
275
|
+
return () => {};
|
|
276
|
+
}
|
|
277
|
+
const chunk = layoutChunk(panel.nodes.length);
|
|
278
|
+
const tick = () => {
|
|
279
|
+
layout.step(chunk);
|
|
280
|
+
const data = graphOrbitData(panel, c, layout.positions());
|
|
281
|
+
chart.setOption({ series: [{ data: data.edges }, { data: data.nodes }] }, {
|
|
282
|
+
notMerge: false,
|
|
283
|
+
lazyUpdate: false
|
|
284
|
+
});
|
|
285
|
+
if (layout.done) {
|
|
286
|
+
remember();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
handle = requestAnimationFrame(tick);
|
|
290
|
+
};
|
|
291
|
+
let handle = requestAnimationFrame(tick);
|
|
292
|
+
return () => cancelAnimationFrame(handle);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* The key to a graph's colours, in HTML over the canvas.
|
|
296
|
+
*
|
|
297
|
+
* HTML rather than an ECharts legend: the graph is a WebGL series with no categories for a legend
|
|
298
|
+
* component to read, and a corner of text a reader can select beats a row of chart furniture. It
|
|
299
|
+
* is `pointer-events:none` so it never eats a click meant for the graph behind it, and an untyped
|
|
300
|
+
* graph gets nothing at all rather than an empty box.
|
|
301
|
+
*
|
|
302
|
+
* It hangs off the panel rather than off the chart's own element, because ECharts empties the
|
|
303
|
+
* element it is initialised on — a legend appended there is gone the moment the graph appears,
|
|
304
|
+
* which is exactly what happened the first time this was drawn.
|
|
305
|
+
*/
|
|
306
|
+
function graphLegendNode(panel, c) {
|
|
307
|
+
const entries = graphLegend(panel, c);
|
|
308
|
+
if (!entries.length) return void 0;
|
|
309
|
+
const box = el("div", { class: "aigui-bs-graph-legend" });
|
|
310
|
+
for (const entry of entries) {
|
|
311
|
+
const row = el("div", { class: "aigui-bs-graph-legend-row" });
|
|
312
|
+
const swatch = el("i", { class: `aigui-bs-graph-legend-${entry.shape === "node" ? "dot" : "line"}` });
|
|
313
|
+
swatch.style.background = entry.colour;
|
|
314
|
+
row.append(swatch, document.createTextNode(entry.label));
|
|
315
|
+
box.appendChild(row);
|
|
316
|
+
}
|
|
317
|
+
return box;
|
|
318
|
+
}
|
|
319
|
+
/** The body height a panel gets when it did not ask for one. */
|
|
320
|
+
function bodyHeight(panel) {
|
|
321
|
+
if (panel.kind === "timeline") return timelineHeight(panel.lanes.length);
|
|
322
|
+
return DEFAULT_HEIGHT[panel.kind];
|
|
323
|
+
}
|
|
324
|
+
function mountPanel(panel, definition, c, animate, earth, events) {
|
|
217
325
|
const node = el("section", {
|
|
218
326
|
class: "aigui-bs-panel",
|
|
219
327
|
"data-aigui-bigscreen-panel": panel.kind
|
|
@@ -231,7 +339,7 @@ function mountPanel(panel, definition, c, animate) {
|
|
|
231
339
|
node.appendChild(head);
|
|
232
340
|
}
|
|
233
341
|
const body = el("div", { class: "aigui-bs-panel-body" });
|
|
234
|
-
const height = panel.height ??
|
|
342
|
+
const height = panel.height ?? bodyHeight(panel);
|
|
235
343
|
if (height) body.style.height = `${height}px`;
|
|
236
344
|
node.appendChild(body);
|
|
237
345
|
let destroy = () => {};
|
|
@@ -253,8 +361,31 @@ function mountPanel(panel, definition, c, animate) {
|
|
|
253
361
|
destroy = mountGl(body, () => chart3dOption(panel, c, animate), definition.theme, () => note(body, "3D panels need echarts-gl and WebGL."));
|
|
254
362
|
break;
|
|
255
363
|
case "globe":
|
|
256
|
-
destroy = mountGl(body, () => globeOption(panel, c, animate,
|
|
364
|
+
destroy = mountGl(body, () => globeOption(panel, c, animate, earthTexture(c, definition.theme, earth), earth), definition.theme, () => note(body, "Globe panels need echarts-gl and WebGL."));
|
|
365
|
+
break;
|
|
366
|
+
case "timeline":
|
|
367
|
+
destroy = mountChart(body, timelineOption(panel, c, animate), definition.theme, (chart) => bindTimeline(chart, events));
|
|
257
368
|
break;
|
|
369
|
+
case "graph3d": {
|
|
370
|
+
const legend = graphLegendNode(panel, c);
|
|
371
|
+
if (legend) node.appendChild(legend);
|
|
372
|
+
const missing = () => {
|
|
373
|
+
legend?.remove();
|
|
374
|
+
note(body, "Knowledge graph panels need echarts-gl and WebGL.");
|
|
375
|
+
};
|
|
376
|
+
if (panel.mode === "flat") {
|
|
377
|
+
destroy = mountGl(body, () => graph3dOption(panel, c, animate), definition.theme, missing, (chart) => bindGraph(chart, events));
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
const key = graphKey(panel);
|
|
381
|
+
const layout = createLayout(panel.nodes, panel.edges, recallPositions(key));
|
|
382
|
+
if (!animate) layout.step(layout.steps);
|
|
383
|
+
destroy = mountGl(body, () => graphOrbitOption(panel, c, animate, layout.positions()), definition.theme, missing, (chart) => {
|
|
384
|
+
bindGraph(chart, events);
|
|
385
|
+
return settleGraph(chart, panel, c, layout, key);
|
|
386
|
+
});
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
258
389
|
}
|
|
259
390
|
} catch {
|
|
260
391
|
note(body, "Panel could not be drawn.");
|
|
@@ -264,8 +395,14 @@ function mountPanel(panel, definition, c, animate) {
|
|
|
264
395
|
destroy
|
|
265
396
|
};
|
|
266
397
|
}
|
|
267
|
-
/**
|
|
268
|
-
|
|
398
|
+
/**
|
|
399
|
+
* Build the whole screen into `host`, returning the teardown the reconciler will call.
|
|
400
|
+
*
|
|
401
|
+
* `earth` is the host's globe configuration, passed through untouched: what the planet looks like
|
|
402
|
+
* is the page's decision, not the fence's. `events` is the same bargain for clicks: what a claim
|
|
403
|
+
* or an entity does when it is clicked is the page's decision too.
|
|
404
|
+
*/
|
|
405
|
+
function mountScreen(host, definition, animate, earth, events) {
|
|
269
406
|
const c = palette(definition);
|
|
270
407
|
host.setAttribute("data-aigui-bigscreen", definition.theme);
|
|
271
408
|
host.style.setProperty("--aigui-bs-accent", c.accent);
|
|
@@ -287,7 +424,7 @@ function mountScreen(host, definition, animate) {
|
|
|
287
424
|
const grid = el("div", { class: "aigui-bs-grid" });
|
|
288
425
|
grid.style.gridTemplateColumns = `repeat(${definition.columns}, minmax(0, 1fr))`;
|
|
289
426
|
host.appendChild(grid);
|
|
290
|
-
const mounted = definition.panels.map((panel) => mountPanel(panel, definition, c, animate));
|
|
427
|
+
const mounted = definition.panels.map((panel) => mountPanel(panel, definition, c, animate, earth, events));
|
|
291
428
|
for (const { node } of mounted) grid.appendChild(node);
|
|
292
429
|
return () => {
|
|
293
430
|
for (const { destroy } of mounted) destroy();
|