In [1]:
import iris
In [2]:
from ipywidgets import interact, interactive, fixed
import ipywidgets as widgets
from IPython.display import display
First we define a generic class which contains ipywidget components which gives us sliders through our data's dimensions.
In [3]:
class CubeWidget(object):
@classmethod
def create(cls, *args, **kwargs):
"""
We do a little dance to avoid having to implement a proper DOMWidget.
That is the approach we should really be taking here...
"""
widget_creator = cls(*args, **kwargs)
widget_creator.form.widget_creator = widget_creator
return widget_creator.form
def __init__(self, cube):
"""
Create the widget interface for the given cube.
"""
form = widgets.VBox()
self._shape = cube.shape
self._ndim = len(self._shape)
self._cube = cube
self._current_index = None
self.cube_summary = widgets.HTML('')
dim_sliders = {}
children = [self.cube_summary]
# A maping of dimension number to slider instance.
self._dim_sliders = {}
self._last_index = None
for dim, dim_length in list(enumerate(self._shape))[::-1]:
coords_on_dim = cube.coords(dimensions=[dim])
is_slider = cube.ndim - dim > 2
if coords_on_dim:
name = coords_on_dim[0].name()
else:
name = 'Axis {}'.format(dim)
toggle = widgets.Checkbox(value=is_slider)
label_widget = widgets.HTML(name)
slider = widgets.IntSlider(min=0, max=dim_length - 1, visible=is_slider)
self._dim_sliders[dim] = slider
container = widgets.HBox(children=[label_widget, slider, toggle])
def on_dimension_toggle_fn(slider):
def on_dimension_toggle(name, value):
slider.visible = value
return on_dimension_toggle
toggle.on_trait_change(on_dimension_toggle_fn(slider), 'value')
slider.on_trait_change(self.on_change, 'value')
toggle.on_trait_change(self.on_change, 'value')
children.append(container)
form.children = children
self.form = form
self.finalise()
def finalise(self):
# Called when the initialisation is complete, and we want to trigger the first event.
return self.on_change(None, None)
def on_change(self, name, value):
"""
An event which is triggered when any widget value is changed.
"""
the_index = [slice(None)] * self._ndim
for dim, slider in self._dim_sliders.items():
if slider.visible:
the_index[dim] = slider.value
if self._current_index != the_index:
self._current_index = tuple(the_index)
sub_cube = self._cube[self._current_index]
summary = '<b>{}</b>'.format(sub_cube.summary(True))
scalar_info = []
for scalar_coord in sub_cube.coords(dimensions=[]):
units = scalar_coord.units
if (units in ['1', 'no_unit', 'unknown'] or
units.is_time_reference()):
unit = ''
else:
unit = ' {!s}'.format(units)
if units.is_time_reference():
msg = '{}'.format(units.num2date(scalar_coord.points[0]))
else:
msg = '{} {}'.format(scalar_coord.points[0], unit)
scalar_info.append('<span style="padding-left: 40px; width: 275px; display:inline-block; '
'white-space:nowrap;">'
'{}:</span><span>{}</span>'.format(scalar_coord.name(), msg))
sep = '<br />'
summary += sep + sep.join(scalar_info)
self.cube_summary.value = summary
We can load some data to navigate through it (textually):
In [4]:
cube = iris.load_cube('./GloSea4_sample_data/ensemble_01?.pp')
In [5]:
widget = CubeWidget(cube)
widget.form
Next we will define a subclass of this navigator which adds plotting functionality.
In [6]:
%matplotlib notebook
In [7]:
import matplotlib.pyplot as plt
import iris.plot as iplt
import iris.quickplot as qplt
import cartopy
In [8]:
class Contourf(object):
def __init__(self):
self.axes = None
self.cs = None
def draw(self, cube):
if self.axes:
plt.sca(self.axes)
self.cs = iplt.contourf(cube, cmap='GnBu')
# Get the current axes, since Iris may change the instance.
self.axes = plt.gca()
def clear(self):
if self.axes and self.axes in self.axes.figure.axes:
self.axes.figure.axes.remove(self.axes)
self.axes = None
class ContourfCoastlines(Contourf):
def __init__(self):
super(ContourfCoastlines, self).__init__()
self.coastlined_axes = None
def draw(self, cube):
super(ContourfCoastlines, self).draw(cube)
is_geoaxes = isinstance(self.axes, cartopy.mpl.geoaxes.GeoAxes)
if is_geoaxes and self.coastlined_axes is not self.axes:
self.axes.coastlines()
self.coastlined_axes = self.axes
self.axes.gridlines()
class Line(object):
def __init__(self):
self.axes = None
self.line = None
def draw(self, cube):
if self.line and self.axes and self.line in self.axes.lines:
self.line.set_ydata(cube.data)
self.axes.relim()
self.axes.autoscale_view()
else:
self.line, = qplt.plot(cube)
self.axes = plt.gca()
plt.grid(True)
def clear(self):
if self.axes and self.axes in self.axes.figure.axes:
self.axes.figure.axes.remove(self.axes)
self.axes = None
class PlotWidget(CubeWidget):
def __init__(self, cube, draw_tools):
self.fig = plt.figure()
self.draw_tools = draw_tools
self._axes_dims = []
self._draw_tool = None
super(PlotWidget, self).__init__(cube)
def on_change(self, name, value):
super(PlotWidget, self).on_change(name, value)
plot_dims = [dim for dim, slider in self._dim_sliders.items()
if not slider.visible]
if self._axes_dims != plot_dims:
self._axes_dims = plot_dims
ax = plt.subplot(1, 1, 1)
ax.clear()
for tool in self.draw_tools.values():
tool.clear()
tool.axes = ax
sub_cube = self._cube[self._current_index]
draw_tool = self.draw_tools.get(sub_cube.ndim, None)
if draw_tool:
self._draw_tool = draw_tool
draw_tool.draw(sub_cube)
else:
# No draw possible for this number of dimensions.
plt.clf()
plt.draw()
In [9]:
PlotWidget.create(cube, {1: Line(), 2: ContourfCoastlines()})