diff --git a/docs/api_config.rst b/docs/api_config.rst
index 26a80d6a..b0522230 100644
--- a/docs/api_config.rst
+++ b/docs/api_config.rst
@@ -19,6 +19,17 @@ vitessce.config
:exclude-members: VitessceConfig, VitessceChainableConfig
:member-order: bysource
+vitessce.widget
+***************
+
+.. autoclass:: vitessce.widget.VitessceWidget()
+ :members:
+ :private-members:
+ :exclude-members: close
+
+.. autoclass:: vitessce.widget.VitesscePlugin
+ :members:
+
vitessce.constants
******************
diff --git a/docs/api_data.rst b/docs/api_data.rst
index 34fffd81..9cecbff1 100644
--- a/docs/api_data.rst
+++ b/docs/api_data.rst
@@ -19,13 +19,13 @@ vitessce.wrappers
:members:
vitessce.export
-*****************
+***************
.. automodule:: vitessce.export
:members:
vitessce.data_utils
-*****************
+*******************
.. automodule:: vitessce.data_utils.ome
:members:
diff --git a/docs/notebooks/marimo.py b/docs/notebooks/marimo.py
index 6d850404..4ce47a6b 100644
--- a/docs/notebooks/marimo.py
+++ b/docs/notebooks/marimo.py
@@ -33,13 +33,13 @@ def _(mo):
@app.cell
def _(vw):
- vw.config
+ vw._config
return
@app.cell
def _(vw):
- vw.config["coordinationSpace"]["embeddingZoom"]
+ vw._config["coordinationSpace"]["embeddingZoom"]
return
diff --git a/docs/notebooks/spatial_data_visium_hd.ipynb b/docs/notebooks/spatial_data_visium_hd.ipynb
new file mode 100644
index 00000000..0ca3257e
--- /dev/null
+++ b/docs/notebooks/spatial_data_visium_hd.ipynb
@@ -0,0 +1,247 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "nbsphinx": "hidden"
+ },
+ "source": [
+ "# Vitessce Widget Tutorial"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Visualization of a SpatialData object"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Import dependencies\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import os\n",
+ "from os.path import join, isfile, isdir\n",
+ "from urllib.request import urlretrieve\n",
+ "import zipfile\n",
+ "\n",
+ "from vitessce import (\n",
+ " VitessceConfig,\n",
+ " ViewType as vt,\n",
+ " CoordinationType as ct,\n",
+ " CoordinationLevel as CL,\n",
+ " SpatialDataWrapper,\n",
+ " get_initial_coordination_scope_prefix\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "data_dir = \"data\"\n",
+ "zip_filepath = join(data_dir, \"visium_hd_3.0.0_io.zip\")\n",
+ "spatialdata_filepath = join(data_dir, \"visium_hd_3.0.0.spatialdata.zarr\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "if not isdir(spatialdata_filepath):\n",
+ " if not isfile(zip_filepath):\n",
+ " os.makedirs(data_dir, exist_ok=True)\n",
+ " urlretrieve('https://s3.embl.de/spatialdata/spatialdata-sandbox/visium_hd_3.0.0_io.zip', zip_filepath)\n",
+ " with zipfile.ZipFile(zip_filepath,\"r\") as zip_ref:\n",
+ " zip_ref.extractall(data_dir)\n",
+ " os.rename(join(data_dir, \"data.zarr\"), spatialdata_filepath)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Rasterize bins\n",
+ "Reference: https://spatialdata.scverse.org/en/stable/tutorials/notebooks/notebooks/examples/technology_visium_hd.html#performant-on-the-fly-data-rasterization"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from spatialdata import (\n",
+ " read_zarr,\n",
+ " rasterize_bins,\n",
+ " rasterize_bins_link_table_to_labels\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "sdata = read_zarr(spatialdata_filepath)\n",
+ "sdata"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "for bin_size in [\"016\", \"008\", \"002\"]:\n",
+ " # rasterize_bins() requires a compresed sparse column (csc) matrix\n",
+ " sdata.tables[f\"square_{bin_size}um\"].X = sdata.tables[f\"square_{bin_size}um\"].X.tocsc()\n",
+ " rasterized = rasterize_bins(\n",
+ " sdata,\n",
+ " f\"Visium_HD_Mouse_Small_Intestine_square_{bin_size}um\",\n",
+ " f\"square_{bin_size}um\",\n",
+ " \"array_col\",\n",
+ " \"array_row\",\n",
+ " # We want to rasterize to a Labels element, rather than an Image element.\n",
+ " return_region_as_labels=True\n",
+ " )\n",
+ " sdata[f\"rasterized_{bin_size}um\"] = rasterized\n",
+ " rasterize_bins_link_table_to_labels(\n",
+ " sdata,\n",
+ " table_name=f\"square_{bin_size}um\",\n",
+ " rasterized_labels_name=f\"rasterized_{bin_size}um\",\n",
+ " )\n",
+ " sdata.write_element(f\"rasterized_{bin_size}um\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "sdata"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Configure Vitessce\n",
+ "\n",
+ "Vitessce needs to know which pieces of data we are interested in visualizing, the visualization types we would like to use, and how we want to coordinate (or link) the views."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "vc = VitessceConfig(\n",
+ " schema_version=\"1.0.18\",\n",
+ " name='Visium HD SpatialData Demo',\n",
+ ")\n",
+ "# Add data to the configuration:\n",
+ "wrapper = SpatialDataWrapper(\n",
+ " sdata_path=spatialdata_filepath,\n",
+ " # The following paths are relative to the root of the SpatialData zarr store on-disk.\n",
+ " image_path=\"images/Visium_HD_Mouse_Small_Intestine_full_image\",\n",
+ " table_path=\"tables/square_016um\",\n",
+ " obs_feature_matrix_path=\"tables/square_016um/X\",\n",
+ " obs_segmentations_path=\"labels/rasterized_016um\",\n",
+ " #region=\"CytAssist_FFPE_Human_Breast_Cancer\",\n",
+ " coordinate_system=\"Visium_HD_Mouse_Small_Intestine\",\n",
+ " coordination_values={\n",
+ " # The following tells Vitessce to consider each observation as a \"bin\"\n",
+ " \"obsType\": \"bin\",\n",
+ " }\n",
+ ")\n",
+ "dataset = vc.add_dataset(name='Visium HD').add_object(wrapper)\n",
+ "\n",
+ "# Add views (visualizations) to the configuration:\n",
+ "spatial = vc.add_view(\"spatialBeta\", dataset=dataset)\n",
+ "feature_list = vc.add_view(\"featureList\", dataset=dataset)\n",
+ "layer_controller = vc.add_view(\"layerControllerBeta\", dataset=dataset)\n",
+ "vc.link_views_by_dict([spatial, layer_controller], {\n",
+ " 'imageLayer': CL([{\n",
+ " 'photometricInterpretation': 'RGB',\n",
+ " }]),\n",
+ "}, scope_prefix=get_initial_coordination_scope_prefix(\"A\", \"image\"))\n",
+ "vc.link_views_by_dict([spatial, layer_controller], {\n",
+ " 'segmentationLayer': CL([{\n",
+ " 'segmentationChannel': CL([{\n",
+ " 'spatialChannelOpacity': 0.5,\n",
+ " 'obsColorEncoding': 'geneSelection',\n",
+ " 'featureValueColormapRange': [0, 0.5],\n",
+ " }])\n",
+ " }]),\n",
+ "}, scope_prefix=get_initial_coordination_scope_prefix(\"A\", \"obsSegmentations\"))\n",
+ "obs_sets = vc.add_view(vt.OBS_SETS, dataset=dataset)\n",
+ "vc.link_views([spatial, layer_controller, feature_list, obs_sets], ['obsType', 'featureSelection'], [wrapper.obs_type_label, ['AA986860']])\n",
+ "\n",
+ "# Layout the views\n",
+ "vc.layout(spatial | (feature_list / layer_controller / obs_sets));"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Render the widget"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "vw = vc.widget()\n",
+ "vw"
+ ]
+ },
+ {
+ "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/pyproject.toml b/pyproject.toml
index 4ba7da9b..2eec62e2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "vitessce"
-version = "3.6.9"
+version = "3.7.0"
authors = [
{ name="Mark Keller", email="mark_keller@hms.harvard.edu" },
]
diff --git a/src/vitessce/config.py b/src/vitessce/config.py
index efaee548..c90c5e81 100644
--- a/src/vitessce/config.py
+++ b/src/vitessce/config.py
@@ -925,6 +925,19 @@ def stop_server(self, port):
del self.background_servers[port]
def stop_all_servers(self):
+ """
+ Stop all background servers associated with this config instance.
+
+ .. code-block:: python
+ :emphasize-lines: 5
+
+ from vitessce import VitessceConfig
+
+ vc = VitessceConfig(schema_version="1.0.18", name='My Config')
+ vw = vc.widget()
+ # ... do something with the widget ...
+ vc.stop_all_servers()
+ """
for server in self.background_servers.values():
server.stop()
self.background_servers = {}
@@ -1791,6 +1804,17 @@ def widget(self, **kwargs):
:param int height: The height of the widget, in pixels. By default, 600.
:param int port: The port to use when serving data objects on localhost. By default, 8000.
:param bool proxy: Is this widget being served through a proxy, for example with a cloud notebook (e.g. Binder)?
+ :param str js_package_version: The version of the NPM package ('vitessce' if not js_dev_mode else '@vitessce/dev').
+ :param bool js_dev_mode: Should @vitessce/dev be used (typically for debugging purposes)? By default, False.
+ :param str custom_js_url: A URL to a JavaScript file to use (instead of 'vitessce' or '@vitessce/dev' NPM package).
+ :param list[VitesscePlugin] plugins: A list of subclasses of VitesscePlugin. Optional.
+ :param bool remount_on_uid_change: Passed to the remountOnUidChange prop of the React component. By default, True.
+ :param bool prefer_local: Should local data be preferred (only applies to `*_artifact` data objects)? By default, True.
+ :param int invoke_timeout: The timeout in milliseconds for invoking Python functions from JavaScript. By default, 300000.
+ :param bool invoke_batched: Should invocations (Zarr gets) be submitted in batch, or individually? By default, True.
+ :param bool page_mode: Whether to render the component in grid-mode or page-mode. By default, False.
+ :param str page_esm: The ES module string for the page component creation function. Optional.
+ :param bool prevent_scroll: Should mouseover in the Vitessce widget prevent disable the scrolling of the notebook? By default, True.
:returns: The Jupyter widget.
:rtype: VitessceWidget
diff --git a/src/vitessce/widget.py b/src/vitessce/widget.py
index 916d64a6..59b88c12 100644
--- a/src/vitessce/widget.py
+++ b/src/vitessce/widget.py
@@ -445,7 +445,7 @@ def get_uid_str(uid):
function VitessceWidget(props) {
const { model, styleContainer } = props;
- const [config, setConfig] = React.useState(prependBaseUrl(model.get('config'), model.get('proxy'), model.get('has_host_name')));
+ const [config, setConfig] = React.useState(prependBaseUrl(model.get('_config'), model.get('proxy'), model.get('has_host_name')));
const [validateConfig, setValidateConfig] = React.useState(true);
const height = model.get('height');
const theme = model.get('theme') === 'auto' ? (prefersDark ? 'dark' : 'light') : model.get('theme');
@@ -484,7 +484,7 @@ def get_uid_str(uid):
// Config changed on JS side (from within ),
// send updated config to Python side.
const onConfigChange = React.useCallback((config) => {
- model.set('config', config);
+ model.set('_config', config);
setValidateConfig(false);
model.save_changes();
}, [model]);
@@ -492,8 +492,8 @@ def get_uid_str(uid):
// Config changed on Python side,
// pass to component to it is updated on JS side.
React.useEffect(() => {
- model.on('change:config', () => {
- const newConfig = prependBaseUrl(model.get('config'), model.get('proxy'), model.get('has_host_name'));
+ model.on('change:_config', () => {
+ const newConfig = prependBaseUrl(model.get('_config'), model.get('proxy'), model.get('has_host_name'));
// Force a re-render and re-validation by setting a new config.uid value.
// TODO: make this conditional on a parameter from Python.
@@ -597,8 +597,12 @@ class VitesscePlugin:
"""
A class that represents a Vitessce widget plugin. Custom plugins can be created by subclassing this class.
"""
- plugin_esm = DEFAULT_PLUGIN_ESM
- commands = {}
+
+ #: The ES module string for the plugin.
+ plugin_esm = DEFAULT_PLUGIN_ESM # type: str
+
+ #: A dictionary mapping command name strings to functions. Functions should take two arguments (message, buffers) and return a tuple (response, buffers).
+ commands = {} # type: dict
def on_config_change(self, new_config):
"""
@@ -614,7 +618,16 @@ def on_config_change(self, new_config):
class VitessceWidget(anywidget.AnyWidget):
"""
- A class to represent a Jupyter widget for Vitessce.
+ A class to represent a Jupyter widget for Vitessce. Not intended to be instantiated directly; instead, use ``VitessceConfig.widget``.
+
+ .. code-block:: python
+ :emphasize-lines: 4
+
+ from vitessce import VitessceConfig
+
+ vc = VitessceConfig.from_object(my_scanpy_object)
+ vw = vc.widget()
+ vw
"""
_esm = ESM
@@ -622,7 +635,13 @@ class VitessceWidget(anywidget.AnyWidget):
# Widget properties are defined as traitlets. Any property tagged with `sync=True`
# is automatically synced to the frontend *any* time it changes in Python.
# It is synced back to Python from the frontend *any* time the model is touched.
- config = Dict({}).tag(sync=True)
+
+ #: Dictionary representation of the Vitessce JSON configuration. Synced via traitlets upon interactions.
+ _config = Dict({}).tag(sync=True) # type: dict
+
+ #: The VitessceConfig instance used to create this widget. Not synced upon interactions.
+ config = None # type: vitessce.config.VitessceConfig
+
height = Int(600).tag(sync=True)
theme = Unicode('auto').tag(sync=True)
proxy = Bool(False).tag(sync=True)
@@ -631,7 +650,7 @@ class VitessceWidget(anywidget.AnyWidget):
next_port = DEFAULT_PORT
- js_package_version = Unicode('3.6.11').tag(sync=True)
+ js_package_version = Unicode('3.6.12').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)
@@ -644,9 +663,10 @@ 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='3.6.11', 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):
+ def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=False, js_package_version='3.6.12', 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):
+ """ """
"""
- Construct a new Vitessce widget.
+ Construct a new Vitessce widget. Not intended to be instantiated directly; instead, use ``VitessceConfig.widget``.
:param config: A view config instance.
:type config: VitessceConfig
@@ -666,19 +686,16 @@ def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=
:param str page_esm: The ES module string for the page component creation function. Optional.
:param bool prevent_scroll: Should mouseover in the Vitessce widget prevent disable the scrolling of the notebook? By default, True.
- .. code-block:: python
- :emphasize-lines: 4
-
- from vitessce import VitessceConfig, VitessceWidget
-
- vc = VitessceConfig.from_object(my_scanpy_object)
- vw = vc.widget()
- vw
+ Note: these parameter docstrings need to be manually kept in sync with the VitessceConfig.widget docstring.
"""
base_url, use_port, VitessceWidget.next_port = get_base_url_and_port(
port, VitessceWidget.next_port, proxy=proxy)
- self.config_obj = config
+ # Note:
+ # - self.config is the VitessceConfig instance.
+ # - self._config is the JSON configuration, synced via traitlets
+
+ self.config = config
self.port = use_port
config_dict = config.to_dict(base_url=base_url)
routes = config.get_routes()
@@ -694,7 +711,7 @@ def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=
uid_str = get_uid_str(uid)
super(VitessceWidget, self).__init__(
- config=config_dict, height=height, theme=theme, proxy=proxy,
+ _config=config_dict, height=height, theme=theme, proxy=proxy,
js_package_version=js_package_version, js_dev_mode=js_dev_mode, custom_js_url=custom_js_url,
plugin_esm=plugin_esm, remount_on_uid_change=remount_on_uid_change,
page_mode=page_mode, page_esm=('' if page_esm is None else page_esm),
@@ -712,14 +729,14 @@ def handle_config_change(change):
# It is optional for plugins to implement on_config_change.
pass
if new_config is not None:
- self.config = new_config
+ self._config = new_config
- self.observe(handle_config_change, names=['config'])
+ self.observe(handle_config_change, names=['_config'])
serve_routes(config, routes, use_port)
def _get_coordination_value(self, coordination_type, coordination_scope):
- obj = self.config['coordinationSpace'][coordination_type]
+ obj = self._config['coordinationSpace'][coordination_type]
obj_scopes = list(obj.keys())
if coordination_scope is not None:
if coordination_scope in obj_scopes:
@@ -742,7 +759,7 @@ def get_cell_selection(self, scope=None):
return self._get_coordination_value('cellSelection', scope)
def close(self):
- self.config_obj.stop_server(self.port)
+ self.config.stop_server(self.port)
super().close()
@anywidget.experimental.command
@@ -780,7 +797,7 @@ def _plugin_command(self, params, buffers):
# 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='3.6.11', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True, page_mode=False, page_esm=None):
+def ipython_display(config, height=600, theme='auto', base_url=None, host_name=None, uid=None, port=None, proxy=False, js_package_version='3.6.12', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True, page_mode=False, page_esm=None):
from IPython.display import display, HTML
uid_str = "vitessce" + get_uid_str(uid)
@@ -808,7 +825,7 @@ def ipython_display(config, height=600, theme='auto', base_url=None, host_name=N
"has_host_name": host_name is not None,
"height": height,
"theme": theme,
- "config": config_dict,
+ "_config": config_dict,
"store_urls": [],
}