anywidget-vector 0.2.8__tar.gz → 0.3.0__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.0/CHANGELOG.md +109 -0
  2. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/PKG-INFO +3 -3
  3. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/examples/fashion-mnist-parallel-coords-6d.py +52 -28
  4. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/pyproject.toml +3 -3
  5. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/ui/__init__.py +35 -4
  6. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/ui/canvas.js +12 -4
  7. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/ui/settings.js +2 -1
  8. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/ui/sidebar.js +122 -0
  9. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/ui/styles.css +18 -13
  10. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/widget.py +1 -1
  11. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/uv.lock +3 -3
  12. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/.github/workflows/ci.yml +0 -0
  13. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/.github/workflows/pypi.yml +0 -0
  14. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/.gitignore +0 -0
  15. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/.python-version +0 -0
  16. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/LICENSE +0 -0
  17. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/README.md +0 -0
  18. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/assets/demo.png +0 -0
  19. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/examples/demo.py +0 -0
  20. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/examples/test_add_numpy.py +0 -0
  21. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/examples/test_projection.py +0 -0
  22. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/__init__.py +0 -0
  23. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/__init__.py +0 -0
  24. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/chroma/__init__.py +0 -0
  25. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/chroma/client.py +0 -0
  26. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/chroma/converter.py +0 -0
  27. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/grafeo/__init__.py +0 -0
  28. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/grafeo/browser.js +0 -0
  29. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/grafeo/client.py +0 -0
  30. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/grafeo/converter.py +0 -0
  31. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/grafeo/embed.js +0 -0
  32. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/lancedb/__init__.py +0 -0
  33. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/lancedb/client.py +0 -0
  34. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/lancedb/converter.py +0 -0
  35. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/pinecone/__init__.py +0 -0
  36. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/pinecone/client.js +0 -0
  37. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/pinecone/converter.py +0 -0
  38. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/qdrant/__init__.py +0 -0
  39. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/qdrant/client.js +0 -0
  40. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/qdrant/converter.py +0 -0
  41. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/weaviate/__init__.py +0 -0
  42. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/weaviate/client.js +0 -0
  43. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/backends/weaviate/converter.py +0 -0
  44. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/demo.py +0 -0
  45. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/py.typed +0 -0
  46. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/static/icons.js +0 -0
  47. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/ui/constants.js +0 -0
  48. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/ui/properties.js +0 -0
  49. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/src/anywidget_vector/ui/toolbar.js +0 -0
  50. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/tests/__init__.py +0 -0
  51. {anywidget_vector-0.2.8 → anywidget_vector-0.3.0}/tests/test_widget.py +0 -0
