wolfhece 2.1.108__py3-none-any.whl → 2.1.110__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.
@@ -2,6 +2,7 @@ from matplotlib.backends.backend_wx import NavigationToolbar2Wx as NavigationToo
2
2
  from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
3
3
  from typing import Literal
4
4
  from matplotlib.figure import Figure
5
+ from matplotlib.axes import Axes
5
6
  import matplotlib.pyplot as plt
6
7
  from matplotlib.gridspec import GridSpec
7
8
  import numpy as np
@@ -11,6 +12,8 @@ from wolfhece.PyParams import Wolf_Param, new_json
11
12
  from wolfhece.PyTranslate import _
12
13
  from wolfhece.PyVertex import getRGBfromI
13
14
 
15
+ from PIL import Image, ImageOps
16
+
14
17
 
15
18
  import wx
16
19
  from matplotlib.backend_bases import KeyEvent, MouseEvent
@@ -55,7 +58,7 @@ def sanitize_fmt(fmt):
55
58
 
56
59
  class Matplotlib_ax_properties():
57
60
 
58
- def __init__(self, ax =None) -> None:
61
+ def __init__(self, ax:Axes =None) -> None:
59
62
 
60
63
  self._ax = ax
61
64
  self._myprops = None
@@ -64,29 +67,55 @@ class Matplotlib_ax_properties():
64
67
  self._tmp_line_prop:Matplolib_line_properties = None
65
68
  self._selected_line = -1
66
69
 
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'
70
+ if ax is None:
71
+ self.title = 'Figure'
72
+ self.xtitle = 'X [m]'
73
+ self.ytitle = 'Y [m]'
74
+ self.legend = False
75
+ self.xmin = -99999
76
+ self.xmax = -99999
77
+ self.ymin = -99999
78
+ self.ymax = -99999
79
+ self.gridx_major = False
80
+ self.gridy_major = False
81
+ self.gridx_minor = False
82
+ self.gridy_minor = False
83
+
84
+ self._equal_axis = 0
85
+ self.scaling_factor = 1.
86
+
87
+ self.ticks_x = 1.
88
+ self.ticks_y = 1.
89
+ self.ticks_label_x = 1.
90
+ self.ticks_label_y = 1.
91
+
92
+ self.format_x = '.2f'
93
+ self.format_y = '.2f'
94
+ else:
95
+ self.title = ax.get_title()
96
+ self.xtitle = ax.get_xlabel()
97
+ self.ytitle = ax.get_ylabel()
98
+ self.legend = ax.get_legend() is not None
99
+ self.xmin = -99999
100
+ self.xmax = -99999
101
+ self.ymin = -99999
102
+ self.ymax = -99999
103
+ self.gridx_major = False
104
+ self.gridy_major = False
105
+ self.gridx_minor = False
106
+ self.gridy_minor = False
107
+
108
+ aspect = ax.get_aspect()
109
+ self._equal_axis = 0 if aspect == 'auto' else 1 if aspect == 1. else 2
110
+ self.scaling_factor = aspect
111
+
112
+ self.ticks_x = ax.get_xticks()
113
+ self.ticks_y = ax.get_yticks
114
+ self.ticks_label_x = ax.get_xticklabels()
115
+ self.ticks_label_y = ax.get_yticklabels()
116
+
117
+ self.format_x = '.2f'
118
+ self.format_y = '.2f'
90
119
 
91
120
  self._set_props()
92
121
 
@@ -234,9 +263,10 @@ class Matplotlib_ax_properties():
234
263
  return
235
264
 
236
265
  lines = self._ax.get_lines()
266
+ img = self._ax.get_images()
237
267
 
238
- if len(lines) == 0:
239
- logging.warning('No lines found')
268
+ if len(lines) == 0 and len(img) == 0:
269
+ logging.warning('No lines/image found')
240
270
  return
241
271
 
242
272
  xmin = np.inf
@@ -253,6 +283,13 @@ class Matplotlib_ax_properties():
253
283
  ymin = min(ymin, np.min(y))
254
284
  ymax = max(ymax, np.max(y))
255
285
 
286
+ for im in img:
287
+ x = im.get_extent()
288
+ xmin = min(xmin, x[0])
289
+ xmax = max(xmax, x[1])
290
+ ymin = min(ymin, x[2])
291
+ ymax = max(ymax, x[3])
292
+
256
293
  return xmin, xmax, ymin, ymax
257
294
 
258
295
  def fill_property(self, verbosity= True):
@@ -346,8 +383,10 @@ class Matplotlib_ax_properties():
346
383
  ax.xaxis.grid(self.gridx_major)
347
384
  ax.yaxis.grid(self.gridy_major)
348
385
 
349
- ax.set_xticks(self.ticks_x, self.ticks_label_x)
350
- ax.set_yticks(self.ticks_y, self.ticks_label_y)
386
+ if len(self.ticks_x) <= 100:
387
+ ax.set_xticks(self.ticks_x, self.ticks_label_x)
388
+ if len(self.ticks_y) <= 100:
389
+ ax.set_yticks(self.ticks_y, self.ticks_label_y)
351
390
 
352
391
  if self.legend:
353
392
  update = any(line.update_legend for line in self._lines)
@@ -612,11 +651,62 @@ class Matplolib_line_properties():
612
651
 
613
652
  self.update_legend = False
614
653
 
654
+ self._scales = [1.0, 1.0]
655
+ self._origin_world = [0.0, 0.0]
656
+ self._origin_local = [0.0, 0.0]
657
+
615
658
  self._set_props()
616
659
 
617
660
  if self._line is not None:
618
661
  self.get_properties()
619
662
 
663
+ def get_xydata(self, two_columns:bool = False):
664
+ """ Get the xy data """
665
+
666
+ if self._line is None:
667
+ return None
668
+
669
+ if two_columns:
670
+ return (self._line.get_xdata() - self._origin_local[0]) * self._scales[0] + self._origin_world[0], \
671
+ (self._line.get_ydata() - self._origin_local[1]) * self._scales[1] + self._origin_world[1]
672
+ else:
673
+ return np.array([(self._line.get_xdata() - self._origin_local[0]) * self._scales[0] + self._origin_world[0],
674
+ (self._line.get_ydata() - self._origin_local[1]) * self._scales[1] + self._origin_world[1]]).T
675
+
676
+ def set_xydata(self, xy_data:np.ndarray):
677
+ """ Set the xy data """
678
+
679
+ if self._line is None:
680
+ return
681
+
682
+ self._line.set_xdata((xy_data[:,0] - self._origin_world[0]) / self._scales[0] + self._origin_local[0])
683
+ self._line.set_ydata((xy_data[:,1] - self._origin_world[1]) / self._scales[1] + self._origin_local[1])
684
+
685
+ @property
686
+ def xdata(self):
687
+ return self.get_xydata(two_columns= True)[0]
688
+
689
+ @property
690
+ def ydata(self):
691
+ return self.get_xydata(two_columns= True)[1]
692
+
693
+ @property
694
+ def xydata(self):
695
+ return self.get_xydata()
696
+
697
+ @xydata.setter
698
+ def xydata(self, value):
699
+
700
+ if not isinstance(value, np.ndarray):
701
+ logging.warning('xydata must be a numpy array')
702
+ return
703
+
704
+ if value.shape[1] != 2:
705
+ logging.warning('xydata must have 2 columns')
706
+ return
707
+
708
+ self.set_xydata(value)
709
+
620
710
  @property
621
711
  def ax_props(self):
622
712
  return self._ax_props
@@ -821,6 +911,14 @@ class Matplolib_line_properties():
821
911
  self._myprops.addparam('Picker', 'Picker', self.picker, 'Logical', 'Picker')
822
912
  self._myprops.addparam('Picker', 'Picker radius', self.picker_radius, 'Float', 'Picker radius')
823
913
 
