Skip to content

Instantly share code, notes, and snippets.

@niallrobinson
Last active December 19, 2015 15:28
Show Gist options
  • Save niallrobinson/5976287 to your computer and use it in GitHub Desktop.
Save niallrobinson/5976287 to your computer and use it in GitHub Desktop.
cube explorer with picker
'''
Proof of concept class for making interactive plot object which allow the addition
of navigation buttons.
Created on Jul 3, 2013
@author: nrobin
'''
import matplotlib.cm as mpl_cm
import matplotlib.pyplot as plt
from matplotlib.widgets import Button
import numpy as np
import iris
from iris.exceptions import CoordinateNotFoundError
import iris.plot as iplt
import iris.quickplot as qplt
class ButtonSetup(object):
"""
Convenience class to store setup for Cube
Explorer buttons.
Args:
* col_margin:
Value for setting the amount of space
on the right of the plot for displaying
buttons.
* initial_but_a_pos:
Positions of button a axis
in slot one.
* initial_but_b_pos:
Positions of button b axis
in slot one.
* slot_diff:
The vertical offset between sets
of buttons.
"""
def __init__(self,
col_margin = 0.85,
initial_but_a_pos = [0.875, 0.85, 0.11, 0.05],
initial_but_b_pos = [0.875, 0.79, 0.11, 0.05],
slot_diff = 0.15):
self.col_margin = col_margin
self.initial_but_a_pos = initial_but_a_pos
self.initial_but_b_pos = initial_but_b_pos
self.slot_diff = slot_diff
class CubeExplorer(object):
def __init__(self, cube, plot_func, current_slice, *args, **kwargs):
"""
Args:
* cube:
Cube of data to plot.
* plot_func:
Pointer to a plotting function
which is compatible with the slice defined
by current_slice.
* current_slice:
Index tuple which gives a slice of cube
which is compatible with plot_func.
"""
self.cube = cube
self.plot_func = plot_func
self.current_slice = current_slice
self.axes_hook = kwargs.pop('axes_hook', None)
self.button_setup = kwargs.pop('button_setup',
ButtonSetup())
# self._ax is set when the plot is made
self._ax = None
self._plot_args = args
self._plot_kwargs = kwargs
# _butts and _butt_fns are added to with
# various class methods
self._butts = {}
self._butt_fns = {}
self._make_plot()
def show(self):
plt.show()
def add_nav_buttons(self, dim, names_tup, slot=0, circular=False):
"""
Adds a set of two buttons to the plot window which allow incrementing
or decrementing over the specified dimension
Args:
* dim:
Dimension number or name to traverse.
* names_tup:
Tuple of two strings to be the names of the
increment and decrement buttons respectively.
* slot:
Level of the plot to display this button set.
* circular:
Boolean - to loop round when limit is reached or not.
"""
if type(dim) is str:
dim, = self.cube.coord_dims(self.cube.coord(dim))
plt.subplots_adjust(right=self.button_setup.col_margin)
if type(self.current_slice[dim]) is slice:
raise TypeError("Cannot iterate over a displayed dimension")
but_pos_a = self.button_setup.initial_but_a_pos
but_pos_a[1] -= slot*self.button_setup.slot_diff
self._butts[names_tup[0]] = Button(plt.axes(but_pos_a), names_tup[0])
self._butt_fns[names_tup[0]] = self._get_nav_fn(dim, 'inc', circular)
self._butts[names_tup[0]].on_clicked(self._butt_fns[names_tup[0]])
but_pos_b = self.button_setup.initial_but_b_pos
but_pos_b[1] -= slot*self.button_setup.slot_diff
self._butts[names_tup[1]] = Button(plt.axes(but_pos_b), names_tup[1])
self._butt_fns[names_tup[1]] = self._get_nav_fn(dim, 'dec', circular)
self._butts[names_tup[1]].on_clicked(self._butt_fns[names_tup[1]])
# set axis back to plot
plt.sca(self._ax)
def add_animate_buttons(self, dim, names_tup, slot=0, refresh_rate=0.2):
"""
Adds a set of two buttons to start/stop cycling through a dimension
of a cube.
Args:
* dim:
Dimension number/name to traverse.
* names_tup:
Tuple of two strings to be the names of the
play and stop buttons respectively.
* slot:
Level of the plot to display this button set.
* refresh_rate:
Number of seconds to wait between each frame.
"""
if type(dim) is str:
dim, = self.cube.coord_dims(self.cube.coord(dim))
plt.subplots_adjust(right=0.85)
self._butts[names_tup[0]] = Button(plt.axes([0.875, 0.85-(slot*0.15), 0.11, 0.05]), names_tup[0])
self._butts[names_tup[1]] = Button(plt.axes([0.875, 0.79-(slot*0.15), 0.11, 0.05]), names_tup[1])
play_fn, stop_fn = self._get_ani_fns(dim, refresh_rate)
self._butt_fns[names_tup[0]] = play_fn
self._butt_fns[names_tup[1]] = stop_fn
self._butts[names_tup[0]].on_clicked(self._butt_fns[names_tup[0]])
self._butts[names_tup[1]].on_clicked(self._butt_fns[names_tup[1]])
# set axis back to plot
plt.sca(self._ax)
def add_picker(self, plot_func, *args, **kwargs):
"""
Adds picker functionality to a Cube Explorer object.
The user can then select points from which to display
data in one of the non-displayed dimension in a pop
up plot by clicking on the cube explorer plot.
Args:
* plot_func:
A plotting function that accepts a cube
consisting of all the non-displayed dimensions.
"""
ax_hook = kwargs.pop('ax_hook', None)
# add picker option to plot
self._plot_kwargs['picker'] = kwargs.pop('picker', True)
self._refresh_plot()
def _on_pick(event):
if event.artist.get_axes() != self._ax:
return
s = [slice(None)] * len(self.cube.shape)
picker_plot_dims = [i for i, v in enumerate(self.current_slice) if type(v) is slice]
xpt = event.mouseevent.xdata
s[picker_plot_dims[0]] = int(xpt)
if len(picker_plot_dims) == 2:
ypt = event.mouseevent.ydata
s[picker_plot_dims[1]] = int(ypt)
plt.figure(num=None)
plot_func(self.cube[tuple(s)], *args, **kwargs)
if ax_hook != None:
ax = plt.gca()
ax_hook(ax)
plt.show()
self._ax.figure.canvas.mpl_connect('pick_event', _on_pick)
def _make_plot(self):
"""
Makes initial plot
"""
self.fig = plt.figure(num=None)
# Make the initial plot.
self.pl = self.plot_func(self.cube[tuple(self.current_slice)], *self._plot_args, **self._plot_kwargs)
self._ax = plt.gca()
if self.axes_hook is not None:
self.axes_hook(self._ax)
def _refresh_plot(self):
"""
Refreshes the displayed plot to display the slice defined
by current_slice.
"""
self._ax.clear()
self.plot_func(self.cube[tuple(self.current_slice)], *self._plot_args, **self._plot_kwargs)
if self.axes_hook is not None:
self.axes_hook(self._ax)
self.fig.canvas.draw()
def _get_nav_fn(self, dim, inc_or_dec, circular):
"""
Returns increment and decrement button functions for a dimension.
"""
if inc_or_dec is 'inc':
def fn(event):
if self.current_slice[dim] < self.cube.shape[dim]-1:
self.current_slice[dim] += 1
elif circular:
self.current_slice[dim] = 0
self._refresh_plot()
elif inc_or_dec is 'dec':
def fn(event):
if self.current_slice[dim] > 0:
self.current_slice[dim] -= 1
elif circular:
self.current_slice[dim] = self.cube.shape[dim]
self._refresh_plot()
return fn
def _get_ani_fns(self, dim, refresh_rate):
def play(event):
self.playing = True
while True:
self.fig.canvas.start_event_loop(timeout=refresh_rate)
if self.playing:
if self.current_slice[dim] < self.cube.shape[dim]-1:
self.current_slice[dim] += 1
else:
self.current_slice[dim] = 0
self._refresh_plot()
def stop(event):
self.playing = False
return play, stop
if __name__ == '__main__':
cube = iris.load_cube(iris.sample_data_path('GloSea4', 'ensemble_001.pp'))
def axes_hook_main(ax):
ax.coastlines()
ax.set_title('Depth slices')
def axes_hook_picker(ax):
ax.set_title('Time series')
ce = CubeExplorer(cube, iplt.pcolormesh, [0, slice(None), slice(None)], cmap=mpl_cm.get_cmap('brewer_OrRd_09'), axes_hook=axes_hook_main)
ce.add_nav_buttons('time', ("Up", "Down"), slot=0)
ce.add_animate_buttons(0, ("Play", "Stop"), slot=1)
ce.add_picker(qplt.pcolormesh, ax_hook=axes_hook_picker)
ce.show()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment