anywidget-vector 0.3.0__tar.gz → 0.3.1__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.1}/CHANGELOG.md +20 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/PKG-INFO +1 -1
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/examples/fashion-mnist-parallel-coords-6d.py +45 -22
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/pyproject.toml +1 -1
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/static/icons.js +1 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/__init__.py +21 -12
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/canvas.js +275 -60
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/properties.js +8 -11
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/sidebar.js +10 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/widget.py +7 -3
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/tests/test_widget.py +2 -2
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/uv.lock +1 -1
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/.github/workflows/ci.yml +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/.github/workflows/pypi.yml +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/.gitignore +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/.python-version +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/LICENSE +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/README.md +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/assets/demo.png +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/examples/demo.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/examples/test_add_numpy.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/examples/test_projection.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/__init__.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/__init__.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/chroma/__init__.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/chroma/client.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/chroma/converter.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/grafeo/__init__.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/grafeo/browser.js +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/grafeo/client.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/grafeo/converter.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/grafeo/embed.js +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/lancedb/__init__.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/lancedb/client.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/lancedb/converter.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/pinecone/__init__.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/pinecone/client.js +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/pinecone/converter.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/qdrant/__init__.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/qdrant/client.js +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/qdrant/converter.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/weaviate/__init__.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/weaviate/client.js +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/weaviate/converter.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/demo.py +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/py.typed +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/constants.js +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/settings.js +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/styles.css +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/toolbar.js +0 -0
- {anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/tests/__init__.py +0 -0
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 (2026-03-16)
|
|
4
|
+
|
|
5
|
+
### New Features
|
|
6
|
+
|
|
7
|
+
- **Lasso selection**: New lasso mode for freehand point selection in the 3D canvas, with point-in-polygon hit testing
|
|
8
|
+
- **Selection highlighting**: Selected points show wireframe highlight rings and non-selected points are dimmed to 15% brightness
|
|
9
|
+
- **Auto-sizing**: Width defaults to 100% (fill container), height defaults to 1:1 aspect ratio. Both can still be set to fixed pixel values
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
- **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`
|
|
14
|
+
- **Distance metric feedback**: Changing the metric now auto-enables k=5 connections when a point is selected, giving immediate visual feedback
|
|
15
|
+
|
|
16
|
+
### Improvements
|
|
17
|
+
|
|
18
|
+
- Reduced scroll zoom sensitivity (`zoomSpeed` halved to 0.5)
|
|
19
|
+
- Search filter dimming increased from 12% to 6% opacity for clearer visual contrast
|
|
20
|
+
- Camera no longer resets on every point update (only on initial load)
|
|
21
|
+
- Cross-widget filtering: example updated so VectorSpace lasso selection feeds back into ParallelCoordinates dimming via marimo reactivity
|
|
22
|
+
|
|
3
23
|
## 0.3.0 (2026-03-16)
|
|
4
24
|
|
|
5
25
|
### New Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: anywidget-vector
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/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
|
|
|
@@ -34,7 +34,6 @@ def _():
|
|
|
34
34
|
from wigglystuff import ParallelCoordinates
|
|
35
35
|
|
|
36
36
|
from anywidget_vector import VectorSpace
|
|
37
|
-
|
|
38
37
|
return PCA, ParallelCoordinates, VectorSpace, fetch_openml, mo, np, pl, plt
|
|
39
38
|
|
|
40
39
|
|
|
@@ -43,7 +42,7 @@ def _(mo):
|
|
|
43
42
|
mo.md(r"""
|
|
44
43
|
# Fashion MNIST: Parallel Coordinates + 6D Vector View
|
|
45
44
|
|
|
46
|
-
Brush the parallel coordinates axes to filter. The 3D scatter updates in real time.
|
|
45
|
+
Brush the parallel coordinates axes to filter. The 3D (xyz)+3D(color/size/shape) scatter updates in real time.
|
|
47
46
|
Use the dropdowns to map PCA dimensions to visual channels.
|
|
48
47
|
""")
|
|
49
48
|
return
|
|
@@ -77,7 +76,7 @@ def _(fetch_openml, np):
|
|
|
77
76
|
@app.cell
|
|
78
77
|
def _(mo):
|
|
79
78
|
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=
|
|
79
|
+
n_components_slider = mo.ui.slider(start=3, stop=15, step=1, value=5, label="PCA dims")
|
|
81
80
|
mo.hstack([n_samples_slider, n_components_slider])
|
|
82
81
|
return n_components_slider, n_samples_slider
|
|
83
82
|
|
|
@@ -204,8 +203,7 @@ def _(
|
|
|
204
203
|
|
|
205
204
|
vs_widget = VectorSpace(
|
|
206
205
|
points=vs_points,
|
|
207
|
-
|
|
208
|
-
height=500,
|
|
206
|
+
height=900,
|
|
209
207
|
show_toolbar=True,
|
|
210
208
|
show_settings=True,
|
|
211
209
|
show_properties=False,
|
|
@@ -213,36 +211,62 @@ def _(
|
|
|
213
211
|
)
|
|
214
212
|
vs = mo.ui.anywidget(vs_widget)
|
|
215
213
|
vs
|
|
216
|
-
return vs_points, vs_widget
|
|
214
|
+
return vs, vs_points, vs_widget
|
|
217
215
|
|
|
218
216
|
|
|
219
217
|
@app.cell
|
|
220
218
|
def _(LABEL_COLORS, color_dim, mo, vs_points, vs_widget, widget):
|
|
221
|
-
|
|
222
|
-
|
|
219
|
+
# ParallelCoordinates -> VectorSpace (one-way)
|
|
220
|
+
# Uses vs_widget (raw) to avoid triggering vs-dependent cells
|
|
221
|
+
_par_selected = list(widget.widget.selected_indices or [])
|
|
222
|
+
_par_filtered = set(widget.widget.filtered_indices)
|
|
223
|
+
_active_set = set(_par_selected) if _par_selected else _par_filtered
|
|
223
224
|
_updated = []
|
|
224
225
|
for _i, _p in enumerate(vs_points):
|
|
226
|
+
if _i not in _active_set:
|
|
227
|
+
continue
|
|
225
228
|
_new_p = dict(_p)
|
|
226
|
-
if
|
|
227
|
-
|
|
228
|
-
_new_p["color"] = LABEL_COLORS[_p["label"]]
|
|
229
|
-
else:
|
|
230
|
-
_new_p.pop("color", None)
|
|
229
|
+
if color_dim.value == "label":
|
|
230
|
+
_new_p["color"] = LABEL_COLORS[_p["label"]]
|
|
231
231
|
else:
|
|
232
|
-
_new_p
|
|
232
|
+
_new_p.pop("color", None)
|
|
233
233
|
_updated.append(_new_p)
|
|
234
234
|
vs_widget.points = _updated
|
|
235
|
-
mo.md(f"**{len(
|
|
235
|
+
mo.md(f"**{len(_updated)}** / {len(vs_points)} points selected")
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@app.cell
|
|
240
|
+
def _(LABEL_COLORS, ParallelCoordinates, df, mo, vs):
|
|
241
|
+
# VectorSpace -> filtered ParallelCoordinates (reactive via vs.value)
|
|
242
|
+
_selected = list(vs.widget.selected_points or [])
|
|
243
|
+
if _selected:
|
|
244
|
+
_indices = sorted(int(sid.split("_")[1]) for sid in _selected)
|
|
245
|
+
_filtered_df = df[_indices]
|
|
246
|
+
_par = ParallelCoordinates(_filtered_df, color_by="label", color_map=LABEL_COLORS)
|
|
247
|
+
mo.vstack([
|
|
248
|
+
mo.md(f"**{len(_indices)}** points selected in 3D view"),
|
|
249
|
+
mo.ui.anywidget(_par),
|
|
250
|
+
])
|
|
251
|
+
else:
|
|
252
|
+
mo.md("*Lasso or box-select points in the 3D view to filter*")
|
|
236
253
|
return
|
|
237
254
|
|
|
238
255
|
|
|
239
256
|
@app.cell
|
|
240
|
-
def _(LABEL_COLORS, idx, images, label_names, labels, np, plt,
|
|
257
|
+
def _(LABEL_COLORS, idx, images, label_names, labels, np, plt, vs, widget):
|
|
258
|
+
_selected_ids = set(vs.widget.selected_points or [])
|
|
241
259
|
_filtered = widget.widget.filtered_indices
|
|
242
|
-
_sample_idx = np.array(_filtered[:10]) if len(_filtered) >= 10 else np.array(_filtered)
|
|
243
260
|
|
|
244
|
-
|
|
245
|
-
|
|
261
|
+
if _selected_ids:
|
|
262
|
+
_show = [
|
|
263
|
+
_i for _i in range(len(idx))
|
|
264
|
+
if f"p_{_i}" in _selected_ids
|
|
265
|
+
][:10]
|
|
266
|
+
else:
|
|
267
|
+
_show = list(_filtered[:10])
|
|
268
|
+
|
|
269
|
+
_sample_idx = np.array(_show) if len(_show) > 0 else np.array([0])
|
|
246
270
|
|
|
247
271
|
_fig, _axes = plt.subplots(1, len(_sample_idx), figsize=(2 * len(_sample_idx), 2.4))
|
|
248
272
|
if len(_sample_idx) == 1:
|
|
@@ -252,7 +276,6 @@ def _(LABEL_COLORS, idx, images, label_names, labels, np, plt, vs_widget, widget
|
|
|
252
276
|
_ax.imshow(images[idx[_si]].reshape(28, 28), cmap="gray")
|
|
253
277
|
_ax.set_title(_name, fontsize=9)
|
|
254
278
|
_ax.axis("off")
|
|
255
|
-
# Highlight if selected in 3D view
|
|
256
279
|
if f"p_{_si}" in _selected_ids:
|
|
257
280
|
_color = LABEL_COLORS.get(_name, "#0880ea")
|
|
258
281
|
for _spine in _ax.spines.values():
|
|
@@ -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,8 +122,16 @@ 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
137
|
// Auto-detect host theme (marimo uses Tailwind class="dark" on <html>)
|
|
@@ -175,7 +183,7 @@ function render({{ model, el }}) {{
|
|
|
175
183
|
onRunQuery: () => runQuery(),
|
|
176
184
|
onFilterInput: (text) => {{
|
|
177
185
|
if (canvas) {{
|
|
178
|
-
const result = canvas.applyFilter(text);
|
|
186
|
+
const result = canvas.applyFilter(text, false);
|
|
179
187
|
if (toolbarUI) toolbarUI.setFilterCount(result.matched, result.total);
|
|
180
188
|
}}
|
|
181
189
|
}},
|
|
@@ -215,21 +223,22 @@ function render({{ model, el }}) {{
|
|
|
215
223
|
main.appendChild(settingsPanel.element);
|
|
216
224
|
}}
|
|
217
225
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}}
|
|
226
|
+
propertiesPanel = createPropertiesPanel(model, {{
|
|
227
|
+
onClose: () => propertiesPanel?.close(),
|
|
228
|
+
}});
|
|
229
|
+
main.appendChild(propertiesPanel.element);
|
|
230
|
+
if (model.get("show_properties")) propertiesPanel.open();
|
|
224
231
|
|
|
225
|
-
const canvas = createCanvas(model, canvasContainer, {{
|
|
232
|
+
const canvas = createCanvas(model, canvasContainer, {{
|
|
233
|
+
onHover: (point) => propertiesPanel?.setHoveredPoint(point),
|
|
234
|
+
}});
|
|
226
235
|
|
|
227
236
|
async function runQuery() {{
|
|
228
237
|
const query = model.get("query_input") || "";
|
|
229
238
|
|
|
230
|
-
//
|
|
239
|
+
// Apply hard client-side filter (completely hide non-matching)
|
|
231
240
|
if (canvas) {{
|
|
232
|
-
const result = canvas.applyFilter(query);
|
|
241
|
+
const result = canvas.applyFilter(query, true);
|
|
233
242
|
if (toolbarUI) toolbarUI.setFilterCount(result.matched, result.total);
|
|
234
243
|
}}
|
|
235
244
|
|
|
@@ -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."""
|
|
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
|
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/chroma/__init__.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/chroma/client.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/chroma/converter.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/grafeo/__init__.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/grafeo/browser.js
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/grafeo/client.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/grafeo/converter.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/grafeo/embed.js
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/lancedb/__init__.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/lancedb/client.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/lancedb/converter.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/pinecone/__init__.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/pinecone/client.js
RENAMED
|
File without changes
|
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/qdrant/__init__.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/qdrant/client.js
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/qdrant/converter.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/weaviate/__init__.py
RENAMED
|
File without changes
|
{anywidget_vector-0.3.0 → anywidget_vector-0.3.1}/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
|
|
File without changes
|