Skip to content

Instantly share code, notes, and snippets.

@ajdawson
Last active December 18, 2015 03:48
Show Gist options
  • Save ajdawson/5720537 to your computer and use it in GitHub Desktop.
Save ajdawson/5720537 to your computer and use it in GitHub Desktop.
This is a fairly bare-bones proof of concept for a cube explorer type function. I didn't bother to add buttons for exploring over more than one dimension, but the code supports it. It only supports iris plot functions, it needs logic adding to handle matplotlib functions.
"""
Very basic implementation of a cube explorer function.
This is a proof of concept rather than a solid design idea. It is
currently hard-wired to work on one dimension only although the
workings allow for multiple dimensions, I just didn't get round to
writing code to add the buttons for these!
"""
import functools
import iris
import iris.plot as iplt
import matplotlib.pyplot as plt
from matplotlib.widgets import Button
import numpy as np
class CoordTraverser(object):
"""
Generates slicers to slice cubes/arrays.
The purpose is to keep track of which slice of the cube is being
viewed.
"""
def __init__(self, coord_shape, traversable_dims):
"""Initialize with the shape of the cube."""
self._indices = [None] * len(coord_shape)
for dim in traversable_dims:
self._indices[dim] = 0
self._coord_shape = coord_shape
def _slicers_from_indices(self):
slicers = []
for dim in xrange(len(self._coord_shape)):
if self._indices[dim] is None:
# slice for the whole dimension
slicers.append(slice(0, None, None))
else:
# single value to extract a slice of other dimensions
slicers.append(self._indices[dim])
# Iris cubes require tuple of slice objects, not a list
# (don't know why)
return tuple(slicers)
def slicers(self):
return self._slicers_from_indices()
def increment(self, dim):
if self._coord_shape[dim] is None:
raise ValueError('dimension is not traversable: {:d}'.format(dim))
if self._indices[dim] == self._coord_shape[dim] - 1:
self._indices[dim] = 0
else:
self._indices[dim] += 1
def decrement(self, dim):
if self._coord_shape[dim] is None:
raise ValueError('dimension is not traversable: {:d}'.format(dim))
if self._indices[dim] == 0:
self._indices[dim] = self._coord_shape[dim] - 1
else:
self._indices[dim] -= 1
def cube_explorer(cube, plot_func, excoords, *args, **kwargs):
"""
Plot a cube with navigation buttons.
Currently plot_func must be an iris plot function. A check should
be added to see if this is the case and if not use the cubes data
attribute for plotting instead of the cube.
"""
# Remove keyword args that are for the cube_explorer function.
axes_hook = kwargs.pop('axes_hook', None)
# Work out the dimension numbers of the coordinates to traverse.
dims = [cube.coord_dims(cube.coord(coord))[0] for coord in excoords]
ndims = len(dims)
if (len(dims) - cube.ndim) > 2:
raise ValueError('can only handle 1-D or 2-D')
# create a traversal helper
traverser = CoordTraverser(cube.shape, dims)
# Make the initial plot.
plot_func(cube[traverser.slicers()], *args, **kwargs)
ax = plt.gca()
if axes_hook is not None:
axes_hook(ax)
def _generic_callback(dim, ax, event, backwards=False):
"""A generic callback function that must be specialized."""
plt.axes(ax)
if backwards:
traverser.decrement(dim)
else:
traverser.increment(dim)
slicers = traverser.slicers()
plot_func(cube[slicers], *args, **kwargs)
if axes_hook is not None:
axes_hook(ax)
plt.draw()
# Create button callbacks for each dimension to be traversed.
callbacks = [functools.partial(_generic_callback, dim, ax, backwards=False)
for dim in dims]
rcallbacks = [functools.partial(_generic_callback, dim, ax, backwards=True)
for dim in dims]
# Add the navigation buttons and hook them up to the appropriate callbacks.
# NOTE: currently this only adds one set, for the first dimension, more
# could be added, the callbacks are all generated anyway...
axprev = plt.axes([0.7, 0.05, 0.1, 0.075])
axnext = plt.axes([0.81, 0.05, 0.1, 0.075])
bnext = Button(axnext, 'Next')
bnext.on_clicked(callbacks[0])
bprev = Button(axprev, 'Prev')
bprev.on_clicked(rcallbacks[0])
plt.show()
if __name__ == '__main__':
cube = iris.load_cube(iris.sample_data_path('GloSea4', 'ensemble_001.pp'))
def axes_hook(ax):
ax.coastlines()
ax.set_title('I am the title of every plot!')
cube_explorer(cube, iplt.contourf, ['time'], cmap=plt.cm.gist_rainbow, axes_hook=axes_hook)
@niallrobinson
Copy link

I can't get it to run for some opaque reason. I guess to extend it to an arbitrary number of dimension we need to start messing around with dictionaries of buttons/functions. the only other thing I'd say is it might be worth returning somthing from cube_explorer so that you can add on other things afterwords e.g. more buttons or a point picker. Looking good though

@ajdawson
Copy link
Author

ajdawson commented Jun 6, 2013

I guess to extend it to an arbitrary number of dimension we need to start messing around with dictionaries of buttons/functions.

The callbacks are all there already, you'd just need to actually create the buttons that use them. I guess some function to layout and generate them is what is called for.

@niallrobinson
Copy link

Yeh - it won't be too much work to extend that but it would be nice. Also be nice to be able to label the buttons. The only other thing is an argument to say if the navigation should wrap or not - we could just make it wrap be default.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment