Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

page_mode and page_esm params #355

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
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
16 changes: 11 additions & 5 deletions vitessce/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ class VitessceConfigView:
A class to represent a view (i.e. visualization component) in the Vitessce view config layout.
"""

def __init__(self, component, coordination_scopes, x, y, w, h):
def __init__(self, component, coordination_scopes, x, y, w, h, uid=None):
"""
Not meant to be instantiated directly, but instead created and returned by the ``VitessceConfig.add_view()`` method.

Expand All @@ -489,14 +489,16 @@ def __init__(self, component, coordination_scopes, x, y, w, h):
"w": w,
"h": h
}
if uid is not None:
self.view["uid"] = uid

def _to_py_params(self):
params_dict = {
"component": self.view["component"],
"x": self.view["x"],
"y": self.view["y"],
"w": self.view["w"],
"h": self.view["h"]
"h": self.view["h"],
}
# Only include coordination_scopes if there are coordination scopes other than
# the coorindation scope for the 'dataset' coordination type.
Expand All @@ -507,6 +509,8 @@ def _to_py_params(self):
}
if len(non_dataset_coordination_scopes) > 0:
params_dict["coordination_scopes"] = non_dataset_coordination_scopes
if "uid" in self.view:
params_dict["uid"] = self.view["uid"]
return params_dict

def get_coordination_scope(self, c_type):
Expand Down Expand Up @@ -1009,7 +1013,7 @@ def get_datasets(self):
"""
return self.config["datasets"]

def add_view(self, view_type, dataset=None, dataset_uid=None, x=0, y=0, w=1, h=1, mapping=None, coordination_scopes=None, props=None):
def add_view(self, view_type, dataset=None, dataset_uid=None, x=0, y=0, w=1, h=1, mapping=None, coordination_scopes=None, props=None, uid=None):
"""
Add a view to the config.

Expand Down Expand Up @@ -1077,7 +1081,7 @@ def add_view(self, view_type, dataset=None, dataset_uid=None, x=0, y=0, w=1, h=1
if coordination_scopes is not None:
internal_coordination_scopes.update(coordination_scopes)
vcv = VitessceConfigView(
component_str, internal_coordination_scopes, x, y, w, h)
component_str, internal_coordination_scopes, x, y, w, h, uid=uid)

# Use the mapping parameter if component is scatterplot and the mapping is not None
if mapping is not None:
Expand Down Expand Up @@ -1737,7 +1741,9 @@ def from_dict(config):
raise ValueError(
"Multiple datasets are present, so every view must have an explicit dataset coordination scope.")
new_view = VitessceConfigView(
c['component'], c_coord_scopes, c['x'], c['y'], c['w'], c['h'])
c['component'], c_coord_scopes, c['x'], c['y'], c['w'], c['h'],
uid=(c["uid"] if "uid" in c else None)
)
if 'props' in c.keys():
new_view.set_props(**c['props'])
vc.config['layout'].append(new_view)
Expand Down
87 changes: 76 additions & 11 deletions vitessce/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ def get_uid_str(uid):
const storeUrls = view.model.get('store_urls');
const invokeTimeout = view.model.get('invoke_timeout');

const pageMode = view.model.get('page_mode');
const pageEsm = view.model.get('page_esm');

const pkgName = (jsDevMode ? "@vitessce/dev" : "vitessce");