914
+ self._myprops.addparam('Scales', 'X scale', self._scales[0], 'Float', 'X scale')
915
+ self._myprops.addparam('Scales', 'Y scale', self._scales[1], 'Float', 'Y scale')
916
+
917
+ self._myprops.addparam('Origin', 'X world', self._origin_world[0], 'Float', 'X origin into world')
918
+ self._myprops.addparam('Origin', 'Y world', self._origin_world[1], 'Float', 'Y origin into world')
919
+ self._myprops.addparam('Origin', 'X local', self._origin_local[0], 'Float', 'X origin into local references')
920
+ self._myprops.addparam('Origin', 'Y local', self._origin_local[1], 'Float', 'Y origin into local references')
921
+
824
922
  self._myprops.Populate()
825
923
  # self._myprops.Layout()
826
924
  # self._myprops.SetSizeHints(500,500)
@@ -848,6 +946,14 @@ class Matplolib_line_properties():
848
946
  self._myprops[('Picker', 'Picker')] = self.picker
849
947
  self._myprops[('Picker', 'Picker radius')] = self.picker_radius
850
948
 
949
+ self._myprops[('Scales', 'X scale')] = self._scales[0]
950
+ self._myprops[('Scales', 'Y scale')] = self._scales[1]
951
+
952
+ self._myprops[('Origin', 'X world')] = self._origin_world[0]
953
+ self._myprops[('Origin', 'Y world')] = self._origin_world[1]
954
+ self._myprops[('Origin', 'X local')] = self._origin_local[0]
955
+ self._myprops[('Origin', 'Y local')] = self._origin_local[1]
956
+
851
957
  self._myprops.Populate()
852
958
 
853
959
  def ui(self):
@@ -902,6 +1008,15 @@ class Matplolib_line_properties():
902
1008
  self.picker = self._myprops[('Picker', 'Picker')]
903
1009
  self.picker_radius = self._myprops[('Picker', 'Picker radius')]
904
1010
 
1011
+ self._scales[0] = self._myprops[('Scales', 'X scale')]
1012
+ self._scales[1] = self._myprops[('Scales', 'Y scale')]
1013
+
1014
+ self._origin_world[0] = self._myprops[('Origin', 'X world')]
1015
+ self._origin_world[1] = self._myprops[('Origin', 'Y world')]
1016
+
1017
+ self._origin_local[0] = self._myprops[('Origin', 'X local')]
1018
+ self._origin_local[1] = self._myprops[('Origin', 'Y local')]
1019
+
905
1020
  self.set_properties()
906
1021
 
907
1022
  def set_properties(self, line:Line2D = None):
@@ -946,13 +1061,19 @@ class Matplolib_line_properties():
946
1061
  def show_properties(self):
947
1062
  self.ui()
948
1063
 
1064
+ @property
1065
+ def has_world_transfer(self):
1066
+ return self._scales[0] != 1.0 or self._scales[1] != 1.0 or self._origin_world[0] != 0. or self._origin_world[1] != 0. or self._origin_local[0] != 0. or self._origin_local[1] != 0.
1067
+
949
1068
  def to_dict(self) -> str:
950
1069
  """ properties to dict """
951
1070
 
952
- xdata = self._line.get_xdata().tolist()
953
- ydata = self._line.get_ydata().tolist()
1071
+ # We need to store the local data
1072
+ xy = self._line.get_xydata()
1073
+ xdata = xy[:,0].tolist()
1074
+ ydata = xy[:,1].tolist()
954
1075
 
955
- return {'color':self.color,
1076
+ locdict = {'color':self.color,
956
1077
  'linewidth':self.linewidth,
957
1078
  'linestyle':self.linestyle,
958
1079
  'marker':self.marker,
@@ -967,12 +1088,31 @@ class Matplolib_line_properties():
967
1088
  'picker':self.picker,
968
1089
  'picker_radius':self.picker_radius,
969
1090
  'xdata':xdata,
970
- 'ydata':ydata}
1091
+ 'ydata':ydata,
1092
+ 'xscale':self._scales[0],
1093
+ 'yscale':self._scales[1],
1094
+ 'xorigin_world':self._origin_world[0],
1095
+ 'yorigin_world':self._origin_world[1],
1096
+ 'xorigin_local':self._origin_local[0],
1097
+ 'yorigin_local':self._origin_local[1]}
1098
+
1099
+ if self.has_world_transfer:
1100
+ world_xdata, world_ydata = self.get_xydata(two_columns = True)
1101
+ world_xdata = world_xdata.tolist()
1102
+ world_ydata = world_ydata.tolist()
1103
+
1104
+ locdict['world_xdata'] = world_xdata
1105
+ locdict['world_ydata'] = world_ydata
1106
+
1107
+ return locdict
971
1108
 
