polyphys 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- polyphys/__init__.py +7 -0
- polyphys/__version__.py +22 -0
- polyphys/analyze/__init__.py +51 -0
- polyphys/analyze/clusters.py +1738 -0
- polyphys/analyze/correlations.py +995 -0
- polyphys/analyze/density.py +1138 -0
- polyphys/analyze/measurer.py +1338 -0
- polyphys/analyze/pipeline.py +1868 -0
- polyphys/analyze/stats.py +810 -0
- polyphys/analyze/sumrule.py +1159 -0
- polyphys/manage/__init__.py +11 -0
- polyphys/manage/organizer.py +2188 -0
- polyphys/manage/parser.py +1264 -0
- polyphys/manage/types.py +171 -0
- polyphys/manage/utils.py +777 -0
- polyphys/probe/__init__.py +25 -0
- polyphys/probe/binders.py +711 -0
- polyphys/probe/chain_size.py +583 -0
- polyphys/probe/core.py +437 -0
- polyphys/probe/histograms.py +863 -0
- polyphys/probe/pairs.py +555 -0
- polyphys/simulate/__init__.py +48 -0
- polyphys/simulate/build.py +1231 -0
- polyphys/simulate/core.py +237 -0
- polyphys/simulate/data.py +977 -0
- polyphys/simulate/dump.py +752 -0
- polyphys/simulate/log.py +788 -0
- polyphys/spec.py +1359 -0
- polyphys/tests/analyze/test_clusters.py +1058 -0
- polyphys/tests/analyze/test_correlations.py +708 -0
- polyphys/tests/analyze/test_density.py +813 -0
- polyphys/tests/analyze/test_measurer.py +785 -0
- polyphys/tests/analyze/test_pipeline.py +1881 -0
- polyphys/tests/analyze/test_stats.py +732 -0
- polyphys/tests/analyze/test_sumrule.py +917 -0
- polyphys/tests/conftest.py +753 -0
- polyphys/tests/data/lammps_build/make_starts.py +70 -0
- polyphys/tests/manage/test_organizer.py +1495 -0
- polyphys/tests/manage/test_parser.py +1222 -0
- polyphys/tests/manage/test_utils.py +498 -0
- polyphys/tests/probe/conftest.py +407 -0
- polyphys/tests/probe/test_binders.py +686 -0
- polyphys/tests/probe/test_chain_size.py +557 -0
- polyphys/tests/probe/test_core.py +391 -0
- polyphys/tests/probe/test_histograms.py +779 -0
- polyphys/tests/probe/test_pairs.py +475 -0
- polyphys/tests/simulate/test_build.py +1042 -0
- polyphys/tests/simulate/test_core.py +106 -0
- polyphys/tests/simulate/test_data.py +858 -0
- polyphys/tests/simulate/test_dump.py +487 -0
- polyphys/tests/simulate/test_log.py +667 -0
- polyphys/tests/strategies.py +120 -0
- polyphys/tests/synthetic.py +410 -0
- polyphys/tests/test_conftest.py +414 -0
- polyphys/tests/test_docs.py +82 -0
- polyphys/tests/test_packaging.py +97 -0
- polyphys/tests/test_spec.py +772 -0
- polyphys/tests/viz/conftest.py +32 -0
- polyphys/tests/viz/test_colors.py +113 -0
- polyphys/tests/viz/test_helpers.py +292 -0
- polyphys/viz/__init__.py +15 -0
- polyphys/viz/colors.py +204 -0
- polyphys/viz/helpers.py +549 -0
- polyphys-1.0.0.dist-info/METADATA +339 -0
- polyphys-1.0.0.dist-info/RECORD +69 -0
- polyphys-1.0.0.dist-info/WHEEL +5 -0
- polyphys-1.0.0.dist-info/licenses/AUTHORS.rst +13 -0
- polyphys-1.0.0.dist-info/licenses/LICENSE +22 -0
- polyphys-1.0.0.dist-info/top_level.txt +1 -0
polyphys/viz/helpers.py
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
"""
|
|
2
|
+
==========================================================
|
|
3
|
+
:mod:`polyphys.viz.helpers`
|
|
4
|
+
==========================================================
|
|
5
|
+
|
|
6
|
+
The :mod:`polyphys.viz.helpers` module holds the matplotlib helpers the
|
|
7
|
+
studies' multi-panel figures use: a rule for placing an axis's ticks, proxy
|
|
8
|
+
legend handles, a legend moved into chosen panels, and a band shaded under
|
|
9
|
+
each line. It needs matplotlib, which the ``viz`` extra installs, and raises
|
|
10
|
+
:exc:`ImportError` with that hint when it is missing.
|
|
11
|
+
|
|
12
|
+
Ticks
|
|
13
|
+
=====
|
|
14
|
+
|
|
15
|
+
:func:`set_xticks` and :func:`set_yticks` place the major ticks of one axis
|
|
16
|
+
by one rule. Given `lower`, `upper`, and `step`:
|
|
17
|
+
|
|
18
|
+
- the ticks are :math:`t_k = \\mathrm{lower} + k \\, \\mathrm{step}` for
|
|
19
|
+
:math:`k = 0, 1, \\dots, \\lfloor (\\mathrm{upper} - \\mathrm{lower}) /
|
|
20
|
+
\\mathrm{step} \\rfloor`, so the last tick is `upper` when `step` divides
|
|
21
|
+
the span;
|
|
22
|
+
- the *tolerance* is ``step / 10`` rounded down to its first non-zero
|
|
23
|
+
digit, by :func:`polyphys.manage.utils.round_down_first_non_zero`; a tick
|
|
24
|
+
closer to zero than the tolerance is written as exactly ``0.0``, so
|
|
25
|
+
``-5.6e-17`` never appears as a label;
|
|
26
|
+
- the axis limits are ``(lower - tolerance, upper + tolerance)``, a frame
|
|
27
|
+
that keeps the first and last ticks off the axes' edges;
|
|
28
|
+
- the labels are the ticks rounded to `decimals` places, or hidden.
|
|
29
|
+
|
|
30
|
+
The ticks are not rounded to multiples of a power of ten; the caller
|
|
31
|
+
chooses `lower`, `upper`, and `step` with that in mind.
|
|
32
|
+
|
|
33
|
+
Legends
|
|
34
|
+
=======
|
|
35
|
+
|
|
36
|
+
- :func:`color_handles` and :func:`line_handles` make *proxy* handles,
|
|
37
|
+
artists that draw nothing on the axes and exist only to sit in a legend,
|
|
38
|
+
so a figure can carry one legend for its colors and another for its line
|
|
39
|
+
styles or markers, whatever the plotting call produced.
|
|
40
|
+
- :func:`move_legend_to_axes` takes an existing legend, such as the figure
|
|
41
|
+
legend a facet grid draws outside its panels, and re-creates it inside
|
|
42
|
+
chosen panels.
|
|
43
|
+
- :func:`band_under_lines` shades a band, such as a confidence interval,
|
|
44
|
+
under each line already drawn on an axes, in that line's color.
|
|
45
|
+
|
|
46
|
+
Examples
|
|
47
|
+
========
|
|
48
|
+
|
|
49
|
+
>>> import matplotlib
|
|
50
|
+
>>> matplotlib.use("Agg")
|
|
51
|
+
>>> import matplotlib.pyplot as plt
|
|
52
|
+
>>> from polyphys.viz.helpers import color_handles, set_xticks
|
|
53
|
+
>>> fig, ax = plt.subplots()
|
|
54
|
+
>>> set_xticks(ax, 0.0, 1.0, 0.25)
|
|
55
|
+
array([0. , 0.25, 0.5 , 0.75, 1. ])
|
|
56
|
+
>>> [float(limit) for limit in ax.get_xlim()]
|
|
57
|
+
[-0.02, 1.02]
|
|
58
|
+
>>> handles = color_handles(["bug", "crowder"], ["#4477AA", "#EE6677"])
|
|
59
|
+
>>> [text.get_text() for text in ax.legend(handles=handles).get_texts()]
|
|
60
|
+
['bug', 'crowder']
|
|
61
|
+
>>> plt.close(fig)
|
|
62
|
+
"""
|
|
63
|
+
import numbers
|
|
64
|
+
from typing import Any, Literal, Optional, Sequence, Union
|
|
65
|
+
|
|
66
|
+
import numpy as np
|
|
67
|
+
|
|
68
|
+
from ..manage.utils import round_down_first_non_zero
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
import matplotlib # noqa: F401
|
|
72
|
+
except ImportError as error: # pragma: no cover - depends on the install
|
|
73
|
+
raise ImportError(
|
|
74
|
+
"polyphys.viz.helpers needs matplotlib; install it with "
|
|
75
|
+
"python -m pip install 'polyphys[viz]'.") from error
|
|
76
|
+
|
|
77
|
+
from matplotlib.axes import Axes
|
|
78
|
+
from matplotlib.collections import PolyCollection
|
|
79
|
+
from matplotlib.legend import Legend
|
|
80
|
+
from matplotlib.lines import Line2D
|
|
81
|
+
from matplotlib.patches import Patch
|
|
82
|
+
|
|
83
|
+
__all__ = ["band_under_lines", "color_handles", "line_handles",
|
|
84
|
+
"move_legend_to_axes", "set_xticks", "set_yticks"]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _ticks(lower: float, upper: float, step: float) -> tuple[Any, float]:
|
|
88
|
+
"""Return the tick positions and the tolerance of the ticks rule."""
|
|
89
|
+
if not lower < upper:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"lower must be below upper, got lower={lower} and "
|
|
92
|
+
f"upper={upper}.")
|
|
93
|
+
span = upper - lower
|
|
94
|
+
if step <= 0 or step >= span:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"step must be positive and smaller than the span "
|
|
97
|
+
f"upper - lower = {span}, got step={step}.")
|
|
98
|
+
tolerance = round_down_first_non_zero(step / 10)
|
|
99
|
+
# The 1e-9 absorbs the float error of a span that step divides exactly.
|
|
100
|
+
count = int(np.floor(span / step + 1e-9)) + 1
|
|
101
|
+
ticks = lower + step * np.arange(count)
|
|
102
|
+
ticks = np.where(np.abs(ticks) < tolerance, 0.0, ticks)
|
|
103
|
+
return ticks, tolerance
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _set_ticks(
|
|
107
|
+
ax: Axes,
|
|
108
|
+
which: Literal["x", "y"],
|
|
109
|
+
lower: float,
|
|
110
|
+
upper: float,
|
|
111
|
+
step: float,
|
|
112
|
+
labels: bool,
|
|
113
|
+
decimals: int,
|
|
114
|
+
text_kwargs: dict[str, Any],
|
|
115
|
+
) -> Any:
|
|
116
|
+
"""Apply the ticks rule to the x or y axis of `ax`."""
|
|
117
|
+
ticks, tolerance = _ticks(lower, upper, step)
|
|
118
|
+
texts = ([str(v) for v in np.around(ticks, decimals)] if labels
|
|
119
|
+
else [""] * len(ticks))
|
|
120
|
+
if which == "x":
|
|
121
|
+
ax.set_xlim(lower - tolerance, upper + tolerance)
|
|
122
|
+
ax.set_xticks(ticks)
|
|
123
|
+
ax.set_xticklabels(texts, **text_kwargs)
|
|
124
|
+
else:
|
|
125
|
+
ax.set_ylim(lower - tolerance, upper + tolerance)
|
|
126
|
+
ax.set_yticks(ticks)
|
|
127
|
+
ax.set_yticklabels(texts, **text_kwargs)
|
|
128
|
+
return ticks
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def set_xticks(
|
|
132
|
+
ax: Axes,
|
|
133
|
+
lower: float,
|
|
134
|
+
upper: float,
|
|
135
|
+
step: float,
|
|
136
|
+
*,
|
|
137
|
+
labels: bool = True,
|
|
138
|
+
decimals: int = 3,
|
|
139
|
+
**text_kwargs: Any,
|
|
140
|
+
) -> Any:
|
|
141
|
+
"""
|
|
142
|
+
Place the major x ticks of `ax` every `step` from `lower` to `upper`.
|
|
143
|
+
|
|
144
|
+
The ticks, the tolerance that frames them, and the labels follow the
|
|
145
|
+
rule in the module docstring. Time and space are :math:`O(n)` in the
|
|
146
|
+
number of ticks.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
ax : matplotlib.axes.Axes
|
|
151
|
+
The axes whose x axis is set.
|
|
152
|
+
lower, upper : float
|
|
153
|
+
The first tick and the largest tick value, in data units;
|
|
154
|
+
``lower < upper``.
|
|
155
|
+
step : float
|
|
156
|
+
The distance between ticks, positive and smaller than
|
|
157
|
+
``upper - lower``.
|
|
158
|
+
labels : bool, default True
|
|
159
|
+
Whether the ticks are labeled; ``False`` hides every label.
|
|
160
|
+
decimals : int, default 3
|
|
161
|
+
The number of decimals the labels are rounded to.
|
|
162
|
+
**text_kwargs
|
|
163
|
+
Passed to :meth:`matplotlib.axes.Axes.set_xticklabels`, such as
|
|
164
|
+
``fontsize``.
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
numpy.ndarray
|
|
169
|
+
The tick positions, shape ``(n_ticks,)``.
|
|
170
|
+
|
|
171
|
+
Raises
|
|
172
|
+
------
|
|
173
|
+
ValueError
|
|
174
|
+
If ``lower >= upper``, or `step` is not positive and smaller than
|
|
175
|
+
``upper - lower``.
|
|
176
|
+
|
|
177
|
+
Examples
|
|
178
|
+
--------
|
|
179
|
+
>>> import matplotlib
|
|
180
|
+
>>> matplotlib.use("Agg")
|
|
181
|
+
>>> import matplotlib.pyplot as plt
|
|
182
|
+
>>> fig, ax = plt.subplots()
|
|
183
|
+
>>> set_xticks(ax, -0.2, 0.2, 0.1, decimals=1)
|
|
184
|
+
array([-0.2, -0.1, 0. , 0.1, 0.2])
|
|
185
|
+
>>> [label.get_text() for label in ax.get_xticklabels()]
|
|
186
|
+
['-0.2', '-0.1', '0.0', '0.1', '0.2']
|
|
187
|
+
>>> plt.close(fig)
|
|
188
|
+
"""
|
|
189
|
+
return _set_ticks(ax, "x", lower, upper, step, labels, decimals,
|
|
190
|
+
text_kwargs)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def set_yticks(
|
|
194
|
+
ax: Axes,
|
|
195
|
+
lower: float,
|
|
196
|
+
upper: float,
|
|
197
|
+
step: float,
|
|
198
|
+
*,
|
|
199
|
+
labels: bool = True,
|
|
200
|
+
decimals: int = 3,
|
|
201
|
+
**text_kwargs: Any,
|
|
202
|
+
) -> Any:
|
|
203
|
+
"""
|
|
204
|
+
Place the major y ticks of `ax` every `step` from `lower` to `upper`.
|
|
205
|
+
|
|
206
|
+
The y-axis twin of :func:`set_xticks`, with the same rule, parameters,
|
|
207
|
+
return value, and errors; `text_kwargs` go to
|
|
208
|
+
:meth:`matplotlib.axes.Axes.set_yticklabels`.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
ax : matplotlib.axes.Axes
|
|
213
|
+
The axes whose y axis is set.
|
|
214
|
+
lower, upper : float
|
|
215
|
+
The first tick and the largest tick value; ``lower < upper``.
|
|
216
|
+
step : float
|
|
217
|
+
The distance between ticks, positive and smaller than
|
|
218
|
+
``upper - lower``.
|
|
219
|
+
labels : bool, default True
|
|
220
|
+
Whether the ticks are labeled.
|
|
221
|
+
decimals : int, default 3
|
|
222
|
+
The number of decimals the labels are rounded to.
|
|
223
|
+
**text_kwargs
|
|
224
|
+
Passed to :meth:`matplotlib.axes.Axes.set_yticklabels`.
|
|
225
|
+
|
|
226
|
+
Returns
|
|
227
|
+
-------
|
|
228
|
+
numpy.ndarray
|
|
229
|
+
The tick positions, shape ``(n_ticks,)``.
|
|
230
|
+
|
|
231
|
+
Examples
|
|
232
|
+
--------
|
|
233
|
+
>>> import matplotlib
|
|
234
|
+
>>> matplotlib.use("Agg")
|
|
235
|
+
>>> import matplotlib.pyplot as plt
|
|
236
|
+
>>> fig, ax = plt.subplots()
|
|
237
|
+
>>> set_yticks(ax, 0.0, 1.0, 0.5, labels=False)
|
|
238
|
+
array([0. , 0.5, 1. ])
|
|
239
|
+
>>> [float(limit) for limit in ax.get_ylim()]
|
|
240
|
+
[-0.05, 1.05]
|
|
241
|
+
>>> plt.close(fig)
|
|
242
|
+
"""
|
|
243
|
+
return _set_ticks(ax, "y", lower, upper, step, labels, decimals,
|
|
244
|
+
text_kwargs)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _per_label(
|
|
248
|
+
name: str,
|
|
249
|
+
values: Union[None, str, float, Sequence[Any]],
|
|
250
|
+
n_labels: int,
|
|
251
|
+
) -> list[Any]:
|
|
252
|
+
"""Return one value per label: one value repeated, or a matching list.
|
|
253
|
+
|
|
254
|
+
One value is None, a string, a real number (NumPy scalars included), a
|
|
255
|
+
tuple of real numbers such as an RGB or RGBA color, or a dash tuple
|
|
256
|
+
``(offset, (on, off, ...))``. Anything else is a sequence with one entry
|
|
257
|
+
per label.
|
|
258
|
+
"""
|
|
259
|
+
if _is_one_value(values):
|
|
260
|
+
return [values] * n_labels
|
|
261
|
+
values = list(values)
|
|
262
|
+
if len(values) != n_labels:
|
|
263
|
+
raise ValueError(
|
|
264
|
+
f"{name} has {len(values)} entries for {n_labels} labels.")
|
|
265
|
+
return values
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _is_one_value(value: Any) -> bool:
|
|
269
|
+
"""Return True if `value` is one style, not one per label."""
|
|
270
|
+
if value is None or isinstance(value, (str, numbers.Real)):
|
|
271
|
+
return True
|
|
272
|
+
if not isinstance(value, tuple):
|
|
273
|
+
return False
|
|
274
|
+
if all(isinstance(item, numbers.Real) for item in value):
|
|
275
|
+
return True
|
|
276
|
+
return (len(value) == 2 and isinstance(value[0], numbers.Real)
|
|
277
|
+
and isinstance(value[1], (tuple, list))
|
|
278
|
+
and all(isinstance(item, numbers.Real) for item in value[1]))
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def color_handles(
|
|
282
|
+
labels: Sequence[str],
|
|
283
|
+
colors: Any,
|
|
284
|
+
**patch_kwargs: Any,
|
|
285
|
+
) -> list[Patch]:
|
|
286
|
+
"""
|
|
287
|
+
Make one filled :class:`~matplotlib.patches.Patch` per label.
|
|
288
|
+
|
|
289
|
+
The patches are proxy handles for a legend keyed by color. Time and
|
|
290
|
+
space are :math:`O(n)` in the number of labels.
|
|
291
|
+
|
|
292
|
+
Parameters
|
|
293
|
+
----------
|
|
294
|
+
labels : sequence of str
|
|
295
|
+
The legend entries.
|
|
296
|
+
colors : matplotlib color or sequence of matplotlib colors
|
|
297
|
+
One color for every label, a name or a tuple of numbers such as the
|
|
298
|
+
RGB triple ``(0.1, 0.2, 0.3)``; or a list, or another sequence, with
|
|
299
|
+
one color per label, in the same order.
|
|
300
|
+
**patch_kwargs
|
|
301
|
+
Passed to every :class:`~matplotlib.patches.Patch`, such as
|
|
302
|
+
``alpha``.
|
|
303
|
+
|
|
304
|
+
Returns
|
|
305
|
+
-------
|
|
306
|
+
list of matplotlib.patches.Patch
|
|
307
|
+
The handles, one per label, for the ``handles`` argument of
|
|
308
|
+
:meth:`matplotlib.axes.Axes.legend`.
|
|
309
|
+
|
|
310
|
+
Raises
|
|
311
|
+
------
|
|
312
|
+
ValueError
|
|
313
|
+
If `colors` is a sequence of colors whose length differs from that
|
|
314
|
+
of `labels`.
|
|
315
|
+
|
|
316
|
+
Examples
|
|
317
|
+
--------
|
|
318
|
+
>>> patches = color_handles(["low", "high"], ["#4477AA", "#EE6677"],
|
|
319
|
+
... alpha=0.5)
|
|
320
|
+
>>> [patch.get_label() for patch in patches]
|
|
321
|
+
['low', 'high']
|
|
322
|
+
>>> patches[0].get_alpha()
|
|
323
|
+
0.5
|
|
324
|
+
|
|
325
|
+
One RGB tuple colors every label:
|
|
326
|
+
|
|
327
|
+
>>> patches = color_handles(["a", "b"], (0.0, 0.5, 1.0))
|
|
328
|
+
>>> [patch.get_facecolor()[:3] for patch in patches]
|
|
329
|
+
[(0.0, 0.5, 1.0), (0.0, 0.5, 1.0)]
|
|
330
|
+
"""
|
|
331
|
+
colors = _per_label("colors", colors, len(labels))
|
|
332
|
+
return [Patch(label=label, color=color, **patch_kwargs)
|
|
333
|
+
for label, color in zip(labels, colors)]
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def line_handles(
|
|
337
|
+
labels: Sequence[str],
|
|
338
|
+
*,
|
|
339
|
+
linestyles: Union[None, str, Sequence[Any]] = None,
|
|
340
|
+
linewidths: Union[None, float, Sequence[float]] = None,
|
|
341
|
+
markers: Union[None, str, Sequence[Any]] = None,
|
|
342
|
+
color: Any = "black",
|
|
343
|
+
**line_kwargs: Any,
|
|
344
|
+
) -> list[Line2D]:
|
|
345
|
+
"""
|
|
346
|
+
Make one empty :class:`~matplotlib.lines.Line2D` per label.
|
|
347
|
+
|
|
348
|
+
The lines are proxy handles for a legend keyed by line style, width,
|
|
349
|
+
or marker. Each of `linestyles`, `linewidths`, and `markers` is either
|
|
350
|
+
one value, used for every label, a sequence with one entry per label,
|
|
351
|
+
or ``None``, which leaves that attribute at matplotlib's default. One
|
|
352
|
+
value is a string, a number, or a dash tuple such as ``(0, (5, 2))``;
|
|
353
|
+
a list, or a tuple of other items, is one entry per label. Time
|
|
354
|
+
and space are :math:`O(n)` in the number of labels.
|
|
355
|
+
|
|
356
|
+
Parameters
|
|
357
|
+
----------
|
|
358
|
+
labels : sequence of str
|
|
359
|
+
The legend entries.
|
|
360
|
+
linestyles : str or sequence, optional
|
|
361
|
+
The line style of each handle, such as ``'-'`` or ``'--'``.
|
|
362
|
+
linewidths : float or sequence of float, optional
|
|
363
|
+
The line width of each handle, in points.
|
|
364
|
+
markers : str or sequence, optional
|
|
365
|
+
The marker of each handle, such as ``'o'`` or ``'^'``.
|
|
366
|
+
color : matplotlib color, default 'black'
|
|
367
|
+
The color of every handle.
|
|
368
|
+
**line_kwargs
|
|
369
|
+
Passed to every :class:`~matplotlib.lines.Line2D`, such as
|
|
370
|
+
``markersize``.
|
|
371
|
+
|
|
372
|
+
Returns
|
|
373
|
+
-------
|
|
374
|
+
list of matplotlib.lines.Line2D
|
|
375
|
+
The handles, one per label.
|
|
376
|
+
|
|
377
|
+
Raises
|
|
378
|
+
------
|
|
379
|
+
ValueError
|
|
380
|
+
If a sequence among `linestyles`, `linewidths`, and `markers` does
|
|
381
|
+
not have one entry per label.
|
|
382
|
+
|
|
383
|
+
Examples
|
|
384
|
+
--------
|
|
385
|
+
>>> lines = line_handles(["ring", "linear"], linestyles=["-", "--"],
|
|
386
|
+
... markers="o", linewidths=2)
|
|
387
|
+
>>> [(line.get_label(), line.get_linestyle(), line.get_marker())
|
|
388
|
+
... for line in lines]
|
|
389
|
+
[('ring', '-', 'o'), ('linear', '--', 'o')]
|
|
390
|
+
"""
|
|
391
|
+
n_labels = len(labels)
|
|
392
|
+
styles = _per_label("linestyles", linestyles, n_labels)
|
|
393
|
+
widths = _per_label("linewidths", linewidths, n_labels)
|
|
394
|
+
marks = _per_label("markers", markers, n_labels)
|
|
395
|
+
handles = []
|
|
396
|
+
for label, style, width, mark in zip(labels, styles, widths, marks):
|
|
397
|
+
kwargs = {"linestyle": style, "linewidth": width, "marker": mark}
|
|
398
|
+
kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
|
399
|
+
handles.append(
|
|
400
|
+
Line2D([], [], label=label, color=color, **kwargs,
|
|
401
|
+
**line_kwargs))
|
|
402
|
+
return handles
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def move_legend_to_axes(
|
|
406
|
+
legend: Legend,
|
|
407
|
+
axes: Sequence[Axes],
|
|
408
|
+
locs: Sequence[Any],
|
|
409
|
+
*,
|
|
410
|
+
title: Optional[str] = None,
|
|
411
|
+
**legend_kwargs: Any,
|
|
412
|
+
) -> list[Legend]:
|
|
413
|
+
"""
|
|
414
|
+
Remove `legend` and re-create it inside each of `axes`.
|
|
415
|
+
|
|
416
|
+
The new legends carry the handles, labels, and title of `legend`; each
|
|
417
|
+
replaces the legend its axes had. This moves, for instance, the legend
|
|
418
|
+
a seaborn facet grid draws beside its panels into one or more panels.
|
|
419
|
+
Time and space are :math:`O(n_\\mathrm{axes} \\, n_\\mathrm{entries})`.
|
|
420
|
+
|
|
421
|
+
Parameters
|
|
422
|
+
----------
|
|
423
|
+
legend : matplotlib.legend.Legend
|
|
424
|
+
The legend to move; it is removed from its figure or axes.
|
|
425
|
+
axes : sequence of matplotlib.axes.Axes
|
|
426
|
+
The axes that receive a copy each.
|
|
427
|
+
locs : sequence
|
|
428
|
+
One legend location per axes, as the ``loc`` argument of
|
|
429
|
+
:meth:`matplotlib.axes.Axes.legend` takes it.
|
|
430
|
+
title : str, optional
|
|
431
|
+
A new title; by default the title of `legend` is kept.
|
|
432
|
+
**legend_kwargs
|
|
433
|
+
Passed to every :meth:`matplotlib.axes.Axes.legend` call, such as
|
|
434
|
+
``frameon``.
|
|
435
|
+
|
|
436
|
+
Returns
|
|
437
|
+
-------
|
|
438
|
+
list of matplotlib.legend.Legend
|
|
439
|
+
The new legends, in the order of `axes`.
|
|
440
|
+
|
|
441
|
+
Raises
|
|
442
|
+
------
|
|
443
|
+
ValueError
|
|
444
|
+
If `axes` and `locs` differ in length.
|
|
445
|
+
|
|
446
|
+
Examples
|
|
447
|
+
--------
|
|
448
|
+
>>> import matplotlib
|
|
449
|
+
>>> matplotlib.use("Agg")
|
|
450
|
+
>>> import matplotlib.pyplot as plt
|
|
451
|
+
>>> fig, (left, right) = plt.subplots(ncols=2)
|
|
452
|
+
>>> _ = left.plot([0, 1], [0, 1], label="rise")
|
|
453
|
+
>>> outer = fig.legend(title="curves")
|
|
454
|
+
>>> inner = move_legend_to_axes(outer, [right], ["upper left"])
|
|
455
|
+
>>> fig.legends
|
|
456
|
+
[]
|
|
457
|
+
>>> [text.get_text() for text in right.get_legend().get_texts()]
|
|
458
|
+
['rise']
|
|
459
|
+
>>> right.get_legend().get_title().get_text()
|
|
460
|
+
'curves'
|
|
461
|
+
>>> plt.close(fig)
|
|
462
|
+
"""
|
|
463
|
+
if len(axes) != len(locs):
|
|
464
|
+
raise ValueError(
|
|
465
|
+
f"locs has {len(locs)} entries for {len(axes)} axes.")
|
|
466
|
+
entries = [(handle, text.get_text())
|
|
467
|
+
for handle, text in zip(legend.legend_handles,
|
|
468
|
+
legend.get_texts())
|
|
469
|
+
if handle is not None]
|
|
470
|
+
handles = [handle for handle, _ in entries]
|
|
471
|
+
labels = [label for _, label in entries]
|
|
472
|
+
new_title = legend.get_title().get_text() if title is None else title
|
|
473
|
+
legend.remove()
|
|
474
|
+
return [ax.legend(handles, labels, loc=loc, title=new_title,
|
|
475
|
+
**legend_kwargs)
|
|
476
|
+
for ax, loc in zip(axes, locs)]
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def band_under_lines(
|
|
480
|
+
ax: Axes,
|
|
481
|
+
bands: Sequence[tuple[Any, Any]],
|
|
482
|
+
*,
|
|
483
|
+
alpha: float = 0.25,
|
|
484
|
+
**fill_kwargs: Any,
|
|
485
|
+
) -> list[PolyCollection]:
|
|
486
|
+
"""
|
|
487
|
+
Shade one band under each line of `ax`, in that line's color.
|
|
488
|
+
|
|
489
|
+
``bands[i]`` is the pair ``(lower, upper)`` of the band under
|
|
490
|
+
``ax.lines[i]``, each an array over that line's x data; the band is
|
|
491
|
+
drawn with :meth:`matplotlib.axes.Axes.fill_between`. Lines beyond
|
|
492
|
+
``len(bands)`` get no band. Time and space are :math:`O(n_\\mathrm{lines}
|
|
493
|
+
\\, n_\\mathrm{points})`.
|
|
494
|
+
|
|
495
|
+
Parameters
|
|
496
|
+
----------
|
|
497
|
+
ax : matplotlib.axes.Axes
|
|
498
|
+
The axes whose lines are shaded, in drawing order.
|
|
499
|
+
bands : sequence of pairs of array_like
|
|
500
|
+
The lower and upper edge of each band, shape ``(n_points,)`` each,
|
|
501
|
+
matching the x data of the line.
|
|
502
|
+
alpha : float, default 0.25
|
|
503
|
+
The opacity of every band.
|
|
504
|
+
**fill_kwargs
|
|
505
|
+
Passed to every :meth:`matplotlib.axes.Axes.fill_between` call,
|
|
506
|
+
such as ``linewidth``.
|
|
507
|
+
|
|
508
|
+
Returns
|
|
509
|
+
-------
|
|
510
|
+
list of matplotlib.collections.PolyCollection
|
|
511
|
+
The bands, in the order of the lines.
|
|
512
|
+
|
|
513
|
+
Raises
|
|
514
|
+
------
|
|
515
|
+
ValueError
|
|
516
|
+
If there are more bands than lines, or a band edge's length differs
|
|
517
|
+
from its line's number of points.
|
|
518
|
+
|
|
519
|
+
Examples
|
|
520
|
+
--------
|
|
521
|
+
>>> import matplotlib
|
|
522
|
+
>>> matplotlib.use("Agg")
|
|
523
|
+
>>> import matplotlib.pyplot as plt
|
|
524
|
+
>>> fig, ax = plt.subplots()
|
|
525
|
+
>>> _ = ax.plot([0, 1, 2], [1, 1, 1], color="#4477AA")
|
|
526
|
+
>>> bands = band_under_lines(ax, [([0.5, 0.5, 0.5], [1.5, 1.5, 1.5])])
|
|
527
|
+
>>> len(bands)
|
|
528
|
+
1
|
|
529
|
+
>>> bands[0].get_facecolor()[0].round(2).tolist()
|
|
530
|
+
[0.27, 0.47, 0.67, 0.25]
|
|
531
|
+
>>> plt.close(fig)
|
|
532
|
+
"""
|
|
533
|
+
lines = list(ax.lines)
|
|
534
|
+
if len(bands) > len(lines):
|
|
535
|
+
raise ValueError(
|
|
536
|
+
f"{len(bands)} bands were given for {len(lines)} lines.")
|
|
537
|
+
collections: list[PolyCollection] = []
|
|
538
|
+
for line, (lower, upper) in zip(lines, bands):
|
|
539
|
+
x = np.asarray(line.get_xdata(), dtype=float)
|
|
540
|
+
lower = np.asarray(lower, dtype=float)
|
|
541
|
+
upper = np.asarray(upper, dtype=float)
|
|
542
|
+
if lower.shape != x.shape or upper.shape != x.shape:
|
|
543
|
+
raise ValueError(
|
|
544
|
+
f"a band has edges of shape {lower.shape} and "
|
|
545
|
+
f"{upper.shape} for a line of {x.shape[0]} points.")
|
|
546
|
+
collections.append(
|
|
547
|
+
ax.fill_between(x, lower, upper, color=line.get_color(),
|
|
548
|
+
alpha=alpha, **fill_kwargs))
|
|
549
|
+
return collections
|