Coverage for /usr/local/lib/python3.11/dist-packages/pyrocko/plot/smartplot.py: 74%
551 statements
« prev ^ index » next coverage.py v6.5.0, created at 2024-09-24 10:38 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2024-09-24 10:38 +0000
1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6'''
7Matplotlib plotting with some fancy extras.
8'''
10from collections import defaultdict
11import math
12import logging
14import numpy as num
15import matplotlib
16from matplotlib.axes import Axes
17# from matplotlib.ticker import MultipleLocator
18from matplotlib import cm, colors, colorbar, figure
20from pyrocko.guts import Tuple, Float, Object
21from pyrocko import plot
23import scipy.optimize
25logger = logging.getLogger('pyrocko.plot.smartplot')
27guts_prefix = 'pf'
29inch = 2.54
32def get_callbacks(obj):
33 try:
34 return obj.callbacks
35 except AttributeError:
36 return obj._callbacks
39class SmartplotAxes(Axes):
41 if matplotlib.__version__.split('.') < '3.6'.split('.'):
42 # Subclassing cla is deprecated on newer mpl but need this fallback for
43 # older versions. Code is duplicated because mpl behaviour depends
44 # on the existence of cla in the subclass...
45 def cla(self):
46 if hasattr(self, 'callbacks'):
47 callbacks = self.callbacks
48 Axes.cla(self)
49 self.callbacks = callbacks
50 else:
51 Axes.cla(self)
53 else:
54 def clear(self):
55 if hasattr(self, 'callbacks'):
56 callbacks = self.callbacks
57 Axes.clear(self)
58 self.callbacks = callbacks
59 elif hasattr(self, '_callbacks'):
60 callbacks = self._callbacks
61 Axes.clear(self)
62 self._callbacks = callbacks
63 else:
64 Axes.clear(self)
67class SmartplotFigure(figure.Figure):
69 def set_smartplot(self, plot):
70 self._smartplot = plot
72 def draw(self, *args, **kwargs):
73 if hasattr(self, '_smartplot'):
74 try:
75 self._smartplot._update_layout()
76 except NotEnoughSpace:
77 logger.error('Figure is too small to show the plot.')
78 return
80 return figure.Figure.draw(self, *args, **kwargs)
83def limits(points):
84 lims = num.zeros((3, 2))
85 if points.size != 0:
86 lims[:, 0] = num.min(points, axis=0)
87 lims[:, 1] = num.max(points, axis=0)
89 return lims
92def wcenter(rect):
93 return rect[0] + rect[2]*0.5
96def hcenter(rect):
97 return rect[1] + rect[3]*0.5
100def window_min(n, w, ml, mu, s, x):
101 return ml + x/float(n) * (w - (ml + mu + (n-1)*s)) + math.floor(x) * s
104def window_max(n, w, ml, mu, s, x):
105 return ml + x/float(n) * (w - (ml + mu + (n-1)*s)) + (math.floor(x)-1) * s
108def make_smap(cmap, norm=None):
109 if isinstance(norm, tuple):
110 norm = colors.Normalize(*norm, clip=False)
111 smap = cm.ScalarMappable(cmap=cmap, norm=norm)
112 smap._A = [] # not needed in newer versions of mpl?
113 return smap
116def solve_layout_fixed_panels(size, shape, limits, aspects, fracs=None):
118 weight_aspect = 1000.
120 sx, sy = size
121 nx, ny = shape
122 nvar = nx+ny
123 vxs, vys = limits
124 uxs = vxs[:, 1] - vxs[:, 0]
125 uys = vys[:, 1] - vys[:, 0]
126 aspects_xx, aspects_yy, aspects_xy = aspects
128 if fracs is None:
129 wxs = num.full(nx, sx / nx)
130 wys = num.full(ny, sy / ny)
131 else:
132 frac_x, frac_y = fracs
133 wxs = sx * frac_x / num.sum(frac_x)
134 wys = sy * frac_y / num.sum(frac_y)
136 data = []
137 weights = []
138 rows = []
139 bounds = []
140 for ix in range(nx):
141 u = uxs[ix]
142 assert u > 0.0
143 row = num.zeros(nvar)
144 row[ix] = u
145 rows.append(row)
146 data.append(wxs[ix])
147 weights.append(1.0 / u)
148 bounds.append((0, wxs[ix] / u))
150 for iy in range(ny):
151 u = uys[iy]
152 assert u > 0.0
153 row = num.zeros(nvar)
154 row[nx+iy] = u
155 rows.append(row)
156 data.append(wys[iy])
157 weights.append(1.0)
158 bounds.append((0, wys[iy] / u))
160 for ix1, ix2, aspect in aspects_xx:
161 row = num.zeros(nvar)
162 row[ix1] = aspect
163 row[ix2] = -1.0
164 weights.append(weight_aspect/aspect)
165 rows.append(row)
166 data.append(0.0)
168 for iy1, iy2, aspect in aspects_yy:
169 row = num.zeros(nvar)
170 row[nx+iy1] = aspect
171 row[nx+iy2] = -1.0
172 weights.append(weight_aspect/aspect)
173 rows.append(row)
174 data.append(0.0)
176 for ix, iy, aspect in aspects_xy:
177 row = num.zeros(nvar)
178 row[ix] = aspect
179 row[nx+iy] = -1.0
180 weights.append(weight_aspect/aspect)
181 rows.append(row)
182 data.append(0.0)
184 weights = num.array(weights)
185 data = num.array(data)
186 mat = num.vstack(rows) * weights[:, num.newaxis]
187 data *= weights
189 bounds = num.array(bounds).T
191 model = scipy.optimize.lsq_linear(mat, data, bounds).x
193 cxs = model[:nx]
194 cys = model[nx:nx+ny]
196 vlimits_x = num.zeros((nx, 2))
197 for ix in range(nx):
198 u = wxs[ix] / cxs[ix]
199 vmin, vmax = vxs[ix]
200 udata = vmax - vmin
201 eps = 1e-7 * u
202 assert udata <= u + eps
203 vlimits_x[ix, 0] = (vmin + vmax) / 2.0 - u / 2.0
204 vlimits_x[ix, 1] = (vmin + vmax) / 2.0 + u / 2.0
206 vlimits_y = num.zeros((ny, 2))
207 for iy in range(ny):
208 u = wys[iy] / cys[iy]
209 vmin, vmax = vys[iy]
210 udata = vmax - vmin
211 eps = 1e-7 * u
212 assert udata <= u + eps
213 vlimits_y[iy, 0] = (vmin + vmax) / 2.0 - u / 2.0
214 vlimits_y[iy, 1] = (vmin + vmax) / 2.0 + u / 2.0
216 def check_aspect(a, awant, eps=1e-2):
217 if abs(1.0 - (a/awant)) > eps:
218 logger.error(
219 'Unable to comply with requested aspect ratio '
220 '(wanted: %g, achieved: %g)' % (awant, a))
222 for ix1, ix2, aspect in aspects_xx:
223 check_aspect(cxs[ix2] / cxs[ix1], aspect)
225 for iy1, iy2, aspect in aspects_yy:
226 check_aspect(cys[iy2] / cys[iy1], aspect)
228 for ix, iy, aspect in aspects_xy:
229 check_aspect(cys[iy] / cxs[ix], aspect)
231 return (vlimits_x, vlimits_y), (wxs, wys)
234def solve_layout_iterative(size, shape, limits, aspects, niterations=3):
236 sx, sy = size
237 nx, ny = shape
238 vxs, vys = limits
239 uxs = vxs[:, 1] - vxs[:, 0]
240 uys = vys[:, 1] - vys[:, 0]
241 aspects_xx, aspects_yy, aspects_xy = aspects
243 fracs_x, fracs_y = num.ones(nx), num.ones(ny)
244 for i in range(niterations):
245 (vlimits_x, vlimits_y), (wxs, wys) = solve_layout_fixed_panels(
246 size, shape, limits, aspects, (fracs_x, fracs_y))
248 uxs_view = vlimits_x[:, 1] - vlimits_x[:, 0]
249 uys_view = vlimits_y[:, 1] - vlimits_y[:, 0]
250 wxs_used = wxs * uxs / uxs_view
251 wys_used = wys * uys / uys_view
252 # wxs_wasted = wxs * (1.0 - uxs / uxs_view)
253 # wys_wasted = wys * (1.0 - uys / uys_view)
255 fracs_x = wxs_used
256 fracs_y = wys_used
258 return (vlimits_x, vlimits_y), (wxs, wys)
261class PlotError(Exception):
262 pass
265class NotEnoughSpace(PlotError):
266 pass
269class PlotConfig(Object):
270 '''
271 Configuration for :py:class:`Plot`.
272 '''
274 font_size = Float.T(default=9.0)
276 size_cm = Tuple.T(
277 2, Float.T(), default=(20., 20.))
279 margins_em = Tuple.T(
280 4, Float.T(), default=(8., 6., 8., 6.))
282 separator_em = Float.T(default=1.5)
284 colorbar_width_em = Float.T(default=2.0)
286 label_offset_em = Tuple.T(
287 2, Float.T(), default=(2., 2.))
289 tick_label_offset_em = Tuple.T(
290 2, Float.T(), default=(0.5, 0.5))
292 @property
293 def size_inch(self):
294 return self.size_cm[0]/inch, self.size_cm[1]/inch
297class Plot(object):
298 '''
299 Matplotlib plotting with some fancy extras.
301 - Absolute sized figure margins, also for interactive plots.
302 - Improved label placement for grids of axes.
303 - Improved shared axis'es across multiple axes, e.g. for cross section
304 plots.
305 - Fixed aspect plotting across multiple axis'es on separate axes.
306 - Automatic subplot sizing based on aspect and data limit constraints.
307 - Serializable plot configuration.
308 '''
310 def __init__(
311 self, x_dims=['x'], y_dims=['y'], z_dims=[], config=None,
312 fig=None, call_mpl_init=True):
314 if config is None:
315 config = PlotConfig()
317 self._shape = len(x_dims), len(y_dims)
319 dims = []
320 for dim in x_dims + y_dims + z_dims:
321 dim = dim.lstrip('-')
322 if dim not in dims:
323 dims.append(dim)
325 self.config = config
326 self._disconnect_data = []
327 self._width = self._height = self._pixels = None
328 if call_mpl_init:
329 self._plt = plot.mpl_init(self.config.font_size)
331 if fig is None:
332 fig = self._plt.figure(
333 figsize=self.config.size_inch, FigureClass=SmartplotFigure)
334 else:
335 assert isinstance(fig, SmartplotFigure)
337 fig.set_smartplot(self)
339 self._fig = fig
340 self._colorbar_width = 0.0
341 self._colorbar_height = 0.0
342 self._colorbar_axes = []
344 self._dims = dims
345 self._dim_index = self._dims.index
346 self._ndims = len(dims)
347 self._labels = {}
348 self._aspects = {}
350 self.setup_axes()
352 self._view_limits = num.zeros((self._ndims, 2))
354 self._view_limits[:, :] = num.nan
355 self._last_mpl_view_limits = None
357 self._x_dims = [dim.lstrip('-') for dim in x_dims]
358 self._x_dims_invert = [dim.startswith('-') for dim in x_dims]
360 self._y_dims = [dim.lstrip('-') for dim in y_dims]
361 self._y_dims_invert = [dim.startswith('-') for dim in y_dims]
363 self._z_dims = [dim.lstrip('-') for dim in z_dims]
364 self._z_dims_invert = [dim.startswith('-') for dim in z_dims]
366 self._mappables = {}
367 self._updating_layout = False
369 self._need_update_layout = True
370 self._update_geometry()
372 for axes in self.axes_list:
373 fig.add_axes(axes)
374 self._connect(axes, 'xlim_changed', self.lim_changed_handler)
375 self._connect(axes, 'ylim_changed', self.lim_changed_handler)
377 self._cid_resize = fig.canvas.mpl_connect(
378 'resize_event', self.resize_handler)
380 try:
381 self._connect(fig, 'dpi_changed', self.dpi_changed_handler)
382 except ValueError:
383 # 'dpi_changed' event has been removed in MPL 3.8.
384 # canvas 'resize_event' may be sufficient but needs to be checked.
385 # https://matplotlib.org/stable/api/prev_api_changes/api_changes_3.8.0.html#text-get-rotation
386 pass
388 self._lim_changed_depth = 0
390 def reset_size(self):
391 self._fig.set_size_inches(self.config.size_inch)
393 def axes(self, ix, iy):
394 if not (isinstance(ix, int) and isinstance(iy, int)):
395 ix = self._x_dims.index(ix)
396 iy = self._y_dims.index(iy)
398 return self._axes[iy][ix]
400 def set_color_dim(self, mappable, dim):
401 assert dim in self._dims
402 self._mappables[mappable] = dim
404 def set_aspect(self, ydim, xdim, aspect=1.0):
405 self._aspects[ydim, xdim] = aspect
407 @property
408 def dims(self):
409 return self._dims
411 @property
412 def fig(self):
413 return self._fig
415 @property
416 def axes_list(self):
417 axes = []
418 for row in self._axes:
419 axes.extend(row)
420 return axes
422 @property
423 def axes_bottom_list(self):
424 return self._axes[0]
426 @property
427 def axes_left_list(self):
428 return [row[0] for row in self._axes]
430 def setup_axes(self):
431 rect = [0., 0., 1., 1.]
432 nx, ny = self._shape
433 axes = []
434 for iy in range(ny):
435 axes.append([])
436 for ix in range(nx):
437 axes[-1].append(SmartplotAxes(self.fig, rect))
439 self._axes = axes
441 for _, _, axes_ in self.iaxes():
442 axes_.set_autoscale_on(False)
444 def _connect(self, obj, sig, handler):
445 cid = get_callbacks(obj).connect(sig, handler)
446 self._disconnect_data.append((obj, cid))
448 def _disconnect_all(self):
449 for obj, cid in self._disconnect_data:
450 get_callbacks(obj).disconnect(cid)
452 self._fig.canvas.mpl_disconnect(self._cid_resize)
454 def dpi_changed_handler(self, fig):
455 if self._updating_layout:
456 return
458 self._update_geometry()
460 def resize_handler(self, event):
461 if self._updating_layout:
462 return
464 self._update_geometry()
466 def lim_changed_handler(self, axes):
467 if self._updating_layout:
468 return
470 current = self._get_mpl_view_limits()
471 last = self._last_mpl_view_limits
472 if last is None:
473 return
475 for iy, ix, axes in self.iaxes():
476 acurrent = current[iy][ix]
477 alast = last[iy][ix]
478 if acurrent[0] != alast[0]:
479 xdim = self._x_dims[ix]
480 logger.debug(
481 'X limits have been changed interactively in subplot '
482 '(%i, %i)' % (ix, iy))
483 self.set_lim(xdim, *sorted(acurrent[0]))
485 if acurrent[1] != alast[1]:
486 ydim = self._y_dims[iy]
487 logger.debug(
488 'Y limits have been changed interactively in subplot '
489 '(%i, %i)' % (ix, iy))
490 self.set_lim(ydim, *sorted(acurrent[1]))
492 def _update_geometry(self):
493 w, h = self._fig.canvas.get_width_height()
494 dp = self.get_device_pixel_ratio()
495 p = self.get_pixels_factor() * dp
497 if (self._width, self._height, self._pixels) != (w, h, p, dp):
498 logger.debug(
499 'New figure size: %g x %g, '
500 'logical-pixel/point: %g, physical-pixel/logical-pixel: %g' % (
501 w, h, p, dp))
503 self._width = w # logical pixel
504 self._height = h # logical pixel
505 self._pixels = p # logical pixel / point
506 self._device_pixel_ratio = dp # physical / logical
507 self.need_update_layout()
509 @property
510 def margins(self):
511 return tuple(
512 x * self.config.font_size / self._pixels
513 for x in self.config.margins_em)
515 @property
516 def separator(self):
517 return self.config.separator_em * self.config.font_size / self._pixels
519 def rect_to_figure_coords(self, rect):
520 left, bottom, width, height = rect
521 return (
522 left / self._width,
523 bottom / self._height,
524 width / self._width,
525 height / self._height)
527 def point_to_axes_coords(self, axes, point):
528 x, y = point
529 aleft, abottom, awidth, aheight = axes.get_position().bounds
531 x_fig = x / self._width
532 y_fig = y / self._height
534 x_axes = (x_fig - aleft) / awidth
535 y_axes = (y_fig - abottom) / aheight
537 return (x_axes, y_axes)
539 def get_pixels_factor(self):
540 try:
541 r = self._fig.canvas.get_renderer()
542 return 1.0 / r.points_to_pixels(1.0)
543 except AttributeError:
544 return 1.0
546 def get_device_pixel_ratio(self):
547 try:
548 return self._fig.canvas.device_pixel_ratio
549 except AttributeError:
550 return 1.0
552 def make_limits(self, lims):
553 a = plot.AutoScaler(space=0.05)
554 return a.make_scale(lims)[:2]
556 def iaxes(self):
557 for iy, row in enumerate(self._axes):
558 for ix, axes in enumerate(row):
559 yield iy, ix, axes
561 def get_data_limits(self):
562 dim_to_values = defaultdict(list)
563 for iy, ix, axes in self.iaxes():
564 dim_to_values[self._y_dims[iy]].extend(
565 axes.get_yaxis().get_data_interval())
566 dim_to_values[self._x_dims[ix]].extend(
567 axes.get_xaxis().get_data_interval())
569 for mappable, dim in self._mappables.items():
570 dim_to_values[dim].extend(mappable.get_clim())
572 lims = num.zeros((self._ndims, 2))
573 for idim in range(self._ndims):
574 dim = self._dims[idim]
575 if dim in dim_to_values:
576 vs = num.array(
577 dim_to_values[self._dims[idim]], dtype=float)
578 vs = vs[num.isfinite(vs)]
579 if vs.size > 0:
580 lims[idim, :] = num.min(vs), num.max(vs)
581 else:
582 lims[idim, :] = num.nan, num.nan
583 else:
584 lims[idim, :] = num.nan, num.nan
586 lims[num.logical_not(num.isfinite(lims))] = 0.0
587 return lims
589 def set_lim(self, dim, vmin, vmax):
590 assert vmin <= vmax
591 self._view_limits[self._dim_index(dim), :] = vmin, vmax
592 self.need_update_layout()
594 def set_x_invert(self, ix, invert):
595 self._x_dims_invert[ix] = invert
596 self.need_update_layout()
598 def set_y_invert(self, iy, invert):
599 self._y_dims_invert[iy] = invert
600 self.need_update_layout()
602 def _get_mpl_view_limits(self):
603 vl = []
604 for row in self._axes:
605 vl_row = []
606 for axes in row:
607 vl_row.append((
608 axes.get_xaxis().get_view_interval().tolist(),
609 axes.get_yaxis().get_view_interval().tolist()))
611 vl.append(vl_row)
613 return vl
615 def _remember_mpl_view_limits(self):
616 self._last_mpl_view_limits = self._get_mpl_view_limits()
618 def window_xmin(self, x):
619 return window_min(
620 self._shape[0], self._width,
621 self.margins[0], self.margins[2] + self._colorbar_width,
622 self.separator, x)
624 def window_xmax(self, x):
625 return window_max(
626 self._shape[0], self._width,
627 self.margins[0], self.margins[2] + self._colorbar_width,
628 self.separator, x)
630 def window_ymin(self, y):
631 return window_min(
632 self._shape[1], self._height,
633 self.margins[3] + self._colorbar_height, self.margins[1],
634 self.separator, y)
636 def window_ymax(self, y):
637 return window_max(
638 self._shape[1], self._height,
639 self.margins[3] + self._colorbar_height, self.margins[1],
640 self.separator, y)
642 def need_update_layout(self):
643 self._need_update_layout = True
645 def _update_layout(self):
646 assert not self._updating_layout
648 if not self._need_update_layout:
649 return
651 self._updating_layout = True
652 try:
653 data_limits = self.get_data_limits()
655 limits = num.zeros((self._ndims, 2))
656 for idim in range(self._ndims):
657 limits[idim, :] = self.make_limits(data_limits[idim, :])
659 mask = num.isfinite(self._view_limits)
660 limits[mask] = self._view_limits[mask]
662 # deltas = limits[:, 1] - limits[:, 0]
664 # data_w = deltas[0]
665 # data_h = deltas[1]
667 ml, mt, mr, mb = self.margins
668 mr += self._colorbar_width
669 mb += self._colorbar_height
670 sw = sh = self.separator
672 nx, ny = self._shape
674 # data_r = data_h / data_w
675 em = self.config.font_size
676 em_pixels = em / self._pixels
677 w = self._width
678 h = self._height
679 fig_w_avail = w - mr - ml - (nx-1) * sw
680 fig_h_avail = h - mt - mb - (ny-1) * sh
682 if fig_w_avail <= 0.0 or fig_h_avail <= 0.0:
683 raise NotEnoughSpace()
685 x_limits = num.zeros((nx, 2))
686 for ix, xdim in enumerate(self._x_dims):
687 x_limits[ix, :] = limits[self._dim_index(xdim)]
689 y_limits = num.zeros((ny, 2))
690 for iy, ydim in enumerate(self._y_dims):
691 y_limits[iy, :] = limits[self._dim_index(ydim)]
693 def get_aspect(dim1, dim2):
694 if (dim2, dim1) in self._aspects:
695 return 1.0/self._aspects[dim2, dim1]
697 return self._aspects.get((dim1, dim2), None)
699 aspects_xx = []
700 for ix1, xdim1 in enumerate(self._x_dims):
701 for ix2, xdim2 in enumerate(self._x_dims):
702 aspect = get_aspect(xdim2, xdim1)
703 if aspect:
704 aspects_xx.append((ix1, ix2, aspect))
706 aspects_yy = []
707 for iy1, ydim1 in enumerate(self._y_dims):
708 for iy2, ydim2 in enumerate(self._y_dims):
709 aspect = get_aspect(ydim2, ydim1)
710 if aspect:
711 aspects_yy.append((iy1, iy2, aspect))
713 aspects_xy = []
714 for iy, ix, axes in self.iaxes():
715 xdim = self._x_dims[ix]
716 ydim = self._y_dims[iy]
717 aspect = get_aspect(ydim, xdim)
718 if aspect:
719 aspects_xy.append((ix, iy, aspect))
721 (x_limits, y_limits), (aws, ahs) = solve_layout_iterative(
722 size=(fig_w_avail, fig_h_avail),
723 shape=(nx, ny),
724 limits=(x_limits, y_limits),
725 aspects=(
726 aspects_xx,
727 aspects_yy,
728 aspects_xy))
730 for iy, ix, axes in self.iaxes():
731 rect = [
732 ml + num.sum(aws[:ix])+(ix*sw),
733 mb + num.sum(ahs[:iy])+(iy*sh),
734 aws[ix], ahs[iy]]
736 axes.set_position(
737 self.rect_to_figure_coords(rect), which='both')
739 self.set_label_coords(
740 axes, 'x', [
741 wcenter(rect),
742 self.config.label_offset_em[0]*em_pixels
743 + self._colorbar_height])
745 self.set_label_coords(
746 axes, 'y', [
747 self.config.label_offset_em[1]*em_pixels,
748 hcenter(rect)])
750 axes.get_xaxis().set_tick_params(
751 bottom=(iy == 0), top=(iy == ny-1),
752 labelbottom=(iy == 0), labeltop=False)
754 axes.get_yaxis().set_tick_params(
755 left=(ix == 0), right=(ix == nx-1),
756 labelleft=(ix == 0), labelright=False)
758 istride = -1 if self._x_dims_invert[ix] else 1
759 axes.set_xlim(*x_limits[ix, ::istride])
760 istride = -1 if self._y_dims_invert[iy] else 1
761 axes.set_ylim(*y_limits[iy, ::istride])
763 axes.tick_params(
764 axis='x',
765 pad=self.config.tick_label_offset_em[0]*em)
767 axes.tick_params(
768 axis='y',
769 pad=self.config.tick_label_offset_em[0]*em)
771 self._remember_mpl_view_limits()
773 for mappable, dim in self._mappables.items():
774 mappable.set_clim(*limits[self._dim_index(dim)])
776 # scaler = plot.AutoScaler()
778 # aspect tick incs same
779 #
780 # inc = scaler.make_scale(
781 # [0, min(data_expanded_w, data_expanded_h)],
782 # override_mode='off')[2]
783 #
784 # for axes in self.axes_list:
785 # axes.set_xlim(*limits[0, :])
786 # axes.set_ylim(*limits[1, :])
787 #
788 # tl = MultipleLocator(inc)
789 # axes.get_xaxis().set_major_locator(tl)
790 # tl = MultipleLocator(inc)
791 # axes.get_yaxis().set_major_locator(tl)
793 for axes, orientation, position in self._colorbar_axes:
794 if orientation == 'horizontal':
795 xmin = self.window_xmin(position[0])
796 xmax = self.window_xmax(position[1])
797 ymin = mb - self._colorbar_height
798 ymax = mb - self._colorbar_height \
799 + self.config.colorbar_width_em * em_pixels
800 else:
801 ymin = self.window_ymin(position[0])
802 ymax = self.window_ymax(position[1])
803 xmin = w - mr + 2 * sw
804 xmax = w - mr + 2 * sw \
805 + self.config.colorbar_width_em * em_pixels
807 rect = [xmin, ymin, xmax-xmin, ymax-ymin]
808 axes.set_position(
809 self.rect_to_figure_coords(rect), which='both')
811 for ix, axes in enumerate(self.axes_bottom_list):
812 dim = self._x_dims[ix]
813 s = self._labels.get(dim, dim)
814 axes.set_xlabel(s)
816 for iy, axes in enumerate(self.axes_left_list):
817 dim = self._y_dims[iy]
818 s = self._labels.get(dim, dim)
819 axes.set_ylabel(s)
821 self._need_update_layout = False
822 self.update_layout_hook()
824 finally:
825 self._updating_layout = False
827 def update_layout_hook(self):
828 pass
830 def set_label_coords(self, axes, which, point):
831 axis = axes.get_xaxis() if which == 'x' else axes.get_yaxis()
832 axis.set_label_coords(*self.point_to_axes_coords(axes, point))
834 def plot(self, points, *args, **kwargs):
835 for iy, row in enumerate(self._axes):
836 y = points[:, self._dim_index(self._y_dims[iy])]
837 for ix, axes in enumerate(row):
838 x = points[:, self._dim_index(self._x_dims[ix])]
839 axes.plot(x, y, *args, **kwargs)
841 def close(self):
842 self._disconnect_all()
843 self._plt.close(self._fig)
845 def show(self):
846 self._plt.show()
847 self.reset_size()
849 def set_label(self, dim, s):
850 # just set attribute, handle in update_layout
851 self._labels[dim] = s
853 def colorbar(
854 self, dim,
855 orientation='vertical',
856 position=None):
858 if dim not in self._dims:
859 raise PlotError(
860 'dimension "%s" is not defined')
862 if orientation not in ('vertical', 'horizontal'):
863 raise PlotError(
864 'orientation must be "vertical" or "horizontal"')
866 mappable = None
867 for mappable_, dim_ in self._mappables.items():
868 if dim_ == dim:
869 if mappable is None:
870 mappable = mappable_
871 else:
872 mappable_.set_cmap(mappable.get_cmap())
874 if mappable is None:
875 raise PlotError(
876 'no mappable registered for dimension "%s"' % dim)
878 if position is None:
879 if orientation == 'vertical':
880 position = (0, self._shape[1])
881 else:
882 position = (0, self._shape[0])
884 em_pixels = self.config.font_size / self._pixels
886 if orientation == 'vertical':
887 self._colorbar_width = self.config.colorbar_width_em*em_pixels + \
888 self.separator * 2.0
889 else:
890 self._colorbar_height = self.config.colorbar_width_em*em_pixels + \
891 self.separator + self.margins[3]
893 axes = SmartplotAxes(self.fig, [0., 0., 1., 1.])
894 self.fig.add_axes(axes)
896 self._colorbar_axes.append(
897 (axes, orientation, position))
899 self.need_update_layout()
900 # axes.plot([1], [1])
901 label = self._labels.get(dim, dim)
902 return colorbar.Colorbar(
903 axes, mappable, orientation=orientation, label=label)
905 def __call__(self, *args):
906 return self.axes(*args)
909if __name__ == '__main__':
910 import sys
911 from pyrocko import util
913 logging.getLogger('matplotlib').setLevel(logging.WARNING)
914 util.setup_logging('smartplot', 'debug')
916 iplots = [int(x) for x in sys.argv[1:]]
918 if 0 in iplots:
919 p = Plot(['x'], ['y'])
920 n = 100
921 x = num.arange(n) * 2.0
922 y = num.random.normal(size=n)
923 p(0, 0).plot(x, y, 'o')
924 p.show()
926 if 1 in iplots:
927 p = Plot(['x', 'x'], ['y'])
928 n = 100
929 x = num.arange(n) * 2.0
930 y = num.random.normal(size=n)
931 p(0, 0).plot(x, y, 'o')
932 x = num.arange(n) * 2.0
933 y = num.random.normal(size=n)
934 p(1, 0).plot(x, y, 'o')
935 p.show()
937 if 11 in iplots:
938 p = Plot(['x'], ['y'])
939 p.set_aspect('y', 'x', 2.0)
940 n = 100
941 xy = num.random.normal(size=(n, 2))
942 p(0, 0).plot(xy[:, 0], xy[:, 1], 'o')
943 p.show()
945 if 12 in iplots:
946 p = Plot(['x', 'x2'], ['y'])
947 p.set_aspect('x2', 'x', 2.0)
948 p.set_aspect('y', 'x', 2.0)
949 n = 100
950 xy = num.random.normal(size=(n, 2))
951 p(0, 0).plot(xy[:, 0], xy[:, 1], 'o')
952 p(1, 0).plot(xy[:, 0], xy[:, 1], 'o')
953 p.show()
955 if 13 in iplots:
956 p = Plot(['x'], ['y', 'y2'])
957 p.set_aspect('y2', 'y', 2.0)
958 p.set_aspect('y', 'x', 2.0)
959 n = 100
960 xy = num.random.normal(size=(n, 2))
961 p(0, 0).plot(xy[:, 0], xy[:, 1], 'o')
962 p(0, 1).plot(xy[:, 0], xy[:, 1], 'o')
963 p.show()
965 if 2 in iplots:
966 p = Plot(['easting', 'depth'], ['northing', 'depth'])
968 n = 100
970 ned = num.random.normal(size=(n, 3))
971 p(0, 0).plot(ned[:, 1], ned[:, 0], 'o')
972 p(1, 0).plot(ned[:, 2], ned[:, 0], 'o')
973 p(0, 1).plot(ned[:, 1], ned[:, 2], 'o')
974 p.show()
976 if 3 in iplots:
977 p = Plot(['easting', 'depth'], ['-depth', 'northing'])
978 p.set_aspect('easting', 'northing', 1.0)
979 p.set_aspect('easting', 'depth', 0.5)
980 p.set_aspect('northing', 'depth', 0.5)
982 n = 100
984 ned = num.random.normal(size=(n, 3))
985 ned[:, 2] *= 0.25
986 p(0, 1).plot(ned[:, 1], ned[:, 0], 'o', color='black')
987 p(0, 0).plot(ned[:, 1], ned[:, 2], 'o')
988 p(1, 1).plot(ned[:, 2], ned[:, 0], 'o')
989 p(1, 0).set_visible(False)
990 p.set_lim('depth', 0., 0.2)
991 p.show()
993 if 5 in iplots:
994 p = Plot(['time'], ['northing', 'easting', '-depth'], ['depth'])
996 n = 100
998 t = num.arange(n)
999 xyz = num.random.normal(size=(n, 4))
1000 xyz[:, 0] *= 0.5
1002 smap = make_smap('summer')
1004 p(0, 0).scatter(
1005 t, xyz[:, 0], c=xyz[:, 2], cmap=smap.cmap, norm=smap.norm)
1006 p(0, 1).scatter(
1007 t, xyz[:, 1], c=xyz[:, 2], cmap=smap.cmap, norm=smap.norm)
1008 p(0, 2).scatter(
1009 t, xyz[:, 2], c=xyz[:, 2], cmap=smap.cmap, norm=smap.norm)
1011 p.set_lim('depth', -1., 1.)
1013 p.set_color_dim(smap, 'depth')
1015 p.set_aspect('northing', 'easting', 1.0)
1016 p.set_aspect('northing', 'depth', 1.0)
1018 p.set_label('time', 'Time [s]')
1019 p.set_label('depth', 'Depth [km]')
1020 p.set_label('easting', 'Easting [km]')
1021 p.set_label('northing', 'Northing [km]')
1023 p.colorbar('depth')
1025 p.show()
1027 if 6 in iplots:
1028 km = 1000.
1029 p = Plot(
1030 ['easting'], ['northing']*3, ['displacement'])
1032 nn, ne = 50, 40
1033 n = num.linspace(-5*km, 5*km, nn)
1034 e = num.linspace(-10*km, 10*km, ne)
1036 displacement = num.zeros((nn, ne, 3))
1037 g = num.exp(
1038 -(n[:, num.newaxis]**2 + e[num.newaxis, :]**2) / (5*km)**2)
1040 displacement[:, :, 0] = g
1041 displacement[:, :, 1] = g * 0.5
1042 displacement[:, :, 2] = -g * 0.2
1044 for icomp in (0, 1, 2):
1045 c = p(0, icomp).pcolormesh(
1046 e/km, n/km, displacement[:, :, icomp], shading='gouraud')
1047 p.set_color_dim(c, 'displacement')
1049 p.colorbar('displacement')
1050 p.set_lim('displacement', -1.0, 1.0)
1051 p.set_label('easting', 'Easting [km]')
1052 p.set_label('northing', 'Northing [km]')
1053 p.set_aspect('northing', 'easting')
1055 p.set_lim('northing', -5.0, 5.0)
1056 p.set_lim('easting', -3.0, 3.0)
1057 p.show()