972
1109
  def from_dict(self, props:dict):
973
1110
  """ properties from dict """
974
1111
 
975
- keys = ['color', 'linewidth', 'linestyle', 'marker', 'markersize', 'alpha', 'label', 'markerfacecolor', 'markeredgecolor', 'markeredgewidth', 'visible', 'zorder', 'picker', 'picker_radius']
1112
+ keys = ['color', 'linewidth', 'linestyle', 'marker', 'markersize', 'alpha',
1113
+ 'label', 'markerfacecolor', 'markeredgecolor', 'markeredgewidth',
1114
+ 'visible', 'zorder', 'picker', 'picker_radius',
1115
+ 'xscale', 'yscale', 'xorigin_world', 'yorigin_world', 'xorigin_local', 'yorigin_local']
976
1116
 
977
1117
  for key in keys:
978
1118
  try:
@@ -981,11 +1121,63 @@ class Matplolib_line_properties():
981
1121
  logging.warning('Key not found in properties dict')
982
1122
  pass
983
1123
 
1124
+ # ATTENTION : The next 2 lines are done in the to_dict method of the axes
1125
+ # xydata = np.array([props['xdata'], props['ydata']]).T
1126
+ # self.set_xydata(xydata)
1127
+
984
1128
  self.populate()
985
1129
  self.set_properties()
986
1130
 
987
1131
  return self
988
1132
 
1133
+ @property
1134
+ def xscale(self):
1135
+ return self._scales[0]
1136
+
1137
+ @xscale.setter
1138
+ def xscale(self, value):
1139
+ self._scales[0] = value
1140
+
1141
+ @property
1142
+ def yscale(self):
1143
+ return self._scales[1]
1144
+
1145
+ @yscale.setter
1146
+ def yscale(self, value):
1147
+ self._scales[1] = value
1148
+
1149
+ @property
1150
+ def xorigin_world(self):
1151
+ return self._origin_world[0]
1152
+
1153
+ @xorigin_world.setter
1154
+ def xorigin_world(self, value):
1155
+ self._origin_world[0] = value
1156
+
1157
+ @property
1158
+ def yorigin_world(self):
1159
+ return self._origin_world[1]
1160
+
1161
+ @yorigin_world.setter
1162
+ def yorigin_world(self, value):
1163
+ self._origin_world[1] = value
1164
+
1165
+ @property
1166
+ def xorigin_local(self):
1167
+ return self._origin_local[0]
1168
+
1169
+ @xorigin_local.setter
1170
+ def xorigin_local(self, value):
1171
+ self._origin_local[0] = value
1172
+
1173
+ @property
1174
+ def yorigin_local(self):
1175
+ return self._origin_local[1]
1176
+
1177
+ @yorigin_local.setter
1178
+ def yorigin_local(self, value):
1179
+ self._origin_local[1] = value
1180
+
989
1181
  def add_props_to_sizer(self, frame:wx.Frame, sizer:wx.BoxSizer):
990
1182
  """ Add the properties to a sizer """
991
1183
 
@@ -1013,8 +1205,9 @@ class Matplolib_line_properties():
1013
1205
  self._line = None
1014
1206
 
1015
1207
  class PRESET_LAYOUTS(Enum):
1016
- DEFAULT = (1,1)
1017
- MAT2X2 = (2,2)
1208
+ DEFAULT = (1,1, 'auto')
1209
+ MAT2X2 = (2,2, 'auto')
1210
+ DEFAULT_EQUAL = (1,1, 'equal')
1018
1211
  class Matplotlib_Figure(wx.Frame):
1019
1212
  """ Matplotlib Figure with wx Frame """
1020
1213
 
@@ -1043,7 +1236,8 @@ class Matplotlib_Figure(wx.Frame):
1043
1236
 
1044
1237
  self.wx_exists = wx.App.Get() is not None
1045
1238
 
