anywidget-vector 0.2.8__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.
Files changed (51) hide show
  1. anywidget_vector-0.3.1/CHANGELOG.md +129 -0
  2. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/PKG-INFO +3 -3
  3. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/examples/fashion-mnist-parallel-coords-6d.py +88 -41
  4. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/pyproject.toml +3 -3
  5. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/static/icons.js +1 -0
  6. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/__init__.py +56 -16
  7. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/canvas.js +287 -64
  8. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/properties.js +8 -11
  9. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/settings.js +2 -1
  10. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/sidebar.js +132 -0
  11. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/styles.css +18 -13
  12. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/widget.py +8 -4
  13. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/tests/test_widget.py +2 -2
  14. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/uv.lock +3 -3
  15. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/.github/workflows/ci.yml +0 -0
  16. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/.github/workflows/pypi.yml +0 -0
  17. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/.gitignore +0 -0
  18. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/.python-version +0 -0
  19. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/LICENSE +0 -0
  20. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/README.md +0 -0
  21. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/assets/demo.png +0 -0
  22. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/examples/demo.py +0 -0
  23. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/examples/test_add_numpy.py +0 -0
  24. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/examples/test_projection.py +0 -0
  25. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/__init__.py +0 -0
  26. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/__init__.py +0 -0
  27. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/chroma/__init__.py +0 -0
  28. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/chroma/client.py +0 -0
  29. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/chroma/converter.py +0 -0
  30. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/grafeo/__init__.py +0 -0
  31. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/grafeo/browser.js +0 -0
  32. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/grafeo/client.py +0 -0
  33. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/grafeo/converter.py +0 -0
  34. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/grafeo/embed.js +0 -0
  35. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/lancedb/__init__.py +0 -0
  36. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/lancedb/client.py +0 -0
  37. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/lancedb/converter.py +0 -0
  38. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/pinecone/__init__.py +0 -0
  39. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/pinecone/client.js +0 -0
  40. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/pinecone/converter.py +0 -0
  41. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/qdrant/__init__.py +0 -0
  42. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/qdrant/client.js +0 -0
  43. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/qdrant/converter.py +0 -0
  44. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/weaviate/__init__.py +0 -0
  45. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/weaviate/client.js +0 -0
  46. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/weaviate/converter.py +0 -0
  47. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/demo.py +0 -0
  48. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/py.typed +0 -0
  49. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/constants.js +0 -0
  50. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/toolbar.js +0 -0
  51. {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/tests/__init__.py +0 -0
@@ -0,0 +1,129 @@
1
+ # Changelog
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
+
23
+ ## 0.3.0 (2026-03-16)
24
+
25
+ ### New Features
26
+
27
+ - **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
28
+ - **Dimension mapping display**: Sidebar now shows active visual mapping fields: color field, size range, shape field and unique shape count
29
+
30
+ ### Improvements
31
+
32
+ - Improved sidebar styling and layout
33
+
34
+ ## 0.2.8 (2026-03-15)
35
+
36
+ ### New Features
37
+
38
+ - **6D parallel coordinates example**: Fashion-MNIST demo with PCA across 6+ dimensions, interactive axis mapping, and shape mapping to categorical labels
39
+
40
+ ## 0.2.7 (2026-03-15)
41
+
42
+ ### New Features
43
+
44
+ - **Dark/light mode toggle**: `dark_mode` trait with real-time CSS color scheme switching and theme-aware styling across all panels
45
+
46
+ ## 0.2.6 (2026-03-15)
47
+
48
+ ### New Features
49
+
50
+ - **NumPy array support**: `add_numpy()` method for adding points from arrays with automatic PCA projection for high-dimensional data (D > 3)
51
+ - **Dimensionality reduction**: `project()` method with PCA, t-SNE and UMAP support; `set_vectors()` for storing high-dimensional vectors separately
52
+ - **Example scripts**: Projection demo and numpy integration examples
53
+
54
+ ### Improvements
55
+
56
+ - High-dimensional vector storage without syncing to JavaScript
57
+ - Coordinate normalization after projection
58
+ - Added demo screenshot to README
59
+
60
+ ## 0.2.5 (2026-03-04)
61
+
62
+ ### Bug Fixes
63
+
64
+ - Fixed lasso selection stability issues
65
+ - Removed problematic custom traitlets implementation
66
+
67
+ ### Improvements
68
+
69
+ - Added keyboard shortcuts: F to fit view, Escape to clear selection
70
+ - Better visual feedback for selection modes
71
+ - Cleaner toolbar layout with integrated mode controls
72
+
73
+ ## 0.2.4 (2026-03-02)
74
+
75
+ ### New Features
76
+
77
+ - **Zoom and pan controls**: Zoom in, zoom out and fit-to-view buttons in the toolbar
78
+ - **Box selection mode**: New selection mode alongside click and multi-select, with visual feedback during selection
79
+ - **HTML export**: `to_html()` and `save_html()` methods for self-contained HTML output with embedded data and Three.js
80
+
81
+ ### Improvements
82
+
83
+ - Improved camera fitting algorithm for better initial view framing
84
+ - Updated README with comprehensive usage examples
85
+ - Updated dependencies
86
+
87
+ ## 0.2.3 (2026-02-12)
88
+
89
+ ### New Features
90
+
91
+ - **Sidebar explorer**: Three collapsible sections: collections browser, dimensions explorer (point count, dimensionality, axis ranges) and clusters explorer (grouped by color field with sizes)
92
+ - **Browser-side Grafeo backend**: JavaScript client for Grafeo server and WASM-embedded queries
93
+ - **Demo module**: Reference implementation showing various widget features
94
+
95
+ ### Improvements
96
+
97
+ - Consistency pass across all backend modules
98
+ - Ruff configuration tuned for marimo compatibility
99
+
100
+ ## 0.2.2 (2026-02-07)
101
+
102
+ ### New Features
103
+
104
+ - **Streaming factory methods**: `from_numpy()`, `from_umap()` and `from_arrays()` constructors for various data formats
105
+
106
+ ### Improvements
107
+
108
+ - Added Apache 2.0 license
109
+ - Expanded test suite with 194 additional test cases
110
+ - ESM bundle aggregation for widget JavaScript
111
+
112
+ ## 0.2.1 (2026-02-03)
113
+
114
+ ### New Features
115
+
116
+ - **Multi-backend architecture**: Six backends: Qdrant, Pinecone, Weaviate (browser-side REST), Chroma, LanceDB, Grafeo (Python-side)
117
+ - **Modular UI**: Separated rendering into canvas, settings, properties and toolbar components
118
+ - **Backend factory methods**: `from_qdrant()`, `from_pinecone()`, `from_weaviate()`, `from_chroma()`, `from_lancedb()`, `from_grafeo()`
119
+ - **Event system**: `on_click()`, `on_hover()` and `on_selection()` decorators with callback signatures
120
+ - **CI/CD**: GitHub Actions pipelines for testing and PyPI publishing
121
+
122
+ ### Improvements
123
+
124
+ - Refactored monolithic widget into modular component architecture
125
+ - Added README with usage examples
126
+
127
+ ## 0.1.0 (2026-02-03)
128
+
129
+ - Initial release: Three.js-based interactive 3D vector visualization with instanced rendering, six shape types, continuous and categorical color mapping, orbit controls, hover tooltips, axes/grid display and raycast-based selection
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anywidget-vector
3
- Version: 0.2.8
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
@@ -33,10 +33,10 @@ Requires-Dist: chromadb>=0.4; extra == 'chroma'
33
33
  Provides-Extra: demo
34
34
  Provides-Extra: dev
35
35
  Requires-Dist: marimo>=0.20; extra == 'dev'
36
- Requires-Dist: prek>=0.3.2; extra == 'dev'
36
+ Requires-Dist: prek>=0.3; extra == 'dev'
37
37
  Requires-Dist: pytest>=9.0.2; extra == 'dev'
38
38
  Requires-Dist: ruff>=0.15; extra == 'dev'
39
- Requires-Dist: ty>=0.0.20; extra == 'dev'
39
+ Requires-Dist: ty>=0.0; extra == 'dev'
40
40
  Provides-Extra: embed
41
41
  Provides-Extra: grafeo
42
42
  Requires-Dist: grafeo>=0.5; extra == 'grafeo'
@@ -1,3 +1,8 @@
1
+ # Based on the Fashion MNIST Parallel Coordinates notebook by the marimo team:
2
+ # https://github.com/marimo-team/gallery-examples/blob/main/notebooks/wigglystuff/fashion-mnist-parallel-coords.py
3
+ #
4
+ # Extended with anywidget-vector 3D scatter view and 6D dimension mapping.
5
+ #
1
6
  # /// script
2
7
  # requires-python = ">=3.12"
3
8
  # dependencies = [
@@ -8,7 +13,7 @@
8
13
  # "wigglystuff==0.2.37",
9
14
  # "matplotlib==3.10.8",
10
15
  # "pandas==3.0.1",
11
- # "anywidget-vector==0.2.7",
16
+ # "anywidget-vector==0.3.1",
12
17
  # ]
13
18
  # ///
14
19
 
@@ -29,16 +34,15 @@ def _():
29
34
  from wigglystuff import ParallelCoordinates
30
35
 
31
36
  from anywidget_vector import VectorSpace
32
-
33
37
  return PCA, ParallelCoordinates, VectorSpace, fetch_openml, mo, np, pl, plt
34
38
 
35
39
 
36
40
  @app.cell(hide_code=True)
37
41
  def _(mo):
38
42
  mo.md(r"""
39
- # Fashion MNIST: Parallel Coordinates + 3D Vector View
43
+ # Fashion MNIST: Parallel Coordinates + 6D Vector View
40
44
 
41
- 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.
42
46
  Use the dropdowns to map PCA dimensions to visual channels.
43
47
  """)
44
48
  return
@@ -62,18 +66,9 @@ def _(fetch_openml, np):
62
66
  8: "Bag",
63
67
  9: "Ankle boot",
64
68
  }
