anywidget-vector 0.1.0__py3-none-any.whl → 0.2.0__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.
@@ -3,4 +3,4 @@
3
3
  from anywidget_vector.widget import VectorSpace
4
4
 
5
5
  __all__ = ["VectorSpace"]
6
- __version__ = "0.1.0"
6
+ __version__ = "0.2.0"
@@ -39,6 +39,33 @@ const SHAPES = {
39
39
  cylinder: () => new THREE.CylinderGeometry(0.5, 0.5, 1, 16),
40
40
  };
41
41
 
42
+ // Distance metrics
43
+ const DISTANCE_METRICS = {
44
+ euclidean: (a, b) => {
45
+ const dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z;
46
+ return Math.sqrt(dx*dx + dy*dy + dz*dz);
47
+ },
48
+ cosine: (a, b) => {
49
+ const dot = a.x*b.x + a.y*b.y + a.z*b.z;
50
+ const magA = Math.sqrt(a.x*a.x + a.y*a.y + a.z*a.z);
51
+ const magB = Math.sqrt(b.x*b.x + b.y*b.y + b.z*b.z);
52
+ if (magA === 0 || magB === 0) return 1;
53
+ return 1 - (dot / (magA * magB)); // Convert similarity to distance
54
+ },
55
+ manhattan: (a, b) => {
56
+ return Math.abs(a.x - b.x) + Math.abs(a.y - b.y) + Math.abs(a.z - b.z);
57
+ },
58
+ dot_product: (a, b) => {
59
+ // Negative dot product as distance (higher dot = closer)
60
+ return -(a.x*b.x + a.y*b.y + a.z*b.z);
61
+ },
62
+ };
63
+
64
+ function computeDistance(p1, p2, metric) {
65
+ const fn = DISTANCE_METRICS[metric] || DISTANCE_METRICS.euclidean;
66
+ return fn(p1, p2);
67
+ }
68
+
42
69
  function getColorFromScale(value, scaleName, domain) {
43
70
  const scale = COLOR_SCALES[scaleName] || COLOR_SCALES.viridis;
44
71
  const [min, max] = domain || [0, 1];
@@ -74,7 +101,7 @@ function getCategoricalColor(value) {
74
101
 
75
102
  function render({ model, el }) {
76
103
  let scene, camera, renderer, controls;
77
- let pointsGroup;
104
+ let pointsGroup, connectionsGroup;
78
105
  let raycaster, mouse;
79
106
  let hoveredObject = null;
80
107
  let tooltip;
@@ -127,6 +154,8 @@ function render({ model, el }) {
127
154
  // Groups
128
155
  pointsGroup = new THREE.Group();
129
156
  scene.add(pointsGroup);
157
+ connectionsGroup = new THREE.Group();
158
+ scene.add(connectionsGroup);
130
159
  axesGroup = new THREE.Group();
131
160
  scene.add(axesGroup);
132
161
 
@@ -135,6 +164,7 @@ function render({ model, el }) {
135
164
  setupRaycaster(container);
136
165
  setupTooltip(container);
137
166
  createPoints();
167
+ createConnections();
138
168
  bindModelEvents();
139
169
  }
140
170
 
@@ -349,6 +379,98 @@ function render({ model, el }) {
349
379
  }
350
380
  }
351
381
 
382
+ function createConnections() {
383
+ // Clear existing connections
384
+ while (connectionsGroup.children.length > 0) {
385
+ const obj = connectionsGroup.children[0];
386
+ if (obj.geometry) obj.geometry.dispose();
387
+ if (obj.material) obj.material.dispose();
388
+ connectionsGroup.remove(obj);
389
+ }
390
+
391
+ const points = model.get("points") || [];
392
+ const showConnections = model.get("show_connections");
393
+ const kNeighbors = model.get("k_neighbors") || 0;
394
+ const distanceThreshold = model.get("distance_threshold");
395
+ const referencePoint = model.get("reference_point");
396
+ const distanceMetric = model.get("distance_metric") || "euclidean";
397
+ const connectionColor = model.get("connection_color") || "#ffffff";
398
+ const connectionOpacity = model.get("connection_opacity") || 0.3;
399
+
400
+ if (!showConnections || points.length < 2) return;
401
+
402
+ const material = new THREE.LineBasicMaterial({
403
+ color: new THREE.Color(connectionColor),
404
+ transparent: true,
405
+ opacity: connectionOpacity,
406
+ });
407
+
408
+ // If reference point is set, connect to k-nearest or within threshold
409
+ if (referencePoint) {
410
+ const refIdx = points.findIndex(p => p.id === referencePoint);
411
+ if (refIdx === -1) return;
412
+ const ref = points[refIdx];
413
+
414
+ // Compute distances from reference
415
+ const distances = points.map((p, i) => ({
416
+ idx: i,
417
+ point: p,
418
+ dist: i === refIdx ? Infinity : computeDistance(ref, p, distanceMetric)
419
+ })).filter(d => d.dist !== Infinity);
420
+
421
+ // Sort by distance
422
+ distances.sort((a, b) => a.dist - b.dist);
423
+
424
+ // Select neighbors
425
+ let neighbors;
426
+ if (distanceThreshold !== null && distanceThreshold !== undefined) {
427
+ neighbors = distances.filter(d => d.dist <= distanceThreshold);
428
+ } else if (kNeighbors > 0) {
429
+ neighbors = distances.slice(0, kNeighbors);
430
+ } else {
431
+ return;
432
+ }
433
+
434
+ // Draw lines
435
+ neighbors.forEach(n => {
436
+ const geometry = new THREE.BufferGeometry();
437
+ const positions = new Float32Array([
438
+ ref.x ?? 0, ref.y ?? 0, ref.z ?? 0,
439
+ n.point.x ?? 0, n.point.y ?? 0, n.point.z ?? 0
440
+ ]);
441
+ geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
442
+ const line = new THREE.Line(geometry, material);
443
+ connectionsGroup.add(line);
444
+ });
445
+ } else if (kNeighbors > 0) {
446
+ // Connect each point to its k-nearest neighbors
447
+ points.forEach((p, i) => {
448
+ const distances = points.map((other, j) => ({
449
+ idx: j,
450
+ point: other,
451
+ dist: i === j ? Infinity : computeDistance(p, other, distanceMetric)
452
+ })).filter(d => d.dist !== Infinity);
453
+
454
+ distances.sort((a, b) => a.dist - b.dist);
455
+ const neighbors = distances.slice(0, kNeighbors);
456
+
457
+ neighbors.forEach(n => {
458
+ // Only draw if i < n.idx to avoid duplicate lines
459
+ if (i < n.idx) {
460
+ const geometry = new THREE.BufferGeometry();
461
+ const positions = new Float32Array([
462
+ p.x ?? 0, p.y ?? 0, p.z ?? 0,
463
+ n.point.x ?? 0, n.point.y ?? 0, n.point.z ?? 0
464
+ ]);
465
+ geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
466
+ const line = new THREE.Line(geometry, material);
467
+ connectionsGroup.add(line);
468
+ }
469
+ });
470
+ });
471
+ }
472
+ }
473
+
352
474
  function setupRaycaster(container) {
353
475
  raycaster = new THREE.Raycaster();
354
476
  mouse = new THREE.Vector2();
@@ -500,7 +622,7 @@ function render({ model, el }) {
500
622
  }
501
623
 
502
624
  function bindModelEvents() {
503
- model.on("change:points", createPoints);
625
+ model.on("change:points", () => { createPoints(); createConnections(); });
504
626
  model.on("change:background", () => {
505
627
  scene.background = new THREE.Color(model.get("background"));
506
628
  });
@@ -514,6 +636,15 @@ function render({ model, el }) {
514
636
  model.on("change:shape_field", createPoints);
515
637
  model.on("change:shape_map", createPoints);
516
638
 
639
+ // Distance/connection related
640
+ model.on("change:show_connections", createConnections);
641
+ model.on("change:k_neighbors", createConnections);
642
+ model.on("change:distance_threshold", createConnections);
643
+ model.on("change:reference_point", createConnections);
644
+ model.on("change:distance_metric", createConnections);
645
+ model.on("change:connection_color", createConnections);
646
+ model.on("change:connection_opacity", createConnections);
647
+
517
648
  model.on("change:camera_position", () => {
518
649
  const pos = model.get("camera_position");
519
650
  if (pos) camera.position.set(pos[0], pos[1], pos[2]);
@@ -614,6 +745,19 @@ class VectorSpace(anywidget.AnyWidget):
614
745
  use_instancing = traitlets.Bool(default_value=True).tag(sync=True)
615
746
  point_budget = traitlets.Int(default_value=100000).tag(sync=True)
616
747
 
748
+ # Distance metrics and connections
749
+ distance_metric = traitlets.Unicode(default_value="euclidean").tag(
750
+ sync=True
751
+ ) # euclidean, cosine, manhattan, dot_product
752
+ show_connections = traitlets.Bool(default_value=False).tag(sync=True)
753
+ k_neighbors = traitlets.Int(default_value=0).tag(sync=True) # k-nearest neighbors to connect
754
+ distance_threshold = traitlets.Float(default_value=None, allow_none=True).tag(
755
+ sync=True
756
+ ) # max distance for connections
757
+ reference_point = traitlets.Unicode(default_value=None, allow_none=True).tag(sync=True) # point ID to measure from
758
+ connection_color = traitlets.Unicode(default_value="#ffffff").tag(sync=True)
759
+ connection_opacity = traitlets.Float(default_value=0.3).tag(sync=True)
760
+
617
761
  def __init__(
618
762
  self,
619
763
  points: list[dict[str, Any]] | None = None,
@@ -933,6 +1077,145 @@ class VectorSpace(anywidget.AnyWidget):
933
1077
  """Clear all selections."""
934
1078
  self.selected_points = []
935
1079
 
1080
+ # === Distance Methods ===
1081
+
1082
+ def compute_distances(
1083
+ self,
1084
+ reference_id: str,
1085
+ metric: str | None = None,
1086
+ vector_field: str = "vector",
1087
+ ) -> dict[str, float]:
1088
+ """Compute distances from a reference point to all other points.
1089
+
1090
+ Args:
1091
+ reference_id: ID of the reference point
1092
+ metric: Distance metric (euclidean, cosine, manhattan, dot_product).
1093
+ If None, uses self.distance_metric
1094
+ vector_field: Field containing the full vector (for high-dim distance).
1095
+ Falls back to x,y,z if not present.
1096
+
1097
+ Returns:
1098
+ Dict mapping point ID to distance from reference
1099
+ """
1100
+ metric = metric or self.distance_metric
1101
+ ref_point = next((p for p in self.points if p.get("id") == reference_id), None)
1102
+ if not ref_point:
1103
+ return {}
1104
+
1105
+ ref_vec = self._get_vector(ref_point, vector_field)
1106
+ distances = {}
1107
+
1108
+ for point in self.points:
1109
+ if point.get("id") == reference_id:
1110
+ continue
1111
+ vec = self._get_vector(point, vector_field)
1112
+ distances[point.get("id")] = self._compute_distance(ref_vec, vec, metric)
1113
+
1114
+ return distances
1115
+
1116
+ def find_neighbors(
1117
+ self,
1118
+ reference_id: str,
1119
+ k: int | None = None,
1120
+ threshold: float | None = None,
1121
+ metric: str | None = None,
1122
+ vector_field: str = "vector",
1123
+ ) -> list[tuple[str, float]]:
1124
+ """Find nearest neighbors of a reference point.
1125
+
1126
+ Args:
1127
+ reference_id: ID of the reference point
1128
+ k: Number of neighbors to return (if None, uses threshold)
1129
+ threshold: Maximum distance (if None, uses k)
1130
+ metric: Distance metric to use
1131
+ vector_field: Field containing the full vector
1132
+
1133
+ Returns:
1134
+ List of (point_id, distance) tuples, sorted by distance
1135
+ """
1136
+ distances = self.compute_distances(reference_id, metric, vector_field)
1137
+ sorted_distances = sorted(distances.items(), key=lambda x: x[1])
1138
+
1139
+ if threshold is not None:
1140
+ return [(pid, d) for pid, d in sorted_distances if d <= threshold]
1141
+ elif k is not None:
1142
+ return sorted_distances[:k]
1143
+ else:
1144
+ return sorted_distances
1145
+
1146
+ def color_by_distance(
1147
+ self,
1148
+ reference_id: str,
1149
+ metric: str | None = None,
1150
+ vector_field: str = "vector",
1151
+ ) -> None:
1152
+ """Color points by distance from a reference point.
1153
+
1154
+ Updates points with a '_distance' field and sets color_field to use it.
1155
+ """
1156
+ distances = self.compute_distances(reference_id, metric, vector_field)
1157
+
1158
+ # Update points with distance field
1159
+ updated_points = []
1160
+ for point in self.points:
1161
+ point_copy = dict(point)
1162
+ pid = point.get("id")
1163
+ if pid == reference_id:
1164
+ point_copy["_distance"] = 0.0
1165
+ elif pid in distances:
1166
+ point_copy["_distance"] = distances[pid]
1167
+ updated_points.append(point_copy)
1168
+
1169
+ self.points = updated_points
1170
+ self.color_field = "_distance"
1171
+ self.reference_point = reference_id
1172
+
1173
+ def show_neighbors(
1174
+ self,
1175
+ reference_id: str,
1176
+ k: int | None = None,
1177
+ threshold: float | None = None,
1178
+ ) -> None:
1179
+ """Show connections to nearest neighbors of a reference point."""
1180
+ self.reference_point = reference_id
1181
+ self.show_connections = True
1182
+ if k is not None:
1183
+ self.k_neighbors = k
1184
+ if threshold is not None:
1185
+ self.distance_threshold = threshold
1186
+
1187
+ def _get_vector(self, point: dict[str, Any], vector_field: str) -> list[float]:
1188
+ """Extract vector from point, falling back to x,y,z."""
1189
+ if vector_field in point and point[vector_field]:
1190
+ vec = point[vector_field]
1191
+ return list(vec) if hasattr(vec, "__iter__") else [vec]
1192
+ return [point.get("x", 0), point.get("y", 0), point.get("z", 0)]
1193
+
1194
+ def _compute_distance(self, v1: list[float], v2: list[float], metric: str) -> float:
1195
+ """Compute distance between two vectors."""
1196
+ import math
1197
+
1198
+ # Ensure same length
1199
+ n = min(len(v1), len(v2))
1200
+ v1, v2 = v1[:n], v2[:n]
1201
+
1202
+ if metric == "euclidean":
1203
+ return math.sqrt(sum((a - b) ** 2 for a, b in zip(v1, v2)))
1204
+ elif metric == "cosine":
1205
+ dot = sum(a * b for a, b in zip(v1, v2))
1206
+ mag1 = math.sqrt(sum(a * a for a in v1))
1207
+ mag2 = math.sqrt(sum(b * b for b in v2))
1208
+ if mag1 == 0 or mag2 == 0:
1209
+ return 1.0
1210
+ return 1 - (dot / (mag1 * mag2))
1211
+ elif metric == "manhattan":
1212
+ return sum(abs(a - b) for a, b in zip(v1, v2))
1213
+ elif metric == "dot_product":
1214
+ return -sum(a * b for a, b in zip(v1, v2))
1215
+ else:
1216
+ # Default to euclidean
1217
+ return math.sqrt(sum((a - b) ** 2 for a, b in zip(v1, v2)))
1218
+
936
1219
  # === Export ===
937
1220
 
938
1221
  def to_json(self) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anywidget-vector
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Interactive vector visualization for Python notebooks using anywidget
5
5
  Project-URL: Homepage, https://grafeo.dev/
6
6
  Project-URL: Repository, https://github.com/GrafeoDB/anywidget-vector
@@ -264,6 +264,73 @@ widget = VectorSpace(
264
264
  )
265
265
  ```
266
266
 
267
+ ## Distance Metrics
268
+
269
+ Compute distances and visualize similarity relationships between points.
270
+
271
+ ### Supported Metrics
272
+
273
+ | Metric | Description |
274
+ |--------|-------------|
275
+ | `euclidean` | Straight-line distance (L2 norm) |
276
+ | `cosine` | Angle-based distance (1 - cosine similarity) |
277
+ | `manhattan` | Sum of absolute differences (L1 norm) |
278
+ | `dot_product` | Negative dot product (higher = closer) |
279
+
280
+ ### Color by Distance
281
+
282
+ ```python
283
+ # Color points by distance from a reference
284
+ widget.color_by_distance("point_a")
285
+ widget.color_by_distance("point_a", metric="cosine")
286
+ ```
287
+
288
+ ### Find Neighbors
289
+
290
+ ```python
291
+ # Find k nearest neighbors
292
+ neighbors = widget.find_neighbors("point_a", k=5)
293
+ # Returns: [("point_b", 0.1), ("point_c", 0.2), ...]
294
+
295
+ # Find neighbors within distance threshold
296
+ neighbors = widget.find_neighbors("point_a", threshold=0.5)
297
+ ```
298
+
299
+ ### Show Connections
300
+
301
+ ```python
302
+ # Draw lines to k-nearest neighbors
303
+ widget.show_neighbors("point_a", k=5)
304
+
305
+ # Draw lines to all points within threshold
306
+ widget.show_neighbors("point_a", threshold=0.3)
307
+
308
+ # Manual connection settings
309
+ widget = VectorSpace(
310
+ points=data,
311
+ show_connections=True,
312
+ k_neighbors=3,
313
+ distance_metric="cosine",
314
+ connection_color="#00ff00",
315
+ connection_opacity=0.5,
316
+ )
317
+ ```
318
+
319
+ ### Compute Distances
320
+
321
+ ```python
322
+ # Get distances from reference to all points
323
+ distances = widget.compute_distances("point_a")
324
+ # Returns: {"point_b": 0.1, "point_c": 0.5, ...}
325
+
326
+ # Use high-dimensional vectors (not just x,y,z)
327
+ distances = widget.compute_distances(
328
+ "point_a",
329
+ metric="cosine",
330
+ vector_field="embedding" # Use full embedding vector
331
+ )
332
+ ```
333
+
267
334
  ## Export
268
335
 
269
336
  ```python
@@ -0,0 +1,6 @@
1
+ anywidget_vector/__init__.py,sha256=6uLVXoFKaF4PNvMpbkOYmYEgooFVec4Bi0pGMxrfMmY,162
2
+ anywidget_vector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ anywidget_vector/widget.py,sha256=6krdOLSW78YbjSO5Uyv2I4buolfLxFoTUVSBQnd9vwM,44704
4
+ anywidget_vector-0.2.0.dist-info/METADATA,sha256=kIgCDD5GvMZRQduAJJRwMafrRcIM3Uryt9nqixK9z1k,8942
5
+ anywidget_vector-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
+ anywidget_vector-0.2.0.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- anywidget_vector/__init__.py,sha256=UA1icM7Y50eVfCf8yambftuG8bxLMIjON68IVJDO4x8,162
2
- anywidget_vector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- anywidget_vector/widget.py,sha256=60AP_awiX6MUaYPIBRvfFBdFQsyqPIDjronMShQDIH0,34021
4
- anywidget_vector-0.1.0.dist-info/METADATA,sha256=kVS4eGG1n0PCXNItyjPdKRTh0QpwWowXrfMv8ML7dWM,7333
5
- anywidget_vector-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
- anywidget_vector-0.1.0.dist-info/RECORD,,