Skip to content

Commit b07a72a

Browse files
authored
Create module for plugin JS (#337)
* Create module for plugin JS * Add docs * Lint * Ignore unused imports
1 parent b2e3120 commit b07a72a

File tree

8 files changed

+246
-1
lines changed

8 files changed

+246
-1
lines changed

docs/data_options.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,5 +106,44 @@ Jupyter process: remote service like Colab/Binder; Files: remote & accessed via
106106

107107
Unfortunately, this will not work because the remote server cannot access the files that are on another machine behind SSH.
108108

109+
========================================================================
110+
Jupyter process: anywhere; Files: anywhere that can be accessed via Zarr
111+
========================================================================
109112

113+
If the data is readable via Zarr (i.e., `zarr.storage.*Store`) and the Jupyter process can access the store contents, then the Vitessce widget can access the data by specifying the Zarr store as the data source for Vitessce data wrapper class instances.
114+
This is currently supported for the ``AnnDataWrapper`` class using the ``adata_store`` parameter (as opposed to ``adata_path`` or ``adata_url``).
115+
116+
.. code-block:: python
117+
118+
from vitessce import VitessceConfig, AnnDataWrapper
119+
120+
# ...
121+
adata.write_zarr("my_store.adata.zarr")
122+
123+
vc = VitessceConfig(name="My Vitessce Configuration")
124+
vc.add_dataset(name="My Dataset").add_object(AnnDataWrapper(
125+
adata_store="my_store.adata.zarr",
126+
# ...
127+
))
128+
# ...
129+
vc.widget()
130+
131+
132+
Or, with a Zarr store instance (instead of a local string path to a DirectoryStore):
133+
134+
.. code-block:: python
135+
136+
import zarr
137+
from vitessce import VitessceConfig, AnnDataWrapper
138+
139+
# ...
140+
store = zarr.storage.FSStore("s3://my_bucket/path/to/my_store.adata.zarr")
141+
142+
vc = VitessceConfig(name="My Vitessce Configuration")
143+
vc.add_dataset(name="My Dataset").add_object(AnnDataWrapper(
144+
adata_store=store,
145+
# ...
146+
))
147+
# ...
148+
vc.widget()
110149

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ The Vitessce widget is compatible with the following interactive Python platform
4848
api_config
4949
api_data
5050
data_options
51+
widget_plugins
5152
screenshots
5253

5354

docs/widget_plugins.rst

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
Widget plugins
2+
##############
3+
4+
Vitessce supports multiple types of `plugins <http://vitessce.io/docs/dev-plugins/>`_ defined using JavaScript code.
5+
6+
Leveraging concepts from `anywidget <https://github.com/manzt/anywidget>`_, we can define such plugins directly from Python: plugin developers can supply custom JavaScript code via a Python string.
7+
8+
The most minimal example of such plugin JavaScript code is the following:
9+
10+
.. code-block:: python
11+
12+
PLUGIN_ESM = """
13+
function createPlugins(utilsForPlugins) {
14+
const {
15+
React,
16+
PluginFileType,
17+
PluginViewType,
18+
PluginCoordinationType,
19+
PluginJointFileType,
20+
z,
21+
useCoordination,
22+
} = utilsForPlugins;
23+
return {
24+
pluginViewTypes: undefined,
25+
pluginFileTypes: undefined,
26+
pluginCoordinationTypes: undefined,
27+
pluginJointFileTypes: undefined,
28+
};
29+
}
30+
export default { createPlugins };
31+
"""
32+
33+
34+
The plugin string must be defined as an EcmaScript Module (ESM) that exports a function named ``createPlugins``.
35+
The ``createPlugins`` function is called (on the initial render of the Jupyter widget) with the ``utilsForPlugins`` argument (to facilitate dependency injection) and returns an object with the following properties:
36+
37+
- ``pluginViewTypes``: an array of objects that define the view types of the plugin.
38+
- ``pluginFileTypes```: an array of objects that define the file types of the plugin.
39+
- ``pluginCoordinationTypes``: an array of objects that define the coordination types of the plugin.
40+
- ``pluginJointFileTypes``: an array of objects that define the joint file types of the plugin.
41+
42+
If defined, these plugin arrays are passed to the Vitessce component as `props <http://vitessce.io/docs/dev-plugins/>`_ with the same names.
43+
44+
**Note**: For maximum stability of plugins, we recommend that plugin developers document which version(s) of the vitessce Python package that plugins have been developed under.
45+
46+
--------------------------------
47+
Passing plugin ESM to the widget
48+
--------------------------------
49+
50+
The plugin string can be passed to the widget using the ``plugin_esm`` parameter:
51+
52+
53+
.. code-block:: python
54+
55+
from vitessce import VitessceConfig
56+
57+
vc = VitessceConfig(
58+
description="A Vitessce widget with a custom plugin",
59+
widget=[
60+
{
61+
"plugin": PLUGIN_ESM,
62+
},
63+
],
64+
)
65+
# Some more configuration here...
66+
67+
vc.widget(plugin_esm=PLUGIN_ESM)
68+
69+
70+
-------------------------------
71+
Defining plugin views using JSX
72+
-------------------------------
73+
74+
Vitessce plugin view types are defined as React components.
75+
During typical React component development, JSX syntax is used.
76+
However, JSX is not valid JavaScript and therefore must be transformed to valid JavaScript before it can be passed to the widget where it will be interpreted as ESM.
77+
Vitessce plugin developers then have two options for defining React components for plugin view types:
78+
79+
* Use ``React.createElement`` directly (without JSX).
80+
* Use the ``transform`` function from `esbuild_py <https://github.com/keller-mark/esbuild-py>`_ to perform JSX to JS transformation.
81+
82+
.. code-block:: python
83+
84+
from esbuild_py import transform
85+
86+
PLUGIN_ESM = transform("""
87+
function createPlugins(utilsForPlugins) {
88+
const {
89+
React,
90+
PluginFileType,
91+
PluginViewType,
92+
PluginCoordinationType,
93+
PluginJointFileType,
94+
z,
95+
useCoordination,
96+
} = utilsForPlugins;
97+
98+
function MyPluginView(props) {
99+
return (
100+
<p>Hello world from JSX!</p>
101+
);
102+
}
103+
104+
const pluginViewTypes = [
105+
new PluginViewType('myPlugin', MyPluginView, []),
106+
];
107+
return { pluginViewTypes };
108+
}
109+
export default { createPlugins };
110+
""")
111+
112+
113+
To import additional dependencies, JavaScript (more specifically, ESM) can be dynamically imported from CDN (such as ``unpkg`` or ``esm.sh``) within the ``createPlugins`` function:
114+
115+
.. code-block:: python
116+
117+
PLUGIN_ESM = """
118+
async function createPlugins(utilsForPlugins) {
119+
const {
120+
React,
121+
PluginFileType,
122+
PluginViewType,
123+
PluginCoordinationType,
124+
PluginJointFileType,
125+
z,
126+
useCoordination,
127+
} = utilsForPlugins;
128+
129+
const d3 = await import('https://cdn.jsdelivr.net/npm/d3@7/+esm');
130+
131+
// Do something with d3 here...
132+
133+
return {
134+
pluginViewTypes: undefined,
135+
pluginFileTypes: undefined,
136+
pluginCoordinationTypes: undefined,
137+
pluginJointFileTypes: undefined,
138+
};
139+
}
140+
export default { createPlugins };
141+
"""
142+
143+
144+
To support more complex import scenarios, see `dynamic-importmap <https://github.com/keller-mark/dynamic-importmap>`_.
145+
146+
147+
148+
vitessce.widget_plugins
149+
***********************
150+
151+
.. automodule:: vitessce.widget_plugins.demo_plugin
152+
:members:

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ docs = [
7373
]
7474
all = [
7575
'jupyter-server-proxy>=1.5.2',
76+
'esbuild_py>=0.1.3',
7677
'anywidget>=0.9.10',
7778
'uvicorn>=0.17.0',
7879
'ujson>=4.0.1',

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ per-file-ignores =
66
# Special case: names are reimported from __init__.py, so unused imports are expected.
77
vitessce/__init__.py: F401
88
vitessce/data_utils/__init__.py: F401
9+
vitessce/widget_plugins/__init__.py: F401
910
ignore =
1011
E501, # Ignore line too long
1112
W605, # Ignore invalid escape sequence '\*'

vitessce/widget_plugins/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .demo_plugin import PLUGIN_ESM as demo_plugin_esm
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from esbuild_py import transform
2+
3+
PLUGIN_ESM = transform("""
4+
function createPlugins(utilsForPlugins) {
5+
const {
6+
React,
7+
PluginFileType,
8+
PluginViewType,
9+
PluginCoordinationType,
10+
PluginJointFileType,
11+
z,
12+
useCoordination,
13+
} = utilsForPlugins;
14+
function DemoView(props) {
15+
const { coordinationScopes } = props;
16+
const [{
17+
obsType,
18+
}, {
19+
setObsType,
20+
}] = useCoordination(['obsType'], coordinationScopes);
21+
22+
return (
23+
<div className="demo">
24+
<p>Demo plugin view</p>
25+
<p>obsType: {obsType}</p>
26+
<button>This is an example button</button>
27+
</div>
28+
);
29+
}
30+
31+
const pluginViewTypes = [
32+
new PluginViewType('demo', DemoView, ['obsType']),
33+
];
34+
return { pluginViewTypes };
35+
}
36+
export default { createPlugins };
37+
""")
38+
"""
39+
Example of a minimal plugin view that gets the obsType coordination value from the coordination space and renders a button.
40+
This plugin view is not meant to be useful for end-users, but rather to demonstrate how to develop a plugin view that uses coordination (and uses eslint_py for JSX transformation).
41+
42+
:meta hide-value:
43+
44+
.. code-block:: python
45+
46+
from vitessce.widget_plugins import demo_plugin_esm
47+
48+
# ...
49+
vc.widget(plugin_esm=demo_plugin_esm)
50+
"""

vitessce/wrappers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -943,7 +943,7 @@ def __init__(self, adata_path=None, adata_url=None, adata_store=None, obs_featur
943943
944944
:param str adata_path: A path to an AnnData object written to a Zarr store containing single-cell experiment data.
945945
:param str adata_url: A remote url pointing to a zarr-backed AnnData store.
946-
:param adata_store: A path to pass to zarr.FSStore, or an existing store instance.
946+
:param adata_store: A path to pass to zarr.DirectoryStore, or an existing store instance.
947947
:type adata_store: str or zarr.Storage
948948
:param str obs_feature_matrix_path: Location of the expression (cell x gene) matrix, like `X` or `obsm/highly_variable_genes_subset`
949949
:param str feature_filter_path: A string like `var/highly_variable` used in conjunction with `obs_feature_matrix_path` if obs_feature_matrix_path points to a subset of `X` of the full `var` list.

0 commit comments

Comments
 (0)