@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.
@@ -0,0 +1,415 @@
1
+ //#region src/palette.ts
2
+ /**
3
+ * Two palettes, one per theme.
4
+ *
5
+ * The dark one is the data-wall look — deep navy, cyan accent, a little glow; the light one is
6
+ * the same screen on a white page. The accent can be overridden per screen, and the series
7
+ * colours start from it so a screen reads as one thing rather than a collage.
8
+ */
9
+ function palette(definition) {
10
+ const accent = definition.accent ?? (definition.theme === "dark" ? "#22d3ee" : "#0369a1");
11
+ return definition.theme === "dark" ? {
12
+ accent,
13
+ text: "#e2e8f0",
14
+ muted: "#94a3b8",
15
+ good: "#34d399",
16
+ bad: "#fb7185",
17
+ warn: "#fbbf24",
18
+ track: "rgba(148,163,184,0.18)",
19
+ series: [
20
+ accent,
21
+ "#a78bfa",
22
+ "#34d399",
23
+ "#fbbf24",
24
+ "#fb7185",
25
+ "#60a5fa",
26
+ "#f472b6",
27
+ "#2dd4bf"
28
+ ],
29
+ gridLine: "rgba(148,163,184,0.16)"
30
+ } : {
31
+ accent,
32
+ text: "#1e293b",
33
+ muted: "#64748b",
34
+ good: "#059669",
35
+ bad: "#e11d48",
36
+ warn: "#d97706",
37
+ track: "rgba(100,116,139,0.16)",
38
+ series: [
39
+ accent,
40
+ "#7c3aed",
41
+ "#059669",
42
+ "#d97706",
43
+ "#e11d48",
44
+ "#2563eb",
45
+ "#db2777",
46
+ "#0d9488"
47
+ ],
48
+ gridLine: "rgba(100,116,139,0.2)"
49
+ };
50
+ }
51
+ /** `#rrggbb` with an alpha, for glows and fills. */
52
+ function withAlpha(hex, alpha) {
53
+ const h = hex.replace("#", "");
54
+ const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
55
+ const n = Number.parseInt(full, 16);
56
+ return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${alpha})`;
57
+ }
58
+
59
+ //#endregion
60
+ //#region src/options.ts
61
+ /**
62
+ * ECharts options built from panels.
63
+ *
64
+ * Pure functions of the panel and the palette, so they can be tested without a canvas and so
65
+ * the same panel draws the same chart on every screen.
66
+ */
67
+ const FONT = "ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'PingFang SC', 'Noto Sans CJK SC', sans-serif";
68
+ /** Format a number for a label: thousands grouped, up to `decimals` places. */
69
+ function formatNumber(value, decimals = 0) {
70
+ return value.toLocaleString("en-US", {
71
+ minimumFractionDigits: decimals,
72
+ maximumFractionDigits: decimals
73
+ });
74
+ }
75
+ /** The colour a gauge shows at `fraction` of its range. */
76
+ function gaugeColour(fraction, thresholds, c) {
77
+ if (!thresholds) return c.accent;
78
+ if (fraction >= thresholds[1]) return c.bad;
79
+ if (fraction >= thresholds[0]) return c.warn;
80
+ return c.good;
81
+ }
82
+ function gaugeOption(panel, c, animate) {
83
+ const max = panel.max ?? 100;
84
+ const fraction = Math.max(0, Math.min(1, panel.value / max));
85
+ const colour = gaugeColour(fraction, panel.thresholds, c);
86
+ const ring = panel.style === "ring";
87
+ return {
88
+ animation: animate,
89
+ animationDuration: 1400,
90
+ animationEasing: "cubicOut",
91
+ series: [{
92
+ type: "gauge",
93
+ startAngle: ring ? 90 : 210,
94
+ endAngle: ring ? -270 : -30,
95
+ min: 0,
96
+ max,
97
+ radius: "92%",
98
+ center: ["50%", ring ? "50%" : "58%"],
99
+ progress: {
100
+ show: true,
101
+ width: ring ? 14 : 16,
102
+ roundCap: true,
103
+ itemStyle: {
104
+ color: colour,
105
+ shadowBlur: 12,
106
+ shadowColor: withAlpha(colour, .5)
107
+ }
108
+ },
109
+ pointer: {
110
+ show: !ring,
111
+ length: "60%",
112
+ width: 4,
113
+ itemStyle: { color: colour }
114
+ },
115
+ axisLine: { lineStyle: {
116
+ width: ring ? 14 : 16,
117
+ color: [[1, c.track]]
118
+ } },
119
+ axisTick: { show: false },
120
+ splitLine: {
121
+ show: !ring,
122
+ length: 8,
123
+ lineStyle: {
124
+ color: c.muted,
125
+ width: 1
126
+ }
127
+ },
128
+ axisLabel: {
129
+ show: !ring,
130
+ color: c.muted,
131
+ fontSize: 10,
132
+ distance: 18,
133
+ fontFamily: FONT
134
+ },
135
+ anchor: {
136
+ show: !ring,
137
+ size: 10,
138
+ itemStyle: { color: colour }
139
+ },
140
+ title: { show: false },
141
+ detail: {
142
+ valueAnimation: animate,
143
+ offsetCenter: [0, ring ? 0 : "28%"],
144
+ fontSize: ring ? 28 : 24,
145
+ fontWeight: 700,
146
+ fontFamily: FONT,
147
+ color: c.text,
148
+ formatter: (value) => `${formatNumber(value, Number.isInteger(panel.value) ? 0 : 1)}${panel.unit ?? ""}`
149
+ },
150
+ data: [{ value: panel.value }]
151
+ }]
152
+ };
153
+ }
154
+ /**
155
+ * The screen's palette laid over whatever the model wrote.
156
+ *
157
+ * The model's option wins on anything it sets; this only fills in the colours, the font, the
158
+ * transparent background and the entrance animation a screen wants, so a plain `bar` series
159
+ * still looks like it belongs.
160
+ */
161
+ function chartOption(panel, c, animate) {
162
+ const axisStyle = {
163
+ axisLine: { lineStyle: { color: c.gridLine } },
164
+ axisLabel: {
165
+ color: c.muted,
166
+ fontFamily: FONT
167
+ },
168
+ splitLine: { lineStyle: { color: c.gridLine } }
169
+ };
170
+ const withAxisDefaults = (axis) => {
171
+ if (Array.isArray(axis)) return axis.map((a) => ({
172
+ ...axisStyle,
173
+ ...a
174
+ }));
175
+ if (axis && typeof axis === "object") return {
176
+ ...axisStyle,
177
+ ...axis
178
+ };
179
+ return axis;
180
+ };
181
+ const option = panel.option;
182
+ return {
183
+ backgroundColor: "transparent",
184
+ color: c.series,
185
+ textStyle: {
186
+ color: c.text,
187
+ fontFamily: FONT
188
+ },
189
+ animation: animate,
190
+ animationDuration: 1200,
191
+ animationEasing: "cubicOut",
192
+ grid: {
193
+ left: 40,
194
+ right: 16,
195
+ top: 28,
196
+ bottom: 28,
197
+ containLabel: true
198
+ },
199
+ legend: { textStyle: { color: c.muted } },
200
+ tooltip: { trigger: "axis" },
201
+ ...option,
202
+ ...option.xAxis !== void 0 ? { xAxis: withAxisDefaults(option.xAxis) } : {},
203
+ ...option.yAxis !== void 0 ? { yAxis: withAxisDefaults(option.yAxis) } : {}
204
+ };
205
+ }
206
+ function chart3dOption(panel, c, animate) {
207
+ const values = panel.data.map((p) => p[2]);
208
+ const max = Math.max(...values);
209
+ const min = Math.min(...values);
210
+ const axis = (categories, name) => ({
211
+ type: categories ? "category" : "value",
212
+ data: categories,
213
+ name: "",
214
+ axisLine: { lineStyle: { color: c.gridLine } },
215
+ axisLabel: {
216
+ color: c.muted,
217
+ fontFamily: FONT
218
+ },
219
+ splitLine: { lineStyle: { color: c.gridLine } },
220
+ axisPointer: { show: false },
221
+ nameTextStyle: { color: c.muted },
222
+ id: name
223
+ });
224
+ const series = {
225
+ type: panel.type,
226
+ data: panel.data,
227
+ shading: "lambert",
228
+ emphasis: { itemStyle: { color: c.text } }
229
+ };
230
+ if (panel.type === "bar3D") series.bevelSize = .2;
231
+ if (panel.type === "scatter3D") series.symbolSize = 9;
232
+ if (panel.type === "line3D") series.lineStyle = {
233
+ width: 3,
234
+ color: c.accent
235
+ };
236
+ if (panel.type === "surface") series.wireframe = {
237
+ show: true,
238
+ lineStyle: { color: withAlpha(c.accent, .35) }
239
+ };
240
+ return {
241
+ backgroundColor: "transparent",
242
+ textStyle: { fontFamily: FONT },
243
+ tooltip: {},
244
+ visualMap: {
245
+ show: false,
246
+ min,
247
+ max,
248
+ dimension: 2,
249
+ inRange: { color: [
250
+ withAlpha(c.accent, .55),
251
+ c.accent,
252
+ c.series[1]
253
+ ] }
254
+ },
255
+ xAxis3D: axis(panel.xAxis, "x"),
256
+ yAxis3D: axis(panel.yAxis, "y"),
257
+ zAxis3D: {
258
+ type: "value",
259
+ name: "",
260
+ axisLine: { lineStyle: { color: c.gridLine } },
261
+ axisLabel: {
262
+ color: c.muted,
263
+ fontFamily: FONT
264
+ },
265
+ splitLine: { lineStyle: { color: c.gridLine } }
266
+ },
267
+ grid3D: {
268
+ boxWidth: 100,
269
+ boxDepth: 80,
270
+ boxHeight: 60,
271
+ axisPointer: { show: false },
272
+ light: {
273
+ main: {
274
+ intensity: 1.3,
275
+ shadow: false,
276
+ alpha: 40,
277
+ beta: 30
278
+ },
279
+ ambient: { intensity: .5 }
280
+ },
281
+ viewControl: {
282
+ autoRotate: animate && panel.rotate !== false,
283
+ autoRotateSpeed: 8,
284
+ distance: 190,
285
+ alpha: 24,
286
+ beta: 35,
287
+ projection: "perspective"
288
+ },
289
+ environment: "none"
290
+ },
291
+ series: [series]
292
+ };
293
+ }
294
+ /**
295
+ * A globe without a satellite photograph.
296
+ *
297
+ * ECharts' globe wants a texture, and the usual one is an image fetched from somewhere — which a
298
+ * page must not do on a model's say-so. So the texture is painted here: a deep sphere with a
299
+ * graticule, which is what a data wall's globe looks like anyway. Arcs get a moving trail.
300
+ */
301
+ function globeTexture(c, theme) {
302
+ if (typeof document === "undefined") return void 0;
303
+ const canvas = document.createElement("canvas");
304
+ canvas.width = 1024;
305
+ canvas.height = 512;
306
+ const ctx = canvas.getContext("2d");
307
+ if (!ctx) return void 0;
308
+ ctx.fillStyle = theme === "dark" ? "#0b1a33" : "#dbe7f5";
309
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
310
+ ctx.strokeStyle = withAlpha(c.accent, theme === "dark" ? .28 : .35);
311
+ ctx.lineWidth = 1.2;
312
+ for (let lng = 0; lng < 360; lng += 15) {
313
+ const x = lng / 360 * canvas.width;
314
+ ctx.beginPath();
315
+ ctx.moveTo(x, 0);
316
+ ctx.lineTo(x, canvas.height);
317
+ ctx.stroke();
318
+ }
319
+ for (let lat = 15; lat < 180; lat += 15) {
320
+ const y = lat / 180 * canvas.height;
321
+ ctx.beginPath();
322
+ ctx.moveTo(0, y);
323
+ ctx.lineTo(canvas.width, y);
324
+ ctx.stroke();
325
+ }
326
+ ctx.strokeStyle = withAlpha(c.accent, .6);
327
+ ctx.lineWidth = 2;
328
+ ctx.beginPath();
329
+ ctx.moveTo(0, canvas.height / 2);
330
+ ctx.lineTo(canvas.width, canvas.height / 2);
331
+ ctx.stroke();
332
+ return canvas.toDataURL("image/png");
333
+ }
334
+ function globeOption(panel, c, animate, texture) {
335
+ const arcs = (panel.arcs ?? []).map((arc) => ({
336
+ coords: [arc.from, arc.to],
337
+ name: arc.label
338
+ }));
339
+ const points = (panel.points ?? []).map((point) => ({
340
+ name: point.label,
341
+ value: [
342
+ point.coord[0],
343
+ point.coord[1],
344
+ 0,
345
+ point.value ?? 1
346
+ ]
347
+ }));
348
+ const maxValue = Math.max(1, ...points.map((p) => p.value[3]));
349
+ return {
350
+ backgroundColor: "transparent",
351
+ globe: {
352
+ baseTexture: texture,
353
+ shading: "color",
354
+ environment: "none",
355
+ globeRadius: 78,
356
+ atmosphere: {
357
+ show: true,
358
+ color: c.accent,
359
+ glowPower: 5,
360
+ innerGlowPower: 2,
361
+ offset: 4
362
+ },
363
+ light: {
364
+ ambient: { intensity: 1 },
365
+ main: { intensity: .2 }
366
+ },
367
+ viewControl: {
368
+ autoRotate: animate && panel.rotate !== false,
369
+ autoRotateSpeed: 3,
370
+ distance: 150,
371
+ targetCoord: arcs[0]?.coords[0] ?? points[0]?.value.slice(0, 2)
372
+ }
373
+ },
374
+ series: [{
375
+ type: "lines3D",
376
+ coordinateSystem: "globe",
377
+ blendMode: "lighter",
378
+ lineStyle: {
379
+ width: 2.5,
380
+ color: c.series[1],
381
+ opacity: .9
382
+ },
383
+ effect: {
384
+ show: animate,
385
+ period: 3,
386
+ trailWidth: 4,
387
+ trailLength: .3,
388
+ trailOpacity: 1,
389
+ trailColor: "#ffffff"
390
+ },
391
+ data: arcs
392
+ }, {
393
+ type: "scatter3D",
394
+ coordinateSystem: "globe",
395
+ symbolSize: (value) => 6 + 10 * Math.sqrt((value[3] ?? 1) / maxValue),
396
+ itemStyle: {
397
+ color: c.series[1],
398
+ opacity: .9
399
+ },
400
+ label: {
401
+ show: true,
402
+ formatter: "{b}",
403
+ color: c.text,
404
+ fontSize: 11,
405
+ fontFamily: FONT,
406
+ backgroundColor: "transparent",
407
+ distance: 6
408
+ },
409
+ data: points
410
+ }]
411
+ };
412
+ }
413
+
414
+ //#endregion
415
+ export { chart3dOption, chartOption, formatNumber, gaugeColour, gaugeOption, globeOption, globeTexture, palette, withAlpha };
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@ai-gui/plugin-bigscreen",
3
+ "version": "0.34.0",
4
+ "description": "Animated data walls for AIGUI — counting KPIs, sweeping gauges, growing ranks, self-drawing charts, turning 3D bars and globes, on a grid the model lays out.",
5
+ "keywords": [
6
+ "llm",
7
+ "ai",
8
+ "dashboard",
9
+ "big-screen",
10
+ "data-wall",
11
+ "echarts",
12
+ "echarts-gl",
13
+ "3d",
14
+ "animation",
15
+ "plugin",
16
+ "aigui"
17
+ ],
18
+ "license": "MIT",
19
+ "author": "Liang Li <ll_faw@hotmail.com>",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/liliang-cn/aigui.git",
23
+ "directory": "packages/plugin-bigscreen"
24
+ },
25
+ "homepage": "https://github.com/liliang-cn/aigui#readme",
26
+ "bugs": "https://github.com/liliang-cn/aigui/issues",
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "main": "./dist/index.cjs",
33
+ "module": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "import": {
38
+ "types": "./dist/index.d.ts",
39
+ "default": "./dist/index.js"
40
+ },
41
+ "require": {
42
+ "types": "./dist/index.d.cts",
43
+ "default": "./dist/index.cjs"
44
+ }
45
+ }
46
+ },
47
+ "files": [
48
+ "dist",
49
+ "README.md",
50
+ "LICENSE",
51
+ "CHANGELOG.md"
52
+ ],
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "dependencies": {
57
+ "echarts": "^5.5.1",
58
+ "@ai-gui/core": "0.34.0"
59
+ },
60
+ "peerDependencies": {
61
+ "echarts-gl": "^2.0.9"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "echarts-gl": {
65
+ "optional": true
66
+ }
67
+ },
68
+ "devDependencies": {
69
+ "echarts-gl": "^2.0.9"
70
+ },
71
+ "scripts": {
72
+ "build": "tsdown",
73
+ "test": "pnpm --dir ../.. exec vitest run --project plugin-bigscreen",
74
+ "typecheck": "tsc --noEmit"
75
+ }
76
+ }