@@ -0,0 +1,109 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0 (2026-03-16)
4
+
5
+ ### New Features
6
+
7
+ - **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
+ - **Dimension mapping display**: Sidebar now shows active visual mapping fields: color field, size range, shape field and unique shape count
9
+
10
+ ### Improvements
11
+
12
+ - Improved sidebar styling and layout
13
+
14
+ ## 0.2.8 (2026-03-15)
15
+
16
+ ### New Features
17
+
18
+ - **6D parallel coordinates example**: Fashion-MNIST demo with PCA across 6+ dimensions, interactive axis mapping, and shape mapping to categorical labels
19
+
20
+ ## 0.2.7 (2026-03-15)
21
+
22
+ ### New Features
23
+
24
+ - **Dark/light mode toggle**: `dark_mode` trait with real-time CSS color scheme switching and theme-aware styling across all panels
25
+
26
+ ## 0.2.6 (2026-03-15)
27
+
28
+ ### New Features
29
+
30
+ - **NumPy array support**: `add_numpy()` method for adding points from arrays with automatic PCA projection for high-dimensional data (D > 3)
31
+ - **Dimensionality reduction**: `project()` method with PCA, t-SNE and UMAP support; `set_vectors()` for storing high-dimensional vectors separately
32
+ - **Example scripts**: Projection demo and numpy integration examples
33
+
34
+ ### Improvements
35
+
36
+ - High-dimensional vector storage without syncing to JavaScript
37
+ - Coordinate normalization after projection
38
+ - Added demo screenshot to README
39
+
40
+ ## 0.2.5 (2026-03-04)
41
+
42
+ ### Bug Fixes
43
+
44
+ - Fixed lasso selection stability issues
45
+ - Removed problematic custom traitlets implementation
46
+
47
+ ### Improvements
48
+
49
+ - Added keyboard shortcuts: F to fit view, Escape to clear selection
50
+ - Better visual feedback for selection modes
51
+ - Cleaner toolbar layout with integrated mode controls
52
+
53
+ ## 0.2.4 (2026-03-02)
54
+
55
+ ### New Features
56
+
57
+ - **Zoom and pan controls**: Zoom in, zoom out and fit-to-view buttons in the toolbar
58
+ - **Box selection mode**: New selection mode alongside click and multi-select, with visual feedback during selection
59
+ - **HTML export**: `to_html()` and `save_html()` methods for self-contained HTML output with embedded data and Three.js
60
+
61
+ ### Improvements
62
+
63
+ - Improved camera fitting algorithm for better initial view framing
64
+ - Updated README with comprehensive usage examples
65
+ - Updated dependencies
66
+
67
+ ## 0.2.3 (2026-02-12)
68
+
69
+ ### New Features
70
+
71
+ - **Sidebar explorer**: Three collapsible sections: collections browser, dimensions explorer (point count, dimensionality, axis ranges) and clusters explorer (grouped by color field with sizes)
72
+ - **Browser-side Grafeo backend**: JavaScript client for Grafeo server and WASM-embedded queries
73
+ - **Demo module**: Reference implementation showing various widget features
74
+
75
+ ### Improvements
76
+
77
+ - Consistency pass across all backend modules
78
+ - Ruff configuration tuned for marimo compatibility
79
+
80
+ ## 0.2.2 (2026-02-07)
81
+
82
+ ### New Features
83
+
84
+ - **Streaming factory methods**: `from_numpy()`, `from_umap()` and `from_arrays()` constructors for various data formats
85
+
86
+ ### Improvements
87
+
88
+ - Added Apache 2.0 license
89
+ - Expanded test suite with 194 additional test cases
90
+ - ESM bundle aggregation for widget JavaScript
91
+
92
+ ## 0.2.1 (2026-02-03)
93
+
94
+ ### New Features
95
+
96
+ - **Multi-backend architecture**: Six backends: Qdrant, Pinecone, Weaviate (browser-side REST), Chroma, LanceDB, Grafeo (Python-side)
97
+ - **Modular UI**: Separated rendering into canvas, settings, properties and toolbar components
98
+ - **Backend factory methods**: `from_qdrant()`, `from_pinecone()`, `from_weaviate()`, `from_chroma()`, `from_lancedb()`, `from_grafeo()`
99
+ - **Event system**: `on_click()`, `on_hover()` and `on_selection()` decorators with callback signatures
100
+ - **CI/CD**: GitHub Actions pipelines for testing and PyPI publishing
101
+
102
+ ### Improvements
103
+
104
+ - Refactored monolithic widget into modular component architecture
105
+ - Added README with usage examples
106
+
107
+ ## 0.1.0 (2026-02-03)
108
+
109
+ - 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.0
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 = [
@@ -14,7 +19,7 @@
14
19
 
15
20
  import marimo
16
21
 
17
- __generated_with = "0.20.2"
22
+ __generated_with = "0.19.7"
18
23
  app = marimo.App(width="full")
19
24
 
20
25
 
@@ -36,7 +41,7 @@ def _():
36
41
  @app.cell(hide_code=True)
37
42
  def _(mo):
38
43
  mo.md(r"""
39
- # Fashion MNIST: Parallel Coordinates + 3D Vector View
44
+ # Fashion MNIST: Parallel Coordinates + 6D Vector View
40
45
 
41
46
  Brush the parallel coordinates axes to filter. The 3D scatter updates in real time.
42
47
  Use the dropdowns to map PCA dimensions to visual channels.
@@ -62,18 +67,9 @@ def _(fetch_openml, np):
62
67
  8: "Bag",
63
68
  9: "Ankle boot",
64
69
  }
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
- }
70
+ # d3 schemeCategory10, assigned alphabetically to match wigglystuff
71
+ _d3 = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"]
72
+ LABEL_COLORS = {_name: _d3[_i] for _i, _name in enumerate(sorted(label_names.values()))}
77
73
  SHAPE_NAMES = ["sphere", "cube", "cone", "octahedron", "cylinder", "tetrahedron"]
78
74
  return LABEL_COLORS, SHAPE_NAMES, images, label_names, labels
79
75
 
@@ -110,8 +106,8 @@ def _(
110
106
 
111
107
 
112
108
  @app.cell
113
- def _(ParallelCoordinates, df, mo):
114
- widget = mo.ui.anywidget(ParallelCoordinates(df, color_by="label"))
109
+ def _(LABEL_COLORS, ParallelCoordinates, df, mo):
110
+ widget = mo.ui.anywidget(ParallelCoordinates(df, color_by="label", color_map=LABEL_COLORS))
115
111
  widget
116
112
  return (widget,)
117
113
 
@@ -122,9 +118,9 @@ def _(mo, n_components_slider):
122
118
  x_dim = mo.ui.dropdown(options=pcs, value="PC1", label="X")
123
119
  y_dim = mo.ui.dropdown(options=pcs, value="PC2", label="Y")
124
120
  z_dim = mo.ui.dropdown(options=pcs, value="PC3", label="Z")
125
- size_dim = mo.ui.dropdown(options=["none", *pcs], value="none", label="Size")
121
+ size_dim = mo.ui.dropdown(options=["none", *pcs], value="PC4", label="Size")
126
122
  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")
123
+ shape_dim = mo.ui.dropdown(options=["label", "none", *pcs], value="PC5", label="Shape")
128
124
  mo.hstack([x_dim, y_dim, z_dim, size_dim, color_dim, shape_dim], gap=0.5)
129
125
  return color_dim, shape_dim, size_dim, x_dim, y_dim, z_dim
130
126
 
@@ -140,6 +136,7 @@ def _(
140
136
  label_names,
141
137
  labels,
142
138
  mo,
139
+ np,
143
140
  shape_dim,
144
141
  size_dim,
145
142
  x_dim,
@@ -152,8 +149,23 @@ def _(
152
149
  _xi, _yi, _zi = _pc(x_dim.value), _pc(y_dim.value), _pc(z_dim.value)
153
150
  _color_pc = None if color_dim.value == "label" else _pc(color_dim.value)
154
151
  _size_pc = None if size_dim.value == "none" else _pc(size_dim.value)
152
+ _shape_mode = shape_dim.value # "label", "none", or a PC name
153
+
154
+ # Shape mapping: label-based or PC-based (binned into 6 shape buckets)
155
155
  _unique_labels = sorted(set(label_names.values()))
156
156
  _shape_map = {_n: SHAPE_NAMES[_j % len(SHAPE_NAMES)] for _j, _n in enumerate(_unique_labels)}
157
+ _shape_bins = None
158
+ if _shape_mode not in ("label", "none"):
159
+ _shape_col = components[:, _pc(_shape_mode)]
160
+ _quantiles = np.percentile(_shape_col, np.linspace(0, 100, len(SHAPE_NAMES) + 1))
161
+ _shape_bins = list(zip(_quantiles[:-1], _quantiles[1:], SHAPE_NAMES, strict=False))
162
+ _shape_map = {_s: _s for _s in SHAPE_NAMES}
163
+
164
+ def _get_shape_bin(_val):
165
+ for _lo, _hi, _s in _shape_bins:
166
+ if _val <= _hi:
167
+ return _s
168
+ return SHAPE_NAMES[-1]
157
169
 
158
170
  vs_points = []
159
171
  for _i in range(len(components)):
@@ -171,8 +183,10 @@ def _(
171
183
  _p["color_val"] = float(components[_i, _color_pc])
172
184
  if _size_pc is not None:
173
185
  _p["size_val"] = float(components[_i, _size_pc])
174
- if shape_dim.value == "label":
186
+ if _shape_mode == "label":
175
187
  _p["shape_cat"] = _name
188
+ elif _shape_bins is not None:
189
+ _p["shape_cat"] = _get_shape_bin(components[_i, _pc(_shape_mode)])
176
190
  vs_points.append(_p)
177
191
 
178
192
  _vs_kwargs = {
@@ -183,17 +197,15 @@ def _(
183
197
  _vs_kwargs["color_scale"] = "viridis"
184
198
  if _size_pc is not None:
185
199
  _vs_kwargs["size_field"] = "size_val"
186
- _vs_kwargs["size_range"] = [0.01, 0.06]
187
- if shape_dim.value == "label":
200
+ _vs_kwargs["size_range"] = [0.01, 0.03]
201
+ if _shape_mode != "none":
188
202
  _vs_kwargs["shape_field"] = "shape_cat"
189
203
  _vs_kwargs["shape_map"] = _shape_map
190
204
 
191
205
  vs_widget = VectorSpace(
192
206
  points=vs_points,
193
- width=1200,
207
+ width=1600,
194
208
  height=500,
195
- dark_mode=False,
196
- background="#fafafa",
197
209
  show_toolbar=True,
198
210
  show_settings=True,
199
211
  show_properties=False,
@@ -201,7 +213,7 @@ def _(
201
213
  )
202
214
  vs = mo.ui.anywidget(vs_widget)
203
215
  vs
204
- return vs, vs_points, vs_widget
216
+ return vs_points, vs_widget
205
217
 
206
218
 
207
219
  @app.cell
@@ -225,17 +237,29 @@ def _(LABEL_COLORS, color_dim, mo, vs_points, vs_widget, widget):
225
237
 
226
238
 
227
239
  @app.cell
228
- def _(idx, images, label_names, labels, np, plt, widget):
240
+ def _(LABEL_COLORS, idx, images, label_names, labels, np, plt, vs_widget, widget):
229
241
  _filtered = widget.widget.filtered_indices
230
242
  _sample_idx = np.array(_filtered[:10]) if len(_filtered) >= 10 else np.array(_filtered)
231
243
 
232
- _fig, _axes = plt.subplots(1, len(_sample_idx), figsize=(2 * len(_sample_idx), 2))
244
+ # Which points are selected in the 3D view?
245
+ _selected_ids = set(vs_widget.selected_points or [])
246
+
247
+ _fig, _axes = plt.subplots(1, len(_sample_idx), figsize=(2 * len(_sample_idx), 2.4))
233
248
  if len(_sample_idx) == 1:
234
249
  _axes = [_axes]
235
250
  for _ax, _si in zip(_axes, _sample_idx, strict=False):
251
+ _name = label_names[labels[idx[_si]]]
236
252
  _ax.imshow(images[idx[_si]].reshape(28, 28), cmap="gray")
237
- _ax.set_title(label_names[labels[idx[_si]]], fontsize=9)
253
+ _ax.set_title(_name, fontsize=9)
238
254
  _ax.axis("off")
255
+ # Highlight if selected in 3D view
256
+ if f"p_{_si}" in _selected_ids:
257
+ _color = LABEL_COLORS.get(_name, "#0880ea")
258
+ for _spine in _ax.spines.values():
259
+ _spine.set_visible(True)
260
+ _spine.set_color(_color)
261
+ _spine.set_linewidth(3)
262
+ _ax.set_title(_name, fontsize=9, fontweight="bold", color=_color)
239
263
  plt.tight_layout()
240
264
  _fig
241
265
  return
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "anywidget-vector"
3
- version = "0.2.8"
3
+ version = "0.3.0"
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"]
@@ -122,16 +122,47 @@ 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
125
  wrapper.style.width = model.get("width") + "px";
127
126
  wrapper.style.height = model.get("height") + "px";
128
127
  el.appendChild(wrapper);
129
128
 
130
- model.on("change:dark_mode", () => {{
131
- const dark = model.get("dark_mode");
129
+ // Auto-detect host theme (marimo uses Tailwind class="dark" on <html>)
130
+ function detectHostDark() {{
131
+ const html = document.documentElement;
132
+ if (html.classList.contains("dark")) return true;
133
+ if (html.dataset.theme === "dark") return true;
134
+ if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) return true;
135
+ return false;
136
+ }}
137
+
138
+ function applyTheme(dark) {{
132
139
  wrapper.classList.toggle("avs-dark", dark);
133
- model.set("background", dark ? "#1a1a2e" : "#fafafa");
140
+ model.set("background", dark ? "#181c1a" : "#ffffff");
134
141
  model.save_changes();
142
+ }}
143
+
144
+ // Detect if we're inside a themed host (marimo, jupyter, etc.)
145
+ const hasHostTheme = !!el.getRootNode()?.host?.tagName?.startsWith("MARIMO-");
146
+ if (hasHostTheme) {{
147
+ wrapper.classList.add("avs-auto-theme");
148
+ model.set("dark_mode", detectHostDark());
149
+ applyTheme(detectHostDark());
150
+ // Watch host for live theme changes
151
+ const themeObserver = new MutationObserver(() => {{
152
+ const dark = detectHostDark();
153
+ if (model.get("dark_mode") !== dark) {{
154
+ model.set("dark_mode", dark);
155
+ applyTheme(dark);
156
+ }}
157
+ }});
158
+ themeObserver.observe(document.documentElement, {{ attributes: true, attributeFilter: ["class", "data-theme"] }});
159
+ }} else {{
160
+ applyTheme(model.get("dark_mode"));
161
+ }}
162
+
163
+ // Manual toggle from settings panel still works
164
+ model.on("change:dark_mode", () => {{
165
+ applyTheme(model.get("dark_mode"));
135
166
  }});
136
167
 
137
168
  let sidebar = null;
@@ -175,12 +175,20 @@ export function createCanvas(model, container, callbacks) {
175
175
  const points = model.get("points") || [];
176
176
  if (points.length === 0) return;
177
177
 
178
+ // Auto-scale point sizes relative to data extent
179
+ const box = new THREE.Box3();
180
+ points.forEach(p => box.expandByPoint(new THREE.Vector3(p.x ?? 0, p.y ?? 0, p.z ?? 0)));
181
+ const dataSize = box.getSize(new THREE.Vector3()).length() || 1;
182
+ const scaleFactor = dataSize / 10;
183
+ const rawRange = model.get("size_range") || [0.02, 0.06];
184
+ const sizeRange = [rawRange[0] * scaleFactor, rawRange[1] * scaleFactor];
185
+
178
186
  const opts = {
179
187
  colorField: model.get("color_field"),
180
188
  colorScale: model.get("color_scale") || "viridis",
181
189
  colorDomain: model.get("color_domain"),
182
190
  sizeField: model.get("size_field"),
183
- sizeRange: model.get("size_range") || [0.02, 0.1],
191
+ sizeRange: sizeRange,
184
192
  shapeField: model.get("shape_field"),
185
193
  shapeMap: model.get("shape_map") || {},
186
194
  };
@@ -460,7 +468,7 @@ export function createCanvas(model, container, callbacks) {
460
468
  const size = box.getSize(new THREE.Vector3()).length();
461
469
  const distance = size / (2 * Math.tan(Math.PI * camera.fov / 360));
462
470
  controls.target.copy(center);
463
- camera.position.copy(center.clone().add(new THREE.Vector3(0, 0, distance * 1.2)));
471
+ camera.position.copy(center.clone().add(new THREE.Vector3(0, 0, distance * 0.55)));
464
472
  camera.near = Math.max(0.01, distance * 0.001);
465
473
  camera.far = Math.max(1000, distance * 10);
466
474
  camera.updateProjectionMatrix();
@@ -485,8 +493,8 @@ export function createCanvas(model, container, callbacks) {
485
493
  zoomOutBtn.innerHTML = ICONS.zoomOut;
486
494
  zoomOutBtn.title = "Zoom out";
487
495
  zoomOutBtn.addEventListener("click", () => {
488
- const dir = camera.position.clone().sub(controls.target).normalize();
489
- camera.position.add(dir.multiplyScalar(0.5));
496
+ const offset = camera.position.clone().sub(controls.target);
497
+ camera.position.add(offset.multiplyScalar(0.3));
490
498
  controls.update();
491
499
  });
492
500
 
@@ -21,8 +21,9 @@ export function createSettingsPanel(model, callbacks) {
21
21
  header.appendChild(closeBtn);
22
22
  inner.appendChild(header);
23
23
 
24
- // Dark mode toggle
24
+ // Dark mode toggle (hidden when host provides theme)
25
25
  const themeGroup = createFormGroup("Theme");
26
+ themeGroup.classList.add("avs-theme-toggle");
26
27
  const toggle = document.createElement("label");
27
28
  toggle.className = "avs-toggle";
28
29
  const checkbox = document.createElement("input");
@@ -114,6 +114,22 @@ export function createSidebar(model, callbacks) {
114
114
  addStatRow(dimContent, "X range", rangeStr(xs));
115
115
  addStatRow(dimContent, "Y range", rangeStr(ys));
116
116
  addStatRow(dimContent, "Z range", rangeStr(zs));
117
+
118
+ // Visual mapping dimensions
119
+ const colorField = model.get("color_field");
120
+ const sizeField = model.get("size_field");
121
+ const shapeField = model.get("shape_field");
122
+ if (colorField) addStatRow(dimContent, "Color", colorField);
123
+ if (sizeField) {
124
+ const sizeRange = model.get("size_range") || [0.02, 0.1];
125
+ addStatRow(dimContent, "Size", sizeField + " [" + sizeRange[0] + "," + sizeRange[1] + "]");
126
+ }
127
+ if (shapeField) {
128
+ const shapeMap = model.get("shape_map") || {};
129
+ const shapes = Object.values(shapeMap);
130
+ const unique = [...new Set(shapes)];
131
+ addStatRow(dimContent, "Shape", shapeField + " (" + unique.length + " shapes)");
132
+ }
117
133
  }
118
134
 
119
135
  function updateClusters() {
@@ -159,6 +175,107 @@ export function createSidebar(model, callbacks) {
159
175
  });
160
176
  }
161
177
 
178
+ // === Distance Section ===
179
+ const distSection = document.createElement("div");
180
+ distSection.className = "avs-sidebar-section";
181
+
182
+ const distHeader = document.createElement("div");
183
+ distHeader.className = "avs-section-header";
184
+ distHeader.textContent = "Distance";
185
+ distSection.appendChild(distHeader);
186
+
187
+ const distContent = document.createElement("div");
188
+ distContent.className = "avs-dimension-content";
189
+
190
+ // Metric selector
191
+ const metricGroup = document.createElement("div");
192
+ metricGroup.className = "avs-form-group";
193
+ const metricLabel = document.createElement("label");
194
+ metricLabel.className = "avs-label";
195
+ metricLabel.textContent = "Metric";
196
+ metricGroup.appendChild(metricLabel);
197
+ const metricSelect = document.createElement("select");
198
+ metricSelect.className = "avs-select";
199
+ metricSelect.style.width = "100%";
200
+ ["euclidean", "cosine", "manhattan", "dot_product"].forEach(m => {
201
+ const opt = document.createElement("option");
202
+ opt.value = m;
203
+ opt.textContent = m;
204
+ opt.selected = m === (model.get("distance_metric") || "euclidean");
205
+ metricSelect.appendChild(opt);
206
+ });
207
+ metricSelect.addEventListener("change", () => {
208
+ model.set("distance_metric", metricSelect.value);
209
+ model.save_changes();
210
+ });
211
+ metricGroup.appendChild(metricSelect);
212
+ distContent.appendChild(metricGroup);
213
+
214
+ // K neighbors
215
+ const kGroup = document.createElement("div");
216
+ kGroup.className = "avs-form-group";
217
+ const kLabel = document.createElement("label");
218
+ kLabel.className = "avs-label";
219
+ kLabel.textContent = "K neighbors";
220
+ kGroup.appendChild(kLabel);
221
+ const kInput = document.createElement("input");
222
+ kInput.type = "number";
223
+ kInput.className = "avs-input";
224
+ kInput.min = "0";
225
+ kInput.max = "50";
226
+ kInput.value = model.get("k_neighbors") || 0;
227
+ kInput.addEventListener("input", () => {
228
+ const k = parseInt(kInput.value, 10) || 0;
229
+ model.set("k_neighbors", k);
230
+ model.set("show_connections", k > 0);
231
+ model.save_changes();
232
+ });
233
+ kGroup.appendChild(kInput);
234
+ distContent.appendChild(kGroup);
235
+
236
+ // Distance info (updates when points are selected)
237
+ const distInfo = document.createElement("div");
238
+ distInfo.className = "avs-note";
239
+ distInfo.textContent = "Select a point to see distances";
240
+ distContent.appendChild(distInfo);
241
+
242
+ distSection.appendChild(distContent);
243
+ inner.appendChild(distSection);
244
+
245
+ function updateDistanceInfo() {
246
+ const selected = model.get("selected_points") || [];
247
+ if (selected.length === 0) {
248
+ distInfo.textContent = "Select a point to see distances";
249
+ return;
250
+ }
251
+ const points = model.get("points") || [];
252
+ const ref = points.find(p => p.id === selected[0]);
253
+ if (!ref) return;
254
+
255
+ const metric = metricSelect.value;
256
+ const others = points.filter(p => p.id !== ref.id);
257
+
258
+ function dist(a, b) {
259
+ const dx = (a.x ?? 0) - (b.x ?? 0), dy = (a.y ?? 0) - (b.y ?? 0), dz = (a.z ?? 0) - (b.z ?? 0);
260
+ if (metric === "manhattan") return Math.abs(dx) + Math.abs(dy) + Math.abs(dz);
261
+ if (metric === "cosine") {
262
+ const dot = (a.x??0)*(b.x??0) + (a.y??0)*(b.y??0) + (a.z??0)*(b.z??0);
263
+ const ma = Math.sqrt((a.x??0)**2 + (a.y??0)**2 + (a.z??0)**2);
264
+ const mb = Math.sqrt((b.x??0)**2 + (b.y??0)**2 + (b.z??0)**2);
265
+ return (ma === 0 || mb === 0) ? 1 : 1 - dot / (ma * mb);
266
+ }
267
+ if (metric === "dot_product") return -((a.x??0)*(b.x??0) + (a.y??0)*(b.y??0) + (a.z??0)*(b.z??0));
268
+ return Math.sqrt(dx*dx + dy*dy + dz*dz);
269
+ }
270
+
271
+ const sorted = others.map(p => ({ id: p.id, label: p.label || p.id, d: dist(ref, p) }))
272
+ .sort((a, b) => a.d - b.d).slice(0, 5);
273
+
274
+ distInfo.innerHTML = "<strong>" + escapeHtml(ref.label || ref.id) + "</strong><br>" +
275
+ sorted.map(n => '<span class="avs-property-key">' + escapeHtml(n.label) +
276
+ '</span> <span class="avs-property-value">' + n.d.toFixed(2) + "</span>").join("<br>");
277
+ }
278
+
162
279
  // === Event Bindings ===
163
280
  model.on("change:points", () => {
164
281
  updateDimensions();
@@ -167,6 +284,11 @@ export function createSidebar(model, callbacks) {
167
284
  model.on("change:color_field", updateClusters);
168
285
  model.on("change:backend_config", updateCollections);
169
286
  model.on("change:backend", updateCollections);
287
+ model.on("change:selected_points", updateDistanceInfo);
288
+ model.on("change:distance_metric", () => {
289
+ metricSelect.value = model.get("distance_metric") || "euclidean";
290
+ updateDistanceInfo();
291
+ });
170
292
 
171
293
  // Initial render
172
294
  updateCollections();
@@ -1,14 +1,14 @@
1
1
  /* === CSS Variables === */
2
2
 
3
- /* Light mode (default) */
3
+ /* Light mode (default, matches marimo light) */
4
4
  .avs-wrapper {
5
5
  --avs-bg: #ffffff;
6
6
  --avs-surface: #f8f9fa;
7
- --avs-border: #e5e7eb;
8
- --avs-text: #111827;
7
+ --avs-border: #e2e8f0;
8
+ --avs-text: #0f172a;
9
9
  --avs-text-muted: #6b7280;
10
- --avs-primary: #6366f1;
11
- --avs-primary-hover: #4f46e5;
10
+ --avs-primary: #0880ea;
11
+ --avs-primary-hover: #0670d0;
12
12
  --avs-success: #10b981;
13
13
  --avs-warning: #f59e0b;
14
14
  --avs-error: #ef4444;
@@ -23,15 +23,15 @@
23
23
  flex-direction: column;
24
24
  }
25
25
 
26
- /* Dark mode */
26
+ /* Dark mode (matches marimo dark) */
27
27
  .avs-wrapper.avs-dark {
28
- --avs-bg: #1a1a2e;
29
- --avs-surface: #252542;
30
- --avs-border: #3a3a5c;
31
- --avs-text: #e0e0e0;
32
- --avs-text-muted: #888;
33
- --avs-primary: #6366f1;
34
- --avs-primary-hover: #818cf8;
28
+ --avs-bg: #181c1a;
29
+ --avs-surface: #252927;
30
+ --avs-border: #3b403e;
31
+ --avs-text: #eceeed;
32
+ --avs-text-muted: #9ca3af;
33
+ --avs-primary: #28879f;
34
+ --avs-primary-hover: #1d5b6a;
35
35
  }
36
36
 
37
37
  /* === Main Layout === */
@@ -601,3 +601,8 @@
601
601
  font-size: 13px;
602
602
  color: var(--avs-text);
603
603
  }
604
+
605
+ /* Hide dark mode toggle when host provides theme */
606
+ .avs-wrapper.avs-auto-theme .avs-theme-toggle {
607
+ display: none;
608
+ }
@@ -54,7 +54,7 @@ class VectorSpace(anywidget.AnyWidget):
54
54
 
55
55
  # === Size Mapping ===
56
56
  size_field = traitlets.Unicode(default_value=None, allow_none=True).tag(sync=True)
57
- size_range = traitlets.List(default_value=[0.02, 0.1]).tag(sync=True)
57
+ size_range = traitlets.List(default_value=[0.02, 0.06]).tag(sync=True)
58
58
 
59
59
  # === Shape Mapping ===
60
60
  shape_field = traitlets.Unicode(default_value=None, allow_none=True).tag(sync=True)
@@ -50,7 +50,7 @@ wheels = [
50
50
 
51
51
  [[package]]
52
52
  name = "anywidget-vector"
53
- version = "0.2.6"
53
+ version = "0.3.0"
54
54
  source = { editable = "." }
55
55
  dependencies = [
56
56
  { name = "anywidget" },
@@ -123,13 +123,13 @@ requires-dist = [
123
123
  { name = "pandas", marker = "extra == 'pandas'", specifier = ">=2.0" },
124
124
  { name = "pinecone-client", marker = "extra == 'all'", specifier = ">=6.0" },
125
125
  { name = "pinecone-client", marker = "extra == 'pinecone'", specifier = ">=3.0" },
126
- { name = "prek", marker = "extra == 'dev'", specifier = ">=0.3.2" },
126
+ { name = "prek", marker = "extra == 'dev'", specifier = ">=0.3" },
127
127
  { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.2" },
128
128
  { name = "qdrant-client", marker = "extra == 'all'", specifier = ">=1" },
129
129
  { name = "qdrant-client", marker = "extra == 'qdrant'", specifier = ">=1.0" },
130
130
  { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15" },
131
131
  { name = "scikit-learn", marker = "extra == 'projection'", specifier = ">=1.3" },
132
- { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.20" },
132
+ { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0" },
133
133
  { name = "umap-learn", marker = "extra == 'projection'", specifier = ">=0.5" },
134
134
  { name = "weaviate-client", marker = "extra == 'all'", specifier = ">=4.20" },
135
135
  { name = "weaviate-client", marker = "extra == 'weaviate'", specifier = ">=4.0" },