Last active
December 18, 2015 03:48
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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) |
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
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.