anywidget-vector 0.2.6__tar.gz → 0.2.8__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 (50) hide show
  1. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/PKG-INFO +1 -1
  2. anywidget_vector-0.2.8/examples/fashion-mnist-parallel-coords-6d.py +245 -0
  3. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/pyproject.toml +1 -1
  4. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/ui/__init__.py +8 -0
  5. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/ui/canvas.js +3 -0
  6. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/ui/settings.js +25 -0
  7. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/ui/styles.css +65 -6
  8. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/widget.py +3 -0
  9. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/.github/workflows/ci.yml +0 -0
  10. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/.github/workflows/pypi.yml +0 -0
  11. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/.gitignore +0 -0
  12. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/.python-version +0 -0
  13. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/LICENSE +0 -0
  14. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/README.md +0 -0
  15. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/assets/demo.png +0 -0
  16. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/examples/demo.py +0 -0
  17. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/examples/test_add_numpy.py +0 -0
  18. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/examples/test_projection.py +0 -0
  19. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/__init__.py +0 -0
  20. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/__init__.py +0 -0
  21. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/chroma/__init__.py +0 -0
  22. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/chroma/client.py +0 -0
  23. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/chroma/converter.py +0 -0
  24. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/grafeo/__init__.py +0 -0
  25. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/grafeo/browser.js +0 -0
  26. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/grafeo/client.py +0 -0
  27. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/grafeo/converter.py +0 -0
  28. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/grafeo/embed.js +0 -0
  29. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/lancedb/__init__.py +0 -0
  30. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/lancedb/client.py +0 -0
  31. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/lancedb/converter.py +0 -0
  32. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/pinecone/__init__.py +0 -0
  33. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/pinecone/client.js +0 -0
  34. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/pinecone/converter.py +0 -0
  35. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/qdrant/__init__.py +0 -0
  36. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/qdrant/client.js +0 -0
  37. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/qdrant/converter.py +0 -0
  38. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/weaviate/__init__.py +0 -0
  39. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/weaviate/client.js +0 -0
  40. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/backends/weaviate/converter.py +0 -0
  41. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/demo.py +0 -0
  42. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/py.typed +0 -0
  43. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/static/icons.js +0 -0
  44. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/ui/constants.js +0 -0
  45. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/ui/properties.js +0 -0
  46. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/ui/sidebar.js +0 -0
  47. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/src/anywidget_vector/ui/toolbar.js +0 -0
  48. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/tests/__init__.py +0 -0
  49. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/tests/test_widget.py +0 -0
  50. {anywidget_vector-0.2.6 → anywidget_vector-0.2.8}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anywidget-vector