1046
- self.fig = plt.figure()
1239
+ self.fig = Figure()
1240
+ # self.fig.set_visible(False)
1047
1241
  dpi = self.fig.get_dpi()
1048
1242
  size_x, size_y = self.fig.get_size_inches()
1049
1243
 
@@ -1053,10 +1247,22 @@ class Matplotlib_Figure(wx.Frame):
1053
1247
  self.ax_dict:dict[str,Axes] = {} # dict of axes
1054
1248
  self.ax:list[Axes] = [] # list of axes -- always flatten
1055
1249
  self.shown_props = None # shown properties
1250
+ self._shiftdown = False
1251
+
1252
+ self._action = None
1253
+ self._keep_first_point = True
1056
1254
 
1057
1255
  self.apply_layout(layout) # apply the layout
1058
1256
  pass
1059
1257
 
1258
+ @property
1259
+ def action(self):
1260
+ return self._action
1261
+
1262
+ @action.setter
1263
+ def action(self, value):
1264
+ self._action = value
1265
+
1060
1266
  def presets(self, which:PRESET_LAYOUTS = PRESET_LAYOUTS.DEFAULT):
1061
1267
  """ Presets """
1062
1268
 
@@ -1102,12 +1308,12 @@ class Matplotlib_Figure(wx.Frame):
1102
1308
  # store the axes in a list -- So we can access them by index, not only by name
1103
1309
  self.ax = [ax for ax in self.ax_dict.values()]
1104
1310
  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')
1311
+ # Tuple or list of 3 elements - subplots
1312
+ if len(layout) != 3:
1313
+ logging.warning('Layout must be a tuple or a list of 3 elements (nbrows:int, nbcols:int, aspect_ratio:str|float)')
1108
1314
  return
1109
1315
 
1110
- self.nbrows, self.nbcols = layout
1316
+ self.nbrows, self.nbcols, ratio = layout
1111
1317
  if self.nbrows*self.nbcols == 1:
1112
1318
  # Convert to list -- subplots returns a single Axes but we want a list
1113
1319
  self.ax = [self.fig.subplots(self.nbrows, self.nbcols)]
@@ -1115,6 +1321,14 @@ class Matplotlib_Figure(wx.Frame):
1115
1321
  # Flatten the axes -- sbplots returns a 2D array of Axes but we want a list
1116
1322
  self.ax = self.fig.subplots(self.nbrows, self.nbcols).flatten()
1117
1323
 
1324
+ for curax in self.ax:
1325
+ if ratio == 'auto':
1326
+ curax.set_aspect('auto')
1327
+ elif ratio == 'equal':
1328
+ curax.set_aspect('equal')
1329
+ else:
1330
+ curax.set_aspect(ratio)
1331
+
1118
1332
  # store the axes in a dict -- So we can access them by name, not only by index
1119
1333
  self.ax_dict = {f'{i}':ax for i, ax in enumerate(self.ax)}
1120
1334
  for key,ax in self.ax_dict.items():
@@ -1192,6 +1406,7 @@ class Matplotlib_Figure(wx.Frame):
1192
1406
  self._canvas.mpl_connect('motion_notify_event', self.UpdateStatusBar)
1193
1407
  self._canvas.mpl_connect('button_press_event', self.OnClickCanvas)
1194
1408
  self._canvas.mpl_connect('key_press_event', self.OnKeyCanvas)
1409
+ self._canvas.mpl_connect('key_release_event', self.OnKeyRelease)
1195
1410
 
1196
1411
  # Toolbar - Matplotlib
1197
1412
  # --------------------
@@ -1280,6 +1495,9 @@ class Matplotlib_Figure(wx.Frame):
1280
1495
  self._add_row = wx.Button(win, -1, 'Add rows')
1281
1496
  self._add_row.Bind(wx.EVT_BUTTON, self.add_row_to_grid)
1282
1497
 
1498
+ self._new_line = wx.Button(win, -1, 'New line')
1499
+ self._new_line.Bind(wx.EVT_BUTTON, self.onnew_line)
1500
+
1283
1501
  self._add_line = wx.Button(win, -1, 'Add line')
1284
1502
  self._add_line.Bind(wx.EVT_BUTTON, self.onadd_line)
