Coverage for /usr/local/lib/python3.11/dist-packages/grond/plot/section.py: 23%

228 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2025-04-03 09:31 +0000

1# https://pyrocko.org/grond - GPLv3 

2# 

3# The Grond Developers, 21st Century 

4import numpy as num 

5from matplotlib.axes import Axes 

6from matplotlib.ticker import MultipleLocator 

7 

8from pyrocko.guts import Tuple, Float 

9from pyrocko import plot 

10 

11from .config import PlotConfig 

12 

13guts_prefix = 'grond' 

14 

15 

16def get_callbacks(obj): 

17 try: 

18 return obj.callbacks 

19 except AttributeError: 

20 return obj._callbacks 

21 

22 

23def limits(points): 

24 lims = num.zeros((3, 2)) 

25 if points.size != 0: 

26 lims[:, 0] = num.min(points, axis=0) 

27 lims[:, 1] = num.max(points, axis=0) 

28 

29 return lims 

30 

31 

32class NotEnoughSpace(Exception): 

33 pass 

34 

35 

36class SectionPlotConfig(PlotConfig): 

37 

38 size_cm = Tuple.T( 

39 2, Float.T(), default=(20., 20.)) 

40 

41 margins_em = Tuple.T( 

42 4, Float.T(), default=(7., 5., 7., 5.)) 

43 

44 separator_em = Float.T(default=1.0) 

45 

46 

47class SectionPlot(object): 

48 

49 def __init__(self, config=None): 

50 if config is None: 

51 config = SectionPlotConfig() 

52 

53 self.config = config 

54 self._disconnect_data = [] 

55 self._width = self._height = self._pixels = None 

56 self._plt = plot.mpl_init(self.config.font_size) 

57 self._fig = fig = self._plt.figure(figsize=self.config.size_inch) 

58 

59 rect = [0., 0., 1., 1.] 

60 self._axes_xy = Axes(fig, rect) 

61 self._axes_xz = Axes(fig, rect) 

62 self._axes_zy = Axes(fig, rect) 

63 

64 self._view_limits = num.zeros((3, 2)) 

65 

66 self._view_limits[:, :] = num.nan 

67 

68 self._update_geometry() 

69 

70 for axes in self.axes_list: 

71 fig.add_axes(axes) 

72 self._connect(axes, 'xlim_changed', self.lim_changed_handler) 

73 self._connect(axes, 'ylim_changed', self.lim_changed_handler) 

74 

75 self._cid_resize = fig.canvas.mpl_connect( 

76 'resize_event', self.resize_handler) 

77 

78 try: 

79 self._connect(fig, 'dpi_changed', self.dpi_changed_handler) 

80 except ValueError: 

81 # 'dpi_changed' event has been removed in MPL 3.8. 

82 # canvas 'resize_event' may be sufficient but needs to be checked. 

83 # https://matplotlib.org/stable/api/prev_api_changes/api_changes_3.8.0.html#text-get-rotation 

84 pass 

85 

86 self._lim_changed_depth = 0 

87 

88 def _connect(self, obj, sig, handler): 

89 cid = get_callbacks(obj).connect(sig, handler) 

90 self._disconnect_data.append((obj, cid)) 

91 

92 def _disconnect_all(self): 

93 for obj, cid in self._disconnect_data: 

94 get_callbacks(obj).disconnect(cid) 

95 

96 self._fig.canvas.mpl_disconnect(self._cid_resize) 

97 

98 def dpi_changed_handler(self, fig): 

99 self._update_geometry() 

100 

101 def resize_handler(self, event): 

102 self._update_geometry() 

103 

104 def lim_changed_handler(self, axes): 

105 self._lim_changed_depth += 1 

106 if self._lim_changed_depth < 2: 

107 self._update_layout() 

108 

109 self._lim_changed_depth -= 1 

110 

111 def _update_geometry(self): 

112 w, h = self._fig.canvas.get_width_height() 

113 p = self.get_pixels_factor() 

114 

115 if (self._width, self._height, self._pixels) != (w, h, p): 

116 self._width = w 

117 self._height = h 

118 self._pixels = p 

119 self._update_layout() 

120 

121 @property 

122 def margins(self): 

123 return tuple( 

124 x * self.config.font_size / self._pixels 

125 for x in self.config.margins_em) 

126 

127 @property 

128 def separator(self): 

129 return self.config.separator_em * self.config.font_size / self._pixels 

130 

131 def rect_to_figure_coords(self, rect): 

132 left, bottom, width, height = rect 

133 return ( 

134 left / self._width, 

135 bottom / self._height, 

136 width / self._width, 

137 height / self._height) 

138 

139 def point_to_axes_coords(self, axes, point): 

140 x, y = point 

141 aleft, abottom, awidth, aheight = axes.get_position().bounds 

142 

143 x_fig = x / self._width 

144 y_fig = y / self._height 

145 

146 x_axes = (x_fig - aleft) / awidth 

147 y_axes = (y_fig - abottom) / aheight 

148 

149 return (x_axes, y_axes) 

150 

151 def get_pixels_factor(self): 

152 try: 

153 r = self._fig.canvas.get_renderer() 

154 return 1.0 / r.points_to_pixels(1.0) 

155 except AttributeError: 

156 return 1.0 

157 

158 def make_limits(self, lims): 

159 a = plot.AutoScaler(space=0.05) 

160 return a.make_scale(lims)[:2] 

161 

162 def get_data_limits(self): 

163 xs = [] 

164 ys = [] 

165 zs = [] 

166 xs.extend(self._axes_xy.get_xaxis().get_data_interval()) 

167 ys.extend(self._axes_xy.get_yaxis().get_data_interval()) 

168 xs.extend(self._axes_xz.get_xaxis().get_data_interval()) 

169 zs.extend(self._axes_xz.get_yaxis().get_data_interval()) 

170 zs.extend(self._axes_zy.get_xaxis().get_data_interval()) 

171 ys.extend(self._axes_zy.get_yaxis().get_data_interval()) 

172 lims = num.zeros((3, 2)) 

173 lims[0, :] = num.nanmin(xs), num.nanmax(xs) 

174 lims[1, :] = num.nanmin(ys), num.nanmax(ys) 

175 lims[2, :] = num.nanmin(zs), num.nanmax(zs) 

176 lims[num.logical_not(num.isfinite(lims))] = 0.0 

177 return lims 

178 

179 def set_xlim(self, xmin, xmax): 

180 self._view_limits[0, :] = xmin, xmax 

181 self._update_layout() 

182 

183 def set_ylim(self, ymin, ymax): 

184 self._view_limits[1, :] = ymin, ymax 

185 self._update_layout() 

186 

187 def set_zlim(self, zmin, zmax): 

188 self._view_limits[2, :] = zmin, zmax 

189 self._update_layout() 

190 

191 def _update_layout(self): 

192 data_limits = self.get_data_limits() 

193 

194 limits = num.zeros((3, 2)) 

195 for i in range(3): 

196 limits[i, :] = self.make_limits(data_limits[i, :]) 

197 

198 mask = num.isfinite(self._view_limits) 

199 limits[mask] = self._view_limits[mask] 

200 

201 deltas = limits[:, 1] - limits[:, 0] 

202 

203 data_w = deltas[0] + deltas[2] 

204 data_h = deltas[1] + deltas[2] 

205 

206 ml, mt, mr, mb = self.margins 

207 ms = self.separator 

208 

209 data_r = data_h / data_w 

210 em = self.config.font_size 

211 w = self._width 

212 h = self._height 

213 fig_w_avail = w - mr - ml - ms 

214 fig_h_avail = h - mt - mb - ms 

215 

216 if fig_w_avail <= 0.0 or fig_h_avail <= 0.0: 

217 raise NotEnoughSpace() 

218 

219 fig_r = fig_h_avail / fig_w_avail 

220 

221 if data_r < fig_r: 

222 data_expanded_h = data_w * fig_r 

223 data_expanded_w = data_w 

224 else: 

225 data_expanded_h = data_h 

226 data_expanded_w = data_h / fig_r 

227 

228 limits[0, 0] -= 0.5 * (data_expanded_w - data_w) 

229 limits[0, 1] += 0.5 * (data_expanded_w - data_w) 

230 limits[1, 0] -= 0.5 * (data_expanded_h - data_h) 

231 limits[1, 1] += 0.5 * (data_expanded_h - data_h) 

232 

233 deltas = limits[:, 1] - limits[:, 0] 

234 

235 w1 = fig_w_avail * deltas[0] / data_expanded_w 

236 w2 = fig_w_avail * deltas[2] / data_expanded_w 

237 

238 h1 = fig_h_avail * deltas[1] / data_expanded_h 

239 h2 = fig_h_avail * deltas[2] / data_expanded_h 

240 

