py-pluto 1.1.4__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.
- pyPLUTO/__init__.py +22 -0
- pyPLUTO/amr.py +745 -0
- pyPLUTO/baseloadmixin.py +258 -0
- pyPLUTO/baseloadstate.py +45 -0
- pyPLUTO/codes/echo_load.py +161 -0
- pyPLUTO/configure.py +261 -0
- pyPLUTO/gui/config.py +174 -0
- pyPLUTO/gui/custom_var.py +435 -0
- pyPLUTO/gui/globals.py +108 -0
- pyPLUTO/gui/main.py +17 -0
- pyPLUTO/gui/main_window.py +177 -0
- pyPLUTO/gui/panels.py +66 -0
- pyPLUTO/gui/utils.py +273 -0
- pyPLUTO/h_pypluto.py +84 -0
- pyPLUTO/image.py +302 -0
- pyPLUTO/imagefuncs/colorbar.py +240 -0
- pyPLUTO/imagefuncs/contour.py +254 -0
- pyPLUTO/imagefuncs/create_axes.py +464 -0
- pyPLUTO/imagefuncs/display.py +306 -0
- pyPLUTO/imagefuncs/figure.py +395 -0
- pyPLUTO/imagefuncs/imagetools.py +487 -0
- pyPLUTO/imagefuncs/interactive.py +403 -0
- pyPLUTO/imagefuncs/legend.py +250 -0
- pyPLUTO/imagefuncs/plot.py +311 -0
- pyPLUTO/imagefuncs/range.py +242 -0
- pyPLUTO/imagefuncs/scatter.py +270 -0
- pyPLUTO/imagefuncs/set_axis.py +497 -0
- pyPLUTO/imagefuncs/streamplot.py +297 -0
- pyPLUTO/imagefuncs/zoom.py +428 -0
- pyPLUTO/imagemixin.py +259 -0
- pyPLUTO/imagestate.py +45 -0
- pyPLUTO/load.py +447 -0
- pyPLUTO/loadfuncs/baseloadtools.py +71 -0
- pyPLUTO/loadfuncs/codeselection.py +48 -0
- pyPLUTO/loadfuncs/defpluto.py +123 -0
- pyPLUTO/loadfuncs/descriptor.py +102 -0
- pyPLUTO/loadfuncs/findfiles.py +182 -0
- pyPLUTO/loadfuncs/findformat.py +245 -0
- pyPLUTO/loadfuncs/initload.py +203 -0
- pyPLUTO/loadfuncs/loadvars.py +227 -0
- pyPLUTO/loadfuncs/offsetdata.py +87 -0
- pyPLUTO/loadfuncs/offsetfluid.py +408 -0
- pyPLUTO/loadfuncs/read_files.py +213 -0
- pyPLUTO/loadfuncs/readdata.py +619 -0
- pyPLUTO/loadfuncs/readdata_old.py +567 -0
- pyPLUTO/loadfuncs/readdefplini.py +101 -0
- pyPLUTO/loadfuncs/readfluid.py +479 -0
- pyPLUTO/loadfuncs/readformat.py +277 -0
- pyPLUTO/loadfuncs/readgridalone.py +224 -0
- pyPLUTO/loadfuncs/readgridfile.py +255 -0
- pyPLUTO/loadfuncs/readgridout.py +451 -0
- pyPLUTO/loadfuncs/readpart.py +419 -0
- pyPLUTO/loadfuncs/readtab.py +105 -0
- pyPLUTO/loadfuncs/write_files.py +283 -0
- pyPLUTO/loadmixin.py +419 -0
- pyPLUTO/loadpart.py +233 -0
- pyPLUTO/loadstate.py +68 -0
- pyPLUTO/newload.py +81 -0
- pyPLUTO/pytools.py +145 -0
- pyPLUTO/toolfuncs/findlines.py +551 -0
- pyPLUTO/toolfuncs/fourier.py +149 -0
- pyPLUTO/toolfuncs/nabla.py +676 -0
- pyPLUTO/toolfuncs/parttools.py +152 -0
- pyPLUTO/toolfuncs/transform.py +638 -0
- pyPLUTO/utils/annotator.py +27 -0
- pyPLUTO/utils/inspector.py +145 -0
- pyPLUTO/utils/make_docstrings.py +3 -0
- py_pluto-1.1.4.dist-info/METADATA +218 -0
- py_pluto-1.1.4.dist-info/RECORD +73 -0
- py_pluto-1.1.4.dist-info/WHEEL +5 -0
- py_pluto-1.1.4.dist-info/entry_points.txt +2 -0
- py_pluto-1.1.4.dist-info/licenses/LICENSE +27 -0
- py_pluto-1.1.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""Interactive functions for image manipulation and display."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
import numpy as np
|
|
10
|
+
from matplotlib import animation
|
|
11
|
+
from matplotlib.artist import Artist
|
|
12
|
+
from matplotlib.axes import Axes
|
|
13
|
+
from matplotlib.collections import Collection, QuadMesh
|
|
14
|
+
from matplotlib.lines import Line2D
|
|
15
|
+
from matplotlib.widgets import Slider
|
|
16
|
+
from numpy.typing import NDArray
|
|
17
|
+
|
|
18
|
+
from pyPLUTO.imagefuncs.display import DisplayManager
|
|
19
|
+
from pyPLUTO.imagefuncs.imagetools import ImageToolsManager
|
|
20
|
+
from pyPLUTO.imagefuncs.plot import PlotManager
|
|
21
|
+
from pyPLUTO.imagemixin import ImageMixin
|
|
22
|
+
from pyPLUTO.imagestate import ImageState
|
|
23
|
+
from pyPLUTO.utils.inspector import track_kwargs
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InteractiveManager(ImageMixin):
|
|
27
|
+
"""InteractiveManager class.
|
|
28
|
+
|
|
29
|
+
It provides methods to create interactive plots with sliders to change the
|
|
30
|
+
data. It is designed to work with fluid variables and allows for dynamic
|
|
31
|
+
visualization of data as a function of time. The class uses the
|
|
32
|
+
DisplayManager and PlotManager to handle the display and plotting of the
|
|
33
|
+
data, respectively.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, state: ImageState) -> None:
|
|
37
|
+
"""Initialize the InteractiveManager with the given state."""
|
|
38
|
+
self.state = state
|
|
39
|
+
self.DisplayManager = DisplayManager(state)
|
|
40
|
+
self.ImageToolsManager = ImageToolsManager(state)
|
|
41
|
+
|
|
42
|
+
self.PlotManager = PlotManager(state)
|
|
43
|
+
self.anim_pcm: Collection | Line2D | None = None
|
|
44
|
+
self.labslider: list[str | float] | None = None
|
|
45
|
+
self.anim_ax: Axes | None = None
|
|
46
|
+
self.anim_var: dict[str, NDArray[Any]] | NDArray[Any]
|
|
47
|
+
self.animkeys: NDArray[Any] | None = None
|
|
48
|
+
self.nsld: int = 0
|
|
49
|
+
self.lenlab: int = 0
|
|
50
|
+
self.limfix: bool = True
|
|
51
|
+
self.slider: Slider | None = None
|
|
52
|
+
self.two_dim: int = 2
|
|
53
|
+
|
|
54
|
+
@track_kwargs
|
|
55
|
+
def interactive(
|
|
56
|
+
self,
|
|
57
|
+
varx: dict[str, NDArray[Any]] | NDArray[Any],
|
|
58
|
+
vary: dict[str, NDArray[Any]] | None = None,
|
|
59
|
+
check: bool = True,
|
|
60
|
+
limfix: bool = True,
|
|
61
|
+
labslider: list[str | float] | None = None,
|
|
62
|
+
**kwargs: Any,
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Create an interactive plot with a slider to change the data.
|
|
65
|
+
|
|
66
|
+
Warning: it works only with the fluid variables.
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
- None
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
- varx (not optional): array_like
|
|
75
|
+
The x-axis variable.
|
|
76
|
+
- vary: array_like, default None
|
|
77
|
+
The y-axis variable.
|
|
78
|
+
- ax: Axes, default None
|
|
79
|
+
The axes instance.
|
|
80
|
+
- labslider: str, default None
|
|
81
|
+
The label of the slider.
|
|
82
|
+
- limfix: bool, default True
|
|
83
|
+
If True, the colorbar limits are fixed through the entire animation.
|
|
84
|
+
- **kwargs: Any
|
|
85
|
+
Other parameters to pass used in the plot or display functions.
|
|
86
|
+
- vmin: float, default None
|
|
87
|
+
The minimum value of the data.
|
|
88
|
+
- vmax: float, default None
|
|
89
|
+
The maximum value of the data.
|
|
90
|
+
|
|
91
|
+
----
|
|
92
|
+
|
|
93
|
+
Examples
|
|
94
|
+
--------
|
|
95
|
+
- Example #1: Create an interactive 2D plot
|
|
96
|
+
|
|
97
|
+
>>> import pyPLUTO as pp
|
|
98
|
+
>>> D = pp.Load("all")
|
|
99
|
+
>>> I = pp.Image()
|
|
100
|
+
>>> I.interactive(
|
|
101
|
+
... D.rho, x1=D.x1, x2=D.x2, cpos="right", vmin=0, vmax=1.0
|
|
102
|
+
... )
|
|
103
|
+
>>> pp.show()
|
|
104
|
+
|
|
105
|
+
- Example #2: Create an interactive 1D plot with a composite variable
|
|
106
|
+
|
|
107
|
+
>>> import pyPLUTO as pp
|
|
108
|
+
>>> import numpy as np
|
|
109
|
+
>>> D = pp.Load("all")
|
|
110
|
+
>>> pp.Image().interactive(D.x1, np.sqrt(D.vx1**2 + D.vx2**2))
|
|
111
|
+
>>> pp.show()
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
kwargs.pop("check", check)
|
|
115
|
+
|
|
116
|
+
# Store the variable x. If vary is None, it is set to varx
|
|
117
|
+
if vary is None:
|
|
118
|
+
if isinstance(varx, dict):
|
|
119
|
+
self.anim_var = varx
|
|
120
|
+
scrh = np.asarray(list(varx.keys()))[0]
|
|
121
|
+
splt = np.ndim(varx[scrh])
|
|
122
|
+
else:
|
|
123
|
+
raise ValueError("varx must be a dictionary")
|
|
124
|
+
|
|
125
|
+
else:
|
|
126
|
+
self.anim_var = vary
|
|
127
|
+
|
|
128
|
+
# Store the variable to animate
|
|
129
|
+
self.animkeys = np.sort(np.asarray(list(self.anim_var.keys())))
|
|
130
|
+
self.nsld = len(self.animkeys)
|
|
131
|
+
nsld = self.nsld - 1
|
|
132
|
+
self.lenlab = len(str(self.animkeys[-1]))
|
|
133
|
+
|
|
134
|
+
# Check the number of dimensions
|
|
135
|
+
splt = np.ndim(self.anim_var[self.animkeys[0]])
|
|
136
|
+
|
|
137
|
+
# Set or create figure and axes (to test)
|
|
138
|
+
# Set or create figure and axes
|
|
139
|
+
ax, _ = self.ImageToolsManager.assign_ax(
|
|
140
|
+
kwargs.pop("ax", None), **kwargs, tight=False
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if self.fig is None:
|
|
144
|
+
raise ValueError(
|
|
145
|
+
"No figure is present. Please create a figure first."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
self.anim_ax = ax
|
|
149
|
+
|
|
150
|
+
# Position the slider
|
|
151
|
+
pos_slider = ax.get_position()
|
|
152
|
+
pos_x0 = pos_slider.x0 * (1.5 + 0.2 * (self.lenlab - 2))
|
|
153
|
+
pos_x1 = pos_slider.x1 * 0.95 - pos_x0
|
|
154
|
+
|
|
155
|
+
# Adjust the lower part of the position by increasing the 'y0' value
|
|
156
|
+
if "xtitle" in kwargs:
|
|
157
|
+
new_pos = (
|
|
158
|
+
pos_slider.x0,
|
|
159
|
+
pos_slider.y0 + 0.07,
|
|
160
|
+
pos_slider.width,
|
|
161
|
+
pos_slider.height - 0.07,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Apply the new position
|
|
165
|
+
ax.set_position(new_pos)
|
|
166
|
+
|
|
167
|
+
sliderax = self.fig.add_axes((pos_x0, 0.02, pos_x1, 0.04))
|
|
168
|
+
|
|
169
|
+
# Create the slider
|
|
170
|
+
if labslider is not None:
|
|
171
|
+
self.labslider = labslider
|
|
172
|
+
label = labslider[0]
|
|
173
|
+
else:
|
|
174
|
+
self.labslider = None
|
|
175
|
+
label = f"nout = {self.animkeys[0]:0{self.lenlab}d}"
|
|
176
|
+
self.slider = Slider(
|
|
177
|
+
sliderax,
|
|
178
|
+
label=str(label),
|
|
179
|
+
valmin=0,
|
|
180
|
+
valmax=nsld,
|
|
181
|
+
valinit=0,
|
|
182
|
+
valstep=1,
|
|
183
|
+
valfmt="%d",
|
|
184
|
+
)
|
|
185
|
+
self.slider.on_changed(self.update_slider)
|
|
186
|
+
|
|
187
|
+
# Display the data
|
|
188
|
+
if splt == self.two_dim:
|
|
189
|
+
self.limfix = limfix
|
|
190
|
+
vmin = (
|
|
191
|
+
min(np.nanmin(array) for array in self.anim_var.values())
|
|
192
|
+
if limfix is True
|
|
193
|
+
else np.nanmin(self.anim_var[self.animkeys[0]])
|
|
194
|
+
)
|
|
195
|
+
vmax = (
|
|
196
|
+
max(np.nanmax(array) for array in self.anim_var.values())
|
|
197
|
+
if limfix is True
|
|
198
|
+
else np.nanmax(self.anim_var[self.animkeys[0]])
|
|
199
|
+
)
|
|
200
|
+
vmin = kwargs.pop("vmin", vmin)
|
|
201
|
+
vmax = kwargs.pop("vmax", vmax)
|
|
202
|
+
|
|
203
|
+
# Display the data if it is 2D
|
|
204
|
+
self.DisplayManager.display(
|
|
205
|
+
self.anim_var[self.animkeys[0]],
|
|
206
|
+
ax=ax,
|
|
207
|
+
vmin=vmin,
|
|
208
|
+
vmax=vmax,
|
|
209
|
+
**kwargs,
|
|
210
|
+
)
|
|
211
|
+
self.anim_pcm = ax.collections[0]
|
|
212
|
+
else:
|
|
213
|
+
var = np.array(self.anim_var[self.animkeys[0]].tolist())
|
|
214
|
+
if isinstance(varx, dict):
|
|
215
|
+
varx = np.array(range(len(var)))
|
|
216
|
+
|
|
217
|
+
# Plot the data if it is 1D
|
|
218
|
+
self.PlotManager.plot(
|
|
219
|
+
varx,
|
|
220
|
+
var,
|
|
221
|
+
ax=ax,
|
|
222
|
+
**kwargs,
|
|
223
|
+
)
|
|
224
|
+
self.anim_pcm = ax.get_lines()[0]
|
|
225
|
+
|
|
226
|
+
def update_slider(self, i: float) -> Iterable[Artist]:
|
|
227
|
+
"""Update the data in the interactive plot.
|
|
228
|
+
|
|
229
|
+
Returns
|
|
230
|
+
-------
|
|
231
|
+
- None
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
- i (not optional): int
|
|
236
|
+
The slider index.
|
|
237
|
+
|
|
238
|
+
----
|
|
239
|
+
|
|
240
|
+
Examples
|
|
241
|
+
--------
|
|
242
|
+
- Example #1: Update the data in the interactive plot
|
|
243
|
+
|
|
244
|
+
>>> _update_slider(1)
|
|
245
|
+
|
|
246
|
+
"""
|
|
247
|
+
# Update the data
|
|
248
|
+
if self.animkeys is None or self.anim_var is None:
|
|
249
|
+
raise ValueError(
|
|
250
|
+
"No data is present. Please create an interactive plot first."
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if self.slider is None:
|
|
254
|
+
raise ValueError(
|
|
255
|
+
"No slider is present. Please create an interactive plot first."
|
|
256
|
+
)
|
|
257
|
+
idx = int(i)
|
|
258
|
+
var = self.anim_var[self.animkeys[idx]]
|
|
259
|
+
if np.ndim(var) == self.two_dim:
|
|
260
|
+
if not isinstance(self.anim_pcm, QuadMesh):
|
|
261
|
+
raise ValueError(
|
|
262
|
+
"The current plot is not a 2D plot. "
|
|
263
|
+
"Please use a 2D variable."
|
|
264
|
+
)
|
|
265
|
+
# Update the data array if it is 2D
|
|
266
|
+
self.anim_pcm.set_array(var.T.ravel())
|
|
267
|
+
|
|
268
|
+
# Update vmin and vmax dynamically
|
|
269
|
+
if self.limfix is False:
|
|
270
|
+
self.anim_pcm.set_clim(
|
|
271
|
+
self.anim_var[self.animkeys[idx]].min(),
|
|
272
|
+
self.anim_var[self.animkeys[idx]].max(),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
elif np.ndim(var) == 1:
|
|
276
|
+
if not isinstance(self.anim_pcm, Line2D):
|
|
277
|
+
raise ValueError(
|
|
278
|
+
"The current plot is not a 1D plot. "
|
|
279
|
+
"Please use a 1D variable."
|
|
280
|
+
)
|
|
281
|
+
# Update the data array if it is 1D
|
|
282
|
+
self.anim_pcm.set_ydata(var)
|
|
283
|
+
|
|
284
|
+
if isinstance(self.labslider, list):
|
|
285
|
+
self.slider.label.set_text(str(self.labslider[idx]))
|
|
286
|
+
else:
|
|
287
|
+
self.slider.label.set_text(
|
|
288
|
+
f"nout = {self.animkeys[idx]:0{self.lenlab}d}"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Update the plot
|
|
292
|
+
if self.fig is None:
|
|
293
|
+
raise ValueError(
|
|
294
|
+
"No figure is present. Please create a figure first."
|
|
295
|
+
)
|
|
296
|
+
self.fig.canvas.draw()
|
|
297
|
+
|
|
298
|
+
# End of the function
|
|
299
|
+
return ()
|
|
300
|
+
|
|
301
|
+
def update_both(self, i: float) -> Iterable[Artist]:
|
|
302
|
+
"""Update both the plot and the slider value during animation.
|
|
303
|
+
|
|
304
|
+
Returns
|
|
305
|
+
-------
|
|
306
|
+
- None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
Parameters
|
|
310
|
+
----------
|
|
311
|
+
- i (not optional): int
|
|
312
|
+
The current frame index.
|
|
313
|
+
|
|
314
|
+
----
|
|
315
|
+
|
|
316
|
+
Examples
|
|
317
|
+
--------
|
|
318
|
+
- Example #1: Update the data in the interactive plot
|
|
319
|
+
|
|
320
|
+
>>> _update_slider(1)
|
|
321
|
+
|
|
322
|
+
"""
|
|
323
|
+
if self.slider is None:
|
|
324
|
+
raise ValueError(
|
|
325
|
+
"No slider is present. Please create an interactive plot first."
|
|
326
|
+
)
|
|
327
|
+
# Update the plot with the current frame
|
|
328
|
+
self.update_slider(i)
|
|
329
|
+
|
|
330
|
+
# Update the slider's position visually
|
|
331
|
+
self.slider.set_val(i)
|
|
332
|
+
|
|
333
|
+
# End of the function
|
|
334
|
+
return ()
|
|
335
|
+
|
|
336
|
+
def animate(
|
|
337
|
+
self,
|
|
338
|
+
gifname: str | None = None,
|
|
339
|
+
frames: int | None = None,
|
|
340
|
+
interval: int = 500,
|
|
341
|
+
updateslider: bool = True,
|
|
342
|
+
script_relative: bool = False,
|
|
343
|
+
) -> None:
|
|
344
|
+
"""Display the animation interactively.
|
|
345
|
+
|
|
346
|
+
Returns
|
|
347
|
+
-------
|
|
348
|
+
- None
|
|
349
|
+
|
|
350
|
+
Parameters
|
|
351
|
+
----------
|
|
352
|
+
- frames: int, default None
|
|
353
|
+
The number of frames in the animation.
|
|
354
|
+
- gifname: str, default None
|
|
355
|
+
The name of the GIF file.
|
|
356
|
+
- interval: int, default 500
|
|
357
|
+
The interval between frames in milliseconds.
|
|
358
|
+
- updateslider: bool, default True
|
|
359
|
+
If True, the slider is shown and updated with each frame.
|
|
360
|
+
|
|
361
|
+
Examples
|
|
362
|
+
--------
|
|
363
|
+
- Example #1: Display the animation
|
|
364
|
+
|
|
365
|
+
>>> animate()
|
|
366
|
+
|
|
367
|
+
- Example #2: Display the animation with a specific number of frames
|
|
368
|
+
|
|
369
|
+
>>> animate(frames=[0, 1, 2], interval=300)
|
|
370
|
+
|
|
371
|
+
"""
|
|
372
|
+
# Choose the frames
|
|
373
|
+
frames = self.nsld if frames is None else frames
|
|
374
|
+
|
|
375
|
+
update = self.update_both if updateslider else self.update_slider
|
|
376
|
+
|
|
377
|
+
if self.fig is None:
|
|
378
|
+
raise ValueError(
|
|
379
|
+
"No figure is present. Please create a figure first."
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# Create the animation
|
|
383
|
+
ani = animation.FuncAnimation(
|
|
384
|
+
self.fig, update, frames=frames, interval=interval
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
if gifname is not None:
|
|
388
|
+
out_path = Path(gifname)
|
|
389
|
+
|
|
390
|
+
if script_relative and not out_path.is_absolute():
|
|
391
|
+
# Find the path of the script calling this method
|
|
392
|
+
caller_file = Path(inspect.stack()[1].filename).resolve()
|
|
393
|
+
base_dir = caller_file.parent
|
|
394
|
+
out_path = base_dir / out_path
|
|
395
|
+
|
|
396
|
+
# Save as GIF
|
|
397
|
+
ani.save(out_path)
|
|
398
|
+
|
|
399
|
+
plt.close(self.fig)
|
|
400
|
+
|
|
401
|
+
else:
|
|
402
|
+
# Display the animation
|
|
403
|
+
plt.show()
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""LegendManager class."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import matplotlib.lines as mlines
|
|
6
|
+
from matplotlib.axes import Axes
|
|
7
|
+
|
|
8
|
+
from pyPLUTO.imagefuncs.imagetools import ImageToolsManager
|
|
9
|
+
from pyPLUTO.imagemixin import ImageMixin
|
|
10
|
+
from pyPLUTO.imagestate import ImageState
|
|
11
|
+
from pyPLUTO.utils.inspector import track_kwargs
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LegendManager(ImageMixin):
|
|
15
|
+
"""LegendManager class.
|
|
16
|
+
|
|
17
|
+
It provides methods to create and manage legends
|
|
18
|
+
in the plots. It is designed to work with the Image class and allows for
|
|
19
|
+
dynamic creation of legends based on the current state of the image.
|
|
20
|
+
The class uses the ImageToolsManager to handle the display and plotting
|
|
21
|
+
of the legends, and it provides methods to customize the appearance of
|
|
22
|
+
the legends.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
exposed_methods = ("legend",)
|
|
26
|
+
|
|
27
|
+
def __init__(self, state: ImageState) -> None:
|
|
28
|
+
"""Initialize the LegendManager with the given state."""
|
|
29
|
+
self.state = state
|
|
30
|
+
self.ImageToolsManager = ImageToolsManager(state)
|
|
31
|
+
|
|
32
|
+
@track_kwargs
|
|
33
|
+
def legend(
|
|
34
|
+
self,
|
|
35
|
+
ax: Axes | int | None = None,
|
|
36
|
+
check: bool = True,
|
|
37
|
+
fromplot: bool = False,
|
|
38
|
+
**kwargs: Any,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Creation of a legend referring to the current figure.
|
|
41
|
+
|
|
42
|
+
If no labels are given, it shows the labels of all the plots in the
|
|
43
|
+
figure, ordered by entry. If specific labels are given, it shows those.
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
- None
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
- ax: ax | int | None, default None
|
|
52
|
+
The axis where to insert the legend. If None, the last considered
|
|
53
|
+
axis will be used.
|
|
54
|
+
- c: str, default self.color
|
|
55
|
+
Determines the line color. If not defined, the program will loop
|
|
56
|
+
over an array of 6 color which are different for the most common
|
|
57
|
+
vision deficiencies.
|
|
58
|
+
- edgecolor: list[str], default [None]
|
|
59
|
+
Sets the edge color of the legend. The default value is black ('k').
|
|
60
|
+
- fillstyle: {'full', 'left', 'right', 'bottom', 'top', 'none'},
|
|
61
|
+
default 'full'
|
|
62
|
+
Sets the marker filling. The default value is the fully filled
|
|
63
|
+
marker ('full').
|
|
64
|
+
- label: [str], default None
|
|
65
|
+
Associates a label to each line. If not specified, the program will
|
|
66
|
+
take the label which are already associated with the plot.
|
|
67
|
+
- legalpha: float, default 0.8
|
|
68
|
+
Sets the opacity of the legend.
|
|
69
|
+
- legcols: int, default 1
|
|
70
|
+
Sets the number of columns that the legend should have.
|
|
71
|
+
- legpad: float, default 0.8
|
|
72
|
+
Sets the space between the lines (or symbols) and the correspondibg
|
|
73
|
+
text in the legend.
|
|
74
|
+
- legpos: int | str, default 0
|
|
75
|
+
Selects the legend location. If not specified the standard
|
|
76
|
+
matplotlib legend function will find the most suitable location.
|
|
77
|
+
- legsize: float, default fontsize
|
|
78
|
+
Sets the fontsize of the legend. The default value is the default
|
|
79
|
+
fontsize value.
|
|
80
|
+
- legspace: float, default 2
|
|
81
|
+
Sets the space between the legend columns, in font-size units.
|
|
82
|
+
- ls: {'-', '--', '-.', ':', ' ', ect.}, default '-'
|
|
83
|
+
Sets the linestyle. The choices available are the ones defined in
|
|
84
|
+
the matplotlib package. Here are reported the most common ones.
|
|
85
|
+
- lw: float, default 1.3
|
|
86
|
+
Sets the linewidth of each line.
|
|
87
|
+
- marker: {'o', 'v', '^', '<', '>', 'X', ' ', etc.}, default ' '
|
|
88
|
+
Sets an optional symbol for every point. The default value is no
|
|
89
|
+
marker (' ').
|
|
90
|
+
- ms: float, default 5 (if label) or 1 (if not label)
|
|
91
|
+
Sets the marker size from the default value of 5.0 (if label is
|
|
92
|
+
given) or the marker scale from the default value of 1.0 (if not
|
|
93
|
+
label).
|
|
94
|
+
- mscale: float, default 1.0
|
|
95
|
+
Sets the marker scale. The default value is 1.0.
|
|
96
|
+
|
|
97
|
+
----
|
|
98
|
+
|
|
99
|
+
Examples
|
|
100
|
+
--------
|
|
101
|
+
- Example #1: create a standard legend
|
|
102
|
+
|
|
103
|
+
>>> import pyPLUTO as pp
|
|
104
|
+
>>> I = pp.Image()
|
|
105
|
+
>>> ax = I.create_axes()
|
|
106
|
+
>>> I.plot(x, y, ax=ax, label="label")
|
|
107
|
+
>>> I.legend(ax)
|
|
108
|
+
|
|
109
|
+
- Example #2: create a legend with custom labels
|
|
110
|
+
|
|
111
|
+
>>> import pyPLUTO as pp
|
|
112
|
+
>>> I = pp.Image()
|
|
113
|
+
>>> I.plot(x, y)
|
|
114
|
+
>>> I.legend(label="y")
|
|
115
|
+
|
|
116
|
+
- Example #3: create a double legend for four lines in a single plot
|
|
117
|
+
|
|
118
|
+
>>> import pyPLUTO as pp
|
|
119
|
+
>>> I = pp.Image()
|
|
120
|
+
>>> I.plot(x, y1, ls="-", c="k")
|
|
121
|
+
>>> I.plot(x, y2, ls="-.", c="r")
|
|
122
|
+
>>> I.plot(x, y3, ls="-", c="r")
|
|
123
|
+
>>> I.plot(x, y4, ls="-.", c="k")
|
|
124
|
+
>>> I.legend(
|
|
125
|
+
... legpos="lower left",
|
|
126
|
+
... ls=["-", "-"],
|
|
127
|
+
... c=["k", "r"],
|
|
128
|
+
... label=["black lines", "red lines"],
|
|
129
|
+
... )
|
|
130
|
+
>>> I.legend(
|
|
131
|
+
... legpos="lower right",
|
|
132
|
+
... ls=["-", "-."],
|
|
133
|
+
... c=["k", "k"],
|
|
134
|
+
... label=["continue", "dotted"],
|
|
135
|
+
... )
|
|
136
|
+
>>> pp.show()
|
|
137
|
+
|
|
138
|
+
"""
|
|
139
|
+
kwargs.pop("check", check)
|
|
140
|
+
|
|
141
|
+
# Find figure and number of the axis
|
|
142
|
+
ax, nax = self.ImageToolsManager.assign_ax(ax, **kwargs)
|
|
143
|
+
self.ImageToolsManager.hide_text(nax, ax.texts)
|
|
144
|
+
|
|
145
|
+
# Finds the legend parameters (position, columns, size, spacing and pad)
|
|
146
|
+
self.legpos[nax] = kwargs.get("legpos", self.legpos[nax])
|
|
147
|
+
self.legpar[nax][0] = kwargs.get("legsize", self.legpar[nax][0])
|
|
148
|
+
self.legpar[nax][1] = kwargs.get("legcols", self.legpar[nax][1])
|
|
149
|
+
self.legpar[nax][2] = kwargs.get("legspace", self.legpar[nax][2])
|
|
150
|
+
self.legpar[nax][3] = kwargs.get("legpad", self.legpar[nax][3])
|
|
151
|
+
self.legpar[nax][4] = kwargs.get("legalpha", self.legpar[nax][4])
|
|
152
|
+
|
|
153
|
+
# Check if another unwanted legend is present and cancel it
|
|
154
|
+
# (only when the legend is called from the plot function)
|
|
155
|
+
if fromplot is True:
|
|
156
|
+
lleg = ax.get_legend()
|
|
157
|
+
if lleg is not None:
|
|
158
|
+
lleg.remove()
|
|
159
|
+
|
|
160
|
+
# Check is custom labels are on and plot the legend
|
|
161
|
+
if kwargs.get("label") is not None:
|
|
162
|
+
lab = (
|
|
163
|
+
kwargs["label"]
|
|
164
|
+
if isinstance(kwargs["label"], list)
|
|
165
|
+
else [kwargs["label"]]
|
|
166
|
+
)
|
|
167
|
+
col = makelist(kwargs.get("c", ["k"]))
|
|
168
|
+
ls = makelist(kwargs.get("ls", ["-"]))
|
|
169
|
+
lw = makelist(kwargs.get("lw", [1.5]))
|
|
170
|
+
mrk = makelist(kwargs.get("marker", [""]))
|
|
171
|
+
ms = makelist(kwargs.get("ms", [5.0]))
|
|
172
|
+
fls = makelist(kwargs.get("fillstyle", ["full"]))
|
|
173
|
+
edgcol = makelist(kwargs.get("edgecolor", [None]))
|
|
174
|
+
lines = []
|
|
175
|
+
# Create the list of lines
|
|
176
|
+
for i, val in enumerate(lab):
|
|
177
|
+
lines.append(
|
|
178
|
+
mlines.Line2D(
|
|
179
|
+
[],
|
|
180
|
+
[],
|
|
181
|
+
label=val,
|
|
182
|
+
color=col[i % len(col)],
|
|
183
|
+
ls=ls[i % len(ls)],
|
|
184
|
+
lw=lw[i % len(lw)],
|
|
185
|
+
marker=mrk[i % len(mrk)],
|
|
186
|
+
ms=ms[i % len(ms)],
|
|
187
|
+
fillstyle=fls[i % len(fls)],
|
|
188
|
+
markeredgecolor=edgcol[i % len(edgcol)],
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
# Create the legend
|
|
192
|
+
legg = ax.legend(
|
|
193
|
+
handles=lines,
|
|
194
|
+
loc=self.legpos[nax],
|
|
195
|
+
fontsize=self.legpar[nax][0],
|
|
196
|
+
ncol=self.legpar[nax][1],
|
|
197
|
+
columnspacing=self.legpar[nax][2],
|
|
198
|
+
handletextpad=self.legpar[nax][3],
|
|
199
|
+
framealpha=self.legpar[nax][4],
|
|
200
|
+
)
|
|
201
|
+
else:
|
|
202
|
+
# Set the markerscale
|
|
203
|
+
mscale = kwargs.get("mscale", 1.0)
|
|
204
|
+
# Create the legend
|
|
205
|
+
legg = ax.legend(
|
|
206
|
+
loc=self.legpos[nax],
|
|
207
|
+
fontsize=self.legpar[nax][0],
|
|
208
|
+
ncol=self.legpar[nax][1],
|
|
209
|
+
columnspacing=self.legpar[nax][2],
|
|
210
|
+
handletextpad=self.legpar[nax][3],
|
|
211
|
+
framealpha=self.legpar[nax][4],
|
|
212
|
+
markerscale=mscale,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Add the legend to the axis
|
|
216
|
+
ax.add_artist(legg)
|
|
217
|
+
|
|
218
|
+
# End of the function
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def makelist[T](el: T | list[T]) -> list[T]:
|
|
222
|
+
"""If the element is not a list, it converts it into a list.
|
|
223
|
+
|
|
224
|
+
Returns
|
|
225
|
+
-------
|
|
226
|
+
- list[Any]
|
|
227
|
+
The list of chosen elements.
|
|
228
|
+
|
|
229
|
+
Parameters
|
|
230
|
+
----------
|
|
231
|
+
- el (not optional): Any
|
|
232
|
+
The element to be converted into a list.
|
|
233
|
+
|
|
234
|
+
----
|
|
235
|
+
|
|
236
|
+
Examples
|
|
237
|
+
--------
|
|
238
|
+
- Example #1: element is a list
|
|
239
|
+
|
|
240
|
+
>>> makelist([1, 2, 3])
|
|
241
|
+
[1,2,3]
|
|
242
|
+
|
|
243
|
+
- Example #2: element is not a list
|
|
244
|
+
|
|
245
|
+
>>> makelist(1)
|
|
246
|
+
[1]
|
|
247
|
+
|
|
248
|
+
"""
|
|
249
|
+
# Return the element as a list
|
|
250
|
+
return el if isinstance(el, list) else [el]
|