From 14d325fa37d52471970fe79db82a0b2540de58e4 Mon Sep 17 00:00:00 2001 From: Tabassum Kakar Date: Wed, 2 Sep 2026 13:59:54 -0400 Subject: [PATCH 1/4] Fixed the encoding error --- src/vitessce/utils.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/vitessce/utils.py b/src/vitessce/utils.py index bbb58c12..dc2b46bd 100644 --- a/src/vitessce/utils.py +++ b/src/vitessce/utils.py @@ -35,7 +35,7 @@ def get_initial_coordination_scope_name(dataset_uid, data_type, i=None): return f"{prefix}{0 if i is None else i}" -def make_ids_csv_data_url(ids): +def make_ids_csv_data_url(ids, for_web_app=False): """ Build a `data:` URL containing a small inline CSV with a single `id` column, given a list of observation IDs (e.g. segment IDs). @@ -56,10 +56,13 @@ def make_ids_csv_data_url(ids): writer = csv.writer(buf) writer.writerow(["id"]) writer.writerows([[i] for i in ids]) - return f"data:text/csv,{quote(buf.getvalue())}" + encoded = quote(buf.getvalue()) + if for_web_app: + encoded = quote(encoded, safe="") + return f"data:text/csv,{encoded}" -def make_colors_csv_data_url(id_to_color): +def make_colors_csv_data_url(id_to_color, for_web_app=False): """ Build a `data:` URL containing a small inline CSV with `id` and `color` columns, given a dict mapping observation ID to a color @@ -80,4 +83,7 @@ def make_colors_csv_data_url(id_to_color): writer = csv.writer(buf) writer.writerow(["id", "color"]) writer.writerows(id_to_color.items()) - return f"data:text/csv,{quote(buf.getvalue())}" + encoded = quote(buf.getvalue()) + if for_web_app: + encoded = quote(encoded, safe="") + return f"data:text/csv,{encoded}" From d9e7ee4018ebe3c22676dd7ea68a792c374c2a6a Mon Sep 17 00:00:00 2001 From: Tabassum Kakar Date: Wed, 2 Sep 2026 16:08:29 -0400 Subject: [PATCH 2/4] Updated notebooks --- ...et_neuroglancer_precomputed-segments.ipynb | 568 ++++++++++++++++++ .../widget_neuroglancer_precomputed.ipynb | 135 +---- uv.lock | 2 +- 3 files changed, 575 insertions(+), 130 deletions(-) create mode 100644 docs/notebooks/widget_neuroglancer_precomputed-segments.ipynb diff --git a/docs/notebooks/widget_neuroglancer_precomputed-segments.ipynb b/docs/notebooks/widget_neuroglancer_precomputed-segments.ipynb new file mode 100644 index 00000000..2ee21180 --- /dev/null +++ b/docs/notebooks/widget_neuroglancer_precomputed-segments.ipynb @@ -0,0 +1,568 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "08d489b0", + "metadata": {}, + "source": [ + "# Vitessce Widget Tutorial" + ] + }, + { + "cell_type": "markdown", + "id": "d6dda7d0", + "metadata": {}, + "source": [ + "# Example usage of Neuroglancer precomputed segmentations: Alternate way to load segments: providing an explicit array\n", + "\n", + "This notebook demonstrates `ObsSegmentationsNgPrecomputedWrapper` and `ObsPointsNgAnnotationsWrapper`, which wrap Neuroglancer precomputed segmentation/mesh data and point-annotation data (e.g. as produced by the [tissue-map-tools](https://github.com/hms-dbmi/tissue-map-tools) library) for use with the `neuroglancer` and `layerControllerBeta` views.\n", + "\n", + "Instead of pointing at a remote `obsSets` CSV (as in section 1 above), you can select and color a specific, known set of segments directly from a Python list/dict. Native Neuroglancer itself supports specifying segments this way, as a plain array (\"segments\": [...]) in its own JSON state — this section replicates that same capability through Vitessce's own coordination system.\n", + "This adds two files instead of one `obsSets.csv`\n", + "- `obsFeatureMatrix.csv` -- just the segment IDs, defining which observations exist.\n", + "- `obsColors.csv` -- an explicit `id -> color` mapping. -- optional\n", + "\n", + "The `segmentationChannel` coordination also changes slightly: `obsColorEncoding` is set to `'obsColors'` (using the explicit colors above) instead of relying on cluster-based coloring." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "6d673a85", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from vitessce import (\n", + " VitessceConfig,\n", + " CoordinationLevel as CL,\n", + " get_initial_coordination_scope_prefix,\n", + " ObsSegmentationsNgPrecomputedWrapper,\n", + " CsvWrapper,\n", + " make_ids_csv_data_url, \n", + " make_colors_csv_data_url\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "8c5bcdc5", + "metadata": {}, + "source": [ + "## 1. Configure Vitessce" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ac0fb259", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vc = VitessceConfig(schema_version=\"1.0.17\", name=\"Neuroglancer precomputed example\")\n", + "dataset = vc.add_dataset(\"Melanoma\")\n", + "\n", + "# A Neuroglancer precomputed segmentation meshes directory.\n", + "# fileUid here must match the value used below in link_views_by_dict's\n", + "# segmentationLayer coordination.\n", + "\n", + "dataset.add_object(ObsSegmentationsNgPrecomputedWrapper(\n", + " data_url=\"https://data-2.vitessce.io/data/sorger/melanoma_meshes\",\n", + " coordination_values={\"fileUid\": \"segmentation\"},\n", + "))\n", + "\n", + "segment_ids = [612, 3351, 4328, 6531, 8446]\n", + "segment_colors = {\n", + " 612: '#d74242',\n", + " 3351: '#b9d742',\n", + " 4328: '#42d77d',\n", + " 6531: '#427dd7',\n", + " 8446: '#b942d7',\n", + "}\n", + "\n", + "# IDs only -- defines which observations exist for this obsType.\n", + "dataset.add_object(CsvWrapper(\n", + " csv_url=make_ids_csv_data_url(segment_ids),\n", + " data_type='obsFeatureMatrix',\n", + " coordination_values={\n", + " 'obsType': 'cell', 'featureType': 'feature', 'featureValueType': 'value',\n", + " },\n", + "))\n", + "\n", + "# Explicit id -> color mapping.\n", + "dataset.add_object(CsvWrapper(\n", + " csv_url=make_colors_csv_data_url(segment_colors),\n", + " data_type='obsColors',\n", + " options={'obsIndex': 'id', 'obsColors': 'color'},\n", + " coordination_values={'obsType': 'cell'},\n", + "))" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "6babd185", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ng_view = vc.add_view(\"neuroglancer\", dataset=dataset).set_props(\n", + " initialNgCameraState={\n", + " 'position': [49.5, 1000.5, 5209.5],\n", + " 'projectionScale': 1024,\n", + " 'projectionOrientation': [\n", + " -0.636204183101654,\n", + " -0.5028395652770996,\n", + " 0.5443811416625977,\n", + " 0.2145828753709793,\n", + " ],\n", + " },\n", + ")\n", + "lc_view = vc.add_view(\"layerControllerBeta\", dataset=dataset)\n", + "\n", + "vc.layout(ng_view | lc_view )" + ] + }, + { + "cell_type": "markdown", + "id": "c8aaa012", + "metadata": { + "tags": [] + }, + "source": [ + "## 2. Coordinate the views\n", + "\n", + "Two separate `link_views_by_dict` calls are needed:\n", + "- A plain (non-meta) link for shared spatial rendering mode and camera position/rotation.\n", + "- A multi-level (meta) link for the segmentation layer + channel, mirroring the shape\n", + " `obsSegmentations.ng-precomputed` files require to resolve correctly." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "648ca751", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vc.link_views_by_dict([ng_view, lc_view], {\n", + " \"spatialRenderingMode\": \"3D\",\n", + " \"spatialZoom\": 0,\n", + " \"spatialTargetX\": 0,\n", + " \"spatialTargetY\": 0,\n", + " \"spatialTargetZ\": 0,\n", + " \"spatialRotationX\": 0,\n", + " \"spatialRotationY\": 0,\n", + " \"spatialRotationOrbit\": 0,\n", + "}, meta=False)\n", + "\n", + "vc.link_views_by_dict([ng_view, lc_view], {\n", + " 'segmentationLayer': CL([{\n", + " 'fileUid': 'segmentation',\n", + " 'spatialLayerOpacity': 1,\n", + " 'spatialLayerVisible': True,\n", + " 'segmentationChannel': CL([{\n", + " 'obsType': 'cell',\n", + " 'featureType': 'feature',\n", + " 'featureValueType': 'value',\n", + " 'spatialChannelVisible': True,\n", + " 'obsColorEncoding': 'obsColors',\n", + " }]),\n", + " }]),\n", + "}, scope_prefix=get_initial_coordination_scope_prefix('A', 'obsSegmentations'))" + ] + }, + { + "cell_type": "markdown", + "id": "10210740", + "metadata": {}, + "source": [ + "## 3. Create the Vitessce widget" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "f562bfc8", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e7ec9b8ec84d4f7aaeb9f06e159fdc37", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vw = vc.widget(custom_js_url='http://localhost:9001/packages/main/dev/dist/index.js')\n", + "vw" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4ae4fc7f-76eb-4b3f-9623-16a220174f48", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": { + "7944f55009c64754a407dfff3408ca92": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "c9218e280b5e411aacd04560cbb1ea89": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "vitessce.widget.VitessceWidget", + "_config": { + "coordinationSpace": { + "dataset": { + "A": "A" + }, + "fileUid": { + "init_A_obsSegmentations_0": "segmentation" + }, + "metaCoordinationScopes": { + "init_A_obsSegmentations_0": { + "segmentationLayer": [ + "init_A_obsSegmentations_0" + ] + } + }, + "metaCoordinationScopesBy": { + "init_A_obsSegmentations_0": { + "segmentationChannel": { + "obsType": { + "init_A_obsSegmentations_0": "init_A_obsSegmentations_0" + }, + "spatialChannelVisible": { + "init_A_obsSegmentations_0": "init_A_obsSegmentations_0" + } + }, + "segmentationLayer": { + "fileUid": { + "init_A_obsSegmentations_0": "init_A_obsSegmentations_0" + }, + "segmentationChannel": { + "init_A_obsSegmentations_0": [ + "init_A_obsSegmentations_0" + ] + }, + "spatialLayerOpacity": { + "init_A_obsSegmentations_0": "init_A_obsSegmentations_0" + }, + "spatialLayerVisible": { + "init_A_obsSegmentations_0": "init_A_obsSegmentations_0" + }, + "spatialTargetResolution": { + "init_A_obsSegmentations_0": "init_A_obsSegmentations_0" + } + } + } + }, + "obsType": { + "init_A_obsSegmentations_0": "cell" + }, + "segmentationChannel": { + "init_A_obsSegmentations_0": "__dummy__" + }, + "segmentationLayer": { + "init_A_obsSegmentations_0": "__dummy__" + }, + "spatialChannelVisible": { + "init_A_obsSegmentations_0": true + }, + "spatialLayerOpacity": { + "init_A_obsSegmentations_0": 1 + }, + "spatialLayerVisible": { + "init_A_obsSegmentations_0": true + }, + "spatialRenderingMode": { + "A": "3D" + }, + "spatialRotationOrbit": { + "A": 0 + }, + "spatialRotationX": { + "A": 0 + }, + "spatialRotationY": { + "A": 0 + }, + "spatialTargetResolution": { + "init_A_obsSegmentations_0": null + }, + "spatialTargetX": { + "A": 0 + }, + "spatialTargetY": { + "A": 0 + }, + "spatialTargetZ": { + "A": 0 + }, + "spatialZoom": { + "A": 0 + } + }, + "datasets": [ + { + "files": [ + { + "coordinationValues": { + "fileUid": "segmentation" + }, + "fileType": "obsSegmentations.ng-precomputed", + "url": "https://data-2.vitessce.io/data/sorger/melanoma_meshes" + }, + { + "coordinationValues": { + "obsType": "cell" + }, + "fileType": "obsSets.csv", + "options": { + "obsIndex": "id", + "obsSets": [ + { + "column": "cluster", + "name": "Clusters" + } + ] + }, + "url": "https://storage.googleapis.com/vitessce-demo-data/neuroglancer-march-2025/melanoma_with_embedding_filtered_ids.csv" + } + ], + "name": "Melanoma", + "uid": "A" + } + ], + "description": "", + "initStrategy": "auto", + "layout": [ + { + "component": "neuroglancer", + "coordinationScopes": { + "dataset": "A", + "metaCoordinationScopes": [ + "init_A_obsSegmentations_0" + ], + "metaCoordinationScopesBy": [ + "init_A_obsSegmentations_0" + ], + "spatialRenderingMode": "A", + "spatialRotationOrbit": "A", + "spatialRotationX": "A", + "spatialRotationY": "A", + "spatialTargetX": "A", + "spatialTargetY": "A", + "spatialTargetZ": "A", + "spatialZoom": "A" + }, + "coordinationScopesBy": {}, + "h": 12, + "w": 3, + "x": 0, + "y": 0 + }, + { + "component": "layerControllerBeta", + "coordinationScopes": { + "dataset": "A", + "metaCoordinationScopes": [ + "init_A_obsSegmentations_0" + ], + "metaCoordinationScopesBy": [ + "init_A_obsSegmentations_0" + ], + "spatialRenderingMode": "A", + "spatialRotationOrbit": "A", + "spatialRotationX": "A", + "spatialRotationY": "A", + "spatialTargetX": "A", + "spatialTargetY": "A", + "spatialTargetZ": "A", + "spatialZoom": "A" + }, + "coordinationScopesBy": {}, + "h": 12, + "w": 3, + "x": 3, + "y": 0 + }, + { + "component": "obsSets", + "coordinationScopes": { + "dataset": "A" + }, + "h": 12, + "w": 6, + "x": 6, + "y": 0 + } + ], + "name": "Neuroglancer precomputed example", + "version": "1.0.17" + }, + "_dom_classes": [], + "_esm": "\nlet importWithMap;\ntry {\n importWithMap = (await import('https://unpkg.com/dynamic-importmap@0.1.0')).importWithMap;\n} catch(e) {\n console.warn(\"Import of dynamic-importmap failed, trying fallback.\");\n importWithMap = (await import('https://cdn.vitessce.io/dynamic-importmap@0.1.0/dist/index.js')).importWithMap;\n}\n\nconst successfulImportMap = {\n imports: {\n\n },\n};\nconst importMap = {\n imports: {\n \"react\": \"https://esm.sh/react@18.2.0?dev\",\n \"react-dom\": \"https://esm.sh/react-dom@18.2.0?dev\",\n \"react-dom/client\": \"https://esm.sh/react-dom@18.2.0/client?dev\",\n },\n};\nconst fallbackImportMap = {\n imports: {\n \"react\": \"https://cdn.vitessce.io/react@18.2.0/index.js\",\n \"react-dom\": \"https://cdn.vitessce.io/react-dom@18.2.0/index.js\",\n \"react-dom/client\": \"https://cdn.vitessce.io/react-dom@18.2.0/es2022/client.mjs\",\n // Replaced with version-specific URL below.\n \"vitessce\": \"https://cdn.vitessce.io/vitessce@VERSION/dist/index.min.js\",\n },\n};\n/*\nconst fallbackDevImportMap = {\n imports: {\n \"react\": \"https://cdn.vitessce.io/react@18.2.0/index_dev.js\",\n \"react-dom\": \"https://cdn.vitessce.io/react-dom@18.2.0/index_dev.js\",\n \"react-dom/client\": \"https://cdn.vitessce.io/react-dom@18.2.0/es2022/client.development.mjs\",\n // Replaced with version-specific URL below.\n \"vitessce\": \"https://cdn.vitessce.io/@vitessce/dev@VERSION/dist/index.js\",\n },\n};\n*/\n\nasync function importWithMapAndFallback(moduleName, importMap, fallbackMap) {\n let result = null;\n if (!fallbackMap) {\n // fallbackMap is null, user may have provided custom JS URL.\n result = await importWithMap(moduleName, {\n imports: {\n ...importMap.imports,\n ...successfulImportMap.imports,\n },\n });\n successfulImportMap.imports[moduleName] = importMap.imports[moduleName];\n } else {\n try {\n result = await importWithMap(moduleName, {\n imports: {\n ...importMap.imports,\n ...successfulImportMap.imports,\n },\n });\n successfulImportMap.imports[moduleName] = importMap.imports[moduleName];\n } catch (e) {\n console.warn(`Importing ${moduleName} failed with importMap`, importMap, \"trying fallback\", fallbackMap, successfulImportMap);\n result = await importWithMap(moduleName, {\n imports: {\n ...fallbackMap.imports,\n ...successfulImportMap.imports,\n },\n });\n successfulImportMap.imports[moduleName] = fallbackMap.imports[moduleName];\n }\n }\n return result;\n}\n\n\nconst React = await importWithMapAndFallback(\"react\", importMap, fallbackImportMap);\nconst { createRoot } = await importWithMapAndFallback(\"react-dom/client\", importMap, fallbackImportMap);\n\nconst e = React.createElement;\n\nfunction isAbsoluteUrl(s) {\n return s?.startsWith('http://') || s?.startsWith('https://');\n}\nconst WORKSPACES_URL_KEYWORD = 'https://workspaces-pt';\nconst OPTIONS_URL_KEYS = ['offsetsUrl', 'refSpecUrl'];\nconst prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;\n// The jupyter server may be running through a proxy,\n// which means that the client needs to prepend the part of the URL before /proxy/8000 such as\n// https://hub.gke2.mybinder.org/user/vitessce-vitessce-python-swi31vcv/proxy/8000/A/0/cells\n// For workspaces: https://workspaces-pt.hubmapconsortium.org/passthrough/HOSTNAME/PORT/ADDITIONAL_PATH_INFO?QUERY_PARAMS=HELLO_WORLD\nfunction prependBaseUrl(config, proxy, hasHostName) {\n if (!proxy || hasHostName) {\n return config;\n }\n const { origin, pathname } = new URL(window.location.href);\n const isInWorkspaces = origin.startsWith(WORKSPACES_URL_KEYWORD);\n const jupyterLabConfigEl = document.getElementById('jupyter-config-data');\n\n let baseUrl;\n if (isInWorkspaces) {\n const pathSegments = pathname.split('/');\n const passthroughIndex = pathSegments.indexOf('passthrough');\n if (passthroughIndex !== -1) {\n baseUrl = pathSegments.slice(0, passthroughIndex + 3).join('/');\n baseUrl += '/';\n }\n } else if (jupyterLabConfigEl) {\n // This is jupyter lab\n baseUrl = JSON.parse(jupyterLabConfigEl.textContent || '').baseUrl;\n } else {\n // This is jupyter notebook\n baseUrl = document.getElementsByTagName('body')[0].getAttribute('data-base-url');\n }\n return {\n ...config,\n datasets: config.datasets.map(d => ({\n ...d,\n files: d.files.map(f => {\n const updatedFileDef = { ...f };\n if (f.url && !isAbsoluteUrl(f.url) ) {\n // Update the main file URL if necessary.\n updatedFileDef.url = `${origin}${baseUrl}${f.url}`;\n }\n if (f.options) {\n // Update any urls within the options object\n const updatedOptions = { ...f.options };\n OPTIONS_URL_KEYS.forEach(key => {\n const optionValue = updatedOptions[key];\n if (optionValue && !isAbsoluteUrl(optionValue)) {\n updatedOptions[key] = `${origin}${baseUrl}${optionValue}`;\n }\n });\n\n // Update image URLs if they exist\n if ('images' in f.options && Array.isArray(f.options.images)) {\n const updatedImages = f.options.images.map(image => {\n const updatedImage = { ...image };\n\n if (image.url && !isAbsoluteUrl(image.url)) {\n updatedImage.url = `${origin}${baseUrl}${image.url}`;\n }\n\n const metadata = { ...image.metadata };\n if (metadata?.omeTiffOffsetsUrl && !isAbsoluteUrl(metadata.omeTiffOffsetsUrl)) {\n metadata.omeTiffOffsetsUrl = `${origin}${baseUrl}${metadata.omeTiffOffsetsUrl}`;\n }\n\n updatedImage.metadata = metadata;\n\n return updatedImage;\n });\n\n updatedOptions.images = updatedImages;\n }\n updatedFileDef.options = updatedOptions;\n }\n return updatedFileDef;\n }),\n })),\n };\n}\n\n// Fallback UUID for non-secure (http://) contexts where crypto.randomUUID is unavailable.\n// Reference: https://stackoverflow.com/a/8809472\nfunction generateId() {\n let d = new Date().getTime(),\n d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0;\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {\n let r = Math.random() * 16;\n if (d > 0) {\n r = (d + r) % 16 | 0;\n d = Math.floor(d / 16);\n } else {\n r = (d2 + r) % 16 | 0;\n d2 = Math.floor(d2 / 16);\n }\n return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);\n });\n}\n\n// Custom invoke matching the anywidget-command protocol implemented on the Python side.\nfunction invoke(model, name, msg, buffers, signal) {\n const id = generateId();\n const abortSignal = signal ?? AbortSignal.timeout(30000);\n return new Promise((resolve, reject) => {\n if (abortSignal.aborted) { reject(abortSignal.reason); return; }\n abortSignal.addEventListener(\"abort\", () => {\n model.off(\"msg:custom\", handler);\n reject(abortSignal.reason);\n });\n function handler(responseMsg, responseBuffers) {\n if (!responseMsg || responseMsg.id !== id) return;\n model.off(\"msg:custom\", handler);\n resolve([responseMsg.response, responseBuffers]);\n }\n model.on(\"msg:custom\", handler);\n model.send({ id, kind: \"anywidget-command\", name, msg }, undefined, buffers ?? []);\n });\n}\n\n\nasync function render(view) {\n const cssUid = view.model.get('uid');\n const jsDevMode = view.model.get('js_dev_mode');\n const jsPackageVersion = view.model.get('js_package_version');\n const customJsUrl = view.model.get('custom_js_url');\n const pluginEsmArr = view.model.get('plugin_esm');\n const remountOnUidChange = view.model.get('remount_on_uid_change');\n const storeUrls = view.model.get('store_urls');\n const invokeTimeout = view.model.get('invoke_timeout');\n const invokeBatched = view.model.get('invoke_batched');\n const preventScroll = view.model.get('prevent_scroll');\n\n const pageMode = view.model.get('page_mode');\n const pageEsm = view.model.get('page_esm');\n\n const pkgName = (jsDevMode ? \"@vitessce/dev\" : \"vitessce\");\n\n const hasCustomJsUrl = customJsUrl.length > 0;\n\n importMap.imports[\"vitessce\"] = (hasCustomJsUrl\n ? customJsUrl\n : `https://unpkg.com/${pkgName}@${jsPackageVersion}`\n );\n let fallbackImportMapToUse = null;\n if (!hasCustomJsUrl) {\n fallbackImportMapToUse = fallbackImportMap;\n if (jsDevMode) {\n fallbackImportMapToUse.imports[\"vitessce\"] = `https://cdn.vitessce.io/vitessce@${jsPackageVersion}/dist/index.min.js`;\n } else {\n fallbackImportMapToUse.imports[\"vitessce\"] = `https://cdn.vitessce.io/@vitessce/dev@${jsPackageVersion}/dist/index.js`;\n }\n }\n\n const {\n Vitessce,\n PluginFileType,\n PluginViewType,\n PluginCoordinationType,\n PluginJointFileType,\n PluginAsyncFunction,\n z,\n useCoordination,\n usePageModeView,\n useGridItemSize,\n // TODO: names and function signatures are subject to change for the following functions\n // Reference: https://github.com/keller-mark/use-coordination/issues/37#issuecomment-1946226827\n useComplexCoordination,\n useMultiCoordinationScopesNonNull,\n useMultiCoordinationScopesSecondaryNonNull,\n useComplexCoordinationSecondary,\n useCoordinationScopes,\n useCoordinationScopesBy,\n } = await importWithMapAndFallback(\"vitessce\", importMap, fallbackImportMapToUse);\n\n let pluginViewTypes = [];\n let pluginCoordinationTypes = [];\n let pluginFileTypes = [];\n let pluginJointFileTypes = [];\n let pluginAsyncFunctions = [];\n\n let pending = [];\n let batchId = 0;\n\n async function processBatch(prevPendingArr) {\n const [dataArr, buffersArr] = await invoke(\n view.model,\n \"_zarr_get_multi\",\n prevPendingArr.map(d => d.params),\n null,\n AbortSignal.timeout(invokeTimeout),\n );\n prevPendingArr.forEach((prevPendingItem, i) => {\n const data = dataArr[i];\n const bufferData = buffersArr[i];\n const { params, resolve, reject } = prevPendingItem;\n const [storeUrl, key] = params;\n\n if (!data.success) {\n resolve(undefined);\n return;\n }\n\n if (ArrayBuffer.isView(bufferData)) {\n resolve(new Uint8Array(bufferData.buffer, bufferData.byteOffset, bufferData.byteLength));\n return;\n }\n resolve(new Uint8Array(bufferData.buffer));\n return;\n });\n }\n\n function run() {\n processBatch(pending);\n pending = [];\n batchId = 0;\n }\n\n function enqueue(params) {\n batchId = batchId || requestAnimationFrame(() => run());\n let { promise, resolve, reject } = Promise.withResolvers();\n pending.push({ params, resolve, reject });\n return promise;\n }\n\n\n const stores = Object.fromEntries(\n storeUrls.map(storeUrl => ([\n storeUrl,\n {\n async get(key) {\n if (invokeBatched) {\n return enqueue([storeUrl, key]);\n } else {\n // Do not submit zarr gets in batches. Instead, submit individually.\n const [data, buffers] = await invoke(\n view.model,\n \"_zarr_get\",\n [storeUrl, key],\n null,\n AbortSignal.timeout(invokeTimeout),\n );\n if (!data.success) return undefined;\n\n if (ArrayBuffer.isView(buffers[0])) {\n return new Uint8Array(buffers[0].buffer, buffers[0].byteOffset, buffers[0].byteLength);\n }\n return new Uint8Array(buffers[0].buffer);\n }\n },\n async getRange(key, rangeQuery) {\n if (invokeBatched) {\n return enqueue([storeUrl, key, rangeQuery]);\n } else {\n // Do not submit zarr gets in batches. Instead, submit individually.\n const [data, buffers] = await invoke(\n view.model,\n \"_zarr_get_range\",\n [storeUrl, key, rangeQuery],\n null,\n AbortSignal.timeout(invokeTimeout),\n );\n if (!data.success) return undefined;\n\n if (ArrayBuffer.isView(buffers[0])) {\n return new Uint8Array(buffers[0].buffer, buffers[0].byteOffset, buffers[0].byteLength);\n }\n return new Uint8Array(buffers[0].buffer);\n }\n },\n }\n ])),\n );\n\n function invokePluginCommand(commandName, commandParams, commandBuffers) {\n return invoke(\n view.model,\n \"_plugin_command\",\n [commandName, commandParams],\n commandBuffers ?? null,\n AbortSignal.timeout(invokeTimeout),\n );\n }\n\n for (const pluginEsm of pluginEsmArr) {\n try {\n const pluginEsmUrl = URL.createObjectURL(new Blob([pluginEsm], { type: \"text/javascript\" }));\n const pluginModule = (await import(pluginEsmUrl)).default;\n URL.revokeObjectURL(pluginEsmUrl);\n\n const pluginDeps = {\n React,\n PluginFileType,\n PluginViewType,\n PluginCoordinationType,\n PluginJointFileType,\n PluginAsyncFunction,\n z,\n invokeCommand: invokePluginCommand,\n useCoordination,\n useGridItemSize,\n useComplexCoordination,\n useMultiCoordinationScopesNonNull,\n useMultiCoordinationScopesSecondaryNonNull,\n useComplexCoordinationSecondary,\n useCoordinationScopes,\n useCoordinationScopesBy,\n };\n const pluginsObj = await pluginModule.createPlugins(pluginDeps);\n if(Array.isArray(pluginsObj.pluginViewTypes)) {\n pluginViewTypes = [...pluginViewTypes, ...pluginsObj.pluginViewTypes];\n }\n if(Array.isArray(pluginsObj.pluginCoordinationTypes)) {\n pluginCoordinationTypes = [...pluginCoordinationTypes, ...pluginsObj.pluginCoordinationTypes];\n }\n if(Array.isArray(pluginsObj.pluginFileTypes)) {\n pluginFileTypes = [...pluginFileTypes, ...pluginsObj.pluginFileTypes];\n }\n if(Array.isArray(pluginsObj.pluginJointFileTypes)) {\n pluginJointFileTypes = [...pluginJointFileTypes, ...pluginsObj.pluginJointFileTypes];\n }\n if(Array.isArray(pluginsObj.pluginAsyncFunctions)) {\n pluginAsyncFunctions = [...pluginAsyncFunctions, ...pluginsObj.pluginAsyncFunctions];\n }\n } catch(e) {\n console.error(\"Error loading plugin ESM or executing createPlugins function.\");\n console.error(e);\n }\n }\n\n let PageComponent;\n if(pageMode && pageEsm.length > 0) {\n try {\n const pageEsmUrl = URL.createObjectURL(new Blob([pageEsm], { type: \"text/javascript\" }));\n const pageModule = (await import(pageEsmUrl)).default;\n URL.revokeObjectURL(pageEsmUrl);\n\n const pageDeps = {\n React,\n usePageModeView,\n };\n PageComponent = await pageModule.createPage(pageDeps);\n } catch(e) {\n console.error(\"Error loading page ESM or executing createPage function.\")\n console.error(e);\n }\n }\n\n function VitessceWidget(props) {\n const { model, styleContainer } = props;\n\n const [config, setConfig] = React.useState(prependBaseUrl(model.get('_config'), model.get('proxy'), model.get('has_host_name')));\n const [validateConfig, setValidateConfig] = React.useState(true);\n const height = model.get('height');\n const theme = model.get('theme') === 'auto' ? (prefersDark ? 'dark' : 'light') : model.get('theme');\n\n const divRef = React.useRef();\n\n React.useEffect(() => {\n if(!divRef.current || !preventScroll) {\n return () => {};\n }\n\n function handleMouseEnter() {\n const jpn = divRef.current.closest('.jp-Notebook');\n if(jpn) {\n jpn.style.overflow = \"hidden\";\n }\n }\n function handleMouseLeave(event) {\n if(event.relatedTarget === null || (event.relatedTarget && event.relatedTarget.closest('.jp-Notebook')?.length)) return;\n const jpn = divRef.current.closest('.jp-Notebook');\n if(jpn) {\n jpn.style.overflow = \"auto\";\n }\n }\n divRef.current.addEventListener(\"mouseenter\", handleMouseEnter);\n divRef.current.addEventListener(\"mouseleave\", handleMouseLeave);\n\n return () => {\n if(divRef.current) {\n divRef.current.removeEventListener(\"mouseenter\", handleMouseEnter);\n divRef.current.removeEventListener(\"mouseleave\", handleMouseLeave);\n }\n };\n }, [divRef, preventScroll]);\n\n // Config changed on JS side (from within ),\n // send updated config to Python side.\n const onConfigChange = React.useCallback((config) => {\n model.set('_config', config);\n setValidateConfig(false);\n model.save_changes();\n }, [model]);\n\n // Config changed on Python side,\n // pass to component to it is updated on JS side.\n React.useEffect(() => {\n model.on('change:_config', () => {\n const newConfig = prependBaseUrl(model.get('_config'), model.get('proxy'), model.get('has_host_name'));\n\n // Force a re-render and re-validation by setting a new config.uid value.\n // TODO: make this conditional on a parameter from Python.\n //newConfig.uid = `random-${Math.random()}`;\n //console.log('newConfig', newConfig);\n setConfig(newConfig);\n });\n }, []);\n\n const vitessceProps = {\n height, theme, config, onConfigChange, validateConfig,\n pluginViewTypes, pluginCoordinationTypes,\n pluginFileTypes,pluginJointFileTypes, pluginAsyncFunctions,\n remountOnUidChange, stores, pageMode, styleContainer,\n };\n\n return e('div', { ref: divRef, style: { height: height + 'px' } },\n e(React.Suspense, { fallback: e('div', {}, 'Loading...') },\n e(React.StrictMode, {},\n e(Vitessce, vitessceProps,\n (pageMode ? e(PageComponent, {}) : null)\n ),\n ),\n ),\n );\n }\n\n const root = createRoot(view.el);\n // Marimo puts AnyWidgets in a Shadow Root, so we need to tell Emotion to\n // insert styles within the Shadow DOM.\n const rootNode = view.el.getRootNode();\n const styleContainer = rootNode === document ? undefined : rootNode;\n root.render(e(VitessceWidget, { model: view.model, styleContainer }));\n\n return () => {\n // Re-enable scrolling.\n const jpn = view.el.closest('.jp-Notebook');\n if(jpn) {\n jpn.style.overflow = \"auto\";\n }\n\n // Clean up React and DOM state.\n root.unmount();\n if(view._isFromDisplay) {\n view.el.remove();\n }\n };\n}\nexport default { render };\n", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "custom_js_url": "", + "has_host_name": false, + "height": 600, + "invoke_batched": true, + "invoke_timeout": 300000, + "js_dev_mode": false, + "js_package_version": "3.9.11", + "layout": "IPY_MODEL_7944f55009c64754a407dfff3408ca92", + "page_esm": "", + "page_mode": false, + "plugin_esm": [], + "prevent_scroll": true, + "proxy": false, + "remount_on_uid_change": true, + "store_urls": [], + "tabbable": null, + "theme": "auto", + "tooltip": null, + "uid": "dd32" + } + } + }, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/notebooks/widget_neuroglancer_precomputed.ipynb b/docs/notebooks/widget_neuroglancer_precomputed.ipynb index 67a64025..8326e1f7 100644 --- a/docs/notebooks/widget_neuroglancer_precomputed.ipynb +++ b/docs/notebooks/widget_neuroglancer_precomputed.ipynb @@ -57,7 +57,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 2, @@ -102,7 +102,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 3, @@ -124,10 +124,6 @@ " },\n", ")\n", "lc_view = vc.add_view(\"layerControllerBeta\", dataset=dataset)\n", - "# TODO: until support to load the segments is added in NG-View\n", - "# The obsSets view is not required for the segmentation to load, but\n", - "# a mounted obsSets view is needed to trigger the underlying data hook \n", - "# that resolves obsSets data for the segmentation channel.\n", "obs_sets_view = vc.add_view(\"obsSets\", dataset=dataset)\n", "vc.layout(ng_view | (lc_view / obs_sets_view))" ] @@ -158,7 +154,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 4, @@ -208,12 +204,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "c80f5124cd2745b1bfaa24ec34c81822", + "model_id": "321061d5910b4cb795eeebc5836090da", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "execution_count": 5, @@ -222,128 +218,9 @@ } ], "source": [ - "vw = vc.widget(custom_js_url=\"http://localhost:9000/packages/main/dev/dist/index.js\")\n", + "vw = vc.widget(custom_js_url=\"http://localhost:9001/packages/main/dev/dist/index.js\")\n", "vw" ] - }, - { - "cell_type": "markdown", - "id": "0418b571", - "metadata": {}, - "source": [ - "## 4. Alternate way to load segments: providing an explicit array\n", - "\n", - "Instead of pointing at a remote `obsSets` CSV (as in section 1 above), you can select and color a specific, known set of segments directly from a Python list/dict. Native Neuroglancer itself supports specifying segments this way, as a plain array (\"segments\": [...]) in its own JSON state — this section replicates that same capability through Vitessce's own coordination system.\n", - "This adds two files instead of one `obsSets.csv`\n", - "- `obsFeatureMatrix.csv` -- just the segment IDs, defining which observations exist.\n", - "- `obsColors.csv` -- an explicit `id -> color` mapping. -- optional\n", - "\n", - "The `segmentationChannel` coordination also changes slightly: `obsColorEncoding` is set to `'obsColors'` (using the explicit colors above) instead of relying on cluster-based coloring." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "27b7431d", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2858ecabc0e34b8f85731d41886f8c4a", - "version_major": 2, - "version_minor": 1 - }, - "text/plain": [ - "" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from vitessce import make_ids_csv_data_url, make_colors_csv_data_url\n", - "\n", - "segment_ids = [612, 3351, 4328, 6531, 8446]\n", - "segment_colors = {\n", - " 612: '#d74242',\n", - " 3351: '#b9d742',\n", - " 4328: '#42d77d',\n", - " 6531: '#427dd7',\n", - " 8446: '#b942d7',\n", - "}\n", - "\n", - "vc_alt = VitessceConfig(\n", - " schema_version='1.0.17',\n", - " name='Neuroglancer precomputed example (explicit segments)',\n", - ")\n", - "dataset_alt = vc_alt.add_dataset('Melanoma')\n", - "\n", - "dataset_alt.add_object(ObsSegmentationsNgPrecomputedWrapper(\n", - " data_url='https://data-2.vitessce.io/data/sorger/melanoma_meshes',\n", - " coordination_values={'fileUid': 'segmentation'},\n", - "))\n", - "\n", - "# IDs only -- defines which observations exist for this obsType.\n", - "dataset_alt.add_object(CsvWrapper(\n", - " csv_url=make_ids_csv_data_url(segment_ids),\n", - " data_type='obsFeatureMatrix',\n", - " coordination_values={\n", - " 'obsType': 'cell', 'featureType': 'feature', 'featureValueType': 'value',\n", - " },\n", - "))\n", - "\n", - "# Explicit id -> color mapping.\n", - "dataset_alt.add_object(CsvWrapper(\n", - " csv_url=make_colors_csv_data_url(segment_colors),\n", - " data_type='obsColors',\n", - " options={'obsIndex': 'id', 'obsColors': 'color'},\n", - " coordination_values={'obsType': 'cell'},\n", - "))\n", - "\n", - "ng_view_alt = vc_alt.add_view('neuroglancer', dataset=dataset_alt).set_props(\n", - " initialNgCameraState={\n", - " 'position': [49.5, 1000.5, 5209.5],\n", - " 'projectionScale': 1024,\n", - " 'projectionOrientation': [\n", - " -0.636204183101654,\n", - " -0.5028395652770996,\n", - " 0.5443811416625977,\n", - " 0.2145828753709793,\n", - " ],\n", - " },\n", - ")\n", - "lc_view_alt = vc_alt.add_view('layerControllerBeta', dataset=dataset_alt)\n", - "vc_alt.layout(ng_view_alt | lc_view_alt)\n", - "\n", - "vc_alt.link_views_by_dict([ng_view_alt, lc_view_alt], {\n", - " 'spatialRenderingMode': '3D',\n", - " 'spatialZoom': 0, 'spatialTargetX': 0, 'spatialTargetY': 0, 'spatialTargetZ': 0,\n", - " 'spatialRotationX': 0, 'spatialRotationY': 0, 'spatialRotationOrbit': 0,\n", - "}, meta=False)\n", - "\n", - "vc_alt.link_views_by_dict([ng_view_alt, lc_view_alt], {\n", - " 'segmentationLayer': CL([{\n", - " 'fileUid': 'segmentation',\n", - " 'spatialLayerOpacity': 1,\n", - " 'spatialTargetResolution': None,\n", - " 'spatialLayerVisible': True,\n", - " 'segmentationChannel': CL([{\n", - " 'obsType': 'cell',\n", - " 'featureType': 'feature',\n", - " 'featureValueType': 'value',\n", - " 'spatialChannelVisible': True,\n", - " 'obsColorEncoding': 'obsColors',\n", - " }]),\n", - " }]),\n", - "}, scope_prefix=get_initial_coordination_scope_prefix('A', 'obsSegmentations'))\n", - "\n", - "# TODO: drop the custom_js_url when updates released\n", - "vw_alt = vc_alt.widget(custom_js_url='http://localhost:9000/packages/main/dev/dist/index.js')\n", - "vw_alt" - ] } ], "metadata": { diff --git a/uv.lock b/uv.lock index a86ef26d..01a0ee60 100644 --- a/uv.lock +++ b/uv.lock @@ -3892,7 +3892,7 @@ wheels = [ [[package]] name = "vitessce" -version = "3.9.4" +version = "3.9.5" source = { editable = "." } dependencies = [ { name = "black" }, From cb6b41d2da9542a75946d8f7d35a603398e4cc7b Mon Sep 17 00:00:00 2001 From: Tabassum Kakar Date: Wed, 2 Sep 2026 16:09:10 -0400 Subject: [PATCH 3/4] Updated js version --- src/vitessce/widget.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vitessce/widget.py b/src/vitessce/widget.py index 1ae42079..a48c1daf 100644 --- a/src/vitessce/widget.py +++ b/src/vitessce/widget.py @@ -800,7 +800,7 @@ class VitessceWidget(anywidget.AnyWidget): next_port = DEFAULT_PORT - js_package_version = Unicode('4.0.5').tag(sync=True) + js_package_version = Unicode('4.0.6').tag(sync=True) js_dev_mode = Bool(False).tag(sync=True) custom_js_url = Unicode('').tag(sync=True) plugin_esm = List(trait=Unicode(''), default_value=[]).tag(sync=True) @@ -813,7 +813,7 @@ class VitessceWidget(anywidget.AnyWidget): store_urls = List(trait=Unicode(''), default_value=[]).tag(sync=True) - def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=False, js_package_version='4.0.5', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True, prefer_local=True, invoke_timeout=300000, invoke_batched=True, page_mode=False, page_esm=None, prevent_scroll=True, server_host=None): + def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=False, js_package_version='4.0.6', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True, prefer_local=True, invoke_timeout=300000, invoke_batched=True, page_mode=False, page_esm=None, prevent_scroll=True, server_host=None): """ Construct a new Vitessce widget. Not intended to be instantiated directly; instead, use ``VitessceConfig.widget``. @@ -1021,7 +1021,7 @@ def _dispatch_command(self, msg: dict, buffers: list[bytes]) -> None: # Launch Vitessce using plain HTML representation (no ipywidgets) -def ipython_display(config, height=600, theme='auto', base_url=None, host_name=None, uid=None, port=None, proxy=False, js_package_version='4.0.5', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True, page_mode=False, page_esm=None, server_host=None): +def ipython_display(config, height=600, theme='auto', base_url=None, host_name=None, uid=None, port=None, proxy=False, js_package_version='4.0.6', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True, page_mode=False, page_esm=None, server_host=None): from IPython.display import display, HTML uid_str = "vitessce" + get_uid_str(uid) From 24bfd2c34ccda63005626b9593000daa6315bc4c Mon Sep 17 00:00:00 2001 From: Tabassum Kakar Date: Thu, 3 Sep 2026 07:35:33 -0400 Subject: [PATCH 4/4] Fixed errors by using web_app() for now --- docs/notebooks/widget_neuroglancer.ipynb | 239 ------------------ .../widget_neuroglancer_precomputed.ipynb | 16 +- ...roglancer_precomputed_with_segments.ipynb} | 86 ++----- 3 files changed, 29 insertions(+), 312 deletions(-) delete mode 100644 docs/notebooks/widget_neuroglancer.ipynb rename docs/notebooks/{widget_neuroglancer_precomputed-segments.ipynb => widget_neuroglancer_precomputed_with_segments.ipynb} (95%) diff --git a/docs/notebooks/widget_neuroglancer.ipynb b/docs/notebooks/widget_neuroglancer.ipynb deleted file mode 100644 index 43f1d933..00000000 --- a/docs/notebooks/widget_neuroglancer.ipynb +++ /dev/null @@ -1,239 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "# Vitessce Widget Tutorial" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Example usage of Neuroglancer view" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from vitessce import (\n", - " VitessceConfig,\n", - " Component as cm,\n", - " CoordinationType as ct,\n", - " ImageOmeTiffWrapper,\n", - " CsvWrapper,\n", - " hconcat,\n", - " vconcat,\n", - " get_initial_coordination_scope_prefix,\n", - " CoordinationLevel as CL\n", - ")\n", - "from os.path import join" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 1. Configure Vitessce" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "vc = VitessceConfig(schema_version=\"1.0.17\")\n", - "dataset = vc.add_dataset(name='Meshes').add_object(\n", - " ImageOmeTiffWrapper(\n", - " img_url='https://lsp-public-data.s3.amazonaws.com/yapp-2023-3d-melanoma/Dataset1-LSP13626-invasive-margin.ome.tiff',\n", - " offsets_url='https://lsp-public-data.s3.amazonaws.com/yapp-2023-3d-melanoma/Dataset1-LSP13626-invasive-margin.offsets.json',\n", - " coordination_values={\n", - " \"fileUid\": 'melanoma',\n", - " },\n", - " )\n", - ").add_object(\n", - " CsvWrapper(\n", - " data_type=\"obsEmbedding\",\n", - " csv_url='https://storage.googleapis.com/vitessce-demo-data/neuroglancer-march-2025/melanoma_with_embedding_filtered_ids.csv',\n", - " options= {\n", - " \"obsIndex\": 'id',\n", - " \"obsEmbedding\": ['tSNE1', 'tSNE2'],\n", - " },\n", - " coordination_values= {\n", - " \"obsType\": 'cell',\n", - " \"embeddingType\": 'TSNE',\n", - " },\n", - " )\n", - ").add_object(\n", - " CsvWrapper(\n", - " data_type=\"obsSets\",\n", - " csv_url='https://storage.googleapis.com/vitessce-demo-data/neuroglancer-march-2025/melanoma_with_embedding_filtered_ids.csv',\n", - " coordination_values={\n", - " \"obsType\": 'cell',\n", - " },\n", - " options= {\n", - " \"obsIndex\": 'id',\n", - " \"obsSets\": [\n", - " {\n", - " \"name\": 'Clusters',\n", - " \"column\": 'cluster',\n", - " },\n", - " ],\n", - " },\n", - " )\n", - ")\n", - "spatialThreeView = vc.add_view('spatialBeta', dataset=dataset);\n", - "lcView = vc.add_view('layerControllerBeta', dataset=dataset);\n", - "obsSets = vc.add_view('obsSets', dataset=dataset);\n", - "scatterView = vc.add_view('scatterplot', dataset=dataset, mapping=\"TSNE\");\n", - "# Configuration via props.viewerState is temporary and subject to change.\n", - "neuroglancerView = vc.add_view('neuroglancer', dataset=dataset).set_props(viewerState={\n", - " \"dimensions\": {\n", - " \"x\": [\n", - " 1e-9,\n", - " \"m\"\n", - " ],\n", - " \"y\": [\n", - " 1e-9,\n", - " \"m\"\n", - " ],\n", - " \"z\": [\n", - " 1e-9,\n", - " \"m\"\n", - " ]\n", - " },\n", - " \"position\": [\n", - " 49.5,\n", - " 1000.5,\n", - " 5209.5\n", - " ],\n", - " \"crossSectionScale\": 1,\n", - " \"projectionOrientation\": [\n", - " -0.636204183101654,\n", - " -0.5028395652770996,\n", - " 0.5443811416625977,\n", - " 0.2145828753709793\n", - " ],\n", - " \"projectionScale\": 1024,\n", - " \"layers\": [\n", - " {\n", - " \"type\": \"segmentation\",\n", - " \"source\": \"precomputed://https://vitessce-data-v2.s3.us-east-1.amazonaws.com/data/sorger/invasive_meshes\",\n", - " \"segments\": [\n", - " \"5\"\n", - " ],\n", - " \"segmentColors\": {\n", - " \"5\": \"red\"\n", - " },\n", - " \"name\": \"segmentation\"\n", - " }\n", - " ],\n", - " \"showSlices\": False,\n", - " \"layout\": \"3d\"\n", - "});\n", - "\n", - "vc.link_views([scatterView], ['embeddingObsRadiusMode', 'embeddingObsRadius'], ['manual', 4]);\n", - "\n", - "# Sync the zoom/rotation/pan states\n", - "vc.link_views_by_dict([spatialThreeView, lcView, neuroglancerView], {\n", - " \"spatialRenderingMode\": '3D',\n", - " \"spatialZoom\": 0,\n", - " \"spatialTargetT\": 0,\n", - " \"spatialTargetX\": 0,\n", - " \"spatialTargetY\": 0,\n", - " \"spatialTargetZ\": 0,\n", - " \"spatialRotationX\": 0,\n", - " \"spatialRotationY\": 0,\n", - "}, meta=False);\n", - "\n", - "# Initialize the image properties\n", - "vc.link_views_by_dict([spatialThreeView, lcView], {\n", - " \"imageLayer\": CL([\n", - " {\n", - " \"fileUid\": 'melanoma',\n", - " \"spatialLayerOpacity\": 1,\n", - " \"spatialTargetResolution\": None,\n", - " \"imageChannel\": CL([\n", - " {\n", - " \"spatialTargetC\": 0,\n", - " \"spatialChannelColor\": [255, 0, 0],\n", - " \"spatialChannelVisible\": True,\n", - " \"spatialChannelOpacity\": 1.0,\n", - " },\n", - " ]),\n", - " },\n", - " ]),\n", - "}, scope_prefix=get_initial_coordination_scope_prefix('A', 'image'));\n", - "\n", - "\n", - "vc.layout(hconcat(neuroglancerView, spatialThreeView, vconcat(lcView, obsSets, scatterView)));" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 2. Create the Vitessce widget" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "vw = vc.widget()\n", - "vw" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.14" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/notebooks/widget_neuroglancer_precomputed.ipynb b/docs/notebooks/widget_neuroglancer_precomputed.ipynb index 8326e1f7..ae72974d 100644 --- a/docs/notebooks/widget_neuroglancer_precomputed.ipynb +++ b/docs/notebooks/widget_neuroglancer_precomputed.ipynb @@ -57,7 +57,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 2, @@ -102,7 +102,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 3, @@ -154,7 +154,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 4, @@ -195,7 +195,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "id": "f562bfc8", "metadata": { "tags": [] @@ -204,21 +204,21 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "321061d5910b4cb795eeebc5836090da", + "model_id": "79075b06653740f790115c754c9fc094", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, - "execution_count": 5, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "vw = vc.widget(custom_js_url=\"http://localhost:9001/packages/main/dev/dist/index.js\")\n", + "vw = vc.widget()\n", "vw" ] } diff --git a/docs/notebooks/widget_neuroglancer_precomputed-segments.ipynb b/docs/notebooks/widget_neuroglancer_precomputed_with_segments.ipynb similarity index 95% rename from docs/notebooks/widget_neuroglancer_precomputed-segments.ipynb rename to docs/notebooks/widget_neuroglancer_precomputed_with_segments.ipynb index 2ee21180..c8722c35 100644 --- a/docs/notebooks/widget_neuroglancer_precomputed-segments.ipynb +++ b/docs/notebooks/widget_neuroglancer_precomputed_with_segments.ipynb @@ -27,7 +27,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "6d673a85", "metadata": { "tags": [] @@ -55,23 +55,12 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "ac0fb259", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "vc = VitessceConfig(schema_version=\"1.0.17\", name=\"Neuroglancer precomputed example\")\n", "dataset = vc.add_dataset(\"Melanoma\")\n", @@ -96,16 +85,18 @@ "\n", "# IDs only -- defines which observations exist for this obsType.\n", "dataset.add_object(CsvWrapper(\n", - " csv_url=make_ids_csv_data_url(segment_ids),\n", + " # NOTE: remove True when widget() is used\n", + " csv_url=make_ids_csv_data_url(segment_ids, True),\n", " data_type='obsFeatureMatrix',\n", " coordination_values={\n", " 'obsType': 'cell', 'featureType': 'feature', 'featureValueType': 'value',\n", " },\n", "))\n", "\n", - "# Explicit id -> color mapping.\n", + "# # Explicit id -> color mapping.\n", "dataset.add_object(CsvWrapper(\n", - " csv_url=make_colors_csv_data_url(segment_colors),\n", + " # NOTE: remove True when widget() is used\n", + " csv_url=make_colors_csv_data_url(segment_colors, True),\n", " data_type='obsColors',\n", " options={'obsIndex': 'id', 'obsColors': 'color'},\n", " coordination_values={'obsType': 'cell'},\n", @@ -114,27 +105,16 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "6babd185", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "ng_view = vc.add_view(\"neuroglancer\", dataset=dataset).set_props(\n", " initialNgCameraState={\n", - " 'position': [49.5, 1000.5, 5209.5],\n", + " 'position': [1134, 602, 5209],\n", " 'projectionScale': 1024,\n", " 'projectionOrientation': [\n", " -0.636204183101654,\n", @@ -166,23 +146,12 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "648ca751", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "vc.link_views_by_dict([ng_view, lc_view], {\n", " \"spatialRenderingMode\": \"3D\",\n", @@ -221,37 +190,24 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "f562bfc8", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e7ec9b8ec84d4f7aaeb9f06e159fdc37", - "version_major": 2, - "version_minor": 1 - }, - "text/plain": [ - "" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "vw = vc.widget(custom_js_url='http://localhost:9001/packages/main/dev/dist/index.js')\n", - "vw" + "#### TODO: uncomment when issue#517 is resolved\n", + "# vw = vc.widget()\n", + "# vw\n", + "\n", + "vc.web_app()" ] }, { "cell_type": "code", "execution_count": null, - "id": "4ae4fc7f-76eb-4b3f-9623-16a220174f48", + "id": "4dab4e94-d920-4a04-9bb8-884463280510", "metadata": {}, "outputs": [], "source": []