3
- Version: 0.2.6
3
+ Version: 0.2.8
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
@@ -0,0 +1,245 @@
1
+ # /// script
2
+ # requires-python = ">=3.12"
3
+ # dependencies = [
4
+ # "marimo",
5
+ # "polars==1.38.1",
6
+ # "numpy==2.4.2",
7
+ # "scikit-learn==1.8.0",
8
+ # "wigglystuff==0.2.37",
9
+ # "matplotlib==3.10.8",
10
+ # "pandas==3.0.1",
11
+ # "anywidget-vector==0.2.7",
12
+ # ]
13
+ # ///
14
+
15
+ import marimo
16
+
17
+ __generated_with = "0.20.2"
18
+ app = marimo.App(width="full")
19
+
20
+
21
+ @app.cell
22
+ def _():
23
+ import marimo as mo
24
+ import matplotlib.pyplot as plt
25
+ import numpy as np
26
+ import polars as pl
27
+ from sklearn.datasets import fetch_openml
28
+ from sklearn.decomposition import PCA
29
+ from wigglystuff import ParallelCoordinates
30
+
31
+ from anywidget_vector import VectorSpace
32
+
33
+ return PCA, ParallelCoordinates, VectorSpace, fetch_openml, mo, np, pl, plt
34
+
35
+
36
+ @app.cell(hide_code=True)
37
+ def _(mo):
38
+ mo.md(r"""
39
+ # Fashion MNIST: Parallel Coordinates + 3D Vector View
40
+
41
+ Brush the parallel coordinates axes to filter. The 3D scatter updates in real time.
42
+ Use the dropdowns to map PCA dimensions to visual channels.
43
+ """)
44
+ return
45
+
46
+
47
+ @app.cell
48
+ def _(fetch_openml, np):
49
+ mnist = fetch_openml("Fashion-MNIST", version=1, as_frame=False, parser="auto")
50
+ images = mnist.data.astype(np.float32)
51
+ labels = mnist.target.astype(int)
52
+
53
+ label_names = {
54
+ 0: "T-shirt/top",
55
+ 1: "Trouser",
56
+ 2: "Pullover",
57
+ 3: "Dress",
58
+ 4: "Coat",
59
+ 5: "Sandal",
60
+ 6: "Shirt",
61
+ 7: "Sneaker",
62
+ 8: "Bag",
63
+ 9: "Ankle boot",
64
+ }
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
+ }
77
+ SHAPE_NAMES = ["sphere", "cube", "cone", "octahedron", "cylinder", "tetrahedron"]
78
+ return LABEL_COLORS, SHAPE_NAMES, images, label_names, labels
79
+
80
+
81
+ @app.cell
82
+ def _(mo):
83
+ 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")
85
+ mo.hstack([n_samples_slider, n_components_slider])
86
+ return n_components_slider, n_samples_slider
87
+
88
+
89
+ @app.cell
90
+ def _(
91
+ PCA,
92
+ images,
93
+ label_names,
94
+ labels,
95
+ n_components_slider,
96
+ n_samples_slider,
97
+ np,
98
+ pl,
99
+ ):
100
+ _rng = np.random.default_rng(42)
101
+ idx = _rng.choice(len(images), size=n_samples_slider.value, replace=False)
102
+
103
+ _pca = PCA(n_components=n_components_slider.value)
104
+ components = _pca.fit_transform(images[idx])
105
+
106
+ df = pl.DataFrame({f"PC{i + 1}": components[:, i] for i in range(n_components_slider.value)}).with_columns(
107
+ pl.Series("label", [label_names[labels[i]] for i in idx])
108
+ )
109
+ return components, df, idx
110
+
111
+
112
+ @app.cell
113
+ def _(ParallelCoordinates, df, mo):
114
+ widget = mo.ui.anywidget(ParallelCoordinates(df, color_by="label"))
115
+ widget
116
+ return (widget,)
117
+
118
+
119
+ @app.cell
120
+ def _(mo, n_components_slider):
121
+ pcs = [f"PC{i + 1}" for i in range(n_components_slider.value)]
122
+ x_dim = mo.ui.dropdown(options=pcs, value="PC1", label="X")
123
+ y_dim = mo.ui.dropdown(options=pcs, value="PC2", label="Y")
124
+ z_dim = mo.ui.dropdown(options=pcs, value="PC3", label="Z")
125
+ size_dim = mo.ui.dropdown(options=["none", *pcs], value="none", label="Size")
126
+ 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")
128
+ mo.hstack([x_dim, y_dim, z_dim, size_dim, color_dim, shape_dim], gap=0.5)
129
+ return color_dim, shape_dim, size_dim, x_dim, y_dim, z_dim
130
+
131
+
132
+ @app.cell
133
+ def _(
134
+ LABEL_COLORS,
135
+ SHAPE_NAMES,
136
+ VectorSpace,
137
+ color_dim,
138
+ components,
139
+ idx,
140
+ label_names,
141
+ labels,
142
+ mo,
143
+ shape_dim,
144
+ size_dim,
145
+ x_dim,
146
+ y_dim,
147
+ z_dim,
148
+ ):
149
+ def _pc(name):
150
+ return int(name[2:]) - 1
151
+
152
+ _xi, _yi, _zi = _pc(x_dim.value), _pc(y_dim.value), _pc(z_dim.value)
153
+ _color_pc = None if color_dim.value == "label" else _pc(color_dim.value)
154
+ _size_pc = None if size_dim.value == "none" else _pc(size_dim.value)
155
+ _unique_labels = sorted(set(label_names.values()))
156
+ _shape_map = {_n: SHAPE_NAMES[_j % len(SHAPE_NAMES)] for _j, _n in enumerate(_unique_labels)}
157
+
158
+ vs_points = []
159
+ for _i in range(len(components)):
160
+ _name = label_names[labels[idx[_i]]]
161
+ _p = {
162
+ "id": f"p_{_i}",
163
+ "x": float(components[_i, _xi]),
164
+ "y": float(components[_i, _yi]),
165
+ "z": float(components[_i, _zi]),
166
+ "label": _name,
167
+ }
168
+ if _color_pc is None:
169
+ _p["color"] = LABEL_COLORS[_name]
170
+ else:
171
+ _p["color_val"] = float(components[_i, _color_pc])
172
+ if _size_pc is not None:
173
+ _p["size_val"] = float(components[_i, _size_pc])
174
+ if shape_dim.value == "label":
175
+ _p["shape_cat"] = _name
176
+ vs_points.append(_p)
177
+
178
+ _vs_kwargs = {
179
+ "axis_labels": {"x": x_dim.value, "y": y_dim.value, "z": z_dim.value},
180
+ }
181
+ if _color_pc is not None:
182
+ _vs_kwargs["color_field"] = "color_val"
183
+ _vs_kwargs["color_scale"] = "viridis"
184
+ if _size_pc is not None:
185
+ _vs_kwargs["size_field"] = "size_val"
186
+ _vs_kwargs["size_range"] = [0.01, 0.06]
187
+ if shape_dim.value == "label":
188
+ _vs_kwargs["shape_field"] = "shape_cat"
189
+ _vs_kwargs["shape_map"] = _shape_map
190
+
191
+ vs_widget = VectorSpace(
192
+ points=vs_points,
193
+ width=1200,
194
+ height=500,
195
+ dark_mode=False,
196
+ background="#fafafa",
197
+ show_toolbar=True,
198
+ show_settings=True,
199
+ show_properties=False,
200
+ **_vs_kwargs,
201
+ )
202
+ vs = mo.ui.anywidget(vs_widget)
203
+ vs
204
+ return vs, vs_points, vs_widget
205
+
206
+
207
+ @app.cell
208
+ def _(LABEL_COLORS, color_dim, mo, vs_points, vs_widget, widget):
209
+ _filtered = set(widget.widget.filtered_indices)
210
+ _dim_color = "#d1d5db"
211
+ _updated = []
212
+ for _i, _p in enumerate(vs_points):
213
+ _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)
219
+ else:
220
+ _new_p["color"] = _dim_color
221
+ _updated.append(_new_p)
222
+ vs_widget.points = _updated
223
+ mo.md(f"**{len(_filtered)}** / {len(vs_points)} points selected")
224
+ return
225
+
226
+
227
+ @app.cell
228
+ def _(idx, images, label_names, labels, np, plt, widget):
229
+ _filtered = widget.widget.filtered_indices
230
+ _sample_idx = np.array(_filtered[:10]) if len(_filtered) >= 10 else np.array(_filtered)
231
+
232
+ _fig, _axes = plt.subplots(1, len(_sample_idx), figsize=(2 * len(_sample_idx), 2))
233
+ if len(_sample_idx) == 1:
234
+ _axes = [_axes]
235
+ for _ax, _si in zip(_axes, _sample_idx, strict=False):
236
+ _ax.imshow(images[idx[_si]].reshape(28, 28), cmap="gray")
237
+ _ax.set_title(label_names[labels[idx[_si]]], fontsize=9)
238
+ _ax.axis("off")
239
+ plt.tight_layout()
240
+ _fig
241
+ return
242
+
243
+
244
+ if __name__ == "__main__":
245
+ app.run()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "anywidget-vector"
3
- version = "0.2.6"
3
+ version = "0.2.8"
4
4
  description = "Interactive 3D vector visualization with query UI for vector databases"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -122,10 +122,18 @@ 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");
