1# http://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6from collections import defaultdict
7import math
8import logging
10import numpy as num
11import matplotlib
12from matplotlib.axes import Axes
13# from matplotlib.ticker import MultipleLocator
14from matplotlib import cm, colors, colorbar, figure
16from pyrocko.guts import Tuple, Float, Object
17from pyrocko import plot
19import scipy.optimize
21logger = logging.getLogger('pyrocko.plot.smartplot')
23guts_prefix = 'pf'
25inch = 2.54
28class SmartplotAxes(Axes):
30 if matplotlib.__version__.split('.') < '3.6'.split('.'):
31 # Subclassing cla is deprecated on newer mpl but need this fallback for
32 # older versions. Code is duplicated because mpl behaviour depends
33 # on the existence of cla in the subclass...
34 def cla(self):
35 if hasattr(self, 'callbacks'):
36 callbacks = self.callbacks
37 Axes.cla(self)
38 self.callbacks = callbacks
39 else:
40 Axes.cla(self)
42 else:
43 def clear(self):
44 if hasattr(self, 'callbacks'):
45 callbacks = self.callbacks
46 Axes.clear(self)
47 self.callbacks = callbacks
48 else:
49 Axes.clear(self)
52class SmartplotFigure(figure.Figure):
54 def set_smartplot(self, plot):
55 self._smartplot = plot
57 def draw(self, *args, **kwargs):
58 if hasattr(self, '_smartplot'):
59 try:
60 self._smartplot._update_layout()
61 except NotEnoughSpace:
62 logger.error('Figure is too small to show the plot.')
63 return
65 return figure.Figure.draw(self, *args, **kwargs)
68def limits(points):
69 lims = num.zeros((3, 2))
70 if points.size != 0:
71 lims[:, 0] = num.min(points, axis=0)
72 lims[:, 1] = num.max(points, axis=0)
74 return lims
77def wcenter(rect):
78 return rect[0] + rect[2]*0.5
81def hcenter(rect):
82 return rect[1] + rect[3]*0.5
85def window_min(n, w, ml, mu, s, x):
86 return ml + x/float(n) * (w - (ml + mu + (n-1)*s)) + math.floor(x) * s
89def window_max(n, w, ml, mu, s, x):
90 return ml + x/float(n) * (w - (ml + mu + (n-1)*s)) + (math.floor(x)-1) * s
93def make_smap(cmap, norm=None):
94 if isinstance(norm, tuple):
95 norm = colors.Normalize(*norm, clip=False)
96 smap = cm.ScalarMappable(cmap=cmap, norm=norm)
97 smap._A = [] # not needed in newer versions of mpl?
98 return smap
101def solve_layout_fixed_panels(size, shape, limits, aspects, fracs=None):
103 weight_aspect = 1000.
105 sx, sy = size
106 nx, ny = shape
107 nvar = nx+ny
108 vxs, vys = limits
109 uxs = vxs[:, 1] - vxs[:, 0]
110 uys = vys[:, 1] - vys[:, 0]
111 aspects_xx, aspects_yy, aspects_xy = aspects
113 if fracs is None:
114 wxs = num.full(nx, sx / nx)
115 wys = num.full(ny, sy / ny)
116 else:
117 frac_x, frac_y = fracs
118 wxs = sx * frac_x / num.sum(frac_x)
119 wys = sy * frac_y / num.sum(frac_y)
121 data = []
122 weights = []
123 rows = []
124 bounds = []
125 for ix in range(nx):
126 u = uxs[ix]
127 assert u > 0.0
128 row = num.zeros(nvar)
129 row[ix] = u
130 rows.append(row)
131 data.append(wxs[ix])
132 weights.append(1.0 / u)
133 bounds.append((0, wxs[ix] / u))
135 for iy in range(ny):
136 u = uys[iy]
137 assert u > 0.0
138 row = num.zeros(nvar)
139 row[nx+iy] = u
140 rows.append(row)
141 data.append(wys[iy])
142 weights.append(1.0)
143 bounds.append((0, wys[iy] / u))
145 for ix1, ix2, aspect in aspects_xx:
146 row = num.zeros(nvar)
147 row[ix1] = aspect
148 row[ix2] = -1.0
149 weights.append(weight_aspect/aspect)
150 rows.append(row)
151 data.append(0.0)
153 for iy1, iy2, aspect in aspects_yy:
154 row = num.zeros(nvar)
155 row[nx+iy1] = aspect
156 row[nx+iy2] = -1.0
157 weights.append(weight_aspect/aspect)
158 rows.append(row)
159 data.append(0.0)
161 for ix, iy, aspect in aspects_xy:
162 row = num.zeros(nvar)
163 row[ix] = aspect
164 row[nx+iy] = -1.0
165 weights.append(weight_aspect/aspect)
166 rows.append(row)
167 data.append(0.0)
169 weights = num.array(weights)
170 data = num.array(data)
171 mat = num.vstack(rows) * weights[:, num.newaxis]
172 data *= weights
174 bounds = num.array(bounds).T
176 model = scipy.optimize.lsq_linear(mat, data, bounds).x
178 cxs = model[:nx]
179 cys = model[nx:nx+ny]
181 vlimits_x = num.zeros((nx, 2))
182 for ix in range(nx):
183 u = wxs[ix] / cxs[ix]
184 vmin, vmax = vxs[ix]
185 udata = vmax - vmin
186 eps = 1e-7 * u
187 assert udata <= u + eps
188 vlimits_x[ix, 0] = (vmin + vmax) / 2.0 - u / 2.0
189 vlimits_x[ix, 1] = (vmin + vmax) / 2.0 + u / 2.0
191 vlimits_y = num.zeros((ny, 2))
192 for iy in range(ny):
193 u = wys[iy] / cys[iy]
194 vmin, vmax = vys[iy]
195 udata = vmax - vmin
196 eps = 1e-7 * u
197 assert udata <= u + eps
198 vlimits_y[iy, 0] = (vmin + vmax) / 2.0 - u / 2.0
199 vlimits_y[iy, 1] = (vmin + vmax) / 2.0 + u / 2.0
201 def check_aspect(a, awant, eps=1e-2):
202 if abs(1.0 - (a/awant)) > eps:
203 logger.error(
204 'Unable to comply with requested aspect ratio '
205 '(wanted: %g, achieved: %g)' % (awant, a))
207 for ix1, ix2, aspect in aspects_xx:
208 check_aspect(cxs[ix2] / cxs[ix1], aspect)
210 for iy1, iy2, aspect in aspects_yy:
211 check_aspect(cys[iy2] / cys[iy1], aspect)
213 for ix, iy, aspect in aspects_xy:
214 check_aspect(cys[iy] / cxs[ix], aspect)
216 return (vlimits_x, vlimits_y), (wxs, wys)
219def solve_layout_iterative(size, shape, limits, aspects, niterations=3):
221 sx, sy = size
222 nx, ny = shape
223 vxs, vys = limits
224 uxs = vxs[:, 1] - vxs[:, 0]
225 uys = vys[:, 1] - vys[:, 0]
226 aspects_xx, aspects_yy, aspects_xy = aspects
228 fracs_x, fracs_y = num.ones(nx), num.ones(ny)
229 for i in range(niterations):
230 (vlimits_x, vlimits_y), (wxs, wys) = solve_layout_fixed_panels(
231 size, shape, limits, aspects, (fracs_x, fracs_y))
233 uxs_view = vlimits_x[:, 1] - vlimits_x[:, 0]
234 uys_view = vlimits_y[:, 1] - vlimits_y[:, 0]
235 wxs_used = wxs * uxs / uxs_view
236 wys_used = wys * uys / uys_view
237 # wxs_wasted = wxs * (1.0 - uxs / uxs_view)
238 # wys_wasted = wys * (1.0 - uys / uys_view)
240 fracs_x = wxs_used
241 fracs_y = wys_used
243 return (vlimits_x, vlimits_y), (wxs, wys)
246class PlotError(Exception):
247 pass
250class NotEnoughSpace(PlotError):
251 pass
254class PlotConfig(Object):
256 font_size = Float.T(default=9.0)
258 size_cm = Tuple.T(
259 2, Float.T(), default=(20., 20.))
261 margins_em = Tuple.T(
262 4, Float.T(), default=(8., 6., 8., 6.))
264 separator_em = Float.T(default=1.5)
266 colorbar_width_em = Float.T(default=2.0)
268 label_offset_em = Tuple.T(
269 2, Float.T(), default=(2., 2.))
271 tick_label_offset_em = Tuple.T(
272 2, Float.T(), default=(0.5, 0.5))
274 @property
275 def size_inch(self):
276 return self.size_cm[0]/inch, self.size_cm[1]/inch
279class Plot(object):
281 def __init__(
282 self, x_dims=['x'], y_dims=['y'], z_dims=[], config=None,
283 fig=None, call_mpl_init=True):
285 if config is None:
286 config = PlotConfig()
288 self._shape = len(x_dims), len(y_dims)
290 dims = []
291 for dim in x_dims + y_dims + z_dims:
292 dim = dim.lstrip('-')
293 if dim not in dims:
294 dims.append(dim)
296 self.config = config
297 self._disconnect_data = []
298 self._width = self._height = self._pixels = None
299 if call_mpl_init:
300 self._plt = plot.mpl_init(self.config.font_size)
302 if fig is None:
303 fig = self._plt.figure(
304 figsize=self.config.size_inch, FigureClass=SmartplotFigure)
305 else:
306 assert isinstance(fig, SmartplotFigure)
308 fig.set_smartplot(self)
310 self._fig = fig
311 self._colorbar_width = 0.0
312 self._colorbar_height = 0.0
313 self._colorbar_axes = []
315 self._dims = dims
316 self._dim_index = self._dims.index
317 self._ndims = len(dims)
318 self._labels = {}
319 self._aspects = {}
321 self.setup_axes()
323 self._view_limits = num.zeros((self._ndims, 2))
325 self._view_limits[:, :] = num.nan
326 self._last_mpl_view_limits = None
328 self._x_dims = [dim.lstrip('-') for dim in x_dims]
329 self._x_dims_invert = [dim.startswith('-') for dim in x_dims]
331 self._y_dims = [dim.lstrip('-') for dim in y_dims]
332 self._y_dims_invert = [dim.startswith('-') for dim in y_dims]
334 self._z_dims = [dim.lstrip('-') for dim in z_dims]
335 self._z_dims_invert = [dim.startswith('-') for dim in z_dims]
337 self._mappables = {}
338 self._updating_layout = False
340 self._need_update_layout = True
341 self._update_geometry()
343 for axes in self.axes_list:
344 fig.add_axes(axes)
345 self._connect(axes, 'xlim_changed', self.lim_changed_handler)
346 self._connect(axes, 'ylim_changed', self.lim_changed_handler)
348 self._cid_resize = fig.canvas.mpl_connect(
349 'resize_event', self.resize_handler)
351 self._connect(fig, 'dpi_changed', self.dpi_changed_handler)
353 self._lim_changed_depth = 0
355 def reset_size(self):
356 self._fig.set_size_inches(self.config.size_inch)
358 def axes(self, ix, iy):
359 if not (isinstance(ix, int) and isinstance(iy, int)):
360 ix = self._x_dims.index(ix)
361 iy = self._y_dims.index(iy)
363 return self._axes[iy][ix]
365 def set_color_dim(self, mappable, dim):
366 assert dim in self._dims
367 self._mappables[mappable] = dim
369 def set_aspect(self, ydim, xdim, aspect=1.0):
370 self._aspects[ydim, xdim] = aspect
372 @property
373 def dims(self):
374 return self._dims
376 @property
377 def fig(self):
378 return self._fig
380 @property
381 def axes_list(self):
382 axes = []
383 for row in self._axes:
384 axes.extend(row)
385 return axes
387 @property
388 def axes_bottom_list(self):
389 return self._axes[0]
391 @property
392 def axes_left_list(self):
393 return [row[0] for row in self._axes]
395 def setup_axes(self):
396 rect = [0., 0., 1., 1.]
397 nx, ny = self._shape
398 axes = []
399 for iy in range(ny):
400 axes.append([])
401 for ix in range(nx):
402 axes[-1].append(SmartplotAxes(self.fig, rect))
404 self._axes = axes
406 for _, _, axes_ in self.iaxes():
407 axes_.set_autoscale_on(False)
409 def _connect(self, obj, sig, handler):
410 cid = obj.callbacks.connect(sig, handler)
411 self._disconnect_data.append((obj, cid))
413 def _disconnect_all(self):
414 for obj, cid in self._disconnect_data:
415 obj.callbacks.disconnect(cid)
417 self._fig.canvas.mpl_disconnect(self._cid_resize)
419 def dpi_changed_handler(self, fig):
420 if self._updating_layout:
421 return
423 self._update_geometry()
425 def resize_handler(self, event):
426 if self._updating_layout:
427 return
429 self._update_geometry()
431 def lim_changed_handler(self, axes):
432 if self._updating_layout:
433 return
435 current = self._get_mpl_view_limits()
436 last = self._last_mpl_view_limits
437 if last is None:
438 return
440 for iy, ix, axes in self.iaxes():
441 acurrent = current[iy][ix]
442 alast = last[iy][ix]
443 if acurrent[0] != alast[0]:
444 xdim = self._x_dims[ix]
445 logger.debug(
446 'X limits have been changed interactively in subplot '
447 '(%i, %i)' % (ix, iy))
448 self.set_lim(xdim, *sorted(acurrent[0]))
450 if acurrent[1] != alast[1]:
451 ydim = self._y_dims[iy]
452 logger.debug(
453 'Y limits have been changed interactively in subplot '
454 '(%i, %i)' % (ix, iy))
455 self.set_lim(ydim, *sorted(acurrent[1]))
457 self.need_update_layout()
459 def _update_geometry(self):
460 w, h = self._fig.canvas.get_width_height()
461 dp = self.get_device_pixel_ratio()
462 p = self.get_pixels_factor() * dp
464 if (self._width, self._height, self._pixels) != (w, h, p, dp):
465 logger.debug(
466 'New figure size: %g x %g, '
467 'logical-pixel/point: %g, physical-pixel/logical-pixel: %g' % (
468 w, h, p, dp))
470 self._width = w # logical pixel
471 self._height = h # logical pixel
472 self._pixels = p # logical pixel / point
473 self._device_pixel_ratio = dp # physical / logical
474 self.need_update_layout()
476 @property
477 def margins(self):
478 return tuple(
479 x * self.config.font_size / self._pixels
480 for x in self.config.margins_em)
482 @property
483 def separator(self):
484 return self.config.separator_em * self.config.font_size / self._pixels
486 def rect_to_figure_coords(self, rect):
487 left, bottom, width, height = rect
488 return (
489 left / self._width,
490 bottom / self._height,
491 width / self._width,
492 height / self._height)
494 def point_to_axes_coords(self, axes, point):
495 x, y = point
496 aleft, abottom, awidth, aheight = axes.get_position().bounds
498 x_fig = x / self._width
499 y_fig = y / self._height
501 x_axes = (x_fig - aleft) / awidth
502 y_axes = (y_fig - abottom) / aheight
504 return (x_axes, y_axes)
506 def get_pixels_factor(self):
507 try:
508 r = self._fig.canvas.get_renderer()
509 return 1.0 / r.points_to_pixels(1.0)
510 except AttributeError:
511 return 1.0
513 def get_device_pixel_ratio(self):
514 try:
515 return self._fig.canvas.device_pixel_ratio
516 except AttributeError:
517 return 1.0
519 def make_limits(self, lims):
520 a = plot.AutoScaler(space=0.05)
521 return a.make_scale(lims)[:2]
523 def iaxes(self):
524 for iy, row in enumerate(self._axes):
525 for ix, axes in enumerate(row):
526 yield iy, ix, axes
528 def get_data_limits(self):
529 dim_to_values = defaultdict(list)
530 for iy, ix, axes in self.iaxes():
531 dim_to_values[self._y_dims[iy]].extend(
532 axes.get_yaxis().get_data_interval())
533 dim_to_values[self._x_dims[ix]].extend(
534 axes.get_xaxis().get_data_interval())
536 for mappable, dim in self._mappables.items():
537 dim_to_values[dim].extend(mappable.get_clim())
539 lims = num.zeros((self._ndims, 2))
540 for idim in range(self._ndims):
541 dim = self._dims[idim]
542 if dim in dim_to_values:
543 vs = num.array(
544 dim_to_values[self._dims[idim]], dtype=float)
545 vs = vs[num.isfinite(vs)]
546 if vs.size > 0:
547 lims[idim, :] = num.min(vs), num.max(vs)
548 else:
549 lims[idim, :] = num.nan, num.nan
550 else:
551 lims[idim, :] = num.nan, num.nan
553 lims[num.logical_not(num.isfinite(lims))] = 0.0
554 return lims
556 def set_lim(self, dim, vmin, vmax):
557 assert vmin <= vmax
558 self._view_limits[self._dim_index(dim), :] = vmin, vmax
560 def _get_mpl_view_limits(self):
561 vl = []
562 for row in self._axes:
563 vl_row = []
564 for axes in row:
565 vl_row.append((
566 axes.get_xaxis().get_view_interval().tolist(),
567 axes.get_yaxis().get_view_interval().tolist()))
569 vl.append(vl_row)
571 return vl
573 def _remember_mpl_view_limits(self):
574 self._last_mpl_view_limits = self._get_mpl_view_limits()
576 def window_xmin(self, x):
577 return window_min(
578 self._shape[0], self._width,
579 self.margins[0], self.margins[2] + self._colorbar_width,
580 self.separator, x)
582 def window_xmax(self, x):
583 return window_max(
584 self._shape[0], self._width,
585 self.margins[0], self.margins[2] + self._colorbar_width,
586 self.separator, x)
588 def window_ymin(self, y):
589 return window_min(
590 self._shape[1], self._height,
591 self.margins[3] + self._colorbar_height, self.margins[1],
592 self.separator, y)
594 def window_ymax(self, y):
595 return window_max(
596 self._shape[1], self._height,
597 self.margins[3] + self._colorbar_height, self.margins[1],
598 self.separator, y)
600 def need_update_layout(self):
601 self._need_update_layout = True
603 def _update_layout(self):
604 assert not self._updating_layout
606 if not self._need_update_layout:
607 return
609 self._updating_layout = True
610 try:
611 data_limits = self.get_data_limits()
613 limits = num.zeros((self._ndims, 2))
614 for idim in range(self._ndims):
615 limits[idim, :] = self.make_limits(data_limits[idim, :])
617 mask = num.isfinite(self._view_limits)
618 limits[mask] = self._view_limits[mask]
620 # deltas = limits[:, 1] - limits[:, 0]
622 # data_w = deltas[0]
623 # data_h = deltas[1]
625 ml, mt, mr, mb = self.margins
626 mr += self._colorbar_width
627 mb += self._colorbar_height
628 sw = sh = self.separator
630 nx, ny = self._shape
632 # data_r = data_h / data_w
633 em = self.config.font_size / self._pixels
634 w = self._width
635 h = self._height
636 fig_w_avail = w - mr - ml - (nx-1) * sw
637 fig_h_avail = h - mt - mb - (ny-1) * sh
639 if fig_w_avail <= 0.0 or fig_h_avail <= 0.0:
640 raise NotEnoughSpace()
642 x_limits = num.zeros((nx, 2))
643 for ix, xdim in enumerate(self._x_dims):
644 x_limits[ix, :] = limits[self._dim_index(xdim)]
646 y_limits = num.zeros((ny, 2))
647 for iy, ydim in enumerate(self._y_dims):
648 y_limits[iy, :] = limits[self._dim_index(ydim)]
650 def get_aspect(dim1, dim2):
651 if (dim2, dim1) in self._aspects:
652 return 1.0/self._aspects[dim2, dim1]
654 return self._aspects.get((dim1, dim2), None)
656 aspects_xx = []
657 for ix1, xdim1 in enumerate(self._x_dims):
658 for ix2, xdim2 in enumerate(self._x_dims):
659 aspect = get_aspect(xdim2, xdim1)
660 if aspect:
661 aspects_xx.append((ix1, ix2, aspect))
663 aspects_yy = []
664 for iy1, ydim1 in enumerate(self._y_dims):
665 for iy2, ydim2 in enumerate(self._y_dims):
666 aspect = get_aspect(ydim2, ydim1)
667 if aspect:
668 aspects_yy.append((iy1, iy2, aspect))
670 aspects_xy = []
671 for iy, ix, axes in self.iaxes():
672 xdim = self._x_dims[ix]
673 ydim = self._y_dims[iy]
674 aspect = get_aspect(ydim, xdim)
675 if aspect:
676 aspects_xy.append((ix, iy, aspect))
678 (x_limits, y_limits), (aws, ahs) = solve_layout_iterative(
679 size=(fig_w_avail, fig_h_avail),
680 shape=(nx, ny),
681 limits=(x_limits, y_limits),
682 aspects=(
683 aspects_xx,
684 aspects_yy,
685 aspects_xy))
687 for iy, ix, axes in self.iaxes():
688 rect = [
689 ml + num.sum(aws[:ix])+(ix*sw),
690 mb + num.sum(ahs[:iy])+(iy*sh),
691 aws[ix], ahs[iy]]
693 axes.set_position(
694 self.rect_to_figure_coords(rect), which='both')
696 self.set_label_coords(
697 axes, 'x', [
698 wcenter(rect),
699 self.config.label_offset_em[0]*em
700 + self._colorbar_height])
702 self.set_label_coords(
703 axes, 'y', [
704 self.config.label_offset_em[1]*em,
705 hcenter(rect)])
707 axes.get_xaxis().set_tick_params(
708 bottom=(iy == 0), top=(iy == ny-1),
709 labelbottom=(iy == 0), labeltop=False)
711 axes.get_yaxis().set_tick_params(
712 left=(ix == 0), right=(ix == nx-1),
713 labelleft=(ix == 0), labelright=False)
715 istride = -1 if self._x_dims_invert[ix] else 1
716 axes.set_xlim(*x_limits[ix, ::istride])
717 istride = -1 if self._y_dims_invert[iy] else 1
718 axes.set_ylim(*y_limits[iy, ::istride])
720 axes.tick_params(
721 axis='x',
722 pad=self.config.tick_label_offset_em[0]*em)
724 axes.tick_params(
725 axis='y',
726 pad=self.config.tick_label_offset_em[0]*em)
728 self._remember_mpl_view_limits()
730 for mappable, dim in self._mappables.items():
731 mappable.set_clim(*limits[self._dim_index(dim)])
733 # scaler = plot.AutoScaler()
735 # aspect tick incs same
736 #
737 # inc = scaler.make_scale(
738 # [0, min(data_expanded_w, data_expanded_h)],
739 # override_mode='off')[2]
740 #
741 # for axes in self.axes_list:
742 # axes.set_xlim(*limits[0, :])
743 # axes.set_ylim(*limits[1, :])
744 #
745 # tl = MultipleLocator(inc)
746 # axes.get_xaxis().set_major_locator(tl)
747 # tl = MultipleLocator(inc)
748 # axes.get_yaxis().set_major_locator(tl)
750 for axes, orientation, position in self._colorbar_axes:
751 if orientation == 'horizontal':
752 xmin = self.window_xmin(position[0])
753 xmax = self.window_xmax(position[1])
754 ymin = mb - self._colorbar_height
755 ymax = mb - self._colorbar_height \
756 + self.config.colorbar_width_em * em
757 else:
758 ymin = self.window_ymin(position[0])
759 ymax = self.window_ymax(position[1])
760 xmin = w - mr + 2 * sw
761 xmax = w - mr + 2 * sw + self.config.colorbar_width_em * em
763 rect = [xmin, ymin, xmax-xmin, ymax-ymin]
764 axes.set_position(
765 self.rect_to_figure_coords(rect), which='both')
767 for ix, axes in enumerate(self.axes_bottom_list):
768 dim = self._x_dims[ix]
769 s = self._labels.get(dim, dim)
770 axes.set_xlabel(s)
772 for iy, axes in enumerate(self.axes_left_list):
773 dim = self._y_dims[iy]
774 s = self._labels.get(dim, dim)
775 axes.set_ylabel(s)
777 finally:
778 self._updating_layout = False
780 def set_label_coords(self, axes, which, point):
781 axis = axes.get_xaxis() if which == 'x' else axes.get_yaxis()
782 axis.set_label_coords(*self.point_to_axes_coords(axes, point))
784 def plot(self, points, *args, **kwargs):
785 for iy, row in enumerate(self._axes):
786 y = points[:, self._dim_index(self._y_dims[iy])]
787 for ix, axes in enumerate(row):
788 x = points[:, self._dim_index(self._x_dims[ix])]
789 axes.plot(x, y, *args, **kwargs)
791 def close(self):
792 self._disconnect_all()
793 self._plt.close(self._fig)
795 def show(self):
796 self._plt.show()
797 self.reset_size()
799 def set_label(self, dim, s):
800 # just set attribute, handle in update_layout
801 self._labels[dim] = s
803 def colorbar(
804 self, dim,
805 orientation='vertical',
806 position=None):
808 if dim not in self._dims:
809 raise PlotError(
810 'dimension "%s" is not defined')
812 if orientation not in ('vertical', 'horizontal'):
813 raise PlotError(
814 'orientation must be "vertical" or "horizontal"')
816 mappable = None
817 for mappable_, dim_ in self._mappables.items():
818 if dim_ == dim:
819 if mappable is None:
820 mappable = mappable_
821 else:
822 mappable_.set_cmap(mappable.get_cmap())
824 if mappable is None:
825 raise PlotError(
826 'no mappable registered for dimension "%s"' % dim)
828 if position is None:
829 if orientation == 'vertical':
830 position = (0, self._shape[1])
831 else:
832 position = (0, self._shape[0])
834 em = self.config.font_size / self._pixels
836 if orientation == 'vertical':
837 self._colorbar_width = self.config.colorbar_width_em*em + \
838 self.separator * 2.0
839 else:
840 self._colorbar_height = self.config.colorbar_width_em*em + \
841 self.separator + self.margins[3]
843 axes = SmartplotAxes(self.fig, [0., 0., 1., 1.])
844 self.fig.add_axes(axes)
846 self._colorbar_axes.append(
847 (axes, orientation, position))
849 self.need_update_layout()
850 # axes.plot([1], [1])
851 label = self._labels.get(dim, dim)
852 return colorbar.Colorbar(
853 axes, mappable, orientation=orientation, label=label)
855 def __call__(self, *args):
856 return self.axes(*args)
859if __name__ == '__main__':
860 import sys
861 from pyrocko import util
863 logging.getLogger('matplotlib').setLevel(logging.WARNING)
864 util.setup_logging('smartplot', 'debug')
866 iplots = [int(x) for x in sys.argv[1:]]
868 if 0 in iplots:
869 p = Plot(['x'], ['y'])
870 n = 100
871 x = num.arange(n) * 2.0
872 y = num.random.normal(size=n)
873 p(0, 0).plot(x, y, 'o')
874 p.show()
876 if 1 in iplots:
877 p = Plot(['x', 'x'], ['y'])
878 n = 100
879 x = num.arange(n) * 2.0
880 y = num.random.normal(size=n)
881 p(0, 0).plot(x, y, 'o')
882 x = num.arange(n) * 2.0
883 y = num.random.normal(size=n)
884 p(1, 0).plot(x, y, 'o')
885 p.show()
887 if 11 in iplots:
888 p = Plot(['x'], ['y'])
889 p.set_aspect('y', 'x', 2.0)
890 n = 100
891 xy = num.random.normal(size=(n, 2))
892 p(0, 0).plot(xy[:, 0], xy[:, 1], 'o')
893 p.show()
895 if 12 in iplots:
896 p = Plot(['x', 'x2'], ['y'])
897 p.set_aspect('x2', 'x', 2.0)
898 p.set_aspect('y', 'x', 2.0)
899 n = 100
900 xy = num.random.normal(size=(n, 2))
901 p(0, 0).plot(xy[:, 0], xy[:, 1], 'o')
902 p(1, 0).plot(xy[:, 0], xy[:, 1], 'o')
903 p.show()
905 if 13 in iplots:
906 p = Plot(['x'], ['y', 'y2'])
907 p.set_aspect('y2', 'y', 2.0)
908 p.set_aspect('y', 'x', 2.0)
909 n = 100
910 xy = num.random.normal(size=(n, 2))
911 p(0, 0).plot(xy[:, 0], xy[:, 1], 'o')
912 p(0, 1).plot(xy[:, 0], xy[:, 1], 'o')
913 p.show()
915 if 2 in iplots:
916 p = Plot(['easting', 'depth'], ['northing', 'depth'])
918 n = 100
920 ned = num.random.normal(size=(n, 3))
921 p(0, 0).plot(ned[:, 1], ned[:, 0], 'o')
922 p(1, 0).plot(ned[:, 2], ned[:, 0], 'o')
923 p(0, 1).plot(ned[:, 1], ned[:, 2], 'o')
924 p.show()
926 if 3 in iplots:
927 p = Plot(['easting', 'depth'], ['-depth', 'northing'])
928 p.set_aspect('easting', 'northing', 1.0)
929 p.set_aspect('easting', 'depth', 0.5)
930 p.set_aspect('northing', 'depth', 0.5)
932 n = 100
934 ned = num.random.normal(size=(n, 3))
935 ned[:, 2] *= 0.25
936 p(0, 1).plot(ned[:, 1], ned[:, 0], 'o', color='black')
937 p(0, 0).plot(ned[:, 1], ned[:, 2], 'o')
938 p(1, 1).plot(ned[:, 2], ned[:, 0], 'o')
939 p(1, 0).set_visible(False)
940 p.set_lim('depth', 0., 0.2)
941 p.show()
943 if 5 in iplots:
944 p = Plot(['time'], ['northing', 'easting', '-depth'], ['depth'])
946 n = 100
948 t = num.arange(n)
949 xyz = num.random.normal(size=(n, 4))
950 xyz[:, 0] *= 0.5
952 smap = make_smap('summer')
954 p(0, 0).scatter(
955 t, xyz[:, 0], c=xyz[:, 2], cmap=smap.cmap, norm=smap.norm)
956 p(0, 1).scatter(
957 t, xyz[:, 1], c=xyz[:, 2], cmap=smap.cmap, norm=smap.norm)
958 p(0, 2).scatter(
959 t, xyz[:, 2], c=xyz[:, 2], cmap=smap.cmap, norm=smap.norm)
961 p.set_lim('depth', -1., 1.)
963 p.set_color_dim(smap, 'depth')
965 p.set_aspect('northing', 'easting', 1.0)
966 p.set_aspect('northing', 'depth', 1.0)
968 p.set_label('time', 'Time [s]')
969 p.set_label('depth', 'Depth [km]')
970 p.set_label('easting', 'Easting [km]')
971 p.set_label('northing', 'Northing [km]')
973 p.colorbar('depth')
975 p.show()
977 if 6 in iplots:
978 km = 1000.
979 p = Plot(
980 ['easting'], ['northing']*3, ['displacement'])
982 nn, ne = 50, 40
983 n = num.linspace(-5*km, 5*km, nn)
984 e = num.linspace(-10*km, 10*km, ne)
986 displacement = num.zeros((nn, ne, 3))
987 g = num.exp(
988 -(n[:, num.newaxis]**2 + e[num.newaxis, :]**2) / (5*km)**2)
990 displacement[:, :, 0] = g
991 displacement[:, :, 1] = g * 0.5
992 displacement[:, :, 2] = -g * 0.2
994 for icomp in (0, 1, 2):
995 c = p(0, icomp).pcolormesh(
996 e/km, n/km, displacement[:, :, icomp], shading='gouraud')
997 p.set_color_dim(c, 'displacement')
999 p.colorbar('displacement')
1000 p.set_lim('displacement', -1.0, 1.0)
1001 p.set_label('easting', 'Easting [km]')
1002 p.set_label('northing', 'Northing [km]')
1003 p.set_aspect('northing', 'easting')
1005 p.set_lim('northing', -5.0, 5.0)
1006 p.set_lim('easting', -3.0, 3.0)
1007 p.show()