65
- LABEL_COLORS = {
66
- "T-shirt/top": "#6366f1",
67
- "Trouser": "#f59e0b",
68
- "Pullover": "#10b981",
69
- "Dress": "#ef4444",
70
- "Coat": "#8b5cf6",
71
- "Sandal": "#06b6d4",
72
- "Shirt": "#f97316",
73
- "Sneaker": "#84cc16",
74
- "Bag": "#ec4899",
75
- "Ankle boot": "#14b8a6",
76
- }
69
+ # d3 schemeCategory10, assigned alphabetically to match wigglystuff
70
+ _d3 = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"]
71
+ LABEL_COLORS = {_name: _d3[_i] for _i, _name in enumerate(sorted(label_names.values()))}
77
72
  SHAPE_NAMES = ["sphere", "cube", "cone", "octahedron", "cylinder", "tetrahedron"]
78
73
  return LABEL_COLORS, SHAPE_NAMES, images, label_names, labels
79
74
 
@@ -81,7 +76,7 @@ def _(fetch_openml, np):
81
76
  @app.cell
82
77
  def _(mo):
83
78
  n_samples_slider = mo.ui.slider(start=500, stop=5000, step=500, value=2000, label="Samples")
84
- n_components_slider = mo.ui.slider(start=3, stop=15, step=1, value=8, label="PCA dims")
79
+ n_components_slider = mo.ui.slider(start=3, stop=15, step=1, value=5, label="PCA dims")
85
80
  mo.hstack([n_samples_slider, n_components_slider])