125
126
  wrapper.style.width = model.get("width") + "px";
126
127
  wrapper.style.height = model.get("height") + "px";
127
128
  el.appendChild(wrapper);
128
129
 
130
+ model.on("change:dark_mode", () => {{
131
+ const dark = model.get("dark_mode");
132
+ wrapper.classList.toggle("avs-dark", dark);
133
+ model.set("background", dark ? "#1a1a2e" : "#fafafa");
134
+ model.save_changes();
135
+ }});
136
+
129
137
  let sidebar = null;
130
138
  let settingsPanel = null;
131
139
  let propertiesPanel = null;
@@ -461,6 +461,9 @@ export function createCanvas(model, container, callbacks) {
461
461
  const distance = size / (2 * Math.tan(Math.PI * camera.fov / 360));
462
462
  controls.target.copy(center);
463
463
  camera.position.copy(center.clone().add(new THREE.Vector3(0, 0, distance * 1.2)));
464
+ camera.near = Math.max(0.01, distance * 0.001);
465
+ camera.far = Math.max(1000, distance * 10);
466
+ camera.updateProjectionMatrix();
464
467
  controls.update();
465
468
  }
466
469
 
@@ -21,6 +21,31 @@ export function createSettingsPanel(model, callbacks) {
21
21
  header.appendChild(closeBtn);
22
22
  inner.appendChild(header);
23
23
 
24
+ // Dark mode toggle
25
+ const themeGroup = createFormGroup("Theme");
26
+ const toggle = document.createElement("label");
27
+ toggle.className = "avs-toggle";
28
+ const checkbox = document.createElement("input");
29
+ checkbox.type = "checkbox";
30
+ checkbox.checked = model.get("dark_mode");
31
+ checkbox.addEventListener("change", (e) => {
32
+ model.set("dark_mode", e.target.checked);
33
+ model.save_changes();
34
+ });
35
+ model.on("change:dark_mode", () => {
36
+ checkbox.checked = model.get("dark_mode");
37
+ });
38
+ const slider = document.createElement("span");
39
+ slider.className = "avs-toggle-slider";
40
+ const toggleLabel = document.createElement("span");
41
+ toggleLabel.className = "avs-toggle-label";
42
+ toggleLabel.textContent = "Dark mode";
43
+ toggle.appendChild(checkbox);
44
+ toggle.appendChild(slider);
45
+ toggle.appendChild(toggleLabel);
46
+ themeGroup.appendChild(toggle);
47
+ inner.appendChild(themeGroup);
48
+
24
49
  // Backend selector
25
50
  const backendGroup = createFormGroup("Backend");
26
51
  const backendSelect = document.createElement("select");
@@ -1,12 +1,14 @@
1
1
  /* === CSS Variables === */
2
+
3
+ /* Light mode (default) */
2
4
  .avs-wrapper {
3
- --avs-bg: #1a1a2e;
4
- --avs-surface: #252542;
5
- --avs-border: #3a3a5c;
6
- --avs-text: #e0e0e0;
7
- --avs-text-muted: #888;
5
+ --avs-bg: #ffffff;
6
+ --avs-surface: #f8f9fa;
7
+ --avs-border: #e5e7eb;
8
+ --avs-text: #111827;
9
+ --avs-text-muted: #6b7280;
8
10
  --avs-primary: #6366f1;
9
- --avs-primary-hover: #818cf8;
11
+ --avs-primary-hover: #4f46e5;
10
12
  --avs-success: #10b981;
11
13
  --avs-warning: #f59e0b;
12
14
  --avs-error: #ef4444;
@@ -21,6 +23,17 @@
21
23
  flex-direction: column;
22
24
  }
23
25
 
26
+ /* Dark mode */
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;
35
+ }
36
+
24
37
  /* === Main Layout === */
