Skip to content

Commit

Permalink
add cued containers for pyobjects matplotlib figures
Browse files Browse the repository at this point in the history
* add CueOutput -> (CueObject, CueFigure) with css
* add regression tests with reference screenshots for the construction of a figure
  • Loading branch information
agoscinski committed Dec 21, 2023
1 parent 76112f9 commit bb2aeb0
Show file tree
Hide file tree
Showing 13 changed files with 686 additions and 7 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencies = [
"ipywidgets>=8.0.0",
"numpy",
"widget_code_input",
"matplotlib"
]
dynamic = ["version"]

Expand Down
10 changes: 10 additions & 0 deletions src/scwidgets/css/widgets.css
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,13 @@
background-color: #e95420;
border-color: #e95420;
}

/* scwidgets visualizer */

.scwidget-cue-output {
opacity: 1.0;
}

.scwidget-cue-output--cue {
opacity: 0.4;
}
6 changes: 6 additions & 0 deletions src/scwidgets/cue/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from ._widget_cue import CueWidget
from ._widget_cue_box import CheckCueBox, CueBox, SaveCueBox, UpdateCueBox
from ._widget_cue_figure import CueFigure
from ._widget_cue_object import CueObject
from ._widget_cue_output import CueOutput
from ._widget_reset_cue_button import (
CheckResetCueButton,
ResetCueButton,
Expand All @@ -17,4 +20,7 @@
"SaveResetCueButton",
"CheckResetCueButton",
"UpdateResetCueButton",
"CueOutput",
"CueObject",
"CueFigure",
]
118 changes: 118 additions & 0 deletions src/scwidgets/cue/_widget_cue_figure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# postpones evaluation of annotations
# see https://stackoverflow.com/a/33533514
from __future__ import annotations

from typing import List, Optional, Union

import matplotlib
import matplotlib.pyplot as plt
from IPython.display import display
from ipywidgets import Widget
from matplotlib.figure import Figure
from traitlets.utils.sentinel import Sentinel

from ._widget_cue_output import CueOutput


class CueFigure(CueOutput):
"""
A cued displayable ipywidget.Output for a matplotlib figure. Provides utilities to
clear and draw the updated figure. For the matplotlib inline backend it closes the
active figure to prevent any display outside of the container, that happens on the
creation of the figure because pyplot does magic behind the curtain that is hard to
suppress. For the matplot interactive widget backend, named "nbagg", it wraps te
figure within.
:param figure:
The matplotlib figure
:param widgets_to_observe:
The widget to observe if the :param traits_to_observe: has changed.
:param traits_to_observe:
The trait from the :param widgets_to_observe: to observe if changed.
Specify `traitlets.All` to observe all traits.
:param cued:
Specifies if it is cued on initialization
:param css_syle:
- **base**: the css style of the box during initialization
- **cue**: the css style that is added when :param
traits_to_observe: in widget :param widgets_to_observe: changes.
It is supposed to change the style of the box such that the user has a visual
cue that :param widget_to_cue: has changed.
"""

def __init__(
self,
figure: Figure,
widgets_to_observe: Union[None, List[Widget], Widget] = None,
traits_to_observe: Union[
None, str, List[str], List[List[str]], Sentinel
] = None,
cued: bool = True,
css_style: Optional[dict] = None,
**kwargs,
):
CueOutput.__init__(
self,
widgets_to_observe,
traits_to_observe,
cued,
css_style,
**kwargs,
)
self.figure = figure

if matplotlib.backends.backend == "module://matplotlib_inline.backend_inline":
# we close the figure so the figure is only contained in this widget
# and not shown using plt.show()
plt.close(self.figure)
elif matplotlib.backends.backend == "module://ipympl.backend_nbagg":
with self:
self.figure.canvas.show()
else:
raise NotImplementedError(
f"matplotlib backend " f"{matplotlib.backends.backend!r} not supported."
)
self.draw_display()

def clear_display(self, wait=False):
"""
:param wait:
same meaning as for the `wait` parameter in the ipywidgets.clear_output
function
"""
if matplotlib.backends.backend == "module://matplotlib_inline.backend_inline":
self.clear_figure()
self.clear_output(wait=wait)
elif matplotlib.backends.backend == "module://ipympl.backend_nbagg":
self.clear_figure()
if not (wait):
self.figure.canvas.draw_idle()
self.figure.canvas.flush_events()
else:
raise NotImplementedError(
f"matplotlib backend " f"{matplotlib.backends.backend!r} not supported."
)