1285
1503
 
@@ -1287,10 +1505,19 @@ class Matplotlib_Figure(wx.Frame):
1287
1505
  self._del_line.Bind(wx.EVT_BUTTON, self.ondel_line)
1288
1506
 
1289
1507
  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)
1508
+
1509
+ self._sizer_update_add = wx.BoxSizer(wx.HORIZONTAL)
1510
+ self._sizer_add_remove = wx.BoxSizer(wx.HORIZONTAL)
1511
+
1512
+ self._sizer_xls.Add(self._sizer_update_add, 0, wx.EXPAND)
1513
+ self._sizer_xls.Add(self._sizer_add_remove, 0, wx.EXPAND)
1514
+
1515
+ self._sizer_update_add.Add(self._update_xy, 1, wx.EXPAND)
1516
+ self._sizer_update_add.Add(self._add_row, 1, wx.EXPAND)
1517
+
1518
+ self._sizer_add_remove.Add(self._new_line, 1, wx.EXPAND)
1519
+ self._sizer_add_remove.Add(self._add_line, 1, wx.EXPAND)
1520
+ self._sizer_add_remove.Add(self._del_line, 1, wx.EXPAND)
1294
1521
 
1295
1522
  # Properties sizer
1296
1523
  # ---------------
@@ -1396,7 +1623,11 @@ class Matplotlib_Figure(wx.Frame):
1396
1623
 
1397
1624
  @property
1398
1625
  def cur_line(self) -> Line2D:
1399
- return self.cur_ax.get_lines()[int(self._line_current.GetSelection())]
1626
+
1627
+ if self._line_current.GetSelection() == -1:
1628
+ return None
1629
+ else:
1630
+ return self.cur_ax.get_lines()[int(self._line_current.GetSelection())]
1400
1631
 
1401
1632
  def get_figax(self):
1402
1633
 
@@ -1447,6 +1678,10 @@ class Matplotlib_Figure(wx.Frame):
1447
1678
  def show_curline_properties(self):
1448
1679
  # self.cur_line_properties.ui()
1449
1680
  self._hide_all_props()
1681
+
1682
+ if self._line_current.GetSelection() == -1:
1683
+ return
1684
+
1450
1685
  self.cur_line_properties.show_props()
1451
1686
  # self.Layout()
1452
1687
  self._sizer_grid_props.Layout()
@@ -1519,13 +1754,25 @@ class Matplotlib_Figure(wx.Frame):
1519
1754
 
1520
1755
  if event.key == 'escape':
1521
1756
  self._axes_properties[int(self._ax_current.GetSelection())].reset_selection()
1757
+ elif event.key == 'shift':
1758
+ self._shiftdown = True
1759
+ elif event.key == 'enter':
1760
+ if self.action is not None:
1761
+ action, callback = self.action
1762
+ if action == 'Digitize':
1763
+ callback((0,0), 'End Digitize')
1764
+
1765
+ def OnKeyRelease(self, event:KeyEvent):
1766
+ if event.key == 'shift':
1767
+ self._shiftdown = False
1522
1768
 
1523
1769
  def OnClickCanvas(self, event:MouseEvent):
1524
1770
 
1525
1771
  rclick = event.button == 3
1526
1772
  lclick = event.button == 1
1773
+ middle = event.button == 2
1527
1774
 
1528
- if not rclick:
1775
+ if not (rclick or middle):
1529
1776
  return
1530
1777
 
1531
1778
  if event.inaxes:
@@ -1533,21 +1780,81 @@ class Matplotlib_Figure(wx.Frame):
1533
1780
  idx= ax.get_figure().axes.index(event.inaxes)
1534
1781
  x, y = event.xdata, event.ydata
