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
« 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
8from pyrocko.guts import Tuple, Float
9from pyrocko import plot
11from .config import PlotConfig
13guts_prefix = 'grond'
16def get_callbacks(obj):
17 try:
18 return obj.callbacks
19 except AttributeError:
20 return obj._callbacks
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)
29 return lims
32class NotEnoughSpace(Exception):
33 pass
36class SectionPlotConfig(PlotConfig):
38 size_cm = Tuple.T(
39 2, Float.T(), default=(20., 20.))
41 margins_em = Tuple.T(
42 4, Float.T(), default=(7., 5., 7., 5.))
44 separator_em = Float.T(default=1.0)
47class SectionPlot(object):
49 def __init__(self, config=None):
50 if config is None:
51 config = SectionPlotConfig()
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)
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)
64 self._view_limits = num.zeros((3, 2))
66 self._view_limits[:, :] = num.nan
68 self._update_geometry()
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)
75 self._cid_resize = fig.canvas.mpl_connect(
76 'resize_event', self.resize_handler)
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
86 self._lim_changed_depth = 0
88 def _connect(self, obj, sig, handler):
89 cid = get_callbacks(obj).connect(sig, handler)
90 self._disconnect_data.append((obj, cid))
92 def _disconnect_all(self):
93 for obj, cid in self._disconnect_data:
94 get_callbacks(obj).disconnect(cid)
96 self._fig.canvas.mpl_disconnect(self._cid_resize)
98 def dpi_changed_handler(self, fig):
99 self._update_geometry()
101 def resize_handler(self, event):
102 self._update_geometry()
104 def lim_changed_handler(self, axes):
105 self._lim_changed_depth += 1
106 if self._lim_changed_depth < 2:
107 self._update_layout()
109 self._lim_changed_depth -= 1
111 def _update_geometry(self):
112 w, h = self._fig.canvas.get_width_height()
113 p = self.get_pixels_factor()
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()
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)
127 @property
128 def separator(self):
129 return self.config.separator_em * self.config.font_size / self._pixels
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)
139 def point_to_axes_coords(self, axes, point):
140 x, y = point
141 aleft, abottom, awidth, aheight = axes.get_position().bounds
143 x_fig = x / self._width
144 y_fig = y / self._height
146 x_axes = (x_fig - aleft) / awidth
147 y_axes = (y_fig - abottom) / aheight
149 return (x_axes, y_axes)
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
158 def make_limits(self, lims):
159 a = plot.AutoScaler(space=0.05)
160 return a.make_scale(lims)[:2]
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
179 def set_xlim(self, xmin, xmax):
180 self._view_limits[0, :] = xmin, xmax
181 self._update_layout()
183 def set_ylim(self, ymin, ymax):
184 self._view_limits[1, :] = ymin, ymax
185 self._update_layout()
187 def set_zlim(self, zmin, zmax):
188 self._view_limits[2, :] = zmin, zmax
189 self._update_layout()
191 def _update_layout(self):
192 data_limits = self.get_data_limits()
194 limits = num.zeros((3, 2))
195 for i in range(3):
196 limits[i, :] = self.make_limits(data_limits[i, :])
198 mask = num.isfinite(self._view_limits)
199 limits[mask] = self._view_limits[mask]
201 deltas = limits[:, 1] - limits[:, 0]
203 data_w = deltas[0] + deltas[2]
204 data_h = deltas[1] + deltas[2]
206 ml, mt, mr, mb = self.margins
207 ms = self.separator
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
216 if fig_w_avail <= 0.0 or fig_h_avail <= 0.0:
217 raise NotEnoughSpace()
219 fig_r = fig_h_avail / fig_w_avail
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
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)
233 deltas = limits[:, 1] - limits[:, 0]
235 w1 = fig_w_avail * deltas[0] / data_expanded_w
236 w2 = fig_w_avail * deltas[2] / data_expanded_w
238 h1 = fig_h_avail * deltas[1] / data_expanded_h
239 h2 = fig_h_avail * deltas[2] / data_expanded_h
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]
245 axes_xy, axes_xz, axes_zy = self.axes_list
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')
254 def wcenter(rect):
255 return rect[0] + rect[2]*0.5
257 def hcenter(rect):
258 return rect[1] + rect[3]*0.5
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)])
269 scaler = plot.AutoScaler()
270 inc = scaler.make_scale(
271 [0, min(data_expanded_w, data_expanded_h)], override_mode='off')[2]
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)
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)
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)
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)
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))
304 @property
305 def fig(self):
306 return self._fig
308 @property
309 def axes_xy(self):
310 return self._axes_xy
312 @property
313 def axes_xz(self):
314 return self._axes_xz
316 @property
317 def axes_zy(self):
318 return self._axes_zy
320 @property
321 def axes_list(self):
322 return [
323 self._axes_xy, self._axes_xz, self._axes_zy]
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)
330 def close(self):
331 self._disconnect_all()
332 self._plt.close(self._fig)
334 def show(self):
335 self._plt.show()
337 def set_xlabel(self, s):
338 self._axes_xy.set_xlabel(s)
340 def set_ylabel(self, s):
341 self._axes_xy.set_ylabel(s)
343 def set_zlabel(self, s):
344 self._axes_xz.set_ylabel(s)
345 self._axes_zy.set_xlabel(s)