Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 137 additions & 21 deletions docs/notebooks/widget_neuroglancer_precomputed.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"\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",
"The `obsSets`-type file (and its corresponding view) is required for the segmentation layer to actually resolve and display any segments -- without it, segments will not be selected/colored dynamically. A static `segments` list can alternatively be passed via `options` for quick testing without a full `obsSets` pipeline."
"The `obsSets`-type file (and its corresponding view) is required for the segmentation layer to actually resolve and display any segments -- without it, segments will not be selected/colored dynamically. A static `segments` list can alternatively be passed for quick testing without a full `obsSets` pipeline -- see the alternative method below."
]
},
{
Expand Down Expand Up @@ -57,7 +57,7 @@
{
"data": {
"text/plain": [
"<vitessce.config.VitessceConfigDataset at 0x1074b4d40>"
"<vitessce.config.VitessceConfigDataset at 0x1069d6450>"
]
},
"execution_count": 2,
Expand All @@ -77,9 +77,9 @@
" coordination_values={\"fileUid\": \"segmentation\"},\n",
"))\n",
"\n",
"# An obsSets-type file is required for segments to be dynamically\n",
"# An obsSets-type file can be used for segments to be dynamically\n",
"# selected/colored -- obsType here must match the segmentationChannel's\n",
"# obsType coordination value set below.\n",
"# obsType coordination value set below. \n",
"dataset.add_object(CsvWrapper(\n",
" csv_url=\"https://storage.googleapis.com/vitessce-demo-data/neuroglancer-march-2025/melanoma_with_embedding_filtered_ids.csv\",\n",
" data_type=\"obsSets\",\n",
Expand All @@ -102,7 +102,7 @@
{
"data": {
"text/plain": [
"<vitessce.config.VitessceConfig at 0x121d1c440>"
"<vitessce.config.VitessceConfig at 0x104dbd6a0>"
]
},
"execution_count": 3,
Expand All @@ -111,7 +111,18 @@
}
],
"source": [
"ng_view = vc.add_view(\"neuroglancer\", dataset=dataset)\n",
"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",
"# 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",
Expand All @@ -124,7 +135,9 @@
{
"cell_type": "markdown",
"id": "c8aaa012",
"metadata": {},
"metadata": {
"tags": []
},
"source": [
"## 2. Coordinate the views\n",
"\n",
Expand All @@ -145,7 +158,7 @@
{
"data": {
"text/plain": [
"<vitessce.config.VitessceConfig at 0x121d1c440>"
"<vitessce.config.VitessceConfig at 0x104dbd6a0>"
]
},
"execution_count": 4,
Expand Down Expand Up @@ -195,12 +208,12 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "dbd6e9854d6442ab80ba290784996dda",
"model_id": "c80f5124cd2745b1bfaa24ec34c81822",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<vitessce.widget.VitessceWidget object at 0x1074b6960>"
"<vitessce.widget.VitessceWidget object at 0x1069d6a50>"
]
},
"execution_count": 5,
Expand All @@ -209,25 +222,128 @@
}
],
"source": [
"vw = vc.widget(custom_js_url=\"http://localhost:9001/packages/main/dev/dist/index.js\")\n",
"vw = vc.widget(custom_js_url=\"http://localhost:9000/packages/main/dev/dist/index.js\")\n",
"vw"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "be0c7d7e-e466-45c2-8b65-503501d6faf9",
"cell_type": "markdown",
"id": "0418b571",
"metadata": {},
"outputs": [],
"source": []
"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": null,
"id": "8fbd9181-2d33-4018-be02-722161342c5c",
"execution_count": 6,
"id": "27b7431d",
"metadata": {},
"outputs": [],
"source": []
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "2858ecabc0e34b8f85731d41886f8c4a",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<vitessce.widget.VitessceWidget object at 0x1069d6c00>"
]
},
"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": {
Expand All @@ -246,7 +362,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.13"
"version": "3.12.7"
},
"widgets": {
"application/vnd.jupyter.widget-state+json": {
Expand Down
2 changes: 2 additions & 0 deletions src/vitessce/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from .utils import (
get_initial_coordination_scope_prefix,
get_initial_coordination_scope_name,
make_ids_csv_data_url,
make_colors_csv_data_url,
)

from .repr import make_repr
Expand Down
48 changes: 48 additions & 0 deletions src/vitessce/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,51 @@ def get_initial_coordination_scope_prefix(dataset_uid, data_type):
def get_initial_coordination_scope_name(dataset_uid, data_type, i=None):
prefix = get_initial_coordination_scope_prefix(dataset_uid, data_type)
return f"{prefix}{0 if i is None else i}"


def make_ids_csv_data_url(ids):
"""
Build a `data:` URL containing a small inline CSV with a single `id`
column, given a list of observation IDs (e.g. segment IDs).

Useful for defining a small, explicit `obsFeatureMatrix.csv` /
`obsSets.csv`-style observation list without needing to host a
separate CSV file.

:param list ids: A list of observation IDs.
:rtype: str
:returns: A `data:text/csv,...` URL.
"""
import csv
import io
from urllib.parse import quote

buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(["id"])
writer.writerows([[i] for i in ids])
return f"data:text/csv,{quote(buf.getvalue())}"


def make_colors_csv_data_url(id_to_color):
"""
Build a `data:` URL containing a small inline CSV with `id` and
`color` columns, given a dict mapping observation ID to a color
string (e.g. a hex color).

Useful for defining explicit per-observation colors (`obsColors.csv`)
without needing to host a separate CSV file.

:param dict id_to_color: A dict mapping observation ID to color.
:rtype: str
:returns: A `data:text/csv,...` URL.
"""
import csv
import io
from urllib.parse import quote

buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(["id", "color"])
writer.writerows(id_to_color.items())
return f"data:text/csv,{quote(buf.getvalue())}"
2 changes: 1 addition & 1 deletion src/vitessce/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ def get_uid_str(uid):
const e = React.createElement;

function isAbsoluteUrl(s) {
return s?.startsWith('http://') || s?.startsWith('https://');
return s?.startsWith('http://') || s?.startsWith('https://') || s?.startsWith('data:') || s?.startsWith('blob:') || s?.startsWith('file:') || s?.startsWith('//');
}
const WORKSPACES_URL_KEYWORD = 'https://workspaces-pt';
const OPTIONS_URL_KEYS = ['offsetsUrl', 'refSpecUrl'];
Expand Down
31 changes: 31 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import unittest
from urllib.parse import unquote

from vitessce.utils import make_ids_csv_data_url, make_colors_csv_data_url


class TestCsvDataUrlUtils(unittest.TestCase):

def test_make_ids_csv_data_url(self):
url = make_ids_csv_data_url([612, 3351, 4328])
self.assertTrue(url.startswith("data:text/csv,"))
decoded = unquote(url[len("data:text/csv,"):])
self.assertEqual(decoded, "id\r\n612\r\n3351\r\n4328\r\n")

def test_make_ids_csv_data_url_empty(self):
url = make_ids_csv_data_url([])
decoded = unquote(url[len("data:text/csv,"):])
self.assertEqual(decoded, "id\r\n")

def test_make_colors_csv_data_url(self):
url = make_colors_csv_data_url({
612: "#d74242",
3351: "#b9d742",
})
self.assertTrue(url.startswith("data:text/csv,"))
decoded = unquote(url[len("data:text/csv,"):])
self.assertEqual(decoded, "id,color\r\n612,#d74242\r\n3351,#b9d742\r\n")


if __name__ == "__main__":
unittest.main()
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading