Skip to content

Commit 809f15c

Browse files
initialize interactive plotting backend for jupyterlab & notebook>=7
1 parent cde6e48 commit 809f15c

1 file changed

Lines changed: 98 additions & 15 deletions

File tree

hypertools/plot/backend.py

Lines changed: 98 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,24 @@
2626
into) is the first in the list.
2727
BACKEND_WARNING : str or None
2828
The warning to be issued upon trying to create an interactive or
29-
animated plot, if any. This is set under two conditions:
29+
animated plot, if any. This is set under three conditions:
3030
1. No compatible interactive backends are available
31-
2. Hypertools was imported into a notebook and the notebook-native
32-
interactive backend (nbAgg) is not available. This should never
31+
2. Hypertools was imported into a notebook that uses the "classic"
32+
notebook JS frontend, and the notebook-native interactive
33+
plotting backend (nbAgg) is not available. This should never
3334
happen, but theoretically could if the
3435
`ipython`/`jupyter`/`jupyter-core`/`notebook` installation is
3536
faulty.
37+
3. Hypertools was imported into a notebook that uses the
38+
JupyterLab JS frontend, the notebook server was launched from a
39+
different Python environment than is used by the IPython
40+
kernel, and the "ipympl" (a.k.a. "widget") interactive plotting
41+
backend is likely to not work properly because the `ipympl`
42+
package is not installed in the server environment. `ipympl` is
43+
a dependency of Hypertools, but when the notebook kernel and
44+
server environments are different, (a compatible version of) it
45+
must also be installed in the server environment to provide the
46+
various JS components needed to display interactive plots.
3647
HYPERTOOLS_BACKEND : str
3748
The `matplotlib` backend used to create interactive and animated
3849
plots.
@@ -73,6 +84,7 @@
7384
import inspect
7485
import os
7586
import shlex
87+
import shutil
7688
import sys
7789
import traceback
7890
import warnings
@@ -542,20 +554,91 @@ def _init_backend():
542554

543555
else:
544556
IS_NOTEBOOK = True
545-
# if running in a notebook, should almost always use nbAgg. May
546-
# eventually let user override this with environment variable
547-
# (e.g., to use ipympl, widget, or WXAgg in JupyterLab), but for
548-
# now this can be changed manually with
549-
# `hypertools.set_interactive_backend` or the `mpl_backend`
550-
# kwarg to `hypertools.plot`
557+
# if running in a notebook, the backend to use depends on the
558+
# user's Jupyter version and frontend. In "classic" Jupyter
559+
# notebooks (i.e., notebook < 7.0), use `nbAgg` since it's the
560+
# most stable, best supported, available out-of-the-box, etc.
561+
# However, that backend no longer works with the new JupyterLab
562+
# JS frontend (which is also used by notebook >= 7.0), so in
563+
# those cases, use the "ipympl"/"widget" plotting backend
564+
# instead.
565+
#
566+
# The user can technically override this by setting the
567+
# HYPERTOOLS_BACKEND environment variable, but for now this is
568+
# undocumented and the two officially supported methods of
569+
# changing the backend hypertools uses for interactive plots are
570+
# to (1) manually set the desired backend after importing
571+
# hypertools with `hypertools.set_interactive_backend`, or (2)
572+
# pass the desired backend identifier to the `mpl_backend` kwarg
573+
# of `hypertools.plot`
574+
#
575+
# Note: the "ipympl"/"widget" backend requires the `ipympl`
576+
# library be installed in both the notebook kernel environment
577+
# AND the notebook server environment, if the two aren't the
578+
# same. The former is guaranteed by hypertools's dependencies,
579+
# but the latter is not, so we must check for it manually.
580+
notebook_frontend = _get_jupyter_frontend()
581+
if notebook_frontend == 'lab':
582+
notebook_backend = 'module://ipympl.backend_nbagg'
583+
else:
584+
notebook_backend = 'nbAgg'
585+
551586
try:
552-
mpl.use('nbAgg')
553-
working_backend = 'nbAgg'
554-
except ImportError:
555-
BACKEND_WARNING = ("Failed to switch to interactive notebook "
556-
"backend ('nbAgg'). Falling back to inline "
557-
"static plots.")
587+
mpl.use(notebook_backend)
588+
except (ImportError, ModuleNotFoundError):
589+
BACKEND_WARNING = (
590+
"Failed to switch to interactive notebook backend "
591+
f"('{notebook_backend}'). Falling back to inline static plots."
592+
)
558593
working_backend = 'inline'
594+
else:
595+
working_backend = notebook_backend
596+
if notebook_frontend == 'lab':
597+
# if the notebook uses the JupyterLab JS frontend and
598+
# the ipympl plotting backend was successfully found in
599+
# the notebook kernel environment (`try` block above),
600+
# determine whether the notebook server environment is
601+
# the same or different
602+
kernel_python = sys.executable
603+
server_python = shutil.which('python')
604+
if server_python != kernel_python:
605+
# if they're different, check whether `ipympl` is
606+
# installed in the server environment
607+
import IPython # guaranteed to be installed at this point
608+
609+
with open(os.devnull, 'w') as devnull, redirect_stdout(devnull):
610+
retcode = IPython.utils.process.system('pip show ipympl')
611+
if retcode != 0:
612+
# NOTE: this is not currently checked, but the
613+
# `ipympl` versions installed in the server and
614+
# kernel environments must also be compatible
615+
# with each other. See
616+
# https://matplotlib.org/ipympl/installing.html#compatibility-table
617+
# NOTE: this check is imperfect and will result
618+
# in a false positive if the user has installed
619+
# just the carved-off JS components from
620+
# `ipympl` via the `jupyter-matplotlib`
621+
# extension instead of installing the full
622+
# package. We *could* account for this by
623+
# additionally checking the output of
624+
# `jupyter labextension check jupyter-matplotlib`,
625+
# but then we'd get into differentiating
626+
# the extension not being installed at all vs.
627+
# being installed but not enabled, etc.
628+
# Ultimately this is a pretty minor edge case
629+
# and the cost of a false positive is pretty
630+
# low (a harmless warning message IF the user
631+
# creates an interactive plot), so IMO it's
632+
# not worth the extra overhead of running more
633+
# shell commands every time hypertools is
634+
# imported.
635+
BACKEND_WARNING = (
636+
"The `ipympl` package is not installed in the "
637+
"Jupyter server's environment. Interactive and "
638+
"animated plots may not appear. To fix this, "
639+
"pip-install `ipympl` and restart the Jupyer "
640+
"server."
641+
)
559642

560643
switch_backend = _switch_backend_notebook
561644
reset_backend = _reset_backend_notebook

0 commit comments

Comments
 (0)