241 rect_xy = [ml, mb+h2+ms, w1, h1] 

242 rect_xz = [ml, mb, w1, h2] 

243 rect_zy = [ml+w1+ms, mb+h2+ms, w2, h1] 

244 

245 axes_xy, axes_xz, axes_zy = self.axes_list 

246 

247 axes_xy.set_position( 

248 self.rect_to_figure_coords(rect_xy), which='both') 

249 axes_xz.set_position( 

250 self.rect_to_figure_coords(rect_xz), which='both') 

251 axes_zy.set_position( 

252 self.rect_to_figure_coords(rect_zy), which='both') 

253 

254 def wcenter(rect): 

255 return rect[0] + rect[2]*0.5 

256 

257 def hcenter(rect): 

258 return rect[1] + rect[3]*0.5 

259 

260 self.set_label_coords( 

261 axes_xy, 'x', [wcenter(rect_xy), h - 1.0*em]) 

262 self.set_label_coords( 

263 axes_xy, 'y', [2.0*em, hcenter(rect_xy)]) 

264 self.set_label_coords( 

265 axes_zy, 'x', [wcenter(rect_zy), h - 1.0*em]) 

266 self.set_label_coords( 

267 axes_xz, 'y', [2.0*em, hcenter(rect_xz)]) 

268 

269 scaler = plot.AutoScaler() 

270 inc = scaler.make_scale( 

271 [0, min(data_expanded_w, data_expanded_h)], override_mode='off')[2] 

272 

273 axes_xy.set_xlim(*limits[0, :]) 

274 axes_xy.set_ylim(*limits[1, :]) 

275 axes_xy.get_xaxis().set_tick_params( 

276 bottom=False, top=True, labelbottom=False, labeltop=True) 

277 axes_xy.get_yaxis().set_tick_params( 

278 left=True, labelleft=True, right=False, labelright=False) 

279 

280 axes_xz.set_xlim(*limits[0, :]) 

281 axes_xz.set_ylim(*limits[2, ::-1]) 

282 axes_xz.get_xaxis().set_tick_params( 

283 bottom=True, top=False, labelbottom=False, labeltop=False) 

284 axes_xz.get_yaxis().set_tick_params( 

285 left=True, labelleft=True, right=True, labelright=False) 

286 

287 axes_zy.set_xlim(*limits[2, :]) 

288 axes_zy.set_ylim(*limits[1, :]) 

289 axes_zy.get_xaxis().set_tick_params( 

290 bottom=True, top=True, labelbottom=False, labeltop=True) 

291 axes_zy.get_yaxis().set_tick_params( 

292 left=False, labelleft=False, right=True, labelright=False) 

293 

294 for axes in self.axes_list: 

295 tl = MultipleLocator(inc) 

296 axes.get_xaxis().set_major_locator(tl) 

297 tl = MultipleLocator(inc) 

298 axes.get_yaxis().set_major_locator(tl) 

299 

300 def set_label_coords(self, axes, which, point): 

301 axis = axes.get_xaxis() if which == 'x' else axes.get_yaxis() 

302 axis.set_label_coords(*self.point_to_axes_coords(axes, point)) 

303 

304 @property 

305 def fig(self): 

306 return self._fig 

307 

308 @property 

309 def axes_xy(self): 

310 return self._axes_xy 

311 

312 @property 

313 def axes_xz(self): 

314 return self._axes_xz 

315 

316 @property 

317 def axes_zy(self): 

318 return self._axes_zy 

319 

320 @property 

321 def axes_list(self): 

322 return [ 

323 self._axes_xy, self._axes_xz, self._axes_zy] 

324 

325 def plot(self, points, *args, **kwargs): 

326 self._axes_xy.plot(points[:, 0], points[:, 1], *args, **kwargs) 

327 self._axes_xz.plot(points[:, 0], points[:, 2], *args, **kwargs) 

328 self._axes_zy.plot(points[:, 2], points[:, 1], *args, **kwargs) 

329 

330 def close(self): 

331 self._disconnect_all() 

332 self._plt.close(self._fig) 

333 

334 def show(self): 

335 self._plt.show() 

336 

337 def set_xlabel(self, s): 

338 self._axes_xy.set_xlabel(s) 

339 

340 def set_ylabel(self, s): 

341 self._axes_xy.set_ylabel(s) 

342 

343 def set_zlabel(self, s): 

344 self._axes_xz.set_ylabel(s) 

345 self._axes_zy.set_xlabel(s)