@ai-gui/plugin-bigscreen 0.34.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 +11 -0
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/index.cjs +588 -0
- package/dist/index.d.cts +205 -0
- package/dist/index.d.ts +205 -0
- package/dist/index.js +574 -0
- package/dist/mount-BPBXhD2D.cjs +298 -0
- package/dist/mount-DVHPS1xP.js +297 -0
- package/dist/options-BpHnWRpI.cjs +499 -0
- package/dist/options-C087Peeh.js +415 -0
- package/package.json +76 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const require_options = require('./options-BpHnWRpI.cjs');
|
|
3
|
+
const echarts_charts = require_options.__toESM(require("echarts/charts"));
|
|
4
|
+
const echarts_components = require_options.__toESM(require("echarts/components"));
|
|
5
|
+
const echarts_core = require_options.__toESM(require("echarts/core"));
|
|
6
|
+
const echarts_renderers = require_options.__toESM(require("echarts/renderers"));
|
|
7
|
+
|
|
8
|
+
//#region src/mount.ts
|
|
9
|
+
(0, echarts_core.use)([
|
|
10
|
+
echarts_charts.BarChart,
|
|
11
|
+
echarts_charts.LineChart,
|
|
12
|
+
echarts_charts.PieChart,
|
|
13
|
+
echarts_charts.ScatterChart,
|
|
14
|
+
echarts_charts.GaugeChart,
|
|
15
|
+
echarts_charts.RadarChart,
|
|
16
|
+
echarts_charts.EffectScatterChart,
|
|
17
|
+
echarts_charts.LinesChart,
|
|
18
|
+
echarts_charts.FunnelChart,
|
|
19
|
+
echarts_charts.HeatmapChart,
|
|
20
|
+
echarts_components.DatasetComponent,
|
|
21
|
+
echarts_components.GridComponent,
|
|
22
|
+
echarts_components.LegendComponent,
|
|
23
|
+
echarts_components.PolarComponent,
|
|
24
|
+
echarts_components.RadarComponent,
|
|
25
|
+
echarts_components.TitleComponent,
|
|
26
|
+
echarts_components.TooltipComponent,
|
|
27
|
+
echarts_components.VisualMapComponent,
|
|
28
|
+
echarts_renderers.CanvasRenderer
|
|
29
|
+
]);
|
|
30
|
+
/** Memoized `echarts-gl` side-effect import, shared across every screen on the page. */
|
|
31
|
+
let glReady = null;
|
|
32
|
+
const loadGl = () => glReady ??= import("echarts-gl");
|
|
33
|
+
const DEFAULT_HEIGHT = {
|
|
34
|
+
kpi: 0,
|
|
35
|
+
gauge: 180,
|
|
36
|
+
rank: 220,
|
|
37
|
+
chart: 240,
|
|
38
|
+
chart3d: 280,
|
|
39
|
+
globe: 300
|
|
40
|
+
};
|
|
41
|
+
const easeOut = (t) => 1 - Math.pow(1 - t, 3);
|
|
42
|
+
function el(tag, attrs = {}, text) {
|
|
43
|
+
const node = document.createElement(tag);
|
|
44
|
+
for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, value);
|
|
45
|
+
if (text !== void 0) node.textContent = text;
|
|
46
|
+
return node;
|
|
47
|
+
}
|
|
48
|
+
/** Count a number up from zero, returning the cancel. */
|
|
49
|
+
function countUp(node, panel, animate) {
|
|
50
|
+
const decimals = panel.decimals ?? 0;
|
|
51
|
+
const show = (value) => {
|
|
52
|
+
node.textContent = `${panel.prefix ?? ""}${require_options.formatNumber(value, decimals)}`;
|
|
53
|
+
};
|
|
54
|
+
if (!animate || typeof requestAnimationFrame !== "function") {
|
|
55
|
+
show(panel.value);
|
|
56
|
+
return () => {};
|
|
57
|
+
}
|
|
58
|
+
const duration = 1400;
|
|
59
|
+
let start;
|
|
60
|
+
let handle = 0;
|
|
61
|
+
const tick = (now) => {
|
|
62
|
+
start ??= now;
|
|
63
|
+
const t = Math.min(1, (now - start) / duration);
|
|
64
|
+
show(panel.value * easeOut(t));
|
|
65
|
+
if (t < 1) handle = requestAnimationFrame(tick);
|
|
66
|
+
};
|
|
67
|
+
handle = requestAnimationFrame(tick);
|
|
68
|
+
return () => cancelAnimationFrame(handle);
|
|
69
|
+
}
|
|
70
|
+
function sparkline(values, c, width, height) {
|
|
71
|
+
const min = Math.min(...values);
|
|
72
|
+
const max = Math.max(...values);
|
|
73
|
+
const span = max - min || 1;
|
|
74
|
+
const step = width / (values.length - 1);
|
|
75
|
+
const points = values.map((v, i) => `${(i * step).toFixed(1)},${(height - 2 - (v - min) / span * (height - 4)).toFixed(1)}`);
|
|
76
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
77
|
+
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
78
|
+
svg.setAttribute("width", "100%");
|
|
79
|
+
svg.setAttribute("height", String(height));
|
|
80
|
+
svg.setAttribute("preserveAspectRatio", "none");
|
|
81
|
+
svg.setAttribute("aria-hidden", "true");
|
|
82
|
+
const fill = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
|
|
83
|
+
fill.setAttribute("points", `0,${height} ${points.join(" ")} ${width},${height}`);
|
|
84
|
+
fill.setAttribute("fill", require_options.withAlpha(c.accent, .18));
|
|
85
|
+
const line = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
|
|
86
|
+
line.setAttribute("points", points.join(" "));
|
|
87
|
+
line.setAttribute("fill", "none");
|
|
88
|
+
line.setAttribute("stroke", c.accent);
|
|
89
|
+
line.setAttribute("stroke-width", "2");
|
|
90
|
+
line.setAttribute("stroke-linejoin", "round");
|
|
91
|
+
line.setAttribute("vector-effect", "non-scaling-stroke");
|
|
92
|
+
svg.append(fill, line);
|
|
93
|
+
return svg;
|
|
94
|
+
}
|
|
95
|
+
function mountKpi(body, panel, c, animate) {
|
|
96
|
+
body.classList.add("aigui-bs-kpi");
|
|
97
|
+
const row = el("div", { class: "aigui-bs-kpi-row" });
|
|
98
|
+
const value = el("span", { class: "aigui-bs-kpi-value" });
|
|
99
|
+
value.style.color = c.accent;
|
|
100
|
+
value.style.textShadow = `0 0 18px ${require_options.withAlpha(c.accent, .45)}`;
|
|
101
|
+
row.appendChild(value);
|
|
102
|
+
if (panel.unit) row.appendChild(el("span", { class: "aigui-bs-kpi-unit" }, panel.unit));
|
|
103
|
+
body.appendChild(row);
|
|
104
|
+
const meta = el("div", { class: "aigui-bs-kpi-meta" });
|
|
105
|
+
if (panel.delta !== void 0) {
|
|
106
|
+
const up = panel.delta >= 0;
|
|
107
|
+
const good = (panel.upIsGood ?? true) === up;
|
|
108
|
+
const delta = el("span", { class: "aigui-bs-kpi-delta" }, `${up ? "▲" : "▼"} ${require_options.formatNumber(Math.abs(panel.delta) * 100, 1)}%`);
|
|
109
|
+
delta.style.color = good ? c.good : c.bad;
|
|
110
|
+
meta.appendChild(delta);
|
|
111
|
+
}
|
|
112
|
+
if (panel.label) meta.appendChild(el("span", { class: "aigui-bs-kpi-label" }, panel.label));
|
|
113
|
+
if (meta.childNodes.length) body.appendChild(meta);
|
|
114
|
+
if (panel.trend) {
|
|
115
|
+
const holder = el("div", { class: "aigui-bs-kpi-spark" });
|
|
116
|
+
holder.appendChild(sparkline(panel.trend, c, 200, 36));
|
|
117
|
+
body.appendChild(holder);
|
|
118
|
+
}
|
|
119
|
+
return countUp(value, panel, animate);
|
|
120
|
+
}
|
|
121
|
+
function mountRank(body, panel, c, animate) {
|
|
122
|
+
body.classList.add("aigui-bs-rank");
|
|
123
|
+
const items = [...panel.items].sort((a, b) => b.value - a.value).slice(0, panel.top ?? 8);
|
|
124
|
+
const max = Math.max(...items.map((item) => item.value), 0) || 1;
|
|
125
|
+
const bars = [];
|
|
126
|
+
items.forEach((item, index) => {
|
|
127
|
+
const row = el("div", { class: "aigui-bs-rank-row" });
|
|
128
|
+
const rank = el("span", { class: "aigui-bs-rank-n" }, String(index + 1));
|
|
129
|
+
if (index < 3) rank.style.color = c.accent;
|
|
130
|
+
const name = el("span", { class: "aigui-bs-rank-name" }, item.name);
|
|
131
|
+
const track = el("span", { class: "aigui-bs-rank-track" });
|
|
132
|
+
track.style.background = c.track;
|
|
133
|
+
const fill = el("span", { class: "aigui-bs-rank-fill" });
|
|
134
|
+
const colour = index === 0 ? c.accent : c.series[index % (c.series.length - 1) + 1];
|
|
135
|
+
fill.style.background = `linear-gradient(90deg, ${require_options.withAlpha(colour, .55)}, ${colour})`;
|
|
136
|
+
fill.style.boxShadow = `0 0 10px ${require_options.withAlpha(colour, .5)}`;
|
|
137
|
+
const width = `${Math.max(2, Math.max(item.value, 0) / max * 100)}%`;
|
|
138
|
+
fill.style.width = animate ? "0%" : width;
|
|
139
|
+
track.appendChild(fill);
|
|
140
|
+
const value = el("span", { class: "aigui-bs-rank-value" }, `${require_options.formatNumber(item.value, Number.isInteger(item.value) ? 0 : 1)}${panel.unit ?? ""}`);
|
|
141
|
+
row.append(rank, name, track, value);
|
|
142
|
+
body.appendChild(row);
|
|
143
|
+
bars.push({
|
|
144
|
+
fill,
|
|
145
|
+
width
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
if (!animate || typeof requestAnimationFrame !== "function") return () => {};
|
|
149
|
+
const handle = requestAnimationFrame(() => requestAnimationFrame(() => bars.forEach(({ fill, width }) => fill.style.width = width)));
|
|
150
|
+
return () => cancelAnimationFrame(handle);
|
|
151
|
+
}
|
|
152
|
+
/** Keep a chart the size of its panel. */
|
|
153
|
+
function follow(host, chart) {
|
|
154
|
+
if (typeof ResizeObserver === "undefined") return () => {};
|
|
155
|
+
const observer = new ResizeObserver(() => chart.resize());
|
|
156
|
+
observer.observe(host);
|
|
157
|
+
return () => observer.disconnect();
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Run `fn` once `body` has a size.
|
|
161
|
+
*
|
|
162
|
+
* The reconciler may call mount before the host is in the document, and ECharts initialised on
|
|
163
|
+
* a 0×0 element warns and draws nothing until the first resize. Waiting a frame when the size is
|
|
164
|
+
* not there yet costs nothing visible and keeps the console quiet.
|
|
165
|
+
*/
|
|
166
|
+
function whenSized(body, fn) {
|
|
167
|
+
if (body.clientWidth > 0 || typeof requestAnimationFrame !== "function") {
|
|
168
|
+
fn();
|
|
169
|
+
return () => {};
|
|
170
|
+
}
|
|
171
|
+
const handle = requestAnimationFrame(fn);
|
|
172
|
+
return () => cancelAnimationFrame(handle);
|
|
173
|
+
}
|
|
174
|
+
function mountChart(body, option, theme) {
|
|
175
|
+
let chart;
|
|
176
|
+
let unfollow;
|
|
177
|
+
const cancel = whenSized(body, () => {
|
|
178
|
+
chart = (0, echarts_core.init)(body, theme === "dark" ? "dark" : void 0, { renderer: "canvas" });
|
|
179
|
+
chart.setOption(option);
|
|
180
|
+
unfollow = follow(body, chart);
|
|
181
|
+
});
|
|
182
|
+
return () => {
|
|
183
|
+
cancel();
|
|
184
|
+
unfollow?.();
|
|
185
|
+
chart?.dispose();
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function mountGl(body, build, theme, fallback) {
|
|
189
|
+
let chart;
|
|
190
|
+
let unfollow;
|
|
191
|
+
let cancel;
|
|
192
|
+
let disposed = false;
|
|
193
|
+
loadGl().then(() => {
|
|
194
|
+
if (disposed) return;
|
|
195
|
+
cancel = whenSized(body, () => {
|
|
196
|
+
if (disposed) return;
|
|
197
|
+
chart = (0, echarts_core.init)(body, theme === "dark" ? "dark" : void 0, { renderer: "canvas" });
|
|
198
|
+
chart.setOption(build());
|
|
199
|
+
unfollow = follow(body, chart);
|
|
200
|
+
});
|
|
201
|
+
}).catch(() => {
|
|
202
|
+
if (disposed) return;
|
|
203
|
+
chart?.dispose();
|
|
204
|
+
chart = void 0;
|
|
205
|
+
fallback();
|
|
206
|
+
});
|
|
207
|
+
return () => {
|
|
208
|
+
disposed = true;
|
|
209
|
+
cancel?.();
|
|
210
|
+
unfollow?.();
|
|
211
|
+
chart?.dispose();
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function note(body, text) {
|
|
215
|
+
body.replaceChildren(el("div", { class: "aigui-bs-note" }, text));
|
|
216
|
+
}
|
|
217
|
+
function mountPanel(panel, definition, c, animate) {
|
|
218
|
+
const node = el("section", {
|
|
219
|
+
class: "aigui-bs-panel",
|
|
220
|
+
"data-aigui-bigscreen-panel": panel.kind
|
|
221
|
+
});
|
|
222
|
+
node.style.gridColumn = `span ${Math.min(panel.span ?? 4, definition.columns)}`;
|
|
223
|
+
node.style.borderColor = require_options.withAlpha(c.accent, definition.theme === "dark" ? .22 : .18);
|
|
224
|
+
if (panel.title) {
|
|
225
|
+
const head = el("header", { class: "aigui-bs-panel-title" }, panel.title);
|
|
226
|
+
const mark = el("i", { class: "aigui-bs-panel-mark" });
|
|
227
|
+
mark.style.background = c.accent;
|
|
228
|
+
mark.style.boxShadow = `0 0 8px ${c.accent}`;
|
|
229
|
+
head.prepend(mark);
|
|
230
|
+
node.appendChild(head);
|
|
231
|
+
}
|
|
232
|
+
const body = el("div", { class: "aigui-bs-panel-body" });
|
|
233
|
+
const height = panel.height ?? DEFAULT_HEIGHT[panel.kind];
|
|
234
|
+
if (height) body.style.height = `${height}px`;
|
|
235
|
+
node.appendChild(body);
|
|
236
|
+
let destroy = () => {};
|
|
237
|
+
try {
|
|
238
|
+
switch (panel.kind) {
|
|
239
|
+
case "kpi":
|
|
240
|
+
destroy = mountKpi(body, panel, c, animate);
|
|
241
|
+
break;
|
|
242
|
+
case "rank":
|
|
243
|
+
destroy = mountRank(body, panel, c, animate);
|
|
244
|
+
break;
|
|
245
|
+
case "gauge":
|
|
246
|
+
destroy = mountChart(body, require_options.gaugeOption(panel, c, animate), definition.theme);
|
|
247
|
+
break;
|
|
248
|
+
case "chart":
|
|
249
|
+
destroy = mountChart(body, require_options.chartOption(panel, c, animate), definition.theme);
|
|
250
|
+
break;
|
|
251
|
+
case "chart3d":
|
|
252
|
+
destroy = mountGl(body, () => require_options.chart3dOption(panel, c, animate), definition.theme, () => note(body, "3D panels need echarts-gl and WebGL."));
|
|
253
|
+
break;
|
|
254
|
+
case "globe":
|
|
255
|
+
destroy = mountGl(body, () => require_options.globeOption(panel, c, animate, require_options.globeTexture(c, definition.theme)), definition.theme, () => note(body, "Globe panels need echarts-gl and WebGL."));
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
note(body, "Panel could not be drawn.");
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
node,
|
|
263
|
+
destroy
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/** Build the whole screen into `host`, returning the teardown the reconciler will call. */
|
|
267
|
+
function mountScreen(host, definition, animate) {
|
|
268
|
+
const c = require_options.palette(definition);
|
|
269
|
+
host.setAttribute("data-aigui-bigscreen", definition.theme);
|
|
270
|
+
host.style.setProperty("--aigui-bs-accent", c.accent);
|
|
271
|
+
host.style.setProperty("--aigui-bs-text", c.text);
|
|
272
|
+
host.style.setProperty("--aigui-bs-muted", c.muted);
|
|
273
|
+
if (definition.title || definition.subtitle) {
|
|
274
|
+
const head = el("header", { class: "aigui-bs-head" });
|
|
275
|
+
if (definition.title) {
|
|
276
|
+
const title = el("h2", { class: "aigui-bs-title" }, definition.title);
|
|
277
|
+
title.style.textShadow = definition.theme === "dark" ? `0 0 24px ${require_options.withAlpha(c.accent, .35)}` : "none";
|
|
278
|
+
head.appendChild(title);
|
|
279
|
+
}
|
|
280
|
+
if (definition.subtitle) head.appendChild(el("p", { class: "aigui-bs-subtitle" }, definition.subtitle));
|
|
281
|
+
const rule = el("div", { class: "aigui-bs-rule" });
|
|
282
|
+
rule.style.background = `linear-gradient(90deg, transparent, ${c.accent}, transparent)`;
|
|
283
|
+
head.appendChild(rule);
|
|
284
|
+
host.appendChild(head);
|
|
285
|
+
}
|
|
286
|
+
const grid = el("div", { class: "aigui-bs-grid" });
|
|
287
|
+
grid.style.gridTemplateColumns = `repeat(${definition.columns}, minmax(0, 1fr))`;
|
|
288
|
+
host.appendChild(grid);
|
|
289
|
+
const mounted = definition.panels.map((panel) => mountPanel(panel, definition, c, animate));
|
|
290
|
+
for (const { node } of mounted) grid.appendChild(node);
|
|
291
|
+
return () => {
|
|
292
|
+
for (const { destroy } of mounted) destroy();
|
|
293
|
+
host.replaceChildren();
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
//#endregion
|
|
298
|
+
exports.mountScreen = mountScreen
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { chart3dOption, chartOption, formatNumber, gaugeOption, globeOption, globeTexture, palette, withAlpha } from "./options-C087Peeh.js";
|
|
2
|
+
import { BarChart, EffectScatterChart, FunnelChart, GaugeChart, HeatmapChart, LineChart, LinesChart, PieChart, RadarChart, ScatterChart } from "echarts/charts";
|
|
3
|
+
import { DatasetComponent, GridComponent, LegendComponent, PolarComponent, RadarComponent, TitleComponent, TooltipComponent, VisualMapComponent } from "echarts/components";
|
|
4
|
+
import { init, use } from "echarts/core";
|
|
5
|
+
import { CanvasRenderer } from "echarts/renderers";
|
|
6
|
+
|
|
7
|
+
//#region src/mount.ts
|
|
8
|
+
use([
|
|
9
|
+
BarChart,
|
|
10
|
+
LineChart,
|
|
11
|
+
PieChart,
|
|
12
|
+
ScatterChart,
|
|
13
|
+
GaugeChart,
|
|
14
|
+
RadarChart,
|
|
15
|
+
EffectScatterChart,
|
|
16
|
+
LinesChart,
|
|
17
|
+
FunnelChart,
|
|
18
|
+
HeatmapChart,
|
|
19
|
+
DatasetComponent,
|
|
20
|
+
GridComponent,
|
|
21
|
+
LegendComponent,
|
|
22
|
+
PolarComponent,
|
|
23
|
+
RadarComponent,
|
|
24
|
+
TitleComponent,
|
|
25
|
+
TooltipComponent,
|
|
26
|
+
VisualMapComponent,
|
|
27
|
+
CanvasRenderer
|
|
28
|
+
]);
|
|
29
|
+
/** Memoized `echarts-gl` side-effect import, shared across every screen on the page. */
|
|
30
|
+
let glReady = null;
|
|
31
|
+
const loadGl = () => glReady ??= import("echarts-gl");
|
|
32
|
+
const DEFAULT_HEIGHT = {
|
|
33
|
+
kpi: 0,
|
|
34
|
+
gauge: 180,
|
|
35
|
+
rank: 220,
|
|
36
|
+
chart: 240,
|
|
37
|
+
chart3d: 280,
|
|
38
|
+
globe: 300
|
|
39
|
+
};
|
|
40
|
+
const easeOut = (t) => 1 - Math.pow(1 - t, 3);
|
|
41
|
+
function el(tag, attrs = {}, text) {
|
|
42
|
+
const node = document.createElement(tag);
|
|
43
|
+
for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, value);
|
|
44
|
+
if (text !== void 0) node.textContent = text;
|
|
45
|
+
return node;
|
|
46
|
+
}
|
|
47
|
+
/** Count a number up from zero, returning the cancel. */
|
|
48
|
+
function countUp(node, panel, animate) {
|
|
49
|
+
const decimals = panel.decimals ?? 0;
|
|
50
|
+
const show = (value) => {
|
|
51
|
+
node.textContent = `${panel.prefix ?? ""}${formatNumber(value, decimals)}`;
|
|
52
|
+
};
|
|
53
|
+
if (!animate || typeof requestAnimationFrame !== "function") {
|
|
54
|
+
show(panel.value);
|
|
55
|
+
return () => {};
|
|
56
|
+
}
|
|
57
|
+
const duration = 1400;
|
|
58
|
+
let start;
|
|
59
|
+
let handle = 0;
|
|
60
|
+
const tick = (now) => {
|
|
61
|
+
start ??= now;
|
|
62
|
+
const t = Math.min(1, (now - start) / duration);
|
|
63
|
+
show(panel.value * easeOut(t));
|
|
64
|
+
if (t < 1) handle = requestAnimationFrame(tick);
|
|
65
|
+
};
|
|
66
|
+
handle = requestAnimationFrame(tick);
|
|
67
|
+
return () => cancelAnimationFrame(handle);
|
|
68
|
+
}
|
|
69
|
+
function sparkline(values, c, width, height) {
|
|
70
|
+
const min = Math.min(...values);
|
|
71
|
+
const max = Math.max(...values);
|
|
72
|
+
const span = max - min || 1;
|
|
73
|
+
const step = width / (values.length - 1);
|
|
74
|
+
const points = values.map((v, i) => `${(i * step).toFixed(1)},${(height - 2 - (v - min) / span * (height - 4)).toFixed(1)}`);
|
|
75
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
76
|
+
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
77
|
+
svg.setAttribute("width", "100%");
|
|
78
|
+
svg.setAttribute("height", String(height));
|
|
79
|
+
svg.setAttribute("preserveAspectRatio", "none");
|
|
80
|
+
svg.setAttribute("aria-hidden", "true");
|
|
81
|
+
const fill = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
|
|
82
|
+
fill.setAttribute("points", `0,${height} ${points.join(" ")} ${width},${height}`);
|
|
83
|
+
fill.setAttribute("fill", withAlpha(c.accent, .18));
|
|
84
|
+
const line = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
|
|
85
|
+
line.setAttribute("points", points.join(" "));
|
|
86
|
+
line.setAttribute("fill", "none");
|
|
87
|
+
line.setAttribute("stroke", c.accent);
|
|
88
|
+
line.setAttribute("stroke-width", "2");
|
|
89
|
+
line.setAttribute("stroke-linejoin", "round");
|
|
90
|
+
line.setAttribute("vector-effect", "non-scaling-stroke");
|
|
91
|
+
svg.append(fill, line);
|
|
92
|
+
return svg;
|
|
93
|
+
}
|
|
94
|
+
function mountKpi(body, panel, c, animate) {
|
|
95
|
+
body.classList.add("aigui-bs-kpi");
|
|
96
|
+
const row = el("div", { class: "aigui-bs-kpi-row" });
|
|
97
|
+
const value = el("span", { class: "aigui-bs-kpi-value" });
|
|
98
|
+
value.style.color = c.accent;
|
|
99
|
+
value.style.textShadow = `0 0 18px ${withAlpha(c.accent, .45)}`;
|
|
100
|
+
row.appendChild(value);
|
|
101
|
+
if (panel.unit) row.appendChild(el("span", { class: "aigui-bs-kpi-unit" }, panel.unit));
|
|
102
|
+
body.appendChild(row);
|
|
103
|
+
const meta = el("div", { class: "aigui-bs-kpi-meta" });
|
|
104
|
+
if (panel.delta !== void 0) {
|
|
105
|
+
const up = panel.delta >= 0;
|
|
106
|
+
const good = (panel.upIsGood ?? true) === up;
|
|
107
|
+
const delta = el("span", { class: "aigui-bs-kpi-delta" }, `${up ? "▲" : "▼"} ${formatNumber(Math.abs(panel.delta) * 100, 1)}%`);
|
|
108
|
+
delta.style.color = good ? c.good : c.bad;
|
|
109
|
+
meta.appendChild(delta);
|
|
110
|
+
}
|
|
111
|
+
if (panel.label) meta.appendChild(el("span", { class: "aigui-bs-kpi-label" }, panel.label));
|
|
112
|
+
if (meta.childNodes.length) body.appendChild(meta);
|
|
113
|
+
if (panel.trend) {
|
|
114
|
+
const holder = el("div", { class: "aigui-bs-kpi-spark" });
|
|
115
|
+
holder.appendChild(sparkline(panel.trend, c, 200, 36));
|
|
116
|
+
body.appendChild(holder);
|
|
117
|
+
}
|
|
118
|
+
return countUp(value, panel, animate);
|
|
119
|
+
}
|
|
120
|
+
function mountRank(body, panel, c, animate) {
|
|
121
|
+
body.classList.add("aigui-bs-rank");
|
|
122
|
+
const items = [...panel.items].sort((a, b) => b.value - a.value).slice(0, panel.top ?? 8);
|
|
123
|
+
const max = Math.max(...items.map((item) => item.value), 0) || 1;
|
|
124
|
+
const bars = [];
|
|
125
|
+
items.forEach((item, index) => {
|
|
126
|
+
const row = el("div", { class: "aigui-bs-rank-row" });
|
|
127
|
+
const rank = el("span", { class: "aigui-bs-rank-n" }, String(index + 1));
|
|
128
|
+
if (index < 3) rank.style.color = c.accent;
|
|
129
|
+
const name = el("span", { class: "aigui-bs-rank-name" }, item.name);
|
|
130
|
+
const track = el("span", { class: "aigui-bs-rank-track" });
|
|
131
|
+
track.style.background = c.track;
|
|
132
|
+
const fill = el("span", { class: "aigui-bs-rank-fill" });
|
|
133
|
+
const colour = index === 0 ? c.accent : c.series[index % (c.series.length - 1) + 1];
|
|
134
|
+
fill.style.background = `linear-gradient(90deg, ${withAlpha(colour, .55)}, ${colour})`;
|
|
135
|
+
fill.style.boxShadow = `0 0 10px ${withAlpha(colour, .5)}`;
|
|
136
|
+
const width = `${Math.max(2, Math.max(item.value, 0) / max * 100)}%`;
|
|
137
|
+
fill.style.width = animate ? "0%" : width;
|
|
138
|
+
track.appendChild(fill);
|
|
139
|
+
const value = el("span", { class: "aigui-bs-rank-value" }, `${formatNumber(item.value, Number.isInteger(item.value) ? 0 : 1)}${panel.unit ?? ""}`);
|
|
140
|
+
row.append(rank, name, track, value);
|
|
141
|
+
body.appendChild(row);
|
|
142
|
+
bars.push({
|
|
143
|
+
fill,
|
|
144
|
+
width
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
if (!animate || typeof requestAnimationFrame !== "function") return () => {};
|
|
148
|
+
const handle = requestAnimationFrame(() => requestAnimationFrame(() => bars.forEach(({ fill, width }) => fill.style.width = width)));
|
|
149
|
+
return () => cancelAnimationFrame(handle);
|
|
150
|
+
}
|
|
151
|
+
/** Keep a chart the size of its panel. */
|
|
152
|
+
function follow(host, chart) {
|
|
153
|
+
if (typeof ResizeObserver === "undefined") return () => {};
|
|
154
|
+
const observer = new ResizeObserver(() => chart.resize());
|
|
155
|
+
observer.observe(host);
|
|
156
|
+
return () => observer.disconnect();
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Run `fn` once `body` has a size.
|
|
160
|
+
*
|
|
161
|
+
* The reconciler may call mount before the host is in the document, and ECharts initialised on
|
|
162
|
+
* a 0×0 element warns and draws nothing until the first resize. Waiting a frame when the size is
|
|
163
|
+
* not there yet costs nothing visible and keeps the console quiet.
|
|
164
|
+
*/
|
|
165
|
+
function whenSized(body, fn) {
|
|
166
|
+
if (body.clientWidth > 0 || typeof requestAnimationFrame !== "function") {
|
|
167
|
+
fn();
|
|
168
|
+
return () => {};
|
|
169
|
+
}
|
|
170
|
+
const handle = requestAnimationFrame(fn);
|
|
171
|
+
return () => cancelAnimationFrame(handle);
|
|
172
|
+
}
|
|
173
|
+
function mountChart(body, option, theme) {
|
|
174
|
+
let chart;
|
|
175
|
+
let unfollow;
|
|
176
|
+
const cancel = whenSized(body, () => {
|
|
177
|
+
chart = init(body, theme === "dark" ? "dark" : void 0, { renderer: "canvas" });
|
|
178
|
+
chart.setOption(option);
|
|
179
|
+
unfollow = follow(body, chart);
|
|
180
|
+
});
|
|
181
|
+
return () => {
|
|
182
|
+
cancel();
|
|
183
|
+
unfollow?.();
|
|
184
|
+
chart?.dispose();
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function mountGl(body, build, theme, fallback) {
|
|
188
|
+
let chart;
|
|
189
|
+
let unfollow;
|
|
190
|
+
let cancel;
|
|
191
|
+
let disposed = false;
|
|
192
|
+
loadGl().then(() => {
|
|
193
|
+
if (disposed) return;
|
|
194
|
+
cancel = whenSized(body, () => {
|
|
195
|
+
if (disposed) return;
|
|
196
|
+
chart = init(body, theme === "dark" ? "dark" : void 0, { renderer: "canvas" });
|
|
197
|
+
chart.setOption(build());
|
|
198
|
+
unfollow = follow(body, chart);
|
|
199
|
+
});
|
|
200
|
+
}).catch(() => {
|
|
201
|
+
if (disposed) return;
|
|
202
|
+
chart?.dispose();
|
|
203
|
+
chart = void 0;
|
|
204
|
+
fallback();
|
|
205
|
+
});
|
|
206
|
+
return () => {
|
|
207
|
+
disposed = true;
|
|
208
|
+
cancel?.();
|
|
209
|
+
unfollow?.();
|
|
210
|
+
chart?.dispose();
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function note(body, text) {
|
|
214
|
+
body.replaceChildren(el("div", { class: "aigui-bs-note" }, text));
|
|
215
|
+
}
|
|
216
|
+
function mountPanel(panel, definition, c, animate) {
|
|
217
|
+
const node = el("section", {
|
|
218
|
+
class: "aigui-bs-panel",
|
|
219
|
+
"data-aigui-bigscreen-panel": panel.kind
|
|
220
|
+
});
|
|
221
|
+
node.style.gridColumn = `span ${Math.min(panel.span ?? 4, definition.columns)}`;
|
|
222
|
+
node.style.borderColor = withAlpha(c.accent, definition.theme === "dark" ? .22 : .18);
|
|
223
|
+
if (panel.title) {
|
|
224
|
+
const head = el("header", { class: "aigui-bs-panel-title" }, panel.title);
|
|
225
|
+
const mark = el("i", { class: "aigui-bs-panel-mark" });
|
|
226
|
+
mark.style.background = c.accent;
|
|
227
|
+
mark.style.boxShadow = `0 0 8px ${c.accent}`;
|
|
228
|
+
head.prepend(mark);
|
|
229
|
+
node.appendChild(head);
|
|
230
|
+
}
|
|
231
|
+
const body = el("div", { class: "aigui-bs-panel-body" });
|
|
232
|
+
const height = panel.height ?? DEFAULT_HEIGHT[panel.kind];
|
|
233
|
+
if (height) body.style.height = `${height}px`;
|
|
234
|
+
node.appendChild(body);
|
|
235
|
+
let destroy = () => {};
|
|
236
|
+
try {
|
|
237
|
+
switch (panel.kind) {
|
|
238
|
+
case "kpi":
|
|
239
|
+
destroy = mountKpi(body, panel, c, animate);
|
|
240
|
+
break;
|
|
241
|
+
case "rank":
|
|
242
|
+
destroy = mountRank(body, panel, c, animate);
|
|
243
|
+
break;
|
|
244
|
+
case "gauge":
|
|
245
|
+
destroy = mountChart(body, gaugeOption(panel, c, animate), definition.theme);
|
|
246
|
+
break;
|
|
247
|
+
case "chart":
|
|
248
|
+
destroy = mountChart(body, chartOption(panel, c, animate), definition.theme);
|
|
249
|
+
break;
|
|
250
|
+
case "chart3d":
|
|
251
|
+
destroy = mountGl(body, () => chart3dOption(panel, c, animate), definition.theme, () => note(body, "3D panels need echarts-gl and WebGL."));
|
|
252
|
+
break;
|
|
253
|
+
case "globe":
|
|
254
|
+
destroy = mountGl(body, () => globeOption(panel, c, animate, globeTexture(c, definition.theme)), definition.theme, () => note(body, "Globe panels need echarts-gl and WebGL."));
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
} catch {
|
|
258
|
+
note(body, "Panel could not be drawn.");
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
node,
|
|
262
|
+
destroy
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/** Build the whole screen into `host`, returning the teardown the reconciler will call. */
|
|
266
|
+
function mountScreen(host, definition, animate) {
|
|
267
|
+
const c = palette(definition);
|
|
268
|
+
host.setAttribute("data-aigui-bigscreen", definition.theme);
|
|
269
|
+
host.style.setProperty("--aigui-bs-accent", c.accent);
|
|
270
|
+
host.style.setProperty("--aigui-bs-text", c.text);
|
|
271
|
+
host.style.setProperty("--aigui-bs-muted", c.muted);
|
|
272
|
+
if (definition.title || definition.subtitle) {
|
|
273
|
+
const head = el("header", { class: "aigui-bs-head" });
|
|
274
|
+
if (definition.title) {
|
|
275
|
+
const title = el("h2", { class: "aigui-bs-title" }, definition.title);
|
|
276
|
+
title.style.textShadow = definition.theme === "dark" ? `0 0 24px ${withAlpha(c.accent, .35)}` : "none";
|
|
277
|
+
head.appendChild(title);
|
|
278
|
+
}
|
|
279
|
+
if (definition.subtitle) head.appendChild(el("p", { class: "aigui-bs-subtitle" }, definition.subtitle));
|
|
280
|
+
const rule = el("div", { class: "aigui-bs-rule" });
|
|
281
|
+
rule.style.background = `linear-gradient(90deg, transparent, ${c.accent}, transparent)`;
|
|
282
|
+
head.appendChild(rule);
|
|
283
|
+
host.appendChild(head);
|
|
284
|
+
}
|
|
285
|
+
const grid = el("div", { class: "aigui-bs-grid" });
|
|
286
|
+
grid.style.gridTemplateColumns = `repeat(${definition.columns}, minmax(0, 1fr))`;
|
|
287
|
+
host.appendChild(grid);
|
|
288
|
+
const mounted = definition.panels.map((panel) => mountPanel(panel, definition, c, animate));
|
|
289
|
+
for (const { node } of mounted) grid.appendChild(node);
|
|
290
|
+
return () => {
|
|
291
|
+
for (const { destroy } of mounted) destroy();
|
|
292
|
+
host.replaceChildren();
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
//#endregion
|
|
297
|
+
export { mountScreen };
|