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.
- anywidget_vector-0.3.1/CHANGELOG.md +129 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/PKG-INFO +3 -3
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/examples/fashion-mnist-parallel-coords-6d.py +88 -41
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/pyproject.toml +3 -3
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/static/icons.js +1 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/__init__.py +56 -16
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/canvas.js +287 -64
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/properties.js +8 -11
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/settings.js +2 -1
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/sidebar.js +132 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/styles.css +18 -13
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/widget.py +8 -4
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/tests/test_widget.py +2 -2
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/uv.lock +3 -3
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/.github/workflows/ci.yml +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/.github/workflows/pypi.yml +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/.gitignore +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/.python-version +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/LICENSE +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/README.md +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/assets/demo.png +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/examples/demo.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/examples/test_add_numpy.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/examples/test_projection.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/__init__.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/__init__.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/chroma/__init__.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/chroma/client.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/chroma/converter.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/grafeo/__init__.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/grafeo/browser.js +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/grafeo/client.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/grafeo/converter.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/grafeo/embed.js +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/lancedb/__init__.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/lancedb/client.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/lancedb/converter.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/pinecone/__init__.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/pinecone/client.js +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/pinecone/converter.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/qdrant/__init__.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/qdrant/client.js +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/qdrant/converter.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/weaviate/__init__.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/weaviate/client.js +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/backends/weaviate/converter.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/demo.py +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/py.typed +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/constants.js +0 -0
- {anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/src/anywidget_vector/ui/toolbar.js +0 -0
- {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.
|
|
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
|
|
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
|
|
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'
|
{anywidget_vector-0.2.8 → anywidget_vector-0.3.1}/examples/fashion-mnist-parallel-coords-6d.py
RENAMED
|
@@ -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.
|
|
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 +
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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=
|
|
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="
|
|
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="
|
|
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
|
|
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.
|
|
187
|
-
if
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
|
215
|
-
|
|
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
|
|
232
|
+
_new_p.pop("color", None)
|
|
221
233
|
_updated.append(_new_p)
|
|
222
234
|
vs_widget.points = _updated
|
|
223
|
-
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*")
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
|
29
|
+
"prek>=0.3",
|
|
30
30
|
"pytest>=9.0.2",
|
|
31
31
|
"ruff>=0.15",
|
|
32
|
-
"ty>=0.0
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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 ? "#
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
//
|
|
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
|
|