1535
1782
 
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)
1783
+ if middle:
1784
+ if self.action is not None:
1785
+ action, callback = self.action
1786
+ if action == 'Digitize':
1787
+ callback((0,0), 'End Digitize')
1788
+
1789
+ if rclick:
1790
+
1791
+ if self._shiftdown:
1792
+ # add a point to the current line, update the grid and plot
1793
+ xy = self.cur_line.get_xydata()
1794
+
1795
+ if xy.shape[0] == 1:
1796
+ if self._keep_first_point:
1797
+ xy = np.vstack((xy, [x,y]))
1798
+ else:
1799
+ xy = np.asarray([[x,y]])
1800
+ self._keep_first_point = True
1801
+ else:
1802
+ xy = np.vstack((xy, [x,y]))
1803
+
1804
+ self.cur_line.set_data(xy[:,0], xy[:,1])
1805
+ self.fill_grid_with_xy_np(self.cur_line_properties.get_xydata())
1806
+ self._canvas.draw()
1807
+
1808
+ if self.action is not None:
1809
+
1810
+ action, callback = self.action
1811
+
1812
+ if action == 'Digitize':
1813
+ if not self._shiftdown:
1814
+ logging.warning('Shift must be down to digitize')
1815
+ elif action == 'Ref X':
1816
+ if not self._shiftdown:
1817
+ logging.warning('Shift must be down to set X reference')
1818
+ else:
1819
+ callback((x,y), action)
1820
+ elif action == 'Ref Y':
1821
+ if not self._shiftdown:
1822
+ logging.warning('Shift must be down to set Y reference')
1823
+ else:
1824
+ callback((x,y), action)
1825
+ elif action == 'Origin':
1826
+ if not self._shiftdown:
1827
+ logging.warning('Shift must be down to set origin')
1828
+ else:
1829
+ callback((x,y), action)
1830
+
1831
+ elif not self._shiftdown:
1832
+ # Find the closest line and select it
1833
+
1834
+ lines = ax.get_lines()
1835
+ if len(lines) == 0:
1836
+ logging.warning('No lines !')
1837
+ return
1838
+
1839
+ dist_min = 1e6
1840
+ line_min = None
1841
+
1842
+ for line in lines:
1843
+ xy = line.get_xydata()
1844
+ if xy.shape[0] == 0:
1845
+ continue
1846
+
1847
+ dist = np.linalg.norm(xy - np.array([x,y]), axis=1)
1848
+ idx_min = np.argmin(dist)
1849
+ if dist[idx_min] < dist_min:
1850
+ dist_min = dist[idx_min]
1851
+ line_min = line
1852
+
1853
+ self._ax_current.SetSelection(idx)
1854
+ self._fill_lines_ax(idx = lines.index(line_min))
1855
+ self._axes_properties[idx].select_line(lines.index(line_min))
1856
+
1857
+ self.fill_grid_with_xy_np(self.cur_line_properties.get_xydata())
1551
1858
 
1552
1859
  self.show_curline_properties()
1553
1860
 
@@ -1572,6 +1879,27 @@ class Matplotlib_Figure(wx.Frame):
1572
1879
  grid.SetCellValue(i, colx, str(xy[i,0]))
1573
1880
  grid.SetCellValue(i, coly, str(xy[i,1]))
1574
1881
 
1882
+ def fill_grid_with_xy_np(self, xy:np.ndarray, grid:CpGrid = None, colx:int= 0, coly:int= 1):
1883
+
1884
+ if grid is None:
1885
+ grid = self._xls
1886
+
1887
+ grid.ClearGrid()
1888
+
1889
+ if grid.GetNumberRows() < len(xy):
1890
+ grid.AppendRows(len(xy)-grid.GetNumberRows())
1891
+ elif grid.GetNumberRows() > len(xy):
1892
+ grid.DeleteRows(len(xy), grid.GetNumberRows()-len(xy))
1893
+
1894
+ for i in range(len(xy)):
1895
+ grid.SetCellValue(i, colx, str(xy[i,0]))
1896
+ grid.SetCellValue(i, coly, str(xy[i,1]))
1897
+
1898
+ def get_xy_from_grid(self):
1899
+ """ Get the xy from the grid """
1900
+
1901
+ return self._get_xy_from_grid(self._xls)
1902
+
1575
1903
  def update_line_from_grid(self, event):
1576
1904
 
1577
1905
  line = self.cur_line
@@ -1588,7 +1916,8 @@ class Matplotlib_Figure(wx.Frame):
1588
1916
  xy[i,0] = float(self._xls.GetCellValue(i, 0))