def draw_display(self):
"""
Enforces redrawing the figure
"""
if matplotlib.backends.backend == "module://matplotlib_inline.backend_inline":
with self:
display(self.figure)
elif matplotlib.backends.backend == "module://ipympl.backend_nbagg":
self.figure.canvas.draw_idle()
self.figure.canvas.flush_events()
else:
raise NotImplementedError(
f"matplotlib backend " f"{matplotlib.backends.backend!r} not supported."
)

def clear_figure(self):
"""
Clears the figure while retainin axes. figure.clear() removes the axes
sometimes.
"""
for ax in self.figure.get_axes():
if ax.has_data() or len(ax.artists) > 0:
ax.clear()
73 changes: 73 additions & 0 deletions src/scwidgets/cue/_widget_cue_object.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# postpones evaluation of annotations
# see https://stackoverflow.com/a/33533514
from __future__ import annotations

from typing import Any, List, Optional, Union

from IPython.display import display
from ipywidgets import Widget
from traitlets.utils.sentinel import Sentinel

from ._widget_cue_output import CueOutput


class CueObject(CueOutput):
"""
A cued displayable ipywidget.Output for any Python object. Provides utilities to
clear and redraw the object, for example after an update.
:param display_object:
The object to display
:param widgets_to_observe:
The widget to observe if the :param traits_to_observe: has changed.
:param traits_to_observe:
The trait from the :param widgets_to_observe: to observe if changed.
Specify `traitlets.All` to observe all traits.
:param cued:
Specifies if it is cued on initialization
:param css_syle:
- **base**: the css style of the box during initialization
- **cue**: the css style that is added when :param
traits_to_observe: in widget :param widgets_to_observe: changes.
It is supposed to change the style of the box such that the user has a visual
cue that :param widget_to_cue: has changed.
"""

def __init__(
self,
display_object: Any,
widgets_to_observe: Union[None, List[Widget], Widget] = None,
traits_to_observe: Union[
None, str, List[str], List[List[str]], Sentinel
] = None,
cued: bool = True,
css_style: Optional[dict] = None,
*args,
**kwargs,
):
CueOutput.__init__(
self,
widgets_to_observe,
traits_to_observe,
cued,
css_style,
**kwargs,
)

self._display_object = display_object
self.draw_display()

@property
def display_object(self):
return self._display_object

@display_object.setter
def display_object(self, display_object: Any):
self._display_object = display_object

def clear_display(self, wait=False):
self.clear_output(wait=wait)

def draw_display(self):
with self:
display(self._display_object)
78 changes: 78 additions & 0 deletions src/scwidgets/cue/_widget_cue_output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# postpones evaluation of annotations
# see https://stackoverflow.com/a/33533514
from __future__ import annotations

from typing import List, Optional, Union

from ipywidgets import Output, Widget
from traitlets.utils.sentinel import Sentinel

from ._widget_cue import CueWidget


class CueOutput(Output, CueWidget):
"""
A cued displayable ipywidget.Output for any Python object.
:param widgets_to_observe:
The widget to observe if the :param traits_to_observe: has changed.
:param traits_to_observe:
The trait from the :param widgets_to_observe: to observe if changed.
Specify `traitlets.All` to observe all traits.
:param cued:
Specifies if it is cued on initialization
:param css_syle:
- **base**: the css style of the box during initialization
- **cue**: the css style that is added when :param
traits_to_observe: in widget :param widgets_to_observe: changes.
It is supposed to change the style of the box such that the user has a visual
cue that :param widget_to_cue: has changed.
"""

def __init__(
self,
widgets_to_observe: Union[None, List[Widget], Widget] = None,
traits_to_observe: Union[
None, str, List[str], List[List[str]], Sentinel
] = None,
cued: bool = True,
css_style: Optional[dict] = None,
*args,
**kwargs,
):
if css_style is None:
css_style = {
"base": "scwidget-cue-output",
"cue": "scwidget-cue-output--cue",
}
if "base" not in css_style.keys():
raise ValueError('css_style is missing key "base".')
if "cue" not in css_style.keys():
raise ValueError('css_style is missing key "cue".')

self._css_style = css_style

# TODO make disabling of cued transparent
if widgets_to_observe is None and traits_to_observe is None:
cued = False
if widgets_to_observe is None:
widgets_to_observe = []
if traits_to_observe is None:
traits_to_observe = []

Output.__init__(self, **kwargs)
CueWidget.__init__(self, widgets_to_observe, traits_to_observe, cued)

self.add_class(self._css_style["base"])

@property
def cued(self):
return self._cued

@cued.setter
def cued(self, cued: bool):
if cued:
self.add_class(self._css_style["cue"])
else:
self.remove_class(self._css_style["cue"])
self._cued = cued
Loading

0 comments on commit bb2aeb0

Please sign in to comment.