bsplot 0.0.2__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.
- bsplot/__init__.py +23 -0
- bsplot/anat.py +55 -0
- bsplot/animate.py +194 -0
- bsplot/bioicons.py +777 -0
- bsplot/brain.py +188 -0
- bsplot/colors.py +1935 -0
- bsplot/data/HCP_avg-SC.txt +379 -0
- bsplot/data/MNI152.rh.pial +0 -0
- bsplot/data/STN_lh.nii.gz +0 -0
- bsplot/data/STN_rh.nii.gz +0 -0
- bsplot/data/__init__.py +22 -0
- bsplot/data/left_electrode.ply +257270 -0
- bsplot/data/parcellations/HCP-MMP1.L.label.gii +442 -0
- bsplot/data/parcellations/HCP-MMP1.R.label.gii +442 -0
- bsplot/data/parcellations/code/split_hcpmmp1.sh +4 -0
- bsplot/data/right_electrode.ply +257270 -0
- bsplot/data/surface.py +112 -0
- bsplot/data/tpl-MNI152NLin2009b_atlas-hcpmmp1_desc-ordered_dseg.nii.gz +0 -0
- bsplot/figure.py +177 -0
- bsplot/graph/__init__.py +31 -0
- bsplot/graph/edges.py +312 -0
- bsplot/graph/flowchart.py +762 -0
- bsplot/graph/layout.py +201 -0
- bsplot/graph/network.py +974 -0
- bsplot/graph/nodes.py +452 -0
- bsplot/panels.py +110 -0
- bsplot/scientific_color_maps/__init__.py +0 -0
- bsplot/scientific_color_maps/_sync_from_zenodo_download.sh +1 -0
- bsplot/scientific_color_maps/batlowK.txt +256 -0
- bsplot/scientific_color_maps/batlowW.txt +256 -0
- bsplot/scientific_color_maps/cyclic/bamO.txt +256 -0
- bsplot/scientific_color_maps/cyclic/brocO.txt +256 -0
- bsplot/scientific_color_maps/cyclic/corkO.txt +256 -0
- bsplot/scientific_color_maps/cyclic/romaO.txt +256 -0
- bsplot/scientific_color_maps/cyclic/vikO.txt +256 -0
- bsplot/scientific_color_maps/diverging/bam.txt +256 -0
- bsplot/scientific_color_maps/diverging/berlin.txt +256 -0
- bsplot/scientific_color_maps/diverging/broc.txt +256 -0
- bsplot/scientific_color_maps/diverging/cork.txt +256 -0
- bsplot/scientific_color_maps/diverging/lisbon.txt +256 -0
- bsplot/scientific_color_maps/diverging/managua.txt +256 -0
- bsplot/scientific_color_maps/diverging/roma.txt +256 -0
- bsplot/scientific_color_maps/diverging/tofino.txt +256 -0
- bsplot/scientific_color_maps/diverging/vanimo.txt +256 -0
- bsplot/scientific_color_maps/diverging/vik.txt +256 -0
- bsplot/scientific_color_maps/multisequential/bukavu.txt +256 -0
- bsplot/scientific_color_maps/multisequential/fes.txt +256 -0
- bsplot/scientific_color_maps/multisequential/oleron.txt +256 -0
- bsplot/scientific_color_maps/naviaW.txt +256 -0
- bsplot/scientific_color_maps/sequential/acton.txt +256 -0
- bsplot/scientific_color_maps/sequential/bamako.txt +256 -0
- bsplot/scientific_color_maps/sequential/batlow.txt +256 -0
- bsplot/scientific_color_maps/sequential/bilbao.txt +256 -0
- bsplot/scientific_color_maps/sequential/buda.txt +256 -0
- bsplot/scientific_color_maps/sequential/davos.txt +256 -0
- bsplot/scientific_color_maps/sequential/devon.txt +256 -0
- bsplot/scientific_color_maps/sequential/glasgow.txt +256 -0
- bsplot/scientific_color_maps/sequential/grayC.txt +256 -0
- bsplot/scientific_color_maps/sequential/hawaii.txt +256 -0
- bsplot/scientific_color_maps/sequential/imola.txt +256 -0
- bsplot/scientific_color_maps/sequential/lajolla.txt +256 -0
- bsplot/scientific_color_maps/sequential/lapaz.txt +256 -0
- bsplot/scientific_color_maps/sequential/lipari.txt +256 -0
- bsplot/scientific_color_maps/sequential/navia.txt +256 -0
- bsplot/scientific_color_maps/sequential/nuuk.txt +256 -0
- bsplot/scientific_color_maps/sequential/oslo.txt +256 -0
- bsplot/scientific_color_maps/sequential/tokyo.txt +256 -0
- bsplot/scientific_color_maps/sequential/turku.txt +256 -0
- bsplot/streamlines.py +314 -0
- bsplot/style.py +791 -0
- bsplot/styles/black.mplstyle +78 -0
- bsplot/styles/bss.mplstyle +50 -0
- bsplot/styles/bwcomp.mplstyle +30 -0
- bsplot/styles/nature.mplstyle +57 -0
- bsplot/styles/transparent.mplstyle +16 -0
- bsplot/styles/tvbo.mplstyle +57 -0
- bsplot/surface.py +2370 -0
- bsplot/templates.py +270 -0
- bsplot/text.py +199 -0
- bsplot/text2obj.py +59 -0
- bsplot/timeseries.py +10 -0
- bsplot/utils.py +13 -0
- bsplot/volume.py +958 -0
- bsplot-0.0.2.dist-info/METADATA +60 -0
- bsplot-0.0.2.dist-info/RECORD +89 -0
- bsplot-0.0.2.dist-info/WHEEL +5 -0
- bsplot-0.0.2.dist-info/entry_points.txt +2 -0
- bsplot-0.0.2.dist-info/licenses/LICENSE +193 -0
- bsplot-0.0.2.dist-info/top_level.txt +1 -0
bsplot/style.py
ADDED
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
import numpy as np
|
|
5
|
+
from matplotlib.ticker import FormatStrFormatter, FuncFormatter
|
|
6
|
+
from bsplot import panels
|
|
7
|
+
import bsplot # Import your package to access its path
|
|
8
|
+
from .colors import make_cycler_from_cmap
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def use(style="bss"):
|
|
12
|
+
"""Apply one or more bsplot style modules.
|
|
13
|
+
|
|
14
|
+
Styles can be composed by separating modules with ``-``.
|
|
15
|
+
Modules are layered left-to-right, so later modules override earlier ones.
|
|
16
|
+
|
|
17
|
+
Examples
|
|
18
|
+
--------
|
|
19
|
+
>>> bsplot.style.use("black") # single base style
|
|
20
|
+
>>> bsplot.style.use("black-transparent") # base + module
|
|
21
|
+
>>> bsplot.style.use("tvbo-transparent") # works with any base
|
|
22
|
+
"""
|
|
23
|
+
styles_dir = os.path.join(os.path.dirname(bsplot.__file__), "styles")
|
|
24
|
+
|
|
25
|
+
def _style_path(name):
|
|
26
|
+
p = os.path.join(styles_dir, f"{name}.mplstyle")
|
|
27
|
+
if not os.path.exists(p):
|
|
28
|
+
available = sorted(
|
|
29
|
+
f.removesuffix(".mplstyle")
|
|
30
|
+
for f in os.listdir(styles_dir)
|
|
31
|
+
if f.endswith(".mplstyle")
|
|
32
|
+
)
|
|
33
|
+
raise ValueError(
|
|
34
|
+
f"Style module '{name}' not found. "
|
|
35
|
+
f"Available modules: {available}"
|
|
36
|
+
)
|
|
37
|
+
return p
|
|
38
|
+
|
|
39
|
+
# Split on "-" to get composable modules: "black-transparent" -> ["black", "transparent"]
|
|
40
|
+
modules = style.split("-")
|
|
41
|
+
paths = [_style_path(m) for m in modules]
|
|
42
|
+
plt.style.use(paths)
|
|
43
|
+
|
|
44
|
+
# Turn off legacy layout flags that can conflict
|
|
45
|
+
plt.rcParams["figure.constrained_layout.use"] = False
|
|
46
|
+
plt.rcParams["figure.autolayout"] = False
|
|
47
|
+
|
|
48
|
+
if modules[0] == "black":
|
|
49
|
+
plt.rcParams["axes.prop_cycle"]._left[0] = {"color": "white"}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def format(obj, **kwargs):
|
|
53
|
+
"""Intelligently format matplotlib objects based on their type.
|
|
54
|
+
|
|
55
|
+
This is a convenience function that dispatches to the appropriate formatter:
|
|
56
|
+
- Figure -> format_fig()
|
|
57
|
+
- Axes with label '<colorbar>' -> format_colorbar()
|
|
58
|
+
- Axes -> format_ax()
|
|
59
|
+
- list/tuple of objects -> applies format() to each
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
obj : Figure, Axes, or list/tuple of these
|
|
64
|
+
The matplotlib object(s) to format.
|
|
65
|
+
**kwargs
|
|
66
|
+
Additional arguments passed to the appropriate formatting function.
|
|
67
|
+
Common kwargs:
|
|
68
|
+
- tick_decimals: int | None | "smart" for axes tick formatting
|
|
69
|
+
- colorbar_decimals: int | None for colorbar tick formatting
|
|
70
|
+
- add_panel_numbers: "auto" | True | False (for figures only)
|
|
71
|
+
- n_ticks: int, number of ticks per axis
|
|
72
|
+
- shift_left_spine, shift_bottom_spine: float, spine offsets
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
obj
|
|
77
|
+
The formatted object (for chaining).
|
|
78
|
+
|
|
79
|
+
Examples
|
|
80
|
+
--------
|
|
81
|
+
>>> fig, ax = plt.subplots()
|
|
82
|
+
>>> ax.plot([1, 2, 3])
|
|
83
|
+
>>> style.format(fig) # Formats entire figure
|
|
84
|
+
|
|
85
|
+
>>> style.format(ax, n_ticks=5) # Format single axes
|
|
86
|
+
|
|
87
|
+
>>> style.format([ax1, ax2], tick_decimals=2) # Format multiple axes
|
|
88
|
+
"""
|
|
89
|
+
import matplotlib.figure
|
|
90
|
+
import matplotlib.axes
|
|
91
|
+
|
|
92
|
+
# Handle lists/tuples of objects
|
|
93
|
+
if isinstance(obj, (list, tuple)):
|
|
94
|
+
for item in obj:
|
|
95
|
+
format(item, **kwargs)
|
|
96
|
+
return obj
|
|
97
|
+
|
|
98
|
+
# Handle Figure
|
|
99
|
+
if isinstance(obj, matplotlib.figure.Figure):
|
|
100
|
+
format_fig(obj, **kwargs)
|
|
101
|
+
return obj
|
|
102
|
+
|
|
103
|
+
# Handle Axes
|
|
104
|
+
if isinstance(obj, matplotlib.axes.Axes):
|
|
105
|
+
# Check if it's a colorbar
|
|
106
|
+
if obj.get_label() == "<colorbar>":
|
|
107
|
+
# Extract colorbar-specific kwargs
|
|
108
|
+
colorbar_kwargs = {}
|
|
109
|
+
if "colorbar_decimals" in kwargs:
|
|
110
|
+
colorbar_kwargs["colorbar_decimals"] = kwargs["colorbar_decimals"]
|
|
111
|
+
format_colorbar(obj, **colorbar_kwargs)
|
|
112
|
+
else:
|
|
113
|
+
# Filter out figure-only kwargs
|
|
114
|
+
ax_kwargs = {k: v for k, v in kwargs.items()
|
|
115
|
+
if k not in ("add_panel_numbers", "colorbar_decimals")}
|
|
116
|
+
format_ax(obj, **ax_kwargs)
|
|
117
|
+
return obj
|
|
118
|
+
|
|
119
|
+
# If we get here, we don't know how to handle this type
|
|
120
|
+
raise TypeError(
|
|
121
|
+
f"Cannot format object of type {type(obj).__name__}. "
|
|
122
|
+
f"Expected Figure, Axes, or list/tuple of these."
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def format_fig(
|
|
127
|
+
fig, colorbar_decimals=None, tick_decimals=None, add_panel_numbers="auto", **kwargs
|
|
128
|
+
):
|
|
129
|
+
"""Format all axes in a figure.
|
|
130
|
+
|
|
131
|
+
Parameters (forwarded):
|
|
132
|
+
- tick_decimals: int | None | "smart" -> number of decimals for normal axes ticks.
|
|
133
|
+
If None or "smart", uses automatic detection based on data range.
|
|
134
|
+
- colorbar_decimals: int | None -> number of decimals for colorbar ticks.
|
|
135
|
+
If None, uses automatic detection based on data range.
|
|
136
|
+
- other kwargs are passed to format_ax/format_colorbar as appropriate
|
|
137
|
+
"""
|
|
138
|
+
i = 1
|
|
139
|
+
for ax in fig.axes:
|
|
140
|
+
# Skip polar (and other non-rectilinear) axes – they lack standard spines
|
|
141
|
+
if ax.name != "rectilinear" and ax.axes.get_label() != "<colorbar>":
|
|
142
|
+
continue
|
|
143
|
+
if ax.axes.get_label() == "<colorbar>":
|
|
144
|
+
format_colorbar(ax, colorbar_decimals=colorbar_decimals)
|
|
145
|
+
else:
|
|
146
|
+
if "shift_left_spine" not in kwargs:
|
|
147
|
+
kwargs["shift_left_spine"] = -0.02
|
|
148
|
+
if "shift_bottom_spine" not in kwargs:
|
|
149
|
+
kwargs["shift_bottom_spine"] = -0.02
|
|
150
|
+
format_ax(
|
|
151
|
+
ax,
|
|
152
|
+
tick_decimals=tick_decimals,
|
|
153
|
+
**kwargs,
|
|
154
|
+
)
|
|
155
|
+
if add_panel_numbers != False and add_panel_numbers == "auto":
|
|
156
|
+
panels.add_panel_number(
|
|
157
|
+
ax, ax.get_label(), x_shift=kwargs["shift_left_spine"], y_shift=0.02
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
elif add_panel_numbers == True:
|
|
161
|
+
panels.add_panel_number(
|
|
162
|
+
ax, i, x_shift=kwargs["shift_left_spine"], y_shift=0.02
|
|
163
|
+
)
|
|
164
|
+
i += 1
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def format_all(**kwargs):
|
|
168
|
+
"""Format every open matplotlib figure.
|
|
169
|
+
|
|
170
|
+
Convenience wrapper that calls :func:`format_fig` on each figure
|
|
171
|
+
returned by ``plt.get_fignums()``.
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
**kwargs
|
|
176
|
+
Forwarded to :func:`format_fig` (e.g. ``tick_decimals``,
|
|
177
|
+
``add_panel_numbers``, ``colorbar_decimals``).
|
|
178
|
+
"""
|
|
179
|
+
for num in plt.get_fignums():
|
|
180
|
+
format_fig(plt.figure(num), **kwargs)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _has_string_ticklabels(ax, axis="x"):
|
|
184
|
+
"""
|
|
185
|
+
Check if an axis has custom string-based (non-numeric) tick labels.
|
|
186
|
+
|
|
187
|
+
Returns True if any tick label is a non-numeric string that cannot be
|
|
188
|
+
converted to a float. This is used to preserve user-set categorical labels
|
|
189
|
+
like '$Dice_L$', 'Category A', etc.
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
if axis == "x":
|
|
193
|
+
labels = [t.get_text() for t in ax.get_xticklabels()]
|
|
194
|
+
else:
|
|
195
|
+
labels = [t.get_text() for t in ax.get_yticklabels()]
|
|
196
|
+
|
|
197
|
+
# Filter out empty labels (matplotlib default)
|
|
198
|
+
labels = [l for l in labels if l.strip()]
|
|
199
|
+
|
|
200
|
+
if not labels:
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
# Check if any label is non-numeric
|
|
204
|
+
for label in labels:
|
|
205
|
+
# Strip common LaTeX wrappers for numeric check
|
|
206
|
+
# Also replace Unicode minus (U+2212) with ASCII hyphen
|
|
207
|
+
clean = label.replace('$', '').replace(',', '').replace('\u2212', '-').strip()
|
|
208
|
+
try:
|
|
209
|
+
float(clean)
|
|
210
|
+
except ValueError:
|
|
211
|
+
# Found a non-numeric label
|
|
212
|
+
return True
|
|
213
|
+
return False
|
|
214
|
+
except Exception:
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _nice_round(x, step, direction="down"):
|
|
219
|
+
"""Round *x* to the nearest multiple of *step*.
|
|
220
|
+
|
|
221
|
+
``"down"`` rounds toward −∞, ``"up"`` toward +∞.
|
|
222
|
+
"""
|
|
223
|
+
if step == 0:
|
|
224
|
+
return x
|
|
225
|
+
if direction == "down":
|
|
226
|
+
return np.floor(x / step) * step
|
|
227
|
+
else:
|
|
228
|
+
return np.ceil(x / step) * step
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _auto_round_step(span):
|
|
232
|
+
"""Derive a small rounding granularity from *span*.
|
|
233
|
+
|
|
234
|
+
Returns a value from {1, 2, 5} × 10^n that is roughly 1/20th of
|
|
235
|
+
the span, keeping padding minimal. Examples:
|
|
236
|
+
|
|
237
|
+
* span ≈ 250 → 10 (so 125 → 130)
|
|
238
|
+
* span ≈ 1.4 → 0.05
|
|
239
|
+
* span ≈ 30 → 1
|
|
240
|
+
"""
|
|
241
|
+
if span <= 0:
|
|
242
|
+
return 1.0
|
|
243
|
+
target = span / 20.0
|
|
244
|
+
mag = 10 ** np.floor(np.log10(target))
|
|
245
|
+
residual = target / mag
|
|
246
|
+
if residual <= 1.5:
|
|
247
|
+
return mag
|
|
248
|
+
elif residual <= 3.5:
|
|
249
|
+
return 2.0 * mag
|
|
250
|
+
elif residual <= 7.5:
|
|
251
|
+
return 5.0 * mag
|
|
252
|
+
else:
|
|
253
|
+
return 10.0 * mag
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def format_ax(
|
|
257
|
+
ax,
|
|
258
|
+
x_datatype="continuous",
|
|
259
|
+
n_ticks=4,
|
|
260
|
+
box_aspect=None,
|
|
261
|
+
shift_left_spine=-0.02,
|
|
262
|
+
shift_bottom_spine=-0.02,
|
|
263
|
+
ylim=None,
|
|
264
|
+
xlim=None,
|
|
265
|
+
tick_decimals=None,
|
|
266
|
+
y_datatype=None,
|
|
267
|
+
datatype=None,
|
|
268
|
+
x_tick_decimals=None,
|
|
269
|
+
y_tick_decimals=None,
|
|
270
|
+
n_x_ticks=None,
|
|
271
|
+
n_y_ticks=None,
|
|
272
|
+
scientific=False,
|
|
273
|
+
round_step=None,
|
|
274
|
+
):
|
|
275
|
+
"""
|
|
276
|
+
Format matplotlib Axes:
|
|
277
|
+
- shifts spines
|
|
278
|
+
- enforces aspect ratio
|
|
279
|
+
- continuous: evenly spaced numeric ticks with optional rounding
|
|
280
|
+
- discrete: integer ticks (first, last, and evenly spaced in between)
|
|
281
|
+
|
|
282
|
+
Parameters:
|
|
283
|
+
- x_datatype: "continuous" | "discrete"
|
|
284
|
+
- y_datatype: "continuous" | "discrete" | None (defaults to x_datatype)
|
|
285
|
+
- datatype: "continuous" | "discrete" | None (if set, applies to both axes)
|
|
286
|
+
- n_ticks: maximum number of ticks per axis (endpoints included when possible)
|
|
287
|
+
- tick_decimals: int | "smart" | None -> default formatting for continuous ticks
|
|
288
|
+
- x_tick_decimals: int | "smart" | None -> override for x (falls back to tick_decimals)
|
|
289
|
+
- y_tick_decimals: int | "smart" | None -> override for y (falls back to tick_decimals)
|
|
290
|
+
- n_x_ticks: int | None -> override tick count for x-axis (falls back to n_ticks)
|
|
291
|
+
- n_y_ticks: int | None -> override tick count for y-axis (falls back to n_ticks)
|
|
292
|
+
- scientific: bool -> if True and smart formatting is selected, allow scientific
|
|
293
|
+
notation with a single axis-wide exponent; if False, never use scientific mode.
|
|
294
|
+
- round_step: float | None -> rounding granularity for the axis endpoints.
|
|
295
|
+
E.g. round_step=10 rounds -67 → -70, 87 → 90.
|
|
296
|
+
If None (default), the step is automatically derived from the data range.
|
|
297
|
+
|
|
298
|
+
Note:
|
|
299
|
+
- Tick formatting is skipped for axes with string-based (categorical) tick labels.
|
|
300
|
+
This preserves custom labels like LaTeX strings or category names.
|
|
301
|
+
"""
|
|
302
|
+
# If a single datatype is provided, apply to both axes (unless y_datatype explicitly set)
|
|
303
|
+
if datatype is not None:
|
|
304
|
+
x_datatype = datatype
|
|
305
|
+
if y_datatype is None:
|
|
306
|
+
y_datatype = datatype
|
|
307
|
+
|
|
308
|
+
if y_datatype is None:
|
|
309
|
+
y_datatype = x_datatype
|
|
310
|
+
|
|
311
|
+
# Polar / non-rectilinear axes lack standard spines; bail out early.
|
|
312
|
+
if ax.name != "rectilinear":
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
if ylim is not None:
|
|
316
|
+
ax.set_ylim(ylim)
|
|
317
|
+
if xlim is not None:
|
|
318
|
+
ax.set_xlim(xlim)
|
|
319
|
+
|
|
320
|
+
ax.spines["left"].set_position(("axes", shift_left_spine))
|
|
321
|
+
ax.spines["bottom"].set_position(("axes", shift_bottom_spine))
|
|
322
|
+
|
|
323
|
+
if box_aspect:
|
|
324
|
+
ax.set_box_aspect(box_aspect)
|
|
325
|
+
|
|
326
|
+
# Resolve per-axis decimals for continuous axes
|
|
327
|
+
_x_dec = x_tick_decimals if x_tick_decimals is not None else tick_decimals
|
|
328
|
+
_y_dec = y_tick_decimals if y_tick_decimals is not None else tick_decimals
|
|
329
|
+
|
|
330
|
+
# Resolve per-axis tick counts
|
|
331
|
+
_nx = n_x_ticks if n_x_ticks is not None else n_ticks
|
|
332
|
+
_ny = n_y_ticks if n_y_ticks is not None else n_ticks
|
|
333
|
+
|
|
334
|
+
# Helper to trim spines to tick range
|
|
335
|
+
def _update_spine_bounds(axis: str, ticks):
|
|
336
|
+
if ticks is None or len(ticks) < 2:
|
|
337
|
+
return
|
|
338
|
+
lo, hi = float(np.min(ticks)), float(np.max(ticks))
|
|
339
|
+
try:
|
|
340
|
+
if axis == "x":
|
|
341
|
+
ax.spines["bottom"].set_bounds(lo, hi)
|
|
342
|
+
else:
|
|
343
|
+
ax.spines["left"].set_bounds(lo, hi)
|
|
344
|
+
except Exception:
|
|
345
|
+
# set_bounds not available for some artists; ignore
|
|
346
|
+
pass
|
|
347
|
+
|
|
348
|
+
# Helpers
|
|
349
|
+
def _set_continuous(axis: str):
|
|
350
|
+
k = _nx if axis == "x" else _ny
|
|
351
|
+
k = max(2, int(k)) # ensure at least 2 ticks
|
|
352
|
+
if k == 0:
|
|
353
|
+
(ax.set_xticks([]) if axis == "x" else ax.set_yticks([]))
|
|
354
|
+
return
|
|
355
|
+
lo, hi = ax.get_xlim() if axis == "x" else ax.get_ylim()
|
|
356
|
+
a, b = min(lo, hi), max(lo, hi)
|
|
357
|
+
# Guard against degenerate or non-finite limits
|
|
358
|
+
if not np.isfinite(a) or not np.isfinite(b) or a == b:
|
|
359
|
+
center = a if np.isfinite(a) else (b if np.isfinite(b) else 0.0)
|
|
360
|
+
span = (
|
|
361
|
+
1.0 if not np.isfinite(a) or not np.isfinite(b) or a == b else (b - a)
|
|
362
|
+
)
|
|
363
|
+
a, b = center - span / 2.0, center + span / 2.0
|
|
364
|
+
|
|
365
|
+
# Round endpoints outward by a small amount so the axis starts
|
|
366
|
+
# and ends at a round number. The rounding step is ~1/10th of
|
|
367
|
+
# the span by default (e.g. 125 → 130 for a 250-wide range).
|
|
368
|
+
step = round_step if round_step is not None else _auto_round_step(b - a)
|
|
369
|
+
a_nice = _nice_round(a, step, "down")
|
|
370
|
+
b_nice = _nice_round(b, step, "up")
|
|
371
|
+
|
|
372
|
+
ticks = np.linspace(a_nice, b_nice, k)
|
|
373
|
+
ticks[0], ticks[-1] = a_nice, b_nice # keep endpoints exact
|
|
374
|
+
# Round intermediate ticks to the same step for clean labels
|
|
375
|
+
for i in range(1, len(ticks) - 1):
|
|
376
|
+
ticks[i] = np.round(ticks[i] / step) * step
|
|
377
|
+
|
|
378
|
+
dec = _x_dec if axis == "x" else _y_dec
|
|
379
|
+
if isinstance(dec, int):
|
|
380
|
+
rounded = np.round(ticks, dec)
|
|
381
|
+
if np.unique(rounded).size == rounded.size:
|
|
382
|
+
ticks = rounded
|
|
383
|
+
|
|
384
|
+
t_lo, t_hi = float(ticks[0]), float(ticks[-1])
|
|
385
|
+
|
|
386
|
+
if axis == "x":
|
|
387
|
+
ax.set_xlim(t_lo, t_hi)
|
|
388
|
+
ax.set_xticks(ticks.tolist())
|
|
389
|
+
if isinstance(dec, int):
|
|
390
|
+
if np.unique(np.round(ticks, dec)).size < 2:
|
|
391
|
+
fmt, offset = make_smart_formatter(t_lo, t_hi, k, allow_sci=scientific)
|
|
392
|
+
ax.xaxis.set_major_formatter(FuncFormatter(fmt))
|
|
393
|
+
_apply_axis_offset(ax, "x", offset, pos="top")
|
|
394
|
+
else:
|
|
395
|
+
ax.xaxis.set_major_formatter(FormatStrFormatter(f"%.{dec}f"))
|
|
396
|
+
else:
|
|
397
|
+
fmt, offset = make_smart_formatter(t_lo, t_hi, k, allow_sci=scientific)
|
|
398
|
+
ax.xaxis.set_major_formatter(FuncFormatter(fmt))
|
|
399
|
+
_apply_axis_offset(ax, "x", offset, pos="top")
|
|
400
|
+
_update_spine_bounds("x", ticks)
|
|
401
|
+
else:
|
|
402
|
+
ax.set_ylim(t_lo, t_hi)
|
|
403
|
+
ax.set_yticks(ticks.tolist())
|
|
404
|
+
if isinstance(dec, int):
|
|
405
|
+
if np.unique(np.round(ticks, dec)).size < 2:
|
|
406
|
+
fmt, offset = make_smart_formatter(t_lo, t_hi, k, allow_sci=scientific)
|
|
407
|
+
ax.yaxis.set_major_formatter(FuncFormatter(fmt))
|
|
408
|
+
_apply_axis_offset(ax, "y", offset, pos="right")
|
|
409
|
+
else:
|
|
410
|
+
ax.yaxis.set_major_formatter(FormatStrFormatter(f"%.{dec}f"))
|
|
411
|
+
else:
|
|
412
|
+
fmt, offset = make_smart_formatter(t_lo, t_hi, k, allow_sci=scientific)
|
|
413
|
+
ax.yaxis.set_major_formatter(FuncFormatter(fmt))
|
|
414
|
+
_apply_axis_offset(ax, "y", offset, pos="right")
|
|
415
|
+
_update_spine_bounds("y", ticks)
|
|
416
|
+
|
|
417
|
+
def _set_discrete(axis: str):
|
|
418
|
+
k_in = _nx if axis == "x" else _ny
|
|
419
|
+
if k_in == 0:
|
|
420
|
+
(ax.set_xticks([]) if axis == "x" else ax.set_yticks([]))
|
|
421
|
+
return
|
|
422
|
+
lo, hi = ax.get_xlim() if axis == "x" else ax.get_ylim()
|
|
423
|
+
a, b = min(lo, hi), max(lo, hi)
|
|
424
|
+
# For imshow-like limits [-0.5, N-0.5], integer indices are [0, N-1]
|
|
425
|
+
ia = int(np.round(a + 0.5))
|
|
426
|
+
ib = int(np.round(b - 0.5))
|
|
427
|
+
if ib < ia:
|
|
428
|
+
ib = ia
|
|
429
|
+
count = ib - ia + 1
|
|
430
|
+
k = max(2, min(int(k_in), count))
|
|
431
|
+
ticks = np.round(np.linspace(ia, ib, k)).astype(int)
|
|
432
|
+
ticks = np.unique(ticks)
|
|
433
|
+
if ticks.size < 2:
|
|
434
|
+
ticks = np.array([ia, ib], dtype=int)
|
|
435
|
+
else:
|
|
436
|
+
ticks[0] = ia
|
|
437
|
+
ticks[-1] = ib
|
|
438
|
+
if axis == "x":
|
|
439
|
+
ax.set_xticks(ticks.tolist())
|
|
440
|
+
ax.xaxis.set_major_formatter(FormatStrFormatter("%d"))
|
|
441
|
+
# Trim bottom spine to tick range
|
|
442
|
+
_update_spine_bounds("x", ticks)
|
|
443
|
+
else:
|
|
444
|
+
ax.set_yticks(ticks.tolist())
|
|
445
|
+
ax.yaxis.set_major_formatter(FormatStrFormatter("%d"))
|
|
446
|
+
# Trim left spine to tick range
|
|
447
|
+
_update_spine_bounds("y", ticks)
|
|
448
|
+
|
|
449
|
+
# Check for string-based tick labels before modifying ticks
|
|
450
|
+
x_has_string_labels = _has_string_ticklabels(ax, "x")
|
|
451
|
+
y_has_string_labels = _has_string_ticklabels(ax, "y")
|
|
452
|
+
|
|
453
|
+
# Apply per-axis (skip if string labels are present)
|
|
454
|
+
if not x_has_string_labels:
|
|
455
|
+
if x_datatype == "continuous":
|
|
456
|
+
_set_continuous("x")
|
|
457
|
+
elif x_datatype == "discrete":
|
|
458
|
+
_set_discrete("x")
|
|
459
|
+
|
|
460
|
+
if not y_has_string_labels:
|
|
461
|
+
if y_datatype == "continuous":
|
|
462
|
+
_set_continuous("y")
|
|
463
|
+
elif y_datatype == "discrete":
|
|
464
|
+
_set_discrete("y")
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def smart_format(x):
|
|
468
|
+
"""Adaptive tick formatter:
|
|
469
|
+
- Uses scientific notation for very small/large values.
|
|
470
|
+
- Otherwise formats with up to 4 decimals and strips trailing zeros.
|
|
471
|
+
- Avoids negative zero.
|
|
472
|
+
"""
|
|
473
|
+
# Normalize near-zero to 0
|
|
474
|
+
if abs(x) < 1e-12:
|
|
475
|
+
return "0"
|
|
476
|
+
|
|
477
|
+
m = abs(x)
|
|
478
|
+
# Extreme magnitudes -> scientific with 3 significant digits
|
|
479
|
+
if m >= 1e4 or m < 1e-3:
|
|
480
|
+
s = f"{x:.3g}"
|
|
481
|
+
else:
|
|
482
|
+
# Adaptive fixed-point decimals by magnitude
|
|
483
|
+
if m >= 100:
|
|
484
|
+
dec = 0
|
|
485
|
+
elif m >= 10:
|
|
486
|
+
dec = 1
|
|
487
|
+
elif m >= 1:
|
|
488
|
+
dec = 2
|
|
489
|
+
elif m >= 0.1:
|
|
490
|
+
dec = 3
|
|
491
|
+
else:
|
|
492
|
+
dec = 4
|
|
493
|
+
s = f"{x:.{dec}f}"
|
|
494
|
+
if "." in s:
|
|
495
|
+
s = s.rstrip("0").rstrip(".")
|
|
496
|
+
if s == "-0":
|
|
497
|
+
s = "0"
|
|
498
|
+
return s
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def make_smart_formatter(lo, hi, k=5, allow_sci=True):
|
|
502
|
+
"""
|
|
503
|
+
Axis-consistent smart formatter:
|
|
504
|
+
- If scientific is chosen (and allowed), show mantissas as tick labels and a single 'e±NN'
|
|
505
|
+
as axis offset text (no mixed styles on the same axis).
|
|
506
|
+
- Else, fixed-point with decimals derived from tick step.
|
|
507
|
+
Returns (formatter_func, offset_text or None).
|
|
508
|
+
"""
|
|
509
|
+
a, b = (min(lo, hi), max(lo, hi))
|
|
510
|
+
magnitude = max(abs(a), abs(b))
|
|
511
|
+
step = (b - a) / max(k - 1, 1) if k and k > 1 else (b - a)
|
|
512
|
+
|
|
513
|
+
use_sci = allow_sci and (
|
|
514
|
+
(magnitude >= 1e4)
|
|
515
|
+
or (0 < magnitude < 1e-3)
|
|
516
|
+
or (abs(step) >= 1e4)
|
|
517
|
+
or (0 < abs(step) < 1e-3)
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
if use_sci:
|
|
521
|
+
if magnitude == 0:
|
|
522
|
+
E = 0
|
|
523
|
+
else:
|
|
524
|
+
E = int(np.floor(np.log10(magnitude)))
|
|
525
|
+
scale = 10.0**E
|
|
526
|
+
step_scaled = abs(step) / scale if step != 0 else 0
|
|
527
|
+
# Build tick mantissas for uniqueness test
|
|
528
|
+
ticks = np.linspace(a, b, max(2, int(k))) if k and k > 1 else np.array([a, b])
|
|
529
|
+
mant_ticks = ticks / scale if scale != 0 else ticks
|
|
530
|
+
|
|
531
|
+
# Choose the minimal decimals (<=6) that yield unique rounded mantissas
|
|
532
|
+
dec = 6
|
|
533
|
+
for d in range(0, 7):
|
|
534
|
+
S = 10**d
|
|
535
|
+
ints = np.round(mant_ticks * S).astype(np.int64)
|
|
536
|
+
if np.unique(ints).size == ints.size:
|
|
537
|
+
dec = d
|
|
538
|
+
break
|
|
539
|
+
|
|
540
|
+
def fmt(v, _pos=None):
|
|
541
|
+
mant = v / scale
|
|
542
|
+
val = np.round(mant, dec)
|
|
543
|
+
s = f"{val:.{dec}f}"
|
|
544
|
+
if "." in s:
|
|
545
|
+
s = s.rstrip("0").rstrip(".")
|
|
546
|
+
return "0" if s == "-0" else s
|
|
547
|
+
|
|
548
|
+
sign = "-" if E < 0 else "+"
|
|
549
|
+
offset = f"e{sign}{abs(E):02d}"
|
|
550
|
+
return fmt, offset
|
|
551
|
+
|
|
552
|
+
# Fixed-point mode
|
|
553
|
+
# Fixed-point mode: choose minimal decimals so that all tick labels are unique
|
|
554
|
+
ticks = np.linspace(a, b, max(2, int(k))) if k and k > 1 else np.array([a, b])
|
|
555
|
+
dec = 6
|
|
556
|
+
for d in range(0, 7):
|
|
557
|
+
S = 10**d
|
|
558
|
+
ints = np.round(ticks * S).astype(np.int64)
|
|
559
|
+
if np.unique(ints).size == ints.size:
|
|
560
|
+
dec = d
|
|
561
|
+
break
|
|
562
|
+
|
|
563
|
+
def fmt(v, _pos=None):
|
|
564
|
+
val = np.round(v, dec)
|
|
565
|
+
s = f"{val:.{dec}f}"
|
|
566
|
+
if "." in s:
|
|
567
|
+
s = s.rstrip("0").rstrip(".")
|
|
568
|
+
return "0" if s == "-0" else s
|
|
569
|
+
|
|
570
|
+
return fmt, None
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _apply_axis_offset(ax, axis, offset, pos="top"):
|
|
574
|
+
"""
|
|
575
|
+
Apply an offset string to an axis in a version-safe way.
|
|
576
|
+
axis: "x" | "y"
|
|
577
|
+
pos: for x -> "top"/"bottom"; for y -> "left"/"right"
|
|
578
|
+
"""
|
|
579
|
+
# Remove any previous manual label if no offset requested
|
|
580
|
+
if not offset:
|
|
581
|
+
tag = "_bss_offset_text_x" if axis == "x" else "_bss_offset_text_y"
|
|
582
|
+
prev = getattr(ax, tag, None)
|
|
583
|
+
if prev is not None:
|
|
584
|
+
try:
|
|
585
|
+
prev.remove()
|
|
586
|
+
except Exception:
|
|
587
|
+
pass
|
|
588
|
+
setattr(ax, tag, None)
|
|
589
|
+
return
|
|
590
|
+
|
|
591
|
+
axis_obj = ax.xaxis if axis == "x" else ax.yaxis
|
|
592
|
+
# Try native API if available
|
|
593
|
+
if hasattr(axis_obj, "set_offset_position"):
|
|
594
|
+
try:
|
|
595
|
+
axis_obj.set_offset_position(pos)
|
|
596
|
+
axis_obj.offsetText.set_visible(True)
|
|
597
|
+
axis_obj.offsetText.set_text(offset)
|
|
598
|
+
return
|
|
599
|
+
except Exception:
|
|
600
|
+
pass
|
|
601
|
+
|
|
602
|
+
# Fallback: draw our own text
|
|
603
|
+
tag = "_bss_offset_text_x" if axis == "x" else "_bss_offset_text_y"
|
|
604
|
+
prev = getattr(ax, tag, None)
|
|
605
|
+
if prev is not None:
|
|
606
|
+
try:
|
|
607
|
+
prev.remove()
|
|
608
|
+
except Exception:
|
|
609
|
+
pass
|
|
610
|
+
|
|
611
|
+
if axis == "x":
|
|
612
|
+
# top or bottom placement in axes coords
|
|
613
|
+
y = 1.02 if pos == "top" else -0.08
|
|
614
|
+
txt = ax.text(1.0, y, offset, transform=ax.transAxes, ha="right", va="bottom")
|
|
615
|
+
else:
|
|
616
|
+
# y-axis: place at top tick, on the requested side of the axis
|
|
617
|
+
if pos == "right":
|
|
618
|
+
# slightly inside the axes so it's to the right of the y-axis line/ticks
|
|
619
|
+
txt = ax.text(
|
|
620
|
+
0.01, 1.0, offset, transform=ax.transAxes, ha="left", va="top"
|
|
621
|
+
)
|
|
622
|
+
else:
|
|
623
|
+
txt = ax.text(
|
|
624
|
+
-0.02, 1.0, offset, transform=ax.transAxes, ha="right", va="top"
|
|
625
|
+
)
|
|
626
|
+
setattr(ax, tag, txt)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def format_colorbar(ax, colorbar_decimals=None):
|
|
630
|
+
for spine in ax.spines.values():
|
|
631
|
+
spine.set_visible(False)
|
|
632
|
+
orientation = (
|
|
633
|
+
"vertical"
|
|
634
|
+
if ax.get_position().width < ax.get_position().height
|
|
635
|
+
else "horizontal"
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
# Try to get the actual data range from the colorbar's mappable
|
|
639
|
+
# This is more reliable than axis limits which may not be set yet
|
|
640
|
+
mappable = getattr(ax, 'mappable', None) if hasattr(ax, 'mappable') else None
|
|
641
|
+
if mappable is None:
|
|
642
|
+
# Try to find mappable from images or collections
|
|
643
|
+
for child in ax.get_children():
|
|
644
|
+
if hasattr(child, 'get_clim'):
|
|
645
|
+
mappable = child
|
|
646
|
+
break
|
|
647
|
+
|
|
648
|
+
def _safe_limits(val_tuple, fallback_tuple):
|
|
649
|
+
"""Safely convert limits to floats, with fallback."""
|
|
650
|
+
try:
|
|
651
|
+
vmin, vmax = val_tuple
|
|
652
|
+
vmin, vmax = float(vmin), float(vmax)
|
|
653
|
+
if np.isfinite(vmin) and np.isfinite(vmax) and vmin != vmax:
|
|
654
|
+
return vmin, vmax
|
|
655
|
+
except (TypeError, ValueError):
|
|
656
|
+
pass
|
|
657
|
+
try:
|
|
658
|
+
vmin, vmax = fallback_tuple
|
|
659
|
+
vmin, vmax = float(vmin), float(vmax)
|
|
660
|
+
if np.isfinite(vmin) and np.isfinite(vmax) and vmin != vmax:
|
|
661
|
+
return vmin, vmax
|
|
662
|
+
except (TypeError, ValueError):
|
|
663
|
+
pass
|
|
664
|
+
return None, None
|
|
665
|
+
|
|
666
|
+
if orientation == "vertical":
|
|
667
|
+
clim = mappable.get_clim() if mappable is not None and hasattr(mappable, 'get_clim') else (None, None)
|
|
668
|
+
vmin, vmax = _safe_limits(clim, ax.get_ylim())
|
|
669
|
+
|
|
670
|
+
# Skip formatting if limits are invalid
|
|
671
|
+
if vmin is None or vmax is None:
|
|
672
|
+
return
|
|
673
|
+
|
|
674
|
+
ticks = [vmin, 0 if vmin < 0 and vmax > 0 else (vmin + vmax) / 2, vmax]
|
|
675
|
+
ax.set_yticks(ticks)
|
|
676
|
+
if colorbar_decimals is not None:
|
|
677
|
+
# Increase decimals if all ticks round to zero (i.e., would be shown as 0,0,0)
|
|
678
|
+
dec = int(colorbar_decimals)
|
|
679
|
+
max_dec = 8
|
|
680
|
+
|
|
681
|
+
def all_round_to_zero(vals, d):
|
|
682
|
+
return all(round(float(v), d) == 0.0 for v in vals)
|
|
683
|
+
|
|
684
|
+
while dec < max_dec and all_round_to_zero(ticks, dec):
|
|
685
|
+
dec += 1
|
|
686
|
+
ax.yaxis.set_major_formatter(FormatStrFormatter(f"%.{dec}f"))
|
|
687
|
+
_apply_axis_offset(ax, "y", None)
|
|
688
|
+
else:
|
|
689
|
+
fmt, offset = make_smart_formatter(vmin, vmax, k=3)
|
|
690
|
+
ax.yaxis.set_major_formatter(FuncFormatter(fmt))
|
|
691
|
+
_apply_axis_offset(ax, "y", offset, pos="left")
|
|
692
|
+
else:
|
|
693
|
+
clim = mappable.get_clim() if mappable is not None and hasattr(mappable, 'get_clim') else (None, None)
|
|
694
|
+
vmin, vmax = _safe_limits(clim, ax.get_xlim())
|
|
695
|
+
|
|
696
|
+
# Skip formatting if limits are invalid
|
|
697
|
+
if vmin is None or vmax is None:
|
|
698
|
+
return
|
|
699
|
+
|
|
700
|
+
ticks = [vmin, 0 if vmin < 0 and vmax > 0 else (vmin + vmax) / 2, vmax]
|
|
701
|
+
ax.set_xticks(ticks)
|
|
702
|
+
if colorbar_decimals is not None:
|
|
703
|
+
# Increase decimals if all ticks round to zero (i.e., would be shown as 0,0,0)
|
|
704
|
+
dec = int(colorbar_decimals)
|
|
705
|
+
max_dec = 8
|
|
706
|
+
|
|
707
|
+
def all_round_to_zero(vals, d):
|
|
708
|
+
return all(round(float(v), d) == 0.0 for v in vals)
|
|
709
|
+
|
|
710
|
+
while dec < max_dec and all_round_to_zero(ticks, dec):
|
|
711
|
+
dec += 1
|
|
712
|
+
ax.xaxis.set_major_formatter(FormatStrFormatter(f"%.{dec}f"))
|
|
713
|
+
_apply_axis_offset(ax, "x", None)
|
|
714
|
+
else:
|
|
715
|
+
fmt, offset = make_smart_formatter(vmin, vmax, k=3)
|
|
716
|
+
ax.xaxis.set_major_formatter(FuncFormatter(fmt))
|
|
717
|
+
_apply_axis_offset(ax, "x", offset, pos="top")
|
|
718
|
+
|
|
719
|
+
# Fully disable clipping on tick marks so extreme ticks are visible
|
|
720
|
+
for tick in ax.yaxis.get_major_ticks() + ax.xaxis.get_major_ticks():
|
|
721
|
+
for line in [tick.tick1line, tick.tick2line]:
|
|
722
|
+
line.set_clip_on(False)
|
|
723
|
+
line.set_clip_box(None)
|
|
724
|
+
line.set_clip_path(None)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def arrowed_spines(ax=None, arrow_length=1, labels=("", ""), arrowprops=None):
|
|
728
|
+
xlabel, ylabel = labels
|
|
729
|
+
if ax is None:
|
|
730
|
+
ax = plt.gca()
|
|
731
|
+
if arrowprops is None:
|
|
732
|
+
arrowprops = dict(arrowstyle="<|-", facecolor="black", lw=1)
|
|
733
|
+
|
|
734
|
+
for i, spine in enumerate(["left", "bottom"]):
|
|
735
|
+
# Set up the annotation parameters
|
|
736
|
+
t = ax.spines[spine].get_transform()
|
|
737
|
+
xy, xycoords = [1, 0], ("axes fraction", t)
|
|
738
|
+
xytext, textcoords = [arrow_length, 0], ("offset points", t)
|
|
739
|
+
ha, va = "left", "bottom"
|
|
740
|
+
|
|
741
|
+
# If axis is reversed, draw the arrow the other way
|
|
742
|
+
top, bottom = ax.spines[spine].axis.get_view_interval()
|
|
743
|
+
if top > bottom:
|
|
744
|
+
xy[0] = 0
|
|
745
|
+
xytext[0] *= -1
|
|
746
|
+
ha, va = "right", "top"
|
|
747
|
+
|
|
748
|
+
if spine == "bottom":
|
|
749
|
+
xarrow = ax.annotate(
|
|
750
|
+
xlabel,
|
|
751
|
+
xy,
|
|
752
|
+
xycoords=xycoords,
|
|
753
|
+
xytext=xytext,
|
|
754
|
+
textcoords=textcoords,
|
|
755
|
+
ha=ha,
|
|
756
|
+
va="center",
|
|
757
|
+
arrowprops=arrowprops,
|
|
758
|
+
)
|
|
759
|
+
else:
|
|
760
|
+
yarrow = ax.annotate(
|
|
761
|
+
ylabel,
|
|
762
|
+
xy[::-1],
|
|
763
|
+
xycoords=xycoords[::-1],
|
|
764
|
+
xytext=xytext[::-1],
|
|
765
|
+
textcoords=textcoords[::-1],
|
|
766
|
+
ha="center",
|
|
767
|
+
va=va,
|
|
768
|
+
arrowprops=arrowprops,
|
|
769
|
+
)
|
|
770
|
+
return xarrow, yarrow
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def set_color_cycle_from_cmap(
|
|
774
|
+
cmap="viridis", n=8, start=0.0, stop=1.0, endpoint=True, reverse=False, to_hex=False
|
|
775
|
+
):
|
|
776
|
+
"""
|
|
777
|
+
Update Matplotlib rcParams to use a color cycle sampled from a continuous colormap.
|
|
778
|
+
|
|
779
|
+
Example:
|
|
780
|
+
set_color_cycle_from_cmap("viridis", n=6)
|
|
781
|
+
"""
|
|
782
|
+
cyc = make_cycler_from_cmap(
|
|
783
|
+
cmap=cmap,
|
|
784
|
+
n=n,
|
|
785
|
+
start=start,
|
|
786
|
+
stop=stop,
|
|
787
|
+
endpoint=endpoint,
|
|
788
|
+
reverse=reverse,
|
|
789
|
+
to_hex=to_hex,
|
|
790
|
+
)
|
|
791
|
+
plt.rcParams["axes.prop_cycle"] = cyc
|