Skip to content

Commit ffa1f39

Browse files
committed
ENG: add a by_label accessor to FigureRegistry
1 parent d33caac commit ffa1f39

File tree

4 files changed

+86
-3
lines changed

4 files changed

+86
-3
lines changed

docs/source/api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Managed
4949
mpl_gui.FigureRegistry.figure
5050
mpl_gui.FigureRegistry.subplots
5151
mpl_gui.FigureRegistry.subplot_mosaic
52+
mpl_gui.FigureRegistry.by_label
5253
mpl_gui.FigureRegistry.show_all
5354
mpl_gui.FigureRegistry.close_all
5455

docs/source/index.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,19 @@ to ::
156156

157157
and have a (mostly) drop-in replacement.
158158

159+
Additionally, there is a `.FigureRegistry.by_label` accessory that returns
160+
a dictionary mapping the Figures' labels to each Figure ::
161+
162+
import mpl_gui as mg
163+
164+
fr = mg.FigureRegistry()
165+
166+
figA = fr.figure(label='A')
167+
figB = fr.subplots(2, 2, label='B')
168+
169+
fr.by_label['A'] is figA
170+
fr.by_label['B'] is figB
171+
159172
FigureContext
160173
+++++++++++++
161174

mpl_gui/__init__.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"""
1515
import logging
1616
import functools
17+
from itertools import count
1718

1819
from matplotlib.backend_bases import FigureCanvasBase as _FigureCanvasBase
1920

@@ -113,19 +114,47 @@ class FigureRegistry:
113114
114115
"""
115116

116-
def __init__(self, *, block=None, timeout=0):
117+
def __init__(self, *, block=None, timeout=0, prefix="Figure "):
118+
# settings stashed to set defaults on show
117119
self._timeout = timeout
118120
self._block = block
121+
# Settings / state to control the default figure label
122+
self._count = count()
123+
self._prefix = prefix
124+
# the canonical location for storing the Figures this registry owns.
125+
# any additional views must never include a figure not in the list but
126+
# may omit figures
119127
self.figures = []
120128

121129
def _register_fig(self, fig):
130+
# if the user closes the figure by any other mechanism, drop our
131+
# reference to it. This is important for getting a "pyplot" like user
132+
# experience
122133
fig.canvas.mpl_connect(
123134
"close_event",
124135
lambda e: self.figures.remove(fig) if fig in self.figures else None,
125136
)
137+
# hold a hard reference to the figure.
126138
self.figures.append(fig)
139+
# Make sure we give the figure a quasi-unique label. We will never set
140+
# the same label twice, but will not over-ride any user label (but
141+
# empty string) on a Figure so if they provide duplicate labels, change
142+
# the labels under us, or provide a label that will be shadowed in the
143+
# future it will be what it is.
144+
fignum = next(self._count)
145+
if fig.get_label() == "":
146+
fig.set_label(f"{self._prefix}{fignum:d}")
127147
return fig
128148

149+
@property
150+
def by_label(self):
151+
"""
152+
Return a dictionary of the current mapping labels -> figures.
153+
154+
If there are duplicate labels, newer figures will take precedence.
155+
"""
156+
return {fig.get_label(): fig for fig in self.figures}
157+
129158
@functools.wraps(figure)
130159
def figure(self, *args, **kwargs):
131160
fig = figure(*args, **kwargs)

mpl_gui/tests/test_examples.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import mpl_gui as mg
88

99

10-
1110
def test_no_pyplot():
1211

1312
assert sys.modules.get("matplotlib.pyplot", None) is None
@@ -79,6 +78,7 @@ class TestException(Exception):
7978

8079
assert isinstance(fig.canvas, FigureCanvasBase)
8180

81+
8282
def test_close_all():
8383
fr = mg.FigureRegistry(block=False)
8484
fig = fr.figure()
@@ -90,11 +90,51 @@ def test_close_all():
9090
new_canvas = fig.canvas
9191
fr.close_all()
9292
assert len(fr.figures) == 0
93-
assert 'destroy' in new_canvas.manager.call_info
93+
assert "destroy" in new_canvas.manager.call_info
9494
assert fig.canvas is not new_canvas
9595
assert new_canvas.figure is None
9696

9797
# test revive
9898
old_canvas = fig.canvas
9999
mg.show([fig])
100100
assert fig.canvas is not old_canvas
101+
102+
103+
def test_labels_prefix():
104+
fr = mg.FigureRegistry(block=False, prefix="Aardvark ")
105+
for j in range(5):
106+
fr.figure()
107+
assert list(fr.by_label) == [f"Aardvark {k}" for k in range(j + 1)]
108+
fr.close_all()
109+
assert len(fr.by_label) == 0
110+
111+
112+
def test_labels_collision():
113+
fr = mg.FigureRegistry(block=False)
114+
for j in range(5):
115+
fr.figure(label="aardvark")
116+
assert list(fr.by_label) == ["aardvark"]
117+
assert len(fr.figures) == 5
118+
assert len(set(fr.figures)) == 5
119+
assert fr.figures[-1] is fr.by_label["aardvark"]
120+
fr.close_all()
121+
assert len(fr.by_label) == 0
122+
123+
124+
def test_by_label_new_dict():
125+
fr = mg.FigureRegistry(block=False)
126+
for j in range(5):
127+
fr.figure()
128+
# test we get a new dict each time!
129+
assert fr.by_label is not fr.by_label
130+
131+
132+
def test_change_labels():
133+
fr = mg.FigureRegistry(block=False)
134+
for j in range(5):
135+
fr.figure()
136+
assert list(fr.by_label) == [f"Figure {j}" for j in range(5)]
137+
138+
for j, f in enumerate(fr.by_label.values()):
139+
f.set_label(f"aardvark {j}")
140+
assert list(fr.by_label) == [f"aardvark {j}" for j in range(5)]

0 commit comments

Comments
 (0)