anywidget-vector 0.1.0__py3-none-any.whl → 0.2.1__py3-none-any.whl

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.
Files changed (34) hide show
  1. anywidget_vector/__init__.py +1 -1
  2. anywidget_vector/backends/__init__.py +103 -0
  3. anywidget_vector/backends/chroma/__init__.py +27 -0
  4. anywidget_vector/backends/chroma/client.py +60 -0
  5. anywidget_vector/backends/chroma/converter.py +86 -0
  6. anywidget_vector/backends/grafeo/__init__.py +20 -0
  7. anywidget_vector/backends/grafeo/client.py +33 -0
  8. anywidget_vector/backends/grafeo/converter.py +46 -0
  9. anywidget_vector/backends/lancedb/__init__.py +22 -0
  10. anywidget_vector/backends/lancedb/client.py +56 -0
  11. anywidget_vector/backends/lancedb/converter.py +71 -0
  12. anywidget_vector/backends/pinecone/__init__.py +21 -0
  13. anywidget_vector/backends/pinecone/client.js +45 -0
  14. anywidget_vector/backends/pinecone/converter.py +62 -0
  15. anywidget_vector/backends/qdrant/__init__.py +26 -0
  16. anywidget_vector/backends/qdrant/client.js +61 -0
  17. anywidget_vector/backends/qdrant/converter.py +83 -0
  18. anywidget_vector/backends/weaviate/__init__.py +33 -0
  19. anywidget_vector/backends/weaviate/client.js +50 -0
  20. anywidget_vector/backends/weaviate/converter.py +81 -0
  21. anywidget_vector/static/icons.js +14 -0
  22. anywidget_vector/traitlets.py +84 -0
  23. anywidget_vector/ui/__init__.py +206 -0
  24. anywidget_vector/ui/canvas.js +521 -0
  25. anywidget_vector/ui/constants.js +64 -0
  26. anywidget_vector/ui/properties.js +158 -0
  27. anywidget_vector/ui/settings.js +265 -0
  28. anywidget_vector/ui/styles.css +348 -0
  29. anywidget_vector/ui/toolbar.js +117 -0
  30. anywidget_vector/widget.py +187 -850
  31. {anywidget_vector-0.1.0.dist-info → anywidget_vector-0.2.1.dist-info}/METADATA +70 -3
  32. anywidget_vector-0.2.1.dist-info/RECORD +34 -0
  33. anywidget_vector-0.1.0.dist-info/RECORD +0 -6
  34. {anywidget_vector-0.1.0.dist-info → anywidget_vector-0.2.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,521 @@
1
+ // 3D Canvas component using Three.js
2
+ import * as THREE from "https://esm.sh/three@0.160.0";
3
+ import { OrbitControls } from "https://esm.sh/three@0.160.0/addons/controls/OrbitControls.js";
4
+ import { COLOR_SCALES, CATEGORICAL_COLORS } from "./constants.js";
5
+
6
+ // Shape geometries factory
7
+ const SHAPES = {
8
+ sphere: () => new THREE.SphereGeometry(1, 16, 16),
9
+ cube: () => new THREE.BoxGeometry(1, 1, 1),
10
+ cone: () => new THREE.ConeGeometry(0.7, 1.4, 16),
11
+ tetrahedron: () => new THREE.TetrahedronGeometry(1),
12
+ octahedron: () => new THREE.OctahedronGeometry(1),
13
+ cylinder: () => new THREE.CylinderGeometry(0.5, 0.5, 1, 16),
14
+ };
15
+
16
+ // Distance metrics
17
+ const DISTANCE_METRICS = {
18
+ euclidean: (a, b) => {
19
+ const dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z;
20
+ return Math.sqrt(dx*dx + dy*dy + dz*dz);
21
+ },
22
+ cosine: (a, b) => {
23
+ const dot = a.x*b.x + a.y*b.y + a.z*b.z;
24
+ const magA = Math.sqrt(a.x*a.x + a.y*a.y + a.z*a.z);
25
+ const magB = Math.sqrt(b.x*b.x + b.y*b.y + b.z*b.z);
26
+ if (magA === 0 || magB === 0) return 1;
27
+ return 1 - (dot / (magA * magB));
28
+ },
29
+ manhattan: (a, b) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y) + Math.abs(a.z - b.z),
30
+ dot_product: (a, b) => -(a.x*b.x + a.y*b.y + a.z*b.z),
31
+ };
32
+
33
+ export function createCanvas(model, container, callbacks) {
34
+ let scene, camera, renderer, controls;
35
+ let pointsGroup, connectionsGroup;
36
+ let raycaster, mouse;
37
+ let hoveredObject = null;
38
+ let tooltip;
39
+ let axesGroup, gridHelper;
40
+ let animationId;
41
+
42
+ init();
43
+ animate();
44
+
45
+ function init() {
46
+ // Scene
47
+ scene = new THREE.Scene();
48
+ scene.background = new THREE.Color(model.get("background"));
49
+
50
+ // Camera
51
+ const aspect = model.get("width") / model.get("height");
52
+ camera = new THREE.PerspectiveCamera(60, aspect, 0.01, 1000);
53
+ const camPos = model.get("camera_position") || [2, 2, 2];
54
+ camera.position.set(camPos[0], camPos[1], camPos[2]);
55
+
56
+ // Renderer
57
+ renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
58
+ renderer.setSize(model.get("width"), model.get("height"));
59
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
60
+ container.appendChild(renderer.domElement);
61
+
62
+ // Controls
63
+ controls = new OrbitControls(camera, renderer.domElement);
64
+ controls.enableDamping = true;
65
+ controls.dampingFactor = 0.05;
66
+ const target = model.get("camera_target") || [0, 0, 0];
67
+ controls.target.set(target[0], target[1], target[2]);
68
+ controls.addEventListener("change", onCameraChange);
69
+
70
+ // Lighting
71
+ scene.add(new THREE.AmbientLight(0xffffff, 0.6));
72
+ const directional = new THREE.DirectionalLight(0xffffff, 0.8);
73
+ directional.position.set(5, 10, 7);
74
+ scene.add(directional);
75
+
76
+ // Groups
77
+ pointsGroup = new THREE.Group();
78
+ scene.add(pointsGroup);
79
+ connectionsGroup = new THREE.Group();
80
+ scene.add(connectionsGroup);
81
+ axesGroup = new THREE.Group();
82
+ scene.add(axesGroup);
83
+
84
+ // Setup
85
+ setupAxesAndGrid();
86
+ setupRaycaster();
87
+ setupTooltip();
88
+ createPoints();
89
+ createConnections();
90
+ bindModelEvents();
91
+ }
92
+
93
+ function setupAxesAndGrid() {
94
+ while (axesGroup.children.length > 0) axesGroup.remove(axesGroup.children[0]);
95
+ if (gridHelper) { scene.remove(gridHelper); gridHelper = null; }
96
+
97
+ if (model.get("show_axes")) {
98
+ const axes = new THREE.AxesHelper(1.2);
99
+ axesGroup.add(axes);
100
+ const labels = model.get("axis_labels") || { x: "X", y: "Y", z: "Z" };
101
+ addAxisLabel(labels.x, [1.3, 0, 0], 0xff4444);
102
+ addAxisLabel(labels.y, [0, 1.3, 0], 0x44ff44);
103
+ addAxisLabel(labels.z, [0, 0, 1.3], 0x4444ff);
104
+ }
105
+
106
+ if (model.get("show_grid")) {
107
+ gridHelper = new THREE.GridHelper(2, model.get("grid_divisions") || 10, 0x444444, 0x333333);
108
+ scene.add(gridHelper);
109
+ }
110
+ }
111
+
112
+ function addAxisLabel(text, position, color) {
113
+ const canvas = document.createElement("canvas");
114
+ const ctx = canvas.getContext("2d");
115
+ canvas.width = 64; canvas.height = 32;
116
+ ctx.font = "bold 24px Arial";
117
+ ctx.fillStyle = "#" + color.toString(16).padStart(6, "0");
118
+ ctx.textAlign = "center";
119
+ ctx.fillText(text, 32, 24);
120
+ const texture = new THREE.CanvasTexture(canvas);
121
+ const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: texture }));
122
+ sprite.position.set(position[0], position[1], position[2]);
123
+ sprite.scale.set(0.25, 0.125, 1);
124
+ axesGroup.add(sprite);
125
+ }
126
+
127
+ function createPoints() {
128
+ while (pointsGroup.children.length > 0) {
129
+ const obj = pointsGroup.children[0];
130
+ if (obj.geometry) obj.geometry.dispose();
131
+ if (obj.material) obj.material.dispose();
132
+ pointsGroup.remove(obj);
133
+ }
134
+
135
+ const points = model.get("points") || [];
136
+ if (points.length === 0) return;
137
+
138
+ const opts = {
139
+ colorField: model.get("color_field"),
140
+ colorScale: model.get("color_scale") || "viridis",
141
+ colorDomain: model.get("color_domain"),
142
+ sizeField: model.get("size_field"),
143
+ sizeRange: model.get("size_range") || [0.02, 0.1],
144
+ shapeField: model.get("shape_field"),
145
+ shapeMap: model.get("shape_map") || {},
146
+ };
147
+
148
+ // Compute domains
149
+ if (opts.colorField && !opts.colorDomain) {
150
+ const values = points.map(p => p[opts.colorField]).filter(v => typeof v === "number");
151
+ if (values.length > 0) opts.colorDomain = [Math.min(...values), Math.max(...values)];
152
+ }
153
+ if (opts.sizeField) {
154
+ const values = points.map(p => p[opts.sizeField]).filter(v => typeof v === "number");
155
+ if (values.length > 0) opts.sizeDomain = [Math.min(...values), Math.max(...values)];
156
+ }
157
+
158
+ const useInstancing = model.get("use_instancing") && points.length > 100;
159
+ if (useInstancing) {
160
+ createInstancedPoints(points, opts);
161
+ } else {
162
+ createIndividualPoints(points, opts);
163
+ }
164
+ }
165
+
166
+ function getPointColor(point, opts) {
167
+ if (point.color) return new THREE.Color(point.color);
168
+ if (opts.colorField && point[opts.colorField] !== undefined) {
169
+ const value = point[opts.colorField];
170
+ if (typeof value === "number") {
171
+ return getColorFromScale(value, opts.colorScale, opts.colorDomain);
172
+ }
173
+ return getCategoricalColor(value);
174
+ }
175
+ return new THREE.Color(0x6366f1);
176
+ }
177
+
178
+ function getPointSize(point, opts) {
179
+ if (point.size !== undefined) return point.size;
180
+ if (opts.sizeField && point[opts.sizeField] !== undefined && opts.sizeDomain) {
181
+ const [min, max] = opts.sizeDomain;
182
+ const t = max > min ? (point[opts.sizeField] - min) / (max - min) : 0.5;
183
+ return opts.sizeRange[0] + t * (opts.sizeRange[1] - opts.sizeRange[0]);
184
+ }
185
+ return (opts.sizeRange[0] + opts.sizeRange[1]) * 0.5;
186
+ }
187
+
188
+ function getPointShape(point, opts) {
189
+ if (point.shape && SHAPES[point.shape]) return point.shape;
190
+ if (opts.shapeField && point[opts.shapeField] !== undefined) {
191
+ const value = String(point[opts.shapeField]);
192
+ if (opts.shapeMap[value] && SHAPES[opts.shapeMap[value]]) return opts.shapeMap[value];
193
+ const shapes = Object.keys(SHAPES);
194
+ return shapes[hashString(value) % shapes.length];
195
+ }
196
+ return "sphere";
197
+ }
198
+
199
+ function createIndividualPoints(points, opts) {
200
+ points.forEach((point, idx) => {
201
+ const shape = getPointShape(point, opts);
202
+ const geometry = SHAPES[shape]();
203
+ const color = getPointColor(point, opts);
204
+ const material = new THREE.MeshPhongMaterial({ color });
205
+ const mesh = new THREE.Mesh(geometry, material);
206
+ const size = getPointSize(point, opts);
207
+ mesh.scale.set(size, size, size);
208
+ mesh.position.set(point.x ?? 0, point.y ?? 0, point.z ?? 0);
209
+ mesh.userData = { pointIndex: idx, pointId: point.id || `point_${idx}` };
210
+ pointsGroup.add(mesh);
211
+ });
212
+ }
213
+
214
+ function createInstancedPoints(points, opts) {
215
+ const groups = {};
216
+ points.forEach((point, idx) => {
217
+ const shape = getPointShape(point, opts);
218
+ if (!groups[shape]) groups[shape] = [];
219
+ groups[shape].push({ point, idx });
220
+ });
221
+
222
+ for (const [shape, items] of Object.entries(groups)) {
223
+ const geometry = SHAPES[shape]();
224
+ const material = new THREE.MeshPhongMaterial({ vertexColors: false });
225
+ const instancedMesh = new THREE.InstancedMesh(geometry, material, items.length);
226
+ const matrix = new THREE.Matrix4();
227
+ const colors = new Float32Array(items.length * 3);
228
+
229
+ items.forEach(({ point, idx }, i) => {
230
+ const size = getPointSize(point, opts);
231
+ const pointColor = getPointColor(point, opts);
232
+ matrix.identity();
233
+ matrix.makeScale(size, size, size);
234
+ matrix.setPosition(point.x ?? 0, point.y ?? 0, point.z ?? 0);
235
+ instancedMesh.setMatrixAt(i, matrix);
236
+ colors[i * 3] = pointColor.r;
237
+ colors[i * 3 + 1] = pointColor.g;
238
+ colors[i * 3 + 2] = pointColor.b;
239
+ });
240
+
241
+ geometry.setAttribute("color", new THREE.InstancedBufferAttribute(colors, 3));
242
+ material.vertexColors = true;
243
+ instancedMesh.instanceMatrix.needsUpdate = true;
244
+ instancedMesh.userData = {
245
+ isInstanced: true,
246
+ pointIndices: items.map(({ idx }) => idx),
247
+ pointIds: items.map(({ point, idx }) => point.id || `point_${idx}`)
248
+ };
249
+ pointsGroup.add(instancedMesh);
250
+ }
251
+ }
252
+
253
+ function createConnections() {
254
+ while (connectionsGroup.children.length > 0) {
255
+ const obj = connectionsGroup.children[0];
256
+ if (obj.geometry) obj.geometry.dispose();
257
+ if (obj.material) obj.material.dispose();
258
+ connectionsGroup.remove(obj);
259
+ }
260
+
261
+ const points = model.get("points") || [];
262
+ const showConnections = model.get("show_connections");
263
+ if (!showConnections || points.length < 2) return;
264
+
265
+ const kNeighbors = model.get("k_neighbors") || 0;
266
+ const distanceThreshold = model.get("distance_threshold");
267
+ const referencePoint = model.get("reference_point");
268
+ const distanceMetric = model.get("distance_metric") || "euclidean";
269
+ const connectionColor = model.get("connection_color") || "#ffffff";
270
+ const connectionOpacity = model.get("connection_opacity") || 0.3;
271
+
272
+ const material = new THREE.LineBasicMaterial({
273
+ color: new THREE.Color(connectionColor),
274
+ transparent: true,
275
+ opacity: connectionOpacity,
276
+ });
277
+
278
+ const computeDist = (p1, p2) => {
279
+ const fn = DISTANCE_METRICS[distanceMetric] || DISTANCE_METRICS.euclidean;
280
+ return fn(p1, p2);
281
+ };
282
+
283
+ if (referencePoint) {
284
+ const refIdx = points.findIndex(p => p.id === referencePoint);
285
+ if (refIdx === -1) return;
286
+ const ref = points[refIdx];
287
+ const distances = points.map((p, i) => ({
288
+ idx: i, point: p,
289
+ dist: i === refIdx ? Infinity : computeDist(ref, p)
290
+ })).filter(d => d.dist !== Infinity).sort((a, b) => a.dist - b.dist);
291
+
292
+ let neighbors;
293
+ if (distanceThreshold != null) neighbors = distances.filter(d => d.dist <= distanceThreshold);
294
+ else if (kNeighbors > 0) neighbors = distances.slice(0, kNeighbors);
295
+ else return;
296
+
297
+ neighbors.forEach(n => {
298
+ const geometry = new THREE.BufferGeometry();
299
+ geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array([
300
+ ref.x ?? 0, ref.y ?? 0, ref.z ?? 0,
301
+ n.point.x ?? 0, n.point.y ?? 0, n.point.z ?? 0
302
+ ]), 3));
303
+ connectionsGroup.add(new THREE.Line(geometry, material));
304
+ });
305
+ } else if (kNeighbors > 0) {
306
+ points.forEach((p, i) => {
307
+ const distances = points.map((other, j) => ({
308
+ idx: j, point: other,
309
+ dist: i === j ? Infinity : computeDist(p, other)
310
+ })).filter(d => d.dist !== Infinity).sort((a, b) => a.dist - b.dist).slice(0, kNeighbors);
311
+
312
+ distances.forEach(n => {
313
+ if (i < n.idx) {
314
+ const geometry = new THREE.BufferGeometry();
315
+ geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array([
316
+ p.x ?? 0, p.y ?? 0, p.z ?? 0,
317
+ n.point.x ?? 0, n.point.y ?? 0, n.point.z ?? 0
318
+ ]), 3));
319
+ connectionsGroup.add(new THREE.Line(geometry, material));
320
+ }
321
+ });
322
+ });
323
+ }
324
+ }
325
+
326
+ function setupRaycaster() {
327
+ raycaster = new THREE.Raycaster();
328
+ mouse = new THREE.Vector2();
329
+ container.addEventListener("mousemove", onMouseMove);
330
+ container.addEventListener("click", onClick);
331
+ }
332
+
333
+ function onMouseMove(event) {
334
+ const rect = container.getBoundingClientRect();
335
+ mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
336
+ mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
337
+
338
+ raycaster.setFromCamera(mouse, camera);
339
+ const intersects = raycaster.intersectObjects(pointsGroup.children, true);
340
+
341
+ if (intersects.length > 0) {
342
+ const hit = intersects[0];
343
+ const points = model.get("points") || [];
344
+ let pointIndex, pointId;
345
+
346
+ if (hit.object.userData.isInstanced) {
347
+ pointIndex = hit.object.userData.pointIndices[hit.instanceId];
348
+ pointId = hit.object.userData.pointIds[hit.instanceId];
349
+ } else {
350
+ pointIndex = hit.object.userData.pointIndex;
351
+ pointId = hit.object.userData.pointId;
352
+ }
353
+
354
+ const point = points[pointIndex];
355
+ if (point && (!hoveredObject || hoveredObject.pointId !== pointId)) {
356
+ hoveredObject = { pointIndex, pointId };
357
+ model.set("hovered_point", point);
358
+ model.save_changes();
359
+ showTooltip(event, point);
360
+ }
361
+ } else if (hoveredObject) {
362
+ hoveredObject = null;
363
+ model.set("hovered_point", null);
364
+ model.save_changes();
365
+ hideTooltip();
366
+ }
367
+ }
368
+
369
+ function onClick(event) {
370
+ const rect = container.getBoundingClientRect();
371
+ mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
372
+ mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
373
+
374
+ raycaster.setFromCamera(mouse, camera);
375
+ const intersects = raycaster.intersectObjects(pointsGroup.children, true);
376
+
377
+ if (intersects.length > 0) {
378
+ const hit = intersects[0];
379
+ let pointId;
380
+ if (hit.object.userData.isInstanced) {
381
+ pointId = hit.object.userData.pointIds[hit.instanceId];
382
+ } else {
383
+ pointId = hit.object.userData.pointId;
384
+ }
385
+
386
+ const selectionMode = model.get("selection_mode") || "click";
387
+ const currentSelection = model.get("selected_points") || [];
388
+
389
+ if (selectionMode === "click") {
390
+ model.set("selected_points", [pointId]);
391
+ } else {
392
+ if (currentSelection.includes(pointId)) {
393
+ model.set("selected_points", currentSelection.filter(id => id !== pointId));
394
+ } else {
395
+ model.set("selected_points", [...currentSelection, pointId]);
396
+ }
397
+ }
398
+ model.save_changes();
399
+ } else {
400
+ model.set("selected_points", []);
401
+ model.save_changes();
402
+ }
403
+ }
404
+
405
+ function setupTooltip() {
406
+ tooltip = document.createElement("div");
407
+ tooltip.className = "avs-tooltip";
408
+ container.appendChild(tooltip);
409
+ }
410
+
411
+ function showTooltip(event, point) {
412
+ if (!model.get("show_tooltip")) return;
413
+ const fields = model.get("tooltip_fields") || ["label", "x", "y", "z"];
414
+ let html = "";
415
+ if (point.label) html += `<div class="avs-tooltip-label">${point.label}</div>`;
416
+ fields.filter(f => f !== "label" && point[f] !== undefined).forEach(f => {
417
+ let value = point[f];
418
+ if (typeof value === "number") value = value.toFixed(3);
419
+ html += `<div class="avs-tooltip-row"><span class="avs-tooltip-key">${f}:</span><span>${value}</span></div>`;
420
+ });
421
+ tooltip.innerHTML = html;
422
+ tooltip.style.display = "block";
423
+ const rect = container.getBoundingClientRect();
424
+ tooltip.style.left = (event.clientX - rect.left + 15) + "px";
425
+ tooltip.style.top = (event.clientY - rect.top + 15) + "px";
426
+ }
427
+
428
+ function hideTooltip() {
429
+ tooltip.style.display = "none";
430
+ }
431
+
432
+ function onCameraChange() {
433
+ model.set("camera_position", [camera.position.x, camera.position.y, camera.position.z]);
434
+ model.set("camera_target", [controls.target.x, controls.target.y, controls.target.z]);
435
+ model.save_changes();
436
+ }
437
+
438
+ function bindModelEvents() {
439
+ model.on("change:points", () => { createPoints(); createConnections(); });
440
+ model.on("change:background", () => { scene.background = new THREE.Color(model.get("background")); });
441
+ model.on("change:show_axes", setupAxesAndGrid);
442
+ model.on("change:show_grid", setupAxesAndGrid);
443
+ model.on("change:color_field", createPoints);
444
+ model.on("change:color_scale", createPoints);
445
+ model.on("change:color_domain", createPoints);
446
+ model.on("change:size_field", createPoints);
447
+ model.on("change:size_range", createPoints);
448
+ model.on("change:shape_field", createPoints);
449
+ model.on("change:shape_map", createPoints);
450
+ model.on("change:show_connections", createConnections);
451
+ model.on("change:k_neighbors", createConnections);
452
+ model.on("change:distance_threshold", createConnections);
453
+ model.on("change:reference_point", createConnections);
454
+ model.on("change:distance_metric", createConnections);
455
+ model.on("change:connection_color", createConnections);
456
+ model.on("change:connection_opacity", createConnections);
457
+ model.on("change:camera_position", () => {
458
+ const pos = model.get("camera_position");
459
+ if (pos) camera.position.set(pos[0], pos[1], pos[2]);
460
+ });
461
+ model.on("change:camera_target", () => {
462
+ const target = model.get("camera_target");
463
+ if (target) controls.target.set(target[0], target[1], target[2]);
464
+ });
465
+ }
466
+
467
+ function animate() {
468
+ animationId = requestAnimationFrame(animate);
469
+ controls.update();
470
+ renderer.render(scene, camera);
471
+ }
472
+
473
+ function cleanup() {
474
+ cancelAnimationFrame(animationId);
475
+ controls.dispose();
476
+ renderer.dispose();
477
+ scene.traverse((obj) => {
478
+ if (obj.geometry) obj.geometry.dispose();
479
+ if (obj.material) {
480
+ if (Array.isArray(obj.material)) obj.material.forEach(m => m.dispose());
481
+ else obj.material.dispose();
482
+ }
483
+ });
484
+ }
485
+
486
+ return { cleanup };
487
+ }
488
+
489
+ // Helper functions
490
+ function getColorFromScale(value, scaleName, domain) {
491
+ const scale = COLOR_SCALES[scaleName] || COLOR_SCALES.viridis;
492
+ const [min, max] = domain || [0, 1];
493
+ const t = Math.max(0, Math.min(1, (value - min) / (max - min)));
494
+ const idx = t * (scale.length - 1);
495
+ const i = Math.floor(idx);
496
+ const f = idx - i;
497
+ if (i >= scale.length - 1) {
498
+ const c = scale[scale.length - 1];
499
+ return new THREE.Color(c[0], c[1], c[2]);
500
+ }
501
+ const c1 = scale[i], c2 = scale[i + 1];
502
+ return new THREE.Color(
503
+ c1[0] + f * (c2[0] - c1[0]),
504
+ c1[1] + f * (c2[1] - c1[1]),
505
+ c1[2] + f * (c2[2] - c1[2])
506
+ );
507
+ }
508
+
509
+ function hashString(str) {
510
+ let hash = 0;
511
+ for (let i = 0; i < str.length; i++) {
512
+ hash = ((hash << 5) - hash) + str.charCodeAt(i);
513
+ hash |= 0;
514
+ }
515
+ return Math.abs(hash);
516
+ }
517
+
518
+ function getCategoricalColor(value) {
519
+ const idx = hashString(String(value)) % CATEGORICAL_COLORS.length;
520
+ return new THREE.Color(CATEGORICAL_COLORS[idx]);
521
+ }
@@ -0,0 +1,64 @@
1
+ // UI constants shared across components
2
+
3
+ export const BACKENDS = {
4
+ qdrant: {
5
+ name: "Qdrant",
6
+ side: "browser",
7
+ hasAuth: true,
8
+ hasCollection: true,
9
+ queryLanguage: "json",
10
+ placeholder: '{"vector": [...], "limit": 10}',
11
+ },
12
+ pinecone: {
13
+ name: "Pinecone",
14
+ side: "browser",
15
+ hasAuth: true,
16
+ hasIndex: true,
17
+ queryLanguage: "json",
18
+ placeholder: '{"vector": [...], "topK": 10}',
19
+ },
20
+ weaviate: {
21
+ name: "Weaviate",
22
+ side: "browser",
23
+ hasAuth: true,
24
+ hasClass: true,
25
+ queryLanguage: "graphql",
26
+ placeholder: '{ Get { Class(limit: 10) { ... } } }',
27
+ },
28
+ chroma: {
29
+ name: "Chroma",
30
+ side: "python",
31
+ hasCollection: true,
32
+ queryLanguage: "dict",
33
+ placeholder: '{"query_embeddings": [...], "n_results": 10}',
34
+ },
35
+ lancedb: {
36
+ name: "LanceDB",
37
+ side: "python",
38
+ hasTable: true,
39
+ queryLanguage: "sql",
40
+ placeholder: 'category = "tech" AND year > 2020',
41
+ },
42
+ grafeo: {
43
+ name: "Grafeo",
44
+ side: "python",
45
+ queryLanguage: "grafeo",
46
+ placeholder: "MATCH (n:Vector) RETURN n LIMIT 10",
47
+ },
48
+ };
49
+
50
+ export const COLOR_SCALES = {
51
+ viridis: [[0.267,0.004,0.329],[0.282,0.140,0.458],[0.253,0.265,0.530],[0.206,0.371,0.553],[0.163,0.471,0.558],[0.127,0.566,0.551],[0.134,0.658,0.518],[0.267,0.749,0.441],[0.478,0.821,0.318],[0.741,0.873,0.150],[0.993,0.906,0.144]],
52
+ plasma: [[0.050,0.030,0.528],[0.254,0.014,0.615],[0.417,0.001,0.658],[0.578,0.015,0.643],[0.716,0.135,0.538],[0.826,0.268,0.407],[0.906,0.411,0.271],[0.959,0.567,0.137],[0.981,0.733,0.106],[0.964,0.903,0.259],[0.940,0.975,0.131]],
53
+ inferno: [[0.001,0.000,0.014],[0.046,0.031,0.186],[0.140,0.046,0.357],[0.258,0.039,0.406],[0.366,0.071,0.432],[0.478,0.107,0.429],[0.591,0.148,0.404],[0.706,0.206,0.347],[0.815,0.290,0.259],[0.905,0.411,0.145],[0.969,0.565,0.026]],
54
+ magma: [[0.001,0.000,0.014],[0.035,0.028,0.144],[0.114,0.049,0.315],[0.206,0.053,0.431],[0.306,0.064,0.505],[0.413,0.086,0.531],[0.529,0.113,0.527],[0.654,0.158,0.501],[0.776,0.232,0.459],[0.878,0.338,0.418],[0.953,0.468,0.392]],
55
+ cividis: [[0.000,0.135,0.304],[0.000,0.179,0.345],[0.117,0.222,0.360],[0.214,0.263,0.365],[0.293,0.304,0.370],[0.366,0.345,0.375],[0.437,0.387,0.382],[0.509,0.429,0.393],[0.582,0.473,0.409],[0.659,0.520,0.431],[0.739,0.570,0.461]],
56
+ turbo: [[0.190,0.072,0.232],[0.254,0.265,0.530],[0.163,0.471,0.558],[0.134,0.658,0.518],[0.478,0.821,0.318],[0.741,0.873,0.150],[0.993,0.906,0.144],[0.988,0.652,0.198],[0.925,0.394,0.235],[0.796,0.177,0.214],[0.480,0.016,0.110]],
57
+ };
58
+
59
+ export const CATEGORICAL_COLORS = [
60
+ "#6366f1", "#f59e0b", "#10b981", "#ef4444", "#8b5cf6",
61
+ "#06b6d4", "#f97316", "#84cc16", "#ec4899", "#14b8a6"
62
+ ];
63
+
64
+ export const SHAPES = ["sphere", "cube", "cone", "tetrahedron", "octahedron", "cylinder"];