diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..837b16a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,109 @@ +# Changelog + +## 0.3.0 (unreleased) + +### New Features + +- **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 +- **Dimension mapping display**: Sidebar now shows active visual mapping fields: color field, size range, shape field and unique shape count + +### Improvements + +- Improved sidebar styling and layout + +## 0.2.8 (2026-03-15) + +### New Features + +- **6D parallel coordinates example**: Fashion-MNIST demo with PCA across 6+ dimensions, interactive axis mapping, and shape mapping to categorical labels + +## 0.2.7 (2026-03-15) + +### New Features + +- **Dark/light mode toggle**: `dark_mode` trait with real-time CSS color scheme switching and theme-aware styling across all panels + +## 0.2.6 (2026-03-15) + +### New Features + +- **NumPy array support**: `add_numpy()` method for adding points from arrays with automatic PCA projection for high-dimensional data (D > 3) +- **Dimensionality reduction**: `project()` method with PCA, t-SNE and UMAP support; `set_vectors()` for storing high-dimensional vectors separately +- **Example scripts**: Projection demo and numpy integration examples + +### Improvements + +- High-dimensional vector storage without syncing to JavaScript +- Coordinate normalization after projection +- Added demo screenshot to README + +## 0.2.5 (2026-03-04) + +### Bug Fixes + +- Fixed lasso selection stability issues +- Removed problematic custom traitlets implementation + +### Improvements + +- Added keyboard shortcuts: F to fit view, Escape to clear selection +- Better visual feedback for selection modes +- Cleaner toolbar layout with integrated mode controls + +## 0.2.4 (2026-03-02) + +### New Features + +- **Zoom and pan controls**: Zoom in, zoom out and fit-to-view buttons in the toolbar +- **Box selection mode**: New selection mode alongside click and multi-select, with visual feedback during selection +- **HTML export**: `to_html()` and `save_html()` methods for self-contained HTML output with embedded data and Three.js + +### Improvements + +- Improved camera fitting algorithm for better initial view framing +- Updated README with comprehensive usage examples +- Updated dependencies + +## 0.2.3 (2026-02-12) + +### New Features + +- **Sidebar explorer**: Three collapsible sections: collections browser, dimensions explorer (point count, dimensionality, axis ranges) and clusters explorer (grouped by color field with sizes) +- **Browser-side Grafeo backend**: JavaScript client for Grafeo server and WASM-embedded queries +- **Demo module**: Reference implementation showing various widget features + +### Improvements + +- Consistency pass across all backend modules +- Ruff configuration tuned for marimo compatibility + +## 0.2.2 (2026-02-07) + +### New Features + +- **Streaming factory methods**: `from_numpy()`, `from_umap()` and `from_arrays()` constructors for various data formats + +### Improvements + +- Added Apache 2.0 license +- Expanded test suite with 194 additional test cases +- ESM bundle aggregation for widget JavaScript + +## 0.2.1 (2026-02-03) + +### New Features + +- **Multi-backend architecture**: Six backends: Qdrant, Pinecone, Weaviate (browser-side REST), Chroma, LanceDB, Grafeo (Python-side) +- **Modular UI**: Separated rendering into canvas, settings, properties and toolbar components +- **Backend factory methods**: `from_qdrant()`, `from_pinecone()`, `from_weaviate()`, `from_chroma()`, `from_lancedb()`, `from_grafeo()` +- **Event system**: `on_click()`, `on_hover()` and `on_selection()` decorators with callback signatures +- **CI/CD**: GitHub Actions pipelines for testing and PyPI publishing + +### Improvements + +- Refactored monolithic widget into modular component architecture +- Added README with usage examples + +## 0.1.0 (2026-02-03) + +- 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 diff --git a/examples/fashion-mnist-parallel-coords-6d.py b/examples/fashion-mnist-parallel-coords-6d.py index 1ef5642..0719d6f 100644 --- a/examples/fashion-mnist-parallel-coords-6d.py +++ b/examples/fashion-mnist-parallel-coords-6d.py @@ -1,3 +1,8 @@ +# Based on the Fashion MNIST Parallel Coordinates notebook by the marimo team: +# https://github.com/marimo-team/gallery-examples/blob/main/notebooks/wigglystuff/fashion-mnist-parallel-coords.py +# +# Extended with anywidget-vector 3D scatter view and 6D dimension mapping. +# # /// script # requires-python = ">=3.12" # dependencies = [ @@ -14,7 +19,7 @@ import marimo -__generated_with = "0.20.2" +__generated_with = "0.19.7" app = marimo.App(width="full") @@ -36,7 +41,7 @@ def _(): @app.cell(hide_code=True) def _(mo): mo.md(r""" - # Fashion MNIST: Parallel Coordinates + 3D Vector View + # Fashion MNIST: Parallel Coordinates + 6D Vector View Brush the parallel coordinates axes to filter. The 3D scatter updates in real time. Use the dropdowns to map PCA dimensions to visual channels. @@ -62,18 +67,9 @@ def _(fetch_openml, np): 8: "Bag", 9: "Ankle boot", } - LABEL_COLORS = { - "T-shirt/top": "#6366f1", - "Trouser": "#f59e0b", - "Pullover": "#10b981", - "Dress": "#ef4444", - "Coat": "#8b5cf6", - "Sandal": "#06b6d4", - "Shirt": "#f97316", - "Sneaker": "#84cc16", - "Bag": "#ec4899", - "Ankle boot": "#14b8a6", - } + # d3 schemeCategory10, assigned alphabetically to match wigglystuff + _d3 = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"] + LABEL_COLORS = {_name: _d3[_i] for _i, _name in enumerate(sorted(label_names.values()))} SHAPE_NAMES = ["sphere", "cube", "cone", "octahedron", "cylinder", "tetrahedron"] return LABEL_COLORS, SHAPE_NAMES, images, label_names, labels @@ -110,8 +106,8 @@ def _( @app.cell -def _(ParallelCoordinates, df, mo): - widget = mo.ui.anywidget(ParallelCoordinates(df, color_by="label")) +def _(LABEL_COLORS, ParallelCoordinates, df, mo): + widget = mo.ui.anywidget(ParallelCoordinates(df, color_by="label", color_map=LABEL_COLORS)) widget return (widget,) @@ -122,9 +118,9 @@ def _(mo, n_components_slider): x_dim = mo.ui.dropdown(options=pcs, value="PC1", label="X") y_dim = mo.ui.dropdown(options=pcs, value="PC2", label="Y") z_dim = mo.ui.dropdown(options=pcs, value="PC3", label="Z") - size_dim = mo.ui.dropdown(options=["none", *pcs], value="none", label="Size") + size_dim = mo.ui.dropdown(options=["none", *pcs], value="PC4", label="Size") color_dim = mo.ui.dropdown(options=["label", *pcs], value="label", label="Color") - shape_dim = mo.ui.dropdown(options=["label", "none"], value="none", label="Shape") + shape_dim = mo.ui.dropdown(options=["label", "none", *pcs], value="PC5", label="Shape") mo.hstack([x_dim, y_dim, z_dim, size_dim, color_dim, shape_dim], gap=0.5) return color_dim, shape_dim, size_dim, x_dim, y_dim, z_dim @@ -140,6 +136,7 @@ def _( label_names, labels, mo, + np, shape_dim, size_dim, x_dim, @@ -152,8 +149,23 @@ def _pc(name): _xi, _yi, _zi = _pc(x_dim.value), _pc(y_dim.value), _pc(z_dim.value) _color_pc = None if color_dim.value == "label" else _pc(color_dim.value) _size_pc = None if size_dim.value == "none" else _pc(size_dim.value) + _shape_mode = shape_dim.value # "label", "none", or a PC name + + # Shape mapping: label-based or PC-based (binned into 6 shape buckets) _unique_labels = sorted(set(label_names.values())) _shape_map = {_n: SHAPE_NAMES[_j % len(SHAPE_NAMES)] for _j, _n in enumerate(_unique_labels)} + _shape_bins = None + if _shape_mode not in ("label", "none"): + _shape_col = components[:, _pc(_shape_mode)] + _quantiles = np.percentile(_shape_col, np.linspace(0, 100, len(SHAPE_NAMES) + 1)) + _shape_bins = list(zip(_quantiles[:-1], _quantiles[1:], SHAPE_NAMES, strict=False)) + _shape_map = {_s: _s for _s in SHAPE_NAMES} + + def _get_shape_bin(_val): + for _lo, _hi, _s in _shape_bins: + if _val <= _hi: + return _s + return SHAPE_NAMES[-1] vs_points = [] for _i in range(len(components)): @@ -171,8 +183,10 @@ def _pc(name): _p["color_val"] = float(components[_i, _color_pc]) if _size_pc is not None: _p["size_val"] = float(components[_i, _size_pc]) - if shape_dim.value == "label": + if _shape_mode == "label": _p["shape_cat"] = _name + elif _shape_bins is not None: + _p["shape_cat"] = _get_shape_bin(components[_i, _pc(_shape_mode)]) vs_points.append(_p) _vs_kwargs = { @@ -183,17 +197,15 @@ def _pc(name): _vs_kwargs["color_scale"] = "viridis" if _size_pc is not None: _vs_kwargs["size_field"] = "size_val" - _vs_kwargs["size_range"] = [0.01, 0.06] - if shape_dim.value == "label": + _vs_kwargs["size_range"] = [0.01, 0.03] + if _shape_mode != "none": _vs_kwargs["shape_field"] = "shape_cat" _vs_kwargs["shape_map"] = _shape_map vs_widget = VectorSpace( points=vs_points, - width=1200, + width=1600, height=500, - dark_mode=False, - background="#fafafa", show_toolbar=True, show_settings=True, show_properties=False, @@ -201,7 +213,7 @@ def _pc(name): ) vs = mo.ui.anywidget(vs_widget) vs - return vs, vs_points, vs_widget + return vs_points, vs_widget @app.cell @@ -225,17 +237,29 @@ def _(LABEL_COLORS, color_dim, mo, vs_points, vs_widget, widget): @app.cell -def _(idx, images, label_names, labels, np, plt, widget): +def _(LABEL_COLORS, idx, images, label_names, labels, np, plt, vs_widget, widget): _filtered = widget.widget.filtered_indices _sample_idx = np.array(_filtered[:10]) if len(_filtered) >= 10 else np.array(_filtered) - _fig, _axes = plt.subplots(1, len(_sample_idx), figsize=(2 * len(_sample_idx), 2)) + # Which points are selected in the 3D view? + _selected_ids = set(vs_widget.selected_points or []) + + _fig, _axes = plt.subplots(1, len(_sample_idx), figsize=(2 * len(_sample_idx), 2.4)) if len(_sample_idx) == 1: _axes = [_axes] for _ax, _si in zip(_axes, _sample_idx, strict=False): + _name = label_names[labels[idx[_si]]] _ax.imshow(images[idx[_si]].reshape(28, 28), cmap="gray") - _ax.set_title(label_names[labels[idx[_si]]], fontsize=9) + _ax.set_title(_name, fontsize=9) _ax.axis("off") + # Highlight if selected in 3D view + if f"p_{_si}" in _selected_ids: + _color = LABEL_COLORS.get(_name, "#0880ea") + for _spine in _ax.spines.values(): + _spine.set_visible(True) + _spine.set_color(_color) + _spine.set_linewidth(3) + _ax.set_title(_name, fontsize=9, fontweight="bold", color=_color) plt.tight_layout() _fig return diff --git a/pyproject.toml b/pyproject.toml index f859028..414b6dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "anywidget-vector" -version = "0.2.8" +version = "0.3.0" description = "Interactive 3D vector visualization with query UI for vector databases" readme = "README.md" license = "Apache-2.0" @@ -26,10 +26,10 @@ dependencies = [ [project.optional-dependencies] dev = [ - "prek>=0.3.2", + "prek>=0.3", "pytest>=9.0.2", "ruff>=0.15", - "ty>=0.0.20", + "ty>=0.0", "marimo>=0.20", ] pandas = ["pandas>=2.0"] diff --git a/src/anywidget_vector/ui/__init__.py b/src/anywidget_vector/ui/__init__.py index 493f1fd..71d577d 100644 --- a/src/anywidget_vector/ui/__init__.py +++ b/src/anywidget_vector/ui/__init__.py @@ -122,16 +122,47 @@ def get_esm() -> str: function render({{ model, el }}) {{ const wrapper = document.createElement("div"); wrapper.className = "avs-wrapper"; - if (model.get("dark_mode")) wrapper.classList.add("avs-dark"); wrapper.style.width = model.get("width") + "px"; wrapper.style.height = model.get("height") + "px"; el.appendChild(wrapper); - model.on("change:dark_mode", () => {{ - const dark = model.get("dark_mode"); + // Auto-detect host theme (marimo uses Tailwind class="dark" on ) + function detectHostDark() {{ + const html = document.documentElement; + if (html.classList.contains("dark")) return true; + if (html.dataset.theme === "dark") return true; + if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) return true; + return false; + }} + + function applyTheme(dark) {{ wrapper.classList.toggle("avs-dark", dark); - model.set("background", dark ? "#1a1a2e" : "#fafafa"); + model.set("background", dark ? "#181c1a" : "#ffffff"); model.save_changes(); + }} + + // Detect if we're inside a themed host (marimo, jupyter, etc.) + const hasHostTheme = !!el.getRootNode()?.host?.tagName?.startsWith("MARIMO-"); + if (hasHostTheme) {{ + wrapper.classList.add("avs-auto-theme"); + model.set("dark_mode", detectHostDark()); + applyTheme(detectHostDark()); + // Watch host for live theme changes + const themeObserver = new MutationObserver(() => {{ + const dark = detectHostDark(); + if (model.get("dark_mode") !== dark) {{ + model.set("dark_mode", dark); + applyTheme(dark); + }} + }}); + themeObserver.observe(document.documentElement, {{ attributes: true, attributeFilter: ["class", "data-theme"] }}); + }} else {{ + applyTheme(model.get("dark_mode")); + }} + + // Manual toggle from settings panel still works + model.on("change:dark_mode", () => {{ + applyTheme(model.get("dark_mode")); }}); let sidebar = null; diff --git a/src/anywidget_vector/ui/canvas.js b/src/anywidget_vector/ui/canvas.js index b7e4efd..8ec5863 100644 --- a/src/anywidget_vector/ui/canvas.js +++ b/src/anywidget_vector/ui/canvas.js @@ -175,12 +175,20 @@ export function createCanvas(model, container, callbacks) { const points = model.get("points") || []; if (points.length === 0) return; + // Auto-scale point sizes relative to data extent + const box = new THREE.Box3(); + points.forEach(p => box.expandByPoint(new THREE.Vector3(p.x ?? 0, p.y ?? 0, p.z ?? 0))); + const dataSize = box.getSize(new THREE.Vector3()).length() || 1; + const scaleFactor = dataSize / 10; + const rawRange = model.get("size_range") || [0.02, 0.06]; + const sizeRange = [rawRange[0] * scaleFactor, rawRange[1] * scaleFactor]; + const opts = { colorField: model.get("color_field"), colorScale: model.get("color_scale") || "viridis", colorDomain: model.get("color_domain"), sizeField: model.get("size_field"), - sizeRange: model.get("size_range") || [0.02, 0.1], + sizeRange: sizeRange, shapeField: model.get("shape_field"), shapeMap: model.get("shape_map") || {}, }; @@ -460,7 +468,7 @@ export function createCanvas(model, container, callbacks) { const size = box.getSize(new THREE.Vector3()).length(); const distance = size / (2 * Math.tan(Math.PI * camera.fov / 360)); controls.target.copy(center); - camera.position.copy(center.clone().add(new THREE.Vector3(0, 0, distance * 1.2))); + camera.position.copy(center.clone().add(new THREE.Vector3(0, 0, distance * 0.55))); camera.near = Math.max(0.01, distance * 0.001); camera.far = Math.max(1000, distance * 10); camera.updateProjectionMatrix(); @@ -485,8 +493,8 @@ export function createCanvas(model, container, callbacks) { zoomOutBtn.innerHTML = ICONS.zoomOut; zoomOutBtn.title = "Zoom out"; zoomOutBtn.addEventListener("click", () => { - const dir = camera.position.clone().sub(controls.target).normalize(); - camera.position.add(dir.multiplyScalar(0.5)); + const offset = camera.position.clone().sub(controls.target); + camera.position.add(offset.multiplyScalar(0.3)); controls.update(); }); diff --git a/src/anywidget_vector/ui/settings.js b/src/anywidget_vector/ui/settings.js index 15525aa..64504ae 100644 --- a/src/anywidget_vector/ui/settings.js +++ b/src/anywidget_vector/ui/settings.js @@ -21,8 +21,9 @@ export function createSettingsPanel(model, callbacks) { header.appendChild(closeBtn); inner.appendChild(header); - // Dark mode toggle + // Dark mode toggle (hidden when host provides theme) const themeGroup = createFormGroup("Theme"); + themeGroup.classList.add("avs-theme-toggle"); const toggle = document.createElement("label"); toggle.className = "avs-toggle"; const checkbox = document.createElement("input"); diff --git a/src/anywidget_vector/ui/sidebar.js b/src/anywidget_vector/ui/sidebar.js index 83e671d..4526d8f 100644 --- a/src/anywidget_vector/ui/sidebar.js +++ b/src/anywidget_vector/ui/sidebar.js @@ -114,6 +114,22 @@ export function createSidebar(model, callbacks) { addStatRow(dimContent, "X range", rangeStr(xs)); addStatRow(dimContent, "Y range", rangeStr(ys)); addStatRow(dimContent, "Z range", rangeStr(zs)); + + // Visual mapping dimensions + const colorField = model.get("color_field"); + const sizeField = model.get("size_field"); + const shapeField = model.get("shape_field"); + if (colorField) addStatRow(dimContent, "Color", colorField); + if (sizeField) { + const sizeRange = model.get("size_range") || [0.02, 0.1]; + addStatRow(dimContent, "Size", sizeField + " [" + sizeRange[0] + "," + sizeRange[1] + "]"); + } + if (shapeField) { + const shapeMap = model.get("shape_map") || {}; + const shapes = Object.values(shapeMap); + const unique = [...new Set(shapes)]; + addStatRow(dimContent, "Shape", shapeField + " (" + unique.length + " shapes)"); + } } function updateClusters() { @@ -159,6 +175,107 @@ export function createSidebar(model, callbacks) { }); } + // === Distance Section === + const distSection = document.createElement("div"); + distSection.className = "avs-sidebar-section"; + + const distHeader = document.createElement("div"); + distHeader.className = "avs-section-header"; + distHeader.textContent = "Distance"; + distSection.appendChild(distHeader); + + const distContent = document.createElement("div"); + distContent.className = "avs-dimension-content"; + + // Metric selector + const metricGroup = document.createElement("div"); + metricGroup.className = "avs-form-group"; + const metricLabel = document.createElement("label"); + metricLabel.className = "avs-label"; + metricLabel.textContent = "Metric"; + metricGroup.appendChild(metricLabel); + const metricSelect = document.createElement("select"); + metricSelect.className = "avs-select"; + metricSelect.style.width = "100%"; + ["euclidean", "cosine", "manhattan", "dot_product"].forEach(m => { + const opt = document.createElement("option"); + opt.value = m; + opt.textContent = m; + opt.selected = m === (model.get("distance_metric") || "euclidean"); + metricSelect.appendChild(opt); + }); + metricSelect.addEventListener("change", () => { + model.set("distance_metric", metricSelect.value); + model.save_changes(); + }); + metricGroup.appendChild(metricSelect); + distContent.appendChild(metricGroup); + + // K neighbors + const kGroup = document.createElement("div"); + kGroup.className = "avs-form-group"; + const kLabel = document.createElement("label"); + kLabel.className = "avs-label"; + kLabel.textContent = "K neighbors"; + kGroup.appendChild(kLabel); + const kInput = document.createElement("input"); + kInput.type = "number"; + kInput.className = "avs-input"; + kInput.min = "0"; + kInput.max = "50"; + kInput.value = model.get("k_neighbors") || 0; + kInput.addEventListener("input", () => { + const k = parseInt(kInput.value, 10) || 0; + model.set("k_neighbors", k); + model.set("show_connections", k > 0); + model.save_changes(); + }); + kGroup.appendChild(kInput); + distContent.appendChild(kGroup); + + // Distance info (updates when points are selected) + const distInfo = document.createElement("div"); + distInfo.className = "avs-note"; + distInfo.textContent = "Select a point to see distances"; + distContent.appendChild(distInfo); + + distSection.appendChild(distContent); + inner.appendChild(distSection); + + function updateDistanceInfo() { + const selected = model.get("selected_points") || []; + if (selected.length === 0) { + distInfo.textContent = "Select a point to see distances"; + return; + } + const points = model.get("points") || []; + const ref = points.find(p => p.id === selected[0]); + if (!ref) return; + + const metric = metricSelect.value; + const others = points.filter(p => p.id !== ref.id); + + function dist(a, b) { + const dx = (a.x ?? 0) - (b.x ?? 0), dy = (a.y ?? 0) - (b.y ?? 0), dz = (a.z ?? 0) - (b.z ?? 0); + if (metric === "manhattan") return Math.abs(dx) + Math.abs(dy) + Math.abs(dz); + if (metric === "cosine") { + const dot = (a.x??0)*(b.x??0) + (a.y??0)*(b.y??0) + (a.z??0)*(b.z??0); + const ma = Math.sqrt((a.x??0)**2 + (a.y??0)**2 + (a.z??0)**2); + const mb = Math.sqrt((b.x??0)**2 + (b.y??0)**2 + (b.z??0)**2); + return (ma === 0 || mb === 0) ? 1 : 1 - dot / (ma * mb); + } + if (metric === "dot_product") return -((a.x??0)*(b.x??0) + (a.y??0)*(b.y??0) + (a.z??0)*(b.z??0)); + return Math.sqrt(dx*dx + dy*dy + dz*dz); + } + + const sorted = others.map(p => ({ id: p.id, label: p.label || p.id, d: dist(ref, p) })) + .sort((a, b) => a.d - b.d).slice(0, 5); + + distInfo.innerHTML = "" + escapeHtml(ref.label || ref.id) + "
" + + sorted.map(n => '' + escapeHtml(n.label) + + ' ' + n.d.toFixed(2) + "").join("
"); + } + // === Event Bindings === model.on("change:points", () => { updateDimensions(); @@ -167,6 +284,11 @@ export function createSidebar(model, callbacks) { model.on("change:color_field", updateClusters); model.on("change:backend_config", updateCollections); model.on("change:backend", updateCollections); + model.on("change:selected_points", updateDistanceInfo); + model.on("change:distance_metric", () => { + metricSelect.value = model.get("distance_metric") || "euclidean"; + updateDistanceInfo(); + }); // Initial render updateCollections(); diff --git a/src/anywidget_vector/ui/styles.css b/src/anywidget_vector/ui/styles.css index efa8a14..90f4f1b 100644 --- a/src/anywidget_vector/ui/styles.css +++ b/src/anywidget_vector/ui/styles.css @@ -1,14 +1,14 @@ /* === CSS Variables === */ -/* Light mode (default) */ +/* Light mode (default, matches marimo light) */ .avs-wrapper { --avs-bg: #ffffff; --avs-surface: #f8f9fa; - --avs-border: #e5e7eb; - --avs-text: #111827; + --avs-border: #e2e8f0; + --avs-text: #0f172a; --avs-text-muted: #6b7280; - --avs-primary: #6366f1; - --avs-primary-hover: #4f46e5; + --avs-primary: #0880ea; + --avs-primary-hover: #0670d0; --avs-success: #10b981; --avs-warning: #f59e0b; --avs-error: #ef4444; @@ -23,15 +23,15 @@ flex-direction: column; } -/* Dark mode */ +/* Dark mode (matches marimo dark) */ .avs-wrapper.avs-dark { - --avs-bg: #1a1a2e; - --avs-surface: #252542; - --avs-border: #3a3a5c; - --avs-text: #e0e0e0; - --avs-text-muted: #888; - --avs-primary: #6366f1; - --avs-primary-hover: #818cf8; + --avs-bg: #181c1a; + --avs-surface: #252927; + --avs-border: #3b403e; + --avs-text: #eceeed; + --avs-text-muted: #9ca3af; + --avs-primary: #28879f; + --avs-primary-hover: #1d5b6a; } /* === Main Layout === */ @@ -601,3 +601,8 @@ font-size: 13px; color: var(--avs-text); } + +/* Hide dark mode toggle when host provides theme */ +.avs-wrapper.avs-auto-theme .avs-theme-toggle { + display: none; +} diff --git a/src/anywidget_vector/widget.py b/src/anywidget_vector/widget.py index 4b33a7e..2005d3c 100644 --- a/src/anywidget_vector/widget.py +++ b/src/anywidget_vector/widget.py @@ -54,7 +54,7 @@ class VectorSpace(anywidget.AnyWidget): # === Size Mapping === size_field = traitlets.Unicode(default_value=None, allow_none=True).tag(sync=True) - size_range = traitlets.List(default_value=[0.02, 0.1]).tag(sync=True) + size_range = traitlets.List(default_value=[0.02, 0.06]).tag(sync=True) # === Shape Mapping === shape_field = traitlets.Unicode(default_value=None, allow_none=True).tag(sync=True) diff --git a/uv.lock b/uv.lock index 2160936..9d5ea4a 100644 --- a/uv.lock +++ b/uv.lock @@ -50,7 +50,7 @@ wheels = [ [[package]] name = "anywidget-vector" -version = "0.2.6" +version = "0.3.0" source = { editable = "." } dependencies = [ { name = "anywidget" }, @@ -123,13 +123,13 @@ requires-dist = [ { name = "pandas", marker = "extra == 'pandas'", specifier = ">=2.0" }, { name = "pinecone-client", marker = "extra == 'all'", specifier = ">=6.0" }, { name = "pinecone-client", marker = "extra == 'pinecone'", specifier = ">=3.0" }, - { name = "prek", marker = "extra == 'dev'", specifier = ">=0.3.2" }, + { name = "prek", marker = "extra == 'dev'", specifier = ">=0.3" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.2" }, { name = "qdrant-client", marker = "extra == 'all'", specifier = ">=1" }, { name = "qdrant-client", marker = "extra == 'qdrant'", specifier = ">=1.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15" }, { name = "scikit-learn", marker = "extra == 'projection'", specifier = ">=1.3" }, - { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.20" }, + { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0" }, { name = "umap-learn", marker = "extra == 'projection'", specifier = ">=0.5" }, { name = "weaviate-client", marker = "extra == 'all'", specifier = ">=4.20" }, { name = "weaviate-client", marker = "extra == 'weaviate'", specifier = ">=4.0" },