@data-navigator/bokeh-wrapper 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/dist/index.cjs +781 -0
- package/dist/index.d.cts +186 -0
- package/dist/index.d.ts +186 -0
- package/dist/index.js +747 -0
- package/package.json +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defProps = Object.defineProperties;
|
|
3
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
4
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
7
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
8
|
+
var __spreadValues = (a, b) => {
|
|
9
|
+
for (var prop in b || (b = {}))
|
|
10
|
+
if (__hasOwnProp.call(b, prop))
|
|
11
|
+
__defNormalProp(a, prop, b[prop]);
|
|
12
|
+
if (__getOwnPropSymbols)
|
|
13
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
14
|
+
if (__propIsEnum.call(b, prop))
|
|
15
|
+
__defNormalProp(a, prop, b[prop]);
|
|
16
|
+
}
|
|
17
|
+
return a;
|
|
18
|
+
};
|
|
19
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
import dataNavigator from "data-navigator";
|
|
23
|
+
|
|
24
|
+
// src/structure-builder.ts
|
|
25
|
+
function resolveEl(ref) {
|
|
26
|
+
if (typeof ref === "string")
|
|
27
|
+
return document.getElementById(ref);
|
|
28
|
+
return ref instanceof HTMLElement ? ref : null;
|
|
29
|
+
}
|
|
30
|
+
function safeId(s) {
|
|
31
|
+
return String(s).replace(/\s+/g, "-").replace(/[^\w-]/g, "");
|
|
32
|
+
}
|
|
33
|
+
function isNumericField(data, field) {
|
|
34
|
+
return data.some((d) => d[field] != null && !isNaN(Number(d[field])));
|
|
35
|
+
}
|
|
36
|
+
function hasMultiplePerX(data, xField) {
|
|
37
|
+
var _a;
|
|
38
|
+
const counts = /* @__PURE__ */ new Map();
|
|
39
|
+
for (const d of data) {
|
|
40
|
+
const v = d[xField];
|
|
41
|
+
counts.set(v, ((_a = counts.get(v)) != null ? _a : 0) + 1);
|
|
42
|
+
}
|
|
43
|
+
return Array.from(counts.values()).some((c) => c > 1);
|
|
44
|
+
}
|
|
45
|
+
function resolveChartType(type, data, xField, yField, groupField) {
|
|
46
|
+
if (type && type !== "auto")
|
|
47
|
+
return type;
|
|
48
|
+
if (!data.length)
|
|
49
|
+
return "bar";
|
|
50
|
+
if (groupField && xField && yField) {
|
|
51
|
+
return hasMultiplePerX(data, xField) ? "stacked_bar" : "multiline";
|
|
52
|
+
}
|
|
53
|
+
if (xField && yField) {
|
|
54
|
+
if (isNumericField(data, xField) && isNumericField(data, yField))
|
|
55
|
+
return "cartesian";
|
|
56
|
+
if (!isNumericField(data, xField))
|
|
57
|
+
return "bar";
|
|
58
|
+
return "line";
|
|
59
|
+
}
|
|
60
|
+
if (xField) {
|
|
61
|
+
return isNumericField(data, xField) ? "line" : "bar";
|
|
62
|
+
}
|
|
63
|
+
return "bar";
|
|
64
|
+
}
|
|
65
|
+
var exitEdge = {
|
|
66
|
+
edgeId: "dn-exit",
|
|
67
|
+
edge: {
|
|
68
|
+
source: (_d, c) => c,
|
|
69
|
+
target: () => "",
|
|
70
|
+
navigationRules: ["exit"]
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var baseNavRules = {
|
|
74
|
+
// Primary (x-axis) navigation
|
|
75
|
+
left: { key: "ArrowLeft", direction: "source" },
|
|
76
|
+
right: { key: "ArrowRight", direction: "target" },
|
|
77
|
+
// Secondary encoding (y-axis / group) navigation
|
|
78
|
+
up: { key: "ArrowUp", direction: "source" },
|
|
79
|
+
down: { key: "ArrowDown", direction: "target" },
|
|
80
|
+
// Tertiary encoding navigation ([ / ])
|
|
81
|
+
backward: { key: "BracketLeft", direction: "source" },
|
|
82
|
+
forward: { key: "BracketRight", direction: "target" },
|
|
83
|
+
// Hierarchy traversal
|
|
84
|
+
child: { key: "Enter", direction: "target" },
|
|
85
|
+
parent: { key: "Backspace", direction: "source" },
|
|
86
|
+
undo: { key: "Delete", direction: "source" },
|
|
87
|
+
// Encoding-specific parent shortcuts (W = x-axis parent, J = y-axis parent, \ = tertiary parent)
|
|
88
|
+
xParent: { key: "KeyW", direction: "source" },
|
|
89
|
+
yParent: { key: "KeyJ", direction: "source" },
|
|
90
|
+
zParent: { key: "Backslash", direction: "source" },
|
|
91
|
+
// Exit and help
|
|
92
|
+
exit: { key: "Escape", direction: "target" },
|
|
93
|
+
help: { key: "KeyY", direction: "target" }
|
|
94
|
+
};
|
|
95
|
+
function buildFlatStructure(data, xField, idField) {
|
|
96
|
+
const sorted = xField ? [...data].sort((a, b) => {
|
|
97
|
+
const av = a[xField];
|
|
98
|
+
const bv = b[xField];
|
|
99
|
+
if (typeof av === "number" && typeof bv === "number")
|
|
100
|
+
return av - bv;
|
|
101
|
+
return String(av) < String(bv) ? -1 : 1;
|
|
102
|
+
}) : [...data];
|
|
103
|
+
const ids = sorted.map(
|
|
104
|
+
(d, i) => idField && d[idField] != null ? safeId(d[idField]) : `dn-item-${i}`
|
|
105
|
+
);
|
|
106
|
+
const augmented = sorted.map((d, i) => __spreadProps(__spreadValues({}, d), { _dnId: ids[i] }));
|
|
107
|
+
return {
|
|
108
|
+
data: augmented,
|
|
109
|
+
idKey: "_dnId",
|
|
110
|
+
navigationRules: baseNavRules,
|
|
111
|
+
genericEdges: [
|
|
112
|
+
{
|
|
113
|
+
edgeId: "dn-right",
|
|
114
|
+
edge: {
|
|
115
|
+
source: (_d, c) => c,
|
|
116
|
+
target: (_d, c) => {
|
|
117
|
+
const i = ids.indexOf(c);
|
|
118
|
+
return ids[(i + 1) % ids.length];
|
|
119
|
+
},
|
|
120
|
+
navigationRules: ["right"]
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
edgeId: "dn-left",
|
|
125
|
+
edge: {
|
|
126
|
+
source: (_d, c) => {
|
|
127
|
+
const i = ids.indexOf(c);
|
|
128
|
+
return ids[(i - 1 + ids.length) % ids.length];
|
|
129
|
+
},
|
|
130
|
+
target: (_d, c) => c,
|
|
131
|
+
navigationRules: ["left"]
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
exitEdge
|
|
135
|
+
]
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function buildCrosslineStructure(data, xField, groupField) {
|
|
139
|
+
const augNavRules = __spreadValues({}, baseNavRules);
|
|
140
|
+
augNavRules.up = { key: "ArrowUp", direction: "target" };
|
|
141
|
+
augNavRules.down = { key: "ArrowDown", direction: "source" };
|
|
142
|
+
return {
|
|
143
|
+
data,
|
|
144
|
+
idKey: "id",
|
|
145
|
+
addIds: true,
|
|
146
|
+
navigationRules: augNavRules,
|
|
147
|
+
dimensions: {
|
|
148
|
+
values: [
|
|
149
|
+
{
|
|
150
|
+
dimensionKey: xField,
|
|
151
|
+
type: "categorical",
|
|
152
|
+
behavior: {
|
|
153
|
+
extents: "circular",
|
|
154
|
+
childmostNavigation: "across"
|
|
155
|
+
},
|
|
156
|
+
// left/right siblings at this dimension level; W drills back up
|
|
157
|
+
navigationRules: {
|
|
158
|
+
sibling_sibling: ["left", "right"],
|
|
159
|
+
parent_child: ["xParent", "child"]
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
dimensionKey: groupField,
|
|
164
|
+
type: "categorical",
|
|
165
|
+
behavior: {
|
|
166
|
+
extents: "circular",
|
|
167
|
+
childmostNavigation: "across"
|
|
168
|
+
},
|
|
169
|
+
// up/down siblings at this dimension level; J drills back up
|
|
170
|
+
navigationRules: {
|
|
171
|
+
sibling_sibling: ["up", "down"],
|
|
172
|
+
parent_child: ["yParent", "child"]
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
},
|
|
177
|
+
genericEdges: [exitEdge]
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function buildCartesianStructure(data, xField, yField, idField) {
|
|
181
|
+
const augmented = data.map((d, i) => {
|
|
182
|
+
const baseId = idField && d[idField] != null ? safeId(String(d[idField])) : d["id"] != null ? safeId(String(d["id"])) : `dn-pt-${i}`;
|
|
183
|
+
return __spreadProps(__spreadValues({}, d), { id: baseId });
|
|
184
|
+
});
|
|
185
|
+
const autoSubdivs = (_key, values) => Math.min(12, Math.max(3, Math.ceil(Math.sqrt(Object.keys(values).length))));
|
|
186
|
+
const augNavRules = __spreadValues({}, baseNavRules);
|
|
187
|
+
augNavRules.up = { key: "ArrowUp", direction: "target" };
|
|
188
|
+
augNavRules.down = { key: "ArrowDown", direction: "source" };
|
|
189
|
+
return {
|
|
190
|
+
data: augmented,
|
|
191
|
+
idKey: "id",
|
|
192
|
+
navigationRules: augNavRules,
|
|
193
|
+
dimensions: {
|
|
194
|
+
values: [
|
|
195
|
+
{
|
|
196
|
+
dimensionKey: xField,
|
|
197
|
+
type: "numerical",
|
|
198
|
+
behavior: {
|
|
199
|
+
extents: "terminal"
|
|
200
|
+
},
|
|
201
|
+
operations: { createNumericalSubdivisions: autoSubdivs },
|
|
202
|
+
navigationRules: {
|
|
203
|
+
sibling_sibling: ["left", "right"],
|
|
204
|
+
parent_child: ["xParent", "child"]
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
dimensionKey: yField,
|
|
209
|
+
type: "numerical",
|
|
210
|
+
behavior: {
|
|
211
|
+
extents: "terminal"
|
|
212
|
+
},
|
|
213
|
+
operations: {
|
|
214
|
+
createNumericalSubdivisions: autoSubdivs
|
|
215
|
+
// sortFunction: (a:any,b:any) => {
|
|
216
|
+
// console.log("sorting dim",yField,a,b)
|
|
217
|
+
// return b[yField] - a[yField]
|
|
218
|
+
// }
|
|
219
|
+
},
|
|
220
|
+
// divisionOptions: {
|
|
221
|
+
// sortFunction: (a:any,b:any) => {
|
|
222
|
+
// console.log("sorting div",yField,a,b)
|
|
223
|
+
// return b[yField] - a[yField]
|
|
224
|
+
// }
|
|
225
|
+
// },
|
|
226
|
+
navigationRules: {
|
|
227
|
+
sibling_sibling: ["down", "up"],
|
|
228
|
+
parent_child: ["yParent", "child"]
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
]
|
|
232
|
+
},
|
|
233
|
+
genericEdges: [exitEdge]
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function buildCartesianGroupedStructure(data, xField, yField, groupField, idField) {
|
|
237
|
+
const augmented = data.map((d, i) => {
|
|
238
|
+
const baseId = idField && d[idField] != null ? safeId(String(d[idField])) : d["id"] != null ? safeId(String(d["id"])) : `dn-pt-${i}`;
|
|
239
|
+
return __spreadProps(__spreadValues({}, d), { id: baseId });
|
|
240
|
+
});
|
|
241
|
+
const autoSubdivs = (_key, values) => Math.min(12, Math.max(3, Math.ceil(Math.sqrt(Object.keys(values).length))));
|
|
242
|
+
const augNavRules = __spreadValues({}, baseNavRules);
|
|
243
|
+
augNavRules.up = { key: "ArrowUp", direction: "target" };
|
|
244
|
+
augNavRules.down = { key: "ArrowDown", direction: "source" };
|
|
245
|
+
return {
|
|
246
|
+
data: augmented,
|
|
247
|
+
idKey: "id",
|
|
248
|
+
navigationRules: augNavRules,
|
|
249
|
+
dimensions: {
|
|
250
|
+
values: [
|
|
251
|
+
{
|
|
252
|
+
dimensionKey: xField,
|
|
253
|
+
type: "numerical",
|
|
254
|
+
behavior: { extents: "terminal" },
|
|
255
|
+
operations: { createNumericalSubdivisions: autoSubdivs },
|
|
256
|
+
navigationRules: {
|
|
257
|
+
sibling_sibling: ["left", "right"],
|
|
258
|
+
parent_child: ["xParent", "child"]
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
dimensionKey: yField,
|
|
263
|
+
type: "numerical",
|
|
264
|
+
behavior: { extents: "terminal" },
|
|
265
|
+
operations: { createNumericalSubdivisions: autoSubdivs },
|
|
266
|
+
navigationRules: {
|
|
267
|
+
sibling_sibling: ["down", "up"],
|
|
268
|
+
parent_child: ["yParent", "child"]
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
dimensionKey: groupField,
|
|
273
|
+
type: "categorical",
|
|
274
|
+
behavior: { extents: "circular" },
|
|
275
|
+
navigationRules: {
|
|
276
|
+
sibling_sibling: ["backward", "forward"],
|
|
277
|
+
parent_child: ["zParent", "child"]
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
]
|
|
281
|
+
},
|
|
282
|
+
genericEdges: [exitEdge]
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function buildDimensionStructure(data, dimensionKey, idField, compositeKey, compressSparseDivisions) {
|
|
286
|
+
let idKey;
|
|
287
|
+
let augmented = data;
|
|
288
|
+
if (compositeKey) {
|
|
289
|
+
augmented = data.map((d) => __spreadProps(__spreadValues({}, d), {
|
|
290
|
+
_dnId: `${safeId(d[dimensionKey])}-${safeId(d[compositeKey])}`
|
|
291
|
+
}));
|
|
292
|
+
idKey = "_dnId";
|
|
293
|
+
} else if (idField) {
|
|
294
|
+
idKey = idField;
|
|
295
|
+
} else {
|
|
296
|
+
idKey = dimensionKey;
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
data: augmented,
|
|
300
|
+
idKey,
|
|
301
|
+
navigationRules: baseNavRules,
|
|
302
|
+
dimensions: {
|
|
303
|
+
values: [
|
|
304
|
+
{
|
|
305
|
+
dimensionKey,
|
|
306
|
+
type: "categorical",
|
|
307
|
+
behavior: { extents: "circular" },
|
|
308
|
+
// Explicitly name rules so edge tags match keys in baseNavRules.
|
|
309
|
+
// Without this, data-navigator auto-generates 'parent_<dimensionKey>'
|
|
310
|
+
// (e.g. 'parent_city'), which won't match the 'parent' rule.
|
|
311
|
+
operations: {
|
|
312
|
+
compressSparseDivisions: compressSparseDivisions || false
|
|
313
|
+
},
|
|
314
|
+
navigationRules: {
|
|
315
|
+
sibling_sibling: ["left", "right"],
|
|
316
|
+
parent_child: ["parent", "child"]
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
]
|
|
320
|
+
},
|
|
321
|
+
genericEdges: [exitEdge]
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
var humanChartType = {
|
|
325
|
+
bar: "bar chart",
|
|
326
|
+
hbar: "horizontal bar chart",
|
|
327
|
+
scatter: "scatter plot",
|
|
328
|
+
cartesian: "scatter plot",
|
|
329
|
+
line: "line chart",
|
|
330
|
+
multiline: "multi-line chart",
|
|
331
|
+
crossline: "cross-navigable multi-line chart",
|
|
332
|
+
stacked_bar: "stacked bar chart"
|
|
333
|
+
};
|
|
334
|
+
function buildChartDescription(options, dimensionCount = 1) {
|
|
335
|
+
var _a;
|
|
336
|
+
const { data, type, xField, yField, groupField, title } = options;
|
|
337
|
+
const chartType = resolveChartType(type, data, xField, yField, groupField);
|
|
338
|
+
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
339
|
+
const parts = [];
|
|
340
|
+
if (title) {
|
|
341
|
+
parts.push(title);
|
|
342
|
+
} else if (xField && yField) {
|
|
343
|
+
parts.push(`${cap(xField)} and ${cap(yField)}`);
|
|
344
|
+
} else if (xField) {
|
|
345
|
+
parts.push(cap(xField));
|
|
346
|
+
}
|
|
347
|
+
parts.push((_a = humanChartType[chartType]) != null ? _a : chartType);
|
|
348
|
+
if (xField) {
|
|
349
|
+
const axisType = isNumericField(data, xField) ? "numerical" : "categorical";
|
|
350
|
+
parts.push(`${xField} along x axis (${axisType})`);
|
|
351
|
+
}
|
|
352
|
+
if (yField) {
|
|
353
|
+
const axisType = isNumericField(data, yField) ? "numerical" : "categorical";
|
|
354
|
+
parts.push(`${yField} along y axis (${axisType})`);
|
|
355
|
+
}
|
|
356
|
+
if (groupField) {
|
|
357
|
+
parts.push(`grouped by ${groupField}`);
|
|
358
|
+
}
|
|
359
|
+
const total = data.length;
|
|
360
|
+
if (dimensionCount > 1) {
|
|
361
|
+
parts.push(`contains ${dimensionCount} dimensions and ${total} total data points`);
|
|
362
|
+
} else {
|
|
363
|
+
let divisionKey;
|
|
364
|
+
if (chartType === "bar" || chartType === "hbar")
|
|
365
|
+
divisionKey = xField;
|
|
366
|
+
else if (chartType === "stacked_bar")
|
|
367
|
+
divisionKey = xField;
|
|
368
|
+
else if (chartType === "multiline")
|
|
369
|
+
divisionKey = groupField;
|
|
370
|
+
if (divisionKey) {
|
|
371
|
+
const divisions = new Set(data.map((d) => d[divisionKey])).size;
|
|
372
|
+
if (divisions !== total) {
|
|
373
|
+
parts.push(`contains ${divisions} divisions and ${total} total data points`);
|
|
374
|
+
} else {
|
|
375
|
+
parts.push(`contains ${total} total data points`);
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
parts.push(`contains ${total} total data points`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return parts.map(cap).join(". ") + ".";
|
|
382
|
+
}
|
|
383
|
+
function buildStructureOptions(options) {
|
|
384
|
+
var _a;
|
|
385
|
+
const { data, type, xField, yField, groupField, idField, structureOptions = {} } = options;
|
|
386
|
+
const chartType = resolveChartType(type, data, xField, yField, groupField);
|
|
387
|
+
let base;
|
|
388
|
+
switch (chartType) {
|
|
389
|
+
case "scatter":
|
|
390
|
+
case "line":
|
|
391
|
+
base = buildFlatStructure(data, xField, idField);
|
|
392
|
+
break;
|
|
393
|
+
case "bar":
|
|
394
|
+
case "hbar": {
|
|
395
|
+
const catField = xField || Object.keys((_a = data[0]) != null ? _a : {}).find((k) => !isNumericField(data, k)) || "x";
|
|
396
|
+
base = buildDimensionStructure(data, catField, idField, "", options.compressSparseDivisions);
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
case "stacked_bar": {
|
|
400
|
+
const stackX = xField || "x";
|
|
401
|
+
const stackGroup = groupField || "group";
|
|
402
|
+
base = buildCrosslineStructure(data, stackX, stackGroup);
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
case "multiline": {
|
|
406
|
+
const lineGroup = groupField || "series";
|
|
407
|
+
base = buildDimensionStructure(data, lineGroup, idField, xField, options.compressSparseDivisions);
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
case "cartesian": {
|
|
411
|
+
const cartX = xField || "x";
|
|
412
|
+
const cartY = yField || "y";
|
|
413
|
+
if (groupField) {
|
|
414
|
+
base = buildCartesianGroupedStructure(data, cartX, cartY, groupField, idField);
|
|
415
|
+
} else {
|
|
416
|
+
base = buildCartesianStructure(data, cartX, cartY, idField);
|
|
417
|
+
}
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
case "crossline": {
|
|
421
|
+
const crossX = xField || "x";
|
|
422
|
+
const crossGroup = groupField || "series";
|
|
423
|
+
base = buildCrosslineStructure(data, crossX, crossGroup);
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
default:
|
|
427
|
+
base = buildFlatStructure(data, xField, idField);
|
|
428
|
+
}
|
|
429
|
+
return __spreadValues(__spreadValues({}, base), structureOptions);
|
|
430
|
+
}
|
|
431
|
+
function buildCommandLabels(options) {
|
|
432
|
+
const { data, type, xField, yField, groupField, commandLabels = {} } = options;
|
|
433
|
+
const chartType = resolveChartType(type, data, xField, yField, groupField);
|
|
434
|
+
const auto = {};
|
|
435
|
+
switch (chartType) {
|
|
436
|
+
case "bar":
|
|
437
|
+
case "hbar":
|
|
438
|
+
auto.left = `Move to previous ${xField != null ? xField : "category"}`;
|
|
439
|
+
auto.right = `Move to next ${xField != null ? xField : "category"}`;
|
|
440
|
+
auto.child = `Drill into ${xField != null ? xField : "category"} detail`;
|
|
441
|
+
auto.parent = "Go back up";
|
|
442
|
+
auto.undo = "Go back up";
|
|
443
|
+
break;
|
|
444
|
+
case "scatter":
|
|
445
|
+
case "line":
|
|
446
|
+
auto.left = `Move to previous ${xField != null ? xField : "point"}`;
|
|
447
|
+
auto.right = `Move to next ${xField != null ? xField : "point"}`;
|
|
448
|
+
break;
|
|
449
|
+
case "stacked_bar":
|
|
450
|
+
auto.left = `Move to previous ${xField != null ? xField : "category"}`;
|
|
451
|
+
auto.right = `Move to next ${xField != null ? xField : "category"}`;
|
|
452
|
+
auto.up = `Move to previous ${groupField != null ? groupField : "group"}`;
|
|
453
|
+
auto.down = `Move to next ${groupField != null ? groupField : "group"}`;
|
|
454
|
+
auto.child = "Drill in";
|
|
455
|
+
auto.xParent = `Go up to ${xField != null ? xField : "category"} level`;
|
|
456
|
+
auto.yParent = `Go up to ${groupField != null ? groupField : "group"} level`;
|
|
457
|
+
break;
|
|
458
|
+
case "multiline":
|
|
459
|
+
auto.left = `Move to previous ${groupField != null ? groupField : "series"}`;
|
|
460
|
+
auto.right = `Move to next ${groupField != null ? groupField : "series"}`;
|
|
461
|
+
auto.child = `Drill into ${groupField != null ? groupField : "series"} data`;
|
|
462
|
+
auto.parent = "Go back up";
|
|
463
|
+
auto.undo = "Go back up";
|
|
464
|
+
break;
|
|
465
|
+
case "cartesian":
|
|
466
|
+
auto.left = `Move to previous ${xField != null ? xField : "x"} range`;
|
|
467
|
+
auto.right = `Move to next ${xField != null ? xField : "x"} range`;
|
|
468
|
+
auto.up = `Move to higher ${yField != null ? yField : "y"} range`;
|
|
469
|
+
auto.down = `Move to lower ${yField != null ? yField : "y"} range`;
|
|
470
|
+
auto.child = "Drill in";
|
|
471
|
+
auto.xParent = `Go up to ${xField != null ? xField : "x"} level`;
|
|
472
|
+
auto.yParent = `Go up to ${yField != null ? yField : "y"} level`;
|
|
473
|
+
if (groupField) {
|
|
474
|
+
auto.backward = `Move to previous ${groupField} group`;
|
|
475
|
+
auto.forward = `Move to next ${groupField} group`;
|
|
476
|
+
auto.zParent = `Go up to ${groupField} level`;
|
|
477
|
+
}
|
|
478
|
+
break;
|
|
479
|
+
case "crossline":
|
|
480
|
+
auto.left = `Move to previous ${xField != null ? xField : "x-axis point"}`;
|
|
481
|
+
auto.right = `Move to next ${xField != null ? xField : "x-axis point"}`;
|
|
482
|
+
auto.up = `Move to previous ${groupField != null ? groupField : "series"}`;
|
|
483
|
+
auto.down = `Move to next ${groupField != null ? groupField : "series"}`;
|
|
484
|
+
auto.child = "Drill in";
|
|
485
|
+
auto.xParent = `Go up to ${xField != null ? xField : "x-axis"} level`;
|
|
486
|
+
auto.yParent = `Go up to ${groupField != null ? groupField : "series"} level`;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
return __spreadValues(__spreadValues({}, auto), commandLabels);
|
|
490
|
+
}
|
|
491
|
+
function buildNodeLabel(node) {
|
|
492
|
+
const data = node.data;
|
|
493
|
+
if (!data)
|
|
494
|
+
return String(node.id);
|
|
495
|
+
if (typeof data.dimensionKey === "string" && data.divisions != null) {
|
|
496
|
+
const divCount = Object.keys(data.divisions).length;
|
|
497
|
+
return `${data.dimensionKey} dimension. Contains ${divCount} division${divCount !== 1 ? "s" : ""}.`;
|
|
498
|
+
}
|
|
499
|
+
if (data.values != null && typeof data.values === "object" && !Array.isArray(data.values)) {
|
|
500
|
+
const dimKey = node.derivedNode;
|
|
501
|
+
const dimValue = dimKey ? data[dimKey] : null;
|
|
502
|
+
if (dimValue != null) {
|
|
503
|
+
const childCount = Object.keys(data.values).length;
|
|
504
|
+
return `${dimValue}. Contains ${childCount} data point${childCount !== 1 ? "s" : ""}.`;
|
|
505
|
+
}
|
|
506
|
+
return String(node.id);
|
|
507
|
+
}
|
|
508
|
+
const parts = Object.entries(data).filter(
|
|
509
|
+
([k, v]) => !k.startsWith("_") && v != null && typeof v !== "object" && typeof v !== "function"
|
|
510
|
+
).map(([k, v]) => `${k}: ${v}`);
|
|
511
|
+
return parts.length > 0 ? parts.join(". ") + "." : String(node.id);
|
|
512
|
+
}
|
|
513
|
+
function prepareNodeSemantics(structure) {
|
|
514
|
+
var _a, _b;
|
|
515
|
+
for (const node of Object.values(structure.nodes)) {
|
|
516
|
+
const n = node;
|
|
517
|
+
if (!((_a = n.semantics) == null ? void 0 : _a.label)) {
|
|
518
|
+
n.semantics = __spreadProps(__spreadValues({}, (_b = n.semantics) != null ? _b : {}), { label: buildNodeLabel(n) });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/index.ts
|
|
524
|
+
function resolveChatContainer(options, plotEl) {
|
|
525
|
+
var _a, _b;
|
|
526
|
+
if (options.chatContainer) {
|
|
527
|
+
const el = resolveEl(options.chatContainer);
|
|
528
|
+
if (el)
|
|
529
|
+
return { el, owned: false };
|
|
530
|
+
}
|
|
531
|
+
const div = document.createElement("div");
|
|
532
|
+
div.className = "dn-bokeh-wrapper";
|
|
533
|
+
(_b = (_a = plotEl.parentElement) == null ? void 0 : _a.insertBefore(div, plotEl.nextSibling)) != null ? _b : document.body.appendChild(div);
|
|
534
|
+
return { el: div, owned: true };
|
|
535
|
+
}
|
|
536
|
+
function setupKeyboardMode(structure, plotEl, options, entryPoint) {
|
|
537
|
+
var _a, _b;
|
|
538
|
+
const { onNavigate, onExit, renderingOptions = {} } = options;
|
|
539
|
+
const width = plotEl.clientWidth || 400;
|
|
540
|
+
const height = plotEl.clientHeight || 300;
|
|
541
|
+
let current = null;
|
|
542
|
+
let previous = null;
|
|
543
|
+
const resolvedEntry = entryPoint != null ? entryPoint : Object.keys(structure.nodes)[0];
|
|
544
|
+
const suffixId = `dn-bokeh-${Math.random().toString(36).slice(2, 6)}`;
|
|
545
|
+
const rendering = dataNavigator.rendering({
|
|
546
|
+
elementData: structure.nodes,
|
|
547
|
+
defaults: __spreadValues({
|
|
548
|
+
cssClass: "dn-node",
|
|
549
|
+
spatialProperties: { x: 0, y: 0, width, height }
|
|
550
|
+
}, renderingOptions.defaults),
|
|
551
|
+
suffixId,
|
|
552
|
+
root: {
|
|
553
|
+
id: typeof options.plotContainer === "string" ? options.plotContainer : options.plotContainer.id || "dn-bokeh-root",
|
|
554
|
+
description: "Accessible data navigation",
|
|
555
|
+
width: "100%",
|
|
556
|
+
height: 0
|
|
557
|
+
},
|
|
558
|
+
entryButton: { include: true, callbacks: { click: enter } },
|
|
559
|
+
exitElement: { include: true }
|
|
560
|
+
});
|
|
561
|
+
rendering.initialize();
|
|
562
|
+
if (rendering.entryButton) {
|
|
563
|
+
rendering.entryButton.addEventListener("keydown", (e) => {
|
|
564
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
565
|
+
e.preventDefault();
|
|
566
|
+
if (!current)
|
|
567
|
+
enter();
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
const input = dataNavigator.input({
|
|
572
|
+
structure,
|
|
573
|
+
navigationRules: (_a = structure.navigationRules) != null ? _a : {},
|
|
574
|
+
entryPoint: resolvedEntry,
|
|
575
|
+
exitPoint: (_b = rendering.exitElement) == null ? void 0 : _b.id
|
|
576
|
+
});
|
|
577
|
+
function enter() {
|
|
578
|
+
const node = input.enter();
|
|
579
|
+
if (!node)
|
|
580
|
+
return;
|
|
581
|
+
navigate(node);
|
|
582
|
+
}
|
|
583
|
+
function navigate(node) {
|
|
584
|
+
if (!node.renderId)
|
|
585
|
+
node.renderId = node.id;
|
|
586
|
+
if (!node.spatialProperties) {
|
|
587
|
+
node.spatialProperties = { x: 0, y: 0, width, height };
|
|
588
|
+
}
|
|
589
|
+
if (previous)
|
|
590
|
+
rendering.remove(previous);
|
|
591
|
+
const el = rendering.render({ renderId: node.renderId, datum: node });
|
|
592
|
+
if (!el)
|
|
593
|
+
return;
|
|
594
|
+
el.style.width = "100%";
|
|
595
|
+
el.style.height = "100%";
|
|
596
|
+
el.style.top = "0";
|
|
597
|
+
el.style.left = "0";
|
|
598
|
+
el.addEventListener("keydown", (e) => {
|
|
599
|
+
const direction = input.keydownValidator(e);
|
|
600
|
+
if (direction) {
|
|
601
|
+
e.preventDefault();
|
|
602
|
+
if (direction === "exit") {
|
|
603
|
+
if (rendering.exitElement) {
|
|
604
|
+
rendering.exitElement.style.display = "block";
|
|
605
|
+
input.focus(rendering.exitElement.id);
|
|
606
|
+
}
|
|
607
|
+
} else {
|
|
608
|
+
const next = input.move(current, direction);
|
|
609
|
+
if (next)
|
|
610
|
+
navigate(next);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
el.addEventListener("focus", () => {
|
|
615
|
+
if (onNavigate)
|
|
616
|
+
onNavigate(node);
|
|
617
|
+
});
|
|
618
|
+
input.focus(node.renderId);
|
|
619
|
+
previous = current;
|
|
620
|
+
current = node.id;
|
|
621
|
+
}
|
|
622
|
+
if (rendering.exitElement) {
|
|
623
|
+
rendering.exitElement.addEventListener("focus", () => {
|
|
624
|
+
if (current)
|
|
625
|
+
rendering.remove(current);
|
|
626
|
+
current = null;
|
|
627
|
+
if (onExit)
|
|
628
|
+
onExit();
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
return {
|
|
632
|
+
destroy() {
|
|
633
|
+
var _a2, _b2, _c, _d;
|
|
634
|
+
try {
|
|
635
|
+
(_b2 = (_a2 = rendering.wrapper) == null ? void 0 : _a2.remove) == null ? void 0 : _b2.call(_a2);
|
|
636
|
+
} catch (_) {
|
|
637
|
+
}
|
|
638
|
+
try {
|
|
639
|
+
(_d = (_c = rendering.exitElement) == null ? void 0 : _c.remove) == null ? void 0 : _d.call(_c);
|
|
640
|
+
} catch (_) {
|
|
641
|
+
}
|
|
642
|
+
},
|
|
643
|
+
getCurrentNode() {
|
|
644
|
+
var _a2;
|
|
645
|
+
return current ? (_a2 = structure.nodes[current]) != null ? _a2 : null : null;
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function addDataNavigator(options) {
|
|
650
|
+
var _a, _b, _c, _d, _e;
|
|
651
|
+
const { mode = "text", onNavigate, onExit, onClick, onHover, llm } = options;
|
|
652
|
+
const plotEl = resolveEl(options.plotContainer);
|
|
653
|
+
if (!plotEl) {
|
|
654
|
+
throw new Error(
|
|
655
|
+
`@data-navigator/bokeh-wrapper: plotContainer "${options.plotContainer}" not found.`
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
const didSetInert = mode === "text";
|
|
659
|
+
if (didSetInert) {
|
|
660
|
+
plotEl.setAttribute("inert", "true");
|
|
661
|
+
}
|
|
662
|
+
const structOpts = buildStructureOptions(options);
|
|
663
|
+
const dimCount = (_c = (_b = (_a = structOpts.dimensions) == null ? void 0 : _a.values) == null ? void 0 : _b.length) != null ? _c : 0;
|
|
664
|
+
const resolveDescription = () => {
|
|
665
|
+
if (typeof options.describeRoot === "function")
|
|
666
|
+
return options.describeRoot(options);
|
|
667
|
+
if (typeof options.describeRoot === "string")
|
|
668
|
+
return options.describeRoot;
|
|
669
|
+
return buildChartDescription(options, dimCount);
|
|
670
|
+
};
|
|
671
|
+
if (dimCount > 1) {
|
|
672
|
+
const dims = structOpts.dimensions;
|
|
673
|
+
if (!dims.parentOptions)
|
|
674
|
+
dims.parentOptions = {};
|
|
675
|
+
if (!dims.parentOptions.addLevel0) {
|
|
676
|
+
const plotId = typeof options.plotContainer === "string" ? options.plotContainer.replace(/[^a-zA-Z0-9_-]/g, "") : "dn";
|
|
677
|
+
dims.parentOptions.addLevel0 = {
|
|
678
|
+
id: `${plotId}-chart-root`,
|
|
679
|
+
edges: [],
|
|
680
|
+
semantics: { label: resolveDescription() }
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
const structure = dataNavigator.structure(structOpts);
|
|
685
|
+
if (dimCount === 1 && structure.dimensions) {
|
|
686
|
+
const desc = resolveDescription();
|
|
687
|
+
for (const dim of Object.values(structure.dimensions)) {
|
|
688
|
+
const node = structure.nodes[dim.nodeId];
|
|
689
|
+
if (node)
|
|
690
|
+
node.semantics = { label: desc };
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
prepareNodeSemantics(structure);
|
|
694
|
+
const level0Node = Object.values(structure.nodes).find((n) => n.dimensionLevel === 0);
|
|
695
|
+
const resolvedEntryPoint = (_e = level0Node == null ? void 0 : level0Node.id) != null ? _e : structure.dimensions ? (_d = structure.dimensions[Object.keys(structure.dimensions)[0]]) == null ? void 0 : _d.nodeId : void 0;
|
|
696
|
+
const cleanups = [];
|
|
697
|
+
let textChatInstance = null;
|
|
698
|
+
if (mode === "text" || mode === "both") {
|
|
699
|
+
const { el: chatEl, owned } = resolveChatContainer(options, plotEl);
|
|
700
|
+
textChatInstance = dataNavigator.textChat({
|
|
701
|
+
structure,
|
|
702
|
+
container: chatEl,
|
|
703
|
+
entryPoint: resolvedEntryPoint,
|
|
704
|
+
data: options.data,
|
|
705
|
+
commandLabels: buildCommandLabels(options),
|
|
706
|
+
onNavigate,
|
|
707
|
+
onExit,
|
|
708
|
+
onClick,
|
|
709
|
+
onHover,
|
|
710
|
+
llm
|
|
711
|
+
});
|
|
712
|
+
if (owned) {
|
|
713
|
+
cleanups.push(() => {
|
|
714
|
+
var _a2;
|
|
715
|
+
try {
|
|
716
|
+
(_a2 = chatEl.parentElement) == null ? void 0 : _a2.removeChild(chatEl);
|
|
717
|
+
} catch (_) {
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
let keyboardMode = null;
|
|
723
|
+
if (mode === "keyboard" || mode === "both") {
|
|
724
|
+
keyboardMode = setupKeyboardMode(structure, plotEl, options, resolvedEntryPoint);
|
|
725
|
+
cleanups.push(() => keyboardMode == null ? void 0 : keyboardMode.destroy());
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
destroy() {
|
|
729
|
+
textChatInstance == null ? void 0 : textChatInstance.destroy();
|
|
730
|
+
for (const cleanup of cleanups)
|
|
731
|
+
cleanup();
|
|
732
|
+
if (didSetInert)
|
|
733
|
+
plotEl.removeAttribute("inert");
|
|
734
|
+
},
|
|
735
|
+
getCurrentNode() {
|
|
736
|
+
var _a2, _b2;
|
|
737
|
+
return (_b2 = (_a2 = textChatInstance == null ? void 0 : textChatInstance.getCurrentNode()) != null ? _a2 : keyboardMode == null ? void 0 : keyboardMode.getCurrentNode()) != null ? _b2 : null;
|
|
738
|
+
},
|
|
739
|
+
structure
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
export {
|
|
743
|
+
addDataNavigator,
|
|
744
|
+
buildChartDescription,
|
|
745
|
+
buildNodeLabel,
|
|
746
|
+
prepareNodeSemantics
|
|
747
|
+
};
|