anywidget-vector 0.2.4__tar.gz → 0.2.6__tar.gz
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.
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/PKG-INFO +6 -1
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/README.md +2 -0
- anywidget_vector-0.2.6/assets/demo.png +0 -0
- anywidget_vector-0.2.6/examples/test_add_numpy.py +98 -0
- anywidget_vector-0.2.6/examples/test_projection.py +98 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/pyproject.toml +2 -1
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/__init__.py +1 -1
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/ui/__init__.py +1 -1
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/ui/canvas.js +37 -18
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/ui/styles.css +14 -32
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/ui/toolbar.js +11 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/widget.py +186 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/uv.lock +218 -12
- anywidget_vector-0.2.4/src/anywidget_vector/traitlets.py +0 -84
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/.github/workflows/ci.yml +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/.github/workflows/pypi.yml +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/.gitignore +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/.python-version +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/LICENSE +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/examples/demo.py +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/__init__.py +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/chroma/__init__.py +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/chroma/client.py +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/chroma/converter.py +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/grafeo/__init__.py +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/grafeo/browser.js +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/grafeo/client.py +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/grafeo/converter.py +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/grafeo/embed.js +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/lancedb/__init__.py +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/lancedb/client.py +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/lancedb/converter.py +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/pinecone/__init__.py +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/pinecone/client.js +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/pinecone/converter.py +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/qdrant/__init__.py +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/qdrant/client.js +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/qdrant/converter.py +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/weaviate/__init__.py +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/weaviate/client.js +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/backends/weaviate/converter.py +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/demo.py +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/py.typed +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/static/icons.js +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/ui/constants.js +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/ui/properties.js +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/ui/settings.js +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/src/anywidget_vector/ui/sidebar.js +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/tests/__init__.py +0 -0
- {anywidget_vector-0.2.4 → anywidget_vector-0.2.6}/tests/test_widget.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: anywidget-vector
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Interactive 3D vector visualization with query UI for vector databases
|
|
5
5
|
Project-URL: Homepage, https://grafeo.dev/
|
|
6
6
|
Project-URL: Repository, https://github.com/GrafeoDB/anywidget-vector
|
|
@@ -48,6 +48,9 @@ Provides-Extra: pandas
|
|
|
48
48
|
Requires-Dist: pandas>=2.0; extra == 'pandas'
|
|
49
49
|
Provides-Extra: pinecone
|
|
50
50
|
Requires-Dist: pinecone-client>=3.0; extra == 'pinecone'
|
|
51
|
+
Provides-Extra: projection
|
|
52
|
+
Requires-Dist: scikit-learn>=1.3; extra == 'projection'
|
|
53
|
+
Requires-Dist: umap-learn>=0.5; extra == 'projection'
|
|
51
54
|
Provides-Extra: qdrant
|
|
52
55
|
Requires-Dist: qdrant-client>=1.0; extra == 'qdrant'
|
|
53
56
|
Provides-Extra: weaviate
|
|
@@ -60,6 +63,8 @@ Interactive 3D vector visualization for Python notebooks.
|
|
|
60
63
|
|
|
61
64
|
Works with Marimo, Jupyter, VS Code, Colab, anywhere [anywidget](https://anywidget.dev/) runs.
|
|
62
65
|
|
|
66
|
+

|
|
67
|
+
|
|
63
68
|
## Features
|
|
64
69
|
|
|
65
70
|
- **Universal**: One widget, every notebook environment
|
|
@@ -4,6 +4,8 @@ Interactive 3D vector visualization for Python notebooks.
|
|
|
4
4
|
|
|
5
5
|
Works with Marimo, Jupyter, VS Code, Colab, anywhere [anywidget](https://anywidget.dev/) runs.
|
|
6
6
|
|
|
7
|
+

|
|
8
|
+
|
|
7
9
|
## Features
|
|
8
10
|
|
|
9
11
|
- **Universal**: One widget, every notebook environment
|
|
Binary file
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.12"
|
|
3
|
+
# dependencies = [
|
|
4
|
+
# "anywidget-vector==0.2.5",
|
|
5
|
+
# "marimo",
|
|
6
|
+
# "numpy==2.4.3",
|
|
7
|
+
# ]
|
|
8
|
+
# ///
|
|
9
|
+
|
|
10
|
+
import marimo
|
|
11
|
+
|
|
12
|
+
__generated_with = "0.18.4"
|
|
13
|
+
app = marimo.App(width="medium")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.cell
|
|
17
|
+
def _():
|
|
18
|
+
import marimo as mo
|
|
19
|
+
|
|
20
|
+
return (mo,)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.cell
|
|
24
|
+
def _():
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
from anywidget_vector import VectorSpace
|
|
28
|
+
|
|
29
|
+
return VectorSpace, np
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@app.cell(hide_code=True)
|
|
33
|
+
def _(mo):
|
|
34
|
+
mo.md("""
|
|
35
|
+
# Test: `add_numpy`
|
|
36
|
+
""")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.cell
|
|
41
|
+
def _(VectorSpace):
|
|
42
|
+
# Start with an empty widget
|
|
43
|
+
w = VectorSpace(
|
|
44
|
+
width=900,
|
|
45
|
+
height=500,
|
|
46
|
+
background="#0f0f1a",
|
|
47
|
+
color_field="group",
|
|
48
|
+
)
|
|
49
|
+
w
|
|
50
|
+
return (w,)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.cell
|
|
54
|
+
def _(mo, np, w):
|
|
55
|
+
# Add two numpy clusters
|
|
56
|
+
rng = np.random.default_rng(42)
|
|
57
|
+
|
|
58
|
+
cluster_a = rng.normal(loc=[0.3, 0.3, 0.3], scale=0.1, size=(50, 3))
|
|
59
|
+
cluster_b = rng.normal(loc=[0.7, 0.7, 0.7], scale=0.1, size=(50, 3))
|
|
60
|
+
|
|
61
|
+
w.add_numpy(
|
|
62
|
+
cluster_a,
|
|
63
|
+
labels=[f"A-{i}" for i in range(len(cluster_a))],
|
|
64
|
+
metadata={"group": ["alpha"] * len(cluster_a)},
|
|
65
|
+
).add_numpy(
|
|
66
|
+
cluster_b,
|
|
67
|
+
labels=[f"B-{i}" for i in range(len(cluster_b))],
|
|
68
|
+
metadata={"group": ["beta"] * len(cluster_b)},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
mo.md(f"Added **{len(w.points)}** points total")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@app.cell
|
|
76
|
+
def _(mo, np, w):
|
|
77
|
+
# Add a third cluster with 2D data (z defaults to 0)
|
|
78
|
+
rng2 = np.random.default_rng(99)
|
|
79
|
+
flat = rng2.normal(loc=[0.5, 0.5], scale=0.08, size=(30, 2))
|
|
80
|
+
|
|
81
|
+
w.add_numpy(flat, metadata={"group": ["flat"] * len(flat)})
|
|
82
|
+
|
|
83
|
+
mo.md(f"Now **{len(w.points)}** points (added 30 flat 2D points)")
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.cell
|
|
88
|
+
def _(mo, w):
|
|
89
|
+
# Verify point IDs are unique
|
|
90
|
+
ids = [p["id"] for p in w.points]
|
|
91
|
+
unique = len(set(ids))
|
|
92
|
+
status = "All IDs unique" if unique == len(ids) else f"DUPLICATE IDS: {len(ids) - unique} collisions"
|
|
93
|
+
mo.md(f"Points: {len(ids)}, Unique IDs: {unique}: **{status}**")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
app.run()
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.12"
|
|
3
|
+
# dependencies = [
|
|
4
|
+
# "anywidget-vector",
|
|
5
|
+
# "marimo",
|
|
6
|
+
# "numpy",
|
|
7
|
+
# "scikit-learn",
|
|
8
|
+
# ]
|
|
9
|
+
# ///
|
|
10
|
+
|
|
11
|
+
import marimo
|
|
12
|
+
|
|
13
|
+
__generated_with = "0.20.0"
|
|
14
|
+
app = marimo.App(width="medium")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.cell
|
|
18
|
+
def _():
|
|
19
|
+
import marimo as mo
|
|
20
|
+
|
|
21
|
+
return (mo,)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.cell
|
|
25
|
+
def _():
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
from anywidget_vector import VectorSpace
|
|
29
|
+
|
|
30
|
+
return VectorSpace, np
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@app.cell(hide_code=True)
|
|
34
|
+
def _(mo):
|
|
35
|
+
mo.md("""# Interactive Projection Demo
|
|
36
|
+
|
|
37
|
+
High-dimensional data projected to 3D. Switch projections live with the dropdown.
|
|
38
|
+
""")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.cell
|
|
43
|
+
def _(np):
|
|
44
|
+
# Generate 50-dimensional clustered data
|
|
45
|
+
rng = np.random.default_rng(42)
|
|
46
|
+
|
|
47
|
+
def make_cluster(center, n=80, noise=0.3):
|
|
48
|
+
return rng.normal(loc=center, scale=noise, size=(n, len(center)))
|
|
49
|
+
|
|
50
|
+
n_dims = 50
|
|
51
|
+
centers = [rng.uniform(-2, 2, size=n_dims) for _ in range(5)]
|
|
52
|
+
cluster_names = ["alpha", "beta", "gamma", "delta", "epsilon"]
|
|
53
|
+
|
|
54
|
+
vectors = np.vstack([make_cluster(c) for c in centers])
|
|
55
|
+
groups = [name for name, c in zip(cluster_names, centers, strict=True) for _ in range(80)]
|
|
56
|
+
labels = [f"{g}-{i}" for i, g in enumerate(groups)]
|
|
57
|
+
|
|
58
|
+
f"Generated {len(vectors)} points in {n_dims}D across 5 clusters"
|
|
59
|
+
return vectors, groups, labels
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.cell
|
|
63
|
+
def _(mo):
|
|
64
|
+
method = mo.ui.dropdown(
|
|
65
|
+
options=["pca", "tsne"],
|
|
66
|
+
value="pca",
|
|
67
|
+
label="Projection method",
|
|
68
|
+
)
|
|
69
|
+
method
|
|
70
|
+
return (method,)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.cell
|
|
74
|
+
def _(VectorSpace, groups, labels, method, vectors):
|
|
75
|
+
w = VectorSpace(
|
|
76
|
+
width=900,
|
|
77
|
+
height=500,
|
|
78
|
+
background="#0f0f1a",
|
|
79
|
+
color_field="group",
|
|
80
|
+
)
|
|
81
|
+
w.add_numpy(
|
|
82
|
+
vectors,
|
|
83
|
+
labels=labels,
|
|
84
|
+
metadata={"group": groups},
|
|
85
|
+
)
|
|
86
|
+
w.project(method.value)
|
|
87
|
+
w
|
|
88
|
+
return (w,)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@app.cell
|
|
92
|
+
def _(mo, method, w):
|
|
93
|
+
mo.md(f"Showing **{method.value.upper()}** projection of {len(w.points)} points")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
app.run()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "anywidget-vector"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.6"
|
|
4
4
|
description = "Interactive 3D vector visualization with query UI for vector databases"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -40,6 +40,7 @@ pinecone = ["pinecone-client>=3.0"]
|
|
|
40
40
|
weaviate = ["weaviate-client>=4.0"]
|
|
41
41
|
lancedb = ["lancedb>=0.1"]
|
|
42
42
|
grafeo = ["grafeo>=0.5"]
|
|
43
|
+
projection = ["scikit-learn>=1.3", "umap-learn>=0.5"]
|
|
43
44
|
embed = [] # Marker extra — WASM loads from CDN on demand
|
|
44
45
|
demo = [] # Marker extra — enables demo mode with pre-populated dataset
|
|
45
46
|
all = [
|
|
@@ -123,6 +123,7 @@ function render({{ model, el }}) {{
|
|
|
123
123
|
const wrapper = document.createElement("div");
|
|
124
124
|
wrapper.className = "avs-wrapper";
|
|
125
125
|
wrapper.style.width = model.get("width") + "px";
|
|
126
|
+
wrapper.style.height = model.get("height") + "px";
|
|
126
127
|
el.appendChild(wrapper);
|
|
127
128
|
|
|
128
129
|
let sidebar = null;
|
|
@@ -154,7 +155,6 @@ function render({{ model, el }}) {{
|
|
|
154
155
|
|
|
155
156
|
const main = document.createElement("div");
|
|
156
157
|
main.className = "avs-main";
|
|
157
|
-
main.style.height = model.get("height") + "px";
|
|
158
158
|
wrapper.appendChild(main);
|
|
159
159
|
|
|
160
160
|
// Sidebar (left, explorer panel)
|
|
@@ -50,15 +50,19 @@ export function createCanvas(model, container, callbacks) {
|
|
|
50
50
|
scene = new THREE.Scene();
|
|
51
51
|
scene.background = new THREE.Color(model.get("background"));
|
|
52
52
|
|
|
53
|
+
// Use actual container dimensions when available, fall back to model values
|
|
54
|
+
const initW = container.clientWidth || model.get("width");
|
|
55
|
+
const initH = container.clientHeight || model.get("height");
|
|
56
|
+
|
|
53
57
|
// Camera
|
|
54
|
-
const aspect =
|
|
58
|
+
const aspect = initW / initH;
|
|
55
59
|
camera = new THREE.PerspectiveCamera(60, aspect, 0.01, 1000);
|
|
56
60
|
const camPos = model.get("camera_position") || [2, 2, 2];
|
|
57
61
|
camera.position.set(camPos[0], camPos[1], camPos[2]);
|
|
58
62
|
|
|
59
63
|
// Renderer
|
|
60
64
|
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|
61
|
-
renderer.setSize(
|
|
65
|
+
renderer.setSize(initW, initH);
|
|
62
66
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
63
67
|
container.appendChild(renderer.domElement);
|
|
64
68
|
|
|
@@ -100,14 +104,32 @@ export function createCanvas(model, container, callbacks) {
|
|
|
100
104
|
setupRaycaster();
|
|
101
105
|
setupTooltip();
|
|
102
106
|
setupZoomControls();
|
|
103
|
-
setupModeControls();
|
|
104
107
|
setupBoxSelection();
|
|
108
|
+
setupKeyboardShortcuts();
|
|
105
109
|
createPoints();
|
|
106
110
|
createConnections();
|
|
107
111
|
fitToView();
|
|
108
112
|
bindModelEvents();
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
function setupKeyboardShortcuts() {
|
|
116
|
+
container.tabIndex = 0;
|
|
117
|
+
container.style.outline = "none";
|
|
118
|
+
container.addEventListener("keydown", (e) => {
|
|
119
|
+
const tag = e.target.tagName;
|
|
120
|
+
if (tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA") return;
|
|
121
|
+
|
|
122
|
+
if (e.key === "f" || e.key === "F") {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
fitToView();
|
|
125
|
+
} else if (e.key === "Escape") {
|
|
126
|
+
model.set("selected_points", []);
|
|
127
|
+
model.set("hovered_point", null);
|
|
128
|
+
model.save_changes();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
111
133
|
function setupAxesAndGrid() {
|
|
112
134
|
while (axesGroup.children.length > 0) axesGroup.remove(axesGroup.children[0]);
|
|
113
135
|
if (gridHelper) { scene.remove(gridHelper); gridHelper = null; }
|
|
@@ -471,27 +493,19 @@ export function createCanvas(model, container, callbacks) {
|
|
|
471
493
|
zoomFitBtn.title = "Fit to view";
|
|
472
494
|
zoomFitBtn.addEventListener("click", fitToView);
|
|
473
495
|
|
|
474
|
-
|
|
475
|
-
zoomControls.appendChild(zoomOutBtn);
|
|
476
|
-
zoomControls.appendChild(zoomFitBtn);
|
|
477
|
-
container.appendChild(zoomControls);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
function setupModeControls() {
|
|
496
|
+
// Mode switcher (above zoom buttons, inside same column)
|
|
481
497
|
const modeGroup = document.createElement("div");
|
|
482
498
|
modeGroup.className = "avs-mode-group";
|
|
483
499
|
|
|
484
500
|
const clickBtn = document.createElement("button");
|
|
485
|
-
clickBtn.className = "avs-mode-btn avs-mode-active";
|
|
501
|
+
clickBtn.className = "avs-zoom-btn avs-mode-btn avs-mode-active";
|
|
486
502
|
clickBtn.innerHTML = ICONS.cursor;
|
|
487
503
|
clickBtn.title = "Click / multi-select mode";
|
|
488
|
-
clickBtn.dataset.mode = "click";
|
|
489
504
|
|
|
490
505
|
const boxBtn = document.createElement("button");
|
|
491
|
-
boxBtn.className = "avs-mode-btn";
|
|
506
|
+
boxBtn.className = "avs-zoom-btn avs-mode-btn";
|
|
492
507
|
boxBtn.innerHTML = ICONS.boxSelect;
|
|
493
508
|
boxBtn.title = "Box select mode";
|
|
494
|
-
boxBtn.dataset.mode = "box";
|
|
495
509
|
|
|
496
510
|
function setActiveMode(mode) {
|
|
497
511
|
currentMode = mode;
|
|
@@ -513,15 +527,20 @@ export function createCanvas(model, container, callbacks) {
|
|
|
513
527
|
clickBtn.addEventListener("click", () => setActiveMode("click"));
|
|
514
528
|
boxBtn.addEventListener("click", () => setActiveMode("box"));
|
|
515
529
|
|
|
516
|
-
modeGroup.appendChild(clickBtn);
|
|
517
|
-
modeGroup.appendChild(boxBtn);
|
|
518
|
-
container.appendChild(modeGroup);
|
|
519
|
-
|
|
520
530
|
// Sync with model changes from Python side
|
|
521
531
|
model.on("change:selection_mode", () => {
|
|
522
532
|
const mode = model.get("selection_mode") || "click";
|
|
523
533
|
if (mode !== currentMode) setActiveMode(mode);
|
|
524
534
|
});
|
|
535
|
+
|
|
536
|
+
modeGroup.appendChild(clickBtn);
|
|
537
|
+
modeGroup.appendChild(boxBtn);
|
|
538
|
+
|
|
539
|
+
zoomControls.appendChild(modeGroup);
|
|
540
|
+
zoomControls.appendChild(zoomInBtn);
|
|
541
|
+
zoomControls.appendChild(zoomOutBtn);
|
|
542
|
+
zoomControls.appendChild(zoomFitBtn);
|
|
543
|
+
container.appendChild(zoomControls);
|
|
525
544
|
}
|
|
526
545
|
|
|
527
546
|
function setupBoxSelection() {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
display: flex;
|
|
48
48
|
flex-direction: column;
|
|
49
49
|
gap: 2px;
|
|
50
|
-
z-index:
|
|
50
|
+
z-index: 25;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
.avs-zoom-btn {
|
|
@@ -87,37 +87,11 @@
|
|
|
87
87
|
|
|
88
88
|
/* === Mode Switcher === */
|
|
89
89
|
.avs-mode-group {
|
|
90
|
-
position: absolute;
|
|
91
|
-
bottom: 12px;
|
|
92
|
-
left: 12px;
|
|
93
90
|
display: flex;
|
|
94
91
|
gap: 2px;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.avs-mode-btn {
|
|
99
|
-
width: 32px;
|
|
100
|
-
height: 32px;
|
|
101
|
-
border: 1px solid var(--avs-border);
|
|
102
|
-
background: var(--avs-bg);
|
|
103
|
-
color: var(--avs-text);
|
|
104
|
-
cursor: pointer;
|
|
105
|
-
display: flex;
|
|
106
|
-
align-items: center;
|
|
107
|
-
justify-content: center;
|
|
108
|
-
border-radius: 4px;
|
|
109
|
-
transition: all 0.15s;
|
|
110
|
-
padding: 0;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
.avs-mode-btn:hover {
|
|
114
|
-
background: var(--avs-surface);
|
|
115
|
-
border-color: var(--avs-text-muted);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
.avs-mode-btn svg {
|
|
119
|
-
width: 16px;
|
|
120
|
-
height: 16px;
|
|
92
|
+
margin-bottom: 4px;
|
|
93
|
+
border-bottom: 1px solid rgba(255,255,255,0.15);
|
|
94
|
+
padding-bottom: 6px;
|
|
121
95
|
}
|
|
122
96
|
|
|
123
97
|
.avs-mode-btn.avs-mode-active {
|
|
@@ -159,6 +133,14 @@
|
|
|
159
133
|
color: var(--avs-text-muted);
|
|
160
134
|
}
|
|
161
135
|
|
|
136
|
+
.avs-count-badge {
|
|
137
|
+
font-size: 11px;
|
|
138
|
+
color: var(--avs-text-muted);
|
|
139
|
+
white-space: nowrap;
|
|
140
|
+
flex-shrink: 0;
|
|
141
|
+
font-variant-numeric: tabular-nums;
|
|
142
|
+
}
|
|
143
|
+
|
|
162
144
|
.avs-filter-count {
|
|
163
145
|
font-size: 11px;
|
|
164
146
|
font-weight: 600;
|
|
@@ -291,11 +273,11 @@
|
|
|
291
273
|
width: 280px;
|
|
292
274
|
}
|
|
293
275
|
|
|
294
|
-
.avs-panel-left {
|
|
276
|
+
.avs-panel-left.avs-open {
|
|
295
277
|
border-right: 1px solid var(--avs-border);
|
|
296
278
|
}
|
|
297
279
|
|
|
298
|
-
.avs-panel-right {
|
|
280
|
+
.avs-panel-right.avs-open {
|
|
299
281
|
border-left: 1px solid var(--avs-border);
|
|
300
282
|
}
|
|
301
283
|
|
|
@@ -39,6 +39,17 @@ export function createToolbar(model, callbacks) {
|
|
|
39
39
|
filterCount.style.display = "none";
|
|
40
40
|
toolbar.appendChild(filterCount);
|
|
41
41
|
|
|
42
|
+
// Point count badge
|
|
43
|
+
const countBadge = document.createElement("span");
|
|
44
|
+
countBadge.className = "avs-count-badge";
|
|
45
|
+
function updatePointCount() {
|
|
46
|
+
const pts = model.get("points") || [];
|
|
47
|
+
countBadge.textContent = `${pts.length} points`;
|
|
48
|
+
}
|
|
49
|
+
updatePointCount();
|
|
50
|
+
model.on("change:points", updatePointCount);
|
|
51
|
+
toolbar.appendChild(countBadge);
|
|
52
|
+
|
|
42
53
|
// Run button
|
|
43
54
|
const runBtn = document.createElement("button");
|
|
44
55
|
runBtn.className = "avs-btn avs-btn-primary";
|
|
@@ -109,6 +109,7 @@ class VectorSpace(anywidget.AnyWidget):
|
|
|
109
109
|
def __init__(self, points: list[dict[str, Any]] | None = None, **kwargs: Any) -> None:
|
|
110
110
|
super().__init__(points=points or [], **kwargs)
|
|
111
111
|
self._backend_client: Any = None
|
|
112
|
+
self._vectors: Any = None # High-dim vectors for projection (numpy array, not synced to JS)
|
|
112
113
|
self.observe(self._on_execute_query, names=["_execute_query"])
|
|
113
114
|
|
|
114
115
|
# === Backend Configuration ===
|
|
@@ -209,6 +210,158 @@ class VectorSpace(anywidget.AnyWidget):
|
|
|
209
210
|
self.observe(_handler, names=["selected_points"])
|
|
210
211
|
return callback
|
|
211
212
|
|
|
213
|
+
# === Add Points ===
|
|
214
|
+
|
|
215
|
+
def add_points(self, data: list[dict[str, Any]]) -> VectorSpace:
|
|
216
|
+
"""Append point dicts to the existing points.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
data: List of point dicts with at least x, y keys.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Self for chaining.
|
|
223
|
+
"""
|
|
224
|
+
self.points = [*self.points, *_normalize_points(data)]
|
|
225
|
+
return self
|
|
226
|
+
|
|
227
|
+
def add_numpy(
|
|
228
|
+
self,
|
|
229
|
+
positions: Any,
|
|
230
|
+
*,
|
|
231
|
+
ids: list[str] | None = None,
|
|
232
|
+
labels: list[str] | None = None,
|
|
233
|
+
metadata: dict[str, list[Any]] | None = None,
|
|
234
|
+
) -> VectorSpace:
|
|
235
|
+
"""Append points from a NumPy array.
|
|
236
|
+
|
|
237
|
+
For arrays with D > 3 columns, the full vectors are stored internally
|
|
238
|
+
and PCA is used for the initial 3D coordinates. Call ``project()`` to
|
|
239
|
+
switch projection methods interactively.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
positions: Array of shape (N, 2), (N, 3), or (N, D) for high-dim data.
|
|
243
|
+
ids: Optional list of point IDs.
|
|
244
|
+
labels: Optional list of labels.
|
|
245
|
+
metadata: Optional dict mapping field names to per-point value lists.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Self for chaining.
|
|
249
|
+
"""
|
|
250
|
+
import numpy as np
|
|
251
|
+
|
|
252
|
+
arr = np.asarray(positions, dtype=np.float64)
|
|
253
|
+
n_samples, n_dims = arr.shape
|
|
254
|
+
|
|
255
|
+
# High-dimensional: store vectors and project to 3D
|
|
256
|
+
if n_dims > 3:
|
|
257
|
+
if self._vectors is not None:
|
|
258
|
+
self._vectors = np.vstack([self._vectors, arr])
|
|
259
|
+
else:
|
|
260
|
+
self._vectors = arr
|
|
261
|
+
coords = _pca(arr, n_components=3)
|
|
262
|
+
else:
|
|
263
|
+
coords = arr
|
|
264
|
+
|
|
265
|
+
offset = len(self.points)
|
|
266
|
+
new_points = []
|
|
267
|
+
for i in range(n_samples):
|
|
268
|
+
point: dict[str, Any] = {
|
|
269
|
+
"id": ids[i] if ids else f"point_{offset + i}",
|
|
270
|
+
"x": float(coords[i, 0]),
|
|
271
|
+
"y": float(coords[i, 1]),
|
|
272
|
+
"z": float(coords[i, 2]) if coords.shape[1] > 2 else 0.0,
|
|
273
|
+
}
|
|
274
|
+
if labels:
|
|
275
|
+
point["label"] = labels[i]
|
|
276
|
+
if metadata:
|
|
277
|
+
for key, values in metadata.items():
|
|
278
|
+
point[key] = values[i]
|
|
279
|
+
new_points.append(point)
|
|
280
|
+
self.points = [*self.points, *new_points]
|
|
281
|
+
return self
|
|
282
|
+
|
|
283
|
+
# === Projection ===
|
|
284
|
+
|
|
285
|
+
def set_vectors(self, vectors: Any) -> VectorSpace:
|
|
286
|
+
"""Store high-dimensional vectors for use with ``project()``.
|
|
287
|
+
|
|
288
|
+
Must have the same number of rows as ``self.points``.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
vectors: Array-like of shape (N, D).
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Self for chaining.
|
|
295
|
+
"""
|
|
296
|
+
import numpy as np
|
|
297
|
+
|
|
298
|
+
self._vectors = np.asarray(vectors, dtype=np.float64)
|
|
299
|
+
return self
|
|
300
|
+
|
|
301
|
+
def project(self, method: str = "pca", *, n_components: int = 3, **kwargs: Any) -> VectorSpace:
|
|
302
|
+
"""Reproject point coordinates using dimensionality reduction.
|
|
303
|
+
|
|
304
|
+
Reads vectors from ``set_vectors()`` / ``add_numpy()`` (for D > 3), or
|
|
305
|
+
falls back to extracting the ``vector`` field from each point dict.
|
|
306
|
+
|
|
307
|
+
Supported methods: ``pca``, ``tsne``, ``umap``.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
method: Projection algorithm name.
|
|
311
|
+
n_components: Target dimensions (2 or 3).
|
|
312
|
+
**kwargs: Passed to the underlying algorithm.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Self for chaining.
|
|
316
|
+
"""
|
|
317
|
+
vectors = self._resolve_vectors()
|
|
318
|
+
n_components = min(n_components, vectors.shape[1])
|
|
319
|
+
|
|
320
|
+
if method == "pca":
|
|
321
|
+
coords = _pca(vectors, n_components=n_components)
|
|
322
|
+
elif method == "tsne":
|
|
323
|
+
coords = _tsne(vectors, n_components=n_components, **kwargs)
|
|
324
|
+
elif method == "umap":
|
|
325
|
+
coords = _umap(vectors, n_components=n_components, **kwargs)
|
|
326
|
+
else:
|
|
327
|
+
raise ValueError(f"Unknown projection method: {method!r}. Use 'pca', 'tsne', or 'umap'.")
|
|
328
|
+
|
|
329
|
+
# Normalize to roughly [-1, 1] so the scene stays well-framed
|
|
330
|
+
for col in range(coords.shape[1]):
|
|
331
|
+
c = coords[:, col]
|
|
332
|
+
lo, hi = c.min(), c.max()
|
|
333
|
+
if hi - lo > 0:
|
|
334
|
+
coords[:, col] = 2 * (c - lo) / (hi - lo) - 1
|
|
335
|
+
|
|
336
|
+
points = [dict(p) for p in self.points]
|
|
337
|
+
for i, p in enumerate(points):
|
|
338
|
+
p["x"] = float(coords[i, 0])
|
|
339
|
+
p["y"] = float(coords[i, 1])
|
|
340
|
+
p["z"] = float(coords[i, 2]) if n_components >= 3 else 0.0
|
|
341
|
+
self.points = points
|
|
342
|
+
return self
|
|
343
|
+
|
|
344
|
+
def _resolve_vectors(self) -> Any:
|
|
345
|
+
"""Get the vector matrix for projection."""
|
|
346
|
+
import numpy as np
|
|
347
|
+
|
|
348
|
+
if self._vectors is not None:
|
|
349
|
+
if len(self._vectors) != len(self.points):
|
|
350
|
+
raise ValueError(
|
|
351
|
+
f"Stored vectors ({len(self._vectors)}) don't match point count ({len(self.points)}). "
|
|
352
|
+
"Call set_vectors() again after modifying points."
|
|
353
|
+
)
|
|
354
|
+
return self._vectors
|
|
355
|
+
|
|
356
|
+
# Fall back to point["vector"] fields
|
|
357
|
+
vecs = [p.get("vector") for p in self.points]
|
|
358
|
+
if all(v is not None for v in vecs):
|
|
359
|
+
return np.array(vecs, dtype=np.float64)
|
|
360
|
+
|
|
361
|
+
raise ValueError(
|
|
362
|
+
"No vectors available. Use set_vectors(), add_numpy() with D > 3, or ensure points have a 'vector' field."
|
|
363
|
+
)
|
|
364
|
+
|
|
212
365
|
# === Factory Methods ===
|
|
213
366
|
|
|
214
367
|
@classmethod
|
|
@@ -923,3 +1076,36 @@ def _to_list(obj: Any) -> list[Any]:
|
|
|
923
1076
|
if hasattr(obj, "tolist"):
|
|
924
1077
|
return obj.tolist()
|
|
925
1078
|
return list(obj)
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
# === Projection Helpers ===
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
def _pca(vectors: Any, n_components: int = 3) -> Any:
|
|
1085
|
+
"""PCA via SVD (numpy only, no sklearn needed)."""
|
|
1086
|
+
import numpy as np
|
|
1087
|
+
|
|
1088
|
+
x = np.asarray(vectors, dtype=np.float64)
|
|
1089
|
+
x_centered = x - x.mean(axis=0)
|
|
1090
|
+
_u, _s, vt = np.linalg.svd(x_centered, full_matrices=False)
|
|
1091
|
+
return x_centered @ vt[:n_components].T
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
def _tsne(vectors: Any, n_components: int = 3, **kwargs: Any) -> Any:
|
|
1095
|
+
"""t-SNE projection (requires scikit-learn)."""
|
|
1096
|
+
try:
|
|
1097
|
+
from sklearn.manifold import TSNE
|
|
1098
|
+
except ImportError:
|
|
1099
|
+
raise ImportError("t-SNE requires scikit-learn: uv add scikit-learn") from None
|
|
1100
|
+
kwargs.setdefault("perplexity", min(30.0, len(vectors) - 1))
|
|
1101
|
+
return TSNE(n_components=n_components, **kwargs).fit_transform(vectors)
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
def _umap(vectors: Any, n_components: int = 3, **kwargs: Any) -> Any:
|
|
1105
|
+
"""UMAP projection (requires umap-learn)."""
|
|
1106
|
+
try:
|
|
1107
|
+
from umap import UMAP
|
|
1108
|
+
except ImportError:
|
|
1109
|
+
raise ImportError("UMAP requires umap-learn: uv add umap-learn") from None
|
|
1110
|
+
kwargs.setdefault("n_neighbors", min(15, len(vectors) - 1))
|
|
1111
|
+
return UMAP(n_components=n_components, **kwargs).fit_transform(vectors)
|