Skip to content

Commit 33a763a

Browse files
committed
Extend 1D plotting capabilities. Resolves #581.
1 parent 8c57eb7 commit 33a763a

23 files changed

+391
-54
lines changed

CHANGES

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,32 @@
1+
Release 1.5 (unreleased)
2+
========================
3+
4+
5+
Features added
6+
--------------
7+
* The functions `iris.plot.plot` and `iris.quickplot.plot` now take up to two
8+
arguments, which may be cubes or coordinates, allowing the user to have full
9+
control over what is plotted on each axis. The coords keyword argument is now
10+
deprecated for these functions.
11+
12+
Bugs fixed
13+
----------
14+
* N/A
15+
16+
Incompatible changes
17+
--------------------
18+
* N/A
19+
20+
Deprecations
21+
------------
22+
* The coords keyword argument for `iris.plot.plot` and `iris.quickplot.plot`
23+
has been deprecated due to the new API which accepts multiple cubes or
24+
coordinates.
25+
26+
27+
----------------------------
28+
29+
130
Release 1.4 (unreleased)
231
========================
332

docs/iris/example_code/graphics/SOI_filtering.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,11 @@ def main():
7878

7979
# plot the SOI time series and both filtered versions
8080
plt.figure(figsize=(9, 4))
81-
iplt.plot(soi, coords=['time'], color='0.7', linewidth=1., linestyle='-',
81+
iplt.plot(soi, color='0.7', linewidth=1., linestyle='-',
8282
alpha=1., label='no filter')
83-
iplt.plot(soi24, coords=['time'], color='b', linewidth=2., linestyle='-',
83+
iplt.plot(soi24, color='b', linewidth=2., linestyle='-',
8484
alpha=.7, label='2-year filter')
85-
iplt.plot(soi84, coords=['time'], color='r', linewidth=2., linestyle='-',
85+
iplt.plot(soi84, color='r', linewidth=2., linestyle='-',
8686
alpha=.7, label='7-year filter')
8787
plt.ylim([-4, 4])
8888
plt.title('Southern Oscillation Index (Darwin Only)')

docs/iris/example_code/graphics/lagged_ensemble.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def main():
126126

127127
for ensemble_member in mean.slices(['time']):
128128
# draw each ensemble member as a dashed line in black
129-
iplt.plot(ensemble_member, '--k', coords=['time'])
129+
iplt.plot(ensemble_member, '--k')
130130

131131
plt.title('Mean temperature anomaly for ENSO 3.4 region')
132132
plt.xlabel('Time')

docs/iris/src/whatsnew/1.5.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
What's new in Iris 1.5
2+
**********************
3+
4+
:Release: 1.5.0
5+
:Date: unreleased
6+
7+
This document explains the new/changed features of Iris in version 1.5.
8+
9+
Iris 1.5 features
10+
=================
11+
12+
A summary of the main features added with version 1.5:
13+
14+
* The functions :func:`iris.plot.plot` and :func:`iris.quickplot.plot` now take
15+
up to two arguments, which may be cubes or coordinates, allowing the user to
16+
have full control over what is plotted on each axis. The coords keyword argument
17+
is now deprecated for these functions.
18+
19+
Bugs fixed
20+
----------
21+
* N/A
22+
23+
Incompatible changes
24+
--------------------
25+
* N/A
26+
27+
Deprecations
28+
------------
29+
* The coords keyword argument for :func:`iris.plot.plot` and :func:`iris.quickplot.plot`
30+
has been deprecated due to the new API which accepts multiple cubes or coordinates.

lib/iris/plot.py

Lines changed: 173 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
import collections
2626
import datetime
27+
import functools
28+
import warnings
2729

2830
import matplotlib.axes
2931
import matplotlib.collections as mpl_collections
@@ -305,35 +307,89 @@ def _fixup_dates(coord, values):
305307
return values
306308

307309

308-
def _draw_1d_from_points(draw_method_name, arg_func, cube, *args, **kwargs):
309-
# NB. In the interests of clarity we use "u" to refer to the horizontal
310-
# axes on the matplotlib plot.
310+
def _data_from_coord_or_cube(c):
311+
if isinstance(c, iris.cube.Cube):
312+
data = c.data
313+
elif isinstance(c, iris.coords.Coord):
314+
data = _fixup_dates(c, c.points)
315+
else:
316+
raise TypeError('Plot arguments must be cubes or coordinates.')
317+
return data
318+
319+
320+
def _uv_from_u_object_v_object(u_object, v_object):
321+
ndim_msg = 'Cube or coordinate must be 1-dimensional. Got {} dimensions.'
322+
if u_object is not None and u_object.ndim > 1:
323+
raise ValueError(ndim_msg.format(u_object.ndim))
324+
if v_object.ndim > 1:
325+
raise ValueError(ndim_msg.format(v_object.ndim))
326+
type_msg = 'Plot arguments must be cubes or coordinates.'
327+
v = _data_from_coord_or_cube(v_object)
328+
if u_object is None:
329+
u = np.arange(v.shape[0])
330+
else:
331+
u = _data_from_coord_or_cube(u_object)
332+
return u, v
311333

312-
# get & remove the coords entry from kwargs
313-
coords = kwargs.pop('coords', None)
314-
mode = iris.coords.POINT_MODE
315-
if coords is not None:
316-
plot_defn = _get_plot_defn_custom_coords_picked(cube, coords, mode,
317-
ndims=1)
334+
335+
def _u_object_from_v_object(v_object):
336+
u_object = None
337+
if isinstance(v_object, iris.cube.Cube):
338+
plot_defn = _get_plot_defn(v_object, iris.coords.POINT_MODE, ndims=1)
339+
u_object, = plot_defn.coords
340+
return u_object
341+
342+
343+
def _get_plot_objects(args):
344+
if len(args) > 1 and isinstance(args[1],
345+
(iris.cube.Cube, iris.coords.Coord)):
346+
# two arguments
347+
u_object, v_object = args[:2]
348+
u, v = _uv_from_u_object_v_object(*args[:2])
349+
args = args[2:]
350+
if len(u) != len(v):
351+
msg = "The x and y-axis objects are not compatible. They should " \
352+
"have equal sizes but got ({}: {}) and ({}: {})."
353+
raise ValueError(msg.format(u_object.name(), len(u),
354+
v_object.name(), len(v)))
318355
else:
319-
plot_defn = _get_plot_defn(cube, mode, ndims=1)
356+
# single argument
357+
v_object = args[0]
358+
u_object = _u_object_from_v_object(v_object)
359+
u, v = _uv_from_u_object_v_object(u_object, args[0])
360+
args = args[1:]
361+
return u_object, v_object, u, v, args
320362

321-
data = cube.data
322363

323-
# Obtain U coordinates
324-
u_coord, = plot_defn.coords
325-
if u_coord:
326-
u = u_coord.points
327-
u = _fixup_dates(u_coord, u)
364+
def _draw_1d_from_points(draw_method_name, arg_func, *args, **kwargs):
365+
# NB. In the interests of clarity we use "u" to refer to the horizontal
366+
# axes on the matplotlib plot and "v" for the vertical axes.
367+
368+
# retrieve the objects that are plotted on the horizontal and vertical
369+
# axes (cubes or coordinates) and their respective values, along with the
370+
# argument tuple with these objects removed
371+
u_object, v_object, u, v, args = _get_plot_objects(args)
372+
373+
# if both u_object and v_object are coordinates then check if a map
374+
# should be drawn
375+
if isinstance(u_object, iris.coords.Coord) and \
376+
isinstance(v_object, iris.coords.Coord) and \
377+
_can_draw_map([v_object, u_object]):
378+
# Replace non-cartopy subplot/axes with a cartopy alternative and set
379+
# the transform keyword.
380+
draw_method, kwargs = _geoaxes_draw_method_and_kwargs(u_object,
381+
v_object,
382+
draw_method_name,
383+
kwargs)
328384
else:
329-
u = np.arange(data.shape[0])
385+
# just use a pyplot function to draw
386+
draw_method = getattr(plt, draw_method_name)
330387

331-
draw_method = getattr(plt, draw_method_name)
332388
if arg_func is not None:
333-
args, kwargs = arg_func(u, data, *args, **kwargs)
389+
args, kwargs = arg_func(u, v, *args, **kwargs)
334390
result = draw_method(*args, **kwargs)
335391
else:
336-
result = draw_method(u, data, *args, **kwargs)
392+
result = draw_method(u, v, *args, **kwargs)
337393

338394
return result
339395

@@ -362,6 +418,31 @@ def _get_cartopy_axes(cartopy_proj):
362418
return ax
363419

364420

421+
def _geoaxes_draw_method_and_kwargs(x_coord, y_coord, draw_method_name,
422+
kwargs):
423+
"""
424+
Retrieve a GeoAxes draw method and appropriate keyword arguments for
425+
calling it given the coordinates and existing keywords.
426+
427+
"""
428+
if x_coord.coord_system != y_coord.coord_system:
429+
raise ValueError('The X and Y coordinates must have equal coordinate'
430+
' systems.')
431+
cs = x_coord.coord_system
432+
if cs is not None:
433+
cartopy_proj = cs.as_cartopy_projection()
434+
else:
435+
cartopy_proj = cartopy.crs.PlateCarree()
436+
ax = _get_cartopy_axes(cartopy_proj)
437+
draw_method = getattr(ax, draw_method_name)
438+
# Set the "from transform" keyword.
439+
new_kwargs = kwargs.copy()
440+
assert 'transform' not in new_kwargs, 'Transform keyword is not allowed.'
441+
new_kwargs['transform'] = cartopy_proj
442+
443+
return draw_method, new_kwargs
444+
445+
365446
def _map_common(draw_method_name, arg_func, mode, cube, plot_defn,
366447
*args, **kwargs):
367448
"""
@@ -410,24 +491,11 @@ def _map_common(draw_method_name, arg_func, mode, cube, plot_defn,
410491
x = np.append(x, x[:, 0:1] + 360 * direction, axis=1)
411492
data = ma.concatenate([data, data[:, 0:1]], axis=1)
412493

413-
# Replace non-cartopy subplot/axes with a cartopy alternative.
414-
if x_coord.coord_system != y_coord.coord_system:
415-
raise ValueError('The X and Y coordinates must have equal coordinate'
416-
' systems.')
417-
cs = x_coord.coord_system
418-
if cs:
419-
cartopy_proj = cs.as_cartopy_projection()
420-
else:
421-
cartopy_proj = cartopy.crs.PlateCarree()
422-
ax = _get_cartopy_axes(cartopy_proj)
423-
424-
draw_method = getattr(ax, draw_method_name)
425-
426-
# Set the "from transform" keyword.
427-
# NB. While cartopy doesn't support spherical contours, just use the
428-
# projection as the source CRS.
429-
assert 'transform' not in kwargs, 'Transform keyword is not allowed.'
430-
kwargs['transform'] = cartopy_proj
494+
# Replace non-cartopy subplot/axes with a cartopy alternative and set the
495+
# transform keyword.
496+
draw_method, kwargs = _geoaxes_draw_method_and_kwargs(x_coord, y_coord,
497+
draw_method_name,
498+
kwargs)
431499

432500
if arg_func is not None:
433501
new_args, kwargs = arg_func(x, y, data, *args, **kwargs)
@@ -706,9 +774,67 @@ def points(cube, *args, **kwargs):
706774
*args, **kwargs)
707775

708776

709-
def plot(cube, *args, **kwargs):
777+
def _1d_coords_deprecation_handler(func):
778+
"""
779+
Manage the deprecation of the coords keyword argument to 1d plot
780+
functions.
781+
782+
"""
783+
@functools.wraps(func)
784+
def _wrapper(*args, **kwargs):
785+
coords = kwargs.pop('coords', None)
786+
if coords is not None:
787+
# issue a deprecation warning and check to see if the old
788+
# interface should be mimicked for the deprecation period
789+
warnings.warn('The coords keyword argument is deprecated.',
790+
stacklevel=2)
791+
if len(coords) != 1:
792+
msg = 'The list of coordinates given should have length 1 ' \
793+
'but it has length {}.'
794+
raise ValueError(msg.format(len(coords)))
795+
if isinstance(args[0], iris.cube.Cube):
796+
if len(args) < 2 or not isinstance(args[1], (iris.cube.Cube,
797+
iris.coords.Coord)):
798+
if isinstance(coords[0], basestring):
799+
coord = args[0].coord(name=coords[0])
800+
else:
801+
coord = args[0].coord(coord=coords[0])
802+
if not args[0].coord_dims(coord):
803+
raise ValueError("The coordinate {!r} doesn't "
804+
"span a data dimension."
805+
"".format(coord.name()))
806+
args = (coord,) + args
807+
return func(*args, **kwargs)
808+
return _wrapper
809+
810+
811+
@_1d_coords_deprecation_handler
812+
def plot(*args, **kwargs):
710813
"""
711-
Draws a line plot based on the given Cube.
814+
Draws a line plot based on the given cube(s) or coordinate(s).
815+
816+
The first one or two arguments may be cubes or coordinates to plot.
817+
Each of the following is valid::
818+
819+
# plot a 1d cube against its dimension coordinate
820+
plot(cube)
821+
822+
# plot a 1d coordinate
823+
plot(coord)
824+
825+
# plot a 1d cube against a given 1d coordinate, with the cube
826+
# values on the y-axis and the coordinate on the x-axis
827+
plot(coord, cube)
828+
829+
# plot a 1d cube against a given 1d coordinate, with the cube
830+
# values on the x-axis and the coordinate on the y-axis
831+
plot(cube, coord)
832+
833+
# plot two 1d coordinates against one-another
834+
plot(coord1, coord2)
835+
836+
# plot two 1d cubes against one-another
837+
plot(cube1, cube2)
712838
713839
Kwargs:
714840
@@ -718,12 +844,17 @@ def plot(cube, *args, **kwargs):
718844
element is the horizontal axis of the plot and the second element is
719845
the vertical axis of the plot.
720846
721-
See :func:`matplotlib.pyplot.plot` for details of other valid keyword
847+
.. deprecated:: 1.5
848+
849+
The plot coordinates can be specified explicitly as in the
850+
above examples, so this keyword is no longer needed.
851+
852+
See :func:`matplotlib.pyplot.plot` for details of valid keyword
722853
arguments.
723854
724855
"""
725856
_plot_args = None
726-
return _draw_1d_from_points('plot', _plot_args, cube, *args, **kwargs)
857+
return _draw_1d_from_points('plot', _plot_args, *args, **kwargs)
727858

728859

729860
# Provide convenience show method from pyplot

0 commit comments

Comments
 (0)