anywidget-vector 0.3.0__tar.gz → 0.3.2__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.3.0 → anywidget_vector-0.3.2}/CHANGELOG.md +38 -2
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/PKG-INFO +1 -1
- anywidget_vector-0.3.2/assets/demo.png +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/examples/fashion-mnist-parallel-coords-6d.py +41 -21
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/pyproject.toml +2 -1
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/static/icons.js +1 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/ui/__init__.py +47 -26
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/ui/canvas.js +275 -60
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/ui/properties.js +8 -11
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/ui/sidebar.js +10 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/ui/styles.css +1 -4
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/widget.py +7 -3
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/tests/test_widget.py +2 -2
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/uv.lock +1 -1
- anywidget_vector-0.3.0/assets/demo.png +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/.github/workflows/ci.yml +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/.github/workflows/pypi.yml +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/.gitignore +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/.python-version +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/LICENSE +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/README.md +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/examples/demo.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/examples/test_add_numpy.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/examples/test_projection.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/__init__.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/__init__.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/chroma/__init__.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/chroma/client.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/chroma/converter.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/grafeo/__init__.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/grafeo/browser.js +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/grafeo/client.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/grafeo/converter.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/grafeo/embed.js +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/lancedb/__init__.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/lancedb/client.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/lancedb/converter.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/pinecone/__init__.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/pinecone/client.js +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/pinecone/converter.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/qdrant/__init__.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/qdrant/client.js +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/qdrant/converter.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/weaviate/__init__.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/weaviate/client.js +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/weaviate/converter.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/demo.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/py.typed +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/ui/constants.js +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/ui/settings.js +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/ui/toolbar.js +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/tests/__init__.py +0 -0
|
@@ -1,10 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.2 (2026-03-16)
|
|
4
|
+
|
|
5
|
+
### Improvements
|
|
6
|
+
|
|
7
|
+
- **Marimo reactivity fix**: Removed camera and hover JS-to-Python sync to prevent continuous cell re-execution. Only user-initiated actions (selection, mode change) now trigger marimo reactive updates
|
|
8
|
+
- **Custom scroll zoom**: Replaced OrbitControls zoom with a custom wheel handler for much finer granularity
|
|
9
|
+
- **Selection hides completely**: Lasso/box selection now fully hides non-selected points (scale-to-zero for instanced meshes) instead of dimming
|
|
10
|
+
- **Auto zoom-to-fit on selection**: Camera automatically frames the selected points after lasso/box selection
|
|
11
|
+
- **Search filter modes**: Typing in the search bar dims non-matching points as a preview, pressing Run/Enter hides them completely
|
|
12
|
+
- **Hover via callback**: Properties panel hover now uses a direct JS callback instead of model sync, eliminating unnecessary Python round-trips
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
- **setSelection scoping**: Moved `setSelection` helper to `createCanvas` scope so lasso/box/click handlers can access it
|
|
17
|
+
- **Camera debounce variable**: Fixed `_cameraDebounce` lexical declaration error by moving it to the correct scope
|
|
18
|
+
|
|
19
|
+
## 0.3.1 (2026-03-16)
|
|
20
|
+
|
|
21
|
+
### New Features
|
|
22
|
+
|
|
23
|
+
- **Lasso selection**: New lasso mode for freehand point selection in the 3D canvas, with point-in-polygon hit testing
|
|
24
|
+
- **Selection highlighting**: Selected points show wireframe highlight rings and non-selected points are dimmed to 15% brightness
|
|
25
|
+
- **Auto-sizing**: Width defaults to 100% (fill container), height defaults to 1:1 aspect ratio. Both can still be set to fixed pixel values
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
- **Properties panel**: Panel is now always created (previously skipped when `show_properties=False`), so the toolbar button always works. Starts closed unless `show_properties=True`
|
|
30
|
+
- **Distance metric feedback**: Changing the metric now auto-enables k=5 connections when a point is selected, giving immediate visual feedback
|
|
31
|
+
|
|
32
|
+
### Improvements
|
|
33
|
+
|
|
34
|
+
- Reduced scroll zoom sensitivity (`zoomSpeed` halved to 0.5)
|
|
35
|
+
- Search filter dimming increased from 12% to 6% opacity for clearer visual contrast
|
|
36
|
+
- Camera no longer resets on every point update (only on initial load)
|
|
37
|
+
- Cross-widget filtering: example updated so VectorSpace lasso selection feeds back into ParallelCoordinates dimming via marimo reactivity
|
|
38
|
+
|
|
3
39
|
## 0.3.0 (2026-03-16)
|
|
4
40
|
|
|
5
41
|
### New Features
|
|
6
42
|
|
|
7
|
-
- **Distance metrics sidebar**: Distance section with metric selector (euclidean, cosine, manhattan, dot product), configurable k-neighbors (0-50)
|
|
43
|
+
- **Distance metrics sidebar**: Distance section with metric selector (euclidean, cosine, manhattan, dot product), configurable k-neighbors (0-50) and live nearest-neighbor display for the selected point
|
|
8
44
|
- **Dimension mapping display**: Sidebar now shows active visual mapping fields: color field, size range, shape field and unique shape count
|
|
9
45
|
|
|
10
46
|
### Improvements
|
|
@@ -15,7 +51,7 @@
|
|
|
15
51
|
|
|
16
52
|
### New Features
|
|
17
53
|
|
|
18
|
-
- **6D parallel coordinates example**: Fashion-MNIST demo with PCA across 6+ dimensions, interactive axis mapping
|
|
54
|
+
- **6D parallel coordinates example**: Fashion-MNIST demo with PCA across 6+ dimensions, interactive axis mapping and shape mapping to categorical labels
|
|
19
55
|
|
|
20
56
|
## 0.2.7 (2026-03-15)
|
|
21
57
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: anywidget-vector
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
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
|
|
Binary file
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/examples/fashion-mnist-parallel-coords-6d.py
RENAMED
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
# "wigglystuff==0.2.37",
|
|
14
14
|
# "matplotlib==3.10.8",
|
|
15
15
|
# "pandas==3.0.1",
|
|
16
|
-
# "anywidget-vector==0.
|
|
16
|
+
# "anywidget-vector==0.3.1",
|
|
17
17
|
# ]
|
|
18
18
|
# ///
|
|
19
19
|
|
|
20
20
|
import marimo
|
|
21
21
|
|
|
22
|
-
__generated_with = "0.
|
|
22
|
+
__generated_with = "0.20.2"
|
|
23
23
|
app = marimo.App(width="full")
|
|
24
24
|
|
|
25
25
|
|
|
@@ -43,7 +43,7 @@ def _(mo):
|
|
|
43
43
|
mo.md(r"""
|
|
44
44
|
# Fashion MNIST: Parallel Coordinates + 6D Vector View
|
|
45
45
|
|
|
46
|
-
Brush the parallel coordinates axes to filter. The 3D scatter updates in real time.
|
|
46
|
+
Brush the parallel coordinates axes to filter. The 3D (xyz)+3D(color/size/shape) scatter updates in real time.
|
|
47
47
|
Use the dropdowns to map PCA dimensions to visual channels.
|
|
48
48
|
""")
|
|
49
49
|
return
|
|
@@ -77,7 +77,7 @@ def _(fetch_openml, np):
|
|
|
77
77
|
@app.cell
|
|
78
78
|
def _(mo):
|
|
79
79
|
n_samples_slider = mo.ui.slider(start=500, stop=5000, step=500, value=2000, label="Samples")
|
|
80
|
-
n_components_slider = mo.ui.slider(start=3, stop=15, step=1, value=
|
|
80
|
+
n_components_slider = mo.ui.slider(start=3, stop=15, step=1, value=5, label="PCA dims")
|
|
81
81
|
mo.hstack([n_samples_slider, n_components_slider])
|
|
82
82
|
return n_components_slider, n_samples_slider
|
|
83
83
|
|
|
@@ -204,8 +204,7 @@ def _(
|
|
|
204
204
|
|
|
205
205
|
vs_widget = VectorSpace(
|
|
206
206
|
points=vs_points,
|
|
207
|
-
|
|
208
|
-
height=500,
|
|
207
|
+
height=900,
|
|
209
208
|
show_toolbar=True,
|
|
210
209
|
show_settings=True,
|
|
211
210
|
show_properties=False,
|
|
@@ -213,36 +212,58 @@ def _(
|
|
|
213
212
|
)
|
|
214
213
|
vs = mo.ui.anywidget(vs_widget)
|
|
215
214
|
vs
|
|
216
|
-
return vs_points, vs_widget
|
|
215
|
+
return vs, vs_points, vs_widget
|
|
217
216
|
|
|
218
217
|
|
|
219
218
|
@app.cell
|
|
220
219
|
def _(LABEL_COLORS, color_dim, mo, vs_points, vs_widget, widget):
|
|
221
|
-
|
|
222
|
-
|
|
220
|
+
# ParallelCoordinates -> VectorSpace (one-way)
|
|
221
|
+
# Uses vs_widget (raw) to avoid triggering vs-dependent cells
|
|
222
|
+
_par_selected = list(widget.widget.selected_indices or [])
|
|
223
|
+
_par_filtered = set(widget.widget.filtered_indices)
|
|
224
|
+
_active_set = set(_par_selected) if _par_selected else _par_filtered
|
|
223
225
|
_updated = []
|
|
224
226
|
for _i, _p in enumerate(vs_points):
|
|
227
|
+
if _i not in _active_set:
|
|
228
|
+
continue
|
|
225
229
|
_new_p = dict(_p)
|
|
226
|
-
if
|
|
227
|
-
|
|
228
|
-
_new_p["color"] = LABEL_COLORS[_p["label"]]
|
|
229
|
-
else:
|
|
230
|
-
_new_p.pop("color", None)
|
|
230
|
+
if color_dim.value == "label":
|
|
231
|
+
_new_p["color"] = LABEL_COLORS[_p["label"]]
|
|
231
232
|
else:
|
|
232
|
-
_new_p
|
|
233
|
+
_new_p.pop("color", None)
|
|
233
234
|
_updated.append(_new_p)
|
|
234
235
|
vs_widget.points = _updated
|
|
235
|
-
mo.md(f"**{len(
|
|
236
|
+
mo.md(f"**{len(_updated)}** / {len(vs_points)} points selected")
|
|
236
237
|
return
|
|
237
238
|
|
|
238
239
|
|
|
239
240
|
@app.cell
|
|
240
|
-
def _(LABEL_COLORS,
|
|
241
|
+
def _(LABEL_COLORS, ParallelCoordinates, df, mo, vs):
|
|
242
|
+
# VectorSpace -> filtered ParallelCoordinates (reactive via vs.value)
|
|
243
|
+
_selected = list(vs.widget.selected_points or [])
|
|
244
|
+
if _selected:
|
|
245
|
+
_indices = sorted(int(sid.split("_")[1]) for sid in _selected)
|
|
246
|
+
_filtered_df = df[_indices]
|
|
247
|
+
_par = ParallelCoordinates(_filtered_df, color_by="label", color_map=LABEL_COLORS)
|
|
248
|
+
mo.vstack(
|
|
249
|
+
[
|
|
250
|
+
mo.md(f"**{len(_indices)}** points selected in 3D view"),
|
|
251
|
+
mo.ui.anywidget(_par),
|
|
252
|
+
]
|
|
253
|
+
)
|
|
254
|
+
else:
|
|
255
|
+
mo.md("*Lasso or box-select points in the 3D view to filter*")
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@app.cell
|
|
260
|
+
def _(LABEL_COLORS, idx, images, label_names, labels, np, plt, vs, widget):
|
|
261
|
+
_selected_ids = set(vs.widget.selected_points or [])
|
|
241
262
|
_filtered = widget.widget.filtered_indices
|
|
242
|
-
_sample_idx = np.array(_filtered[:10]) if len(_filtered) >= 10 else np.array(_filtered)
|
|
243
263
|
|
|
244
|
-
|
|
245
|
-
|
|
264
|
+
_show = [_i for _i in range(len(idx)) if f"p_{_i}" in _selected_ids][:10] if _selected_ids else list(_filtered[:10])
|
|
265
|
+
|
|
266
|
+
_sample_idx = np.array(_show) if len(_show) > 0 else np.array([0])
|
|
246
267
|
|
|
247
268
|
_fig, _axes = plt.subplots(1, len(_sample_idx), figsize=(2 * len(_sample_idx), 2.4))
|
|
248
269
|
if len(_sample_idx) == 1:
|
|
@@ -252,7 +273,6 @@ def _(LABEL_COLORS, idx, images, label_names, labels, np, plt, vs_widget, widget
|
|
|
252
273
|
_ax.imshow(images[idx[_si]].reshape(28, 28), cmap="gray")
|
|
253
274
|
_ax.set_title(_name, fontsize=9)
|
|
254
275
|
_ax.axis("off")
|
|
255
|
-
# Highlight if selected in 3D view
|
|
256
276
|
if f"p_{_si}" in _selected_ids:
|
|
257
277
|
_color = LABEL_COLORS.get(_name, "#0880ea")
|
|
258
278
|
for _spine in _ax.spines.values():
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "anywidget-vector"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.2"
|
|
4
4
|
description = "Interactive 3D vector visualization with query UI for vector databases"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -73,6 +73,7 @@ select = ["E", "F", "W", "I", "UP", "B", "SIM", "RUF"]
|
|
|
73
73
|
|
|
74
74
|
[tool.ruff.lint.per-file-ignores]
|
|
75
75
|
"src/anywidget_vector/widget.py" = ["E501"] # Long lines in embedded JS
|
|
76
|
+
"src/anywidget_vector/ui/__init__.py" = ["E501"] # Long lines in embedded JS
|
|
76
77
|
"examples/**" = ["B007", "B018"] # Marimo cells use bare expressions for output
|
|
77
78
|
|
|
78
79
|
[tool.ty.environment]
|
|
@@ -16,4 +16,5 @@ export const ICONS = {
|
|
|
16
16
|
zoomFit: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>',
|
|
17
17
|
cursor: '<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M4 2l12 14h-5l-2.3 5.5L6 20.3 8.3 15H4V2z"/></svg>',
|
|
18
18
|
boxSelect: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="3 2"><rect x="3" y="3" width="18" height="18" rx="1"/></svg>',
|
|
19
|
+
lasso: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="10" rx="9" ry="7"/><path d="M12 17v3M12 20l-2 2"/></svg>',
|
|
19
20
|
};
|
|
@@ -122,15 +122,25 @@ async function executeBackendQuery(model) {{
|
|
|
122
122
|
function render({{ model, el }}) {{
|
|
123
123
|
const wrapper = document.createElement("div");
|
|
124
124
|
wrapper.className = "avs-wrapper";
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
|
|
126
|
+
const _w = model.get("width");
|
|
127
|
+
const _h = model.get("height");
|
|
128
|
+
wrapper.style.width = _w > 0 ? _w + "px" : "100%";
|
|
129
|
+
if (_h > 0) {{
|
|
130
|
+
wrapper.style.height = _h + "px";
|
|
131
|
+
}} else {{
|
|
132
|
+
wrapper.style.aspectRatio = "1 / 1";
|
|
133
|
+
}}
|
|
134
|
+
|
|
127
135
|
el.appendChild(wrapper);
|
|
128
136
|
|
|
129
|
-
// Auto-detect host theme (
|
|
137
|
+
// Auto-detect host theme (Tailwind dark/dark-theme class, data-theme, or prefers-color-scheme)
|
|
130
138
|
function detectHostDark() {{
|
|
131
139
|
const html = document.documentElement;
|
|
132
|
-
if (html.classList.contains("dark")) return true;
|
|
140
|
+
if (html.classList.contains("dark") || html.classList.contains("dark-theme")) return true;
|
|
133
141
|
if (html.dataset.theme === "dark") return true;
|
|
142
|
+
if (document.body?.classList.contains("dark") || document.body?.classList.contains("dark-theme")) return true;
|
|
143
|
+
if (document.body?.dataset.theme === "dark") return true;
|
|
134
144
|
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) return true;
|
|
135
145
|
return false;
|
|
136
146
|
}}
|
|
@@ -141,27 +151,37 @@ function render({{ model, el }}) {{
|
|
|
141
151
|
model.save_changes();
|
|
142
152
|
}}
|
|
143
153
|
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
154
|
+
// Always auto-detect and observe theme changes
|
|
155
|
+
let autoTheme = true;
|
|
156
|
+
model.set("dark_mode", detectHostDark());
|
|
157
|
+
applyTheme(detectHostDark());
|
|
158
|
+
|
|
159
|
+
const themeObserver = new MutationObserver(() => {{
|
|
160
|
+
if (!autoTheme) return;
|
|
161
|
+
const dark = detectHostDark();
|
|
162
|
+
if (model.get("dark_mode") !== dark) {{
|
|
163
|
+
model.set("dark_mode", dark);
|
|
164
|
+
applyTheme(dark);
|
|
165
|
+
}}
|
|
166
|
+
}});
|
|
167
|
+
themeObserver.observe(document.documentElement, {{ attributes: true, attributeFilter: ["class", "data-theme", "style"] }});
|
|
168
|
+
if (document.body) {{
|
|
169
|
+
themeObserver.observe(document.body, {{ attributes: true, attributeFilter: ["class", "data-theme", "style"] }});
|
|
170
|
+
}}
|
|
171
|
+
if (window.matchMedia) {{
|
|
172
|
+
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {{
|
|
173
|
+
if (!autoTheme) return;
|
|
152
174
|
const dark = detectHostDark();
|
|
153
175
|
if (model.get("dark_mode") !== dark) {{
|
|
154
176
|
model.set("dark_mode", dark);
|
|
155
177
|
applyTheme(dark);
|
|
156
178
|
}}
|
|
157
179
|
}});
|
|
158
|
-
themeObserver.observe(document.documentElement, {{ attributes: true, attributeFilter: ["class", "data-theme"] }});
|
|
159
|
-
}} else {{
|
|
160
|
-
applyTheme(model.get("dark_mode"));
|
|
161
180
|
}}
|
|
162
181
|
|
|
163
|
-
// Manual toggle from settings panel
|
|
182
|
+
// Manual toggle from settings panel disables auto-theme
|
|
164
183
|
model.on("change:dark_mode", () => {{
|
|
184
|
+
autoTheme = false;
|
|
165
185
|
applyTheme(model.get("dark_mode"));
|
|
166
186
|
}});
|
|
167
187
|
|
|
@@ -175,7 +195,7 @@ function render({{ model, el }}) {{
|
|
|
175
195
|
onRunQuery: () => runQuery(),
|
|
176
196
|
onFilterInput: (text) => {{
|
|
177
197
|
if (canvas) {{
|
|
178
|
-
const result = canvas.applyFilter(text);
|
|
198
|
+
const result = canvas.applyFilter(text, false);
|
|
179
199
|
if (toolbarUI) toolbarUI.setFilterCount(result.matched, result.total);
|
|
180
200
|
}}
|
|
181
201
|
}},
|
|
@@ -215,21 +235,22 @@ function render({{ model, el }}) {{
|
|
|
215
235
|
main.appendChild(settingsPanel.element);
|
|
216
236
|
}}
|
|
217
237
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}}
|
|
238
|
+
propertiesPanel = createPropertiesPanel(model, {{
|
|
239
|
+
onClose: () => propertiesPanel?.close(),
|
|
240
|
+
}});
|
|
241
|
+
main.appendChild(propertiesPanel.element);
|
|
242
|
+
if (model.get("show_properties")) propertiesPanel.open();
|
|
224
243
|
|
|
225
|
-
const canvas = createCanvas(model, canvasContainer, {{
|
|
244
|
+
const canvas = createCanvas(model, canvasContainer, {{
|
|
245
|
+
onHover: (point) => propertiesPanel?.setHoveredPoint(point),
|
|
246
|
+
}});
|
|
226
247
|
|
|
227
248
|
async function runQuery() {{
|
|
228
249
|
const query = model.get("query_input") || "";
|
|
229
250
|
|
|
230
|
-
//
|
|
251
|
+
// Apply hard client-side filter (completely hide non-matching)
|
|
231
252
|
if (canvas) {{
|
|
232
|
-
const result = canvas.applyFilter(query);
|
|
253
|
+
const result = canvas.applyFilter(query, true);
|
|
233
254
|
if (toolbarUI) toolbarUI.setFilterCount(result.matched, result.total);
|
|
234
255
|
}}
|
|
235
256
|
|
|
@@ -40,8 +40,17 @@ export function createCanvas(model, container, callbacks) {
|
|
|
40
40
|
let animationId;
|
|
41
41
|
let resizeObserver;
|
|
42
42
|
let selectionRect, boxOverlay;
|
|
43
|
+
let selectionGroup;
|
|
44
|
+
let lassoOverlayEl, lassoSvg, lassoPathEl;
|
|
45
|
+
let isLassoing = false, lassoCoords = [];
|
|
43
46
|
let currentMode = model.get("selection_mode") || "click";
|
|
44
47
|
|
|
48
|
+
function setSelection(ids) {
|
|
49
|
+
model.set("selected_points", ids);
|
|
50
|
+
model.set("_selection_version", (model.get("_selection_version") || 0) + 1);
|
|
51
|
+
model.save_changes();
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
init();
|
|
46
55
|
animate();
|
|
47
56
|
|
|
@@ -80,10 +89,21 @@ export function createCanvas(model, container, callbacks) {
|
|
|
80
89
|
// Controls
|
|
81
90
|
controls = new OrbitControls(camera, renderer.domElement);
|
|
82
91
|
controls.enableDamping = true;
|
|
83
|
-
controls.dampingFactor = 0.
|
|
92
|
+
controls.dampingFactor = 0.08;
|
|
93
|
+
controls.zoomSpeed = 0.3;
|
|
94
|
+
controls.enableZoom = false; // disable built-in scroll zoom
|
|
84
95
|
const target = model.get("camera_target") || [0, 0, 0];
|
|
85
96
|
controls.target.set(target[0], target[1], target[2]);
|
|
86
|
-
|
|
97
|
+
|
|
98
|
+
// Custom smooth scroll zoom with reduced sensitivity
|
|
99
|
+
renderer.domElement.addEventListener("wheel", (e) => {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
const factor = 1 + e.deltaY * 0.0003;
|
|
102
|
+
const offset = camera.position.clone().sub(controls.target);
|
|
103
|
+
offset.multiplyScalar(factor);
|
|
104
|
+
camera.position.copy(controls.target).add(offset);
|
|
105
|
+
controls.update();
|
|
106
|
+
}, { passive: false });
|
|
87
107
|
|
|
88
108
|
// Lighting
|
|
89
109
|
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
|
|
@@ -96,6 +116,8 @@ export function createCanvas(model, container, callbacks) {
|
|
|
96
116
|
scene.add(pointsGroup);
|
|
97
117
|
connectionsGroup = new THREE.Group();
|
|
98
118
|
scene.add(connectionsGroup);
|
|
119
|
+
selectionGroup = new THREE.Group();
|
|
120
|
+
scene.add(selectionGroup);
|
|
99
121
|
axesGroup = new THREE.Group();
|
|
100
122
|
scene.add(axesGroup);
|
|
101
123
|
|
|
@@ -105,6 +127,7 @@ export function createCanvas(model, container, callbacks) {
|
|
|
105
127
|
setupTooltip();
|
|
106
128
|
setupZoomControls();
|
|
107
129
|
setupBoxSelection();
|
|
130
|
+
setupLassoSelection();
|
|
108
131
|
setupKeyboardShortcuts();
|
|
109
132
|
createPoints();
|
|
110
133
|
createConnections();
|
|
@@ -123,9 +146,9 @@ export function createCanvas(model, container, callbacks) {
|
|
|
123
146
|
e.preventDefault();
|
|
124
147
|
fitToView();
|
|
125
148
|
} else if (e.key === "Escape") {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
149
|
+
hoveredObject = null;
|
|
150
|
+
hideTooltip();
|
|
151
|
+
setSelection([]);
|
|
129
152
|
}
|
|
130
153
|
});
|
|
131
154
|
}
|
|
@@ -402,14 +425,12 @@ export function createCanvas(model, container, callbacks) {
|
|
|
402
425
|
const point = points[pointIndex];
|
|
403
426
|
if (point && (!hoveredObject || hoveredObject.pointId !== pointId)) {
|
|
404
427
|
hoveredObject = { pointIndex, pointId };
|
|
405
|
-
|
|
406
|
-
model.save_changes();
|
|
428
|
+
callbacks.onHover?.(point);
|
|
407
429
|
showTooltip(event, point);
|
|
408
430
|
}
|
|
409
431
|
} else if (hoveredObject) {
|
|
410
432
|
hoveredObject = null;
|
|
411
|
-
|
|
412
|
-
model.save_changes();
|
|
433
|
+
callbacks.onHover?.(null);
|
|
413
434
|
hideTooltip();
|
|
414
435
|
}
|
|
415
436
|
}
|
|
@@ -438,18 +459,16 @@ export function createCanvas(model, container, callbacks) {
|
|
|
438
459
|
const currentSelection = model.get("selected_points") || [];
|
|
439
460
|
|
|
440
461
|
if (selectionMode === "click") {
|
|
441
|
-
|
|
462
|
+
setSelection([pointId]);
|
|
442
463
|
} else if (selectionMode === "multi") {
|
|
443
464
|
if (currentSelection.includes(pointId)) {
|
|
444
|
-
|
|
465
|
+
setSelection(currentSelection.filter(id => id !== pointId));
|
|
445
466
|
} else {
|
|
446
|
-
|
|
467
|
+
setSelection([...currentSelection, pointId]);
|
|
447
468
|
}
|
|
448
469
|
}
|
|
449
|
-
model.save_changes();
|
|
450
470
|
} else {
|
|
451
|
-
|
|
452
|
-
model.save_changes();
|
|
471
|
+
setSelection([]);
|
|
453
472
|
}
|
|
454
473
|
}
|
|
455
474
|
|
|
@@ -518,25 +537,28 @@ export function createCanvas(model, container, callbacks) {
|
|
|
518
537
|
boxBtn.innerHTML = ICONS.boxSelect;
|
|
519
538
|
boxBtn.title = "Box select mode";
|
|
520
539
|
|
|
540
|
+
const lassoBtn = document.createElement("button");
|
|
541
|
+
lassoBtn.className = "avs-zoom-btn avs-mode-btn";
|
|
542
|
+
lassoBtn.innerHTML = ICONS.lasso;
|
|
543
|
+
lassoBtn.title = "Lasso select mode";
|
|
544
|
+
|
|
521
545
|
function setActiveMode(mode) {
|
|
522
546
|
currentMode = mode;
|
|
523
|
-
model.set("selection_mode", mode
|
|
547
|
+
model.set("selection_mode", mode);
|
|
524
548
|
model.save_changes();
|
|
525
549
|
|
|
526
|
-
clickBtn.classList.toggle("avs-mode-active", mode
|
|
550
|
+
clickBtn.classList.toggle("avs-mode-active", mode === "click");
|
|
527
551
|
boxBtn.classList.toggle("avs-mode-active", mode === "box");
|
|
552
|
+
lassoBtn.classList.toggle("avs-mode-active", mode === "lasso");
|
|
528
553
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
// Show/hide box overlay
|
|
533
|
-
if (boxOverlay) {
|
|
534
|
-
boxOverlay.style.display = (mode === "box") ? "block" : "none";
|
|
535
|
-
}
|
|
554
|
+
controls.enabled = (mode === "click");
|
|
555
|
+
if (boxOverlay) boxOverlay.style.display = (mode === "box") ? "block" : "none";
|
|
556
|
+
if (lassoOverlayEl) lassoOverlayEl.style.display = (mode === "lasso") ? "block" : "none";
|
|
536
557
|
}
|
|
537
558
|
|
|
538
559
|
clickBtn.addEventListener("click", () => setActiveMode("click"));
|
|
539
560
|
boxBtn.addEventListener("click", () => setActiveMode("box"));
|
|
561
|
+
lassoBtn.addEventListener("click", () => setActiveMode("lasso"));
|
|
540
562
|
|
|
541
563
|
// Sync with model changes from Python side
|
|
542
564
|
model.on("change:selection_mode", () => {
|
|
@@ -546,6 +568,7 @@ export function createCanvas(model, container, callbacks) {
|
|
|
546
568
|
|
|
547
569
|
modeGroup.appendChild(clickBtn);
|
|
548
570
|
modeGroup.appendChild(boxBtn);
|
|
571
|
+
modeGroup.appendChild(lassoBtn);
|
|
549
572
|
|
|
550
573
|
zoomControls.appendChild(modeGroup);
|
|
551
574
|
zoomControls.appendChild(zoomInBtn);
|
|
@@ -619,8 +642,7 @@ export function createCanvas(model, container, callbacks) {
|
|
|
619
642
|
{ x1, y1, x2, y2 },
|
|
620
643
|
rect.width, rect.height
|
|
621
644
|
);
|
|
622
|
-
|
|
623
|
-
model.save_changes();
|
|
645
|
+
setSelection(selectedIds);
|
|
624
646
|
});
|
|
625
647
|
}
|
|
626
648
|
|
|
@@ -659,14 +681,8 @@ export function createCanvas(model, container, callbacks) {
|
|
|
659
681
|
tooltip.style.display = "none";
|
|
660
682
|
}
|
|
661
683
|
|
|
662
|
-
function onCameraChange() {
|
|
663
|
-
model.set("camera_position", [camera.position.x, camera.position.y, camera.position.z]);
|
|
664
|
-
model.set("camera_target", [controls.target.x, controls.target.y, controls.target.z]);
|
|
665
|
-
model.save_changes();
|
|
666
|
-
}
|
|
667
|
-
|
|
668
684
|
function bindModelEvents() {
|
|
669
|
-
model.on("change:points", () => { createPoints(); createConnections();
|
|
685
|
+
model.on("change:points", () => { createPoints(); createConnections(); updateSelectionHighlight(); });
|
|
670
686
|
model.on("change:background", () => { scene.background = new THREE.Color(model.get("background")); });
|
|
671
687
|
model.on("change:show_axes", setupAxesAndGrid);
|
|
672
688
|
model.on("change:show_grid", setupAxesAndGrid);
|
|
@@ -684,6 +700,7 @@ export function createCanvas(model, container, callbacks) {
|
|
|
684
700
|
model.on("change:distance_metric", createConnections);
|
|
685
701
|
model.on("change:connection_color", createConnections);
|
|
686
702
|
model.on("change:connection_opacity", createConnections);
|
|
703
|
+
model.on("change:selected_points", updateSelectionHighlight);
|
|
687
704
|
model.on("change:camera_position", () => {
|
|
688
705
|
const pos = model.get("camera_position");
|
|
689
706
|
if (pos) camera.position.set(pos[0], pos[1], pos[2]);
|
|
@@ -714,32 +731,43 @@ export function createCanvas(model, container, callbacks) {
|
|
|
714
731
|
});
|
|
715
732
|
}
|
|
716
733
|
|
|
717
|
-
function applyFilter(filterText) {
|
|
734
|
+
function applyFilter(filterText, hard) {
|
|
718
735
|
const points = model.get("points") || [];
|
|
719
736
|
const filter = (filterText || "").toLowerCase().trim();
|
|
720
737
|
const total = points.length;
|
|
721
738
|
|
|
722
739
|
if (!filter) {
|
|
723
|
-
// Reset all to
|
|
740
|
+
// Reset all to visible
|
|
724
741
|
pointsGroup.children.forEach(obj => {
|
|
725
742
|
if (obj.userData.isInstanced) {
|
|
726
|
-
|
|
743
|
+
if (obj.userData._filterOrigMatrices) {
|
|
744
|
+
const m = new THREE.Matrix4();
|
|
745
|
+
for (let i = 0; i < obj.userData.pointIndices.length; i++) {
|
|
746
|
+
m.fromArray(obj.userData._filterOrigMatrices, i * 16);
|
|
747
|
+
obj.setMatrixAt(i, m);
|
|
748
|
+
}
|
|
749
|
+
obj.instanceMatrix.needsUpdate = true;
|
|
750
|
+
obj.userData._filterOrigMatrices = null;
|
|
751
|
+
}
|
|
727
752
|
if (obj.userData._originalColors) {
|
|
728
753
|
const attr = obj.geometry.getAttribute("color");
|
|
729
754
|
attr.array.set(obj.userData._originalColors);
|
|
730
755
|
attr.needsUpdate = true;
|
|
756
|
+
obj.userData._originalColors = null;
|
|
757
|
+
}
|
|
758
|
+
} else {
|
|
759
|
+
obj.visible = true;
|
|
760
|
+
if (obj.material) {
|
|
761
|
+
obj.material.opacity = 1;
|
|
762
|
+
obj.material.transparent = false;
|
|
763
|
+
obj.material.needsUpdate = true;
|
|
731
764
|
}
|
|
732
|
-
} else if (obj.material) {
|
|
733
|
-
obj.material.opacity = 1;
|
|
734
|
-
obj.material.transparent = false;
|
|
735
|
-
obj.material.needsUpdate = true;
|
|
736
765
|
}
|
|
737
766
|
});
|
|
738
767
|
return { matched: total, total };
|
|
739
768
|
}
|
|
740
769
|
|
|
741
770
|
let matched = 0;
|
|
742
|
-
// Build match set from points data
|
|
743
771
|
const matchSet = new Set();
|
|
744
772
|
points.forEach((point, idx) => {
|
|
745
773
|
if (pointMatchesFilter(point, filter)) {
|
|
@@ -748,32 +776,219 @@ export function createCanvas(model, container, callbacks) {
|
|
|
748
776
|
}
|
|
749
777
|
});
|
|
750
778
|
|
|
779
|
+
if (hard) {
|
|
780
|
+
// Hard filter: completely hide non-matching
|
|
781
|
+
const zeroMatrix = new THREE.Matrix4().makeScale(0, 0, 0);
|
|
782
|
+
pointsGroup.children.forEach(obj => {
|
|
783
|
+
if (obj.userData.isInstanced) {
|
|
784
|
+
const indices = obj.userData.pointIndices;
|
|
785
|
+
if (!obj.userData._filterOrigMatrices) {
|
|
786
|
+
const buf = new Float32Array(indices.length * 16);
|
|
787
|
+
const m = new THREE.Matrix4();
|
|
788
|
+
for (let i = 0; i < indices.length; i++) {
|
|
789
|
+
obj.getMatrixAt(i, m);
|
|
790
|
+
m.toArray(buf, i * 16);
|
|
791
|
+
}
|
|
792
|
+
obj.userData._filterOrigMatrices = buf;
|
|
793
|
+
}
|
|
794
|
+
const m = new THREE.Matrix4();
|
|
795
|
+
for (let i = 0; i < indices.length; i++) {
|
|
796
|
+
if (matchSet.has(indices[i])) {
|
|
797
|
+
m.fromArray(obj.userData._filterOrigMatrices, i * 16);
|
|
798
|
+
} else {
|
|
799
|
+
m.copy(zeroMatrix);
|
|
800
|
+
}
|
|
801
|
+
obj.setMatrixAt(i, m);
|
|
802
|
+
}
|
|
803
|
+
obj.instanceMatrix.needsUpdate = true;
|
|
804
|
+
} else {
|
|
805
|
+
obj.visible = matchSet.has(obj.userData.pointIndex);
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
} else {
|
|
809
|
+
// Soft filter: dim non-matching
|
|
810
|
+
pointsGroup.children.forEach(obj => {
|
|
811
|
+
if (obj.userData.isInstanced) {
|
|
812
|
+
const indices = obj.userData.pointIndices;
|
|
813
|
+
const attr = obj.geometry.getAttribute("color");
|
|
814
|
+
if (!obj.userData._originalColors) {
|
|
815
|
+
obj.userData._originalColors = new Float32Array(attr.array);
|
|
816
|
+
}
|
|
817
|
+
for (let i = 0; i < indices.length; i++) {
|
|
818
|
+
const dim = matchSet.has(indices[i]) ? 1.0 : 0.08;
|
|
819
|
+
attr.array[i * 3] = obj.userData._originalColors[i * 3] * dim;
|
|
820
|
+
attr.array[i * 3 + 1] = obj.userData._originalColors[i * 3 + 1] * dim;
|
|
821
|
+
attr.array[i * 3 + 2] = obj.userData._originalColors[i * 3 + 2] * dim;
|
|
822
|
+
}
|
|
823
|
+
attr.needsUpdate = true;
|
|
824
|
+
} else if (obj.material) {
|
|
825
|
+
const matches = matchSet.has(obj.userData.pointIndex);
|
|
826
|
+
obj.material.transparent = !matches;
|
|
827
|
+
obj.material.opacity = matches ? 1.0 : 0.08;
|
|
828
|
+
obj.material.needsUpdate = true;
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
return { matched, total };
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function setupLassoSelection() {
|
|
837
|
+
// SVG overlay for drawing the lasso path
|
|
838
|
+
lassoSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
839
|
+
lassoSvg.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;z-index:16;display:none;pointer-events:none;";
|
|
840
|
+
container.appendChild(lassoSvg);
|
|
841
|
+
|
|
842
|
+
lassoPathEl = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
|
|
843
|
+
lassoPathEl.setAttribute("fill", "rgba(99, 102, 241, 0.1)");
|
|
844
|
+
lassoPathEl.setAttribute("stroke", "#6366f1");
|
|
845
|
+
lassoPathEl.setAttribute("stroke-width", "2");
|
|
846
|
+
lassoPathEl.setAttribute("stroke-dasharray", "4 2");
|
|
847
|
+
lassoSvg.appendChild(lassoPathEl);
|
|
848
|
+
|
|
849
|
+
// Transparent overlay to capture mouse events in lasso mode
|
|
850
|
+
lassoOverlayEl = document.createElement("div");
|
|
851
|
+
lassoOverlayEl.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;z-index:15;display:none;cursor:crosshair;";
|
|
852
|
+
container.appendChild(lassoOverlayEl);
|
|
853
|
+
|
|
854
|
+
lassoOverlayEl.addEventListener("mousedown", (e) => {
|
|
855
|
+
if (currentMode !== "lasso") return;
|
|
856
|
+
e.preventDefault();
|
|
857
|
+
isLassoing = true;
|
|
858
|
+
lassoCoords = [];
|
|
859
|
+
const rect = container.getBoundingClientRect();
|
|
860
|
+
lassoCoords.push([e.clientX - rect.left, e.clientY - rect.top]);
|
|
861
|
+
lassoSvg.style.display = "block";
|
|
862
|
+
lassoPathEl.setAttribute("points", "");
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
lassoOverlayEl.addEventListener("mousemove", (e) => {
|
|
866
|
+
if (!isLassoing) return;
|
|
867
|
+
const rect = container.getBoundingClientRect();
|
|
868
|
+
lassoCoords.push([e.clientX - rect.left, e.clientY - rect.top]);
|
|
869
|
+
lassoPathEl.setAttribute("points", lassoCoords.map(([x, y]) => `${x},${y}`).join(" "));
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
lassoOverlayEl.addEventListener("mouseup", () => {
|
|
873
|
+
if (!isLassoing) return;
|
|
874
|
+
isLassoing = false;
|
|
875
|
+
lassoSvg.style.display = "none";
|
|
876
|
+
|
|
877
|
+
if (lassoCoords.length < 3) return;
|
|
878
|
+
|
|
879
|
+
// Close the polygon
|
|
880
|
+
lassoCoords.push(lassoCoords[0]);
|
|
881
|
+
|
|
882
|
+
const rect = container.getBoundingClientRect();
|
|
883
|
+
const selectedIds = [];
|
|
884
|
+
const points = model.get("points") || [];
|
|
885
|
+
points.forEach((p, idx) => {
|
|
886
|
+
const pos = new THREE.Vector3(p.x ?? 0, p.y ?? 0, p.z ?? 0).project(camera);
|
|
887
|
+
const sx = (pos.x * 0.5 + 0.5) * rect.width;
|
|
888
|
+
const sy = (-pos.y * 0.5 + 0.5) * rect.height;
|
|
889
|
+
if (pointInPolygon(sx, sy, lassoCoords)) {
|
|
890
|
+
selectedIds.push(p.id || `point_${idx}`);
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
setSelection(selectedIds);
|
|
895
|
+
lassoCoords = [];
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function pointInPolygon(x, y, polygon) {
|
|
900
|
+
let inside = false;
|
|
901
|
+
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
902
|
+
const [xi, yi] = polygon[i];
|
|
903
|
+
const [xj, yj] = polygon[j];
|
|
904
|
+
if (((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) {
|
|
905
|
+
inside = !inside;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return inside;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function updateSelectionHighlight() {
|
|
912
|
+
// Clear previous highlight rings
|
|
913
|
+
while (selectionGroup.children.length > 0) {
|
|
914
|
+
const obj = selectionGroup.children[0];
|
|
915
|
+
if (obj.geometry) obj.geometry.dispose();
|
|
916
|
+
if (obj.material) obj.material.dispose();
|
|
917
|
+
selectionGroup.remove(obj);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const selectedIds = model.get("selected_points") || [];
|
|
921
|
+
const points = model.get("points") || [];
|
|
922
|
+
|
|
923
|
+
if (selectedIds.length === 0) {
|
|
924
|
+
// Restore all points: show everything
|
|
925
|
+
pointsGroup.children.forEach(obj => {
|
|
926
|
+
if (obj.userData.isInstanced) {
|
|
927
|
+
// Restore original instance matrices
|
|
928
|
+
if (obj.userData._selOrigMatrices) {
|
|
929
|
+
const m = new THREE.Matrix4();
|
|
930
|
+
for (let i = 0; i < obj.userData.pointIds.length; i++) {
|
|
931
|
+
m.fromArray(obj.userData._selOrigMatrices, i * 16);
|
|
932
|
+
obj.setMatrixAt(i, m);
|
|
933
|
+
}
|
|
934
|
+
obj.instanceMatrix.needsUpdate = true;
|
|
935
|
+
obj.userData._selOrigMatrices = null;
|
|
936
|
+
}
|
|
937
|
+
} else {
|
|
938
|
+
obj.visible = true;
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const selectedSet = new Set(selectedIds);
|
|
945
|
+
const zeroMatrix = new THREE.Matrix4().makeScale(0, 0, 0);
|
|
946
|
+
|
|
947
|
+
// Hide non-selected points completely
|
|
751
948
|
pointsGroup.children.forEach(obj => {
|
|
752
949
|
if (obj.userData.isInstanced) {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
950
|
+
const pointIds = obj.userData.pointIds;
|
|
951
|
+
// Back up original matrices on first selection
|
|
952
|
+
if (!obj.userData._selOrigMatrices) {
|
|
953
|
+
const buf = new Float32Array(pointIds.length * 16);
|
|
954
|
+
const m = new THREE.Matrix4();
|
|
955
|
+
for (let i = 0; i < pointIds.length; i++) {
|
|
956
|
+
obj.getMatrixAt(i, m);
|
|
957
|
+
m.toArray(buf, i * 16);
|
|
958
|
+
}
|
|
959
|
+
obj.userData._selOrigMatrices = buf;
|
|
758
960
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
961
|
+
// Scale non-selected instances to zero
|
|
962
|
+
const m = new THREE.Matrix4();
|
|
963
|
+
for (let i = 0; i < pointIds.length; i++) {
|
|
964
|
+
if (selectedSet.has(pointIds[i])) {
|
|
965
|
+
m.fromArray(obj.userData._selOrigMatrices, i * 16);
|
|
966
|
+
} else {
|
|
967
|
+
m.copy(zeroMatrix);
|
|
968
|
+
}
|
|
969
|
+
obj.setMatrixAt(i, m);
|
|
765
970
|
}
|
|
766
|
-
|
|
767
|
-
} else
|
|
768
|
-
|
|
769
|
-
const matches = matchSet.has(idx);
|
|
770
|
-
obj.material.transparent = !matches;
|
|
771
|
-
obj.material.opacity = matches ? 1.0 : 0.12;
|
|
772
|
-
obj.material.needsUpdate = true;
|
|
971
|
+
obj.instanceMatrix.needsUpdate = true;
|
|
972
|
+
} else {
|
|
973
|
+
obj.visible = selectedSet.has(obj.userData.pointId);
|
|
773
974
|
}
|
|
774
975
|
});
|
|
775
976
|
|
|
776
|
-
|
|
977
|
+
// Zoom to fit selected points
|
|
978
|
+
const selectedPoints = points.filter(p => selectedSet.has(p.id));
|
|
979
|
+
if (selectedPoints.length > 0) {
|
|
980
|
+
const box = new THREE.Box3();
|
|
981
|
+
selectedPoints.forEach(p => box.expandByPoint(new THREE.Vector3(p.x ?? 0, p.y ?? 0, p.z ?? 0)));
|
|
982
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
983
|
+
const size = box.getSize(new THREE.Vector3()).length() || 0.1;
|
|
984
|
+
const distance = size / (2 * Math.tan(Math.PI * camera.fov / 360));
|
|
985
|
+
controls.target.copy(center);
|
|
986
|
+
camera.position.copy(center.clone().add(new THREE.Vector3(0, 0, distance * 0.55)));
|
|
987
|
+
camera.near = Math.max(0.01, distance * 0.001);
|
|
988
|
+
camera.far = Math.max(1000, distance * 10);
|
|
989
|
+
camera.updateProjectionMatrix();
|
|
990
|
+
controls.update();
|
|
991
|
+
}
|
|
777
992
|
}
|
|
778
993
|
|
|
779
994
|
return { cleanup, applyFilter };
|
|
@@ -46,23 +46,20 @@ export function createPropertiesPanel(model, callbacks) {
|
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
// Update on hover
|
|
50
|
-
model.on("change:hovered_point", () => {
|
|
51
|
-
const hovered = model.get("hovered_point");
|
|
52
|
-
const selectedIds = model.get("selected_points") || [];
|
|
53
|
-
if (hovered && selectedIds.length === 0) {
|
|
54
|
-
showPointProperties(content, hovered, true);
|
|
55
|
-
} else if (selectedIds.length === 0) {
|
|
56
|
-
showEmpty(content);
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
49
|
return {
|
|
61
50
|
element: panel,
|
|
62
51
|
open: () => panel.classList.add("avs-open"),
|
|
63
52
|
close: () => panel.classList.remove("avs-open"),
|
|
64
53
|
toggle: () => panel.classList.toggle("avs-open"),
|
|
65
54
|
isOpen: () => panel.classList.contains("avs-open"),
|
|
55
|
+
setHoveredPoint: (point) => {
|
|
56
|
+
const selectedIds = model.get("selected_points") || [];
|
|
57
|
+
if (point && selectedIds.length === 0) {
|
|
58
|
+
showPointProperties(content, point, true);
|
|
59
|
+
} else if (!point && selectedIds.length === 0) {
|
|
60
|
+
showEmpty(content);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
66
63
|
};
|
|
67
64
|
}
|
|
68
65
|
|
|
@@ -206,6 +206,16 @@ export function createSidebar(model, callbacks) {
|
|
|
206
206
|
});
|
|
207
207
|
metricSelect.addEventListener("change", () => {
|
|
208
208
|
model.set("distance_metric", metricSelect.value);
|
|
209
|
+
// Auto-enable connections when a point is selected
|
|
210
|
+
const selected = model.get("selected_points") || [];
|
|
211
|
+
if (selected.length === 1) {
|
|
212
|
+
model.set("reference_point", selected[0]);
|
|
213
|
+
if (!model.get("show_connections") || model.get("k_neighbors") === 0) {
|
|
214
|
+
model.set("k_neighbors", 5);
|
|
215
|
+
model.set("show_connections", true);
|
|
216
|
+
kInput.value = 5;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
209
219
|
model.save_changes();
|
|
210
220
|
});
|
|
211
221
|
metricGroup.appendChild(metricSelect);
|
|
@@ -34,8 +34,9 @@ class VectorSpace(anywidget.AnyWidget):
|
|
|
34
34
|
points = traitlets.List(trait=traitlets.Dict()).tag(sync=True)
|
|
35
35
|
|
|
36
36
|
# === Display ===
|
|
37
|
-
width
|
|
38
|
-
|
|
37
|
+
# width=0 means "auto" (fill container). height=0 means "match width" (1:1 ratio).
|
|
38
|
+
width = traitlets.Int(default_value=0).tag(sync=True)
|
|
39
|
+
height = traitlets.Int(default_value=0).tag(sync=True)
|
|
39
40
|
background = traitlets.Unicode(default_value="#1a1a2e").tag(sync=True)
|
|
40
41
|
|
|
41
42
|
# === Theme ===
|
|
@@ -66,8 +67,11 @@ class VectorSpace(anywidget.AnyWidget):
|
|
|
66
67
|
|
|
67
68
|
# === Interaction ===
|
|
68
69
|
selected_points = traitlets.List(default_value=[]).tag(sync=True)
|
|
70
|
+
_selection_version = traitlets.Int(default_value=0).tag(sync=True)
|
|
69
71
|
hovered_point = traitlets.Dict(default_value=None, allow_none=True).tag(sync=True)
|
|
70
|
-
selection_mode = traitlets.CaselessStrEnum(values=["click", "multi", "box"], default_value="click").tag(
|
|
72
|
+
selection_mode = traitlets.CaselessStrEnum(values=["click", "multi", "box", "lasso"], default_value="click").tag(
|
|
73
|
+
sync=True
|
|
74
|
+
)
|
|
71
75
|
|
|
72
76
|
# === Tooltip ===
|
|
73
77
|
show_tooltip = traitlets.Bool(default_value=True).tag(sync=True)
|
|
@@ -12,8 +12,8 @@ class TestVectorSpaceInit:
|
|
|
12
12
|
"""Test empty initialization."""
|
|
13
13
|
widget = VectorSpace()
|
|
14
14
|
assert widget.points == []
|
|
15
|
-
assert widget.width ==
|
|
16
|
-
assert widget.height ==
|
|
15
|
+
assert widget.width == 0
|
|
16
|
+
assert widget.height == 0
|
|
17
17
|
|
|
18
18
|
def test_with_points(self):
|
|
19
19
|
"""Test initialization with points."""
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/chroma/__init__.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/chroma/client.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/chroma/converter.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/grafeo/__init__.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/grafeo/browser.js
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/grafeo/client.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/grafeo/converter.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/grafeo/embed.js
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/lancedb/__init__.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/lancedb/client.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/lancedb/converter.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/pinecone/__init__.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/pinecone/client.js
RENAMED
|
File without changes
|
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/qdrant/__init__.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/qdrant/client.js
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/qdrant/converter.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/weaviate/__init__.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.2}/src/anywidget_vector/backends/weaviate/client.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|