@ai-gui/plugin-bigscreen 0.36.3 → 0.37.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/CHANGELOG.md +64 -0
- package/README.md +20 -3
- package/dist/index.cjs +478 -14
- package/dist/index.d.cts +634 -4
- package/dist/index.d.ts +634 -4
- package/dist/index.js +416 -6
- package/dist/{mount-DPyPP9Ha.cjs → mount-BXSCLn67.cjs} +164 -27
- package/dist/{mount-CbzNLMoI.js → mount-ChcoxgHd.js} +147 -10
- package/dist/positions-CHtRx0fb.cjs +1933 -0
- package/dist/positions-CN8aP4-S.js +1621 -0
- package/package.json +2 -2
- package/dist/options-BpHnWRpI.cjs +0 -499
- package/dist/options-C087Peeh.js +0 -415
|
@@ -0,0 +1,1933 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
//#region rolldown:runtime
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
|
|
26
|
+
//#region src/palette.ts
|
|
27
|
+
/**
|
|
28
|
+
* Two palettes, one per theme.
|
|
29
|
+
*
|
|
30
|
+
* The dark one is the data-wall look — deep navy, cyan accent, a little glow; the light one is
|
|
31
|
+
* the same screen on a white page. The accent can be overridden per screen, and the series
|
|
32
|
+
* colours start from it so a screen reads as one thing rather than a collage.
|
|
33
|
+
*/
|
|
34
|
+
function palette(definition) {
|
|
35
|
+
const accent = definition.accent ?? (definition.theme === "dark" ? "#22d3ee" : "#0369a1");
|
|
36
|
+
return definition.theme === "dark" ? {
|
|
37
|
+
accent,
|
|
38
|
+
text: "#e2e8f0",
|
|
39
|
+
muted: "#94a3b8",
|
|
40
|
+
good: "#34d399",
|
|
41
|
+
bad: "#fb7185",
|
|
42
|
+
warn: "#fbbf24",
|
|
43
|
+
track: "rgba(148,163,184,0.18)",
|
|
44
|
+
series: [
|
|
45
|
+
accent,
|
|
46
|
+
"#a78bfa",
|
|
47
|
+
"#34d399",
|
|
48
|
+
"#fbbf24",
|
|
49
|
+
"#fb7185",
|
|
50
|
+
"#60a5fa",
|
|
51
|
+
"#f472b6",
|
|
52
|
+
"#2dd4bf"
|
|
53
|
+
],
|
|
54
|
+
gridLine: "rgba(148,163,184,0.16)"
|
|
55
|
+
} : {
|
|
56
|
+
accent,
|
|
57
|
+
text: "#1e293b",
|
|
58
|
+
muted: "#64748b",
|
|
59
|
+
good: "#059669",
|
|
60
|
+
bad: "#e11d48",
|
|
61
|
+
warn: "#d97706",
|
|
62
|
+
track: "rgba(100,116,139,0.16)",
|
|
63
|
+
series: [
|
|
64
|
+
accent,
|
|
65
|
+
"#7c3aed",
|
|
66
|
+
"#059669",
|
|
67
|
+
"#d97706",
|
|
68
|
+
"#e11d48",
|
|
69
|
+
"#2563eb",
|
|
70
|
+
"#db2777",
|
|
71
|
+
"#0d9488"
|
|
72
|
+
],
|
|
73
|
+
gridLine: "rgba(100,116,139,0.2)"
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** `#rrggbb` with an alpha, for glows and fills. */
|
|
77
|
+
function withAlpha(hex, alpha) {
|
|
78
|
+
const h = hex.replace("#", "");
|
|
79
|
+
const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
|
|
80
|
+
const n = Number.parseInt(full, 16);
|
|
81
|
+
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${alpha})`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/options.ts
|
|
86
|
+
/**
|
|
87
|
+
* ECharts options built from panels.
|
|
88
|
+
*
|
|
89
|
+
* Pure functions of the panel and the palette, so they can be tested without a canvas and so
|
|
90
|
+
* the same panel draws the same chart on every screen.
|
|
91
|
+
*/
|
|
92
|
+
/** The one font every panel writes in, exported so the panels in their own files share it. */
|
|
93
|
+
const FONT = "ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'PingFang SC', 'Noto Sans CJK SC', sans-serif";
|
|
94
|
+
/** Format a number for a label: thousands grouped, up to `decimals` places. */
|
|
95
|
+
function formatNumber(value, decimals = 0) {
|
|
96
|
+
return value.toLocaleString("en-US", {
|
|
97
|
+
minimumFractionDigits: decimals,
|
|
98
|
+
maximumFractionDigits: decimals
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** The colour a gauge shows at `fraction` of its range. */
|
|
102
|
+
function gaugeColour(fraction, thresholds, c) {
|
|
103
|
+
if (!thresholds) return c.accent;
|
|
104
|
+
if (fraction >= thresholds[1]) return c.bad;
|
|
105
|
+
if (fraction >= thresholds[0]) return c.warn;
|
|
106
|
+
return c.good;
|
|
107
|
+
}
|
|
108
|
+
function gaugeOption(panel, c, animate) {
|
|
109
|
+
const max = panel.max ?? 100;
|
|
110
|
+
const fraction = Math.max(0, Math.min(1, panel.value / max));
|
|
111
|
+
const colour = gaugeColour(fraction, panel.thresholds, c);
|
|
112
|
+
const ring = panel.style === "ring";
|
|
113
|
+
return {
|
|
114
|
+
animation: animate,
|
|
115
|
+
animationDuration: 1400,
|
|
116
|
+
animationEasing: "cubicOut",
|
|
117
|
+
series: [{
|
|
118
|
+
type: "gauge",
|
|
119
|
+
startAngle: ring ? 90 : 210,
|
|
120
|
+
endAngle: ring ? -270 : -30,
|
|
121
|
+
min: 0,
|
|
122
|
+
max,
|
|
123
|
+
radius: "92%",
|
|
124
|
+
center: ["50%", ring ? "50%" : "58%"],
|
|
125
|
+
progress: {
|
|
126
|
+
show: true,
|
|
127
|
+
width: ring ? 14 : 16,
|
|
128
|
+
roundCap: true,
|
|
129
|
+
itemStyle: {
|
|
130
|
+
color: colour,
|
|
131
|
+
shadowBlur: 12,
|
|
132
|
+
shadowColor: withAlpha(colour, .5)
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
pointer: {
|
|
136
|
+
show: !ring,
|
|
137
|
+
length: "60%",
|
|
138
|
+
width: 4,
|
|
139
|
+
itemStyle: { color: colour }
|
|
140
|
+
},
|
|
141
|
+
axisLine: { lineStyle: {
|
|
142
|
+
width: ring ? 14 : 16,
|
|
143
|
+
color: [[1, c.track]]
|
|
144
|
+
} },
|
|
145
|
+
axisTick: { show: false },
|
|
146
|
+
splitLine: {
|
|
147
|
+
show: !ring,
|
|
148
|
+
length: 8,
|
|
149
|
+
lineStyle: {
|
|
150
|
+
color: c.muted,
|
|
151
|
+
width: 1
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
axisLabel: {
|
|
155
|
+
show: !ring,
|
|
156
|
+
color: c.muted,
|
|
157
|
+
fontSize: 10,
|
|
158
|
+
distance: 18,
|
|
159
|
+
fontFamily: FONT
|
|
160
|
+
},
|
|
161
|
+
anchor: {
|
|
162
|
+
show: !ring,
|
|
163
|
+
size: 10,
|
|
164
|
+
itemStyle: { color: colour }
|
|
165
|
+
},
|
|
166
|
+
title: { show: false },
|
|
167
|
+
detail: {
|
|
168
|
+
valueAnimation: animate,
|
|
169
|
+
offsetCenter: [0, ring ? 0 : "28%"],
|
|
170
|
+
fontSize: ring ? 28 : 24,
|
|
171
|
+
fontWeight: 700,
|
|
172
|
+
fontFamily: FONT,
|
|
173
|
+
color: c.text,
|
|
174
|
+
formatter: (value) => `${formatNumber(value, Number.isInteger(panel.value) ? 0 : 1)}${panel.unit ?? ""}`
|
|
175
|
+
},
|
|
176
|
+
data: [{ value: panel.value }]
|
|
177
|
+
}]
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The screen's palette laid over whatever the model wrote.
|
|
182
|
+
*
|
|
183
|
+
* The model's option wins on anything it sets; this only fills in the colours, the font, the
|
|
184
|
+
* transparent background and the entrance animation a screen wants, so a plain `bar` series
|
|
185
|
+
* still looks like it belongs.
|
|
186
|
+
*/
|
|
187
|
+
function chartOption(panel, c, animate) {
|
|
188
|
+
const axisStyle = {
|
|
189
|
+
axisLine: { lineStyle: { color: c.gridLine } },
|
|
190
|
+
axisLabel: {
|
|
191
|
+
color: c.muted,
|
|
192
|
+
fontFamily: FONT
|
|
193
|
+
},
|
|
194
|
+
splitLine: { lineStyle: { color: c.gridLine } }
|
|
195
|
+
};
|
|
196
|
+
const withAxisDefaults = (axis) => {
|
|
197
|
+
if (Array.isArray(axis)) return axis.map((a) => ({
|
|
198
|
+
...axisStyle,
|
|
199
|
+
...a
|
|
200
|
+
}));
|
|
201
|
+
if (axis && typeof axis === "object") return {
|
|
202
|
+
...axisStyle,
|
|
203
|
+
...axis
|
|
204
|
+
};
|
|
205
|
+
return axis;
|
|
206
|
+
};
|
|
207
|
+
const option = panel.option;
|
|
208
|
+
return {
|
|
209
|
+
backgroundColor: "transparent",
|
|
210
|
+
color: c.series,
|
|
211
|
+
textStyle: {
|
|
212
|
+
color: c.text,
|
|
213
|
+
fontFamily: FONT
|
|
214
|
+
},
|
|
215
|
+
animation: animate,
|
|
216
|
+
animationDuration: 1200,
|
|
217
|
+
animationEasing: "cubicOut",
|
|
218
|
+
grid: {
|
|
219
|
+
left: 40,
|
|
220
|
+
right: 16,
|
|
221
|
+
top: 28,
|
|
222
|
+
bottom: 28,
|
|
223
|
+
containLabel: true
|
|
224
|
+
},
|
|
225
|
+
legend: { textStyle: { color: c.muted } },
|
|
226
|
+
tooltip: { trigger: "axis" },
|
|
227
|
+
...option,
|
|
228
|
+
...option.xAxis !== void 0 ? { xAxis: withAxisDefaults(option.xAxis) } : {},
|
|
229
|
+
...option.yAxis !== void 0 ? { yAxis: withAxisDefaults(option.yAxis) } : {}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function chart3dOption(panel, c, animate) {
|
|
233
|
+
const values = panel.data.map((p) => p[2]);
|
|
234
|
+
const max = Math.max(...values);
|
|
235
|
+
const min = Math.min(...values);
|
|
236
|
+
const axis = (categories, name) => ({
|
|
237
|
+
type: categories ? "category" : "value",
|
|
238
|
+
data: categories,
|
|
239
|
+
name: "",
|
|
240
|
+
axisLine: { lineStyle: { color: c.gridLine } },
|
|
241
|
+
axisLabel: {
|
|
242
|
+
color: c.muted,
|
|
243
|
+
fontFamily: FONT
|
|
244
|
+
},
|
|
245
|
+
splitLine: { lineStyle: { color: c.gridLine } },
|
|
246
|
+
axisPointer: { show: false },
|
|
247
|
+
nameTextStyle: { color: c.muted },
|
|
248
|
+
id: name
|
|
249
|
+
});
|
|
250
|
+
const series = {
|
|
251
|
+
type: panel.type,
|
|
252
|
+
data: panel.data,
|
|
253
|
+
shading: "lambert",
|
|
254
|
+
emphasis: { itemStyle: { color: c.text } }
|
|
255
|
+
};
|
|
256
|
+
if (panel.type === "bar3D") series.bevelSize = .2;
|
|
257
|
+
if (panel.type === "scatter3D") series.symbolSize = 9;
|
|
258
|
+
if (panel.type === "line3D") series.lineStyle = {
|
|
259
|
+
width: 3,
|
|
260
|
+
color: c.accent
|
|
261
|
+
};
|
|
262
|
+
if (panel.type === "surface") series.wireframe = {
|
|
263
|
+
show: true,
|
|
264
|
+
lineStyle: { color: withAlpha(c.accent, .35) }
|
|
265
|
+
};
|
|
266
|
+
return {
|
|
267
|
+
backgroundColor: "transparent",
|
|
268
|
+
textStyle: { fontFamily: FONT },
|
|
269
|
+
tooltip: {},
|
|
270
|
+
visualMap: {
|
|
271
|
+
show: false,
|
|
272
|
+
min,
|
|
273
|
+
max,
|
|
274
|
+
dimension: 2,
|
|
275
|
+
inRange: { color: [
|
|
276
|
+
withAlpha(c.accent, .55),
|
|
277
|
+
c.accent,
|
|
278
|
+
c.series[1]
|
|
279
|
+
] }
|
|
280
|
+
},
|
|
281
|
+
xAxis3D: axis(panel.xAxis, "x"),
|
|
282
|
+
yAxis3D: axis(panel.yAxis, "y"),
|
|
283
|
+
zAxis3D: {
|
|
284
|
+
type: "value",
|
|
285
|
+
name: "",
|
|
286
|
+
axisLine: { lineStyle: { color: c.gridLine } },
|
|
287
|
+
axisLabel: {
|
|
288
|
+
color: c.muted,
|
|
289
|
+
fontFamily: FONT
|
|
290
|
+
},
|
|
291
|
+
splitLine: { lineStyle: { color: c.gridLine } }
|
|
292
|
+
},
|
|
293
|
+
grid3D: {
|
|
294
|
+
boxWidth: 100,
|
|
295
|
+
boxDepth: 80,
|
|
296
|
+
boxHeight: 60,
|
|
297
|
+
axisPointer: { show: false },
|
|
298
|
+
light: {
|
|
299
|
+
main: {
|
|
300
|
+
intensity: 1.3,
|
|
301
|
+
shadow: false,
|
|
302
|
+
alpha: 40,
|
|
303
|
+
beta: 30
|
|
304
|
+
},
|
|
305
|
+
ambient: { intensity: .5 }
|
|
306
|
+
},
|
|
307
|
+
viewControl: {
|
|
308
|
+
autoRotate: animate && panel.rotate !== false,
|
|
309
|
+
autoRotateSpeed: 8,
|
|
310
|
+
distance: 190,
|
|
311
|
+
alpha: 24,
|
|
312
|
+
beta: 35,
|
|
313
|
+
projection: "perspective"
|
|
314
|
+
},
|
|
315
|
+
environment: "none"
|
|
316
|
+
},
|
|
317
|
+
series: [series]
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* A globe without a satellite photograph.
|
|
322
|
+
*
|
|
323
|
+
* ECharts' globe wants a texture, and the usual one is an image fetched from somewhere — which a
|
|
324
|
+
* page must not do on a model's say-so. So the texture is painted here: a deep sphere with a
|
|
325
|
+
* graticule, which is what a data wall's globe looks like anyway. A host that wants a real earth
|
|
326
|
+
* says so in `BigscreenOptions.globe` and `earth.ts` takes over; this stays the fallback.
|
|
327
|
+
*/
|
|
328
|
+
function globeTexture(c, theme) {
|
|
329
|
+
if (typeof document === "undefined") return void 0;
|
|
330
|
+
const canvas = document.createElement("canvas");
|
|
331
|
+
canvas.width = 1024;
|
|
332
|
+
canvas.height = 512;
|
|
333
|
+
const ctx = canvas.getContext("2d");
|
|
334
|
+
if (!ctx) return void 0;
|
|
335
|
+
ctx.fillStyle = theme === "dark" ? "#0b1a33" : "#dbe7f5";
|
|
336
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
337
|
+
ctx.strokeStyle = withAlpha(c.accent, theme === "dark" ? .28 : .35);
|
|
338
|
+
ctx.lineWidth = 1.2;
|
|
339
|
+
for (let lng = 0; lng < 360; lng += 15) {
|
|
340
|
+
const x = lng / 360 * canvas.width;
|
|
341
|
+
ctx.beginPath();
|
|
342
|
+
ctx.moveTo(x, 0);
|
|
343
|
+
ctx.lineTo(x, canvas.height);
|
|
344
|
+
ctx.stroke();
|
|
345
|
+
}
|
|
346
|
+
for (let lat = 15; lat < 180; lat += 15) {
|
|
347
|
+
const y = lat / 180 * canvas.height;
|
|
348
|
+
ctx.beginPath();
|
|
349
|
+
ctx.moveTo(0, y);
|
|
350
|
+
ctx.lineTo(canvas.width, y);
|
|
351
|
+
ctx.stroke();
|
|
352
|
+
}
|
|
353
|
+
ctx.strokeStyle = withAlpha(c.accent, .6);
|
|
354
|
+
ctx.lineWidth = 2;
|
|
355
|
+
ctx.beginPath();
|
|
356
|
+
ctx.moveTo(0, canvas.height / 2);
|
|
357
|
+
ctx.lineTo(canvas.width, canvas.height / 2);
|
|
358
|
+
ctx.stroke();
|
|
359
|
+
return canvas.toDataURL("image/png");
|
|
360
|
+
}
|
|
361
|
+
/** How many of a globe's points carry a written label; the rest are on the tooltip. */
|
|
362
|
+
const GLOBE_LABELS = 8;
|
|
363
|
+
/**
|
|
364
|
+
* The sphere itself, with and without a host-supplied earth.
|
|
365
|
+
*
|
|
366
|
+
* Without one, everything below the `skin` branch is the globe this plugin has always drawn: a
|
|
367
|
+
* flat-lit ball with a graticule on it, every point labelled. A wire grid needs no sun — there is
|
|
368
|
+
* nothing on it a shadow would tell you about — and `shading: "color"` with the light turned down
|
|
369
|
+
* is what keeps it evenly readable while it turns.
|
|
370
|
+
*
|
|
371
|
+
* With one, the ball is a planet: lambert (or the host's choice) so the terminator falls where the
|
|
372
|
+
* sun actually is, an atmosphere at the rim, and labels only on the points worth reading from
|
|
373
|
+
* across a room. `postEffect` stays off — bloom and depth of field on one of six panels is a
|
|
374
|
+
* frame budget spent on the wrong thing.
|
|
375
|
+
*/
|
|
376
|
+
function globeOption(panel, c, animate, texture, skin) {
|
|
377
|
+
const arcs = (panel.arcs ?? []).map((arc) => ({
|
|
378
|
+
coords: [arc.from, arc.to],
|
|
379
|
+
name: arc.label
|
|
380
|
+
}));
|
|
381
|
+
const points = (panel.points ?? []).map((point) => ({
|
|
382
|
+
name: point.label,
|
|
383
|
+
value: [
|
|
384
|
+
point.coord[0],
|
|
385
|
+
point.coord[1],
|
|
386
|
+
0,
|
|
387
|
+
point.value ?? 1
|
|
388
|
+
]
|
|
389
|
+
}));
|
|
390
|
+
const maxValue = Math.max(1, ...points.map((p) => p.value[3]));
|
|
391
|
+
const shading = skin ? skin.shading ?? "lambert" : "color";
|
|
392
|
+
const globe = {
|
|
393
|
+
baseTexture: texture,
|
|
394
|
+
shading,
|
|
395
|
+
environment: "none",
|
|
396
|
+
globeRadius: 78,
|
|
397
|
+
atmosphere: {
|
|
398
|
+
show: skin ? skin.atmosphere !== false : true,
|
|
399
|
+
color: c.accent,
|
|
400
|
+
glowPower: 5,
|
|
401
|
+
innerGlowPower: 2,
|
|
402
|
+
offset: 4
|
|
403
|
+
},
|
|
404
|
+
light: skin ? {
|
|
405
|
+
ambient: { intensity: skin.light?.ambient ?? .5 },
|
|
406
|
+
main: {
|
|
407
|
+
intensity: skin.light?.intensity ?? 1.1,
|
|
408
|
+
shadow: false,
|
|
409
|
+
time: skin.light?.time ?? new Date()
|
|
410
|
+
}
|
|
411
|
+
} : {
|
|
412
|
+
ambient: { intensity: 1 },
|
|
413
|
+
main: { intensity: .2 }
|
|
414
|
+
},
|
|
415
|
+
viewControl: {
|
|
416
|
+
autoRotate: animate && panel.rotate !== false,
|
|
417
|
+
autoRotateSpeed: 3,
|
|
418
|
+
distance: 150,
|
|
419
|
+
targetCoord: arcs[0]?.coords[0] ?? points[0]?.value.slice(0, 2)
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
if (skin) {
|
|
423
|
+
globe.postEffect = { enable: false };
|
|
424
|
+
if (skin.heightTexture) globe.heightTexture = skin.heightTexture;
|
|
425
|
+
if (shading === "realistic") globe.realisticMaterial = {
|
|
426
|
+
roughness: .85,
|
|
427
|
+
metalness: 0
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
const named = new Set([...points].sort((a, b) => b.value[3] - a.value[3]).slice(0, GLOBE_LABELS).map((p) => p.name));
|
|
431
|
+
return {
|
|
432
|
+
backgroundColor: "transparent",
|
|
433
|
+
...skin ? { tooltip: {
|
|
434
|
+
trigger: "item",
|
|
435
|
+
confine: true,
|
|
436
|
+
formatter: "{b}"
|
|
437
|
+
} } : {},
|
|
438
|
+
globe,
|
|
439
|
+
series: [{
|
|
440
|
+
type: "lines3D",
|
|
441
|
+
coordinateSystem: "globe",
|
|
442
|
+
blendMode: "lighter",
|
|
443
|
+
lineStyle: {
|
|
444
|
+
width: 2.5,
|
|
445
|
+
color: c.series[1],
|
|
446
|
+
opacity: .9
|
|
447
|
+
},
|
|
448
|
+
effect: {
|
|
449
|
+
show: animate,
|
|
450
|
+
period: 3,
|
|
451
|
+
trailWidth: 4,
|
|
452
|
+
trailLength: .3,
|
|
453
|
+
trailOpacity: 1,
|
|
454
|
+
trailColor: "#ffffff"
|
|
455
|
+
},
|
|
456
|
+
data: arcs
|
|
457
|
+
}, {
|
|
458
|
+
type: "scatter3D",
|
|
459
|
+
coordinateSystem: "globe",
|
|
460
|
+
symbolSize: (value) => 6 + 10 * Math.sqrt((value[3] ?? 1) / maxValue),
|
|
461
|
+
itemStyle: {
|
|
462
|
+
color: c.series[1],
|
|
463
|
+
opacity: .9
|
|
464
|
+
},
|
|
465
|
+
label: {
|
|
466
|
+
show: true,
|
|
467
|
+
formatter: skin ? (params) => params.name && named.has(params.name) ? params.name : "" : "{b}",
|
|
468
|
+
color: c.text,
|
|
469
|
+
fontSize: 11,
|
|
470
|
+
fontFamily: FONT,
|
|
471
|
+
backgroundColor: "transparent",
|
|
472
|
+
distance: 6
|
|
473
|
+
},
|
|
474
|
+
data: points
|
|
475
|
+
}]
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
//#endregion
|
|
480
|
+
//#region src/earth.ts
|
|
481
|
+
/**
|
|
482
|
+
* The planet a globe panel is drawn on.
|
|
483
|
+
*
|
|
484
|
+
* echarts-gl's globe samples one equirectangular image: 2:1, longitude -180..180 left to right,
|
|
485
|
+
* latitude 90..-90 top to bottom. A host that has a satellite photograph hands it over as
|
|
486
|
+
* `baseTexture` and this file does nothing; a host that has only country outlines hands over
|
|
487
|
+
* GeoJSON and this file paints them, because a page must not fetch a map on a model's say-so and
|
|
488
|
+
* because the same outlines have to come out in two palettes.
|
|
489
|
+
*
|
|
490
|
+
* The canvas is 2048x1024 rather than the graticule's 1024x512: a border is one pixel wide, and
|
|
491
|
+
* at half this width the coastline of anywhere smaller than France stops being a shape.
|
|
492
|
+
*/
|
|
493
|
+
const WIDTH = 2048;
|
|
494
|
+
const HEIGHT = 1024;
|
|
495
|
+
function earthColours(c, theme, skin) {
|
|
496
|
+
const dark = theme === "dark";
|
|
497
|
+
return {
|
|
498
|
+
ocean: skin?.ocean ?? (dark ? "#0b1a33" : "#dbe7f5"),
|
|
499
|
+
land: skin?.land ?? (dark ? "#334155" : "#f8fafc"),
|
|
500
|
+
border: skin?.border ?? withAlpha(c.accent, dark ? .55 : .75)
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
/** The rings of a geometry, as a list of polygons; anything that is not a polygon is skipped. */
|
|
504
|
+
function polygonsOf(geometry) {
|
|
505
|
+
if (!geometry || !Array.isArray(geometry.coordinates)) return [];
|
|
506
|
+
if (geometry.type === "Polygon") return [geometry.coordinates];
|
|
507
|
+
if (geometry.type === "MultiPolygon") return geometry.coordinates;
|
|
508
|
+
return [];
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* One ring, in canvas pixels, shifted by whole worlds.
|
|
512
|
+
*
|
|
513
|
+
* Longitude is unwrapped as it goes: a step of more than half the world is the antimeridian, not
|
|
514
|
+
* a country, and drawing it literally paints a bar all the way across the Pacific — which is how
|
|
515
|
+
* Russia and Fiji ruin an otherwise correct map. Carrying an offset instead keeps the ring
|
|
516
|
+
* continuous past ±180, and the caller draws it three times, one world left and one right, so the
|
|
517
|
+
* part that has left the canvas comes back on the other side.
|
|
518
|
+
*/
|
|
519
|
+
function traceRing(ctx, ring, shift) {
|
|
520
|
+
if (ring.length < 2) return;
|
|
521
|
+
let offset = 0;
|
|
522
|
+
let previous = ring[0][0];
|
|
523
|
+
ring.forEach((point, index) => {
|
|
524
|
+
const lon = point[0];
|
|
525
|
+
const step = lon - previous;
|
|
526
|
+
if (step > 180) offset -= 360;
|
|
527
|
+
else if (step < -180) offset += 360;
|
|
528
|
+
previous = lon;
|
|
529
|
+
const x = (lon + offset + 180) / 360 * WIDTH + shift;
|
|
530
|
+
const y = (90 - point[1]) / 180 * HEIGHT;
|
|
531
|
+
if (index === 0) ctx.moveTo(x, y);
|
|
532
|
+
else ctx.lineTo(x, y);
|
|
533
|
+
});
|
|
534
|
+
ctx.closePath();
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Rasterise a world onto a 2:1 canvas and hand it over as a data URL.
|
|
538
|
+
*
|
|
539
|
+
* A data URL rather than the canvas element: that is the path echarts-gl treats as an image to
|
|
540
|
+
* decode, and it involves no request. A canvas element handed to `baseTexture` is a white ball.
|
|
541
|
+
*
|
|
542
|
+
* Rings are filled even-odd so that a country drawn as an outer ring plus holes — Lesotho inside
|
|
543
|
+
* South Africa, the lakes inside Canada — comes out with the holes in it whichever way round the
|
|
544
|
+
* source wound them.
|
|
545
|
+
*/
|
|
546
|
+
function countriesTexture(countries, colours) {
|
|
547
|
+
if (typeof document === "undefined") return void 0;
|
|
548
|
+
const canvas = document.createElement("canvas");
|
|
549
|
+
canvas.width = WIDTH;
|
|
550
|
+
canvas.height = HEIGHT;
|
|
551
|
+
const ctx = canvas.getContext("2d");
|
|
552
|
+
if (!ctx) return void 0;
|
|
553
|
+
ctx.fillStyle = colours.ocean;
|
|
554
|
+
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
|
555
|
+
ctx.fillStyle = colours.land;
|
|
556
|
+
ctx.strokeStyle = colours.border;
|
|
557
|
+
ctx.lineWidth = 1;
|
|
558
|
+
ctx.lineJoin = "round";
|
|
559
|
+
for (const feature of countries.features ?? []) for (const polygon of polygonsOf(feature?.geometry)) for (const shift of [
|
|
560
|
+
-WIDTH,
|
|
561
|
+
0,
|
|
562
|
+
WIDTH
|
|
563
|
+
]) {
|
|
564
|
+
ctx.beginPath();
|
|
565
|
+
for (const ring of polygon) traceRing(ctx, ring, shift);
|
|
566
|
+
ctx.fill("evenodd");
|
|
567
|
+
ctx.stroke();
|
|
568
|
+
}
|
|
569
|
+
return canvas.toDataURL("image/png");
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* The texture a globe panel wears, by the host's precedence.
|
|
573
|
+
*
|
|
574
|
+
* `baseTexture` wins because a host that has a photograph has already decided; `countries` is
|
|
575
|
+
* next because outlines a host bundled are still the host's map; and with neither there is the
|
|
576
|
+
* graticule, which is what this plugin has always drawn and what every consumer that never heard
|
|
577
|
+
* of a `globe` option keeps getting.
|
|
578
|
+
*/
|
|
579
|
+
function earthTexture(c, theme, skin) {
|
|
580
|
+
if (skin?.baseTexture) return skin.baseTexture;
|
|
581
|
+
if (skin?.countries) return countriesTexture(skin.countries, earthColours(c, theme, skin)) ?? globeTexture(c, theme);
|
|
582
|
+
return globeTexture(c, theme);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
//#endregion
|
|
586
|
+
//#region src/timeline.ts
|
|
587
|
+
/**
|
|
588
|
+
* A timeline of claims, one lane per source.
|
|
589
|
+
*
|
|
590
|
+
* The panel exists for the line nobody else draws: two outlets said things that cannot both be
|
|
591
|
+
* true, and the red segment between their two points is the whole point of the picture. So the
|
|
592
|
+
* lanes stay in the order they were given (a reader compares row against row, and a chart that
|
|
593
|
+
* reorders them silently answers a different question), time runs across, and every claim is one
|
|
594
|
+
* point that can be clicked back to the page it came from.
|
|
595
|
+
*
|
|
596
|
+
* Pure: the option is a function of the panel and the palette, so it can be tested without a
|
|
597
|
+
* canvas and so the same claims draw the same picture on every screen.
|
|
598
|
+
*/
|
|
599
|
+
/** How many claims carry a written label; the rest are on the tooltip. */
|
|
600
|
+
const TIMELINE_LABELS = 12;
|
|
601
|
+
/** The default point, in pixels, for a claim with no `value`. */
|
|
602
|
+
const DEFAULT_SYMBOL = 12;
|
|
603
|
+
const HOUR = 60 * 60 * 1e3;
|
|
604
|
+
/** How far into the window a claim has to be before its label is written on the other side. */
|
|
605
|
+
const LABEL_FLIPS_AT = .62;
|
|
606
|
+
const escapeHtml$1 = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
607
|
+
/**
|
|
608
|
+
* The window drawn, in epoch milliseconds.
|
|
609
|
+
*
|
|
610
|
+
* The panel's own `from` and `to` win where they are given. Otherwise it is the claims' own range
|
|
611
|
+
* with 5% on each side, because a point exactly on the axis reads as clipped rather than as first.
|
|
612
|
+
* A single claim has no range to take a fraction of, so it gets an hour either way — enough for
|
|
613
|
+
* the point to sit somewhere rather than in the middle of nothing.
|
|
614
|
+
*/
|
|
615
|
+
function timelineWindow(panel) {
|
|
616
|
+
const times = panel.items.map((item) => Date.parse(item.at));
|
|
617
|
+
const low = panel.from === void 0 ? void 0 : Date.parse(panel.from);
|
|
618
|
+
const high = panel.to === void 0 ? void 0 : Date.parse(panel.to);
|
|
619
|
+
if (low !== void 0 && high !== void 0) return [low, high];
|
|
620
|
+
const min = Math.min(...times);
|
|
621
|
+
const max = Math.max(...times);
|
|
622
|
+
const pad = (max - min) * .05 || HOUR;
|
|
623
|
+
return [low ?? min - pad, high ?? max + pad];
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Which claims have room for a label, as indexes into `panel.items`.
|
|
627
|
+
*
|
|
628
|
+
* A label is drawn beside its point, on its own lane, so what decides whether it can be read is
|
|
629
|
+
* the empty time on either side of it within that lane — not how important the claim is and not
|
|
630
|
+
* what order it was written in. Twelve labels on a wall panel is about where a reader stops
|
|
631
|
+
* reading them anyway, and the rest are one hover away.
|
|
632
|
+
*/
|
|
633
|
+
function spacedItems(panel, max = TIMELINE_LABELS) {
|
|
634
|
+
const byLane = new Map();
|
|
635
|
+
panel.items.forEach((item, index) => {
|
|
636
|
+
const lane = byLane.get(item.lane) ?? [];
|
|
637
|
+
lane.push({
|
|
638
|
+
index,
|
|
639
|
+
at: Date.parse(item.at)
|
|
640
|
+
});
|
|
641
|
+
byLane.set(item.lane, lane);
|
|
642
|
+
});
|
|
643
|
+
const room = new Map();
|
|
644
|
+
for (const lane of byLane.values()) {
|
|
645
|
+
const sorted = [...lane].sort((a, b) => a.at - b.at);
|
|
646
|
+
sorted.forEach((entry, i) => {
|
|
647
|
+
const before = i > 0 ? entry.at - sorted[i - 1].at : Number.POSITIVE_INFINITY;
|
|
648
|
+
const after = i < sorted.length - 1 ? sorted[i + 1].at - entry.at : Number.POSITIVE_INFINITY;
|
|
649
|
+
room.set(entry.index, Math.min(before, after));
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
return new Set([...room.entries()].sort((a, b) => b[1] - a[1] || a[0] - b[0]).slice(0, max).map(([index]) => index));
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* How tall a timeline of this many lanes has to be.
|
|
656
|
+
*
|
|
657
|
+
* 320 is the default panel, and it holds a handful of lanes comfortably. Past that the lanes get
|
|
658
|
+
* a floor of 28 pixels each: below that the points of two neighbouring outlets touch, and a
|
|
659
|
+
* contradiction drawn between them stops being a line between two rows.
|
|
660
|
+
*/
|
|
661
|
+
function timelineHeight(lanes) {
|
|
662
|
+
return Math.min(900, Math.max(320, 60 + lanes * 28));
|
|
663
|
+
}
|
|
664
|
+
/** How a link of each kind is drawn. `follows` is what an untyped link gets. */
|
|
665
|
+
const LINK_KINDS = {
|
|
666
|
+
contradicts: {
|
|
667
|
+
danger: true,
|
|
668
|
+
dotted: false,
|
|
669
|
+
arrow: false,
|
|
670
|
+
width: 2,
|
|
671
|
+
opacity: .95
|
|
672
|
+
},
|
|
673
|
+
follows: {
|
|
674
|
+
danger: false,
|
|
675
|
+
dotted: false,
|
|
676
|
+
arrow: true,
|
|
677
|
+
width: 1.4,
|
|
678
|
+
opacity: .6
|
|
679
|
+
},
|
|
680
|
+
same: {
|
|
681
|
+
danger: false,
|
|
682
|
+
dotted: true,
|
|
683
|
+
arrow: false,
|
|
684
|
+
width: 1.4,
|
|
685
|
+
opacity: .6
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
/** When a claim was made, written the way a wall is read: minutes, UTC, no locale surprises. */
|
|
689
|
+
function when(at) {
|
|
690
|
+
return `${new Date(at).toISOString().slice(0, 16).replace("T", " ")} UTC`;
|
|
691
|
+
}
|
|
692
|
+
function timelineOption(panel, c, animate) {
|
|
693
|
+
const [from, to] = timelineWindow(panel);
|
|
694
|
+
const laneName = new Map(panel.lanes.map((lane) => [lane.id, lane.name]));
|
|
695
|
+
const labelled = spacedItems(panel);
|
|
696
|
+
const maxValue = Math.max(0, ...panel.items.map((item) => item.value ?? 0));
|
|
697
|
+
const symbolSize = (value) => {
|
|
698
|
+
const raw = typeof value[2] === "number" ? value[2] : 0;
|
|
699
|
+
return raw > 0 && maxValue > 0 ? 8 + 12 * Math.sqrt(raw / maxValue) : DEFAULT_SYMBOL;
|
|
700
|
+
};
|
|
701
|
+
const series = panel.lanes.map((lane, laneIndex) => {
|
|
702
|
+
const colour = lane.color ?? c.series[laneIndex % c.series.length];
|
|
703
|
+
const data = panel.items.map((item, index) => ({
|
|
704
|
+
item,
|
|
705
|
+
index
|
|
706
|
+
})).filter(({ item }) => item.lane === lane.id).map(({ item, index }) => {
|
|
707
|
+
const at = Date.parse(item.at);
|
|
708
|
+
return {
|
|
709
|
+
name: item.label,
|
|
710
|
+
value: [
|
|
711
|
+
at,
|
|
712
|
+
lane.name,
|
|
713
|
+
item.value ?? 0
|
|
714
|
+
],
|
|
715
|
+
item,
|
|
716
|
+
label: {
|
|
717
|
+
show: labelled.has(index),
|
|
718
|
+
position: (at - from) / (to - from) > LABEL_FLIPS_AT ? "left" : "right"
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
});
|
|
722
|
+
return {
|
|
723
|
+
name: lane.name,
|
|
724
|
+
type: "scatter",
|
|
725
|
+
symbolSize,
|
|
726
|
+
itemStyle: {
|
|
727
|
+
color: colour,
|
|
728
|
+
borderColor: withAlpha(colour, .35),
|
|
729
|
+
borderWidth: 4
|
|
730
|
+
},
|
|
731
|
+
label: {
|
|
732
|
+
show: false,
|
|
733
|
+
position: "right",
|
|
734
|
+
distance: 8,
|
|
735
|
+
color: c.text,
|
|
736
|
+
fontSize: 11,
|
|
737
|
+
fontFamily: FONT,
|
|
738
|
+
formatter: "{b}"
|
|
739
|
+
},
|
|
740
|
+
emphasis: {
|
|
741
|
+
scale: 1.35,
|
|
742
|
+
label: { show: true }
|
|
743
|
+
},
|
|
744
|
+
animationDelay: (index) => index * 30,
|
|
745
|
+
data
|
|
746
|
+
};
|
|
747
|
+
});
|
|
748
|
+
const byId = new Map(panel.items.filter((item) => item.id !== void 0).map((item) => [item.id, item]));
|
|
749
|
+
const coord = (id) => {
|
|
750
|
+
const item = byId.get(id);
|
|
751
|
+
const name = item && laneName.get(item.lane);
|
|
752
|
+
return item && name !== void 0 ? [Date.parse(item.at), name] : void 0;
|
|
753
|
+
};
|
|
754
|
+
const links = (panel.links ?? []).flatMap((link) => {
|
|
755
|
+
const a = coord(link.from);
|
|
756
|
+
const b = coord(link.to);
|
|
757
|
+
if (!a || !b) return [];
|
|
758
|
+
const kind = link.kind ?? "follows";
|
|
759
|
+
const style = LINK_KINDS[kind];
|
|
760
|
+
return [{
|
|
761
|
+
coords: [a, b],
|
|
762
|
+
name: kind,
|
|
763
|
+
symbol: style.arrow ? ["none", "arrow"] : ["none", "none"],
|
|
764
|
+
symbolSize: 7,
|
|
765
|
+
lineStyle: {
|
|
766
|
+
color: style.danger ? c.bad : c.muted,
|
|
767
|
+
width: style.width,
|
|
768
|
+
type: style.dotted ? "dotted" : "solid",
|
|
769
|
+
opacity: style.opacity,
|
|
770
|
+
curveness: .15
|
|
771
|
+
}
|
|
772
|
+
}];
|
|
773
|
+
});
|
|
774
|
+
if (links.length) series.push({
|
|
775
|
+
name: "links",
|
|
776
|
+
type: "lines",
|
|
777
|
+
coordinateSystem: "cartesian2d",
|
|
778
|
+
z: 1,
|
|
779
|
+
polyline: false,
|
|
780
|
+
animationDuration: 1400,
|
|
781
|
+
data: links
|
|
782
|
+
});
|
|
783
|
+
const axisLine = { lineStyle: { color: c.gridLine } };
|
|
784
|
+
return {
|
|
785
|
+
backgroundColor: "transparent",
|
|
786
|
+
textStyle: {
|
|
787
|
+
color: c.text,
|
|
788
|
+
fontFamily: FONT
|
|
789
|
+
},
|
|
790
|
+
animation: animate,
|
|
791
|
+
animationDuration: 1200,
|
|
792
|
+
animationEasing: "cubicOut",
|
|
793
|
+
grid: {
|
|
794
|
+
left: 8,
|
|
795
|
+
right: 28,
|
|
796
|
+
top: 18,
|
|
797
|
+
bottom: 24,
|
|
798
|
+
containLabel: true
|
|
799
|
+
},
|
|
800
|
+
tooltip: {
|
|
801
|
+
trigger: "item",
|
|
802
|
+
confine: true,
|
|
803
|
+
formatter: (params) => {
|
|
804
|
+
const data = params.data;
|
|
805
|
+
const item = data?.item;
|
|
806
|
+
if (!item) return escapeHtml$1(String(params.seriesName ?? ""));
|
|
807
|
+
const rows = [`<span style="opacity:.7">${escapeHtml$1(params.seriesName ?? "")} · ${when(Date.parse(item.at))}</span>`, `<b>${escapeHtml$1(item.label)}</b>`];
|
|
808
|
+
if (item.detail) rows.push(`<span style="opacity:.85">${escapeHtml$1(item.detail)}</span>`);
|
|
809
|
+
return rows.join("<br/>");
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
xAxis: {
|
|
813
|
+
type: "time",
|
|
814
|
+
min: from,
|
|
815
|
+
max: to,
|
|
816
|
+
axisLine,
|
|
817
|
+
axisTick: { show: false },
|
|
818
|
+
axisLabel: {
|
|
819
|
+
color: c.muted,
|
|
820
|
+
fontFamily: FONT,
|
|
821
|
+
fontSize: 10,
|
|
822
|
+
hideOverlap: true
|
|
823
|
+
},
|
|
824
|
+
splitLine: {
|
|
825
|
+
show: true,
|
|
826
|
+
lineStyle: {
|
|
827
|
+
color: c.gridLine,
|
|
828
|
+
type: "dashed"
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
},
|
|
832
|
+
yAxis: {
|
|
833
|
+
type: "category",
|
|
834
|
+
data: panel.lanes.map((lane) => lane.name),
|
|
835
|
+
inverse: true,
|
|
836
|
+
axisLine,
|
|
837
|
+
axisTick: { show: false },
|
|
838
|
+
axisLabel: {
|
|
839
|
+
color: c.text,
|
|
840
|
+
fontFamily: FONT,
|
|
841
|
+
fontSize: 11
|
|
842
|
+
},
|
|
843
|
+
splitLine: {
|
|
844
|
+
show: true,
|
|
845
|
+
lineStyle: { color: c.gridLine }
|
|
846
|
+
},
|
|
847
|
+
splitArea: {
|
|
848
|
+
show: true,
|
|
849
|
+
areaStyle: { color: [withAlpha(c.muted, .06), "transparent"] }
|
|
850
|
+
}
|
|
851
|
+
},
|
|
852
|
+
series
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
//#endregion
|
|
857
|
+
//#region src/layout3d.ts
|
|
858
|
+
/**
|
|
859
|
+
* A spring–electrical layout in three dimensions.
|
|
860
|
+
*
|
|
861
|
+
* Fruchterman–Reingold's forces, one dimension further: every pair of entities pushes apart with
|
|
862
|
+
* `k²/d`, every edge pulls its ends together with `d²/k`, a weak spring holds the whole thing to
|
|
863
|
+
* the origin, and a falling temperature caps how far a node may move in one step so the graph
|
|
864
|
+
* cools into a shape instead of oscillating around one.
|
|
865
|
+
*
|
|
866
|
+
* Written here rather than taken from a dependency for two reasons. The panel needs positions in
|
|
867
|
+
* three dimensions and echarts-gl's own layouts are two-dimensional; and the layout has to be
|
|
868
|
+
* *steppable* — a few steps per animation frame — so the reader watches the graph pull itself
|
|
869
|
+
* apart rather than being handed a settled picture. A library that runs to convergence behind a
|
|
870
|
+
* promise cannot do that.
|
|
871
|
+
*
|
|
872
|
+
* Deterministic on purpose: the starting positions are a hash of the node ids on a sphere, so the
|
|
873
|
+
* same knowledge graph draws the same picture twice running, and a screenshot test means
|
|
874
|
+
* something. Nothing here calls `Math.random`.
|
|
875
|
+
*
|
|
876
|
+
* Complexity is O(n²) per step, which is why `layoutSteps` spends fewer steps on a bigger graph.
|
|
877
|
+
*/
|
|
878
|
+
/** The distance an edge settles at: the unit everything else here is measured in. */
|
|
879
|
+
const LAYOUT_SPRING = 1;
|
|
880
|
+
/**
|
|
881
|
+
* The pull to the origin, per unit of distance from it.
|
|
882
|
+
*
|
|
883
|
+
* Without it a knowledge graph with two unconnected components is unbounded: nothing attracts
|
|
884
|
+
* them to each other, the repulsion between them is never opposed, and one of them leaves the
|
|
885
|
+
* panel and keeps going.
|
|
886
|
+
*/
|
|
887
|
+
const LAYOUT_GRAVITY = .08;
|
|
888
|
+
/** How far a node may move in the first step, as a fraction of the graph's own radius. */
|
|
889
|
+
const HEAT = .12;
|
|
890
|
+
/** Below this the repulsion between two nodes in the same place would be infinite. */
|
|
891
|
+
const MIN_DISTANCE = .001;
|
|
892
|
+
const FNV_OFFSET = 2166136261;
|
|
893
|
+
const FNV_PRIME = 16777619;
|
|
894
|
+
/**
|
|
895
|
+
* FNV-1a.
|
|
896
|
+
*
|
|
897
|
+
* The one hash in this package: the same function that decides a type's colour decides where a
|
|
898
|
+
* node starts, so both are a function of the name and of nothing else.
|
|
899
|
+
*/
|
|
900
|
+
function hash(value) {
|
|
901
|
+
let h = FNV_OFFSET;
|
|
902
|
+
for (let i = 0; i < value.length; i++) {
|
|
903
|
+
h ^= value.charCodeAt(i);
|
|
904
|
+
h = Math.imul(h, FNV_PRIME);
|
|
905
|
+
}
|
|
906
|
+
return h >>> 0;
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* How many steps a graph of `n` entities is given.
|
|
910
|
+
*
|
|
911
|
+
* Each step is O(n²), so the count falls as the graph grows: three hundred steps at fifty
|
|
912
|
+
* entities (a small graph settles properly and still inside a second), a hundred and twenty at
|
|
913
|
+
* five hundred, forty at two thousand — the panel's own limit, where a single step already costs
|
|
914
|
+
* a frame. Interpolated on a log scale between those three measured points, and flat outside
|
|
915
|
+
* them, so there is no size at which the number jumps.
|
|
916
|
+
*/
|
|
917
|
+
const STEP_ANCHORS = [
|
|
918
|
+
[50, 300],
|
|
919
|
+
[500, 120],
|
|
920
|
+
[2e3, 40]
|
|
921
|
+
];
|
|
922
|
+
function layoutSteps(n) {
|
|
923
|
+
const size = Math.max(1, n);
|
|
924
|
+
if (size <= STEP_ANCHORS[0][0]) return STEP_ANCHORS[0][1];
|
|
925
|
+
for (let i = 1; i < STEP_ANCHORS.length; i++) {
|
|
926
|
+
const [lowN, lowSteps] = STEP_ANCHORS[i - 1];
|
|
927
|
+
const [highN, highSteps] = STEP_ANCHORS[i];
|
|
928
|
+
if (size > highN) continue;
|
|
929
|
+
const t = (Math.log(size) - Math.log(lowN)) / (Math.log(highN) - Math.log(lowN));
|
|
930
|
+
return Math.round(lowSteps + (highSteps - lowSteps) * t);
|
|
931
|
+
}
|
|
932
|
+
return STEP_ANCHORS[STEP_ANCHORS.length - 1][1];
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* How many steps are spent per animation frame while the graph settles in front of the reader.
|
|
936
|
+
*
|
|
937
|
+
* Small enough to leave the frame time for drawing, and large enough that a six-entity graph —
|
|
938
|
+
* which is given three hundred steps — is finished in well under a second rather than in five.
|
|
939
|
+
* At a few hundred entities a single step already costs most of a frame, so it drops to one and
|
|
940
|
+
* the graph settles at whatever pace the machine can manage.
|
|
941
|
+
*/
|
|
942
|
+
function layoutChunk(n) {
|
|
943
|
+
return Math.max(1, Math.min(8, Math.round(600 / Math.max(1, n))));
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* The radius a graph of `n` entities settles inside, which is also where it is started.
|
|
947
|
+
*
|
|
948
|
+
* A cube root, not a square root. Inside a ball of charge the repulsion at radius `r` comes only
|
|
949
|
+
* from what is enclosed — `n(r/R)³` of it, over `r²` — so balancing it against a gravity of `g·r`
|
|
950
|
+
* gives `R = k·∛(n/g)`. Guessing `sqrt` instead put a two-thousand-entity graph's starting sphere
|
|
951
|
+
* five times too wide, and the first half of its forty steps went on collapsing rather than on
|
|
952
|
+
* laying anything out.
|
|
953
|
+
*/
|
|
954
|
+
function layoutRadius(n) {
|
|
955
|
+
return LAYOUT_SPRING * Math.cbrt(Math.max(1, n) / LAYOUT_GRAVITY);
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Where every node starts, as `[x, y, z]` per node in the order the ids were given.
|
|
959
|
+
*
|
|
960
|
+
* A node the caller remembers keeps exactly the position it had, which is what stops a graph that
|
|
961
|
+
* gained three entities from reshuffling the twenty that were already there. Everything else is
|
|
962
|
+
* placed on a sphere by the hash of its id — a Fibonacci-like spiral from two independent hashes,
|
|
963
|
+
* so ids that are one character apart do not end up next to each other — scaled to the cloud the
|
|
964
|
+
* remembered nodes already occupy, or to the graph's own settling radius when there is nothing to
|
|
965
|
+
* remember.
|
|
966
|
+
*/
|
|
967
|
+
function seedPositions(ids, seed) {
|
|
968
|
+
const out = new Float32Array(ids.length * 3);
|
|
969
|
+
let remembered = 0;
|
|
970
|
+
let rememberedRadius = 0;
|
|
971
|
+
for (const id of ids) {
|
|
972
|
+
const previous = seed?.get(id);
|
|
973
|
+
if (!previous) continue;
|
|
974
|
+
remembered++;
|
|
975
|
+
rememberedRadius += Math.hypot(previous[0], previous[1], previous[2]);
|
|
976
|
+
}
|
|
977
|
+
const radius = remembered > 0 ? Math.max(rememberedRadius / remembered, MIN_DISTANCE) : layoutRadius(ids.length);
|
|
978
|
+
for (const [index, id] of ids.entries()) {
|
|
979
|
+
const previous = seed?.get(id);
|
|
980
|
+
if (previous) {
|
|
981
|
+
out[index * 3] = previous[0];
|
|
982
|
+
out[index * 3 + 1] = previous[1];
|
|
983
|
+
out[index * 3 + 2] = previous[2];
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
const h = hash(id);
|
|
987
|
+
const z = h % 2048 / 2048 * 2 - 1;
|
|
988
|
+
const theta = hash(`${id}`) % 4096 * (Math.PI * 2 / 4096);
|
|
989
|
+
const ring = Math.sqrt(Math.max(0, 1 - z * z));
|
|
990
|
+
out[index * 3] = radius * ring * Math.cos(theta);
|
|
991
|
+
out[index * 3 + 1] = radius * ring * Math.sin(theta);
|
|
992
|
+
out[index * 3 + 2] = radius * z;
|
|
993
|
+
}
|
|
994
|
+
return out;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* A layout over `nodes` and `edges`, ready to be stepped.
|
|
998
|
+
*
|
|
999
|
+
* `seed` is the positions the same graph settled into last time, by node id; the ids it does not
|
|
1000
|
+
* mention are placed by their hash. An edge naming a node that is not there is skipped rather
|
|
1001
|
+
* than refused — the parser has already rejected those, and a layout is not the place to raise.
|
|
1002
|
+
*/
|
|
1003
|
+
function createLayout(nodes, edges, seed) {
|
|
1004
|
+
const ids = nodes.map((node) => node.id);
|
|
1005
|
+
const index = new Map(ids.map((id, i) => [id, i]));
|
|
1006
|
+
const position = seedPositions(ids, seed);
|
|
1007
|
+
const displacement = new Float64Array(position.length);
|
|
1008
|
+
const count = nodes.length;
|
|
1009
|
+
const steps = layoutSteps(count);
|
|
1010
|
+
const links = [];
|
|
1011
|
+
for (const edge of edges) {
|
|
1012
|
+
const from = index.get(edge.from);
|
|
1013
|
+
const to = index.get(edge.to);
|
|
1014
|
+
if (from === void 0 || to === void 0 || from === to) continue;
|
|
1015
|
+
links.push(from, to);
|
|
1016
|
+
}
|
|
1017
|
+
const k = LAYOUT_SPRING;
|
|
1018
|
+
const kSquared = k * k;
|
|
1019
|
+
const heat = layoutRadius(count) * HEAT;
|
|
1020
|
+
let taken = 0;
|
|
1021
|
+
const one = () => {
|
|
1022
|
+
displacement.fill(0);
|
|
1023
|
+
for (let i = 0; i < count; i++) {
|
|
1024
|
+
const xi = position[i * 3];
|
|
1025
|
+
const yi = position[i * 3 + 1];
|
|
1026
|
+
const zi = position[i * 3 + 2];
|
|
1027
|
+
for (let j = i + 1; j < count; j++) {
|
|
1028
|
+
const dx = xi - position[j * 3];
|
|
1029
|
+
const dy = yi - position[j * 3 + 1];
|
|
1030
|
+
const dz = zi - position[j * 3 + 2];
|
|
1031
|
+
const squared = Math.max(dx * dx + dy * dy + dz * dz, MIN_DISTANCE * MIN_DISTANCE);
|
|
1032
|
+
const force = kSquared / squared;
|
|
1033
|
+
displacement[i * 3] += dx * force;
|
|
1034
|
+
displacement[i * 3 + 1] += dy * force;
|
|
1035
|
+
displacement[i * 3 + 2] += dz * force;
|
|
1036
|
+
displacement[j * 3] -= dx * force;
|
|
1037
|
+
displacement[j * 3 + 1] -= dy * force;
|
|
1038
|
+
displacement[j * 3 + 2] -= dz * force;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
for (let e = 0; e < links.length; e += 2) {
|
|
1042
|
+
const a = links[e];
|
|
1043
|
+
const b = links[e + 1];
|
|
1044
|
+
const dx = position[a * 3] - position[b * 3];
|
|
1045
|
+
const dy = position[a * 3 + 1] - position[b * 3 + 1];
|
|
1046
|
+
const dz = position[a * 3 + 2] - position[b * 3 + 2];
|
|
1047
|
+
const distance = Math.max(Math.hypot(dx, dy, dz), MIN_DISTANCE);
|
|
1048
|
+
const force = distance / k;
|
|
1049
|
+
displacement[a * 3] -= dx * force;
|
|
1050
|
+
displacement[a * 3 + 1] -= dy * force;
|
|
1051
|
+
displacement[a * 3 + 2] -= dz * force;
|
|
1052
|
+
displacement[b * 3] += dx * force;
|
|
1053
|
+
displacement[b * 3 + 1] += dy * force;
|
|
1054
|
+
displacement[b * 3 + 2] += dz * force;
|
|
1055
|
+
}
|
|
1056
|
+
const temperature = Math.max(heat * (1 - taken / steps), heat * .01);
|
|
1057
|
+
for (let i = 0; i < count; i++) {
|
|
1058
|
+
const dx = displacement[i * 3] - position[i * 3] * LAYOUT_GRAVITY;
|
|
1059
|
+
const dy = displacement[i * 3 + 1] - position[i * 3 + 1] * LAYOUT_GRAVITY;
|
|
1060
|
+
const dz = displacement[i * 3 + 2] - position[i * 3 + 2] * LAYOUT_GRAVITY;
|
|
1061
|
+
const length = Math.hypot(dx, dy, dz);
|
|
1062
|
+
if (length < MIN_DISTANCE) continue;
|
|
1063
|
+
const scale = Math.min(length, temperature) / length;
|
|
1064
|
+
position[i * 3] += dx * scale;
|
|
1065
|
+
position[i * 3 + 1] += dy * scale;
|
|
1066
|
+
position[i * 3 + 2] += dz * scale;
|
|
1067
|
+
}
|
|
1068
|
+
taken++;
|
|
1069
|
+
};
|
|
1070
|
+
return {
|
|
1071
|
+
step(request = 1) {
|
|
1072
|
+
for (let i = 0; i < request && taken < steps; i++) one();
|
|
1073
|
+
},
|
|
1074
|
+
positions: () => position,
|
|
1075
|
+
get done() {
|
|
1076
|
+
return taken >= steps;
|
|
1077
|
+
},
|
|
1078
|
+
steps,
|
|
1079
|
+
get taken() {
|
|
1080
|
+
return taken;
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
//#endregion
|
|
1086
|
+
//#region src/graph3d.ts
|
|
1087
|
+
/**
|
|
1088
|
+
* The vocabulary every knowledge graph panel is drawn in, and the flat mode's own option.
|
|
1089
|
+
*
|
|
1090
|
+
* A type's colour, a node's degree, the legend in the corner and the tooltip are the same in both
|
|
1091
|
+
* modes and live here. `orbit.ts` — the default — turns the entities into a model in space;
|
|
1092
|
+
* `graph3dOption` below is `flat`, echarts-gl's `graphGL`: force-atlas2 on the GPU, drawn on a
|
|
1093
|
+
* plane with an orthographic camera. It draws more nodes at once than the model does, which is
|
|
1094
|
+
* the reason it is still here.
|
|
1095
|
+
*
|
|
1096
|
+
* Pure: the option is a function of the panel and the palette.
|
|
1097
|
+
*/
|
|
1098
|
+
/** How many entities carry a written label. The rest are one hover away. */
|
|
1099
|
+
const GRAPH_LABELS = 20;
|
|
1100
|
+
/**
|
|
1101
|
+
* How the layout is run.
|
|
1102
|
+
*
|
|
1103
|
+
* `steps` is iterations per frame and `maxSteps` is where the layout stops. Settling takes
|
|
1104
|
+
* `maxSteps / steps` frames — 125 of them, a shade over two seconds at sixty frames a second,
|
|
1105
|
+
* which is long enough to watch a graph of five hundred entities pull itself apart and short
|
|
1106
|
+
* enough that nobody waits for it. When nothing is meant to move, the same thousand iterations
|
|
1107
|
+
* are spent four frames deep and the reader gets the settled graph instead of the settling.
|
|
1108
|
+
*
|
|
1109
|
+
* (echarts-gl 2.x bounds the layout with `maxSteps`; there is no convergence threshold to set.)
|
|
1110
|
+
*/
|
|
1111
|
+
const GRAPH_SETTLE_STEPS = 8;
|
|
1112
|
+
const GRAPH_INSTANT_STEPS = 250;
|
|
1113
|
+
const GRAPH_MAX_STEPS = 1e3;
|
|
1114
|
+
/** How many types the corner legend lists before it would start covering the graph. */
|
|
1115
|
+
const GRAPH_LEGEND_ROWS = 12;
|
|
1116
|
+
/** How many of the palette's series colours a type may be hashed onto. */
|
|
1117
|
+
const GRAPH_COLOURS = 7;
|
|
1118
|
+
/**
|
|
1119
|
+
* Where the graph starts, and where it is pulled to.
|
|
1120
|
+
*
|
|
1121
|
+
* echarts-gl scatters the first positions at random inside the panel and fits the camera to them
|
|
1122
|
+
* once — `_updateCamera` refits only when the graph has left the frame entirely — so a random
|
|
1123
|
+
* start is why a settled graph sits wherever its first throw of the dice happened to land. Seeding
|
|
1124
|
+
* the nodes on a ring instead fixes the frame, and makes the same entities draw the same picture
|
|
1125
|
+
* twice running.
|
|
1126
|
+
*
|
|
1127
|
+
* The frame it fits is not centred on that ring: it pads the left by a fifth of the padded width,
|
|
1128
|
+
* which leaves the ring's own centre 0.16 of a radius right of the middle. So the layout's
|
|
1129
|
+
* `gravityCenter` is moved the same distance the other way, and the settled graph comes out in
|
|
1130
|
+
* the middle of the panel.
|
|
1131
|
+
*/
|
|
1132
|
+
const SEED_RADIUS = 45;
|
|
1133
|
+
const SEED_OFFSET = .16;
|
|
1134
|
+
/**
|
|
1135
|
+
* The colour of a type.
|
|
1136
|
+
*
|
|
1137
|
+
* Hashed rather than handed out in order of appearance, because two graph panels on one wall must
|
|
1138
|
+
* colour `outlet` the same way, and the order the types happen to appear in is different in each
|
|
1139
|
+
* of them. A hash means two types can land on the same colour, which is what the legend in the
|
|
1140
|
+
* corner is for; a panel that cares says so in `types`.
|
|
1141
|
+
*
|
|
1142
|
+
* Only the first seven series colours are in the ring. The eighth is a second cyan a shade off
|
|
1143
|
+
* the accent, and two types a reader cannot tell apart is worse than two types sharing a colour
|
|
1144
|
+
* the legend admits to sharing.
|
|
1145
|
+
*
|
|
1146
|
+
* A node with no type gets the muted colour: it is not a category, so it does not get one of the
|
|
1147
|
+
* colours the categories are being told apart by.
|
|
1148
|
+
*/
|
|
1149
|
+
function typeColour(type, c, overrides) {
|
|
1150
|
+
if (type === void 0) return c.muted;
|
|
1151
|
+
const ring = c.series.slice(0, GRAPH_COLOURS);
|
|
1152
|
+
return overrides?.[type] ?? ring[hash(type) % ring.length];
|
|
1153
|
+
}
|
|
1154
|
+
/** How many edges touch each node. Both ends count, so a self-edge counts twice. */
|
|
1155
|
+
function degrees(panel) {
|
|
1156
|
+
const count = new Map(panel.nodes.map((node) => [node.id, 0]));
|
|
1157
|
+
for (const edge of panel.edges) for (const id of [edge.from, edge.to]) {
|
|
1158
|
+
const current = count.get(id);
|
|
1159
|
+
if (current !== void 0) count.set(id, current + 1);
|
|
1160
|
+
}
|
|
1161
|
+
return count;
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* The key drawn in the panel's corner: every type that appears, once, in its own colour.
|
|
1165
|
+
*
|
|
1166
|
+
* Nodes first, then edges, each in the order it first appears — which is the order the model
|
|
1167
|
+
* wrote them in, and therefore the order it was thinking in. An untyped graph gets no legend at
|
|
1168
|
+
* all rather than an empty box.
|
|
1169
|
+
*/
|
|
1170
|
+
function graphLegend(panel, c) {
|
|
1171
|
+
const entries = [];
|
|
1172
|
+
const seen = new Set();
|
|
1173
|
+
const add = (shape, type) => {
|
|
1174
|
+
if (type === void 0) return;
|
|
1175
|
+
const key = `${shape}:${type}`;
|
|
1176
|
+
if (seen.has(key)) return;
|
|
1177
|
+
seen.add(key);
|
|
1178
|
+
entries.push({
|
|
1179
|
+
label: type,
|
|
1180
|
+
colour: typeColour(type, c, panel.types),
|
|
1181
|
+
shape
|
|
1182
|
+
});
|
|
1183
|
+
};
|
|
1184
|
+
for (const node of panel.nodes) add("node", node.type);
|
|
1185
|
+
for (const edge of panel.edges) add("edge", edge.type);
|
|
1186
|
+
return entries.slice(0, GRAPH_LEGEND_ROWS);
|
|
1187
|
+
}
|
|
1188
|
+
const escapeHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1189
|
+
/**
|
|
1190
|
+
* What an entity or an edge says on hover, in both modes.
|
|
1191
|
+
*
|
|
1192
|
+
* Escaped by hand: ECharts renders a formatter's return as HTML and every name here was written
|
|
1193
|
+
* by a model. An edge is told apart by `dataType` in the flat series and by carrying its own
|
|
1194
|
+
* `coords` in the orbit one, which is the only thing the two series disagree about.
|
|
1195
|
+
*/
|
|
1196
|
+
function graphTooltip(params) {
|
|
1197
|
+
const data = params.data ?? {};
|
|
1198
|
+
if (params.dataType === "edge" || data.coords !== void 0) return escapeHtml(data.edgeType ?? "linked to");
|
|
1199
|
+
const rows = [`<b>${escapeHtml(data.name ?? "")}</b>`];
|
|
1200
|
+
if (data.nodeType) rows.push(`<span style="opacity:.7">${escapeHtml(data.nodeType)}</span>`);
|
|
1201
|
+
rows.push(`<span style="opacity:.7">${data.degree ?? 0} connections</span>`);
|
|
1202
|
+
return rows.join("<br/>");
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* The `flat` mode: `graphGL`, laid out by force-atlas2 on the GPU.
|
|
1206
|
+
*
|
|
1207
|
+
* Unchanged since it was written, byte for byte, and pinned by a test that says so — a panel that
|
|
1208
|
+
* asked for this mode asked for exactly this picture. The 3D model is `graphOrbitOption`, which
|
|
1209
|
+
* takes positions rather than computing them, because it is stepped in front of the reader.
|
|
1210
|
+
*/
|
|
1211
|
+
function graph3dOption(panel, c, animate) {
|
|
1212
|
+
const degree = degrees(panel);
|
|
1213
|
+
const weight = (node) => node.value ?? degree.get(node.id) ?? 0;
|
|
1214
|
+
const maxWeight = Math.max(1, ...panel.nodes.map(weight));
|
|
1215
|
+
const named = new Set([...panel.nodes].sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0)).slice(0, GRAPH_LABELS).map((node) => node.id));
|
|
1216
|
+
if (panel.focus !== void 0) named.add(panel.focus);
|
|
1217
|
+
const radius = SEED_RADIUS * Math.sqrt(panel.nodes.length) + 60;
|
|
1218
|
+
const nodes = panel.nodes.map((node, index) => {
|
|
1219
|
+
const focused = node.id === panel.focus;
|
|
1220
|
+
const symbolSize = (focused ? 6 : 0) + 6 + 16 * Math.sqrt(weight(node) / maxWeight);
|
|
1221
|
+
const angle = index / panel.nodes.length * Math.PI * 2;
|
|
1222
|
+
return {
|
|
1223
|
+
id: node.id,
|
|
1224
|
+
name: node.name,
|
|
1225
|
+
x: radius * Math.cos(angle),
|
|
1226
|
+
y: radius * Math.sin(angle),
|
|
1227
|
+
nodeType: node.type,
|
|
1228
|
+
degree: degree.get(node.id) ?? 0,
|
|
1229
|
+
node,
|
|
1230
|
+
symbolSize,
|
|
1231
|
+
itemStyle: {
|
|
1232
|
+
color: focused ? c.accent : typeColour(node.type, c, panel.types),
|
|
1233
|
+
opacity: .95
|
|
1234
|
+
},
|
|
1235
|
+
label: focused ? {
|
|
1236
|
+
show: true,
|
|
1237
|
+
fontSize: 13,
|
|
1238
|
+
fontWeight: "bold",
|
|
1239
|
+
color: c.accent
|
|
1240
|
+
} : { show: named.has(node.id) }
|
|
1241
|
+
};
|
|
1242
|
+
});
|
|
1243
|
+
const edges = panel.edges.map((edge) => ({
|
|
1244
|
+
source: edge.from,
|
|
1245
|
+
target: edge.to,
|
|
1246
|
+
edgeType: edge.type,
|
|
1247
|
+
lineStyle: {
|
|
1248
|
+
color: typeColour(edge.type, c, panel.types),
|
|
1249
|
+
opacity: .55,
|
|
1250
|
+
width: 1.5
|
|
1251
|
+
}
|
|
1252
|
+
}));
|
|
1253
|
+
const settling = animate && panel.rotate !== false;
|
|
1254
|
+
return {
|
|
1255
|
+
backgroundColor: "transparent",
|
|
1256
|
+
textStyle: {
|
|
1257
|
+
color: c.text,
|
|
1258
|
+
fontFamily: FONT
|
|
1259
|
+
},
|
|
1260
|
+
tooltip: {
|
|
1261
|
+
trigger: "item",
|
|
1262
|
+
confine: true,
|
|
1263
|
+
formatter: graphTooltip
|
|
1264
|
+
},
|
|
1265
|
+
series: [{
|
|
1266
|
+
type: "graphGL",
|
|
1267
|
+
layout: "forceAtlas2",
|
|
1268
|
+
forceAtlas2: {
|
|
1269
|
+
GPU: true,
|
|
1270
|
+
steps: settling ? GRAPH_SETTLE_STEPS : GRAPH_INSTANT_STEPS,
|
|
1271
|
+
maxSteps: GRAPH_MAX_STEPS,
|
|
1272
|
+
gravity: .6,
|
|
1273
|
+
scaling: 5,
|
|
1274
|
+
gravityCenter: [-SEED_OFFSET * radius, 0],
|
|
1275
|
+
edgeWeightInfluence: 0,
|
|
1276
|
+
repulsionByDegree: true,
|
|
1277
|
+
preventOverlap: true
|
|
1278
|
+
},
|
|
1279
|
+
roam: true,
|
|
1280
|
+
focusNodeAdjacency: true,
|
|
1281
|
+
zoom: 1,
|
|
1282
|
+
label: {
|
|
1283
|
+
show: false,
|
|
1284
|
+
position: "right",
|
|
1285
|
+
distance: 4,
|
|
1286
|
+
color: c.text,
|
|
1287
|
+
fontSize: 11,
|
|
1288
|
+
fontFamily: FONT,
|
|
1289
|
+
formatter: "{b}"
|
|
1290
|
+
},
|
|
1291
|
+
emphasis: { label: { show: true } },
|
|
1292
|
+
lineStyle: {
|
|
1293
|
+
color: c.gridLine,
|
|
1294
|
+
width: 1,
|
|
1295
|
+
opacity: .35
|
|
1296
|
+
},
|
|
1297
|
+
nodes,
|
|
1298
|
+
edges
|
|
1299
|
+
}]
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
//#endregion
|
|
1304
|
+
//#region src/orbit.ts
|
|
1305
|
+
/**
|
|
1306
|
+
* The knowledge graph as a model you look at, rather than a picture you look down on.
|
|
1307
|
+
*
|
|
1308
|
+
* `layout3d.ts` puts the entities in space; this puts a camera in there with them. A `grid3D`
|
|
1309
|
+
* holds the coordinate system, a `scatter3D` draws the entities and a `line3D` draws the edges,
|
|
1310
|
+
* and the box the three of them share is deliberately invisible: a knowledge graph has no axes
|
|
1311
|
+
* to read, and the only thing an axis would add is furniture.
|
|
1312
|
+
*
|
|
1313
|
+
* Pure: the option is a function of the panel, the palette and the positions. The positions come
|
|
1314
|
+
* from outside because the layout is stepped — the mount loop hands the same builder a slightly
|
|
1315
|
+
* different set of coordinates every frame while the graph settles.
|
|
1316
|
+
*/
|
|
1317
|
+
/**
|
|
1318
|
+
* The half-width of the space the graph is drawn in, in the box's own units.
|
|
1319
|
+
*
|
|
1320
|
+
* Everything about the camera — the distance it sits at, how big a node reads at that distance —
|
|
1321
|
+
* is a constant, so the graph has to be scaled to it rather than the other way round. The layout
|
|
1322
|
+
* works in units of its own spring length and a graph of two thousand is naturally four times
|
|
1323
|
+
* wider than a graph of six; `orbitScale` divides that difference out, and this is what is left.
|
|
1324
|
+
*/
|
|
1325
|
+
const ORBIT_EXTENT = 50;
|
|
1326
|
+
/** The cube the coordinate system is drawn in. Equal on all three sides, or the shape is a lie. */
|
|
1327
|
+
const ORBIT_BOX = 140;
|
|
1328
|
+
/** How solid an edge is drawn. */
|
|
1329
|
+
const EDGE_OPACITY = .6;
|
|
1330
|
+
/**
|
|
1331
|
+
* The camera: how far out it sits, how fast it goes round, and the field of view it does it with.
|
|
1332
|
+
*
|
|
1333
|
+
* The distance is not a matter of taste. The graph is fitted to the box, so its furthest entity
|
|
1334
|
+
* sits half a box from the middle — and as the camera turns, that entity comes round to the near
|
|
1335
|
+
* side, where the perspective divide is over a distance of `ORBIT_DISTANCE - ORBIT_BOX / 2`
|
|
1336
|
+
* rather than `ORBIT_DISTANCE`. A camera framed for the middle therefore crops the graph twice a
|
|
1337
|
+
* revolution, which is exactly what it did at 180: an entity and its edge walked off the bottom
|
|
1338
|
+
* of the panel and back. So the distance is set from the near face, with room left over for the
|
|
1339
|
+
* labels, which stick out further than the entities they belong to.
|
|
1340
|
+
*
|
|
1341
|
+
* echarts-gl's grid3D camera has a vertical field of view of 50 degrees and is not configurable
|
|
1342
|
+
* through `viewControl`, so it is written down here rather than passed.
|
|
1343
|
+
*/
|
|
1344
|
+
const ORBIT_FOV = 50;
|
|
1345
|
+
const ORBIT_DISTANCE = 260;
|
|
1346
|
+
const ORBIT_SPEED = 6;
|
|
1347
|
+
/**
|
|
1348
|
+
* The graph's own bounding box, as the transform that puts it in the middle of the panel.
|
|
1349
|
+
*
|
|
1350
|
+
* Its box, not its bounding sphere, and its centre, not the origin. A settled graph is rarely a
|
|
1351
|
+
* ball: the eight-entity example comes out about 72 units wide, 26 tall and 40 deep, centred six
|
|
1352
|
+
* units left of where it started. Scaling that by its largest radius left it filling a fifth of
|
|
1353
|
+
* the panel and sitting off to one side — correct, and unreadable. Fitting the longest of the
|
|
1354
|
+
* three spans instead fills the frame in whichever direction the graph is actually long, and
|
|
1355
|
+
* because every other span is shorter by definition, nothing lands outside the box.
|
|
1356
|
+
*/
|
|
1357
|
+
function orbitFrame(positions) {
|
|
1358
|
+
if (positions.length < 3) return {
|
|
1359
|
+
centre: [
|
|
1360
|
+
0,
|
|
1361
|
+
0,
|
|
1362
|
+
0
|
|
1363
|
+
],
|
|
1364
|
+
scale: 1
|
|
1365
|
+
};
|
|
1366
|
+
const low = [
|
|
1367
|
+
Infinity,
|
|
1368
|
+
Infinity,
|
|
1369
|
+
Infinity
|
|
1370
|
+
];
|
|
1371
|
+
const high = [
|
|
1372
|
+
-Infinity,
|
|
1373
|
+
-Infinity,
|
|
1374
|
+
-Infinity
|
|
1375
|
+
];
|
|
1376
|
+
for (let i = 0; i < positions.length; i += 3) for (let axis = 0; axis < 3; axis++) {
|
|
1377
|
+
low[axis] = Math.min(low[axis], positions[i + axis]);
|
|
1378
|
+
high[axis] = Math.max(high[axis], positions[i + axis]);
|
|
1379
|
+
}
|
|
1380
|
+
const centre = [
|
|
1381
|
+
(low[0] + high[0]) / 2,
|
|
1382
|
+
(low[1] + high[1]) / 2,
|
|
1383
|
+
(low[2] + high[2]) / 2
|
|
1384
|
+
];
|
|
1385
|
+
const half = Math.max((high[0] - low[0]) / 2, (high[1] - low[1]) / 2, (high[2] - low[2]) / 2);
|
|
1386
|
+
return {
|
|
1387
|
+
centre,
|
|
1388
|
+
scale: half > 0 ? ORBIT_EXTENT / half : 1
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
/** How many vertices one edge contributes to that polyline. */
|
|
1392
|
+
const EDGE_VERTICES = 4;
|
|
1393
|
+
/**
|
|
1394
|
+
* The two series' data, from the panel and wherever the layout has got to.
|
|
1395
|
+
*
|
|
1396
|
+
* Built as one thing because the edges have to agree with the nodes: an edge is drawn between two
|
|
1397
|
+
* points, not between two ids, so a frame that moved the nodes and left the edges behind would
|
|
1398
|
+
* detach every line from both its ends.
|
|
1399
|
+
*/
|
|
1400
|
+
function graphOrbitData(panel, c, positions) {
|
|
1401
|
+
const degree = degrees(panel);
|
|
1402
|
+
const weight = (node) => node.value ?? degree.get(node.id) ?? 0;
|
|
1403
|
+
const maxWeight = Math.max(1, ...panel.nodes.map(weight));
|
|
1404
|
+
const named = new Set([...panel.nodes].sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0)).slice(0, GRAPH_LABELS).map((node) => node.id));
|
|
1405
|
+
if (panel.focus !== void 0) named.add(panel.focus);
|
|
1406
|
+
const { centre, scale } = orbitFrame(positions);
|
|
1407
|
+
const at = (index) => [
|
|
1408
|
+
(positions[index * 3] - centre[0]) * scale,
|
|
1409
|
+
(positions[index * 3 + 1] - centre[1]) * scale,
|
|
1410
|
+
(positions[index * 3 + 2] - centre[2]) * scale
|
|
1411
|
+
];
|
|
1412
|
+
const place = new Map();
|
|
1413
|
+
const nodes = panel.nodes.map((node, index) => {
|
|
1414
|
+
const focused = node.id === panel.focus;
|
|
1415
|
+
const [x, y, z] = at(index);
|
|
1416
|
+
place.set(node.id, [
|
|
1417
|
+
x,
|
|
1418
|
+
y,
|
|
1419
|
+
z
|
|
1420
|
+
]);
|
|
1421
|
+
return {
|
|
1422
|
+
id: node.id,
|
|
1423
|
+
name: node.name,
|
|
1424
|
+
value: [
|
|
1425
|
+
x,
|
|
1426
|
+
y,
|
|
1427
|
+
z,
|
|
1428
|
+
(focused ? 6 : 0) + 6 + 16 * Math.sqrt(weight(node) / maxWeight)
|
|
1429
|
+
],
|
|
1430
|
+
itemStyle: {
|
|
1431
|
+
color: focused ? c.accent : typeColour(node.type, c, panel.types),
|
|
1432
|
+
opacity: .95
|
|
1433
|
+
},
|
|
1434
|
+
labelled: named.has(node.id),
|
|
1435
|
+
nodeType: node.type,
|
|
1436
|
+
degree: degree.get(node.id) ?? 0,
|
|
1437
|
+
node
|
|
1438
|
+
};
|
|
1439
|
+
});
|
|
1440
|
+
const origin = [
|
|
1441
|
+
0,
|
|
1442
|
+
0,
|
|
1443
|
+
0
|
|
1444
|
+
];
|
|
1445
|
+
const edges = [];
|
|
1446
|
+
for (const edge of panel.edges) {
|
|
1447
|
+
const from = place.get(edge.from) ?? origin;
|
|
1448
|
+
const to = place.get(edge.to) ?? origin;
|
|
1449
|
+
const color = typeColour(edge.type, c, panel.types);
|
|
1450
|
+
for (const [value, opacity] of [
|
|
1451
|
+
[from, 0],
|
|
1452
|
+
[from, EDGE_OPACITY],
|
|
1453
|
+
[to, EDGE_OPACITY],
|
|
1454
|
+
[to, 0]
|
|
1455
|
+
]) edges.push({
|
|
1456
|
+
value,
|
|
1457
|
+
lineStyle: {
|
|
1458
|
+
color,
|
|
1459
|
+
opacity
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
return {
|
|
1464
|
+
nodes,
|
|
1465
|
+
edges
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* An axis that exists to hold coordinates and to show nothing.
|
|
1470
|
+
*
|
|
1471
|
+
* Note what is *not* here: `axisLine: { show: false }`. echarts-gl records the axis line's two
|
|
1472
|
+
* endpoints only while it is drawing that line, and the camera-change handler then reads them to
|
|
1473
|
+
* decide which way the labels face — so an axis with its line switched off throws
|
|
1474
|
+
* `Cannot read properties of null` out of `_updateAxisLabelAlign` on every frame the camera
|
|
1475
|
+
* moves, which on an auto-rotating panel is all of them. The box is hidden with `grid3D.show`
|
|
1476
|
+
* instead, which takes the whole thing — faces, axes, labels — out of the scene and skips that
|
|
1477
|
+
* handler entirely.
|
|
1478
|
+
*/
|
|
1479
|
+
function hiddenAxis() {
|
|
1480
|
+
return {
|
|
1481
|
+
type: "value",
|
|
1482
|
+
name: "",
|
|
1483
|
+
min: -ORBIT_EXTENT,
|
|
1484
|
+
max: ORBIT_EXTENT,
|
|
1485
|
+
axisPointer: { show: false }
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* The whole scene.
|
|
1490
|
+
*
|
|
1491
|
+
* `animate` is the host's word and `panel.rotate` is the fence's; either one saying no stops the
|
|
1492
|
+
* camera. ECharts' own animation is off throughout — on each series as well as at the root,
|
|
1493
|
+
* because echarts-gl's vertex animation asks the *series* whether it may animate and does not
|
|
1494
|
+
* fall back to the screen's answer. While the graph settles this option's data is replaced every
|
|
1495
|
+
* frame, and an animated transition would spend that frame interpolating towards positions the
|
|
1496
|
+
* next frame has already superseded.
|
|
1497
|
+
*/
|
|
1498
|
+
function graphOrbitOption(panel, c, animate, positions) {
|
|
1499
|
+
const data = graphOrbitData(panel, c, positions);
|
|
1500
|
+
const name = (params) => params.data?.name ?? "";
|
|
1501
|
+
return {
|
|
1502
|
+
backgroundColor: "transparent",
|
|
1503
|
+
textStyle: {
|
|
1504
|
+
color: c.text,
|
|
1505
|
+
fontFamily: FONT
|
|
1506
|
+
},
|
|
1507
|
+
animation: false,
|
|
1508
|
+
tooltip: {
|
|
1509
|
+
trigger: "item",
|
|
1510
|
+
confine: true,
|
|
1511
|
+
formatter: graphTooltip
|
|
1512
|
+
},
|
|
1513
|
+
xAxis3D: hiddenAxis(),
|
|
1514
|
+
yAxis3D: hiddenAxis(),
|
|
1515
|
+
zAxis3D: hiddenAxis(),
|
|
1516
|
+
grid3D: {
|
|
1517
|
+
show: false,
|
|
1518
|
+
boxWidth: ORBIT_BOX,
|
|
1519
|
+
boxHeight: ORBIT_BOX,
|
|
1520
|
+
boxDepth: ORBIT_BOX,
|
|
1521
|
+
environment: "none",
|
|
1522
|
+
axisPointer: { show: false },
|
|
1523
|
+
light: {
|
|
1524
|
+
main: {
|
|
1525
|
+
intensity: 1.2,
|
|
1526
|
+
shadow: false,
|
|
1527
|
+
alpha: 30,
|
|
1528
|
+
beta: 40
|
|
1529
|
+
},
|
|
1530
|
+
ambient: { intensity: .6 }
|
|
1531
|
+
},
|
|
1532
|
+
viewControl: {
|
|
1533
|
+
projection: "perspective",
|
|
1534
|
+
autoRotate: animate && panel.rotate !== false,
|
|
1535
|
+
autoRotateSpeed: ORBIT_SPEED,
|
|
1536
|
+
autoRotateAfterStill: 4,
|
|
1537
|
+
distance: ORBIT_DISTANCE,
|
|
1538
|
+
alpha: 18,
|
|
1539
|
+
beta: 25,
|
|
1540
|
+
damping: .85
|
|
1541
|
+
}
|
|
1542
|
+
},
|
|
1543
|
+
series: [{
|
|
1544
|
+
type: "line3D",
|
|
1545
|
+
coordinateSystem: "cartesian3D",
|
|
1546
|
+
animation: false,
|
|
1547
|
+
lineStyle: { width: 1.6 },
|
|
1548
|
+
silent: true,
|
|
1549
|
+
data: data.edges
|
|
1550
|
+
}, {
|
|
1551
|
+
type: "scatter3D",
|
|
1552
|
+
coordinateSystem: "cartesian3D",
|
|
1553
|
+
animation: false,
|
|
1554
|
+
symbolSize: (value) => value[3],
|
|
1555
|
+
itemStyle: { opacity: .95 },
|
|
1556
|
+
label: {
|
|
1557
|
+
show: true,
|
|
1558
|
+
formatter: (params) => params.data?.labelled ? params.data.name ?? "" : "",
|
|
1559
|
+
color: c.text,
|
|
1560
|
+
fontSize: 11,
|
|
1561
|
+
fontFamily: FONT,
|
|
1562
|
+
distance: 4,
|
|
1563
|
+
backgroundColor: "transparent"
|
|
1564
|
+
},
|
|
1565
|
+
emphasis: {
|
|
1566
|
+
label: {
|
|
1567
|
+
show: true,
|
|
1568
|
+
formatter: name
|
|
1569
|
+
},
|
|
1570
|
+
itemStyle: { opacity: 1 }
|
|
1571
|
+
},
|
|
1572
|
+
data: data.nodes
|
|
1573
|
+
}]
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
//#endregion
|
|
1578
|
+
//#region src/positions.ts
|
|
1579
|
+
/**
|
|
1580
|
+
* Where each graph settled last time it was drawn.
|
|
1581
|
+
*
|
|
1582
|
+
* A streamed answer re-renders the same fence as the model writes it: a graph of twenty entities
|
|
1583
|
+
* becomes the same graph of twenty-three, and the reconciler tears the panel down and mounts a
|
|
1584
|
+
* new one. Without a memory the new layout starts from the hash again, every node lands somewhere
|
|
1585
|
+
* else, and the reader — who was reading it — watches the picture they had just understood
|
|
1586
|
+
* dissolve because three names arrived.
|
|
1587
|
+
*
|
|
1588
|
+
* So the positions are kept by node id, under a key that deliberately does *not* depend on the
|
|
1589
|
+
* node set: the ids that are still there resume exactly where they were, and only the new ones
|
|
1590
|
+
* have to find a place. It is module-level state, which is what makes it survive the remount; it
|
|
1591
|
+
* is bounded so a long conversation full of graphs cannot grow it without limit, and it is
|
|
1592
|
+
* forgettable so tests do not leak into each other.
|
|
1593
|
+
*/
|
|
1594
|
+
/** How many graphs are remembered at once. Beyond this the least recently used one is dropped. */
|
|
1595
|
+
const REMEMBERED_GRAPHS = 8;
|
|
1596
|
+
/** Insertion order is the recency order: `Map` iterates oldest first, which is the whole trick. */
|
|
1597
|
+
const settled = new Map();
|
|
1598
|
+
/**
|
|
1599
|
+
* What identifies a graph across a re-render.
|
|
1600
|
+
*
|
|
1601
|
+
* The title, because that is the one thing a model rewriting its own fence keeps stable while the
|
|
1602
|
+
* entities underneath it change. A panel with no title falls back to its first entity, which is
|
|
1603
|
+
* the next most stable thing there is — a graph usually keeps the entity it was drawn around.
|
|
1604
|
+
* The prefix keeps a panel titled `kyiv` from colliding with an untitled one whose first node is
|
|
1605
|
+
* `kyiv`.
|
|
1606
|
+
*/
|
|
1607
|
+
function graphKey(panel) {
|
|
1608
|
+
return panel.title !== void 0 ? `title:${panel.title}` : `node:${panel.nodes[0]?.id ?? ""}`;
|
|
1609
|
+
}
|
|
1610
|
+
/** The positions this graph settled into last time, by node id, or nothing. */
|
|
1611
|
+
function recallPositions(key) {
|
|
1612
|
+
const found = settled.get(key);
|
|
1613
|
+
if (!found) return void 0;
|
|
1614
|
+
settled.delete(key);
|
|
1615
|
+
settled.set(key, found);
|
|
1616
|
+
return found;
|
|
1617
|
+
}
|
|
1618
|
+
/**
|
|
1619
|
+
* Remember where a graph settled.
|
|
1620
|
+
*
|
|
1621
|
+
* The positions are copied out of the buffer rather than kept by reference: `Layout.positions()`
|
|
1622
|
+
* hands back the live array, and a layout that is still running would otherwise have this
|
|
1623
|
+
* remember whatever it became several frames later.
|
|
1624
|
+
*/
|
|
1625
|
+
function rememberPositions(key, ids, positions) {
|
|
1626
|
+
const map = new Map();
|
|
1627
|
+
for (const [index, id] of ids.entries()) map.set(id, [
|
|
1628
|
+
positions[index * 3],
|
|
1629
|
+
positions[index * 3 + 1],
|
|
1630
|
+
positions[index * 3 + 2]
|
|
1631
|
+
]);
|
|
1632
|
+
settled.delete(key);
|
|
1633
|
+
settled.set(key, map);
|
|
1634
|
+
while (settled.size > REMEMBERED_GRAPHS) {
|
|
1635
|
+
const oldest = settled.keys().next();
|
|
1636
|
+
if (oldest.done) break;
|
|
1637
|
+
settled.delete(oldest.value);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
/** Drop every remembered graph. For tests, and for a host tearing a page down. */
|
|
1641
|
+
function forgetPositions() {
|
|
1642
|
+
settled.clear();
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
//#endregion
|
|
1646
|
+
Object.defineProperty(exports, 'EDGE_VERTICES', {
|
|
1647
|
+
enumerable: true,
|
|
1648
|
+
get: function () {
|
|
1649
|
+
return EDGE_VERTICES;
|
|
1650
|
+
}
|
|
1651
|
+
});
|
|
1652
|
+
Object.defineProperty(exports, 'GRAPH_LABELS', {
|
|
1653
|
+
enumerable: true,
|
|
1654
|
+
get: function () {
|
|
1655
|
+
return GRAPH_LABELS;
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
Object.defineProperty(exports, 'GRAPH_LEGEND_ROWS', {
|
|
1659
|
+
enumerable: true,
|
|
1660
|
+
get: function () {
|
|
1661
|
+
return GRAPH_LEGEND_ROWS;
|
|
1662
|
+
}
|
|
1663
|
+
});
|
|
1664
|
+
Object.defineProperty(exports, 'GRAPH_MAX_STEPS', {
|
|
1665
|
+
enumerable: true,
|
|
1666
|
+
get: function () {
|
|
1667
|
+
return GRAPH_MAX_STEPS;
|
|
1668
|
+
}
|
|
1669
|
+
});
|
|
1670
|
+
Object.defineProperty(exports, 'GRAPH_SETTLE_STEPS', {
|
|
1671
|
+
enumerable: true,
|
|
1672
|
+
get: function () {
|
|
1673
|
+
return GRAPH_SETTLE_STEPS;
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
Object.defineProperty(exports, 'LAYOUT_GRAVITY', {
|
|
1677
|
+
enumerable: true,
|
|
1678
|
+
get: function () {
|
|
1679
|
+
return LAYOUT_GRAVITY;
|
|
1680
|
+
}
|
|
1681
|
+
});
|
|
1682
|
+
Object.defineProperty(exports, 'LAYOUT_SPRING', {
|
|
1683
|
+
enumerable: true,
|
|
1684
|
+
get: function () {
|
|
1685
|
+
return LAYOUT_SPRING;
|
|
1686
|
+
}
|
|
1687
|
+
});
|
|
1688
|
+
Object.defineProperty(exports, 'ORBIT_BOX', {
|
|
1689
|
+
enumerable: true,
|
|
1690
|
+
get: function () {
|
|
1691
|
+
return ORBIT_BOX;
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
Object.defineProperty(exports, 'ORBIT_DISTANCE', {
|
|
1695
|
+
enumerable: true,
|
|
1696
|
+
get: function () {
|
|
1697
|
+
return ORBIT_DISTANCE;
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
Object.defineProperty(exports, 'ORBIT_EXTENT', {
|
|
1701
|
+
enumerable: true,
|
|
1702
|
+
get: function () {
|
|
1703
|
+
return ORBIT_EXTENT;
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1706
|
+
Object.defineProperty(exports, 'ORBIT_FOV', {
|
|
1707
|
+
enumerable: true,
|
|
1708
|
+
get: function () {
|
|
1709
|
+
return ORBIT_FOV;
|
|
1710
|
+
}
|
|
1711
|
+
});
|
|
1712
|
+
Object.defineProperty(exports, 'REMEMBERED_GRAPHS', {
|
|
1713
|
+
enumerable: true,
|
|
1714
|
+
get: function () {
|
|
1715
|
+
return REMEMBERED_GRAPHS;
|
|
1716
|
+
}
|
|
1717
|
+
});
|
|
1718
|
+
Object.defineProperty(exports, 'TIMELINE_LABELS', {
|
|
1719
|
+
enumerable: true,
|
|
1720
|
+
get: function () {
|
|
1721
|
+
return TIMELINE_LABELS;
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
Object.defineProperty(exports, '__toESM', {
|
|
1725
|
+
enumerable: true,
|
|
1726
|
+
get: function () {
|
|
1727
|
+
return __toESM;
|
|
1728
|
+
}
|
|
1729
|
+
});
|
|
1730
|
+
Object.defineProperty(exports, 'chart3dOption', {
|
|
1731
|
+
enumerable: true,
|
|
1732
|
+
get: function () {
|
|
1733
|
+
return chart3dOption;
|
|
1734
|
+
}
|
|
1735
|
+
});
|
|
1736
|
+
Object.defineProperty(exports, 'chartOption', {
|
|
1737
|
+
enumerable: true,
|
|
1738
|
+
get: function () {
|
|
1739
|
+
return chartOption;
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1742
|
+
Object.defineProperty(exports, 'countriesTexture', {
|
|
1743
|
+
enumerable: true,
|
|
1744
|
+
get: function () {
|
|
1745
|
+
return countriesTexture;
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1748
|
+
Object.defineProperty(exports, 'createLayout', {
|
|
1749
|
+
enumerable: true,
|
|
1750
|
+
get: function () {
|
|
1751
|
+
return createLayout;
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
Object.defineProperty(exports, 'degrees', {
|
|
1755
|
+
enumerable: true,
|
|
1756
|
+
get: function () {
|
|
1757
|
+
return degrees;
|
|
1758
|
+
}
|
|
1759
|
+
});
|
|
1760
|
+
Object.defineProperty(exports, 'earthColours', {
|
|
1761
|
+
enumerable: true,
|
|
1762
|
+
get: function () {
|
|
1763
|
+
return earthColours;
|
|
1764
|
+
}
|
|
1765
|
+
});
|
|
1766
|
+
Object.defineProperty(exports, 'earthTexture', {
|
|
1767
|
+
enumerable: true,
|
|
1768
|
+
get: function () {
|
|
1769
|
+
return earthTexture;
|
|
1770
|
+
}
|
|
1771
|
+
});
|
|
1772
|
+
Object.defineProperty(exports, 'forgetPositions', {
|
|
1773
|
+
enumerable: true,
|
|
1774
|
+
get: function () {
|
|
1775
|
+
return forgetPositions;
|
|
1776
|
+
}
|
|
1777
|
+
});
|
|
1778
|
+
Object.defineProperty(exports, 'formatNumber', {
|
|
1779
|
+
enumerable: true,
|
|
1780
|
+
get: function () {
|
|
1781
|
+
return formatNumber;
|
|
1782
|
+
}
|
|
1783
|
+
});
|
|
1784
|
+
Object.defineProperty(exports, 'gaugeColour', {
|
|
1785
|
+
enumerable: true,
|
|
1786
|
+
get: function () {
|
|
1787
|
+
return gaugeColour;
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
Object.defineProperty(exports, 'gaugeOption', {
|
|
1791
|
+
enumerable: true,
|
|
1792
|
+
get: function () {
|
|
1793
|
+
return gaugeOption;
|
|
1794
|
+
}
|
|
1795
|
+
});
|
|
1796
|
+
Object.defineProperty(exports, 'globeOption', {
|
|
1797
|
+
enumerable: true,
|
|
1798
|
+
get: function () {
|
|
1799
|
+
return globeOption;
|
|
1800
|
+
}
|
|
1801
|
+
});
|
|
1802
|
+
Object.defineProperty(exports, 'globeTexture', {
|
|
1803
|
+
enumerable: true,
|
|
1804
|
+
get: function () {
|
|
1805
|
+
return globeTexture;
|
|
1806
|
+
}
|
|
1807
|
+
});
|
|
1808
|
+
Object.defineProperty(exports, 'graph3dOption', {
|
|
1809
|
+
enumerable: true,
|
|
1810
|
+
get: function () {
|
|
1811
|
+
return graph3dOption;
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
Object.defineProperty(exports, 'graphKey', {
|
|
1815
|
+
enumerable: true,
|
|
1816
|
+
get: function () {
|
|
1817
|
+
return graphKey;
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
1820
|
+
Object.defineProperty(exports, 'graphLegend', {
|
|
1821
|
+
enumerable: true,
|
|
1822
|
+
get: function () {
|
|
1823
|
+
return graphLegend;
|
|
1824
|
+
}
|
|
1825
|
+
});
|
|
1826
|
+
Object.defineProperty(exports, 'graphOrbitData', {
|
|
1827
|
+
enumerable: true,
|
|
1828
|
+
get: function () {
|
|
1829
|
+
return graphOrbitData;
|
|
1830
|
+
}
|
|
1831
|
+
});
|
|
1832
|
+
Object.defineProperty(exports, 'graphOrbitOption', {
|
|
1833
|
+
enumerable: true,
|
|
1834
|
+
get: function () {
|
|
1835
|
+
return graphOrbitOption;
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
1838
|
+
Object.defineProperty(exports, 'graphTooltip', {
|
|
1839
|
+
enumerable: true,
|
|
1840
|
+
get: function () {
|
|
1841
|
+
return graphTooltip;
|
|
1842
|
+
}
|
|
1843
|
+
});
|
|
1844
|
+
Object.defineProperty(exports, 'hash', {
|
|
1845
|
+
enumerable: true,
|
|
1846
|
+
get: function () {
|
|
1847
|
+
return hash;
|
|
1848
|
+
}
|
|
1849
|
+
});
|
|
1850
|
+
Object.defineProperty(exports, 'layoutChunk', {
|
|
1851
|
+
enumerable: true,
|
|
1852
|
+
get: function () {
|
|
1853
|
+
return layoutChunk;
|
|
1854
|
+
}
|
|
1855
|
+
});
|
|
1856
|
+
Object.defineProperty(exports, 'layoutRadius', {
|
|
1857
|
+
enumerable: true,
|
|
1858
|
+
get: function () {
|
|
1859
|
+
return layoutRadius;
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1862
|
+
Object.defineProperty(exports, 'layoutSteps', {
|
|
1863
|
+
enumerable: true,
|
|
1864
|
+
get: function () {
|
|
1865
|
+
return layoutSteps;
|
|
1866
|
+
}
|
|
1867
|
+
});
|
|
1868
|
+
Object.defineProperty(exports, 'orbitFrame', {
|
|
1869
|
+
enumerable: true,
|
|
1870
|
+
get: function () {
|
|
1871
|
+
return orbitFrame;
|
|
1872
|
+
}
|
|
1873
|
+
});
|
|
1874
|
+
Object.defineProperty(exports, 'palette', {
|
|
1875
|
+
enumerable: true,
|
|
1876
|
+
get: function () {
|
|
1877
|
+
return palette;
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
Object.defineProperty(exports, 'recallPositions', {
|
|
1881
|
+
enumerable: true,
|
|
1882
|
+
get: function () {
|
|
1883
|
+
return recallPositions;
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
Object.defineProperty(exports, 'rememberPositions', {
|
|
1887
|
+
enumerable: true,
|
|
1888
|
+
get: function () {
|
|
1889
|
+
return rememberPositions;
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1892
|
+
Object.defineProperty(exports, 'seedPositions', {
|
|
1893
|
+
enumerable: true,
|
|
1894
|
+
get: function () {
|
|
1895
|
+
return seedPositions;
|
|
1896
|
+
}
|
|
1897
|
+
});
|
|
1898
|
+
Object.defineProperty(exports, 'spacedItems', {
|
|
1899
|
+
enumerable: true,
|
|
1900
|
+
get: function () {
|
|
1901
|
+
return spacedItems;
|
|
1902
|
+
}
|
|
1903
|
+
});
|
|
1904
|
+
Object.defineProperty(exports, 'timelineHeight', {
|
|
1905
|
+
enumerable: true,
|
|
1906
|
+
get: function () {
|
|
1907
|
+
return timelineHeight;
|
|
1908
|
+
}
|
|
1909
|
+
});
|
|
1910
|
+
Object.defineProperty(exports, 'timelineOption', {
|
|
1911
|
+
enumerable: true,
|
|
1912
|
+
get: function () {
|
|
1913
|
+
return timelineOption;
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1916
|
+
Object.defineProperty(exports, 'timelineWindow', {
|
|
1917
|
+
enumerable: true,
|
|
1918
|
+
get: function () {
|
|
1919
|
+
return timelineWindow;
|
|
1920
|
+
}
|
|
1921
|
+
});
|
|
1922
|
+
Object.defineProperty(exports, 'typeColour', {
|
|
1923
|
+
enumerable: true,
|
|
1924
|
+
get: function () {
|
|
1925
|
+
return typeColour;
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
Object.defineProperty(exports, 'withAlpha', {
|
|
1929
|
+
enumerable: true,
|
|
1930
|
+
get: function () {
|
|
1931
|
+
return withAlpha;
|
|
1932
|
+
}
|
|
1933
|
+
});
|