Source code for bluesky_widgets.headless.figures

import collections.abc
from pathlib import Path
import matplotlib

from ..models.plot_specs import FigureSpec, FigureSpecList
from .._matplotlib_axes import MatplotlibAxes
from ..utils.dict_view import DictView


[docs]class HeadlessFigures: """ A headless "view" for a FigureSpecList model. It does not produce a graphical user interface. Instead, it provides methods for exporting figures as images. Examples -------- Export all the figures to a directory. They will be named by their title. If there are duplciate titles, a counting number will appended like x-1.png, x-2.png. >>> headless = HeadlessFigures(model) >>> headless.export_all("path/to/directory/") Control the format. >>> headless.export_all("path/to/directory/", format="png") >>> headless.export_all("path/to/directory/", format="jpg") """ def __init__(self, model: FigureSpecList): self.model = model # Map Figure UUID to widget with HeadlessFigure self._figures = {} for figure_spec in model: self._add_figure(figure_spec) model.events.added.connect(self._on_figure_added) model.events.removed.connect(self._on_figure_removed) @property def figures(self): "Read-only access to the mapping FigureSpec UUID -> HeadlessFigure" 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): "Create a new matplotlib Figure." figure = HeadlessFigure(figure_spec) self._figures[figure_spec.uuid] = figure def _on_figure_removed(self, event): "Remove the associated tab and close its canvas." figure_spec = event.item figure = self._figures[figure_spec.uuid] figure.close_figure() del self._figures[figure_spec.uuid] def close_figures(self): for figure in self._figures.values(): figure.close_figure() close = close_figures
[docs] def export_all(self, directory, format="png", **kwargs): """ Export all figures. Parameters ---------- directory : str | Path format : str, optional Default is "png". **kwargs : Passed through to matplotlib.figure.Figure.savefig Returns ------- filenames : List[String] """ # Avoid name collisions in the case of duplicate titles by appending # "-1", "-2", "-3", ... to duplicates. titles_tallied = {} filenames = [] for figure_spec in self.model: title = figure_spec.title if title in titles_tallied: filename = f"{title}-{titles_tallied[title]}" titles_tallied[title] += 1 else: filename = title titles_tallied[title] = 1 filename = str(Path(directory, f"{filename}.{format}")) figure = self._figures[figure_spec.uuid] figure.export(filename, format=format, **kwargs) filenames.append(filename) return filenames
[docs]class HeadlessFigure: """ A Headless "view" for a FigureSpec model. This always contains one Figure. Examples -------- Export the figure. >>> headless = HeadlessFigure(model) >>> headless.export("my-figure.png") """ def __init__(self, model: FigureSpec): 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) model.events.title.connect(self._on_title_changed) # 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) def close_figure(self): _close_figure(self.figure) close = close_figure
[docs] def export(self, filename, format="png", **kwargs): """ Export figure. Parameters ---------- filename : str | Path format : str, optional Default is "png". **kwargs : Passed through to matplotlib.figure.Figure.savefig """ self.figure.savefig(str(filename), format=format, **kwargs)
def _make_figure(figure_spec): "Create a Figure and Axes." matplotlib.use("Agg") # must set before importing matplotlib.pyplot import matplotlib.pyplot as plt # noqa # TODO Let FigureSpec give different options to subplots here, # but verify that number of axes created matches the number of axes # specified. fig, axes = plt.subplots(len(figure_spec.axes)) # Handl return type instability in plt.subplots. if not isinstance(axes, collections.abc.Iterable): axes = [axes] return fig, axes def _close_figure(figure): """ Workaround for matplotlib regression relating to closing figures in Agg See https://github.com/matplotlib/matplotlib/pull/18184/ """ # TODO It would be better to switch the approach based on matplotlib # versions known to have this problem, rather than blindly trying. Update # this once a fixed has been released and we know the earliest version of # matplotlib that does not have this bug. try: figure.canvas.close() except AttributeError: from matplotlib._pylab_helpers import Gcf num = next( ( manager.num for manager in Gcf.figs.values() if manager.canvas.figure == figure ), None, ) if num is not None: Gcf.destroy(num)