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

POC: sidebar widget plugins #2

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
90 changes: 64 additions & 26 deletions glue_solara/app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pathlib import Path
from typing import Callable, List, Optional, cast
from typing import Any, Callable, List, Tuple, Optional, cast

import glue.core.hub
import glue.core.message
Expand All @@ -11,6 +11,8 @@
from glue.viewers.common.viewer import Viewer
from glue_jupyter.data import require_data

import glue_solara.plugins.multiply_primary

from .hooks import use_glue_watch, use_layers_watch
from .linker import Linker
from .mdi import MDI_HEADER_SIZES, Mdi
Expand Down Expand Up @@ -47,6 +49,33 @@
}


def create_glue_application() -> gj.JupyterApplication:
app = glue_jupyter.app.JupyterApplication()
return app

def init_plugins(app) -> list[Tuple[Callable, Any]]:
return [(glue_solara.plugins.multiply_primary.PluginUI,
glue_solara.plugins.multiply_primary.PluginState(app))]


class AppAPI:
def __init__(self):
self._app = glue_jupyter.app.JupyterApplication()
self._plugins = init_plugins(self._app)

@property
def glue_app(self):
return self._app

@property
def plugins(self):
# return only the API objects
return {p[1].name: p[1] for p in self._plugins if p[1]._is_relevant.value}

def show(self):
solara.display(GlueApp(self._app, self._plugins))


@solara.component
def JupyterApp():
"""Best used in the notebook"""
Expand All @@ -58,17 +87,14 @@ def JupyterApp():
def Page():
"""This component is used by default in solara server (standalone app)"""

def create_glue_application() -> gj.JupyterApplication:
app = glue_jupyter.app.JupyterApplication()
return app

# make the app only once
# make the app and plugins list only once
app = solara.use_memo(create_glue_application, [])
GlueApp(app)
plugins = solara.use_memo(lambda: init_plugins(app), [])
GlueApp(app, plugins)


@solara.component
def GlueApp(app: gj.JupyterApplication):
def GlueApp(app: gj.JupyterApplication, plugins: List[Tuple[Callable, Any]] = []):
# TODO: check if we can limit the messages we listen to
# for better performance (less re-renders)
use_glue_watch(app.session.hub, glue.core.message.Message)
Expand Down Expand Up @@ -171,32 +197,44 @@ def add_to_current_viewer(data: glue.core.Data):
"Subset mode:", style={"font-size": "1.2em", "font-weight": "bold"}
)
solara.Row(children=[app.widget_subset_mode])
solara.v.Divider()
with solara.Card("Data", margin="0", elevation=0):
with solara.Details("Data", expand=False).key('data'):
DataList(
app,
active_viewer_index=viewer_index.value,
on_add_viewer=request_viewer_for,
on_add_data_to_viewer=add_to_current_viewer,
)

if viewer_index.value is not None and view_type.value != "grid":
viewer = app.viewers[viewer_index.value]
solara.v.Divider()
with solara.Card(
"Plot Layers",
children=[
viewer.layer_options,
],
margin="0",
elevation=0,
):
pass
with solara.Details(
summary="Plot Options",
expand=False
).key('plot-options'):
viewer = app.viewers[viewer_index.value]
with solara.Card(
"Plot Layers",
children=[
viewer.layer_options,
],
margin="0",
elevation=0,
):
pass


with solara.Card(
"Plot options", children=[viewer.viewer_options], margin="0", elevation=0
):
pass

for plugin, plugin_state in plugins:
if plugin_state._is_relevant.value:
with solara.Details(
summary=plugin_state.name,
expand=False
).key(f'plugin-{plugin_state.name}'):
plugin(state=plugin_state)

solara.v.Divider()
with solara.Card(
"Plot options", children=[viewer.viewer_options], margin="0", elevation=0
):
pass

with solara.AppBar():
if len(data_collection) > 0:
Expand Down
1 change: 1 addition & 0 deletions glue_solara/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .multiply_primary import *
86 changes: 86 additions & 0 deletions glue_solara/plugins/multiply_primary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import solara
import glue_jupyter as gj
from glue.core.message import DataCollectionAddMessage, DataCollectionDeleteMessage
from glue.core.hub import HubListener

__all__ = ['PluginState', 'PluginUI']

# base class inherited by all plugins
class SidebarWidgetState(HubListener):
def __init__(self, app: gj.JupyterApplication):
self._app = app
self._hub = app.session.hub
self._name = "Multiply PRIMARY Data"
self._is_relevant = solara.reactive(False)

@property
def name(self):
return self._name

def show(self):
solara.display(PluginUI(self))


class PluginState(SidebarWidgetState):
def __init__(self, app: gj.JupyterApplication):
super().__init__(app)
self._multiply_by = solara.reactive(2)
self._message = solara.reactive("")

self._hub.subscribe(self, DataCollectionAddMessage, handler=self._on_data_added)
self._hub.subscribe(self, DataCollectionDeleteMessage, handler=self._on_data_deleted)
# currently no message for adding/removing viewers

@property
def multiply_by(self):
return self._multiply_by.value

@multiply_by.setter
def multiply_by(self, value):
# NOTE: this is only called when setting from the API, not from the UI
# If adding any extra logic that should react to a change to multiply_by,
self._multiply_by.value = value
if value > 6:
self._message.value = f"Multiplying by large value ({value})"
else:
self._message.value = ""

@property
def message(self):
return self._message.value

def _on_data_added(self, msg):
if self._is_relevant.value:
return
if msg.data is not None:
if 'PRIMARY' in [c.label for c in msg.data.components]:
self._is_relevant.value = True
return

def _on_data_deleted(self, msg):
if not self._is_relevant.value:
return
# need to check if 'PRIMARY' in any remaining data collection entry
for data in self._app.data_collection:
if 'PRIMARY' in [c.label for c in data.components]:
# leave is_relevant.value = True
return
else:
self._is_relevant.value = False

def multiply(self):
for data in self._app.data_collection:
if 'PRIMARY' in [c.label for c in data.components]:
data.update_components({data.get_component('PRIMARY'): self.multiply_by * data['PRIMARY']})
for viewer in self._app.viewers:
viewer.state.reset_limits()

@solara.component
def PluginUI(state: PluginState):
solara.SliderInt("Multiply by", value=state.multiply_by, on_value=lambda value: setattr(state, 'multiply_by', value)) # lambda value: state.multiply_by := value
solara.Button(label=f"Multiply by {state.multiply_by}", on_click=state.multiply)
for data in state._app.data_collection:
if 'PRIMARY' in [c.label for c in data.components]:
solara.Text(f"* {data.label}")
if len(state.message):
solara.Warning(state.message)