86
81
  return n_components_slider, n_samples_slider
87
82
 
@@ -110,8 +105,8 @@ def _(
110
105
 
111
106
 
112
107
  @app.cell
113
- def _(ParallelCoordinates, df, mo):
114
- widget = mo.ui.anywidget(ParallelCoordinates(df, color_by="label"))
108
+ def _(LABEL_COLORS, ParallelCoordinates, df, mo):
109
+ widget = mo.ui.anywidget(ParallelCoordinates(df, color_by="label", color_map=LABEL_COLORS))
115
110
  widget
116
111
  return (widget,)
117
112
 
@@ -122,9 +117,9 @@ def _(mo, n_components_slider):
122
117
  x_dim = mo.ui.dropdown(options=pcs, value="PC1", label="X")
123
118
  y_dim = mo.ui.dropdown(options=pcs, value="PC2", label="Y")
124
119
  z_dim = mo.ui.dropdown(options=pcs, value="PC3", label="Z")
125
- size_dim = mo.ui.dropdown(options=["none", *pcs], value="none", label="Size")
120
+ size_dim = mo.ui.dropdown(options=["none", *pcs], value="PC4", label="Size")
126
121
  color_dim = mo.ui.dropdown(options=["label", *pcs], value="label", label="Color")
127
- shape_dim = mo.ui.dropdown(options=["label", "none"], value="none", label="Shape")
122
+ shape_dim = mo.ui.dropdown(options=["label", "none", *pcs], value="PC5", label="Shape")
128
123
  mo.hstack([x_dim, y_dim, z_dim, size_dim, color_dim, shape_dim], gap=0.5)
129
124
  return color_dim, shape_dim, size_dim, x_dim, y_dim, z_dim
130
125
 
@@ -140,6 +135,7 @@ def _(
140
135
  label_names,
141
136
  labels,
142
137
  mo,
138
+ np,
143
139
  shape_dim,
144
140
  size_dim,
145
141
  x_dim,
@@ -152,8 +148,23 @@ def _(
152
148
  _xi, _yi, _zi = _pc(x_dim.value), _pc(y_dim.value), _pc(z_dim.value)
153
149
  _color_pc = None if color_dim.value == "label" else _pc(color_dim.value)
154
150
  _size_pc = None if size_dim.value == "none" else _pc(size_dim.value)
151
+ _shape_mode = shape_dim.value # "label", "none", or a PC name
152
+
153
+ # Shape mapping: label-based or PC-based (binned into 6 shape buckets)
155
154
  _unique_labels = sorted(set(label_names.values()))
156
155
  _shape_map = {_n: SHAPE_NAMES[_j % len(SHAPE_NAMES)] for _j, _n in enumerate(_unique_labels)}
156
+ _shape_bins = None
157
+ if _shape_mode not in ("label", "none"):
158
+ _shape_col = components[:, _pc(_shape_mode)]
159
+ _quantiles = np.percentile(_shape_col, np.linspace(0, 100, len(SHAPE_NAMES) + 1))
160
+ _shape_bins = list(zip(_quantiles[:-1], _quantiles[1:], SHAPE_NAMES, strict=False))
161
+ _shape_map = {_s: _s for _s in SHAPE_NAMES}
162
+
163
+ def _get_shape_bin(_val):
164
+ for _lo, _hi, _s in _shape_bins:
165
+ if _val <= _hi:
166
+ return _s
167
+ return SHAPE_NAMES[-1]
157
168
 
158
169
  vs_points = []
159
170
  for _i in range(len(components)):
@@ -171,8 +182,10 @@ def _(
171
182
  _p["color_val"] = float(components[_i, _color_pc])
172
183
  if _size_pc is not None:
173
184
  _p["size_val"] = float(components[_i, _size_pc])
174
- if shape_dim.value == "label":
185
+ if _shape_mode == "label":
175
186
  _p["shape_cat"] = _name
187
+ elif _shape_bins is not None:
188
+ _p["shape_cat"] = _get_shape_bin(components[_i, _pc(_shape_mode)])
176
189
  vs_points.append(_p)
177
190
 
178
191
  _vs_kwargs = {
@@ -183,17 +196,14 @@ def _(
183
196
  _vs_kwargs["color_scale"] = "viridis"
184
197
  if _size_pc is not None:
185
198
  _vs_kwargs["size_field"] = "size_val"
186
- _vs_kwargs["size_range"] = [0.01, 0.06]
187
- if shape_dim.value == "label":
199
+ _vs_kwargs["size_range"] = [0.01, 0.03]
200
+ if _shape_mode != "none":
188
201
  _vs_kwargs["shape_field"] = "shape_cat"
189
202
  _vs_kwargs["shape_map"] = _shape_map
190
203
 
191
204
  vs_widget = VectorSpace(
192
205
  points=vs_points,
193
- width=1200,
194
- height=500,
195
- dark_mode=False,
196
- background="#fafafa",
206
+ height=900,
197
207
  show_toolbar=True,
198
208
  show_settings=True,
199
209
  show_properties=False,
@@ -206,36 +216,73 @@ def _(
206
216
 
207
217
  @app.cell
208
218
  def _(LABEL_COLORS, color_dim, mo, vs_points, vs_widget, widget):
209
- _filtered = set(widget.widget.filtered_indices)
210
- _dim_color = "#d1d5db"
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
211
224
  _updated = []
212
225
  for _i, _p in enumerate(vs_points):
226
+ if _i not in _active_set:
227
+ continue
213
228
  _new_p = dict(_p)
214
- if _i in _filtered:
215
- if color_dim.value == "label":
216
- _new_p["color"] = LABEL_COLORS[_p["label"]]
217
- else:
218
- _new_p.pop("color", None)
229
+ if color_dim.value == "label":
230
+ _new_p["color"] = LABEL_COLORS[_p["label"]]
219
231
  else:
220
- _new_p["color"] = _dim_color
232
+ _new_p.pop("color", None)
221
233
  _updated.append(_new_p)
222
234
  vs_widget.points = _updated
223
- mo.md(f"**{len(_filtered)}** / {len(vs_points)} points selected")
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*")
224
253
  return
225
254
 
226
255
 
227
256
  @app.cell
228
- def _(idx, images, label_names, labels, np, plt, widget):
257
+ def _(LABEL_COLORS, idx, images, label_names, labels, np, plt, vs, widget):
258
+ _selected_ids = set(vs.widget.selected_points or [])
229
259
  _filtered = widget.widget.filtered_indices
230
- _sample_idx = np.array(_filtered[:10]) if len(_filtered) >= 10 else np.array(_filtered)
231
260
 
232
- _fig, _axes = plt.subplots(1, len(_sample_idx), figsize=(2 * len(_sample_idx), 2))
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])
270
+
271
+ _fig, _axes = plt.subplots(1, len(_sample_idx), figsize=(2 * len(_sample_idx), 2.4))
233
272
  if len(_sample_idx) == 1:
234
273
  _axes = [_axes]
235
274
  for _ax, _si in zip(_axes, _sample_idx, strict=False):
275
+ _name = label_names[labels[idx[_si]]]
236
276
  _ax.imshow(images[idx[_si]].reshape(28, 28), cmap="gray")
237
- _ax.set_title(label_names[labels[idx[_si]]], fontsize=9)
277
+ _ax.set_title(_name, fontsize=9)
238
278
  _ax.axis("off")
279
+ if f"p_{_si}" in _selected_ids:
280
+ _color = LABEL_COLORS.get(_name, "#0880ea")
281
+ for _spine in _ax.spines.values():
282
+ _spine.set_visible(True)
283
+ _spine.set_color(_color)
284
+ _spine.set_linewidth(3)
285
+ _ax.set_title(_name, fontsize=9, fontweight="bold", color=_color)
239
286
  plt.tight_layout()
240
287
  _fig
241
288
  return
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "anywidget-vector"
3
- version = "0.2.8"
3
+ version = "0.3.1"
4
4
  description = "Interactive 3D vector visualization with query UI for vector databases"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -26,10 +26,10 @@ dependencies = [
26
26
 
27
27
  [project.optional-dependencies]
28
28
  dev = [
29
- "prek>=0.3.2",
29
+ "prek>=0.3",
30
30
  "pytest>=9.0.2",
31
31
  "ruff>=0.15",
32
- "ty>=0.0.20",
32
+ "ty>=0.0",
33
33
  "marimo>=0.20",
34
34
  ]
35
35
  pandas = ["pandas>=2.0"]
@@ -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,16 +122,55 @@ async function executeBackendQuery(model) {{
122
122
  function render({{ model, el }}) {{
123
123
  const wrapper = document.createElement("div");
124
124
  wrapper.className = "avs-wrapper";
125
- if (model.get("dark_mode")) wrapper.classList.add("avs-dark");
126
- wrapper.style.width = model.get("width") + "px";
127
- wrapper.style.height = model.get("height") + "px";
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
+
128
135
  el.appendChild(wrapper);
129
136
 
130
- model.on("change:dark_mode", () => {{
131
- const dark = model.get("dark_mode");
137
+ // Auto-detect host theme (marimo uses Tailwind class="dark" on <html>)
138
+ function detectHostDark() {{
139
+ const html = document.documentElement;
140
+ if (html.classList.contains("dark")) return true;
141
+ if (html.dataset.theme === "dark") return true;
142
+ if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) return true;
143
+ return false;
144
+ }}
145
+
146
+ function applyTheme(dark) {{
132
147
  wrapper.classList.toggle("avs-dark", dark);
133
- model.set("background", dark ? "#1a1a2e" : "#fafafa");
148
+ model.set("background", dark ? "#181c1a" : "#ffffff");
134
149
  model.save_changes();
150
+ }}
151
+
152
+ // Detect if we're inside a themed host (marimo, jupyter, etc.)
153
+ const hasHostTheme = !!el.getRootNode()?.host?.tagName?.startsWith("MARIMO-");
154
+ if (hasHostTheme) {{
155
+ wrapper.classList.add("avs-auto-theme");
156
+ model.set("dark_mode", detectHostDark());
157
+ applyTheme(detectHostDark());
158
+ // Watch host for live theme changes
159
+ const themeObserver = new MutationObserver(() => {{
160
+ const dark = detectHostDark();
161
+ if (model.get("dark_mode") !== dark) {{
162
+ model.set("dark_mode", dark);
163
+ applyTheme(dark);
164
+ }}
165
+ }});
166
+ themeObserver.observe(document.documentElement, {{ attributes: true, attributeFilter: ["class", "data-theme"] }});
167
+ }} else {{
168
+ applyTheme(model.get("dark_mode"));
169
+ }}
170
+
171
+ // Manual toggle from settings panel still works
172
+ model.on("change:dark_mode", () => {{
173
+ applyTheme(model.get("dark_mode"));
135
174
  }});
136
175
 
137
176
  let sidebar = null;
@@ -144,7 +183,7 @@ function render({{ model, el }}) {{
144
183
  onRunQuery: () => runQuery(),
145
184
  onFilterInput: (text) => {{
146
185
  if (canvas) {{
147
- const result = canvas.applyFilter(text);
186
+ const result = canvas.applyFilter(text, false);
148
187
  if (toolbarUI) toolbarUI.setFilterCount(result.matched, result.total);
149
188
  }}
150
189
  }},
@@ -184,21 +223,22 @@ function render({{ model, el }}) {{
184
223
  main.appendChild(settingsPanel.element);
185
224
  }}
186
225
 
187
- if (model.get("show_properties")) {{
188
- propertiesPanel = createPropertiesPanel(model, {{
189
- onClose: () => propertiesPanel?.close(),
190
- }});
191
- main.appendChild(propertiesPanel.element);
192
- }}
226
+ propertiesPanel = createPropertiesPanel(model, {{
227
+ onClose: () => propertiesPanel?.close(),
228
+ }});
229
+ main.appendChild(propertiesPanel.element);
230
+ if (model.get("show_properties")) propertiesPanel.open();
193
231
 
194
- const canvas = createCanvas(model, canvasContainer, {{}});
232
+ const canvas = createCanvas(model, canvasContainer, {{
233
+ onHover: (point) => propertiesPanel?.setHoveredPoint(point),
234
+ }});
195
235
 
196
236
  async function runQuery() {{
197
237
  const query = model.get("query_input") || "";
198
238
 
199
- // Always apply client-side filter
239
+ // Apply hard client-side filter (completely hide non-matching)
200
240
  if (canvas) {{
201
- const result = canvas.applyFilter(query);
241
+ const result = canvas.applyFilter(query, true);
202
242
  if (toolbarUI) toolbarUI.setFilterCount(result.matched, result.total);
203
243
  }}
204
244