25
38
  .avs-main {
26
39
  display: flex;
@@ -542,3 +555,49 @@
542
555
  padding: 10px 16px;
543
556
  }
544
557
  }
558
+
559
+ /* === Toggle Switch === */
560
+ .avs-toggle {
561
+ display: flex;
562
+ align-items: center;
563
+ cursor: pointer;
564
+ gap: 10px;
565
+ }
566
+
567
+ .avs-toggle input {
568
+ display: none;
569
+ }
570
+
571
+ .avs-toggle-slider {
572
+ width: 36px;
573
+ height: 20px;
574
+ background: var(--avs-border);
575
+ border-radius: 10px;
576
+ position: relative;
577
+ transition: background 0.2s;
578
+ }
579
+
580
+ .avs-toggle-slider::after {
581
+ content: "";
582
+ position: absolute;
583
+ top: 2px;
584
+ left: 2px;
585
+ width: 16px;
586
+ height: 16px;
587
+ background: #fff;
588
+ border-radius: 50%;
589
+ transition: transform 0.2s;
590
+ }
591
+
592
+ .avs-toggle input:checked + .avs-toggle-slider {
593
+ background: var(--avs-primary);
594
+ }
595
+
596
+ .avs-toggle input:checked + .avs-toggle-slider::after {
597
+ transform: translateX(16px);
598
+ }
599
+
600
+ .avs-toggle-label {
601
+ font-size: 13px;
602
+ color: var(--avs-text);
603
+ }
@@ -38,6 +38,9 @@ class VectorSpace(anywidget.AnyWidget):
38
38
  height = traitlets.Int(default_value=600).tag(sync=True)
39
39
  background = traitlets.Unicode(default_value="#1a1a2e").tag(sync=True)
40
40
 
41
+ # === Theme ===
42
+ dark_mode = traitlets.Bool(default_value=True).tag(sync=True)
43
+
41
44
  # === Axes and Grid ===
42
45
  show_axes = traitlets.Bool(default_value=True).tag(sync=True)
43
46
  show_grid = traitlets.Bool(default_value=True).tag(sync=True)