1589
1917
  xy[i,1] = float(self._xls.GetCellValue(i, 1))
1590
1918
 
1591
- line.set_data(xy[:,0], xy[:,1])
1919
+ self.cur_line_properties.set_xydata(xy)
1920
+ self._canvas.draw()
1592
1921
  self.update_layout()
1593
1922
 
1594
1923
  def add_row_to_grid(self, event):
@@ -1609,6 +1938,23 @@ class Matplotlib_Figure(wx.Frame):
1609
1938
  xy = self._get_xy_from_grid(self._xls)
1610
1939
  self.add_line(xy, self.cur_ax)
1611
1940
 
1941
+ def onnew_line(self, event):
1942
+ """ Add a plot to the current ax """
1943
+ self.new_line()
1944
+
1945
+ def new_line(self, ax:Axes=None, **kwargs) -> Matplolib_line_properties:
1946
+ """ Add a plot to the current ax """
1947
+
1948
+ curline = self.cur_line
1949
+ if curline is not None:
1950
+ xy = curline.get_xydata()
1951
+ xy = np.asarray([[xy[0,0],xy[0,1]]])
1952
+ else:
1953
+ xy = np.asarray([[0,0]])
1954
+
1955
+ self._keep_first_point = False
1956
+ return self.add_line(xy, ax, **kwargs)
1957
+
1612
1958
  def _get_xy_from_grid(self, grid:CpGrid, colx:int= 0, coly:int= 1):
1613
1959
  """ Get the xy from a grid """
1614
1960
 
@@ -1627,7 +1973,7 @@ class Matplotlib_Figure(wx.Frame):
1627
1973
 
1628
1974
  return xy
1629
1975
 
1630
- def add_line(self, xy:np.ndarray, ax:Axes=None, **kwargs):
1976
+ def add_line(self, xy:np.ndarray, ax:Axes=None, **kwargs) -> Matplolib_line_properties:
1631
1977
  """ Add a plot to the current ax """
1632
1978
 
1633
1979
  ax, idx_ax = self.get_ax_idx(ax)
@@ -1637,23 +1983,44 @@ class Matplotlib_Figure(wx.Frame):
1637
1983
  cur_ax_prop:Matplotlib_ax_properties = self._axes_properties[idx_ax]
1638
1984
  cur_ax_prop._lines.append(Matplolib_line_properties(ax.get_lines()[-1], cur_ax_prop))
1639
1985
  cur_ax_prop._lines[-1].add_props_to_sizer(self._collaps_pane.GetPane(), self._sizer_grid_props)
1986
+
1987
+ self._fill_lines_ax(len(ax.get_lines())-1)
1988
+
1640
1989
  self.update_layout()
1990
+ self._canvas.SetFocus()
1991
+
1992
+ return self.cur_ax_properties._lines[-1]
1993
+
1994
+ def add_image(self, image:np.ndarray | str, ax:Axes= None, **kwargs):
1995
+
1996
+ ax, idx_ax = self.get_ax_idx(ax)
1997
+
1998
+ if isinstance(image, str):
1999
+ image = ImageOps.flip(Image.open(image))
2000
+ ax.axis('off') # clear x-axis and y-axis
2001
+
2002
+ ax.imshow(image, **kwargs)
2003
+
2004
+ self.update_layout()
2005
+ self._canvas.SetFocus()
1641
2006
 
1642
2007
  def ondel_line(self, event):
1643
2008
  """ Remove a plot from the current ax """
1644
2009
 
2010
+ if self._line_current.GetSelection() == -1:
2011
+ return
2012
+
1645
2013
  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
2014
 
1647
2015
  ret = dlg.ShowModal()
1648
2016
  if ret == wx.ID_NO:
1649
2017
  return
1650
2018
 
1651
- if self._line_current.GetSelection() == -1:
1652
- return
1653
-
1654
2019
  idx = self._line_current.GetSelection()
1655
2020
  self.del_line(idx)
1656
2021
 
2022
+ self._fill_lines_ax()
2023
+
1657
2024
  def del_line(self, idx:int):
1658
2025
  """ Delete a line """
1659
2026