Source code for mast_aladin.app_sidecar

import solara
import warnings
from ipyaladin import Aladin
from sidecar import Sidecar as UpstreamSidecar
from mast_table import MastTable
from mast_aladin.app import MastAladin, gca
import jdaviz

try:
    from jdaviz.core.helpers import ConfigHelper
except ImportError:
    ConfigHelper = None

default_height = 500
default_anchor = 'split-bottom'


def is_jdaviz(app):
    """
    If jdaviz can be imported, check app is instanace of ConfigHelper;
    otherwise you can't have a jdaviz app:
    """
    if ConfigHelper is not None:
        return isinstance(app, ConfigHelper)

    return False


def is_aladin(app):
    return isinstance(app, Aladin)


class AppSidecarManager:
    _sidecar_context = None
    _jdaviz_counter = 0
    _aladin_counter = 0
    _other_counter = 0

    def __init__(self):
        self.loaded_apps = []

    def open(
        self,
        *apps,
        anchor=None,
        use_current_apps=False,
        titles=None,
        include_aladin=False,
        include_jdaviz=False,
        close_existing=True,
        height=default_height,
    ):
        """
        Open ``apps`` in a sidecar [1]_. If none are given and
        ``include_aladin`` and ``include_jdaviz`` are `True`,
        open a sidecar with one of each.

        Parameters
        ----------
        anchor : str or list of str, optional (default: None)
            One or more of the anchor location options available from
            ``jupyterlab-sidecar``, which include:

                {'split-right', 'split-left', 'split-top',
                 'split-bottom', 'tab-before', 'tab-after',
                 'right'}

            - If no anchor is provided, the first sidecar will have anchor
            'split-bottom', and subsequent sidecars will be 'split-right'
            relative to the previous sidecar.
            - If multiple anchors are provided, each app is launched in a new
            sidecar, and each anchor defines the new sidecar's position relative
            to the previous sidecar.

        use_current_apps : bool, optional (default is `False`)
            If `True`, get the last constructed jdaviz and
            mast-aladin instances to open in the sidecar

        titles : str or list of str, optional (default: None)
            Title to appear in the tab label for each sidecar in
            jupyterlab. If `None`, label sidecars sequentially wit
            "Sidecar 0", "Sidecar 1", etc.

        include_aladin : bool, optional (default is `False`)
            The sidecar must include at least one
            mast-aladin instance. If none are already
            available, a new one will be created.

        include_jdaviz : bool, optional (default is `False`)
            The sidecar must include at least one
            jdaviz instance. If none are already available,
            a new one will be created.

        close_existing : bool, optional (default is `True`)
            Close existing sidecar(s) before opening a new one.

        References
        ----------
        .. [1] https://github.com/jupyter-widgets/jupyterlab-sidecar
        """
        # initialize the object here:
        # This must be run first because we don't have the ability to close multiple
        # sidecars without possibly closing all widgets
        if close_existing:
            self.close_all()

        apps, default_titles = self._resolve_apps(
            apps, include_aladin, include_jdaviz, use_current_apps
        )

        if isinstance(titles, str):
            titles = [titles]
        elif titles is None:
            titles = default_titles

        if not apps:
            raise ValueError("No apps to show in sidecar.")

        self.loaded_apps += apps

        self._attach_sidecars(apps, anchor, titles)

        self._display_sidecar_contents(apps, height)

        return tuple(apps)

    def _resolve_apps(self, apps, include_aladin, include_jdaviz, use_current_apps):
        """
        Ensure requested apps exist, creating or reusing as needed.
        """
        apps = list(apps)

        if not len(apps) and not include_aladin and not include_jdaviz:
            # if no apps are given, include one of each:
            include_jdaviz = include_aladin = True

        mal_instances = [app for app in apps if is_aladin(app)]
        jdaviz_instances = [app for app in apps if is_jdaviz(app)]

        if not len(mal_instances) and include_aladin:
            mal = gca()
            if not use_current_apps or (use_current_apps and mal is None):
                mal = MastAladin()
            apps.append(mal)

        try:
            jdaviz_instances = [app for app in apps if is_jdaviz(app)]

            if not len(jdaviz_instances) and include_jdaviz:
                if not use_current_apps or jdaviz.gca() is None:
                    jdaviz.new_app()
                viz = jdaviz.gca()
                apps.append(viz)

        except ImportError:
            warnings.warn(
                "`AppSidecar` found that jdaviz was not installed. To install it, "
                "run `pip install jdaviz`.",
                UserWarning
            )

        default_titles = []
        for app in apps:
            if is_jdaviz(app):
                default_titles.append(
                    "jdaviz" + (f" ({self._jdaviz_counter})" if self._jdaviz_counter else '')
                )
                self._jdaviz_counter += 1
            elif is_aladin(app):
                default_titles.append(
                    "mast-aladin" + (f" ({self._aladin_counter})" if self._aladin_counter else '')
                )
                self._aladin_counter += 1
            else:
                default_titles.append(
                    "Sidecar" + (f" ({self._other_counter})" if self._other_counter else '')
                )
                self._other_counter += 1
        return apps, default_titles

    def _attach_sidecars(self, apps, anchor, titles):
        """
        Attach apps to sidecars. If only one anchor, all apps share
        a single sidecar. Otherwise, create one sidecar per app.
        In the multiple case, each sidecar `n` will reference the
        `n-1` sidecar instance.
        """

        anchor = self._normalize_anchor(anchor, apps)
        ctx = None
        for app, anc, title in zip(apps, anchor, titles):
            ctx = UpstreamSidecar(anchor=anc, title=title, ref=ctx)
            app.sidecar = ctx

    def _normalize_anchor(self, anchor, apps):
        """
        Set the default (sidecar 1: split-bottom, otherwise: split-right relative
        to last sidecar), and check that `anchor` such that `len(anchor)==len(apps)`
        """

        if isinstance(anchor, str):
            anchor = [anchor]
        elif anchor is None:
            anchor = [default_anchor]

        n_apps = len(apps)
        n_anchors = len(anchor)

        if n_anchors < n_apps:
            return anchor + ['split-right'] * (n_apps - n_anchors)

        return anchor

    def _display_sidecar_contents(self, apps, height):
        @solara.component
        def SidecarContents(apps):
            style = f"height={height} !important;"

            with solara.Columns(len(apps) * [1], gutters_dense=True) as main:
                for app in apps:

                    if is_aladin(app):
                        # MastAladin:
                        with solara.Column(gap='0px', style=style):
                            solara.display(app)

                    elif is_jdaviz(app):
                        # jdaviz:
                        with solara.Column(gap='0px', style=style):
                            solara.display(app._app)

                    else:
                        # other:
                        with solara.Column(gap='0px'):
                            solara.display(app)

                    set_app_height(app, height)

            return main

        for app in apps:
            with app.sidecar:
                solara.display(SidecarContents(apps=[app]))

    def close_all(self):
        """
        Close this particular `sidecar` instance.
        """
        for app in self.loaded_apps:
            # close jdaviz apps within the sidecar:
            if is_jdaviz(app):
                app._app.close()

            # now close sidecar(s):
            if getattr(app, 'sidecar', None) is not None:
                app.sidecar.close()
        self.loaded_apps = []

    def resize_all(self, height=default_height):
        """
        Resize all opened sidecars with ``height`` in pixels.
        """
        for app in self.loaded_apps:
            set_app_height(app, height)


[docs] def set_app_height(app, height): """ For an app instance ``app``, set the app height to be ``height`` pixels. ``height`` may be an integer in units of pixels, or "100%". """ if is_jdaviz(app): if isinstance(height, int): height = f"{height}px" app._app.layout.height = height app._app.state.settings['context']['notebook']['max_height'] = height elif is_aladin(app): if height == '100%': app.height = -1 elif isinstance(height, int): app.height = height elif isinstance(app, MastTable): if isinstance(height, int): height = f"{height}px" app.layout.height = height
AppSidecar = AppSidecarManager()