wolfhece 2.1.98__py3-none-any.whl → 2.1.99__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.
@@ -0,0 +1,1980 @@
1
+ from matplotlib.backends.backend_wx import NavigationToolbar2Wx as NavigationToolbar
2
+ from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
3
+ from typing import Literal
4
+ from matplotlib.figure import Figure
5
+ import matplotlib.pyplot as plt
6
+ from matplotlib.gridspec import GridSpec
7
+ import numpy as np
8
+ from matplotlib.axes import Axes
9
+ from wolfhece.CpGrid import CpGrid
10
+ from wolfhece.PyParams import Wolf_Param, new_json
11
+ from wolfhece.PyTranslate import _
12
+ from wolfhece.PyVertex import getRGBfromI
13
+
14
+
15
+ import wx
16
+ from matplotlib.backend_bases import KeyEvent, MouseEvent
17
+ from matplotlib.lines import Line2D
18
+
19
+
20
+ import logging
21
+ import json
22
+ from pathlib import Path
23
+ from enum import Enum
24
+
25
+
26
+ def sanitize_fmt(fmt):
27
+ """
28
+ Sanitizes the given format string for numerical formatting.
29
+ This function ensures that the format string is in a valid format
30
+ for floating-point number representation. If the input format string
31
+ is 'None' or an empty string, it defaults to '.2f'. Otherwise, it
32
+ ensures that the format string contains a decimal point and ends with
33
+ 'f' for floating-point representation. If the format string is '.f',
34
+ it defaults to '.2f'.
35
+
36
+ :param fmt: The format string to be sanitized.
37
+ :type fmt: str
38
+ :return: A sanitized format string suitable for floating-point number formatting.
39
+ :rtype: str
40
+ """
41
+ if fmt in ['None', '']:
42
+ return '.2f'
43
+ else:
44
+ if not '.' in fmt:
45
+ fmt = '.' + fmt
46
+
47
+ if not 'f' in fmt:
48
+ fmt = fmt + 'f'
49
+
50
+ if fmt == '.f':
51
+ fmt = '.2f'
52
+
53
+ return fmt
54
+
55
+
56
+ class Matplotlib_ax_properties():
57
+
58
+ def __init__(self, ax =None) -> None:
59
+
60
+ self._ax = ax
61
+ self._myprops = None
62
+ self._lines:list[Matplolib_line_properties] = []
63
+
64
+ self._tmp_line_prop:Matplolib_line_properties = None
65
+ self._selected_line = -1
66
+
67
+ self.title = 'Figure'
68
+ self.xtitle = 'X [m]'
69
+ self.ytitle = 'Y [m]'
70
+ self.legend = False
71
+ self.xmin = 0.
72
+ self.xmax = 1.
73
+ self.ymin = 0.
74
+ self.ymax = 1.
75
+ self.gridx_major = False
76
+ self.gridy_major = False
77
+ self.gridx_minor = False
78
+ self.gridy_minor = False
79
+
80
+ self._equal_axis = 0
81
+ self.scaling_factor = 1.
82
+
83
+ self.ticks_x = 1.
84
+ self.ticks_y = 1.
85
+ self.ticks_label_x = 1.
86
+ self.ticks_label_y = 1.
87
+
88
+ self.format_x = '.2f'
89
+ self.format_y = '.2f'
90
+
91
+ self._set_props()
92
+
93
+ @property
94
+ def is_equal(self):
95
+
96
+ if self._equal_axis == 1:
97
+ return 'equal'
98
+ elif self._equal_axis == 0:
99
+ return 'auto'
100
+ else:
101
+ return self.scaling_factor
102
+
103
+ def reset_selection(self):
104
+ if self._selected_line>=0:
105
+ for curline in self._lines:
106
+ curline.selected = False
107
+ self._selected_line = -1
108
+
109
+ def select_line(self, idx:int):
110
+
111
+ if self._selected_line>=0:
112
+ self.reset_selection()
113
+
114
+ if idx>=0 and idx<len(self._lines):
115
+ self._selected_line = idx
116
+ self._lines[idx].selected = True
117
+
118
+ def set_ax(self, ax:Axes):
119
+ self._ax = ax
120
+
121
+ if ax is None:
122
+ return
123
+
124
+ self.get_properties()
125
+
126
+ self._lines = [Matplolib_line_properties(line, self) for line in ax.get_lines()]
127
+
128
+ return self
129
+
130
+ def _set_props(self):
131
+ """ Set the properties UI """
132
+
133
+ if self._myprops is not None:
134
+ return
135
+
136
+ self._myprops = Wolf_Param(title='Figure properties',
137
+ w= 500, h= 400,
138
+ to_read= False,
139
+ ontop= False,
140
+ init_GUI= False)
141
+
142
+ self._myprops.set_callbacks(None, self.destroyprop)
143
+
144
+ # self._myprops.hide_selected_buttons() # only 'Apply' button
145
+
146
+ self._myprops.addparam('Draw','Title',self.title,'String','Title')
147
+ self._myprops.addparam('Draw','X title',self.xtitle,'String','X title')
148
+ self._myprops.addparam('Draw','Y title',self.ytitle,'String','Y title')
149
+ self._myprops.addparam('Draw','Legend',self.legend,'Logical','Legend')
150
+
151
+ self._myprops.addparam('Bounds','X min',self.xmin,'Float','X min')
152
+ self._myprops.addparam('Bounds','X max',self.xmax,'Float','X max')
153
+ self._myprops.addparam('Bounds','Y min',self.ymin,'Float','Y min')
154
+ self._myprops.addparam('Bounds','Y max',self.ymax,'Float','Y max')
155
+
156
+ self._myprops.addparam('Ticks X','Positions',self.ticks_x,'String','X ticks')
157
+ self._myprops.addparam('Ticks X','Labels',self.ticks_label_x,'String','X ticks labels')
158
+
159
+ self._myprops.addparam('Ticks Y','Positions',self.ticks_y,'String','Y ticks')
160
+ self._myprops.addparam('Ticks Y','Labels',self.ticks_label_y,'String','Y ticks labels')
161
+
162
+ self._myprops.addparam('Formats','Ticks X',self.format_x,'String','X format')
163
+ self._myprops.addparam('Formats','Ticks Y',self.format_y,'String','Y format')
164
+ self._myprops.addparam('Formats','Shape',self._equal_axis,'Integer','Shape', jsonstr= new_json({'auto':0, 'equal':1, 'specific':2}))
165
+ self._myprops.addparam('Formats','Scaling factor',self.scaling_factor,'Float','Scaling factor')
166
+
167
+ self._myprops.add_param('Grid','Major X', self.gridx_major, 'Logical', 'Major grid X')
168
+ self._myprops.add_param('Grid','Major Y', self.gridy_major, 'Logical', 'Major grid Y')
169
+
170
+ self._myprops.Populate()
171
+
172
+ def populate(self):
173
+ """ Populate the properties UI """
174
+
175
+ if self._myprops is None:
176
+ self._set_props()
177
+
178
+ self._myprops[('Draw','Title')] = self.title
179
+ self._myprops[('Draw','X title')] = self.xtitle
180
+ self._myprops[('Draw','Y title')] = self.ytitle
181
+ self._myprops[('Draw','Legend')] = self.legend
182
+
183
+ self._myprops[('Bounds','X min')] = self.xmin
184
+ self._myprops[('Bounds','X max')] = self.xmax
185
+ self._myprops[('Bounds','Y min')] = self.ymin
186
+ self._myprops[('Bounds','Y max')] = self.ymax
187
+
188
+ self._myprops[('Grid','Major X')] = self.gridx_major
189
+ self._myprops[('Grid','Major Y')] = self.gridy_major
190
+
191
+ self._myprops[('Ticks X','Positions')] = self.ticks_x
192
+ self._myprops[('Ticks X','Labels')] = self.ticks_label_x
193
+
194
+ self._myprops[('Ticks Y','Positions')] = self.ticks_y
195
+ self._myprops[('Ticks Y','Labels')] = self.ticks_label_y
196
+
197
+ self._myprops[('Formats','Ticks X')] = self.format_x
198
+ self._myprops[('Formats','Ticks Y')] = self.format_y
199
+
200
+ self._myprops[('Formats','Shape')] = self._equal_axis
201
+ self._myprops[('Formats','Scaling factor')] = self.scaling_factor
202
+
203
+ self._myprops.Populate()
204
+
205
+ def ui(self):
206
+
207
+ if self._myprops is not None:
208
+ self._myprops.CenterOnScreen()
209
+ self._myprops.Raise()
210
+ self._myprops.Show()
211
+ return
212
+
213
+ self._set_props()
214
+
215
+ self._myprops.Show()
216
+
217
+ self._myprops.SetTitle(_('Ax properties'))
218
+
219
+ icon = wx.Icon()
220
+ icon_path = Path(__file__).parent / "apps/wolf_logo2.bmp"
221
+ icon.CopyFromBitmap(wx.Bitmap(str(icon_path), wx.BITMAP_TYPE_ANY))
222
+ self._myprops.SetIcon(icon)
223
+
224
+ self._myprops.Center()
225
+ self._myprops.Raise()
226
+
227
+ def destroyprop(self):
228
+ self._myprops=None
229
+
230
+ def bounds_lines(self):
231
+
232
+ if self._ax is None:
233
+ logging.warning('No axes found')
234
+ return
235
+
236
+ lines = self._ax.get_lines()
237
+
238
+ if len(lines) == 0:
239
+ logging.warning('No lines found')
240
+ return
241
+
242
+ xmin = np.inf
243
+ xmax = -np.inf
244
+ ymin = np.inf
245
+ ymax = -np.inf
246
+
247
+ for line in lines:
248
+ x = line.get_xdata()
249
+ y = line.get_ydata()
250
+
251
+ xmin = min(xmin, np.min(x))
252
+ xmax = max(xmax, np.max(x))
253
+ ymin = min(ymin, np.min(y))
254
+ ymax = max(ymax, np.max(y))
255
+
256
+ return xmin, xmax, ymin, ymax
257
+
258
+ def fill_property(self, verbosity= True):
259
+
260
+ if self._myprops is None:
261
+ logging.warning('Properties UI not found')
262
+ return
263
+
264
+ self._myprops.apply_changes_to_memory(verbosity= verbosity)
265
+
266
+ self.title = self._myprops[('Draw','Title')]
267
+ self.xtitle = self._myprops[('Draw','X title')]
268
+ self.ytitle = self._myprops[('Draw','Y title')]
269
+ self.legend = self._myprops[('Draw','Legend')]
270
+
271
+ self.xmin = self._myprops[('Bounds','X min')]
272
+ self.xmax = self._myprops[('Bounds','X max')]
273
+ self.ymin = self._myprops[('Bounds','Y min')]
274
+ self.ymax = self._myprops[('Bounds','Y max')]
275
+
276
+ self.gridx_major = self._myprops[('Grid','Major X')]
277
+ self.gridy_major = self._myprops[('Grid','Major Y')]
278
+
279
+ xmin, xmax, ymin, ymax = self.bounds_lines()
280
+
281
+ if self.xmin == -99999.:
282
+ self.xmin = xmin
283
+ if self.xmax == -99999.:
284
+ self.xmax = xmax
285
+ if self.ymin == -99999.:
286
+ self.ymin = ymin
287
+ if self.ymax == -99999.:
288
+ self.ymax = ymax
289
+
290
+ self.format_x = sanitize_fmt(self._myprops[('Formats','Ticks X')])
291
+ self.format_y = sanitize_fmt(self._myprops[('Formats','Ticks Y')])
292
+
293
+ def format_value(value, fmt):
294
+ return '{value:{fmt}}'.format(value=value, fmt=fmt)
295
+
296
+ ticks_x = self._myprops[('Ticks X','Positions')]
297
+ if '[' in ticks_x:
298
+ self.ticks_x = [float(cur.replace("'",'').replace(',','')) for cur in self._myprops[('Ticks X','Positions')].replace('[','').replace(']','').split()]
299
+ else:
300
+ try:
301
+ self.ticks_x = float(ticks_x)
302
+ self.ticks_x = np.linspace(self.xmin, self.xmax, int(np.ceil((self.xmax-self.xmin)/self.ticks_x)+1), endpoint=True).tolist()
303
+ except:
304
+ self.ticks_x = np.linspace(self.xmin, self.xmax, 5).tolist()
305
+
306
+ ticks_label_x = self._myprops[('Ticks X','Labels')]
307
+ if '[' in ticks_label_x:
308
+ self.ticks_label_x = [cur.replace("'",'').replace(',','') for cur in self._myprops[('Ticks X','Labels')].replace('[','').replace(']','').split()]
309
+ if len(self.ticks_label_x) != len(self.ticks_x):
310
+ self.ticks_label_x = [format_value(cur, self.format_x) for cur in self.ticks_x]
311
+ else:
312
+ self.ticks_label_x = [format_value(cur, self.format_x) for cur in self.ticks_x]
313
+
314
+ ticks_y = self._myprops[('Ticks Y','Positions')]
315
+ if '[' in ticks_y:
316
+ self.ticks_y = [float(cur.replace("'",'').replace(',','')) for cur in self._myprops[('Ticks Y','Positions')].replace('[','').replace(']','').split()]
317
+ else:
318
+ try:
319
+ self.ticks_y = float(ticks_y)
320
+ self.ticks_y = np.linspace(self.ymin, self.ymax, int(np.ceil((self.ymax-self.ymin)/self.ticks_y)+1), endpoint= True).tolist()
321
+ except:
322
+ self.ticks_y = np.linspace(self.ymin, self.ymax, 5).tolist()
323
+
324
+ ticks_label_y = self._myprops[('Ticks Y','Labels')]
325
+ if '[' in ticks_label_y:
326
+ self.ticks_label_y = [cur.replace("'",'').replace(',','') for cur in self._myprops[('Ticks Y','Labels')].replace('[','').replace(']','').split()]
327
+ if len(self.ticks_label_y) != len(self.ticks_y):
328
+ self.ticks_label_y = [format_value(cur, self.format_y) for cur in self.ticks_y]
329
+ else:
330
+ self.ticks_label_y = [format_value(cur, self.format_y) for cur in self.ticks_y]
331
+
332
+ self._equal_axis = self._myprops[('Formats','Shape')]
333
+ self.scaling_factor = self._myprops[('Formats','Scaling factor')]
334
+
335
+ self.set_properties()
336
+
337
+ def set_properties(self, ax:Axes = None):
338
+
339
+ if ax is None:
340
+ ax = self._ax
341
+
342
+ ax.set_title(self.title)
343
+ ax.set_xlabel(self.xtitle)
344
+ ax.set_ylabel(self.ytitle)
345
+
346
+ ax.xaxis.grid(self.gridx_major)
347
+ ax.yaxis.grid(self.gridy_major)
348
+
349
+ ax.set_xticks(self.ticks_x, self.ticks_label_x)
350
+ ax.set_yticks(self.ticks_y, self.ticks_label_y)
351
+
352
+ if self.legend:
353
+ update = any(line.update_legend for line in self._lines)
354
+ if update:
355
+ ax.legend().set_visible(False)
356
+ for line in self._lines:
357
+ line.update_legend = True
358
+ ax.legend().set_visible(True)
359
+ else:
360
+ ax.legend().set_visible(False)
361
+
362
+ ax.set_aspect(self.is_equal)
363
+
364
+ ax.set_xlim(self.xmin, self.xmax)
365
+ ax.set_ylim(self.ymin, self.ymax)
366
+
367
+ ax.figure.canvas.draw()
368
+
369
+ self.get_properties()
370
+
371
+ def get_properties(self, ax:Axes = None):
372
+
373
+ if ax is None:
374
+ ax = self._ax
375
+
376
+ self.title = ax.get_title()
377
+ self.xtitle = ax.get_xlabel()
378
+ self.ytitle = ax.get_ylabel()
379
+ self.legend = ax.legend().get_visible()
380
+ self.xmin, self.xmax = ax.get_xlim()
381
+ self.ymin, self.ymax = ax.get_ylim()
382
+
383
+ self.gridx_major = any(line.get_visible() for line in ax.get_xgridlines())
384
+ self.gridy_major = any(line.get_visible() for line in ax.get_ygridlines())
385
+
386
+ self.ticks_x = [str(cur) for cur in ax.get_xticks()]
387
+ self.ticks_y = [str(cur) for cur in ax.get_yticks()]
388
+
389
+ self.ticks_label_x = [label.get_text().replace("'",'').replace(',','') for label in ax.get_xticklabels()]
390
+ self.ticks_label_y = [label.get_text().replace("'",'').replace(',','') for label in ax.get_yticklabels()]
391
+
392
+ if ax.get_aspect() == 'auto':
393
+ self._equal_axis = 0
394
+ self.scaling_factor = 1.
395
+ elif ax.get_aspect() == 1.:
396
+ self._equal_axis = 1
397
+ self.scaling_factor = 1.
398
+ else:
399
+ self._equal_axis = 2
400
+ self.scaling_factor = ax.get_aspect()
401
+ logging.warning('Aspect ratio not found, set to auto')
402
+
403
+ self.populate()
404
+
405
+ def to_dict(self) -> str:
406
+ """ properties to dict """
407
+
408
+ props= {'title':self.title,
409
+ 'xtitle':self.xtitle,
410
+ 'ytitle':self.ytitle,
411
+ 'legend':self.legend,
412
+ 'xmin':self.xmin,
413
+ 'xmax':self.xmax,
414
+ 'ymin':self.ymin,
415
+ 'ymax':self.ymax,
416
+ 'ticks_x':self.ticks_x,
417
+ 'ticks_y':self.ticks_y,
418
+ 'ticks_label_x':self.ticks_label_x,
419
+ 'ticks_label_y':self.ticks_label_y}
420
+
421
+ if self._lines is not None:
422
+ props['lines'] = [line.to_dict() for line in self._lines]
423
+ else:
424
+ props['lines'] = []
425
+
426
+ return props
427
+
428
+ def from_dict(self, props:dict, frame:wx.Frame = None):
429
+ """ properties from dict """
430
+
431
+ keys = ['title', 'xtitle', 'ytitle', 'legend', 'xmin', 'xmax', 'ymin', 'ymax', 'ticks_x', 'ticks_y', 'ticks_label_x', 'ticks_label_y']
432
+
433
+ for key in keys:
434
+ try:
435
+ setattr(self, key, props[key])
436
+ except:
437
+ logging.warning('Key not found in properties dict')
438
+ pass
439
+
440
+ if isinstance(self.ticks_x,list):
441
+ self.ticks_x = [float(cur) for cur in props['ticks_x']]
442
+ elif isinstance(self.ticks_x,float):
443
+ self.ticks_x = [self.ticks_x]
444
+ elif isinstance(self.ticks_x,str):
445
+ self.ticks_x = [float(self.ticks_x)]
446
+
447
+ if isinstance(self.ticks_y,list):
448
+ self.ticks_y = [float(cur) for cur in props['ticks_y']]
449
+ elif isinstance(self.ticks_y,float):
450
+ self.ticks_y = [self.ticks_y]
451
+ elif isinstance(self.ticks_y,str):
452
+ self.ticks_y = [float(self.ticks_y)]
453
+
454
+ if isinstance(self.ticks_label_x,list):
455
+ pass
456
+ elif isinstance(self.ticks_label_x,float):
457
+ self.ticks_label_x = [self.ticks_label_x]
458
+ elif isinstance(self.ticks_label_x,str):
459
+ self.ticks_label_x = [self.ticks_label_x]
460
+
461
+ if isinstance(self.ticks_label_y,list):
462
+ pass
463
+ elif isinstance(self.ticks_label_y,float):
464
+ self.ticks_label_y = [self.ticks_label_y]
465
+ elif isinstance(self.ticks_label_y,str):
466
+ self.ticks_label_y = [self.ticks_label_y]
467
+
468
+ assert len(self.ticks_x) == len(self.ticks_label_x), f'{len(self.ticks_x)} != {len(self.ticks_label_x)}'
469
+ assert len(self.ticks_y) == len(self.ticks_label_y), f'{len(self.ticks_y)} != {len(self.ticks_label_y)}'
470
+
471
+ for line in props['lines']:
472
+ if 'xdata' in line and 'ydata' in line:
473
+ xdata = line['xdata']
474
+ ydata = line['ydata']
475
+ self._ax.plot(xdata, ydata)
476
+
477
+ self.populate()
478
+
479
+ self._lines = [Matplolib_line_properties(line, self).from_dict(line_props) for line_props, line in zip(props['lines'], self._ax.get_lines())]
480
+
481
+ return self
482
+
483
+ def serialize(self):
484
+ """ Serialize the properties """
485
+
486
+ return json.dumps(self.to_dict(), indent=4)
487
+
488
+ def deserialize(self, props:str):
489
+ """ Deserialize the properties """
490
+
491
+ self.from_dict(json.loads(props))
492
+
493
+ def add_props_to_sizer(self, frame:wx.Frame, sizer:wx.BoxSizer):
494
+ """ Add the properties to a sizer """
495
+
496
+ self._myprops.ensure_prop(frame, show_in_active_if_default=True, height=300)
497
+ sizer.Add(self._myprops.prop, proportion= 1, flag= wx.EXPAND)
498
+
499
+ self._myprops.prop.Hide()
500
+
501
+ def show_props(self):
502
+ """ Show the properties """
503
+
504
+ self._myprops.prop.Show()
505
+
506
+ def hide_props(self):
507
+ """ Hide the properties """
508
+
509
+ self._myprops.prop.Hide()
510
+
511
+ def hide_all_props(self):
512
+ """ Hide all properties """
513
+
514
+ self.hide_props()
515
+
516
+ for line in self._lines:
517
+ line.hide_props()
518
+
519
+ def del_line(self, idx:int):
520
+ """ Delete a line """
521
+
522
+ if idx>=0 and idx<len(self._lines):
523
+ self._lines[idx].delete()
524
+ self._lines.pop(idx)
525
+ self._ax.lines.pop(idx)
526
+
527
+
528
+ MARKERS_MPL = ['None','o', 'v', '^', '<', '>', 's', 'p', 'P', '*', 'h', 'H', '+', 'x', 'X', 'D', 'd', '|', '_']
529
+ LINESTYLE_MPL = ['-', '--', '-.', ':', 'solid', 'dashed', 'dashdot', 'dotted', 'None']
530
+
531
+
532
+ def convert_colorname_rgb(color:str) -> str:
533
+ """
534
+ Convert a given color name or abbreviation to its corresponding RGB tuple.
535
+
536
+ :param color: The color name or abbreviation to convert.
537
+ Supported colors are 'b'/'blue', 'g'/'green', 'r'/'red',
538
+ 'c'/'cyan', 'm'/'magenta', 'y'/'yellow', 'k'/'black',
539
+ 'w'/'white', and 'o'/'orange'.
540
+ :type color: str
541
+ :return: A tuple representing the RGB values of the color. If the color is not
542
+ recognized, returns (0, 0, 0) which corresponds to black.
543
+ :rtype: tuple
544
+ """
545
+
546
+ if color in COLORS_MPL:
547
+ if color in ['b', 'blue']:
548
+ return (0,0,255)
549
+ elif color in ['g', 'green']:
550
+ return (0,128,0)
551
+ elif color in ['r', 'red']:
552
+ return (255,0,0)
553
+ elif color in ['c', 'cyan']:
554
+ return (0,255,255)
555
+ elif color in ['m', 'magenta']:
556
+ return (255,0,255)
557
+ elif color in ['y', 'yellow']:
558
+ return (255,255,0)
559
+ elif color in ['k', 'black']:
560
+ return (0,0,0)
561
+ elif color in ['w', 'white']:
562
+ return (255,255,255)
563
+ elif color in ['o', 'orange']:
564
+ return (255,165,0)
565
+ else:
566
+ return(0,0,0)
567
+
568
+
569
+ def convert_color(value:str | tuple) -> tuple:
570
+ """ Convert a hex color to RGB """
571
+
572
+ if isinstance(value, tuple):
573
+ return tuple([int(cur*255) for cur in value])
574
+ elif isinstance(value, str):
575
+ if value.startswith('#'):
576
+ value = value.lstrip('#')
577
+ return tuple(int(value[i:i+2], 16) for i in (0, 2, 4))
578
+ else:
579
+ return convert_colorname_rgb(value)
580
+ else:
581
+ return (0,0,0)
582
+
583
+
584
+ class Matplolib_line_properties():
585
+
586
+ def __init__(self, line:Line2D=None, ax_props:"Matplotlib_ax_properties"= None) -> None:
587
+
588
+ self.wx_exits = wx.App.Get() is not None
589
+
590
+ self._ax_props = ax_props
591
+
592
+ self.color = (0,0,255)
593
+ self.linewidth = 1.5
594
+ self._linestyle = 0
595
+ self._marker = 0
596
+ self.markersize = 6
597
+ self.alpha = 1.0
598
+ self.label = 'Line'
599
+ self.markerfacecolor = (0,0,255)
600
+ self.markeredgecolor = (0,0,255)
601
+ self.markeredgewidth = 1.5
602
+ self.visible = True
603
+ self.zorder = 1
604
+ self.picker:bool = False
605
+ self.picker_radius:float = 5.0
606
+
607
+ self._selected = False
608
+ self._selected_prop:Matplolib_line_properties = None
609
+
610
+ self._myprops = None
611
+ self._line = line
612
+
613
+ self.update_legend = False
614
+
615
+ self._set_props()
616
+
617
+ if self._line is not None:
618
+ self.get_properties()
619
+
620
+ @property
621
+ def ax_props(self):
622
+ return self._ax_props
623
+
624
+ @ax_props.setter
625
+ def ax_props(self, value):
626
+ self._ax_props = value
627
+
628
+ @property
629
+ def ax(self):
630
+ return self._ax_props._ax
631
+
632
+ @property
633
+ def fig(self):
634
+ return self._ax_props._ax.figure
635
+
636
+ def copy(self):
637
+ new_prop = Matplolib_line_properties()
638
+
639
+ new_prop._ax_props = self._ax_props
640
+
641
+ new_prop.color = self.color
642
+ new_prop.linewidth = self.linewidth
643
+ new_prop._linestyle = self._linestyle
644
+ new_prop._marker = self._marker
645
+ new_prop.markersize = self.markersize
646
+ new_prop.alpha = self.alpha
647
+ new_prop.label = self.label
648
+ new_prop.markerfacecolor = self.markerfacecolor
649
+ new_prop.markeredgecolor = self.markeredgecolor
650
+ new_prop.markeredgewidth = self.markeredgewidth
651
+ new_prop.visible = self.visible
652
+ new_prop.zorder = self.zorder
653
+ new_prop.picker = self.picker
654
+ new_prop.picker_radius = self.picker_radius
655
+
656
+ return new_prop
657
+
658
+ def presets(self, preset:str):
659
+ """ Set the properties to a preset """
660
+
661
+ self.color = (0,0,255)
662
+ self.linewidth = 1.5
663
+ self._linestyle = 0
664
+ self._marker = 0
665
+ self.markersize = 6
666
+ self.alpha = 1.0
667
+ self.label = 'Line'
668
+ self.markerfacecolor = (0,0,255)
669
+ self.markeredgecolor = (0,0,255)
670
+ self.markeredgewidth = 1.5
671
+ self.visible = True
672
+ self.zorder = 1
673
+ self.picker = False
674
+ self.picker_radius = 5.0
675
+
676
+ if preset == 'default':
677
+ pass
678
+ elif preset == 'water':
679
+ self.color = (0,0,255)
680
+ self.linewidth = 2.5
681
+ self.label = 'Water'
682
+ elif preset == 'land':
683
+ self.color = (0,255,0)
684
+ self.linewidth = 2.5
685
+ self.label = 'Land'
686
+ elif preset == 'banks':
687
+ self.color = (128,128,128)
688
+ self.linestyle = 1
689
+ self.linewidth = 1.0
690
+
691
+ self.set_properties()
692
+
693
+ @property
694
+ def selected(self):
695
+ return self._selected
696
+
697
+ @selected.setter
698
+ def selected(self, value):
699
+ self._selected = value
700
+ self.set_properties()
701
+
702
+ @property
703
+ def linestyle(self):
704
+ return LINESTYLE_MPL[self._linestyle]
705
+
706
+ @linestyle.setter
707
+ def linestyle(self, value):
708
+
709
+ if isinstance(value, str):
710
+ if value in LINESTYLE_MPL:
711
+ self._linestyle = LINESTYLE_MPL.index(value)
712
+ else:
713
+ logging.warning('Line style not found, set to default')
714
+ self._linestyle = 0
715
+ elif isinstance(value, int):
716
+ self._linestyle = value
717
+ else:
718
+ logging.warning('Line style not found, set to default')
719
+ self._linestyle = 0
720
+
721
+ @property
722
+ def marker(self):
723
+ return MARKERS_MPL[self._marker]
724
+
725
+ @marker.setter
726
+ def marker(self, value):
727
+ if isinstance(value, str):
728
+ if value in MARKERS_MPL:
729
+ self._marker = MARKERS_MPL.index(value)
730
+ else:
731
+ logging.warning('Marker not found, set to default')
732
+ self._marker = 0
733
+ elif isinstance(value, int):
734
+ self._marker = value
735
+ else:
736
+ logging.warning('Marker not found, set to default')
737
+ self._marker = 0
738
+
739
+ def set_line(self, line:Line2D):
740
+ self._line = line
741
+
742
+ if line is None:
743
+ return
744
+
745
+ self.get_properties()
746
+
747
+ return self
748
+
749
+
750
+ def on_pick(self, line:Line2D, mouseevent:MouseEvent):
751
+ if mouseevent.button == 1:
752
+ pass
753
+ print(mouseevent.xdata, mouseevent.ydata)
754
+
755
+ # line.set_color('r')
756
+ # line.figure.canvas.draw()
757
+
758
+ return True, dict()
759
+
760
+ def get_properties(self, line:Line2D= None):
761
+
762
+ if line is None:
763
+ line = self._line
764
+
765
+ if line is None:
766
+ logging.warning('Line not found/defined')
767
+ return
768
+
769
+ self.color = convert_color(line.get_color())
770
+ self.linewidth = line.get_linewidth()
771
+ self.linestyle = line.get_linestyle()
772
+
773
+ if self.linestyle not in LINESTYLE_MPL:
774
+ self.linestyle = '-'
775
+ logging.warning('Line style not found, set to default')
776
+
777
+ self.marker = line.get_marker()
778
+
779
+ if self.marker not in MARKERS_MPL:
780
+ self.marker = 'o'
781
+ logging.warning('Marker not found, set to default')
782
+
783
+ self.markersize = line.get_markersize()
784
+ self.alpha = line.get_alpha() if line.get_alpha() is not None else 1.0
785
+ self.label = line.get_label()
786
+ self.markerfacecolor = convert_color(line.get_markerfacecolor())
787
+ self.markeredgecolor = convert_color(line.get_markeredgecolor())
788
+ self.markeredgewidth = line.get_markeredgewidth()
789
+ self.visible = line.get_visible()
790
+ self.zorder = line.get_zorder()
791
+
792
+ self.picker = line.get_picker() is not None
793
+ self.picker_radius = line.get_pickradius()
794
+
795
+ def _set_props(self):
796
+ """ Set the properties UI """
797
+
798
+ if self._myprops is not None:
799
+ return
800
+
801
+ self._myprops = Wolf_Param(title='Line properties', w= 500, h= 400, to_read= False, ontop= False, init_GUI= False)
802
+
803
+ self._myprops.set_callbacks(None, self.destroyprop)
804
+
805
+ # self._myprops.hide_selected_buttons() # only 'Apply' button
806
+
807
+ self._myprops.addparam('Draw','Color',self.color,'Color','Drawing color')
808
+ self._myprops.addparam('Draw','Width',self.linewidth,'Float','Drawing width')
809
+ self._myprops.addparam('Draw','Style',self._linestyle,'Integer','Drawing style', jsonstr= new_json({'-':0, '--':1, '-.':2, ':':3, 'None':8, 'solid': 0, 'dashed': 1, 'dashdot': 2, 'dotted': 3}))
810
+ self._myprops.addparam('Draw', 'Alpha', self.alpha, 'Float', 'Transparency')
811
+ self._myprops.addparam('Draw', 'Label', self.label, 'String', 'Label')
812
+ self._myprops.addparam('Draw', 'Visible', self.visible, 'Logical', 'Visible')
813
+ self._myprops.addparam('Draw', 'Zorder', self.zorder, 'Integer', 'Zorder')
814
+
815
+ self._myprops.addparam('Marker', 'Marker', self._marker, 'Integer', 'Marker style', jsonstr= new_json({'None':0, 'o': 1, 'v': 2, '^': 3, '<': 4, '>': 5, 's': 6, 'p': 7, 'P': 8, '*': 9, 'h': 10, 'H': 11, '+': 12, 'x': 13, 'X': 14, 'D': 15, 'd': 16, '|': 17, '_': 18}))
816
+ self._myprops.addparam('Marker', 'Markersize', self.markersize, 'Float', 'Marker size')
817
+ self._myprops.addparam('Marker', 'Markerfacecolor', self.markerfacecolor, 'Color', 'Marker face color')
818
+ self._myprops.addparam('Marker', 'Markeredgecolor', self.markeredgecolor, 'Color', 'Marker edge color')
819
+ self._myprops.addparam('Marker', 'Markeredgewidth', self.markeredgewidth, 'Float', 'Marker edge width')
820
+
821
+ self._myprops.addparam('Picker', 'Picker', self.picker, 'Logical', 'Picker')
822
+ self._myprops.addparam('Picker', 'Picker radius', self.picker_radius, 'Float', 'Picker radius')
823
+
824
+ self._myprops.Populate()
825
+ # self._myprops.Layout()
826
+ # self._myprops.SetSizeHints(500,500)
827
+
828
+ def populate(self):
829
+ """ Populate the properties UI """
830
+
831
+ if self._myprops is None:
832
+ self._set_props()
833
+
834
+ self._myprops[('Draw','Color')] = self.color
835
+ self._myprops[('Draw','Width')] = self.linewidth
836
+ self._myprops[('Draw','Style')] = self._linestyle
837
+ self._myprops[('Draw','Alpha')] = self.alpha
838
+ self._myprops[('Draw','Label')] = self.label
839
+ self._myprops[('Draw','Visible')] = self.visible
840
+ self._myprops[('Draw','Zorder')] = self.zorder
841
+
842
+ self._myprops[('Marker', 'Marker')] = self._marker
843
+ self._myprops[('Marker', 'Markersize')] = self.markersize
844
+ self._myprops[('Marker', 'Markeredgecolor')] = self.markeredgecolor
845
+ self._myprops[('Marker', 'Markerfacecolor')] = self.markerfacecolor
846
+ self._myprops[('Marker', 'Markeredgewidth')] = self.markeredgewidth
847
+
848
+ self._myprops[('Picker', 'Picker')] = self.picker
849
+ self._myprops[('Picker', 'Picker radius')] = self.picker_radius
850
+
851
+ self._myprops.Populate()
852
+
853
+ def ui(self):
854
+
855
+ if self._myprops is not None:
856
+ self._myprops.CenterOnScreen()
857
+ self._myprops.Raise()
858
+ self._myprops.Show()
859
+ return
860
+
861
+ self._set_props()
862
+
863
+ self._myprops.Show()
864
+ self._myprops.SetTitle(_('Line properties'))
865
+
866
+ icon = wx.Icon()
867
+ icon_path = Path(__file__).parent / "apps/wolf_logo2.bmp"
868
+ icon.CopyFromBitmap(wx.Bitmap(str(icon_path), wx.BITMAP_TYPE_ANY))
869
+ self._myprops.SetIcon(icon)
870
+
871
+ self._myprops.Center()
872
+ self._myprops.Raise()
873
+
874
+ def destroyprop(self):
875
+ self._myprops=None
876
+
877
+ def fill_property(self, verbosity:bool= True):
878
+
879
+ if self._myprops is None:
880
+ logging.warning('Properties UI not found')
881
+ return
882
+
883
+ self._myprops.apply_changes_to_memory(verbosity= verbosity)
884
+
885
+ self.color = getRGBfromI(self._myprops[('Draw','Color')])
886
+ self.linewidth = self._myprops[('Draw','Width')]
887
+ self.linestyle = self._myprops[('Draw','Style')]
888
+ self.alpha = self._myprops[('Draw', 'Alpha')]
889
+
890
+ self.update_legend = self.label == self._myprops[('Draw', 'Label')]
891
+
892
+ self.label = self._myprops[('Draw', 'Label')]
893
+ self.visible = self._myprops[('Draw', 'Visible')]
894
+ self.zorder = self._myprops[('Draw', 'Zorder')]
895
+
896
+ self.marker = self._myprops[('Marker', 'Marker')]
897
+ self.markersize = self._myprops[('Marker', 'Markersize')]
898
+ self.markeredgecolor = getRGBfromI(self._myprops[('Marker', 'Markeredgecolor')])
899
+ self.markerfacecolor = getRGBfromI(self._myprops[('Marker', 'Markerfacecolor')])
900
+ self.markeredgewidth = self._myprops[('Marker', 'Markeredgewidth')]
901
+
902
+ self.picker = self._myprops[('Picker', 'Picker')]
903
+ self.picker_radius = self._myprops[('Picker', 'Picker radius')]
904
+
905
+ self.set_properties()
906
+
907
+ def set_properties(self, line:Line2D = None):
908
+
909
+ if line is None:
910
+ line = self._line
911
+
912
+ if line is None:
913
+ logging.warning('Line not found/defined')
914
+ return
915
+
916
+ def check_color(color):
917
+ if isinstance(color, str):
918
+ color = convert_colorname_rgb(color)
919
+ color = tuple([c/255. for c in color])
920
+ return color
921
+
922
+ line.set_color(check_color(self.color if not self.selected else (255,0,0)))
923
+ line.set_linewidth(self.linewidth if not self.selected else 3.0)
924
+ line.set_linestyle(self.linestyle if not self.selected else '-')
925
+
926
+ line.set_marker(self.marker)
927
+ line.set_markersize(self.markersize)
928
+ line.set_alpha(self.alpha)
929
+ line.set_label(self.label)
930
+ line.set_markerfacecolor(check_color(self.markerfacecolor if not self.selected else (255,0,0)))
931
+ line.set_markeredgecolor(check_color(self.markeredgecolor))
932
+ line.set_markeredgewidth(self.markeredgewidth)
933
+
934
+ line.set_visible(self.visible)
935
+ line.set_zorder(self.zorder)
936
+
937
+ line.set_pickradius(self.picker_radius)
938
+
939
+ line.set_picker(self.on_pick if self.picker else lambda line,mouseevent: (False, dict()))
940
+
941
+ if self._ax_props is not None:
942
+ self._ax_props.fill_property(verbosity= False)
943
+ else:
944
+ line.axes.figure.canvas.draw()
945
+
946
+ def show_properties(self):
947
+ self.ui()
948
+
949
+ def to_dict(self) -> str:
950
+ """ properties to dict """
951
+
952
+ xdata = self._line.get_xdata().tolist()
953
+ ydata = self._line.get_ydata().tolist()
954
+
955
+ return {'color':self.color,
956
+ 'linewidth':self.linewidth,
957
+ 'linestyle':self.linestyle,
958
+ 'marker':self.marker,
959
+ 'markersize':self.markersize,
960
+ 'alpha':self.alpha,
961
+ 'label':self.label,
962
+ 'markerfacecolor':self.markerfacecolor,
963
+ 'markeredgecolor':self.markeredgecolor,
964
+ 'markeredgewidth':self.markeredgewidth,
965
+ 'visible':self.visible,
966
+ 'zorder':self.zorder,
967
+ 'picker':self.picker,
968
+ 'picker_radius':self.picker_radius,
969
+ 'xdata':xdata,
970
+ 'ydata':ydata}
971
+
972
+ def from_dict(self, props:dict):
973
+ """ properties from dict """
974
+
975
+ keys = ['color', 'linewidth', 'linestyle', 'marker', 'markersize', 'alpha', 'label', 'markerfacecolor', 'markeredgecolor', 'markeredgewidth', 'visible', 'zorder', 'picker', 'picker_radius']
976
+
977
+ for key in keys:
978
+ try:
979
+ setattr(self, key, props[key])
980
+ except:
981
+ logging.warning('Key not found in properties dict')
982
+ pass
983
+
984
+ self.populate()
985
+ self.set_properties()
986
+
987
+ return self
988
+
989
+ def add_props_to_sizer(self, frame:wx.Frame, sizer:wx.BoxSizer):
990
+ """ Add the properties to a sizer """
991
+
992
+ self._myprops.ensure_prop(frame, show_in_active_if_default=True, height=300)
993
+ sizer.Add(self._myprops.prop, proportion= 1, flag= wx.EXPAND)
994
+ self._myprops.prop.Hide()
995
+
996
+ def show_props(self):
997
+ """ Show the properties """
998
+
999
+ self.populate()
1000
+ self._myprops.prop.Show()
1001
+
1002
+ def hide_props(self):
1003
+ """ Hide the properties """
1004
+
1005
+ self._myprops.prop.Hide()
1006
+
1007
+ def delete(self):
1008
+ """ Delete the properties """
1009
+
1010
+ self._myprops.prop.Hide()
1011
+ self._myprops.prop.Destroy()
1012
+ self._myprops = None
1013
+ self._line = None
1014
+
1015
+ class PRESET_LAYOUTS(Enum):
1016
+ DEFAULT = (1,1)
1017
+ MAT2X2 = (2,2)
1018
+ class Matplotlib_Figure(wx.Frame):
1019
+ """ Matplotlib Figure with wx Frame """
1020
+
1021
+ def __init__(self, layout:tuple | list | dict | PRESET_LAYOUTS = None) -> None:
1022
+ """
1023
+ Layout can be a tuple, a list, a dict or a string.
1024
+ If a string, it must be a list of strings or a list of lists. It will be used in fig.subplot_mosaic.
1025
+ If a tuple or a list of 2 integers. It will be used in fig.subplots.
1026
+ if a dict, it must contain 'nrows' and 'ncols' and 'ax_cells' (list of tuples with row_start, row_end, col_start, col_end, key).
1027
+ It will be used in fig.add_gridspec.
1028
+
1029
+ The class has:
1030
+ - fig: the figure
1031
+ - ax_dict: a dict of axes --> key: name of the axes, value: axes
1032
+ - ax: a list of axes --> always flatten
1033
+
1034
+ The properties of the figure can be accessed by self.fig_properties.
1035
+ The properties of the axes can be accessed by self._axes_properties.
1036
+ The current Axes can be accessed by self.cur_ax.
1037
+
1038
+ A plot can be added by self.add_plot(xdata, ydata, label, color, linestyle, linewidth, marker, markersize, markerfacecolor, markeredgecolor, markeredgewidth, alpha, visible, zorder, picker, picker_radius)
1039
+
1040
+ :param layout: layout of the figure
1041
+ :type layout: tuple | list | dict | str
1042
+ """
1043
+
1044
+ self.wx_exists = wx.App.Get() is not None
1045
+
1046
+ self.fig = plt.figure()
1047
+ dpi = self.fig.get_dpi()
1048
+ size_x, size_y = self.fig.get_size_inches()
1049
+
1050
+ if self.wx_exists:
1051
+ wx.Frame.__init__(self, None, -1, 'Matplotlib Figure', size=(size_x*dpi+16, size_y*dpi+240), style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER)
1052
+
1053
+ self.ax_dict:dict[str,Axes] = {} # dict of axes
1054
+ self.ax:list[Axes] = [] # list of axes -- always flatten
1055
+ self.shown_props = None # shown properties
1056
+
1057
+ self.apply_layout(layout) # apply the layout
1058
+ pass
1059
+
1060
+ def presets(self, which:PRESET_LAYOUTS = PRESET_LAYOUTS.DEFAULT):
1061
+ """ Presets """
1062
+
1063
+ if which not in PRESET_LAYOUTS:
1064
+ logging.warning('Preset not found')
1065
+ return
1066
+
1067
+ self.apply_layout(which)
1068
+
1069
+ @property
1070
+ def layout(self):
1071
+ return self._layout
1072
+
1073
+ def apply_layout(self, layout:tuple | list | dict | PRESET_LAYOUTS):
1074
+ """ Apply the layout
1075
+
1076
+ Choose between (subplots, subplot_mosaic, gridspec) according to the type of layout (tuple, list[str], dict)
1077
+ """
1078
+
1079
+ self._layout = layout
1080
+
1081
+ if self._layout is None:
1082
+ logging.info('No layout defined')
1083
+ return
1084
+
1085
+ if isinstance(layout, PRESET_LAYOUTS):
1086
+
1087
+ self.apply_layout(layout.value)
1088
+ return
1089
+
1090
+ if isinstance(layout, tuple | list):
1091
+ # check is the first element is a string - layout can be a list of lists
1092
+ tmp_layout = []
1093
+ for cur in layout:
1094
+ if isinstance(cur, list):
1095
+ tmp_layout.extend(cur)
1096
+ else:
1097
+ tmp_layout.append(cur)
1098
+
1099
+ if isinstance(tmp_layout[0], str):
1100
+ # List of strings - subplot_mosaic returns a dict of Axes
1101
+ self.ax_dict = self.fig.subplot_mosaic(layout)
1102
+ # store the axes in a list -- So we can access them by index, not only by name
1103
+ self.ax = [ax for ax in self.ax_dict.values()]
1104
+ else:
1105
+ # Tuple or list of 2 elements - subplots
1106
+ if len(layout) != 2:
1107
+ logging.warning('Layout must be a tuple or a list of 2 elements')
1108
+ return
1109
+
1110
+ self.nbrows, self.nbcols = layout
1111
+ if self.nbrows*self.nbcols == 1:
1112
+ # Convert to list -- subplots returns a single Axes but we want a list
1113
+ self.ax = [self.fig.subplots(self.nbrows, self.nbcols)]
1114
+ else:
1115
+ # Flatten the axes -- sbplots returns a 2D array of Axes but we want a list
1116
+ self.ax = self.fig.subplots(self.nbrows, self.nbcols).flatten()
1117
+
1118
+ # store the axes in a dict -- So we can access them by name, not only by index
1119
+ self.ax_dict = {f'{i}':ax for i, ax in enumerate(self.ax)}
1120
+ for key,ax in self.ax_dict.items():
1121
+ ax._label = key
1122
+
1123
+ elif isinstance(layout, dict):
1124
+ # dict --> Gridspec
1125
+
1126
+ # Check if nrows and ncols are defined
1127
+ if 'nrows' not in layout or 'ncols' not in layout:
1128
+ logging.warning('nrows and ncols must be defined in the layout')
1129
+ return
1130
+
1131
+ if 'ax_cells' not in layout:
1132
+ logging.warning('ax_cells must be defined in the layout')
1133
+ return
1134
+
1135
+ gs:GridSpec = self.fig.add_gridspec(nrows= layout['nrows'], ncols= layout['ncols'])
1136
+ ax_cells = layout['ax_cells']
1137
+
1138
+ for row_start, row_end, col_start, col_end, key in ax_cells:
1139
+ self.ax_dict[key] = self.fig.add_subplot(gs[row_start:row_end, col_start:col_end])
1140
+ self.ax_dict[key]._label = key
1141
+
1142
+ self.ax = [ax for ax in self.ax_dict.values()]
1143
+
1144
+ self._fig_properties = Matplotlib_figure_properties(self, self.fig)
1145
+
1146
+ if self.wx_exists:
1147
+ self.set_wx()
1148
+
1149
+ @property
1150
+ def fig_properties(self) -> "Matplotlib_figure_properties":
1151
+ return self._fig_properties
1152
+
1153
+ @property
1154
+ def _axes_properties(self) -> list[Matplotlib_ax_properties]:
1155
+ return self._fig_properties._axes
1156
+
1157
+ @property
1158
+ def nbrows(self):
1159
+ return self._nbrows
1160
+
1161
+ @nbrows.setter
1162
+ def nbrows(self, value:int):
1163
+ self._nbrows = value
1164
+
1165
+ @property
1166
+ def nbcols(self):
1167
+ return self._nbcols
1168
+
1169
+ @nbcols.setter
1170
+ def nbcols(self, value:int):
1171
+ self._nbcols = value
1172
+
1173
+ @property
1174
+ def nb_axes(self):
1175
+ return len(self.ax)
1176
+
1177
+ def set_wx(self):
1178
+ """ Set the wx Frame Design """
1179
+
1180
+ self.SetIcon(wx.Icon(str(Path(__file__).parent / "apps/wolf_logo2.bmp")))
1181
+
1182
+ self._sizer = wx.BoxSizer(wx.VERTICAL)
1183
+
1184
+ # Matplotlib canvas interacting with wx
1185
+ # --------------------------------------
1186
+
1187
+ self._canvas = FigureCanvas(self, -1, self.fig)
1188
+ self._sizer.Add(self._canvas, 1, wx.EXPAND | wx.ALL)
1189
+
1190
+ # Bind events
1191
+ self._canvas.Bind(wx.EVT_ENTER_WINDOW, self.ChangeCursor)
1192
+ self._canvas.mpl_connect('motion_notify_event', self.UpdateStatusBar)
1193
+ self._canvas.mpl_connect('button_press_event', self.OnClickCanvas)
1194
+ self._canvas.mpl_connect('key_press_event', self.OnKeyCanvas)
1195
+
1196
+ # Toolbar - Matplotlib
1197
+ # --------------------
1198
+
1199
+ self._toolbar = NavigationToolbar(self._canvas, self)
1200
+
1201
+
1202
+ # Buttons - Figure, Axes, Lines properties
1203
+ # --------- ------------------------------
1204
+
1205
+ self._prop_but = wx.Button(self, -1, 'Figure Properties')
1206
+
1207
+ self._ax_sizer = wx.BoxSizer(wx.HORIZONTAL)
1208
+ self._ax_current = wx.Choice(self, -1, choices=[ax._label for ax in self.ax])
1209
+ self._ax_current.SetToolTip('Select the current ax -- Axes are enumerated from left to right and top to bottom')
1210
+ self._ax_current.SetSelection(0)
1211
+ self._ax_but = wx.Button(self, -1, 'Ax Properties')
1212
+ self._ax_but.SetToolTip('Choosing the properties of the current ax -- Axes are enumerated from left to right and top to bottom')
1213
+
1214
+ self._ax_current.Bind(wx.EVT_CHOICE, self.on_ax_choice)
1215
+ self._ax_but.Bind(wx.EVT_BUTTON, self.on_ax_properties)
1216
+
1217
+ self._ax_sizer.Add(self._ax_current, 1, wx.EXPAND)
1218
+ self._ax_sizer.Add(self._ax_but, 1, wx.EXPAND)
1219
+
1220
+ self._line_sizer = wx.BoxSizer(wx.HORIZONTAL)
1221
+ self._line_current = wx.Choice(self, -1, choices=[str(i) for i in range(len(self.cur_ax.get_lines()))])
1222
+ self._line_current.SetSelection(0)
1223
+ self._line_but = wx.Button(self, -1, 'Line Properties')
1224
+
1225
+ self._line_but.Bind(wx.EVT_BUTTON, self.on_line_properties)
1226
+ self._line_current.Bind(wx.EVT_CHOICE, self.on_line_choose)
1227
+
1228
+ self._line_sizer.Add(self._line_current, 1, wx.EXPAND)
1229
+ self._line_sizer.Add(self._line_but, 1, wx.EXPAND)
1230
+
1231
+ self.Bind(wx.EVT_CLOSE, self.on_close)
1232
+ self._prop_but.Bind(wx.EVT_BUTTON, self.on_fig_properties)
1233
+
1234
+ self._sizer.Add(self._toolbar, 0, wx.EXPAND)
1235
+ self._sizer.Add(self._prop_but, 0, wx.EXPAND)
1236
+
1237
+ self._sizer.Add(self._ax_sizer, 0, wx.EXPAND)
1238
+ self._sizer.Add(self._line_sizer, 0, wx.EXPAND)
1239
+
1240
+ self._statusbar = wx.StatusBar(self)
1241
+ self._sizer.Add(self._statusbar, 0, wx.EXPAND)
1242
+
1243
+ # Buttons - Save, Load
1244
+ # --------------------
1245
+
1246
+ self._save_but = wx.Button(self, -1, 'Save')
1247
+ self._load_but = wx.Button(self, -1, 'Load')
1248
+
1249
+ self._save_but.Bind(wx.EVT_BUTTON, self.on_save)
1250
+ self._load_but.Bind(wx.EVT_BUTTON, self.on_load)
1251
+
1252
+ self._sizer_save_load = wx.BoxSizer(wx.HORIZONTAL)
1253
+ self._sizer_save_load.Add(self._save_but, 1, wx.EXPAND)
1254
+ self._sizer_save_load.Add(self._load_but, 1, wx.EXPAND)
1255
+ self._sizer.Add(self._sizer_save_load, 0, wx.EXPAND)
1256
+
1257
+ self._applyt_but = wx.Button(self, -1, 'Apply Properties')
1258
+ self._applyt_but.Bind(wx.EVT_BUTTON, self.onapply_properties)
1259
+ self._sizer.Add(self._applyt_but, 0, wx.EXPAND)
1260
+
1261
+ # Collapsible pane -- Grid Xls, Properties
1262
+ # ----------------------------------
1263
+
1264
+ self._collaps_pane = wx.CollapsiblePane(self, label='Properties', style=wx.CP_DEFAULT_STYLE | wx.CP_NO_TLW_RESIZE)
1265
+ self._collaps_pane.Bind(wx.EVT_COLLAPSIBLEPANE_CHANGED, self.on_collaps_pane)
1266
+
1267
+ win = self._collaps_pane.GetPane()
1268
+ self._sizer_grid_props = wx.BoxSizer(wx.HORIZONTAL)
1269
+ win.SetSizer(self._sizer_grid_props)
1270
+ self._sizer_grid_props.SetSizeHints(win)
1271
+
1272
+ # XLS sizer
1273
+ # ---------
1274
+ self._sizer_xls = wx.BoxSizer(wx.VERTICAL)
1275
+
1276
+ self._xls = CpGrid(win, -1, wx.WANTS_CHARS)
1277
+ self._update_xy = wx.Button(win, -1, 'Update XY')
1278
+ self._update_xy.Bind(wx.EVT_BUTTON, self.update_line_from_grid)
1279
+
1280
+ self._add_row = wx.Button(win, -1, 'Add rows')
1281
+ self._add_row.Bind(wx.EVT_BUTTON, self.add_row_to_grid)
1282
+
1283
+ self._add_line = wx.Button(win, -1, 'Add line')
1284
+ self._add_line.Bind(wx.EVT_BUTTON, self.onadd_line)
1285
+
1286
+ self._del_line = wx.Button(win, -1, 'Remove line')
1287
+ self._del_line.Bind(wx.EVT_BUTTON, self.ondel_line)
1288
+
1289
+ self._sizer_xls.Add(self._xls, 1, wx.EXPAND)
1290
+ self._sizer_xls.Add(self._update_xy, 0, wx.EXPAND)
1291
+ self._sizer_xls.Add(self._add_row, 0, wx.EXPAND)
1292
+ self._sizer_xls.Add(self._add_line, 0, wx.EXPAND)
1293
+ self._sizer_xls.Add(self._del_line, 0, wx.EXPAND)
1294
+
1295
+ # Properties sizer
1296
+ # ---------------
1297
+
1298
+ # Add all props from axes
1299
+ self._fig_properties.add_props_to_sizer(win, self._sizer_grid_props)
1300
+
1301
+ self._sizer_grid_props.Add(self._sizer_xls, 1, wx.GROW | wx.ALL)
1302
+
1303
+ # self._sizer.Add(self._sizer_grid_props, 1, wx.EXPAND)
1304
+ self._collaps_pane.Expand()
1305
+
1306
+ self._sizer.Add(self._collaps_pane, 0, wx.EXPAND | wx.ALL)
1307
+
1308
+ self._xls.CreateGrid(10, 2)
1309
+ self._xls.SetColLabelValue(0, 'X')
1310
+ self._xls.SetColLabelValue(1, 'Y')
1311
+ self._xls.SetMaxSize((-1, 400))
1312
+
1313
+
1314
+ self.SetSizer(self._sizer)
1315
+ self.SetAutoLayout(True)
1316
+ # self.Layout()
1317
+ self.Fit()
1318
+
1319
+ self.Bind(wx.EVT_SIZE, self.on_size)
1320
+
1321
+ self.Show()
1322
+ self._collapsible_size = self._collaps_pane.GetSize()
1323
+
1324
+ def on_save(self, event):
1325
+ """ Save the figure """
1326
+
1327
+ with wx.FileDialog(self, "Save figure", wildcard="JSON files (*.json)|*.json", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as fileDialog:
1328
+ if fileDialog.ShowModal() == wx.ID_CANCEL:
1329
+ return
1330
+
1331
+ path = fileDialog.GetPath()
1332
+ self.save(str(path))
1333
+
1334
+ def on_load(self, event):
1335
+ """ Load the figure """
1336
+
1337
+ with wx.FileDialog(self, "Open figure", wildcard="JSON files (*.json)|*.json", style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) as fileDialog:
1338
+ if fileDialog.ShowModal() == wx.ID_CANCEL:
1339
+ return
1340
+
1341
+ path = fileDialog.GetPath()
1342
+ self.load(str(path))
1343
+
1344
+ def ChangeCursor(self, event:MouseEvent):
1345
+ self._canvas.SetCursor(wx.Cursor(wx.CURSOR_BULLSEYE))
1346
+
1347
+ def UpdateStatusBar(self, event:MouseEvent):
1348
+
1349
+ if event.inaxes:
1350
+ idx= event.inaxes.get_figure().axes.index(event.inaxes)
1351
+ x, y = event.xdata, event.ydata
1352
+ self._statusbar.SetStatusText("Axes index= " + str(idx) + " -- x= "+str(x)+" -- y="+str(y))
1353
+
1354
+ def _mask_all_axes_props(self):
1355
+ for ax_prop in self._axes_properties:
1356
+ ax_prop._myprops.prop.Hide()
1357
+
1358
+ def _show_axes_props(self, idx:int):
1359
+ self._mask_all_axes_props()
1360
+ self._axes_properties[idx]._myprops.prop.Show()
1361
+
1362
+ @property
1363
+ def cur_ax(self) -> Axes:
1364
+ return self.ax[int(self._ax_current.GetSelection())]
1365
+
1366
+ @cur_ax.setter
1367
+ def cur_ax(self, idx:int):
1368
+ if idx < 0 or idx >= len(self.ax):
1369
+ logging.warning('Index out of range')
1370
+ return
1371
+
1372
+ self._ax_current.SetSelection(idx)
1373
+ self._fill_lines_ax()
1374
+
1375
+ @property
1376
+ def cur_ax_properties(self) -> Matplotlib_ax_properties:
1377
+ return self._axes_properties[int(self._ax_current.GetSelection())]
1378
+
1379
+ @cur_ax_properties.setter
1380
+ def cur_ax_properties(self, idx:int):
1381
+
1382
+ if idx < 0 or idx >= len(self._axes_properties):
1383
+ logging.warning('Index out of range')
1384
+ return
1385
+
1386
+ self._ax_current.SetSelection(idx)
1387
+ self._fill_lines_ax()
1388
+
1389
+ @property
1390
+ def cur_line_properties(self) -> Matplolib_line_properties:
1391
+
1392
+ if self._line_current.GetSelection() == -1:
1393
+ return None
1394
+
1395
+ return self.cur_ax_properties._lines[int(self._line_current.GetSelection())]
1396
+
1397
+ @property
1398
+ def cur_line(self) -> Line2D:
1399
+ return self.cur_ax.get_lines()[int(self._line_current.GetSelection())]
1400
+
1401
+ def get_figax(self):
1402
+
1403
+ if len(self.ax) == 1:
1404
+ return self.fig, self.ax[0]
1405
+ else:
1406
+ return self.fig, self.ax
1407
+
1408
+ def on_close(self, event):
1409
+ self.Destroy()
1410
+
1411
+ def on_fig_properties(self, event):
1412
+ """ Show the figure properties """
1413
+ self.show_fig_properties()
1414
+
1415
+ def show_fig_properties(self):
1416
+ # self._fig_properties.ui()
1417
+ self._hide_all_props()
1418
+ self._fig_properties.show_props()
1419
+ self.Layout()
1420
+ self.shown_props = self._fig_properties
1421
+
1422
+ def _fill_lines_ax(self, idx:int = None):
1423
+ self._line_current.SetItems([line.get_label() for line in self.cur_ax.get_lines()])
1424
+ self._line_current.SetSelection(0)
1425
+
1426
+ if idx is not None:
1427
+ self._line_current.SetSelection(idx)
1428
+
1429
+ def on_ax_choice(self, event):
1430
+ self._fill_lines_ax()
1431
+
1432
+ def on_ax_properties(self, event):
1433
+ """ Show the ax properties """
1434
+ self.show_curax_properties()
1435
+
1436
+ def show_curax_properties(self):
1437
+ # self.cur_ax_properties.ui()
1438
+ self._hide_all_props()
1439
+ self.cur_ax_properties.show_props()
1440
+ self.Layout()
1441
+ self.shown_props = self.cur_ax_properties
1442
+
1443
+ def on_line_properties(self, event):
1444
+ """ Show the line properties """
1445
+ self.show_curline_properties()
1446
+
1447
+ def show_curline_properties(self):
1448
+ # self.cur_line_properties.ui()
1449
+ self._hide_all_props()
1450
+ self.cur_line_properties.show_props()
1451
+ # self.Layout()
1452
+ self._sizer_grid_props.Layout()
1453
+ self.shown_props = self.cur_line_properties
1454
+
1455
+ def onapply_properties(self, event):
1456
+ """ Apply the properties """
1457
+
1458
+ if self.shown_props is not None:
1459
+ self.shown_props.fill_property()
1460
+ self.update_layout()
1461
+
1462
+ def _hide_all_props(self):
1463
+
1464
+ self._fig_properties.hide_props()
1465
+ for ax_prop in self._axes_properties:
1466
+ ax_prop.hide_all_props()
1467
+
1468
+ def on_line_choose(self, event):
1469
+
1470
+ self.cur_ax_properties.reset_selection()
1471
+ self.cur_ax_properties.select_line(self._line_current.GetSelection())
1472
+ self.fill_grid_with_xy()
1473
+
1474
+ def on_size(self, event):
1475
+ """ Resize event """
1476
+
1477
+ width, height = self.fig.get_size_inches()
1478
+ dpi = self.fig.get_dpi()
1479
+ width_pix = int(width * dpi)
1480
+ height_pix = int(height * dpi)
1481
+
1482
+ self._canvas.MinSize = (width_pix, height_pix)
1483
+
1484
+ self._collapsible_size = self._collaps_pane.GetSize()
1485
+
1486
+ event.Skip()
1487
+
1488
+ def update_layout(self):
1489
+
1490
+ if not self.wx_exists:
1491
+ return
1492
+
1493
+ width, height = self.fig.get_size_inches()
1494
+ dpi = self.fig.get_dpi()
1495
+ width_pix = int(width * dpi)
1496
+ height_pix = int(height * dpi)
1497
+
1498
+ self._canvas.MinSize = (width_pix, height_pix)
1499
+
1500
+ self.SetSize((width_pix + 16, height_pix + 210 + self._collapsible_size[1]))
1501
+
1502
+ self.Fit()
1503
+
1504
+ def on_collaps_pane(self, event):
1505
+ """ Collapsible pane event """
1506
+
1507
+ if event.GetCollapsed():
1508
+ self._collaps_pane.Collapse()
1509
+ else:
1510
+ self._collaps_pane.Expand()
1511
+
1512
+ if self._collapsible_size != self._collaps_pane.GetSize():
1513
+ self.SetSize((self.GetSize()[0], self.GetSize()[1] + self._collaps_pane.GetSize()[1]-self._collapsible_size[1]))
1514
+ self._collapsible_size = self._collaps_pane.GetSize()
1515
+
1516
+ self.Fit()
1517
+
1518
+ def OnKeyCanvas(self, event:KeyEvent):
1519
+
1520
+ if event.key == 'escape':
1521
+ self._axes_properties[int(self._ax_current.GetSelection())].reset_selection()
1522
+
1523
+ def OnClickCanvas(self, event:MouseEvent):
1524
+
1525
+ rclick = event.button == 3
1526
+ lclick = event.button == 1
1527
+
1528
+ if not rclick:
1529
+ return
1530
+
1531
+ if event.inaxes:
1532
+ ax:Axes = event.inaxes
1533
+ idx= ax.get_figure().axes.index(event.inaxes)
1534
+ x, y = event.xdata, event.ydata
1535
+
1536
+ dist_min = 1e6
1537
+ line_min = None
1538
+
1539
+ for line in ax.get_lines():
1540
+ xy = line.get_xydata()
1541
+ dist = np.linalg.norm(xy - np.array([x,y]), axis=1)
1542
+ idx_min = np.argmin(dist)
1543
+ if dist[idx_min] < dist_min:
1544
+ dist_min = dist[idx_min]
1545
+ line_min = line
1546
+
1547
+ self._ax_current.SetSelection(idx)
1548
+ self._fill_lines_ax(idx = ax.get_lines().index(line_min))
1549
+ self._axes_properties[idx].select_line(ax.get_lines().index(line_min))
1550
+ self.fill_grid_with_xy(line_min)
1551
+
1552
+ self.show_curline_properties()
1553
+
1554
+ def fill_grid_with_xy(self, line:Line2D= None, grid:CpGrid= None, colx:int= 0, coly:int= 1):
1555
+
1556
+ if line is None:
1557
+ line = self.cur_line
1558
+
1559
+ if grid is None:
1560
+ grid = self._xls
1561
+
1562
+ xy = line.get_xydata()
1563
+
1564
+ grid.ClearGrid()
1565
+
1566
+ if grid.GetNumberRows() < len(xy):
1567
+ grid.AppendRows(len(xy)-grid.GetNumberRows())
1568
+ elif grid.GetNumberRows() > len(xy):
1569
+ grid.DeleteRows(len(xy), grid.GetNumberRows()-len(xy))
1570
+
1571
+ for i in range(len(xy)):
1572
+ grid.SetCellValue(i, colx, str(xy[i,0]))
1573
+ grid.SetCellValue(i, coly, str(xy[i,1]))
1574
+
1575
+ def update_line_from_grid(self, event):
1576
+
1577
+ line = self.cur_line
1578
+
1579
+ #count not null values
1580
+ n = 0
1581
+ for i in range(self._xls.GetNumberRows()):
1582
+ if self._xls.GetCellValue(i, 0) != '' and self._xls.GetCellValue(i, 1) != '':
1583
+ n += 1
1584
+
1585
+ xy = np.zeros((n, 2))
1586
+
1587
+ for i in range(n):
1588
+ xy[i,0] = float(self._xls.GetCellValue(i, 0))
1589
+ xy[i,1] = float(self._xls.GetCellValue(i, 1))
1590
+
1591
+ line.set_data(xy[:,0], xy[:,1])
1592
+ self.update_layout()
1593
+
1594
+ def add_row_to_grid(self, event):
1595
+
1596
+ dlg = wx.TextEntryDialog(self, 'Number of rows to add', 'Add rows', '1')
1597
+ dlg.ShowModal()
1598
+
1599
+ try:
1600
+ n = int(dlg.GetValue())
1601
+ except:
1602
+ n = 1
1603
+
1604
+ self._xls.AppendRows(n)
1605
+
1606
+ def onadd_line(self, event):
1607
+ """ Add a plot to the current ax """
1608
+
1609
+ xy = self._get_xy_from_grid(self._xls)
1610
+ self.add_line(xy, self.cur_ax)
1611
+
1612
+ def _get_xy_from_grid(self, grid:CpGrid, colx:int= 0, coly:int= 1):
1613
+ """ Get the xy from a grid """
1614
+
1615
+ #Searching xy in the grid
1616
+ #count not null values
1617
+ n = 0
1618
+ for i in range(grid.GetNumberRows()):
1619
+ if grid.GetCellValue(i, colx) != '' and grid.GetCellValue(i, coly) != '':
1620
+ n += 1
1621
+
1622
+ xy = np.zeros((n, 2))
1623
+
1624
+ for i in range(n):
1625
+ xy[i,0] = float(grid.GetCellValue(i, colx))
1626
+ xy[i,1] = float(grid.GetCellValue(i, coly))
1627
+
1628
+ return xy
1629
+
1630
+ def add_line(self, xy:np.ndarray, ax:Axes=None, **kwargs):
1631
+ """ Add a plot to the current ax """
1632
+
1633
+ ax, idx_ax = self.get_ax_idx(ax)
1634
+
1635
+ ax.plot(xy[:,0], xy[:,1], **kwargs)
1636
+
1637
+ cur_ax_prop:Matplotlib_ax_properties = self._axes_properties[idx_ax]
1638
+ cur_ax_prop._lines.append(Matplolib_line_properties(ax.get_lines()[-1], cur_ax_prop))
1639
+ cur_ax_prop._lines[-1].add_props_to_sizer(self._collaps_pane.GetPane(), self._sizer_grid_props)
1640
+ self.update_layout()
1641
+
1642
+ def ondel_line(self, event):
1643
+ """ Remove a plot from the current ax """
1644
+
1645
+ dlg = wx.MessageDialog(self, _('Do you want to remove the selected line?\n\nSuch action is irrevocable !\n\nPlease consider to set "Visible" to "False" to hide data'), _('Remove line'), wx.YES_NO | wx.ICON_QUESTION | wx.NO_DEFAULT)
1646
+
1647
+ ret = dlg.ShowModal()
1648
+ if ret == wx.ID_NO:
1649
+ return
1650
+
1651
+ if self._line_current.GetSelection() == -1:
1652
+ return
1653
+
1654
+ idx = self._line_current.GetSelection()
1655
+ self.del_line(idx)
1656
+
1657
+ def del_line(self, idx:int):
1658
+ """ Delete a line """
1659
+
1660
+ self.cur_ax_properties.del_line(idx)
1661
+ self.update_layout()
1662
+
1663
+ def get_ax_idx(self, key:str | int | Axes= None) -> Axes:
1664
+
1665
+ if key is None:
1666
+ return self.cur_ax, self._ax_current.GetSelection()
1667
+
1668
+ if isinstance(key, str):
1669
+ if key in self.ax_dict:
1670
+ return self.ax_dict[key], list(self.ax_dict.keys()).index(key)
1671
+ else:
1672
+ logging.warning('Key not found')
1673
+ return None
1674
+ elif isinstance(key, int):
1675
+ if key >= 0 and key < len(self.ax):
1676
+ return self.ax[key], key
1677
+ else:
1678
+ logging.warning('Index out of range')
1679
+ return None
1680
+ elif isinstance(key, Axes):
1681
+ return key, list(self.ax_dict.values()).index(key)
1682
+
1683
+ def plot(self, x:np.ndarray, y:np.ndarray, ax:Axes | int | str= None, **kwargs):
1684
+
1685
+ ax, idx_ax = self.get_ax_idx(ax)
1686
+
1687
+ ax.plot(x, y, **kwargs)
1688
+
1689
+ new_props = Matplolib_line_properties(ax.get_lines()[-1], self._axes_properties[idx_ax])
1690
+
1691
+ if self.wx_exists:
1692
+ new_props.add_props_to_sizer(self._collaps_pane.GetPane(), self._sizer_grid_props)
1693
+
1694
+ ax_prop:Matplotlib_ax_properties = self._axes_properties[idx_ax]
1695
+ ax_prop._lines.append(new_props)
1696
+ ax_prop.get_properties()
1697
+
1698
+ if self.wx_exists:
1699
+ if ax == self.cur_ax:
1700
+ self._line_current.SetItems([line.get_label() for line in ax.get_lines()])
1701
+ self._line_current.SetSelection(len(ax.get_lines())-1)
1702
+
1703
+ self.fig.tight_layout()
1704
+ self.update_layout()
1705
+
1706
+ def to_dict(self) -> dict:
1707
+ """ properties to dict """
1708
+
1709
+ ret = {}
1710
+ if self.wx_exists:
1711
+ ret['frame_name'] = self.GetName()
1712
+ ret['frame_size_x'] = self.GetSize()[0]
1713
+ ret['frame_size_y'] = self.GetSize()[1]
1714
+
1715
+ ret['layout'] = self._layout
1716
+ ret['fig'] = self._fig_properties.to_dict()
1717
+ ret['axes'] = [ax.to_dict() for ax in self._axes_properties]
1718
+
1719
+ return ret
1720
+
1721
+ def from_dict(self, props:dict):
1722
+ """ properties from dict """
1723
+
1724
+ if 'layout' not in props:
1725
+ logging.error('No layout found in properties')
1726
+ return
1727
+
1728
+ self.apply_layout(props['layout'])
1729
+ self._fig_properties.from_dict(props['fig'])
1730
+ for ax_props, ax in zip(props['axes'], self._axes_properties):
1731
+ ax:Matplotlib_ax_properties
1732
+ ax.from_dict(ax_props)
1733
+
1734
+
1735
+ if self.wx_exists:
1736
+ for ax_props, ax in zip(props['axes'], self._axes_properties):
1737
+ for line in ax._lines:
1738
+ line.add_props_to_sizer(self._collaps_pane.GetPane(), self._sizer_grid_props)
1739
+
1740
+ if 'frame_name' in props:
1741
+ self.SetName(props['frame_name'])
1742
+ if 'frame_size_x' in props and 'frame_size_y' in props:
1743
+ self.SetSize(props['frame_size_x'], props['frame_size_y'])
1744
+
1745
+ self.Layout()
1746
+
1747
+ return self
1748
+
1749
+ def serialize(self):
1750
+ """ Serialize the properties """
1751
+
1752
+ return json.dumps(self.to_dict(), indent=4)
1753
+
1754
+ def deserialize(self, props:str):
1755
+ """ Deserialize the properties """
1756
+
1757
+ self.from_dict(json.loads(props))
1758
+
1759
+ def save(self, filename:str):
1760
+
1761
+ with open(filename, 'w') as f:
1762
+ f.write(self.serialize())
1763
+
1764
+ def load(self, filename:str):
1765
+
1766
+ with open(filename, 'r') as f:
1767
+ self.deserialize(f.read())
1768
+
1769
+ def save_image(self, filename:str, dpi:int= 100):
1770
+
1771
+ self.fig.savefig(filename, dpi= dpi)
1772
+
1773
+ def set_x_bounds(self, xmin:float, xmax:float, ax:Axes | int | str= None):
1774
+
1775
+ ax, idx_ax = self.get_ax_idx(ax)
1776
+
1777
+ ax.set_xlim(xmin, xmax)
1778
+ self._axes_properties[idx_ax].get_properties()
1779
+
1780
+ self.fig.tight_layout()
1781
+ self._canvas.draw()
1782
+
1783
+ def set_y_bounds(self, ymin:float, ymax:float, ax:Axes | int | str= None):
1784
+
1785
+ ax, idx_ax = self.get_ax_idx(ax)
1786
+
1787
+ ax.set_ylim(ymin, ymax)
1788
+ self._axes_properties[idx_ax].get_properties()
1789
+
1790
+ self.fig.tight_layout()
1791
+ self._canvas.draw()
1792
+ class Matplotlib_figure_properties():
1793
+
1794
+ def __init__(self, parent:Matplotlib_Figure = None, fig:Figure = None) -> None:
1795
+
1796
+ self.wx_exists = wx.App.Get() is not None
1797
+
1798
+ self.parent = parent
1799
+ self._myprops = None
1800
+ self._fig:Figure = None
1801
+ self._axes = None
1802
+
1803
+ self.title = 'Figure'
1804
+ self.size_width = 8
1805
+ self.size_height = 6
1806
+ self.dpi = 100
1807
+ self._filename = None
1808
+
1809
+ self.set_fig(fig)
1810
+ self._set_props()
1811
+
1812
+ def set_fig(self, fig:Figure):
1813
+
1814
+ self._fig = fig
1815
+
1816
+ if fig is None:
1817
+ return
1818
+
1819
+ self._axes:list[Matplotlib_ax_properties] = [Matplotlib_ax_properties(ax) for ax in fig.get_axes()]
1820
+ self.get_properties()
1821
+
1822
+ return self
1823
+
1824
+ def _set_props(self):
1825
+ """ Set the properties UI """
1826
+
1827
+ if self._myprops is not None:
1828
+ return
1829
+
1830
+ self._myprops = Wolf_Param(title='Figure properties', w= 500, h= 400, to_read= False, ontop= False, init_GUI= False)
1831
+
1832
+ self._myprops.set_callbacks(None, self.destroyprop)
1833
+
1834
+ # self._myprops.hide_selected_buttons()
1835
+
1836
+ self._myprops.addparam('Draw','Title',self.title,'String','SupTitle of the figure')
1837
+ self._myprops.addparam('Draw','Width',self.size_width,'Float','Width in inches')
1838
+ self._myprops.addparam('Draw','Height',self.size_height,'Float','Height in inches')
1839
+ self._myprops.addparam('Draw','DPI',self.dpi,'Integer','DPI - Dots per inch')
1840
+ self._myprops.addparam('Draw','Filename',self._filename,'File','Filename')
1841
+
1842
+ self._myprops.Populate()
1843
+
1844
+ # self._myprops.Layout()
1845
+ # self._myprops.SetSizeHints(500,500)
1846
+
1847
+ def populate(self):
1848
+ """ Populate the properties UI """
1849
+
1850
+ if self._myprops is None:
1851
+ self._set_props()
1852
+
1853
+ self._myprops[('Draw','Title')] = self.title
1854
+ self._myprops[('Draw','Width')] = self.size_width
1855
+ self._myprops[('Draw','Height')] = self.size_height
1856
+ self._myprops[('Draw','DPI')] = self.dpi
1857
+ self._myprops[('Draw','Filename')] = self._filename
1858
+
1859
+ self._myprops.Populate()
1860
+
1861
+ def ui(self):
1862
+ """ Create the properties UI """
1863
+
1864
+ if not self.wx_exists:
1865
+ return
1866
+
1867
+ if self._myprops is not None:
1868
+ self._myprops.CenterOnScreen()
1869
+ self._myprops.Raise()
1870
+ self._myprops.Show()
1871
+ return
1872
+
1873
+ self._set_props()
1874
+ self._myprops.Show()
1875
+
1876
+ self._myprops.SetTitle(_('Figure properties'))
1877
+
1878
+ icon = wx.Icon()
1879
+ icon_path = Path(__file__).parent / "apps/wolf_logo2.bmp"
1880
+ icon.CopyFromBitmap(wx.Bitmap(str(icon_path), wx.BITMAP_TYPE_ANY))
1881
+ self._myprops.SetIcon(icon)
1882
+
1883
+ self._myprops.Center()
1884
+
1885
+ self._myprops.Raise()
1886
+
1887
+ def destroyprop(self):
1888
+ self._myprops=None
1889
+
1890
+ def fill_property(self):
1891
+
1892
+ if self._myprops is None:
1893
+ logging.warning('Properties UI not found')
1894
+ return
1895
+
1896
+ self._myprops.apply_changes_to_memory()
1897
+
1898
+ self.title = self._myprops[('Draw','Title')]
1899
+ self.size_width = self._myprops[('Draw','Width')]
1900
+ self.size_height = self._myprops[('Draw','Height')]
1901
+ self.dpi = self._myprops[('Draw','DPI')]
1902
+ self._filename = self._myprops[('Draw','Filename')]
1903
+
1904
+ self.set_properties()
1905
+
1906
+ def set_properties(self, fig:Figure = None):
1907
+
1908
+ if fig is None:
1909
+ fig = self._fig
1910
+
1911
+ if self.size_height == 0 or self.size_width == 0:
1912
+ logging.warning('Size is 0')
1913
+ return
1914
+
1915
+ fig.set_dpi(self.dpi)
1916
+ fig.set_size_inches(self.size_width, self.size_height)
1917
+ fig.suptitle(self.title)
1918
+ fig.tight_layout()
1919
+
1920
+ fig.canvas.draw()
1921
+
1922
+ self.get_properties()
1923
+
1924
+ def get_properties(self, fig:Figure = None):
1925
+
1926
+ if fig is None:
1927
+ fig = self._fig
1928
+
1929
+ self.title = ''
1930
+ self.size_width, self.size_height = fig.get_size_inches()
1931
+ self.dpi = fig.get_dpi()
1932
+
1933
+ def to_dict(self) -> str:
1934
+ """ properties to dict """
1935
+
1936
+ return {'title':self.title if self.title != 'Figure' else '',
1937
+ 'size_width':self.size_width,
1938
+ 'size_height':self.size_height,
1939
+ 'dpi':self.dpi}
1940
+
1941
+ def from_dict(self, props:dict):
1942
+ """ properties from dict """
1943
+
1944
+ keys = ['title', 'size_width', 'size_height', 'dpi']
1945
+
1946
+ for key in keys:
1947
+ try:
1948
+ setattr(self, key, props[key])
1949
+ except:
1950
+ logging.warning('Key not found in properties dict')
1951
+ pass
1952
+
1953
+ self.set_properties()
1954
+
1955
+ return self
1956
+
1957
+ def add_props_to_sizer(self, frame:wx.Frame, sizer:wx.BoxSizer):
1958
+ """ Add the properties to a sizer """
1959
+
1960
+ self._myprops.ensure_prop(frame, show_in_active_if_default=True, height=300)
1961
+ sizer.Add(self._myprops.prop, proportion= 1, flag= wx.EXPAND)
1962
+ self._myprops.prop.Hide()
1963
+
1964
+ for ax in self._axes:
1965
+ ax.add_props_to_sizer(frame, sizer)
1966
+
1967
+ pass
1968
+
1969
+ def show_props(self):
1970
+ """ Show the properties """
1971
+
1972
+ self._myprops.prop.Show()
1973
+
1974
+ def hide_props(self):
1975
+ """ Hide the properties """
1976
+
1977
+ self._myprops.prop.Hide()
1978
+
1979
+
1980
+ COLORS_MPL = ['b', 'g', 'r', 'c', 'm', 'y', 'k', 'w', 'blue', 'green', 'red', 'cyan', 'magenta', 'yellow', 'black', 'white', 'orange']