importMap.imports["vitessce"] = (customJsUrl.length > 0
Expand All @@ -217,8 +220,10 @@ def get_uid_str(uid):
PluginViewType,
PluginCoordinationType,
PluginJointFileType,
PluginAsyncFunction,
z,
useCoordination,
usePageModeView,
useGridItemSize,
// TODO: names and function signatures are subject to change for the following functions
// Reference: https://github.com/keller-mark/use-coordination/issues/37#issuecomment-1946226827
Expand All @@ -234,6 +239,7 @@ def get_uid_str(uid):
let pluginCoordinationTypes = [];
let pluginFileTypes = [];
let pluginJointFileTypes = [];
let pluginAsyncFunctions = [];

const stores = Object.fromEntries(
storeUrls.map(storeUrl => ([
Expand Down Expand Up @@ -263,12 +269,13 @@ def get_uid_str(uid):
const pluginModule = (await import(pluginEsmUrl)).default;
URL.revokeObjectURL(pluginEsmUrl);

const pluginsObj = await pluginModule.createPlugins({
const pluginDeps = {
React,
PluginFileType,
PluginViewType,
PluginCoordinationType,
PluginJointFileType,
PluginAsyncFunction,
z,
invokeCommand: invokePluginCommand,
useCoordination,
Expand All @@ -279,7 +286,8 @@ def get_uid_str(uid):
useComplexCoordinationSecondary,
useCoordinationScopes,
useCoordinationScopesBy,
});
};
const pluginsObj = await pluginModule.createPlugins(pluginDeps);
if(Array.isArray(pluginsObj.pluginViewTypes)) {
pluginViewTypes = [...pluginViewTypes, ...pluginsObj.pluginViewTypes];
}
Expand All @@ -292,7 +300,28 @@ def get_uid_str(uid):
if(Array.isArray(pluginsObj.pluginJointFileTypes)) {
pluginJointFileTypes = [...pluginJointFileTypes, ...pluginsObj.pluginJointFileTypes];
}
if(Array.isArray(pluginsObj.pluginAsyncFunctions)) {
pluginAsyncFunctions = [...pluginAsyncFunctions, ...pluginsObj.pluginAsyncFunctions];
}
} catch(e) {
console.error("Error loading plugin ESM or executing createPlugins function.");
console.error(e);
}
}

let PageComponent;
if(pageMode && pageEsm.length > 0) {
try {
const pageEsmUrl = URL.createObjectURL(new Blob([pageEsm], { type: "text/javascript" }));
const pageModule = (await import(pageEsmUrl)).default;
URL.revokeObjectURL(pageEsmUrl);

const pageDeps = {
usePageModeView,
};
PageComponent = await pageModule.createPage(pageDeps);
} catch(e) {
console.error("Error loading page ESM or executing createPage function.")
console.error(e);
}
}
Expand Down Expand Up @@ -360,14 +389,17 @@ def get_uid_str(uid):

const vitessceProps = {
height, theme, config, onConfigChange, validateConfig,
pluginViewTypes, pluginCoordinationTypes, pluginFileTypes, pluginJointFileTypes,
remountOnUidChange, stores,
pluginViewTypes, pluginCoordinationTypes,
pluginFileTypes,pluginJointFileTypes, pluginAsyncFunctions,
remountOnUidChange, stores, pageMode,
};

return e('div', { ref: divRef, style: { height: height + 'px' } },
e(React.Suspense, { fallback: e('div', {}, 'Loading...') },
e(React.StrictMode, {},
e(Vitessce, vitessceProps)
e(Vitessce, vitessceProps,
(pageMode ? e(PageComponent, {}) : null)
),
),
),
);
Expand Down Expand Up @@ -401,6 +433,7 @@ def get_uid_str(uid):
PluginViewType,
PluginCoordinationType,
PluginJointFileType,
PluginAsyncFunction,
z,
useCoordination,
invokeCommand,
Expand All @@ -410,11 +443,32 @@ def get_uid_str(uid):
pluginFileTypes: undefined,
pluginCoordinationTypes: undefined,
pluginJointFileTypes: undefined,
pluginAsyncFunctions: undefined,
};
}
export default { createPlugins };
"""

DEFAULT_PAGE_ESM = """
function createPage(utilsForPage) {
const {
usePageModeView,
} = utilsForPage;

function PageComponent(props) {
const BiomarkerSelect = usePageModeView('biomarker-select');

return (
<div>
<BiomarkerSelect />
</div>
);
}
return PageComponent;
}
export default { createPage };
"""


class VitesscePlugin:
"""
Expand Down Expand Up @@ -454,16 +508,18 @@ class VitessceWidget(anywidget.AnyWidget):

next_port = DEFAULT_PORT

js_package_version = Unicode('3.4.14').tag(sync=True)
js_package_version = Unicode('3.5.0').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)
remount_on_uid_change = Bool(True).tag(sync=True)
invoke_timeout = Int(30000).tag(sync=True)
page_mode = Bool(False).tag(sync=True)
page_esm = Unicode('').tag(sync=True)

store_urls = List(trait=Unicode(''), default_value=[]).tag(sync=True)
invoke_timeout = Int(30000).tag(sync=True)

def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=False, js_package_version='3.4.14', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True, invoke_timeout=30000):
def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=False, js_package_version='3.5.0', js_dev_mode=False, custom_js_url='', plugins=None, remount_on_uid_change=True, invoke_timeout=30000, page_mode=False, page_esm=None):
"""
Construct a new Vitessce widget.

Expand All @@ -476,9 +532,11 @@ def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=
: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[WidgetPlugin] plugins: A list of subclasses of VitesscePlugin. Optional.
:param list[VitesscePlugin] plugins: A list of subclasses of VitesscePlugin. Optional.
:param bool remount_on_uid_change: Passed to the remountOnUidChange prop of the <Vitessce/> React component. By default, True.
:param int invoke_timeout: The timeout in milliseconds for invoking Python functions from JavaScript. By default, 30000.
:param bool page_mode: Whether to render the <Vitessce/> 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.

.. code-block:: python
:emphasize-lines: 4
Expand Down Expand Up @@ -510,7 +568,9 @@ def __init__(self, config, height=600, theme='auto', uid=None, port=None, proxy=
super(VitessceWidget, self).__init__(
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, invoke_timeout=invoke_timeout,
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),
invoke_timeout=invoke_timeout,
uid=uid_str, store_urls=list(self._stores.keys())
)

Expand Down Expand Up @@ -576,7 +636,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.4.14', js_dev_mode=False, custom_js_url='', plugin_esm=DEFAULT_PLUGIN_ESM, remount_on_uid_change=True):
def ipython_display(config, height=600, theme='auto', base_url=None, host_name=None, uid=None, port=None, proxy=False, js_package_version='3.5.0', js_dev_mode=False, custom_js_url='', plugin_esm=DEFAULT_PLUGIN_ESM, remount_on_uid_change=True, page_mode=False, page_esm=None):
from IPython.display import display, HTML
uid_str = "vitessce" + get_uid_str(uid)

Expand All @@ -586,12 +646,17 @@ def ipython_display(config, height=600, theme='auto', base_url=None, host_name=N
routes = config.get_routes()
serve_routes(config, routes, use_port)

plugins = plugins or []
plugin_esm = [p.plugin_esm for p in plugins]

model_vals = {
"uid": uid_str,
"js_package_version": js_package_version,
"js_dev_mode": js_dev_mode,
"custom_js_url": custom_js_url,
"plugin_esm": plugin_esm,
"page_mode": page_mode,
"page_esm": ('' if page_esm is None else page_esm),
"remount_on_uid_change": remount_on_uid_change,
"invoke_timeout": 30000,
"proxy": proxy,
Expand Down
Loading