Source code for bluesky_widgets.jupyter.figures
import collections.abc
from ipywidgets import widgets
from ..models.plot_specs import FigureSpec, FigureSpecList
from .._matplotlib_axes import MatplotlibAxes
from ..utils.dict_view import DictView
def _initialize_mpl():
"Set backend to ipympl and import pyplot."
import matplotlib
matplotlib.use(
"module://ipympl.backend_nbagg"
) # must set before importing matplotlib.pyplot
# must import matplotlib.pyplot here because bluesky.utils.during_task
# expects it to be imported
import matplotlib.pyplot as plt # noqa
[docs]class JupyterFigures(widgets.Tab):
"""
A Jupyter (ipywidgets) view for a FigureSpecList model.
"""
def __init__(self, model: FigureSpecList, *args, **kwargs):
_initialize_mpl()
super().__init__(*args, **kwargs)
self.model = model
# Map Figure UUID to widget with JupyterFigureTab
self._figures = {}
for figure_spec in model:
self._add_figure(figure_spec)
self.model.events.added.connect(self._on_figure_added)
self.model.events.removed.connect(self._on_figure_removed)
@property
def figures(self):
"Read-only access to the mapping FigureSpec UUID -> JupyterFigure"
return DictView(self._figures)
def _on_figure_added(self, event):
figure_spec = event.item
self._add_figure(figure_spec)
def _add_figure(self, figure_spec):
"Add a new tab with a matplotlib Figure."
tab = _JupyterFigureTab(figure_spec, parent=self)
self._figures[figure_spec.uuid] = tab
self.children = (*self.children, tab)
index = len(self.children) - 1
self.set_title(index, figure_spec.title)
figure_spec.events.title.connect(self._on_title_changed)
figure_spec.events.short_title.connect(self._on_short_title_changed)
# Workaround: If the tabs are cleared and then children are added
# again, no tab is selected.
if index == 0:
self.selected_index = 0
def _on_figure_removed(self, event):
"Remove the associated tab and close its canvas."
figure_spec = event.item
widget = self._figures[figure_spec.uuid]
children = list(self.children)
children.remove(widget)
self.children = tuple(children)
widget.close_figure()
del self._figures[figure_spec.uuid]
def _on_short_title_changed(self, event):
"This sets the tab title."
figure_spec = event.figure_spec
widget = self._figures[figure_spec.uuid]
index = self.children.index(widget)
# Fall back to title if short_title is being unset.
if event.value is None:
self.set_title(index, figure_spec.title)
else:
self.set_title(index, event.value)
def _on_title_changed(self, event):
"This sets the tab title only if short_title is None."
figure_spec = event.figure_spec
if figure_spec.short_title is None:
widget = self._figures[figure_spec.uuid]
index = self.children.index(widget)
self.set_title(index, event.value)
def on_close_tab_requested(self, model):
# When closing is initiated from the view, remove the associated
# model.
self.model.remove(model)
[docs]class JupyterFigure(widgets.HBox):
"""
A Jupyter view for a FigureSpec model. This always contains one Figure.
"""
def __init__(self, model: FigureSpec):
_initialize_mpl()
super().__init__()
self.model = model
self.figure, self.axes_list = _make_figure(model)
self.figure.suptitle(model.title)
self._axes = {}
for axes_spec, axes in zip(model.axes, self.axes_list):
self._axes[axes_spec.uuid] = MatplotlibAxes(model=axes_spec, axes=axes)
self.children = (self.figure.canvas,)
model.events.title.connect(self._on_title_changed)
# By "resizing" (even without actually changing the size) we bump the
# ipympl machinery that sets up frontend--backend communication and
# starting displaying data from the figure. Without this, the figure
# *widget* displays instantly but the actual *plot* (the PNG data sent from
# matplotlib) is not displayed until cell execution completes.
_, _, width, height = self.figure.bbox.bounds
self.figure.canvas.manager.resize(width, height)
self.figure.canvas.draw_idle()
# The FigureSpec model does not currently allow axes to be added or
# removed, so we do not need to handle changes in model.axes.
@property
def axes(self):
"Read-only access to the mapping AxesSpec UUID -> MatplotlibAxes"
return DictView(self._axes)
def _on_title_changed(self, event):
self.figure.suptitle(event.value)
self._redraw()
def _redraw(self):
"Redraw the canvas."
self.figure.canvas.draw_idle()
def close_figure(self):
self.figure.canvas.close()
class _JupyterFigureTab(widgets.HBox):
"""
A tab in a widgets.Tab container that contains a JupyterFigure.
This is aware of its parent in order to support tab-closing.
"""
def __init__(self, model: FigureSpec, parent):
super().__init__()
self.model = model
self.parent = parent
self.button = widgets.Button(description="Close")
self.button.on_click(lambda _: self.parent.on_close_tab_requested(self.model))
self._jupyter_figure = JupyterFigure(model)
self.children = (self._jupyter_figure, self.button)
# Pass-through accessors to match the API of QtFigure, which has/needs
# one less layer.
self.figure = self._jupyter_figure.figure
def close_figure(self):
# Pass through toe JupyterFigure instance.
return self._jupyter_figure.close_figure()
@property
def axes(self):
"Read-only access to the mapping AxesSpec UUID -> MatplotlibAxes"
return DictView(self._jupyter_figure.axes)
def _make_figure(figure_spec):
"Create a Figure and Axes."
# This import must be deferred until after the matplotlib backend is set,
# which happens when a JupyterFigure or JupyterFigures is instantiated
# for the first time.
import matplotlib.pyplot as plt
# By default, with interactive mode on, each fig.show() will be called
# automatically, and we'll get duplicates littering the output area. We
# only want to see the figures where they are placed explicitly in widgets.
plt.ioff()
# TODO Let FigureSpec give different options to subplots here,
# but verify that number of axes created matches the number of axes
# specified.
figure, axes = plt.subplots(len(figure_spec.axes))
# Handle return type instability in plt.subplots.
if not isinstance(axes, collections.abc.Iterable):
axes = [axes]
return figure, axes