From 3030f865dccfdaed7d4a3a517978379eaa4f3536 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Mon, 3 Aug 2015 10:21:59 -0700 Subject: [PATCH 01/37] add parameter add_labels=True to turn off labeling --- .travis.yml | 2 +- doc/plotting.rst | 150 +++++++++++++++++++++++--- xray/plot/__init__.py | 2 + xray/plot/facetgrid.py | 231 +++++++++++++++++++++++++++++++++++++++++ xray/plot/plot.py | 102 +++++++++++++----- xray/test/test_plot.py | 61 +++++++++++ 6 files changed, 505 insertions(+), 43 deletions(-) create mode 100644 xray/plot/facetgrid.py diff --git a/.travis.yml b/.travis.yml index 3538bf9e0a5..67b22de9822 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ matrix: fast_finish: true include: - python: 2.6 - env: UPDATE_ENV="conda install unittest2 pandas==0.15.0 h5py cython && pip install h5netcdf" + env: UPDATE_ENV="conda install unittest2 pandas==0.15.0 h5py cython matplotlib && pip install h5netcdf" # Test on Python 2.7 with and without netCDF4/scipy/cdat-lite - python: 2.7 env: UPDATE_ENV="conda install -c scitools cdat-lite h5py cython matplotlib && pip install cyordereddict h5netcdf" diff --git a/doc/plotting.rst b/doc/plotting.rst index 0fef6b5d493..4628e71a0cd 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -1,6 +1,7 @@ Plotting ======== + Introduction ------------ @@ -8,6 +9,9 @@ The goal of xray's plotting is to make exploratory plotting quick and easy by using metadata from :py:class:`xray.DataArray` objects to add informative labels. To plot :py:class:`xray.Dataset` objects simply access the relevant DataArrays, ie ``dset['var1']``. +Here we focus mostly on arrays 2d or larger. If your data fits +nicely into a pandas DataFrame then you're better off using one of the more +developed tools there. Xray plotting functionality is a thin wrapper around the popular `matplotlib `_ library. @@ -45,6 +49,123 @@ The following imports are necessary for all of the examples. import matplotlib.pyplot as plt import xray +Faceting +-------- + +Xray's basic plotting is useful for plotting two dimensional arrays. What +about three or four dimensional arrays? + +Consider the temperature data set. There are 4 observations per day for two +years. Let's use a slice to pick 6 times throughout the first year. + +.. ipython:: python + + # TODO- define and use function load_dataset + temperature = xray.open_dataset('/users/clark.fitzgerald/dev/xray-data/ncep_temperature_north-america_2013-14.nc') + air = temperature['air'] + + t = air.isel(time=slice(0, 365 * 4, 250)) + t + + +One way to visualize this data is to make a +seperate plot for each time period. This is what we call faceting; splitting an +array along one dimension into different facets and plotting each one. + +Simple Example +~~~~~~~~~~~~~~ + +Here's one way to do it in matplotlib: + +.. ipython:: python + + fig, axes = plt.subplots(nrows=2, ncols=3, sharex=True, sharey=True) + + kwargs = {'vmin': t.min(), 'vmax': t.max(), 'add_colorbar': False} + + for i in range(len(t.time)): + ax = axes.flat[i] + im = t[i].plot(ax=ax, **kwargs) + #** hack fix for Vim syntax highlight + ax.set_xlabel('') + ax.set_ylabel('') + ax.set_title(t.time.values[i]) + + #plt.colorbar(im, ax=axes.tolist()) + + @savefig plot_facet_simple.png height=12in + plt.show() + +We can use ``map_dataarray`` on a DataArray: + +.. ipython:: python + + g = xray.plot.FacetGrid(t, col='time', col_wrap=2) + + @savefig plot_facet_dataarray.png height=12in + g.map_dataarray(xray.plot.contourf, 'lon', 'lat') + +Iterating over the FacetGrid iterates over the individual axes. + +.. ipython:: python + + g = xray.plot.FacetGrid(t, col='time', col_wrap=2) + g.map_dataarray(xray.plot.contourf, 'lon', 'lat') + + for i, ax in enumerate(g): + ax.set_title('Air Temperature %d' % i) + + @savefig plot_facet_iterator.png height=12in + plt.show() + +In this case we don't actually need to pass these args in: + +.. ipython:: python + + g = xray.plot.FacetGrid(t, col='time', col_wrap=2) + + @savefig plot_facet_dataarray2.png height=12in + g.map_dataarray(xray.plot.contourf) + +This design picks out the data args and lets us use any matplotlib +function with a Dataset: + +.. ipython:: python + + tds = t.to_dataset() + g = xray.plot.FacetGrid(tds, 'time', col_wrap=2) + + @savefig plot_facet_mapds.png height=12in + g.map(plt.contourf, 'lon', 'lat', 'air') + +But this really isn't possible for a DataArray since what would the last +arg be here? + +.. ipython:: python + + g = xray.plot.FacetGrid(t, 'time', col_wrap=2) + + @savefig plot_facet_mpl_contourf.png height=12in + g.map(plt.contourf, 'lon', 'lat', Ellipsis) + +In this interest of getting something useful that's internally consistent +and bounded in scope here's +a rough proposal- Get it all working nicely for DataArrays now, and then +revisit the question of whether and how to provide support for Datasets. + +Implement the +``map_dataarray`` method that requires the plotting function to accept a +DataArray as the first arg. + + +More +~~~~~~~~~~~~~~ + +Xray faceted plotting uses an API and code from `Seaborn +`_ + + + One Dimension ------------- @@ -59,7 +180,7 @@ Xray uses the coordinate name to label the x axis: sinpts = xray.DataArray(np.sin(t), {'t': t}, name='sin(t)') sinpts - @savefig plotting_example_sin.png width=4in + @savefig plot_sin.png width=4in sinpts.plot() Additional Arguments @@ -76,10 +197,10 @@ can be used: .. ipython:: python - @savefig plotting_example_sin2.png width=4in + @savefig plot_sin2.png width=4in sinpts.plot.line('b-^') -.. warning:: +.. note:: Not all xray plotting methods support passing positional arguments to the wrapped matplotlib functions, but they do all support keyword arguments. @@ -88,7 +209,7 @@ Keyword arguments work the same way, and are more explicit. .. ipython:: python - @savefig plotting_example_sin3.png width=4in + @savefig plot_sin3.png width=4in sinpts.plot.line(color='purple', marker='o') Adding to Existing Axis @@ -143,8 +264,10 @@ calls :py:func:`xray.plot.imshow`. .. ipython:: python - a = xray.DataArray(np.zeros((4, 3)), dims=('y', 'x')) - a[0, 0] = 1 + a0 = xray.DataArray(np.zeros((4, 3, 2)), dims=('y', 'x', 'z'), + name='temperature') + a0[0, 0, 0] = 1 + a = a0.isel(z=0) a The plot will produce an image corresponding to the values of the array. @@ -216,19 +339,13 @@ Changing Axes ~~~~~~~~~~~~~ To swap the variables plotted on vertical and horizontal axes one can -transpose the array. +pass in the names of the coordinates to be plotted on x and y axis. .. ipython:: python + # TODO - verify works @savefig plotting_changing_axes.png width=4in - distance.T.plot() - -To make x and y increase: - -.. ipython:: python - - @savefig plotting_changing_axes2.png width=4in - distance.T.plot(xincrease=True, yincrease=True) + distance.plot(x='y') Nonuniform Coordinates ~~~~~~~~~~~~~~~~~~~~~~ @@ -264,7 +381,7 @@ matplotlib is available. @savefig plotting_2d_call_matplotlib.png width=4in plt.show() -.. warning:: +.. note:: Xray methods update label information and generally play around with the axes. So any kind of updates to the plot @@ -310,6 +427,7 @@ later. halfd = distance / 2 halfd.plot(ax=axes[1], **kwargs) + #** hack vim syntax highlight plt.colorbar(im, ax=axes.tolist()) diff --git a/xray/plot/__init__.py b/xray/plot/__init__.py index ae2f6ad8f6f..3cd075b6b90 100644 --- a/xray/plot/__init__.py +++ b/xray/plot/__init__.py @@ -1,2 +1,4 @@ from .plot import (plot, line, contourf, contour, hist, imshow, pcolormesh) + +from .facetgrid import FacetGrid diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py new file mode 100644 index 00000000000..c2954e6fe9a --- /dev/null +++ b/xray/plot/facetgrid.py @@ -0,0 +1,231 @@ +import numpy as np + + +class FacetGrid_seaborn(object): + ''' + Copied from Seaborn + ''' + + def __init__(self, data, row=None, col=None, col_wrap=None, + sharex=True, sharey=True, size=3, aspect=1, + dropna=True, legend_out=True, despine=True, + margin_titles=False, xlim=None, ylim=None, subplot_kws=None, + gridspec_kws=None): + + import matplotlib as mpl + import matplotlib.pyplot as plt + + MPL_GRIDSPEC_VERSION = LooseVersion('1.4') + OLD_MPL = LooseVersion(mpl.__version__) < MPL_GRIDSPEC_VERSION + + if row: + row_names = data[row].values + nrow = len(row_names) + else: + nrow = 1 + + if col: + col_names = data[col].values + ncol = len(col_names) + else: + ncol = 1 + + # Compute the grid shape + self._n_facets = ncol * nrow + + self._col_wrap = col_wrap + if col_wrap is not None: + if row is not None: + err = "Cannot use `row` and `col_wrap` together." + raise ValueError(err) + ncol = col_wrap + nrow = int(np.ceil(len(data[col].unique()) / col_wrap)) + self._ncol = ncol + self._nrow = nrow + + # Calculate the base figure size + # This can get stretched later by a legend + figsize = (ncol * size * aspect, nrow * size) + + # Validate some inputs + if col_wrap is not None: + margin_titles = False + + # Build the subplot keyword dictionary + subplot_kws = {} if subplot_kws is None else subplot_kws.copy() + gridspec_kws = {} if gridspec_kws is None else gridspec_kws.copy() + if xlim is not None: + subplot_kws["xlim"] = xlim + if ylim is not None: + subplot_kws["ylim"] = ylim + + # Initialize the subplot grid + if col_wrap is None: + kwargs = dict(figsize=figsize, squeeze=False, + sharex=sharex, sharey=sharey, + subplot_kw=subplot_kws, + gridspec_kw=gridspec_kws) + + if OLD_MPL: + _ = kwargs.pop('gridspec_kw', None) + if gridspec_kws: + msg = "gridspec module only available in mpl >= {}" + warnings.warn(msg.format(MPL_GRIDSPEC_VERSION)) + + fig, axes = plt.subplots(nrow, ncol, **kwargs) + self.axes = axes + + else: + # If wrapping the col variable we need to make the grid ourselves + if gridspec_kws: + warnings.warn("`gridspec_kws` ignored when using `col_wrap`") + + n_axes = len(col_names) + fig = plt.figure(figsize=figsize) + axes = np.empty(n_axes, object) + axes[0] = fig.add_subplot(nrow, ncol, 1, **subplot_kws) + if sharex: + subplot_kws["sharex"] = axes[0] + if sharey: + subplot_kws["sharey"] = axes[0] + for i in range(1, n_axes): + axes[i] = fig.add_subplot(nrow, ncol, i + 1, **subplot_kws) + self.axes = axes + + # Now we turn off labels on the inner axes + if sharex: + for ax in self._not_bottom_axes: + for label in ax.get_xticklabels(): + label.set_visible(False) + ax.xaxis.offsetText.set_visible(False) + if sharey: + for ax in self._not_left_axes: + for label in ax.get_yticklabels(): + label.set_visible(False) + ax.yaxis.offsetText.set_visible(False) + + # Set up the class attributes + # --------------------------- + + # First the public API + self.data = data + self.fig = fig + self.axes = axes + + #self.row_names = row_names + #self.col_names = col_names + + # Next the private variables + self._nrow = nrow + self._row_var = row + self._ncol = ncol + self._col_var = col + + self._margin_titles = margin_titles + self._col_wrap = col_wrap + self._legend_out = legend_out + self._legend = None + self._legend_data = {} + self._x_var = None + self._y_var = None + + +class FacetGrid(object): + ''' + Mostly copied from Seaborn + ''' + + def __init__(self, darray, col=None, col_wrap=None): + import matplotlib.pyplot as plt + self.darray = darray + #self.row = row + self.col = col + self.col_wrap = col_wrap + + self.nfacet = len(darray[col]) + + # Compute grid shape + if col_wrap is not None: + self.ncol = col_wrap + else: + # TODO- add heuristic for inference here to get a nice shape + # like 3 x 4 + self.ncol = self.nfacet + + self.nrow = int(np.ceil(self.nfacet / self.ncol)) + + self.fig, self.axes = plt.subplots(self.nrow, self.ncol, + sharex=True, sharey=True) + + def __iter__(self): + return self.axes.flat + + def map_dataarray(self, func, *args, **kwargs): + """Apply a plotting function to each facet's subset of the data. + + Differs from Seaborn style - requires the func to know how to plot a + dataarray. + + Parameters + ---------- + func : callable + A plotting function with the first argument an xray dataarray + args : + positional arguments to func + kwargs : + keyword arguments to func + + Returns + ------- + self : object + Returns self. + + """ + import matplotlib.pyplot as plt + + for ax, (name, data) in zip(self, self.darray.groupby(self.col)): + + plt.sca(ax) + + # For now I'm going to write this assuming func is an xray 2d + # plotting function + func(data, *args, add_colorbar=False, **kwargs) + + return self + + def map(self, func, *args, **kwargs): + """Apply a plotting function to each facet's subset of the data. + + True to Seaborn style + + Parameters + ---------- + func : callable + A plotting function that takes data and keyword arguments. It + must plot to the currently active matplotlib Axes and take a + `color` keyword argument. If faceting on the `hue` dimension, + it must also take a `label` keyword argument. + args : strings + Column names in self.data that identify variables with data to + plot. The data for each variable is passed to `func` in the + order the variables are specified in the call. + kwargs : keyword arguments + All keyword arguments are passed to the plotting function. + + Returns + ------- + self : object + Returns self. + + """ + import matplotlib.pyplot as plt + + for ax, (name, data) in zip(self, self.darray.groupby(self.col)): + + kwargs['add_colorbar'] = False + plt.sca(ax) + + innerargs = [data[a] for a in args] + func(*innerargs, **kwargs) + + return self diff --git a/xray/plot/plot.py b/xray/plot/plot.py index 665400f8844..78d851d8dfc 100644 --- a/xray/plot/plot.py +++ b/xray/plot/plot.py @@ -6,8 +6,12 @@ DataArray.plot._____ """ +from __future__ import division import pkg_resources import functools +from textwrap import dedent +from itertools import cycle +from distutils.version import LooseVersion import numpy as np import pandas as pd @@ -16,11 +20,6 @@ from ..core.pycompat import basestring -# TODO - implement this -class FacetGrid(): - pass - - # Maybe more appropriate to keep this in .utils def _right_dtype(arr, types): """ @@ -55,6 +54,17 @@ def _load_default_cmap(fname='default_colormap.csv'): return LinearSegmentedColormap.from_list('viridis', cm_data) +def _title_for_slice(darray): + ''' + If the dataarray comes from a slice we can show that info in the title + ''' + title = [] + for dim, coord in darray.coords.items(): + if coord.size == 1: + title.append('{dim} = {v}'.format(dim=dim, v=coord.values)) + return ', '.join(title) + + def plot(darray, ax=None, rtol=0.01, **kwargs): """ Default plot of DataArray using matplotlib / pylab. @@ -123,7 +133,8 @@ def line(darray, *args, **kwargs): ndims = len(darray.dims) if ndims != 1: raise ValueError('Line plots are for 1 dimensional DataArrays. ' - 'Passed DataArray has {} dimensions'.format(ndims)) + 'Passed DataArray has {ndims} ' + 'dimensions'.format(ndims=ndims)) # Ensures consistency with .plot method ax = kwargs.pop('ax', None) @@ -181,7 +192,7 @@ def hist(darray, ax=None, **kwargs): ax.set_ylabel('Count') if darray.name is not None: - ax.set_title('Histogram of {}'.format(darray.name)) + ax.set_title('Histogram of {0}'.format(darray.name)) return primitive @@ -361,6 +372,10 @@ def _plot2d(plotfunc): ---------- darray : DataArray Must be 2 dimensional + x : string, optional + Coordinate for x axis. If None use darray.dims[1] + y : string, optional + Coordinate for y axis. If None use darray.dims[0] ax : matplotlib axes object, optional If None, uses the current axis xincrease : None, True, or False, optional @@ -371,6 +386,8 @@ def _plot2d(plotfunc): if None, use the default for the matplotlib function add_colorbar : Boolean, optional Adds colorbar to axis + add_labels : Boolean, optional + Use xray metadata to label axes vmin, vmax : floats, optional Values to anchor the colormap, otherwise they are inferred from the data and other keyword arguments. When a diverging dataset is inferred, @@ -407,8 +424,8 @@ def _plot2d(plotfunc): plotfunc.__doc__ = '\n'.join((plotfunc.__doc__, commondoc)) @functools.wraps(plotfunc) - def newplotfunc(darray, ax=None, xincrease=None, yincrease=None, - add_colorbar=True, vmin=None, vmax=None, cmap=None, + def newplotfunc(darray, x=None, y=None, ax=None, xincrease=None, yincrease=None, + add_colorbar=True, add_labels=True, vmin=None, vmax=None, cmap=None, center=None, robust=False, extend=None, levels=None, **kwargs): # All 2d plots in xray share this function signature. @@ -419,25 +436,53 @@ def newplotfunc(darray, ax=None, xincrease=None, yincrease=None, if ax is None: ax = plt.gca() - try: - ylab, xlab = darray.dims - except ValueError: + dims = list(darray.dims) + + if len(dims) != 2: raise ValueError('{} plots are for 2 dimensional DataArrays. ' 'Passed DataArray has {} dimensions' - .format(plotfunc.__name__, len(darray.dims))) + .format(plotfunc.__name__, len(dims))) + + if x and x not in darray: + raise KeyError('{} is not a dimension of this DataArray. Use {} or {} for x' + .format(x, *darray.dims)) + + if y and y not in darray: + raise KeyError('{} is not a dimension of this DataArray. Use {} or {} for y' + .format(y, *darray.dims)) + + # Get label names + if x and y: + xlab = x + ylab = y + elif x and not y: + xlab = x + del dims[dims.index(x)] + ylab = dims.pop() + elif y and not x: + ylab = y + del dims[dims.index(y)] + xlab = dims.pop() + else: + ylab, xlab = dims # some plotting functions only know how to handle ndarrays - x = darray[xlab].values - y = darray[ylab].values - z = darray.to_masked_array(copy=False) + xval = darray[xlab].values + yval = darray[ylab].values + zval = darray.to_masked_array(copy=False) - _ensure_plottable(x, y) + # May need to transpose for correct x, y labels + if xlab == darray.dims[0]: + zval = zval.T + + _ensure_plottable(xval, yval) if 'contour' in plotfunc.__name__ and levels is None: levels = 7 # this is the matplotlib default + filled = plotfunc.__name__ != 'contour' - cmap_params = _determine_cmap_params(z.data, vmin, vmax, cmap, center, + cmap_params = _determine_cmap_params(zval.data, vmin, vmax, cmap, center, robust, extend, levels, filled) if 'contour' in plotfunc.__name__: @@ -450,17 +495,22 @@ def newplotfunc(darray, ax=None, xincrease=None, yincrease=None, # This allows the user to pass in a custom norm coming via kwargs kwargs.setdefault('norm', cmap_params['cnorm']) - ax, primitive = plotfunc(x, y, z, ax=ax, + ax, primitive = plotfunc(xval, yval, zval, ax=ax, cmap=cmap_params['cmap'], vmin=cmap_params['vmin'], vmax=cmap_params['vmax'], **kwargs) - ax.set_xlabel(xlab) - ax.set_ylabel(ylab) + # Label the plot with metadata + if add_labels: + ax.set_xlabel(xlab) + ax.set_ylabel(ylab) + ax.set_title(_title_for_slice(darray)) if add_colorbar: - plt.colorbar(primitive, ax=ax, extend=cmap_params['extend']) + cbar = plt.colorbar(primitive, ax=ax, extend=cmap_params['extend']) + if darray.name and add_labels: + cbar.set_label(darray.name) _update_axes_limits(ax, xincrease, yincrease) @@ -468,13 +518,13 @@ def newplotfunc(darray, ax=None, xincrease=None, yincrease=None, # For use as DataArray.plot.plotmethod @functools.wraps(newplotfunc) - def plotmethod(_PlotMethods_obj, ax=None, xincrease=None, yincrease=None, - add_colorbar=True, vmin=None, vmax=None, cmap=None, + def plotmethod(_PlotMethods_obj, x=None, y=None, ax=None, xincrease=None, yincrease=None, + add_colorbar=True, add_labels=True, vmin=None, vmax=None, cmap=None, center=None, robust=False, extend=None, levels=None, **kwargs): - return newplotfunc(_PlotMethods_obj._da, ax=ax, xincrease=xincrease, + return newplotfunc(_PlotMethods_obj._da, x=x, y=y, ax=ax, xincrease=xincrease, yincrease=yincrease, add_colorbar=add_colorbar, - vmin=vmin, vmax=vmax, cmap=cmap, + add_labels=add_labels, vmin=vmin, vmax=vmax, cmap=cmap, center=center, robust=robust, extend=extend, levels=levels, **kwargs) diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index 20ed7b95a26..9d95234fcad 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -375,6 +375,50 @@ def test_diverging_color_limits(self): vmin, vmax = artist.get_clim() self.assertAlmostEqual(-vmin, vmax) + def test_xy_strings(self): + self.plotmethod('y', 'x') + ax = plt.gca() + self.assertEqual('y', ax.get_xlabel()) + self.assertEqual('x', ax.get_ylabel()) + + def test_positional_x_string(self): + self.plotmethod('y') + ax = plt.gca() + self.assertEqual('y', ax.get_xlabel()) + self.assertEqual('x', ax.get_ylabel()) + + def test_y_string(self): + self.plotmethod(y='x') + ax = plt.gca() + self.assertEqual('y', ax.get_xlabel()) + self.assertEqual('x', ax.get_ylabel()) + + def test_bad_x_string_exception(self): + with self.assertRaisesRegexp(KeyError, r'y'): + self.plotmethod('not_a_real_dim') + + def test_default_title(self): + a = DataArray(np.random.randn(4, 3, 2, 1), dims=['a', 'b', 'c', 'd']) + self.plotfunc(a.isel(c=1)) + title = plt.gca().get_title() + self.assertEqual('c = 1, d = 0', title) + + def test_colorbar_label(self): + self.darray.name = 'testvar' + self.plotmethod() + alltxt = [t.get_text() for t in plt.gcf().findobj(mpl.text.Text)] + # Set comprehension not compatible with Python 2.6 + alltxt = set(alltxt) + self.assertIn(self.darray.name, alltxt) + + def test_no_labels(self): + self.darray.name = 'testvar' + self.plotmethod(add_labels=False) + alltxt = [t.get_text() for t in plt.gcf().findobj(mpl.text.Text)] + alltxt = set(alltxt) + for string in ['x', 'y', 'testvar']: + self.assertNotIn(string, alltxt) + class TestContourf(Common2dMixin, PlotTestCase): @@ -453,3 +497,20 @@ def test_can_change_aspect(self): def test_primitive_artist_returned(self): artist = self.plotmethod() self.assertTrue(isinstance(artist, mpl.image.AxesImage)) + + +class TestFacetGrid(PlotTestCase): + + def setUp(self): + d = np.arange(10 * 15 * 3).reshape(10, 15, 3) + self.darray = DataArray(d, dims=['y', 'x', 'z']) + self.g = xplt.FacetGrid(self.darray, col='z') + + def test_loop_over_axes(self): + self.g.map_dataarray(xplt.contourf, 'x', 'y') + for ax in self.g: + self.assertTrue(ax.has_data()) + + def test_colorbar_same_scale(self): + self.g.map_dataarray(xplt.contourf, 'x', 'y') + pass From 30385f846c49e74face7b481b0b5fd021aba24c3 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Wed, 12 Aug 2015 18:04:19 -0700 Subject: [PATCH 02/37] facets share common colorbar --- doc/plotting.rst | 38 ++++++++++++++++++-------------------- xray/plot/facetgrid.py | 18 +++++++++++++++--- xray/plot/plot.py | 12 +++++++----- xray/test/test_plot.py | 17 +++++++++++++---- 4 files changed, 53 insertions(+), 32 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 4628e71a0cd..1db4c2caf13 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -91,7 +91,7 @@ Here's one way to do it in matplotlib: ax.set_ylabel('') ax.set_title(t.time.values[i]) - #plt.colorbar(im, ax=axes.tolist()) + plt.colorbar(im) @savefig plot_facet_simple.png height=12in plt.show() @@ -118,15 +118,6 @@ Iterating over the FacetGrid iterates over the individual axes. @savefig plot_facet_iterator.png height=12in plt.show() -In this case we don't actually need to pass these args in: - -.. ipython:: python - - g = xray.plot.FacetGrid(t, col='time', col_wrap=2) - - @savefig plot_facet_dataarray2.png height=12in - g.map_dataarray(xray.plot.contourf) - This design picks out the data args and lets us use any matplotlib function with a Dataset: @@ -138,16 +129,6 @@ function with a Dataset: @savefig plot_facet_mapds.png height=12in g.map(plt.contourf, 'lon', 'lat', 'air') -But this really isn't possible for a DataArray since what would the last -arg be here? - -.. ipython:: python - - g = xray.plot.FacetGrid(t, 'time', col_wrap=2) - - @savefig plot_facet_mpl_contourf.png height=12in - g.map(plt.contourf, 'lon', 'lat', Ellipsis) - In this interest of getting something useful that's internally consistent and bounded in scope here's a rough proposal- Get it all working nicely for DataArrays now, and then @@ -157,6 +138,23 @@ Implement the ``map_dataarray`` method that requires the plotting function to accept a DataArray as the first arg. +4 dimensional +~~~~~~~~~~~~~~ + +For 4 dimensional arrays we can use the rows and columns. + +.. ipython:: python + + # Make a 4d array + t3 = t.isel(time=slice(0, 3)) + t4d = xray.concat([t3, t3 + 10], 'fourth_dim') + t4d.coords + + g = xray.plot.FacetGrid(t4d, col='time', row='fouth_dim') + + @savefig plot_facet_4d.png height=12in + g.map_dataarray(xray.plot.contourf, 'lon', 'lat') + More ~~~~~~~~~~~~~~ diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index c2954e6fe9a..b765b766d07 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -166,6 +166,9 @@ def map_dataarray(self, func, *args, **kwargs): Differs from Seaborn style - requires the func to know how to plot a dataarray. + For now I'm going to write this assuming func is an xray 2d + plotting function + Parameters ---------- func : callable @@ -183,13 +186,22 @@ def map_dataarray(self, func, *args, **kwargs): """ import matplotlib.pyplot as plt + defaults = dict(add_colorbar=False, + add_labels=False, + vmin=float(self.darray.min()), + vmax=float(self.darray.max()), + ) + + defaults.update(kwargs) + for ax, (name, data) in zip(self, self.darray.groupby(self.col)): plt.sca(ax) - # For now I'm going to write this assuming func is an xray 2d - # plotting function - func(data, *args, add_colorbar=False, **kwargs) + func(data, *args, **defaults) + + plt.title('{coord} = {val}'.format(coord=self.col, + val=str(name)[:10])) return self diff --git a/xray/plot/plot.py b/xray/plot/plot.py index e15ce99fa37..a8474c65857 100644 --- a/xray/plot/plot.py +++ b/xray/plot/plot.py @@ -441,16 +441,18 @@ def newplotfunc(darray, x=None, y=None, ax=None, xincrease=None, yincrease=None, dims = list(darray.dims) if len(dims) != 2: - raise ValueError('{} plots are for 2 dimensional DataArrays. ' - 'Passed DataArray has {} dimensions' - .format(plotfunc.__name__, len(dims))) + raise ValueError('{type} plots are for 2 dimensional DataArrays. ' + 'Passed DataArray has {ndim} dimensions' + .format(type=plotfunc.__name__, ndim=len(dims))) if x and x not in darray: - raise KeyError('{} is not a dimension of this DataArray. Use {} or {} for x' + raise KeyError('{0} is not a dimension of this DataArray. Use ' + '{1} or {2} for x' .format(x, *darray.dims)) if y and y not in darray: - raise KeyError('{} is not a dimension of this DataArray. Use {} or {} for y' + raise KeyError('{0} is not a dimension of this DataArray. Use ' + '{1} or {2} for y' .format(y, *darray.dims)) # Get label names diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index d715f8ee5c3..736ba8d4505 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -517,11 +517,20 @@ def setUp(self): self.darray = DataArray(d, dims=['y', 'x', 'z']) self.g = xplt.FacetGrid(self.darray, col='z') - def test_loop_over_axes(self): - self.g.map_dataarray(xplt.contourf, 'x', 'y') + def test_no_args(self): + self.g.map_dataarray(xplt.contourf) for ax in self.g: self.assertTrue(ax.has_data()) - def test_colorbar_same_scale(self): + def test_names_in_title(self): self.g.map_dataarray(xplt.contourf, 'x', 'y') - pass + for i, ax in enumerate(self.g): + self.assertEqual('z = {0}'.format(i), ax.get_title()) + + def test_colorbar_same_scale(self): + self.g.map_dataarray(xplt.imshow, 'x', 'y') + contours = plt.gcf().findobj(mpl.image.AxesImage) + + # They should all have the same color limits + clims = set((ax.get_clim() for ax in contours)) + self.assertEqual(1, len(clims)) From 9e9d8ee84f44851ecc3eefbddce66049204e41a8 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 13 Aug 2015 10:19:28 -0700 Subject: [PATCH 03/37] display colorbar on facetgrid --- xray/plot/facetgrid.py | 3 ++- xray/test/test_plot.py | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index b765b766d07..e53cd7a4366 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -198,11 +198,12 @@ def map_dataarray(self, func, *args, **kwargs): plt.sca(ax) - func(data, *args, **defaults) + mappable = func(data, *args, **defaults) plt.title('{coord} = {val}'.format(coord=self.col, val=str(name)[:10])) + plt.colorbar(mappable, ax=self.axes.ravel().tolist()) return self def map(self, func, *args, **kwargs): diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index 736ba8d4505..19e7dad99bc 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -527,10 +527,15 @@ def test_names_in_title(self): for i, ax in enumerate(self.g): self.assertEqual('z = {0}'.format(i), ax.get_title()) - def test_colorbar_same_scale(self): + def test_colors_same_scale(self): self.g.map_dataarray(xplt.imshow, 'x', 'y') contours = plt.gcf().findobj(mpl.image.AxesImage) # They should all have the same color limits clims = set((ax.get_clim() for ax in contours)) self.assertEqual(1, len(clims)) + + def test_row_and_col(self): + a = np.arange(10 * 15 * 3 * 2).reshape(10, 15, 3, 2) + d = DataArray(a) + From 9f12a324585137fb18468c36c806e5022d28d750 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 13 Aug 2015 11:49:24 -0700 Subject: [PATCH 04/37] beginning logic for row/col facets --- xray/plot/facetgrid.py | 14 ++++++++++++-- xray/test/test_plot.py | 31 ++++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index e53cd7a4366..a85d27ca051 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -135,13 +135,23 @@ class FacetGrid(object): Mostly copied from Seaborn ''' - def __init__(self, darray, col=None, col_wrap=None): + def __init__(self, darray, col=None, row=None, col_wrap=None): import matplotlib.pyplot as plt self.darray = darray - #self.row = row + self.row = row self.col = col self.col_wrap = col_wrap + if col and row: + if col_wrap is not None: + pass + elif col and not row: + pass + elif not col and row: + pass + else: + raise ValueError('Pass a coordinate name as an argument for row or col') + self.nfacet = len(darray[col]) # Compute grid shape diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index 19e7dad99bc..5bcd6fbfc42 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -430,6 +430,14 @@ def test_no_labels(self): for string in ['x', 'y', 'testvar']: self.assertNotIn(string, alltxt) + def test_facetgrid(self): + a = np.arange(10 * 15 * 3).reshape(10, 15, 3) + d = DataArray(a, dims=['y', 'x', 'z']) + g = xplt.FacetGrid(d, col='z') + g.map_dataarray(self.plotfunc, 'x', 'y') + for ax in g: + self.assertTrue(ax.has_data()) + class TestContourf(Common2dMixin, PlotTestCase): @@ -527,15 +535,28 @@ def test_names_in_title(self): for i, ax in enumerate(self.g): self.assertEqual('z = {0}'.format(i), ax.get_title()) - def test_colors_same_scale(self): + def test_colorbar(self): self.g.map_dataarray(xplt.imshow, 'x', 'y') - contours = plt.gcf().findobj(mpl.image.AxesImage) + images = plt.gcf().findobj(mpl.image.AxesImage) # They should all have the same color limits - clims = set((ax.get_clim() for ax in contours)) + clims = set([ax.get_clim() for ax in images]) self.assertEqual(1, len(clims)) - def test_row_and_col(self): + # One colorbar + cbar = plt.gcf().findobj(mpl.collections.QuadMesh) + self.assertEqual(1, len(cbar)) + + def test_row_and_col_shape(self): a = np.arange(10 * 15 * 3 * 2).reshape(10, 15, 3, 2) - d = DataArray(a) + d = DataArray(a, dims=['y', 'x', 'col', 'row']) + g = xplt.FacetGrid(d, col='col', row='row') + self.assertEqual((2, 3), g.axes.shape) + + def test_norow_nocol_error(self): + with self.assertRaisesRegexp(ValueError, r'[Rr]ow'): + xplt.FacetGrid(self.darray) + def test_colwrap_error(self): + with self.assertRaisesRegexp(ValueError, r'[Rr]ow'): + xplt.FacetGrid(self.darray) From 8516c5cf2cac47111510267c4f5e5ecd9b7d76d6 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 13 Aug 2015 14:49:09 -0700 Subject: [PATCH 05/37] about to bring in set_label from seaborn --- doc/plotting.rst | 13 ++- xray/plot/facetgrid.py | 224 +++++++++++++++++++++++++++++------------ xray/test/test_plot.py | 1 + 3 files changed, 167 insertions(+), 71 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 1db4c2caf13..30fa6f7f29f 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -46,6 +46,7 @@ The following imports are necessary for all of the examples. .. ipython:: python import numpy as np + import pandas as pd import matplotlib.pyplot as plt import xray @@ -145,15 +146,17 @@ For 4 dimensional arrays we can use the rows and columns. .. ipython:: python - # Make a 4d array - t3 = t.isel(time=slice(0, 3)) - t4d = xray.concat([t3, t3 + 10], 'fourth_dim') + t2 = t.isel(time=slice(0, 2)) + t4d = xray.concat([t2, t2 + 50], pd.Index(['normal', 'hot'], name='fourth_dim')) + # This is a 4d array t4d.coords - g = xray.plot.FacetGrid(t4d, col='time', row='fouth_dim') + g = xray.plot.FacetGrid(t4d, col='time', row='fourth_dim') + g.map_dataarray(xray.plot.imshow, 'lon', 'lat') + @savefig plot_facet_4d.png height=12in - g.map_dataarray(xray.plot.contourf, 'lon', 'lat') + g.set_titles() More diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index a85d27ca051..507108129bf 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -1,3 +1,5 @@ +import warnings + import numpy as np @@ -7,17 +9,12 @@ class FacetGrid_seaborn(object): ''' def __init__(self, data, row=None, col=None, col_wrap=None, - sharex=True, sharey=True, size=3, aspect=1, - dropna=True, legend_out=True, despine=True, margin_titles=False, xlim=None, ylim=None, subplot_kws=None, gridspec_kws=None): import matplotlib as mpl import matplotlib.pyplot as plt - MPL_GRIDSPEC_VERSION = LooseVersion('1.4') - OLD_MPL = LooseVersion(mpl.__version__) < MPL_GRIDSPEC_VERSION - if row: row_names = data[row].values nrow = len(row_names) @@ -43,67 +40,30 @@ def __init__(self, data, row=None, col=None, col_wrap=None, self._ncol = ncol self._nrow = nrow - # Calculate the base figure size - # This can get stretched later by a legend - figsize = (ncol * size * aspect, nrow * size) - # Validate some inputs if col_wrap is not None: margin_titles = False - # Build the subplot keyword dictionary - subplot_kws = {} if subplot_kws is None else subplot_kws.copy() - gridspec_kws = {} if gridspec_kws is None else gridspec_kws.copy() - if xlim is not None: - subplot_kws["xlim"] = xlim - if ylim is not None: - subplot_kws["ylim"] = ylim - # Initialize the subplot grid if col_wrap is None: kwargs = dict(figsize=figsize, squeeze=False, - sharex=sharex, sharey=sharey, - subplot_kw=subplot_kws, - gridspec_kw=gridspec_kws) - - if OLD_MPL: - _ = kwargs.pop('gridspec_kw', None) - if gridspec_kws: - msg = "gridspec module only available in mpl >= {}" - warnings.warn(msg.format(MPL_GRIDSPEC_VERSION)) + sharex=True, sharey=True, + ) fig, axes = plt.subplots(nrow, ncol, **kwargs) self.axes = axes else: # If wrapping the col variable we need to make the grid ourselves - if gridspec_kws: - warnings.warn("`gridspec_kws` ignored when using `col_wrap`") - n_axes = len(col_names) fig = plt.figure(figsize=figsize) axes = np.empty(n_axes, object) axes[0] = fig.add_subplot(nrow, ncol, 1, **subplot_kws) - if sharex: - subplot_kws["sharex"] = axes[0] - if sharey: - subplot_kws["sharey"] = axes[0] + for i in range(1, n_axes): axes[i] = fig.add_subplot(nrow, ncol, i + 1, **subplot_kws) self.axes = axes - # Now we turn off labels on the inner axes - if sharex: - for ax in self._not_bottom_axes: - for label in ax.get_xticklabels(): - label.set_visible(False) - ax.xaxis.offsetText.set_visible(False) - if sharey: - for ax in self._not_left_axes: - for label in ax.get_yticklabels(): - label.set_visible(False) - ax.yaxis.offsetText.set_visible(False) - # Set up the class attributes # --------------------------- @@ -142,34 +102,75 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): self.col = col self.col_wrap = col_wrap + # _group is the grouping variable, if there is only one if col and row: + self._group = False + self.nrow = len(darray[row]) + self.ncol = len(darray[col]) if col_wrap is not None: - pass - elif col and not row: - pass - elif not col and row: - pass + warnings.warn("Can't use col_wrap when both col and row are passed") + elif row and not col: + self._group = row + elif not row and col: + self._group = col else: raise ValueError('Pass a coordinate name as an argument for row or col') - self.nfacet = len(darray[col]) - # Compute grid shape - if col_wrap is not None: - self.ncol = col_wrap - else: - # TODO- add heuristic for inference here to get a nice shape - # like 3 x 4 - self.ncol = self.nfacet - - self.nrow = int(np.ceil(self.nfacet / self.ncol)) + if self._group: + self.nfacet = len(darray[self._group]) + if col: + # TODO - could add heuristic for nice shape like 3x4 + self.ncol = self.nfacet + if row: + self.ncol = 1 + if col_wrap is not None: + # Overrides previous settings + self.ncol = col_wrap + self.nrow = int(np.ceil(self.nfacet / self.ncol)) self.fig, self.axes = plt.subplots(self.nrow, self.ncol, sharex=True, sharey=True) + # Next the private variables + ''' + self._nrow = nrow + self._row_var = row + self._ncol = ncol + self._col_var = col + + self._margin_titles = margin_titles + self._col_wrap = col_wrap + ''' + def __iter__(self): return self.axes.flat + def map_dataarray2(self, func, *args, **kwargs): + """Experimenting with row and col + """ + import matplotlib.pyplot as plt + + defaults = dict(add_colorbar=False, + add_labels=False, + vmin=float(self.darray.min()), + vmax=float(self.darray.max()), + ) + + defaults.update(kwargs) + + # Looping over the indices helps keep sanity + for col in range(ncol): + for row in range(nrow): + plt.sca(axes[row, col]) + # Similar to groupby + group = darray[{self.row: row, self.col: col}] + mappable = func(group, *args, **defaults) + + plt.colorbar(mappable, ax=self.axes.ravel().tolist()) + return self + + def map_dataarray(self, func, *args, **kwargs): """Apply a plotting function to each facet's subset of the data. @@ -204,18 +205,109 @@ def map_dataarray(self, func, *args, **kwargs): defaults.update(kwargs) - for ax, (name, data) in zip(self, self.darray.groupby(self.col)): + if self._group: + # TODO - bug should groupby _group + for ax, (name, data) in zip(self, self.darray.groupby(self.col)): + plt.sca(ax) + mappable = func(data, *args, **defaults) + plt.title('{coord} = {val}'.format(coord=self.col, + val=str(name)[:10])) + else: + # Looping over the indices helps keep sanity + for col in range(self.ncol): + for row in range(self.nrow): + plt.sca(self.axes[row, col]) + # Similar to groupby + group = self.darray[{self.row: row, self.col: col}] + mappable = func(group, *args, **defaults) - plt.sca(ax) + plt.colorbar(mappable, ax=self.axes.ravel().tolist()) + + #self.set_titles() - mappable = func(data, *args, **defaults) + return self - plt.title('{coord} = {val}'.format(coord=self.col, - val=str(name)[:10])) + def set_titles(self, template=None, row_template=None, col_template=None, + **kwargs): + """Draw titles either above each facet or on the grid margins. - plt.colorbar(mappable, ax=self.axes.ravel().tolist()) + Parameters + ---------- + template : string + Template for all titles with the formatting keys {col_var} and + {col_name} (if using a `col` faceting variable) and/or {row_var} + and {row_name} (if using a `row` faceting variable). + row_template: + Template for the row variable when titles are drawn on the grid + margins. Must have {row_var} and {row_name} formatting keys. + col_template: + Template for the row variable when titles are drawn on the grid + margins. Must have {col_var} and {col_name} formatting keys. + + Returns + ------- + self: object + Returns self. + + """ + args = dict(row_var=self._row_var, col_var=self._col_var) + kwargs["size"] = kwargs.pop("size", mpl.rcParams["axes.labelsize"]) + + # Establish default templates + if row_template is None: + row_template = "{row_var} = {row_name}" + if col_template is None: + col_template = "{col_var} = {col_name}" + if template is None: + if self._row_var is None: + template = col_template + elif self._col_var is None: + template = row_template + else: + template = " | ".join([row_template, col_template]) + + if self._margin_titles: + if self.row_names is not None: + # Draw the row titles on the right edge of the grid + for i, row_name in enumerate(self.row_names): + ax = self.axes[i, -1] + args.update(dict(row_name=row_name)) + title = row_template.format(**args) + bgcolor = self.fig.get_facecolor() + ax.annotate(title, xy=(1.02, .5), xycoords="axes fraction", + rotation=270, ha="left", va="center", + backgroundcolor=bgcolor, **kwargs) + + if self.col_names is not None: + # Draw the column titles as normal titles + for j, col_name in enumerate(self.col_names): + args.update(dict(col_name=col_name)) + title = col_template.format(**args) + self.axes[0, j].set_title(title, **kwargs) + + return self + + # Otherwise title each facet with all the necessary information + if (self._row_var is not None) and (self._col_var is not None): + for i, row_name in enumerate(self.row_names): + for j, col_name in enumerate(self.col_names): + args.update(dict(row_name=row_name, col_name=col_name)) + title = template.format(**args) + self.axes[i, j].set_title(title, **kwargs) + elif self.row_names is not None and len(self.row_names): + for i, row_name in enumerate(self.row_names): + args.update(dict(row_name=row_name)) + title = template.format(**args) + self.axes[i, 0].set_title(title, **kwargs) + elif self.col_names is not None and len(self.col_names): + for i, col_name in enumerate(self.col_names): + args.update(dict(col_name=col_name)) + title = template.format(**args) + # Index the flat array so col_wrap works + self.axes.flat[i].set_title(title, **kwargs) return self + def map(self, func, *args, **kwargs): """Apply a plotting function to each facet's subset of the data. diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index 5bcd6fbfc42..ef48e276698 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -552,6 +552,7 @@ def test_row_and_col_shape(self): d = DataArray(a, dims=['y', 'x', 'col', 'row']) g = xplt.FacetGrid(d, col='col', row='row') self.assertEqual((2, 3), g.axes.shape) + g.map_dataarray(xplt.imshow, 'x', 'y') def test_norow_nocol_error(self): with self.assertRaisesRegexp(ValueError, r'[Rr]ow'): From 2f1fa0cfe5c9b4ca0decb9cf6e72d08275b5c928 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 13 Aug 2015 15:02:17 -0700 Subject: [PATCH 06/37] labeled grid using seaborn --- xray/plot/facetgrid.py | 98 ++++++++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 41 deletions(-) diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index 507108129bf..caf55641c21 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -95,7 +95,8 @@ class FacetGrid(object): Mostly copied from Seaborn ''' - def __init__(self, darray, col=None, row=None, col_wrap=None): + def __init__(self, darray, col=None, row=None, col_wrap=None, + margin_titles=True): import matplotlib.pyplot as plt self.darray = darray self.row = row @@ -105,8 +106,8 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): # _group is the grouping variable, if there is only one if col and row: self._group = False - self.nrow = len(darray[row]) - self.ncol = len(darray[col]) + self._nrow = len(darray[row]) + self._ncol = len(darray[col]) if col_wrap is not None: warnings.warn("Can't use col_wrap when both col and row are passed") elif row and not col: @@ -121,56 +122,41 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): self.nfacet = len(darray[self._group]) if col: # TODO - could add heuristic for nice shape like 3x4 - self.ncol = self.nfacet + self._ncol = self.nfacet if row: - self.ncol = 1 + self._ncol = 1 if col_wrap is not None: # Overrides previous settings - self.ncol = col_wrap - self.nrow = int(np.ceil(self.nfacet / self.ncol)) + self._ncol = col_wrap + self._nrow = int(np.ceil(self.nfacet / self._ncol)) - self.fig, self.axes = plt.subplots(self.nrow, self.ncol, + self.fig, self.axes = plt.subplots(self._nrow, self._ncol, sharex=True, sharey=True) + # Set up the lists of names for the row and column facet variables + if row is None: + row_names = [] + else: + row_names = list(darray[row].values) + + if col is None: + col_names = [] + else: + col_names = list(darray[col].values) + + self.row_names = row_names + self.col_names = col_names + # Next the private variables - ''' - self._nrow = nrow self._row_var = row - self._ncol = ncol self._col_var = col self._margin_titles = margin_titles self._col_wrap = col_wrap - ''' def __iter__(self): return self.axes.flat - def map_dataarray2(self, func, *args, **kwargs): - """Experimenting with row and col - """ - import matplotlib.pyplot as plt - - defaults = dict(add_colorbar=False, - add_labels=False, - vmin=float(self.darray.min()), - vmax=float(self.darray.max()), - ) - - defaults.update(kwargs) - - # Looping over the indices helps keep sanity - for col in range(ncol): - for row in range(nrow): - plt.sca(axes[row, col]) - # Similar to groupby - group = darray[{self.row: row, self.col: col}] - mappable = func(group, *args, **defaults) - - plt.colorbar(mappable, ax=self.axes.ravel().tolist()) - return self - - def map_dataarray(self, func, *args, **kwargs): """Apply a plotting function to each facet's subset of the data. @@ -214,16 +200,17 @@ def map_dataarray(self, func, *args, **kwargs): val=str(name)[:10])) else: # Looping over the indices helps keep sanity - for col in range(self.ncol): - for row in range(self.nrow): + for col in range(self._ncol): + for row in range(self._nrow): plt.sca(self.axes[row, col]) # Similar to groupby group = self.darray[{self.row: row, self.col: col}] mappable = func(group, *args, **defaults) - plt.colorbar(mappable, ax=self.axes.ravel().tolist()) + if self._margin_titles: + self.set_titles() - #self.set_titles() + plt.colorbar(mappable, ax=self.axes.ravel().tolist()) return self @@ -250,6 +237,7 @@ def set_titles(self, template=None, row_template=None, col_template=None, Returns self. """ + import matplotlib as mpl args = dict(row_var=self._row_var, col_var=self._col_var) kwargs["size"] = kwargs.pop("size", mpl.rcParams["axes.labelsize"]) @@ -344,3 +332,31 @@ def map(self, func, *args, **kwargs): func(*innerargs, **kwargs) return self + + + +def map_dataarray2(self, func, *args, **kwargs): + """Experimenting with row and col + """ + import matplotlib.pyplot as plt + + defaults = dict(add_colorbar=False, + add_labels=False, + vmin=float(self.darray.min()), + vmax=float(self.darray.max()), + ) + + defaults.update(kwargs) + + # Looping over the indices helps keep sanity + for col in range(ncol): + for row in range(nrow): + plt.sca(axes[row, col]) + # Similar to groupby + group = darray[{self.row: row, self.col: col}] + mappable = func(group, *args, **defaults) + + plt.colorbar(mappable, ax=self.axes.ravel().tolist()) + return self + + From ef5e2e26214da68616c5b36e4124c627e3386932 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 13 Aug 2015 17:53:08 -0700 Subject: [PATCH 07/37] rotate text to match --- doc/plotting.rst | 4 +-- xray/plot/facetgrid.py | 62 +++++++++++++++++++++--------------------- xray/test/test_plot.py | 5 ++++ 3 files changed, 37 insertions(+), 34 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 30fa6f7f29f..b84f67e50d8 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -153,10 +153,8 @@ For 4 dimensional arrays we can use the rows and columns. g = xray.plot.FacetGrid(t4d, col='time', row='fourth_dim') - g.map_dataarray(xray.plot.imshow, 'lon', 'lat') - @savefig plot_facet_4d.png height=12in - g.set_titles() + g.map_dataarray(xray.plot.imshow, 'lon', 'lat') More diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index caf55641c21..3faf280c97f 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -95,8 +95,7 @@ class FacetGrid(object): Mostly copied from Seaborn ''' - def __init__(self, darray, col=None, row=None, col_wrap=None, - margin_titles=True): + def __init__(self, darray, col=None, row=None, col_wrap=None): import matplotlib.pyplot as plt self.darray = darray self.row = row @@ -108,12 +107,15 @@ def __init__(self, darray, col=None, row=None, col_wrap=None, self._group = False self._nrow = len(darray[row]) self._ncol = len(darray[col]) + self._margin_titles = True if col_wrap is not None: warnings.warn("Can't use col_wrap when both col and row are passed") elif row and not col: self._group = row + self._margin_titles = False elif not row and col: self._group = col + self._margin_titles = False else: raise ValueError('Pass a coordinate name as an argument for row or col') @@ -150,10 +152,10 @@ def __init__(self, darray, col=None, row=None, col_wrap=None, # Next the private variables self._row_var = row self._col_var = col - - self._margin_titles = margin_titles self._col_wrap = col_wrap + self.set_titles() + def __iter__(self): return self.axes.flat @@ -196,8 +198,8 @@ def map_dataarray(self, func, *args, **kwargs): for ax, (name, data) in zip(self, self.darray.groupby(self.col)): plt.sca(ax) mappable = func(data, *args, **defaults) - plt.title('{coord} = {val}'.format(coord=self.col, - val=str(name)[:10])) + #plt.title('{coord} = {val}'.format(coord=self.col, + # val=str(name)[:10])) else: # Looping over the indices helps keep sanity for col in range(self._ncol): @@ -207,16 +209,16 @@ def map_dataarray(self, func, *args, **kwargs): group = self.darray[{self.row: row, self.col: col}] mappable = func(group, *args, **defaults) - if self._margin_titles: - self.set_titles() - - plt.colorbar(mappable, ax=self.axes.ravel().tolist()) + cbar = plt.colorbar(mappable, ax=self.axes.ravel().tolist()) + cbar.set_label(self.darray.name, rotation=270) return self - def set_titles(self, template=None, row_template=None, col_template=None, + def set_titles(self, template=None, row_template="{row_var} = {row_name}", + col_template="{col_var} = {col_name}", maxchar=10, **kwargs): - """Draw titles either above each facet or on the grid margins. + ''' + Draw titles either above each facet or on the grid margins. Parameters ---------- @@ -230,22 +232,21 @@ def set_titles(self, template=None, row_template=None, col_template=None, col_template: Template for the row variable when titles are drawn on the grid margins. Must have {col_var} and {col_name} formatting keys. + maxchar : int + Truncate strings at maxchar Returns ------- self: object Returns self. - """ + ''' import matplotlib as mpl + args = dict(row_var=self._row_var, col_var=self._col_var) kwargs["size"] = kwargs.pop("size", mpl.rcParams["axes.labelsize"]) # Establish default templates - if row_template is None: - row_template = "{row_var} = {row_name}" - if col_template is None: - col_template = "{col_var} = {col_name}" if template is None: if self._row_var is None: template = col_template @@ -254,22 +255,22 @@ def set_titles(self, template=None, row_template=None, col_template=None, else: template = " | ".join([row_template, col_template]) + def shorten(name, maxchar=maxchar): + return str(name)[:maxchar] + if self._margin_titles: - if self.row_names is not None: + if self.row_names: # Draw the row titles on the right edge of the grid for i, row_name in enumerate(self.row_names): ax = self.axes[i, -1] - args.update(dict(row_name=row_name)) + args['row_name'] = shorten(row_name) title = row_template.format(**args) - bgcolor = self.fig.get_facecolor() ax.annotate(title, xy=(1.02, .5), xycoords="axes fraction", - rotation=270, ha="left", va="center", - backgroundcolor=bgcolor, **kwargs) - - if self.col_names is not None: - # Draw the column titles as normal titles + rotation=270, ha="left", va="center", **kwargs) + if self.col_names: + # Draw the column titles as normal titles for j, col_name in enumerate(self.col_names): - args.update(dict(col_name=col_name)) + args['col_name'] = shorten(col_name) title = col_template.format(**args) self.axes[0, j].set_title(title, **kwargs) @@ -279,17 +280,18 @@ def set_titles(self, template=None, row_template=None, col_template=None, if (self._row_var is not None) and (self._col_var is not None): for i, row_name in enumerate(self.row_names): for j, col_name in enumerate(self.col_names): - args.update(dict(row_name=row_name, col_name=col_name)) + args['row_name'] = shorten(row_name) + args['col_name'] = shorten(col_name) title = template.format(**args) self.axes[i, j].set_title(title, **kwargs) elif self.row_names is not None and len(self.row_names): for i, row_name in enumerate(self.row_names): - args.update(dict(row_name=row_name)) + args['row_name'] = shorten(row_name) title = template.format(**args) self.axes[i, 0].set_title(title, **kwargs) elif self.col_names is not None and len(self.col_names): for i, col_name in enumerate(self.col_names): - args.update(dict(col_name=col_name)) + args['col_name'] = shorten(col_name) title = template.format(**args) # Index the flat array so col_wrap works self.axes.flat[i].set_title(title, **kwargs) @@ -358,5 +360,3 @@ def map_dataarray2(self, func, *args, **kwargs): plt.colorbar(mappable, ax=self.axes.ravel().tolist()) return self - - diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index ef48e276698..8917f91d48a 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -531,10 +531,15 @@ def test_no_args(self): self.assertTrue(ax.has_data()) def test_names_in_title(self): + self.darray.name = 'testvar' self.g.map_dataarray(xplt.contourf, 'x', 'y') for i, ax in enumerate(self.g): self.assertEqual('z = {0}'.format(i), ax.get_title()) + alltxt = [t.get_text() for t in plt.gcf().findobj(mpl.text.Text)] + # Set comprehension not compatible with Python 2.6 + self.assertIn(self.darray.name, alltxt) + def test_colorbar(self): self.g.map_dataarray(xplt.imshow, 'x', 'y') images = plt.gcf().findobj(mpl.image.AxesImage) From e8e2f1ee2539ecda2c0f485eb3c02bf8cdf7e6d4 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 14 Aug 2015 13:13:22 -0700 Subject: [PATCH 08/37] properly place colorbar label --- doc/plotting.rst | 12 +++++++++++- xray/plot/facetgrid.py | 5 ++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index b84f67e50d8..d5ae68af191 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -55,9 +55,19 @@ Faceting Xray's basic plotting is useful for plotting two dimensional arrays. What about three or four dimensional arrays? +Once the data is stored in an appropriate form, the code to visualize +it should be clear, natural, and not too verbose. Consider the temperature data set. There are 4 observations per day for two -years. Let's use a slice to pick 6 times throughout the first year. +years which makes for 2920 values along the time dimension. +Note that the faceted dimension should not have too many values; +faceting on the time dimension will produce 2920 plots. That's +too much to be helpful. To handle this situation try performing +an operation that reduces the size of the data in some way. For example, we +could compute the average air temperature for each month and reduce the +size of this dimension from 2920 -> 12. A simpler way is +to just take a slice on that dimension. +So let's use a slice to pick 6 times throughout the first year. .. ipython:: python diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index 3faf280c97f..c34129b8e6e 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -210,7 +210,10 @@ def map_dataarray(self, func, *args, **kwargs): mappable = func(group, *args, **defaults) cbar = plt.colorbar(mappable, ax=self.axes.ravel().tolist()) - cbar.set_label(self.darray.name, rotation=270) + + #label=self.darray.name, orientation='horizontal') + cbar.set_label(self.darray.name, rotation=270, + verticalalignment='bottom') return self From 900ec9359bd8098ff5bcebfc420377c0565a3680 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 14 Aug 2015 16:47:33 -0700 Subject: [PATCH 09/37] add failing test for figure labels --- doc/plotting.rst | 4 ++++ xray/test/test_plot.py | 28 +++++++++++++++------------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index d5ae68af191..09de1a24264 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -117,6 +117,7 @@ We can use ``map_dataarray`` on a DataArray: g.map_dataarray(xray.plot.contourf, 'lon', 'lat') Iterating over the FacetGrid iterates over the individual axes. +Pick out individual axes using the ``.axes`` attribute. .. ipython:: python @@ -126,6 +127,9 @@ Iterating over the FacetGrid iterates over the individual axes. for i, ax in enumerate(g): ax.set_title('Air Temperature %d' % i) + bottomright = g.axes[-1, -1] + bottomright.annotate('America', (240, 40)) + @savefig plot_facet_iterator.png height=12in plt.show() diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index 8917f91d48a..66071614dd7 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -21,6 +21,15 @@ pass +def text_in_fig(): + ''' + Return the set of all text in the figure + ''' + alltxt = [t.get_text() for t in plt.gcf().findobj(mpl.text.Text)] + # Set comprehension not compatible with Python 2.6 + return set(alltxt) + + @requires_matplotlib class PlotTestCase(TestCase): @@ -417,16 +426,12 @@ def test_default_title(self): def test_colorbar_label(self): self.darray.name = 'testvar' self.plotmethod() - alltxt = [t.get_text() for t in plt.gcf().findobj(mpl.text.Text)] - # Set comprehension not compatible with Python 2.6 - alltxt = set(alltxt) - self.assertIn(self.darray.name, alltxt) + self.assertIn(self.darray.name, text_in_fig()) def test_no_labels(self): self.darray.name = 'testvar' self.plotmethod(add_labels=False) - alltxt = [t.get_text() for t in plt.gcf().findobj(mpl.text.Text)] - alltxt = set(alltxt) + alltxt = text_in_fig() for string in ['x', 'y', 'testvar']: self.assertNotIn(string, alltxt) @@ -530,15 +535,16 @@ def test_no_args(self): for ax in self.g: self.assertTrue(ax.has_data()) - def test_names_in_title(self): + def test_names_appear_somewhere(self): self.darray.name = 'testvar' self.g.map_dataarray(xplt.contourf, 'x', 'y') for i, ax in enumerate(self.g): self.assertEqual('z = {0}'.format(i), ax.get_title()) - alltxt = [t.get_text() for t in plt.gcf().findobj(mpl.text.Text)] - # Set comprehension not compatible with Python 2.6 + alltxt = text_in_fig() self.assertIn(self.darray.name, alltxt) + for label in ['x', 'y']: + self.assertIn(label, alltxt) def test_colorbar(self): self.g.map_dataarray(xplt.imshow, 'x', 'y') @@ -562,7 +568,3 @@ def test_row_and_col_shape(self): def test_norow_nocol_error(self): with self.assertRaisesRegexp(ValueError, r'[Rr]ow'): xplt.FacetGrid(self.darray) - - def test_colwrap_error(self): - with self.assertRaisesRegexp(ValueError, r'[Rr]ow'): - xplt.FacetGrid(self.darray) From e99d1d50701c96ed56a9d87fe5566e73a85832a0 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Mon, 24 Aug 2015 19:20:07 -0700 Subject: [PATCH 10/37] data structures for facetgrid --- xray/plot/facetgrid.py | 55 ++++++++++++++++++++++++++---------------- xray/test/test_plot.py | 10 ++++++++ 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index c34129b8e6e..fd595f25a29 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -1,4 +1,5 @@ import warnings +import itertools import numpy as np @@ -102,26 +103,27 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): self.col = col self.col_wrap = col_wrap - # _group is the grouping variable, if there is only one + # _single_group is the grouping variable, if there is only one if col and row: - self._group = False + self._single_group = False self._nrow = len(darray[row]) self._ncol = len(darray[col]) + self.nfacet = self._nrow * self._ncol self._margin_titles = True if col_wrap is not None: warnings.warn("Can't use col_wrap when both col and row are passed") elif row and not col: - self._group = row + self._single_group = row self._margin_titles = False elif not row and col: - self._group = col + self._single_group = col self._margin_titles = False else: raise ValueError('Pass a coordinate name as an argument for row or col') # Compute grid shape - if self._group: - self.nfacet = len(darray[self._group]) + if self._single_group: + self.nfacet = len(darray[self._single_group]) if col: # TODO - could add heuristic for nice shape like 3x4 self._ncol = self.nfacet @@ -135,6 +137,7 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): self.fig, self.axes = plt.subplots(self._nrow, self._ncol, sharex=True, sharey=True) + # Set up the lists of names for the row and column facet variables if row is None: row_names = [] @@ -146,6 +149,23 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): else: col_names = list(darray[col].values) + + # Build a 2d array of dictionaries mapping names to coordinates. + + if self._single_group: + full = [{self._single_group: x} for x in + self.darray[self._single_group].values] + empty = [None for x in range(self.nfacet - len(full))] + # We could potentially used a masked array instead of None as + # the sentinel value here + name_dicts = full + empty + else: + rowcols = itertools.product(row_names, col_names) + name_dicts = [{row: r, col: c} for r, c in rowcols] + + self.name_dicts = np.array(name_dicts).reshape(self._nrow, self._ncol) + + self.row_names = row_names self.col_names = col_names @@ -193,28 +213,21 @@ def map_dataarray(self, func, *args, **kwargs): defaults.update(kwargs) - if self._group: - # TODO - bug should groupby _group - for ax, (name, data) in zip(self, self.darray.groupby(self.col)): - plt.sca(ax) - mappable = func(data, *args, **defaults) - #plt.title('{coord} = {val}'.format(coord=self.col, - # val=str(name)[:10])) - else: - # Looping over the indices helps keep sanity - for col in range(self._ncol): - for row in range(self._nrow): - plt.sca(self.axes[row, col]) - # Similar to groupby - group = self.darray[{self.row: row, self.col: col}] - mappable = func(group, *args, **defaults) + for d, ax in zip(self.name_dicts.flat, self.axes.flat): + group = self.darray[d] + mappable = func(group, ax=ax, *args, **defaults) + # All this could potentially be a post processing step cbar = plt.colorbar(mappable, ax=self.axes.ravel().tolist()) #label=self.darray.name, orientation='horizontal') cbar.set_label(self.darray.name, rotation=270, verticalalignment='bottom') + # Label x, y axes + #self.fig.text(0.5, 0.04, 'bottom', ha='center', va='top') + #self.fig.text(0.04, 0.5, '', ha='left', va='center', rotation='vertical') + return self def set_titles(self, template=None, row_template="{row_var} = {row_name}", diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index c31a924f6ca..b5c5c69fcff 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -574,3 +574,13 @@ def test_row_and_col_shape(self): def test_norow_nocol_error(self): with self.assertRaisesRegexp(ValueError, r'[Rr]ow'): xplt.FacetGrid(self.darray) + + def test_groups(self): + self.g.map_dataarray(xplt.imshow, 'x', 'y') + upperleft_dict = self.g.name_dicts[0, 0] + upperleft_array = self.darray[upperleft_dict] + z0 = self.darray.isel(z=0) + + self.assertDataArrayEqual(upperleft_array, z0) + # Not sure if we need to expose this in this way + #self.assertDataArrayEqual(self.facet_data[0, 0], z0) From 68c7b06516b2e4b8d12a806c17e1fa3c218839ab Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 25 Aug 2015 07:19:13 -0700 Subject: [PATCH 11/37] failing test for floating index --- xray/plot/facetgrid.py | 11 +++++++++-- xray/test/test_plot.py | 5 +++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index fd595f25a29..8ed0a770524 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -137,6 +137,9 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): self.fig, self.axes = plt.subplots(self._nrow, self._ncol, sharex=True, sharey=True) + # subplots flattens this array if one dimension + self.axes.shape = self._nrow, self._ncol + # Set up the lists of names for the row and column facet variables if row is None: @@ -214,8 +217,12 @@ def map_dataarray(self, func, *args, **kwargs): defaults.update(kwargs) for d, ax in zip(self.name_dicts.flat, self.axes.flat): - group = self.darray[d] - mappable = func(group, ax=ax, *args, **defaults) + func(self.darray[d], ax=ax, *args, **defaults) + + # Add the labels to the bottom left plot + defaults['add_labels'] = True + mappable = func(self.darray[self.name_dicts[0, -1]], + ax=self.axes[0, -1], *args, **defaults) # All this could potentially be a post processing step cbar = plt.colorbar(mappable, ax=self.axes.ravel().tolist()) diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index b5c5c69fcff..687082fc6b0 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -584,3 +584,8 @@ def test_groups(self): self.assertDataArrayEqual(upperleft_array, z0) # Not sure if we need to expose this in this way #self.assertDataArrayEqual(self.facet_data[0, 0], z0) + + def test_float_index(self): + self.darray.coords['z'] = [0.1, 0.2, 0.4] + g = xplt.FacetGrid(self.darray, col='z') + g.map_dataarray(xplt.imshow, 'x', 'y') From a5d37dfad1d89fddfdeef8f9c4c7d20f28b21a54 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 25 Aug 2015 07:57:53 -0700 Subject: [PATCH 12/37] failing test for nonunique index --- doc/plotting.rst | 5 ++++- xray/plot/facetgrid.py | 12 +++++++++--- xray/test/test_plot.py | 6 ++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 9aba0f8d435..02819cc9855 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -172,11 +172,14 @@ DataArray as the first arg. ~~~~~~~~~~~~~~ For 4 dimensional arrays we can use the rows and columns. +Here we create a 4 dimensional array by taking the original data and adding +a fixed amount. Now we can see what the temperature map would look like if +it were much hotter. .. ipython:: python t2 = t.isel(time=slice(0, 2)) - t4d = xray.concat([t2, t2 + 50], pd.Index(['normal', 'hot'], name='fourth_dim')) + t4d = xray.concat([t2, t2 + 40], pd.Index(['normal', 'hot'], name='fourth_dim')) # This is a 4d array t4d.coords diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index 8ed0a770524..7f06adb935f 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -217,12 +217,18 @@ def map_dataarray(self, func, *args, **kwargs): defaults.update(kwargs) for d, ax in zip(self.name_dicts.flat, self.axes.flat): - func(self.darray[d], ax=ax, *args, **defaults) + func(self.darray.loc[d], ax=ax, *args, **defaults) # Add the labels to the bottom left plot + # => plotting this one twice + # This would be easier to implement if there were separate args to + # add titles and to add axis labels defaults['add_labels'] = True - mappable = func(self.darray[self.name_dicts[0, -1]], - ax=self.axes[0, -1], *args, **defaults) + bottomleft = self.axes[-1, 0] + oldtitle = bottomleft.get_title() + mappable = func(self.darray.loc[self.name_dicts[-1, 0]], + ax=bottomleft, *args, **defaults) + bottomleft.set_title(oldtitle) # All this could potentially be a post processing step cbar = plt.colorbar(mappable, ax=self.axes.ravel().tolist()) diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index 687082fc6b0..7aac3b5b3e2 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -589,3 +589,9 @@ def test_float_index(self): self.darray.coords['z'] = [0.1, 0.2, 0.4] g = xplt.FacetGrid(self.darray, col='z') g.map_dataarray(xplt.imshow, 'x', 'y') + + def test_nonunique_index_error(self): + self.darray.coords['z'] = [0.1, 0.2, 0.2] + g = xplt.FacetGrid(self.darray, col='z') + with self.assertRaisesRegexp(ValueError, r'[Uu]nique'): + g.map_dataarray(xplt.imshow, 'x', 'y') From e7bce95cffc482d5a501147dad862e9d4faeabb5 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 25 Aug 2015 11:29:12 -0700 Subject: [PATCH 13/37] handle nonunique coordinates --- xray/plot/facetgrid.py | 19 ++++++++++--------- xray/test/test_plot.py | 3 +-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index 7f06adb935f..99606f95c67 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -121,11 +121,19 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): else: raise ValueError('Pass a coordinate name as an argument for row or col') + # Relying on short circuit behavior + # Not sure when nonunique coordinates are a problem + rep_col = col is not None and not self.darray[col].to_index().is_unique + rep_row = row is not None and not self.darray[row].to_index().is_unique + if rep_col or rep_row: + raise ValueError('Coordinates used for faceting cannot ' + 'contain repeated (nonunique) values.') + # Compute grid shape if self._single_group: self.nfacet = len(darray[self._single_group]) if col: - # TODO - could add heuristic for nice shape like 3x4 + # TODO - could add heuristic for nice shapes like 3x4 self._ncol = self.nfacet if row: self._ncol = 1 @@ -140,7 +148,6 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): # subplots flattens this array if one dimension self.axes.shape = self._nrow, self._ncol - # Set up the lists of names for the row and column facet variables if row is None: row_names = [] @@ -230,17 +237,11 @@ def map_dataarray(self, func, *args, **kwargs): ax=bottomleft, *args, **defaults) bottomleft.set_title(oldtitle) - # All this could potentially be a post processing step + # The colorbar cbar = plt.colorbar(mappable, ax=self.axes.ravel().tolist()) - - #label=self.darray.name, orientation='horizontal') cbar.set_label(self.darray.name, rotation=270, verticalalignment='bottom') - # Label x, y axes - #self.fig.text(0.5, 0.04, 'bottom', ha='center', va='top') - #self.fig.text(0.04, 0.5, '', ha='left', va='center', rotation='vertical') - return self def set_titles(self, template=None, row_template="{row_var} = {row_name}", diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index 7aac3b5b3e2..23a900971dd 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -592,6 +592,5 @@ def test_float_index(self): def test_nonunique_index_error(self): self.darray.coords['z'] = [0.1, 0.2, 0.2] - g = xplt.FacetGrid(self.darray, col='z') with self.assertRaisesRegexp(ValueError, r'[Uu]nique'): - g.map_dataarray(xplt.imshow, 'x', 'y') + g = xplt.FacetGrid(self.darray, col='z') From 11e9f2bb8e6e18a1f55a90b715edbabb3fcd8826 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 25 Aug 2015 15:48:07 -0700 Subject: [PATCH 14/37] refactor set_titles, add failing test for long titles --- doc/plotting.rst | 7 +- xray/plot/facetgrid.py | 167 ++++++++++++++++++++++++++--------------- xray/test/test_plot.py | 34 +++++++++ 3 files changed, 144 insertions(+), 64 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 02819cc9855..92c2c4dd961 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -173,8 +173,8 @@ DataArray as the first arg. For 4 dimensional arrays we can use the rows and columns. Here we create a 4 dimensional array by taking the original data and adding -a fixed amount. Now we can see what the temperature map would look like if -it were much hotter. +a fixed amount. Now we can see how the temperature maps would compare if +one were 30 degrees hotter. .. ipython:: python @@ -185,9 +185,10 @@ it were much hotter. g = xray.plot.FacetGrid(t4d, col='time', row='fourth_dim') - @savefig plot_facet_4d.png height=12in g.map_dataarray(xray.plot.imshow, 'lon', 'lat') + @savefig plot_facet_4d.png height=12in + plt.show() More ~~~~~~~~~~~~~~ diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index 99606f95c67..47d590e2b10 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -159,7 +159,6 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): else: col_names = list(darray[col].values) - # Build a 2d array of dictionaries mapping names to coordinates. if self._single_group: @@ -168,6 +167,7 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): empty = [None for x in range(self.nfacet - len(full))] # We could potentially used a masked array instead of None as # the sentinel value here + #raise ValueError name_dicts = full + empty else: rowcols = itertools.product(row_names, col_names) @@ -224,7 +224,8 @@ def map_dataarray(self, func, *args, **kwargs): defaults.update(kwargs) for d, ax in zip(self.name_dicts.flat, self.axes.flat): - func(self.darray.loc[d], ax=ax, *args, **defaults) + subset = self.darray.loc[d] + func(subset, ax=ax, *args, **defaults) # Add the labels to the bottom left plot # => plotting this one twice @@ -244,24 +245,14 @@ def map_dataarray(self, func, *args, **kwargs): return self - def set_titles(self, template=None, row_template="{row_var} = {row_name}", - col_template="{col_var} = {col_name}", maxchar=10, - **kwargs): + def set_titles(self, template="{0} = {1}", maxchar=10, **kwargs): ''' Draw titles either above each facet or on the grid margins. Parameters ---------- template : string - Template for all titles with the formatting keys {col_var} and - {col_name} (if using a `col` faceting variable) and/or {row_var} - and {row_name} (if using a `row` faceting variable). - row_template: - Template for the row variable when titles are drawn on the grid - margins. Must have {row_var} and {row_name} formatting keys. - col_template: - Template for the row variable when titles are drawn on the grid - margins. Must have {col_var} and {col_name} formatting keys. + Template for plot titles maxchar : int Truncate strings at maxchar @@ -273,58 +264,27 @@ def set_titles(self, template=None, row_template="{row_var} = {row_name}", ''' import matplotlib as mpl - args = dict(row_var=self._row_var, col_var=self._col_var) kwargs["size"] = kwargs.pop("size", mpl.rcParams["axes.labelsize"]) - # Establish default templates - if template is None: - if self._row_var is None: - template = col_template - elif self._col_var is None: - template = row_template - else: - template = " | ".join([row_template, col_template]) - + # TODO - use core string formatting def shorten(name, maxchar=maxchar): return str(name)[:maxchar] - if self._margin_titles: - if self.row_names: - # Draw the row titles on the right edge of the grid - for i, row_name in enumerate(self.row_names): - ax = self.axes[i, -1] - args['row_name'] = shorten(row_name) - title = row_template.format(**args) - ax.annotate(title, xy=(1.02, .5), xycoords="axes fraction", - rotation=270, ha="left", va="center", **kwargs) - if self.col_names: - # Draw the column titles as normal titles - for j, col_name in enumerate(self.col_names): - args['col_name'] = shorten(col_name) - title = col_template.format(**args) - self.axes[0, j].set_title(title, **kwargs) - - return self - - # Otherwise title each facet with all the necessary information - if (self._row_var is not None) and (self._col_var is not None): - for i, row_name in enumerate(self.row_names): - for j, col_name in enumerate(self.col_names): - args['row_name'] = shorten(row_name) - args['col_name'] = shorten(col_name) - title = template.format(**args) - self.axes[i, j].set_title(title, **kwargs) - elif self.row_names is not None and len(self.row_names): - for i, row_name in enumerate(self.row_names): - args['row_name'] = shorten(row_name) - title = template.format(**args) - self.axes[i, 0].set_title(title, **kwargs) - elif self.col_names is not None and len(self.col_names): - for i, col_name in enumerate(self.col_names): - args['col_name'] = shorten(col_name) - title = template.format(**args) - # Index the flat array so col_wrap works - self.axes.flat[i].set_title(title, **kwargs) + if self._single_group: + for d, ax in zip(self.name_dicts.flat, self.axes.flat): + ax.set_title(template.format(*d.items()[0])) + else: + # The row titles on the right edge of the grid + for ax, row_name in zip(self.axes[:, -1], self.row_names): + title = template.format(self.row, row_name) + ax.annotate(title, xy=(1.02, .5), xycoords="axes fraction", + rotation=270, ha="left", va="center", **kwargs) + + # The column titles on the top row + for ax, col_name in zip(self.axes[0, :], self.col_names): + title = template.format(self.col, col_name) + ax.set_title(title) + return self @@ -367,6 +327,91 @@ def map(self, func, *args, **kwargs): + +def set_titles_backup(self, template=None, row_template="{row_var} = {row_name}", + col_template="{col_var} = {col_name}", maxchar=10, + **kwargs): + ''' + Draw titles either above each facet or on the grid margins. + + Parameters + ---------- + template : string + Template for all titles with the formatting keys {col_var} and + {col_name} (if using a `col` faceting variable) and/or {row_var} + and {row_name} (if using a `row` faceting variable). + row_template: + Template for the row variable when titles are drawn on the grid + margins. Must have {row_var} and {row_name} formatting keys. + col_template: + Template for the row variable when titles are drawn on the grid + margins. Must have {col_var} and {col_name} formatting keys. + maxchar : int + Truncate strings at maxchar + + Returns + ------- + self: object + Returns self. + + ''' + import matplotlib as mpl + + args = dict(row_var=self._row_var, col_var=self._col_var) + kwargs["size"] = kwargs.pop("size", mpl.rcParams["axes.labelsize"]) + + # Establish default templates + if template is None: + if self._row_var is None: + template = col_template + elif self._col_var is None: + template = row_template + else: + template = " | ".join([row_template, col_template]) + + def shorten(name, maxchar=maxchar): + return str(name)[:maxchar] + + if self._margin_titles: + if self.row_names: + # Draw the row titles on the right edge of the grid + for i, row_name in enumerate(self.row_names): + ax = self.axes[i, -1] + args['row_name'] = shorten(row_name) + title = row_template.format(**args) + ax.annotate(title, xy=(1.02, .5), xycoords="axes fraction", + rotation=270, ha="left", va="center", **kwargs) + if self.col_names: + # Draw the column titles as normal titles + for j, col_name in enumerate(self.col_names): + args['col_name'] = shorten(col_name) + title = col_template.format(**args) + self.axes[0, j].set_title(title, **kwargs) + + return self + + # Otherwise title each facet with all the necessary information + if (self._row_var is not None) and (self._col_var is not None): + for i, row_name in enumerate(self.row_names): + for j, col_name in enumerate(self.col_names): + args['row_name'] = shorten(row_name) + args['col_name'] = shorten(col_name) + title = template.format(**args) + self.axes[i, j].set_title(title, **kwargs) + elif self.row_names is not None and len(self.row_names): + for i, row_name in enumerate(self.row_names): + args['row_name'] = shorten(row_name) + title = template.format(**args) + self.axes[i, 0].set_title(title, **kwargs) + elif self.col_names is not None and len(self.col_names): + for i, col_name in enumerate(self.col_names): + args['col_name'] = shorten(col_name) + title = template.format(**args) + # Index the flat array so col_wrap works + self.axes.flat[i].set_title(title, **kwargs) + return self + + def map_dataarray2(self, func, *args, **kwargs): """Experimenting with row and col """ diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index 23a900971dd..98dc1bfa87d 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -30,6 +30,17 @@ def text_in_fig(): return set(alltxt) +def substring_in_axes(substring, ax): + ''' + Return True if a substring is found anywhere in an axes + ''' + alltxt = set([t.get_text() for t in ax.findobj(mpl.text.Text)]) + for txt in alltxt: + if substring in txt: + return True + return False + + @requires_matplotlib class PlotTestCase(TestCase): @@ -552,6 +563,14 @@ def test_names_appear_somewhere(self): for label in ['x', 'y']: self.assertIn(label, alltxt) + def test_text_not_super_long(self): + self.darray.coords['z'] = [100 * letter for letter in 'abc'] + g = xplt.FacetGrid(self.darray, col='z') + g.map_dataarray(xplt.contour, 'x', 'y') + alltxt = text_in_fig() + maxlen = max(len(txt) for txt in alltxt) + self.assertLess(maxlen, 50) + def test_colorbar(self): self.g.map_dataarray(xplt.imshow, 'x', 'y') images = plt.gcf().findobj(mpl.image.AxesImage) @@ -567,10 +586,25 @@ def test_colorbar(self): def test_row_and_col_shape(self): a = np.arange(10 * 15 * 3 * 2).reshape(10, 15, 3, 2) d = DataArray(a, dims=['y', 'x', 'col', 'row']) + + d.coords['col'] = np.array(['col' + str(x) for x in + d.coords['col'].values]) + d.coords['row'] = np.array(['row' + str(x) for x in + d.coords['row'].values]) + g = xplt.FacetGrid(d, col='col', row='row') self.assertEqual((2, 3), g.axes.shape) + g.map_dataarray(xplt.imshow, 'x', 'y') + # Rightmost column should be labeled + for label, ax in zip(d.coords['row'].values, g.axes[:, -1]): + self.assertTrue(substring_in_axes(label, ax)) + + # Top row should be labeled + for label, ax in zip(d.coords['col'].values, g.axes[0, :]): + self.assertTrue(substring_in_axes(label, ax)) + def test_norow_nocol_error(self): with self.assertRaisesRegexp(ValueError, r'[Rr]ow'): xplt.FacetGrid(self.darray) From 668698882ca7d5921207977760ae06a73c7a2eb3 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 25 Aug 2015 16:44:45 -0700 Subject: [PATCH 15/37] use core.formatting for titles --- xray/plot/facetgrid.py | 135 +++++------------------------------------ 1 file changed, 15 insertions(+), 120 deletions(-) diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index 47d590e2b10..fce91b8d53e 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -3,6 +3,8 @@ import numpy as np +from ..core.formatting import format_item + class FacetGrid_seaborn(object): ''' @@ -245,7 +247,8 @@ def map_dataarray(self, func, *args, **kwargs): return self - def set_titles(self, template="{0} = {1}", maxchar=10, **kwargs): + + def set_titles(self, template="{coord} = {value}", maxchar=30, **kwargs): ''' Draw titles either above each facet or on the grid margins. @@ -254,7 +257,8 @@ def set_titles(self, template="{0} = {1}", maxchar=10, **kwargs): template : string Template for plot titles maxchar : int - Truncate strings at maxchar + Truncate titles at maxchar + # TODO - may want to append '...' to indicate Returns ------- @@ -266,23 +270,25 @@ def set_titles(self, template="{0} = {1}", maxchar=10, **kwargs): kwargs["size"] = kwargs.pop("size", mpl.rcParams["axes.labelsize"]) - # TODO - use core string formatting - def shorten(name, maxchar=maxchar): - return str(name)[:maxchar] - if self._single_group: for d, ax in zip(self.name_dicts.flat, self.axes.flat): - ax.set_title(template.format(*d.items()[0])) + coord, value = d.items()[0] + prettyvalue = format_item(value) + title = template.format(coord=coord, value=prettyvalue) + title = title[:maxchar] + ax.set_title(title) else: # The row titles on the right edge of the grid for ax, row_name in zip(self.axes[:, -1], self.row_names): - title = template.format(self.row, row_name) + title = template.format(coord=self.row, value=format_item(row_name)) + title = title[:maxchar] ax.annotate(title, xy=(1.02, .5), xycoords="axes fraction", rotation=270, ha="left", va="center", **kwargs) # The column titles on the top row for ax, col_name in zip(self.axes[0, :], self.col_names): - title = template.format(self.col, col_name) + title = template.format(coord=self.col, value=format_item(col_name)) + title = title[:maxchar] ax.set_title(title) return self @@ -324,114 +330,3 @@ def map(self, func, *args, **kwargs): func(*innerargs, **kwargs) return self - - - - -def set_titles_backup(self, template=None, row_template="{row_var} = {row_name}", - col_template="{col_var} = {col_name}", maxchar=10, - **kwargs): - ''' - Draw titles either above each facet or on the grid margins. - - Parameters - ---------- - template : string - Template for all titles with the formatting keys {col_var} and - {col_name} (if using a `col` faceting variable) and/or {row_var} - and {row_name} (if using a `row` faceting variable). - row_template: - Template for the row variable when titles are drawn on the grid - margins. Must have {row_var} and {row_name} formatting keys. - col_template: - Template for the row variable when titles are drawn on the grid - margins. Must have {col_var} and {col_name} formatting keys. - maxchar : int - Truncate strings at maxchar - - Returns - ------- - self: object - Returns self. - - ''' - import matplotlib as mpl - - args = dict(row_var=self._row_var, col_var=self._col_var) - kwargs["size"] = kwargs.pop("size", mpl.rcParams["axes.labelsize"]) - - # Establish default templates - if template is None: - if self._row_var is None: - template = col_template - elif self._col_var is None: - template = row_template - else: - template = " | ".join([row_template, col_template]) - - def shorten(name, maxchar=maxchar): - return str(name)[:maxchar] - - if self._margin_titles: - if self.row_names: - # Draw the row titles on the right edge of the grid - for i, row_name in enumerate(self.row_names): - ax = self.axes[i, -1] - args['row_name'] = shorten(row_name) - title = row_template.format(**args) - ax.annotate(title, xy=(1.02, .5), xycoords="axes fraction", - rotation=270, ha="left", va="center", **kwargs) - if self.col_names: - # Draw the column titles as normal titles - for j, col_name in enumerate(self.col_names): - args['col_name'] = shorten(col_name) - title = col_template.format(**args) - self.axes[0, j].set_title(title, **kwargs) - - return self - - # Otherwise title each facet with all the necessary information - if (self._row_var is not None) and (self._col_var is not None): - for i, row_name in enumerate(self.row_names): - for j, col_name in enumerate(self.col_names): - args['row_name'] = shorten(row_name) - args['col_name'] = shorten(col_name) - title = template.format(**args) - self.axes[i, j].set_title(title, **kwargs) - elif self.row_names is not None and len(self.row_names): - for i, row_name in enumerate(self.row_names): - args['row_name'] = shorten(row_name) - title = template.format(**args) - self.axes[i, 0].set_title(title, **kwargs) - elif self.col_names is not None and len(self.col_names): - for i, col_name in enumerate(self.col_names): - args['col_name'] = shorten(col_name) - title = template.format(**args) - # Index the flat array so col_wrap works - self.axes.flat[i].set_title(title, **kwargs) - return self - - -def map_dataarray2(self, func, *args, **kwargs): - """Experimenting with row and col - """ - import matplotlib.pyplot as plt - - defaults = dict(add_colorbar=False, - add_labels=False, - vmin=float(self.darray.min()), - vmax=float(self.darray.max()), - ) - - defaults.update(kwargs) - - # Looping over the indices helps keep sanity - for col in range(ncol): - for row in range(nrow): - plt.sca(axes[row, col]) - # Similar to groupby - group = darray[{self.row: row, self.col: col}] - mappable = func(group, *args, **defaults) - - plt.colorbar(mappable, ax=self.axes.ravel().tolist()) - return self From 7d9cdbb89cbc423e1a661af21754e03fd7793cbd Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Wed, 26 Aug 2015 14:01:39 -0700 Subject: [PATCH 16/37] failing test for robust kwarg --- doc/plotting.rst | 29 +++++++++++++++++++---------- xray/plot/facetgrid.py | 1 + xray/plot/plot.py | 11 ----------- xray/test/test_plot.py | 22 ++++++++++++++++++++++ 4 files changed, 42 insertions(+), 21 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 92c2c4dd961..05417c8e6f7 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -143,7 +143,7 @@ Pick out individual axes using the ``.axes`` attribute. ax.set_title('Air Temperature %d' % i) bottomright = g.axes[-1, -1] - bottomright.annotate('America', (240, 40)) + bottomright.annotate('bottom right', (240, 40)) @savefig plot_facet_iterator.png height=12in plt.show() @@ -159,15 +159,6 @@ function with a Dataset: @savefig plot_facet_mapds.png height=12in g.map(plt.contourf, 'lon', 'lat', 'air') -In this interest of getting something useful that's internally consistent -and bounded in scope here's -a rough proposal- Get it all working nicely for DataArrays now, and then -revisit the question of whether and how to provide support for Datasets. - -Implement the -``map_dataarray`` method that requires the plotting function to accept a -DataArray as the first arg. - 4 dimensional ~~~~~~~~~~~~~~ @@ -190,6 +181,24 @@ one were 30 degrees hotter. @savefig plot_facet_4d.png height=12in plt.show() +Other features +~~~~~~~~~~~~~~ + +Faceted plotting supports other arguments common to xray 2d plots. + +.. ipython:: python + + hasoutliers = t.isel(time=slice(0, 2)).copy() + hasoutliers[0, 0, 0] = -100 + hasoutliers[-1, -1, -1] = 400 + + g = xray.plot.FacetGrid(hasoutliers, col='time') + + @savefig plot_facet_robust.png height=12in + g.map_dataarray(xray.plot.contourf, robust=True, cmap='viridis') + +TODO - Make sure robust arg is working. + More ~~~~~~~~~~~~~~ diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index fce91b8d53e..f2f1f028e24 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -4,6 +4,7 @@ import numpy as np from ..core.formatting import format_item +from .plot import _determine_cmap_params class FacetGrid_seaborn(object): diff --git a/xray/plot/plot.py b/xray/plot/plot.py index 706b5d58cce..07d603fbc05 100644 --- a/xray/plot/plot.py +++ b/xray/plot/plot.py @@ -54,17 +54,6 @@ def _load_default_cmap(fname='default_colormap.csv'): return LinearSegmentedColormap.from_list('viridis', cm_data) -def _title_for_slice(darray): - ''' - If the dataarray comes from a slice we can show that info in the title - ''' - title = [] - for dim, coord in darray.coords.items(): - if coord.size == 1: - title.append('{dim} = {v}'.format(dim=dim, v=coord.values)) - return ', '.join(title) - - def plot(darray, ax=None, rtol=0.01, **kwargs): """ Default plot of DataArray using matplotlib / pylab. diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index 98dc1bfa87d..ca1b9ce33b6 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -628,3 +628,25 @@ def test_nonunique_index_error(self): self.darray.coords['z'] = [0.1, 0.2, 0.2] with self.assertRaisesRegexp(ValueError, r'[Uu]nique'): g = xplt.FacetGrid(self.darray, col='z') + + def test_robust(self): + + z = np.zeros((20, 20, 2)) + darray = DataArray(z, dims=['y', 'x', 'z']) + darray[:, :, 1] = 1 + darray[2, 0, 0] = -1000 + darray[3, 0, 0] = 1000 + g = xplt.FacetGrid(darray, col='z') + g.map_dataarray(xplt.imshow, 'x', 'y', robust=True) + + # Color limits should be 0, 1 + # The largest number in the figure should be less than 21 + numbers = set() + alltxt = text_in_fig() + for txt in alltxt: + try: + numbers.add(float(txt)) + except ValueError: + pass + largest = max(abs(x) for x in numbers) + self.assertLess(largest, 21) From fbfbad1ef08f049e18ad860850f7d1b419d7607d Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 28 Aug 2015 10:56:17 -0700 Subject: [PATCH 17/37] refactoring _determine_cmap_parms --- xray/plot/facetgrid.py | 52 +++++++++++++++++++++++++++--------------- xray/plot/plot.py | 36 +++++++++++++++++++++-------- xray/test/test_plot.py | 11 ++++++--- 3 files changed, 69 insertions(+), 30 deletions(-) diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index f2f1f028e24..4f441ab1498 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -2,6 +2,7 @@ import itertools import numpy as np +import pandas as pd from ..core.formatting import format_item from .plot import _determine_cmap_params @@ -192,23 +193,23 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): def __iter__(self): return self.axes.flat - def map_dataarray(self, func, *args, **kwargs): + def map_dataarray(self, plotfunc, *args, **kwargs): """Apply a plotting function to each facet's subset of the data. - Differs from Seaborn style - requires the func to know how to plot a + Differs from Seaborn style - requires the plotfunc to know how to plot a dataarray. - For now I'm going to write this assuming func is an xray 2d + For now I'm going to write this assuming plotfunc is an xray 2d plotting function Parameters ---------- - func : callable + plotfunc : callable A plotting function with the first argument an xray dataarray args : - positional arguments to func + positional arguments to plotfunc kwargs : - keyword arguments to func + keyword arguments to plotfunc Returns ------- @@ -218,33 +219,48 @@ def map_dataarray(self, func, *args, **kwargs): """ import matplotlib.pyplot as plt - defaults = dict(add_colorbar=False, - add_labels=False, - vmin=float(self.darray.min()), - vmax=float(self.darray.max()), - ) + # defaults should be consistent with the 2d plot functions, except for + # add_colorbar and add_labels + defaults = { + 'plotfunc': plotfunc, + 'add_colorbar': False, + 'add_labels': False, + 'robust': False, + } + # Keyword args will override the defaults defaults.update(kwargs) + # Color limit calculations + robust = defaults['robust'] + calc_data = self.darray.values + calc_data = calc_data[~pd.isnull(calc_data)] + + # TODO - use percentile as global variable from other module + vmin = np.percentile(calc_data, 2) if robust else calc_data.min() + vmax = np.percentile(calc_data, 98) if robust else calc_data.max() + defaults.setdefault('vmin', vmin) + defaults.setdefault('vmax', vmax) + for d, ax in zip(self.name_dicts.flat, self.axes.flat): subset = self.darray.loc[d] - func(subset, ax=ax, *args, **defaults) + plotfunc(subset, ax=ax, *args, **defaults) # Add the labels to the bottom left plot # => plotting this one twice - # This would be easier to implement if there were separate args to - # add titles and to add axis labels defaults['add_labels'] = True bottomleft = self.axes[-1, 0] oldtitle = bottomleft.get_title() - mappable = func(self.darray.loc[self.name_dicts[-1, 0]], + mappable = plotfunc(self.darray.loc[self.name_dicts[-1, 0]], ax=bottomleft, *args, **defaults) bottomleft.set_title(oldtitle) # The colorbar - cbar = plt.colorbar(mappable, ax=self.axes.ravel().tolist()) - cbar.set_label(self.darray.name, rotation=270, - verticalalignment='bottom') + if defaults['add_colorbar']: + cbar = plt.colorbar(mappable, ax=self.axes.ravel().tolist(), + extend=cmap_params['extend']) + cbar.set_label(self.darray.name, rotation=270, + verticalalignment='bottom') return self diff --git a/xray/plot/plot.py b/xray/plot/plot.py index 07d603fbc05..a39acfeec73 100644 --- a/xray/plot/plot.py +++ b/xray/plot/plot.py @@ -216,9 +216,20 @@ def _determine_cmap_params(plot_data, vmin=None, vmax=None, cmap=None, Adapted from Seaborn: https://github.com/mwaskom/seaborn/blob/v0.6/seaborn/matrix.py#L158 + + Parameters + ========== + plot_data: Numpy array + Doesn't handle xray objects + + Returns + ======= + cmap_params : dict + Use depends on the type of the plotting function """ import matplotlib as mpl + #calc_data = np.ravel(plot_data[~pd.isnull(plot_data)]) calc_data = plot_data[~pd.isnull(plot_data)] if vmin is None: vmin = np.percentile(calc_data, 2) if robust else calc_data.min() @@ -416,8 +427,8 @@ def _plot2d(plotfunc): @functools.wraps(plotfunc) def newplotfunc(darray, x=None, y=None, ax=None, xincrease=None, yincrease=None, - add_colorbar=True, add_labels=True, vmin=None, vmax=None, cmap=None, - center=None, robust=False, extend=None, levels=None, + add_colorbar=True, add_labels=True, vmin=None, vmax=None, + cmap=None, center=None, robust=False, extend=None, levels=None, **kwargs): # All 2d plots in xray share this function signature. # Method signature below should be consistent. @@ -437,12 +448,12 @@ def newplotfunc(darray, x=None, y=None, ax=None, xincrease=None, yincrease=None, if x and x not in darray: raise KeyError('{0} is not a dimension of this DataArray. Use ' '{1} or {2} for x' - .format(x, *darray.dims)) + .format(x, *dims)) if y and y not in darray: raise KeyError('{0} is not a dimension of this DataArray. Use ' '{1} or {2} for y' - .format(y, *darray.dims)) + .format(y, *dims)) # Get label names if x and y: @@ -459,7 +470,7 @@ def newplotfunc(darray, x=None, y=None, ax=None, xincrease=None, yincrease=None, else: ylab, xlab = dims - # some plotting functions only know how to handle ndarrays + # better to pass the ndarrays directly to plotting functions xval = darray[xlab].values yval = darray[ylab].values zval = darray.to_masked_array(copy=False) @@ -473,10 +484,17 @@ def newplotfunc(darray, x=None, y=None, ax=None, xincrease=None, yincrease=None, if 'contour' in plotfunc.__name__ and levels is None: levels = 7 # this is the matplotlib default - filled = plotfunc.__name__ != 'contour' - - cmap_params = _determine_cmap_params(zval.data, vmin, vmax, cmap, center, - robust, extend, levels, filled) + cmap_kwargs = {'plot_data': zval.data, + 'vmin': vmin, + 'vmax': vmax, + 'cmap': cmap, + 'center': center, + 'robust': robust, + 'extend': extend, + 'levels': levels, + 'filled': plotfunc.__name__ != 'contour', + } + cmap_params = _determine_cmap_params(**cmap_kwargs) if 'contour' in plotfunc.__name__: # extend is a keyword argument only for contour and contourf, but diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index ca1b9ce33b6..640227d13cf 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -1,3 +1,5 @@ +import unittest + import numpy as np import pandas as pd @@ -540,6 +542,7 @@ def test_primitive_artist_returned(self): self.assertTrue(isinstance(artist, mpl.image.AxesImage)) +@unittest.skip class TestFacetGrid(PlotTestCase): def setUp(self): @@ -576,7 +579,10 @@ def test_colorbar(self): images = plt.gcf().findobj(mpl.image.AxesImage) # They should all have the same color limits - clims = set([ax.get_clim() for ax in images]) + clims = [ax.get_clim() for ax in images] + # Can't be Numpy arrays + clims = [(float(a), float(b)) for a, b in clims] + clims = set(clims) self.assertEqual(1, len(clims)) # One colorbar @@ -630,7 +636,6 @@ def test_nonunique_index_error(self): g = xplt.FacetGrid(self.darray, col='z') def test_robust(self): - z = np.zeros((20, 20, 2)) darray = DataArray(z, dims=['y', 'x', 'z']) darray[:, :, 1] = 1 @@ -640,7 +645,7 @@ def test_robust(self): g.map_dataarray(xplt.imshow, 'x', 'y', robust=True) # Color limits should be 0, 1 - # The largest number in the figure should be less than 21 + # The largest number displayed in the figure should be less than 21 numbers = set() alltxt = text_in_fig() for txt in alltxt: From 9cdfab7a99751220d97d3e7df890c37d0e9306ea Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 28 Aug 2015 13:00:45 -0700 Subject: [PATCH 18/37] predictable colormaps for facetgrids --- xray/plot/facetgrid.py | 83 +++++++++++++++++++++++++++++------------- xray/test/test_plot.py | 5 ++- 2 files changed, 61 insertions(+), 27 deletions(-) diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index 4f441ab1498..2c2e28ea75a 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -194,18 +194,15 @@ def __iter__(self): return self.axes.flat def map_dataarray(self, plotfunc, *args, **kwargs): - """Apply a plotting function to each facet's subset of the data. + """Apply a plotting function to a 2d facet's subset of the data. - Differs from Seaborn style - requires the plotfunc to know how to plot a - dataarray. - - For now I'm going to write this assuming plotfunc is an xray 2d - plotting function + This is more convenient and less general than the map method. Parameters ---------- plotfunc : callable - A plotting function with the first argument an xray dataarray + A plotting function with the same signature as a 2d xray + plotting method such as `xray.plot.imshow` args : positional arguments to plotfunc kwargs : @@ -213,34 +210,68 @@ def map_dataarray(self, plotfunc, *args, **kwargs): Returns ------- - self : object - Returns self. + self : FacetGrid object + the same FacetGrid on which the method was called """ import matplotlib.pyplot as plt - # defaults should be consistent with the 2d plot functions, except for - # add_colorbar and add_labels + # These should be consistent with xray.plot._plot2d + cmap_kwargs = {'plot_data': self.darray.values, + 'vmin': None, + 'vmax': None, + 'cmap': None, + 'center': None, + 'robust': False, + 'extend': None, + 'levels': 7 if 'contour' in plotfunc.__name__ else None, # MPL default + 'filled': plotfunc.__name__ != 'contour', + } + + # Allow kwargs to override these defaults + for param in kwargs: + if param in cmap_kwargs: + cmap_kwargs[param] = kwargs[param] + + # colormap inference has to happen here since all the data in self.darray + # is required to make the right choice + cmap_params = _determine_cmap_params(**cmap_kwargs) + + if 'contour' in plotfunc.__name__: + # extend is a keyword argument only for contour and contourf, but + # passing it to the colorbar is sufficient for imshow and + # pcolormesh + kwargs['extend'] = cmap_params['extend'] + kwargs['levels'] = cmap_params['levels'] + + ''' + return dict(vmin=vmin, vmax=vmax, cmap=cmap, extend=extend, + levels=levels, cnorm=cnorm) + def _determine_cmap_params(plot_data, vmin=None, vmax=None, cmap=None, + center=None, robust=False, extend=None, + levels=None, filled=True, cnorm=None): + ''' + defaults = { - 'plotfunc': plotfunc, 'add_colorbar': False, 'add_labels': False, - 'robust': False, + 'norm': cmap_params.pop('cnorm'), } - # Keyword args will override the defaults + # Order is important + defaults.update(cmap_params) defaults.update(kwargs) - # Color limit calculations - robust = defaults['robust'] - calc_data = self.darray.values - calc_data = calc_data[~pd.isnull(calc_data)] + ## Color limit calculations + #robust = defaults['robust'] + #calc_data = self.darray.values + #calc_data = calc_data[~pd.isnull(calc_data)] - # TODO - use percentile as global variable from other module - vmin = np.percentile(calc_data, 2) if robust else calc_data.min() - vmax = np.percentile(calc_data, 98) if robust else calc_data.max() - defaults.setdefault('vmin', vmin) - defaults.setdefault('vmax', vmax) + ## TODO - use percentile as global variable from other module + #vmin = np.percentile(calc_data, 2) if robust else calc_data.min() + #vmax = np.percentile(calc_data, 98) if robust else calc_data.max() + #defaults.setdefault('vmin', vmin) + #defaults.setdefault('vmax', vmax) for d, ax in zip(self.name_dicts.flat, self.axes.flat): subset = self.darray.loc[d] @@ -255,8 +286,8 @@ def map_dataarray(self, plotfunc, *args, **kwargs): ax=bottomleft, *args, **defaults) bottomleft.set_title(oldtitle) - # The colorbar - if defaults['add_colorbar']: + # colorbar + if kwargs.get('add_colorbar', True): cbar = plt.colorbar(mappable, ax=self.axes.ravel().tolist(), extend=cmap_params['extend']) cbar.set_label(self.darray.name, rotation=270, @@ -289,7 +320,7 @@ def set_titles(self, template="{coord} = {value}", maxchar=30, **kwargs): if self._single_group: for d, ax in zip(self.name_dicts.flat, self.axes.flat): - coord, value = d.items()[0] + coord, value = list(d.items()).pop() prettyvalue = format_item(value) title = template.format(coord=coord, value=prettyvalue) title = title[:maxchar] diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index 640227d13cf..918136940cb 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -542,7 +542,6 @@ def test_primitive_artist_returned(self): self.assertTrue(isinstance(artist, mpl.image.AxesImage)) -@unittest.skip class TestFacetGrid(PlotTestCase): def setUp(self): @@ -655,3 +654,7 @@ def test_robust(self): pass largest = max(abs(x) for x in numbers) self.assertLess(largest, 21) + + @unittest.skip + def test_can_set_vmin_vmax(self): + raise NotImplementedError From a0ce7cfa4c1bac44b8f4afc54bd9d2f8a3480cfc Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 28 Aug 2015 14:29:21 -0700 Subject: [PATCH 19/37] better tests for colorbars --- xray/test/test_plot.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index 918136940cb..cb75baf5480 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -574,19 +574,20 @@ def test_text_not_super_long(self): self.assertLess(maxlen, 50) def test_colorbar(self): + vmin = self.darray.values.min() + vmax = self.darray.values.max() + expected = np.array((vmin, vmax)) + self.g.map_dataarray(xplt.imshow, 'x', 'y') - images = plt.gcf().findobj(mpl.image.AxesImage) - # They should all have the same color limits - clims = [ax.get_clim() for ax in images] - # Can't be Numpy arrays - clims = [(float(a), float(b)) for a, b in clims] - clims = set(clims) - self.assertEqual(1, len(clims)) + for image in plt.gcf().findobj(mpl.image.AxesImage): + clim = np.array(image.get_clim()) + self.assertTrue(np.allclose(expected, clim)) - # One colorbar + # There's only one colorbar cbar = plt.gcf().findobj(mpl.collections.QuadMesh) self.assertEqual(1, len(cbar)) + def test_row_and_col_shape(self): a = np.arange(10 * 15 * 3 * 2).reshape(10, 15, 3, 2) @@ -655,6 +656,11 @@ def test_robust(self): largest = max(abs(x) for x in numbers) self.assertLess(largest, 21) - @unittest.skip def test_can_set_vmin_vmax(self): - raise NotImplementedError + vmin, vmax = 50.0, 1000.0 + expected = np.array((vmin, vmax)) + self.g.map_dataarray(xplt.imshow, 'x', 'y', vmin=vmin, vmax=vmax) + + for image in plt.gcf().findobj(mpl.image.AxesImage): + clim = np.array(image.get_clim()) + self.assertTrue(np.allclose(expected, clim)) From 870025aa9a5fe89be976e01c7655e031c05667d2 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 28 Aug 2015 16:21:21 -0700 Subject: [PATCH 20/37] col wrapping works now --- doc/plotting.rst | 12 +++++++++++- xray/plot/facetgrid.py | 42 ++++++++++++++++++++---------------------- xray/test/test_plot.py | 6 ++++++ 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 05417c8e6f7..e65ac8bb5be 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -197,7 +197,17 @@ Faceted plotting supports other arguments common to xray 2d plots. @savefig plot_facet_robust.png height=12in g.map_dataarray(xray.plot.contourf, robust=True, cmap='viridis') -TODO - Make sure robust arg is working. +Pass in some optional parameters. + +.. ipython:: python + + t5 = t.isel(time=slice(0, 5)) + g = xray.plot.FacetGrid(t5, col='time', col_wrap=2) + + @savefig plot_contour_color.png height=12in + g.map_dataarray(xray.plot.contour, color='k') + +TODO - reconcile this behavior with color kwargs - Github 537. More ~~~~~~~~~~~~~~ diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index 2c2e28ea75a..c857c5e7d29 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -1,3 +1,5 @@ +from __future__ import division + import warnings import itertools @@ -163,15 +165,16 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): else: col_names = list(darray[col].values) - # Build a 2d array of dictionaries mapping names to coordinates. + # TODO- + # Refactor to have two data structures + # name_dicts - a list of dicts corresponding to the flattened axes arrays + # allows iterating through without hitting the sentinel value + # name_array - an array of dicts corresponding to axes if self._single_group: full = [{self._single_group: x} for x in self.darray[self._single_group].values] - empty = [None for x in range(self.nfacet - len(full))] - # We could potentially used a masked array instead of None as - # the sentinel value here - #raise ValueError + empty = [None for x in range(self._nrow * self._ncol - len(full))] name_dicts = full + empty else: rowcols = itertools.product(row_names, col_names) @@ -179,7 +182,6 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): self.name_dicts = np.array(name_dicts).reshape(self._nrow, self._ncol) - self.row_names = row_names self.col_names = col_names @@ -244,14 +246,6 @@ def map_dataarray(self, plotfunc, *args, **kwargs): kwargs['extend'] = cmap_params['extend'] kwargs['levels'] = cmap_params['levels'] - ''' - return dict(vmin=vmin, vmax=vmax, cmap=cmap, extend=extend, - levels=levels, cnorm=cnorm) - def _determine_cmap_params(plot_data, vmin=None, vmax=None, cmap=None, - center=None, robust=False, extend=None, - levels=None, filled=True, cnorm=None): - ''' - defaults = { 'add_colorbar': False, 'add_labels': False, @@ -274,8 +268,10 @@ def _determine_cmap_params(plot_data, vmin=None, vmax=None, cmap=None, #defaults.setdefault('vmax', vmax) for d, ax in zip(self.name_dicts.flat, self.axes.flat): - subset = self.darray.loc[d] - plotfunc(subset, ax=ax, *args, **defaults) + # Handle the sentinel value + if d is not None: + subset = self.darray.loc[d] + plotfunc(subset, ax=ax, *args, **defaults) # Add the labels to the bottom left plot # => plotting this one twice @@ -320,13 +316,15 @@ def set_titles(self, template="{coord} = {value}", maxchar=30, **kwargs): if self._single_group: for d, ax in zip(self.name_dicts.flat, self.axes.flat): - coord, value = list(d.items()).pop() - prettyvalue = format_item(value) - title = template.format(coord=coord, value=prettyvalue) - title = title[:maxchar] - ax.set_title(title) + # TODO Remove check for sentinel value + if d is not None: + coord, value = list(d.items()).pop() + prettyvalue = format_item(value) + title = template.format(coord=coord, value=prettyvalue) + title = title[:maxchar] + ax.set_title(title) else: - # The row titles on the right edge of the grid + # The row titles on the left edge of the grid for ax, row_name in zip(self.axes[:, -1], self.row_names): title = template.format(coord=self.row, value=format_item(row_name)) title = title[:maxchar] diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index cb75baf5480..c27da5ae4d2 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -588,6 +588,12 @@ def test_colorbar(self): cbar = plt.gcf().findobj(mpl.collections.QuadMesh) self.assertEqual(1, len(cbar)) + def test_empty_cell(self): + g = xplt.FacetGrid(self.darray, col='z', col_wrap=2) + g.map_dataarray(xplt.imshow, 'x', 'y') + + bottomright = g.axes[-1, -1] + self.assertFalse(bottomright.has_data()) def test_row_and_col_shape(self): a = np.arange(10 * 15 * 3 * 2).reshape(10, 15, 3, 2) From 43f4b4af72ae98828e4d4aa5e41dd630f9cd037b Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Mon, 31 Aug 2015 11:13:45 -0700 Subject: [PATCH 21/37] failing test for font size --- xray/plot/facetgrid.py | 33 ++++++++++++++++++++++----------- xray/test/test_plot.py | 4 ++++ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index c857c5e7d29..904ccc1f182 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -2,6 +2,7 @@ import warnings import itertools +import functools import numpy as np import pandas as pd @@ -97,6 +98,16 @@ def __init__(self, data, row=None, col=None, col_wrap=None, self._y_var = None +def _nicetitle(coord, value, maxchar, template): + ''' + Put coord, value in template and truncate + ''' + prettyvalue = format_item(value) + title = template.format(coord=coord, value=prettyvalue) + # TODO - may want to append '...' to show this happened + return title[:maxchar] + + class FacetGrid(object): ''' Mostly copied from Seaborn @@ -302,7 +313,8 @@ def set_titles(self, template="{coord} = {value}", maxchar=30, **kwargs): Template for plot titles maxchar : int Truncate titles at maxchar - # TODO - may want to append '...' to indicate + kwargs : keyword args + additional arguments to matplotlib.text Returns ------- @@ -312,30 +324,29 @@ def set_titles(self, template="{coord} = {value}", maxchar=30, **kwargs): ''' import matplotlib as mpl - kwargs["size"] = kwargs.pop("size", mpl.rcParams["axes.labelsize"]) + kwargs.setdefault('size', 'small') + + nicetitle = functools.partial(_nicetitle, maxchar=maxchar, + template=template) if self._single_group: for d, ax in zip(self.name_dicts.flat, self.axes.flat): # TODO Remove check for sentinel value if d is not None: coord, value = list(d.items()).pop() - prettyvalue = format_item(value) - title = template.format(coord=coord, value=prettyvalue) - title = title[:maxchar] - ax.set_title(title) + title = nicetitle(coord, value, maxchar=maxchar) + ax.set_title(title, **kwargs) else: # The row titles on the left edge of the grid for ax, row_name in zip(self.axes[:, -1], self.row_names): - title = template.format(coord=self.row, value=format_item(row_name)) - title = title[:maxchar] + title = nicetitle(coord=self.row, value=row_name, maxchar=maxchar) ax.annotate(title, xy=(1.02, .5), xycoords="axes fraction", rotation=270, ha="left", va="center", **kwargs) # The column titles on the top row for ax, col_name in zip(self.axes[0, :], self.col_names): - title = template.format(coord=self.col, value=format_item(col_name)) - title = title[:maxchar] - ax.set_title(title) + title = nicetitle(coord=self.col, value=col_name, maxchar=maxchar) + ax.set_title(title, **kwargs) return self diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index c27da5ae4d2..4691b266d6f 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -554,6 +554,10 @@ def test_no_args(self): for ax in self.g: self.assertTrue(ax.has_data()) + # Font size should be small + fontsize = ax.title.get_size() + self.assertLessEqual(fontsize, 12) + def test_names_appear_somewhere(self): self.darray.name = 'testvar' self.g.map_dataarray(xplt.contourf, 'x', 'y') From afed851dd32eb0e0190973ee89c666118a6b891c Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Mon, 31 Aug 2015 14:56:29 -0700 Subject: [PATCH 22/37] refactoring init --- xray/plot/facetgrid.py | 179 +++++++++++------------------------------ xray/test/test_plot.py | 3 + 2 files changed, 51 insertions(+), 131 deletions(-) diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index 904ccc1f182..c9f82ae4114 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -11,91 +11,9 @@ from .plot import _determine_cmap_params -class FacetGrid_seaborn(object): - ''' - Copied from Seaborn - ''' - - def __init__(self, data, row=None, col=None, col_wrap=None, - margin_titles=False, xlim=None, ylim=None, subplot_kws=None, - gridspec_kws=None): - - import matplotlib as mpl - import matplotlib.pyplot as plt - - if row: - row_names = data[row].values - nrow = len(row_names) - else: - nrow = 1 - - if col: - col_names = data[col].values - ncol = len(col_names) - else: - ncol = 1 - - # Compute the grid shape - self._n_facets = ncol * nrow - - self._col_wrap = col_wrap - if col_wrap is not None: - if row is not None: - err = "Cannot use `row` and `col_wrap` together." - raise ValueError(err) - ncol = col_wrap - nrow = int(np.ceil(len(data[col].unique()) / col_wrap)) - self._ncol = ncol - self._nrow = nrow - - # Validate some inputs - if col_wrap is not None: - margin_titles = False - - # Initialize the subplot grid - if col_wrap is None: - kwargs = dict(figsize=figsize, squeeze=False, - sharex=True, sharey=True, - ) - - fig, axes = plt.subplots(nrow, ncol, **kwargs) - self.axes = axes - - else: - # If wrapping the col variable we need to make the grid ourselves - n_axes = len(col_names) - fig = plt.figure(figsize=figsize) - axes = np.empty(n_axes, object) - axes[0] = fig.add_subplot(nrow, ncol, 1, **subplot_kws) - - for i in range(1, n_axes): - axes[i] = fig.add_subplot(nrow, ncol, i + 1, **subplot_kws) - self.axes = axes - - # Set up the class attributes - # --------------------------- - - # First the public API - self.data = data - self.fig = fig - self.axes = axes - - #self.row_names = row_names - #self.col_names = col_names - - # Next the private variables - self._nrow = nrow - self._row_var = row - self._ncol = ncol - self._col_var = col - - self._margin_titles = margin_titles - self._col_wrap = col_wrap - self._legend_out = legend_out - self._legend = None - self._legend_data = {} - self._x_var = None - self._y_var = None +# Using this over mpl.rcParams["axes.labelsize"] since there are many of +# these strings, and they can get long +_TITLESIZE = 'small' def _nicetitle(coord, value, maxchar, template): @@ -104,8 +22,11 @@ def _nicetitle(coord, value, maxchar, template): ''' prettyvalue = format_item(value) title = template.format(coord=coord, value=prettyvalue) - # TODO - may want to append '...' to show this happened - return title[:maxchar] + + if len(title) > maxchar: + title = title[:(maxchar - 3)] + '...' + + return title class FacetGrid(object): @@ -114,43 +35,37 @@ class FacetGrid(object): ''' def __init__(self, darray, col=None, row=None, col_wrap=None): + import matplotlib.pyplot as plt - self.darray = darray - self.row = row - self.col = col - self.col_wrap = col_wrap - # _single_group is the grouping variable, if there is only one + # Handle corner case of nonunique coordinates + rep_col = col is not None and not darray[col].to_index().is_unique + rep_row = row is not None and not darray[row].to_index().is_unique + if rep_col or rep_row: + raise ValueError('Coordinates used for faceting cannot ' + 'contain repeated (nonunique) values.') + + # self._single_group is the grouping variable, if there is exactly one if col and row: self._single_group = False self._nrow = len(darray[row]) self._ncol = len(darray[col]) self.nfacet = self._nrow * self._ncol - self._margin_titles = True if col_wrap is not None: - warnings.warn("Can't use col_wrap when both col and row are passed") + warnings.warn('Ignoring col_wrap since both col and row ' + 'were passed') elif row and not col: self._single_group = row - self._margin_titles = False elif not row and col: self._single_group = col - self._margin_titles = False else: raise ValueError('Pass a coordinate name as an argument for row or col') - # Relying on short circuit behavior - # Not sure when nonunique coordinates are a problem - rep_col = col is not None and not self.darray[col].to_index().is_unique - rep_row = row is not None and not self.darray[row].to_index().is_unique - if rep_col or rep_row: - raise ValueError('Coordinates used for faceting cannot ' - 'contain repeated (nonunique) values.') - # Compute grid shape if self._single_group: self.nfacet = len(darray[self._single_group]) if col: - # TODO - could add heuristic for nice shapes like 3x4 + # idea - could add heuristic for nice shapes like 3x4 self._ncol = self.nfacet if row: self._ncol = 1 @@ -166,25 +81,12 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): self.axes.shape = self._nrow, self._ncol # Set up the lists of names for the row and column facet variables - if row is None: - row_names = [] - else: - row_names = list(darray[row].values) - - if col is None: - col_names = [] - else: - col_names = list(darray[col].values) - - # TODO- - # Refactor to have two data structures - # name_dicts - a list of dicts corresponding to the flattened axes arrays - # allows iterating through without hitting the sentinel value - # name_array - an array of dicts corresponding to axes + col_names = list(darray[col].values) if col else [] + row_names = list(darray[row].values) if row else [] if self._single_group: full = [{self._single_group: x} for x in - self.darray[self._single_group].values] + darray[self._single_group].values] empty = [None for x in range(self._nrow * self._ncol - len(full))] name_dicts = full + empty else: @@ -195,6 +97,11 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): self.row_names = row_names self.col_names = col_names + self.darray = darray + self.row = row + self.col = col + self.col_wrap = col_wrap + # Next the private variables self._row_var = row @@ -284,14 +191,25 @@ def map_dataarray(self, plotfunc, *args, **kwargs): subset = self.darray.loc[d] plotfunc(subset, ax=ax, *args, **defaults) - # Add the labels to the bottom left plot - # => plotting this one twice - defaults['add_labels'] = True - bottomleft = self.axes[-1, 0] - oldtitle = bottomleft.get_title() - mappable = plotfunc(self.darray.loc[self.name_dicts[-1, 0]], - ax=bottomleft, *args, **defaults) - bottomleft.set_title(oldtitle) + # Plot the last subset again to determine x and y values + try: + dummyfig = plt.figure() + dummyax = dummyfig.add_axes((0, 0, 0, 0)) + defaults['add_labels'] = True + + mappable = plotfunc(subset, + ax=dummyax, *args, **defaults) + + xlab, ylab = dummyax.get_xlabel(), dummyax.get_ylabel() + bottomleft = self.axes[-1, 0] + bottomleft.set_xlabel(xlab) + bottomleft.set_ylabel(ylab) + # Something to discuss- these labels could be centered on the + # whole figure instead of the bottom left axes + #self.fig.text(0.5, 0, xlab) + #self.fig.text(0, 0.5, ylab) + finally: + plt.close(dummyfig) # colorbar if kwargs.get('add_colorbar', True): @@ -324,14 +242,13 @@ def set_titles(self, template="{coord} = {value}", maxchar=30, **kwargs): ''' import matplotlib as mpl - kwargs.setdefault('size', 'small') + kwargs.setdefault('size', _TITLESIZE) nicetitle = functools.partial(_nicetitle, maxchar=maxchar, template=template) if self._single_group: for d, ax in zip(self.name_dicts.flat, self.axes.flat): - # TODO Remove check for sentinel value if d is not None: coord, value = list(d.items()).pop() title = nicetitle(coord, value, maxchar=maxchar) diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index 4691b266d6f..34ef1c66681 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -577,6 +577,9 @@ def test_text_not_super_long(self): maxlen = max(len(txt) for txt in alltxt) self.assertLess(maxlen, 50) + t0 = g.axes[0, 0].get_title() + self.assertTrue(t0.endswith('...')) + def test_colorbar(self): vmin = self.darray.values.min() vmax = self.darray.values.max() From 92d11892666d99eb4c64ba287f9ab01fb21ba546 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Mon, 31 Aug 2015 15:55:44 -0700 Subject: [PATCH 23/37] reorganizing docs --- doc/plotting.rst | 262 ++++++++++++++++++----------------------- xray/plot/facetgrid.py | 25 ++-- 2 files changed, 128 insertions(+), 159 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index e65ac8bb5be..41e6732d91b 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -69,153 +69,6 @@ For these examples we'll use the North American air temperature dataset. air = airtemps.air - 273.15 -Faceting --------- - -Xray's basic plotting is useful for plotting two dimensional arrays. What -about three or four dimensional arrays? -Once the data is stored in an appropriate form, the code to visualize -it should be clear, natural, and not too verbose. - -Consider the temperature data set. There are 4 observations per day for two -years which makes for 2920 values along the time dimension. -Note that the faceted dimension should not have too many values; -faceting on the time dimension will produce 2920 plots. That's -too much to be helpful. To handle this situation try performing -an operation that reduces the size of the data in some way. For example, we -could compute the average air temperature for each month and reduce the -size of this dimension from 2920 -> 12. A simpler way is -to just take a slice on that dimension. -So let's use a slice to pick 6 times throughout the first year. - -.. ipython:: python - - t = air.isel(time=slice(0, 365 * 4, 250)) - t - - -One way to visualize this data is to make a -seperate plot for each time period. This is what we call faceting; splitting an -array along one dimension into different facets and plotting each one. - -Simple Example -~~~~~~~~~~~~~~ - -Here's one way to do it in matplotlib: - -.. ipython:: python - - fig, axes = plt.subplots(nrows=2, ncols=3, sharex=True, sharey=True) - - kwargs = {'vmin': t.min(), 'vmax': t.max(), 'add_colorbar': False} - - for i in range(len(t.time)): - ax = axes.flat[i] - im = t[i].plot(ax=ax, **kwargs) - #** hack fix for Vim syntax highlight - ax.set_xlabel('') - ax.set_ylabel('') - ax.set_title(t.time.values[i]) - - plt.colorbar(im) - - @savefig plot_facet_simple.png height=12in - plt.show() - -We can use ``map_dataarray`` on a DataArray: - -.. ipython:: python - - g = xray.plot.FacetGrid(t, col='time', col_wrap=2) - - @savefig plot_facet_dataarray.png height=12in - g.map_dataarray(xray.plot.contourf, 'lon', 'lat') - -Iterating over the FacetGrid iterates over the individual axes. -Pick out individual axes using the ``.axes`` attribute. - -.. ipython:: python - - g = xray.plot.FacetGrid(t, col='time', col_wrap=2) - g.map_dataarray(xray.plot.contourf, 'lon', 'lat') - - for i, ax in enumerate(g): - ax.set_title('Air Temperature %d' % i) - - bottomright = g.axes[-1, -1] - bottomright.annotate('bottom right', (240, 40)) - - @savefig plot_facet_iterator.png height=12in - plt.show() - -This design picks out the data args and lets us use any matplotlib -function with a Dataset: - -.. ipython:: python - - tds = t.to_dataset() - g = xray.plot.FacetGrid(tds, 'time', col_wrap=2) - - @savefig plot_facet_mapds.png height=12in - g.map(plt.contourf, 'lon', 'lat', 'air') - -4 dimensional -~~~~~~~~~~~~~~ - -For 4 dimensional arrays we can use the rows and columns. -Here we create a 4 dimensional array by taking the original data and adding -a fixed amount. Now we can see how the temperature maps would compare if -one were 30 degrees hotter. - -.. ipython:: python - - t2 = t.isel(time=slice(0, 2)) - t4d = xray.concat([t2, t2 + 40], pd.Index(['normal', 'hot'], name='fourth_dim')) - # This is a 4d array - t4d.coords - - g = xray.plot.FacetGrid(t4d, col='time', row='fourth_dim') - - g.map_dataarray(xray.plot.imshow, 'lon', 'lat') - - @savefig plot_facet_4d.png height=12in - plt.show() - -Other features -~~~~~~~~~~~~~~ - -Faceted plotting supports other arguments common to xray 2d plots. - -.. ipython:: python - - hasoutliers = t.isel(time=slice(0, 2)).copy() - hasoutliers[0, 0, 0] = -100 - hasoutliers[-1, -1, -1] = 400 - - g = xray.plot.FacetGrid(hasoutliers, col='time') - - @savefig plot_facet_robust.png height=12in - g.map_dataarray(xray.plot.contourf, robust=True, cmap='viridis') - -Pass in some optional parameters. - -.. ipython:: python - - t5 = t.isel(time=slice(0, 5)) - g = xray.plot.FacetGrid(t5, col='time', col_wrap=2) - - @savefig plot_contour_color.png height=12in - g.map_dataarray(xray.plot.contour, color='k') - -TODO - reconcile this behavior with color kwargs - Github 537. - -More -~~~~~~~~~~~~~~ - -Xray faceted plotting uses an API and code from `Seaborn -`_ - - One Dimension ------------- @@ -414,6 +267,121 @@ Finally, if you have `Seaborn `_ @savefig plotting_custom_colors_levels.png width=4in air2d.plot(levels=[0, 12, 18, 30], cmap=flatui) +Faceting +-------- + +Faceting here refers to splitting an array along one or two dimensions and +plotting each group. +Xray's basic plotting is useful for plotting two dimensional arrays. What +about three or four dimensional arrays? That's where facets become helpful. + +Consider the temperature data set. There are 4 observations per day for two +years which makes for 2920 values along the time dimension. +One way to visualize this data is to make a +seperate plot for each time period. + +The faceted dimension should not have too many values; +faceting on the time dimension will produce 2920 plots. That's +too much to be helpful. To handle this situation try performing +an operation that reduces the size of the data in some way. For example, we +could compute the average air temperature for each month and reduce the +size of this dimension from 2920 -> 12. A simpler way is +to just take a slice on that dimension. +So let's use a slice to pick 6 times throughout the first year. + +.. ipython:: python + + t = air.isel(time=slice(0, 365 * 4, 250)) + t.coords + +Simple Example +~~~~~~~~~~~~~~ + +TODO - replace with the convenience method from plot + +We can use :py:meth:`xray.FacetGrid.map_dataarray` on a DataArray: + +.. ipython:: python + + g = xray.plot.FacetGrid(t, col='time', col_wrap=2) + + @savefig plot_facet_dataarray.png height=12in + g.map_dataarray(xray.plot.contourf, 'lon', 'lat') + +FacetGrid Objects +~~~~~~~~~~~~~~~~~ + +:py:class:`xray.plot.FacetGrid` is used to control the behavior of the +multiple plots. +It borrows an API and code from `Seaborn +`_ + +Iterating over the FacetGrid iterates over the individual axes. +Pick out individual axes using the ``.axes`` attribute. + +.. ipython:: python + + g = xray.plot.FacetGrid(t, col='time', col_wrap=2) + g.map_dataarray(xray.plot.contourf, 'lon', 'lat') + + for i, ax in enumerate(g): + ax.set_title('Air Temperature %d' % i) + + bottomright = g.axes[-1, -1] + bottomright.annotate('bottom right', (240, 40)) + + @savefig plot_facet_iterator.png height=12in + plt.show() + +`map` is more general, but probably less convenient. + +.. ipython:: python + + tds = t.to_dataset() + g = xray.plot.FacetGrid(tds, 'time', col_wrap=2) + + @savefig plot_facet_mapds.png height=12in + g.map(plt.contourf, 'lon', 'lat', 'air') + +4 dimensional +~~~~~~~~~~~~~~ + +For 4 dimensional arrays we can create use the rows and columns. +Here we create a 4 dimensional array by taking the original data and adding +a fixed amount. Now we can see how the temperature maps would compare if +one were 30 degrees hotter. + +.. ipython:: python + + t2 = t.isel(time=slice(0, 2)) + t4d = xray.concat([t2, t2 + 40], pd.Index(['normal', 'hot'], name='fourth_dim')) + # This is a 4d array + t4d.coords + + g = xray.plot.FacetGrid(t4d, col='time', row='fourth_dim') + + g.map_dataarray(xray.plot.imshow, 'lon', 'lat') + + @savefig plot_facet_4d.png height=12in + plt.show() + +Other features +~~~~~~~~~~~~~~ + +Faceted plotting supports other arguments common to xray 2d plots. + +.. ipython:: python + + hasoutliers = t.isel(time=slice(0, 5)).copy() + hasoutliers[0, 0, 0] = -100 + hasoutliers[-1, -1, -1] = 400 + + g = xray.plot.FacetGrid(hasoutliers, col='time', col_wrap=2) + + @savefig plot_facet_robust.png height=12in + g.map_dataarray(xray.plot.contourf, robust=True, cmap='viridis') + + Maps ---- diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index c9f82ae4114..ca3920a144f 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -32,6 +32,19 @@ def _nicetitle(coord, value, maxchar, template): class FacetGrid(object): ''' Mostly copied from Seaborn + + Attributes + ---------- + axes : numpy object array + Contains axes in corresponding position, as returned from + plt.subplots + fig : matplotlib.Figure + containing figure + name_dicts : numpy object array + Contains dictionaries mapping coordinate names to values. None is + used as a sentinel value for axes which should remain empty, ie. + sometimes the bottom right + ''' def __init__(self, darray, col=None, row=None, col_wrap=None): @@ -102,7 +115,6 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): self.col = col self.col_wrap = col_wrap - # Next the private variables self._row_var = row self._col_var = col @@ -174,17 +186,6 @@ def map_dataarray(self, plotfunc, *args, **kwargs): defaults.update(cmap_params) defaults.update(kwargs) - ## Color limit calculations - #robust = defaults['robust'] - #calc_data = self.darray.values - #calc_data = calc_data[~pd.isnull(calc_data)] - - ## TODO - use percentile as global variable from other module - #vmin = np.percentile(calc_data, 2) if robust else calc_data.min() - #vmax = np.percentile(calc_data, 98) if robust else calc_data.max() - #defaults.setdefault('vmin', vmin) - #defaults.setdefault('vmax', vmax) - for d, ax in zip(self.name_dicts.flat, self.axes.flat): # Handle the sentinel value if d is not None: From c4abf4199c08a952980e28b644a7c181a46d84b0 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 1 Sep 2015 10:31:59 -0700 Subject: [PATCH 24/37] remove all random tests from plotting --- xray/plot/facetgrid.py | 4 ++-- xray/test/test_plot.py | 50 +++++++++++++++++++++++++++--------------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index ca3920a144f..1b8beb71497 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -88,10 +88,10 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): self._nrow = int(np.ceil(self.nfacet / self._ncol)) self.fig, self.axes = plt.subplots(self._nrow, self._ncol, - sharex=True, sharey=True) + sharex=True, sharey=True, squeeze=False) # subplots flattens this array if one dimension - self.axes.shape = self._nrow, self._ncol + #self.axes.shape = self._nrow, self._ncol # Set up the lists of names for the row and column facet variables col_names = list(darray[col].values) if col else [] diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index 34ef1c66681..6ee29c4b02e 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -43,6 +43,16 @@ def substring_in_axes(substring, ax): return False +def easy_array(shape, start=0, stop=1): + ''' + Make an array with desired shape using np.linspace + + shape is a tuple like (2, 3) + ''' + a = np.linspace(start, stop, num=np.prod(shape)) + return a.reshape(shape) + + @requires_matplotlib class PlotTestCase(TestCase): @@ -69,7 +79,7 @@ def contourf_called(self, plotmethod): class TestPlot(PlotTestCase): def setUp(self): - self.darray = DataArray(np.random.randn(2, 3, 4)) + self.darray = DataArray(np.arange(2*3*4).reshape(2, 3, 4)) def test1d(self): self.darray[:, 0, 0].plot() @@ -160,7 +170,7 @@ def test_slice_in_title(self): class TestPlotHistogram(PlotTestCase): def setUp(self): - self.darray = DataArray(np.random.randn(2, 3, 4)) + self.darray = DataArray(easy_array((2, 3, 4))) def test_3d_array(self): self.darray.plot.hist() @@ -170,7 +180,7 @@ def test_title_no_name(self): self.assertEqual('', plt.gca().get_title()) def test_title_uses_name(self): - self.darray.name = 'randompoints' + self.darray.name = 'testpoints' self.darray.plot.hist() self.assertIn(self.darray.name, plt.gca().get_title()) @@ -197,19 +207,20 @@ def test_plot_nans(self): @requires_matplotlib class TestDetermineCmapParams(TestCase): + def setUp(self): + self.data = np.linspace(0, 1, num=100) + def test_robust(self): - data = np.random.RandomState(1).rand(100) - cmap_params = _determine_cmap_params(data, robust=True) - self.assertEqual(cmap_params['vmin'], np.percentile(data, 2)) - self.assertEqual(cmap_params['vmax'], np.percentile(data, 98)) + cmap_params = _determine_cmap_params(self.data, robust=True) + self.assertEqual(cmap_params['vmin'], np.percentile(self.data, 2)) + self.assertEqual(cmap_params['vmax'], np.percentile(self.data, 98)) self.assertEqual(cmap_params['cmap'].name, 'viridis') self.assertEqual(cmap_params['extend'], 'both') self.assertIsNone(cmap_params['levels']) self.assertIsNone(cmap_params['cnorm']) def test_center(self): - data = np.random.RandomState(2).rand(100) - cmap_params = _determine_cmap_params(data, center=0.5) + cmap_params = _determine_cmap_params(self.data, center=0.5) self.assertEqual(cmap_params['vmax'] - 0.5, 0.5 - cmap_params['vmin']) self.assertEqual(cmap_params['cmap'], 'RdBu_r') self.assertEqual(cmap_params['extend'], 'neither') @@ -217,7 +228,7 @@ def test_center(self): self.assertIsNone(cmap_params['cnorm']) def test_integer_levels(self): - data = 1 + np.random.RandomState(3).rand(100) + data = self.data + 1 cmap_params = _determine_cmap_params(data, levels=5, vmin=0, vmax=5, cmap='Blues') self.assertEqual(cmap_params['vmin'], cmap_params['levels'][0]) @@ -233,7 +244,7 @@ def test_integer_levels(self): self.assertEqual(cmap_params['extend'], 'max') def test_list_levels(self): - data = 1 + np.random.RandomState(3).rand(100) + data = self.data + 1 orig_levels = [0, 1, 2, 3, 4, 5] # vmin and vmax should be ignored if levels are explicitly provided @@ -337,8 +348,7 @@ class Common2dMixin: Should have the same name as the method. """ def setUp(self): - rs = np.random.RandomState(123) - self.darray = DataArray(rs.randn(10, 15), dims=['y', 'x']) + self.darray = DataArray(easy_array((10, 15), start=-1), dims=['y', 'x']) self.plotmethod = getattr(self.darray.plot, self.plotfunc.__name__) def test_label_names(self): @@ -351,12 +361,12 @@ def test_1d_raises_valueerror(self): self.plotfunc(self.darray[0, :]) def test_3d_raises_valueerror(self): - a = DataArray(np.random.randn(2, 3, 4)) + a = DataArray(easy_array((2, 3, 4))) with self.assertRaisesRegexp(ValueError, r'[Dd]im'): self.plotfunc(a) def test_nonnumeric_index_raises_typeerror(self): - a = DataArray(np.random.randn(3, 2), + a = DataArray(easy_array((3, 2)), coords=[['a', 'b', 'c'], ['d', 'e']]) with self.assertRaisesRegexp(TypeError, r'[Pp]lot'): self.plotfunc(a) @@ -430,13 +440,13 @@ def test_bad_x_string_exception(self): self.plotmethod('not_a_real_dim') def test_default_title(self): - a = DataArray(np.random.randn(4, 3, 2, 1), dims=['a', 'b', 'c', 'd']) + a = DataArray(easy_array((4, 3, 2, 1)), dims=['a', 'b', 'c', 'd']) self.plotfunc(a.isel(c=1)) title = plt.gca().get_title() self.assertEqual('c = 1, d = 0', title) def test_default_title(self): - a = DataArray(np.random.randn(4, 3, 2), dims=['a', 'b', 'c']) + a = DataArray(easy_array((4, 3, 2)), dims=['a', 'b', 'c']) a.coords['d'] = 10 self.plotfunc(a.isel(c=1)) title = plt.gca().get_title() @@ -455,7 +465,7 @@ def test_no_labels(self): self.assertNotIn(string, alltxt) def test_facetgrid(self): - a = np.arange(10 * 15 * 3).reshape(10, 15, 3) + a = easy_array((10, 15, 3)) d = DataArray(a, dims=['y', 'x', 'z']) g = xplt.FacetGrid(d, col='z') g.map_dataarray(self.plotfunc, 'x', 'y') @@ -480,9 +490,13 @@ def test_extend(self): artist = self.plotmethod() self.assertEqual(artist.extend, 'neither') + self.darray[0, 0] = -100 + self.darray[-1, -1] = 100 artist = self.plotmethod(robust=True) self.assertEqual(artist.extend, 'both') + self.darray[0, 0] = 0 + self.darray[-1, -1] = 0 artist = self.plotmethod(vmin=-0, vmax=10) self.assertEqual(artist.extend, 'min') From 1546dc280cde5c4288f263359ba86d5e0b1812c8 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 1 Sep 2015 11:40:41 -0700 Subject: [PATCH 25/37] helper function for x, y label inference --- xray/plot/facetgrid.py | 38 ++++++++-------------- xray/plot/plot.py | 73 ++++++++++++++++++++++++------------------ xray/test/test_plot.py | 6 +++- 3 files changed, 60 insertions(+), 57 deletions(-) diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index 1b8beb71497..6ca90c46d6f 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -90,9 +90,6 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): self.fig, self.axes = plt.subplots(self._nrow, self._ncol, sharex=True, sharey=True, squeeze=False) - # subplots flattens this array if one dimension - #self.axes.shape = self._nrow, self._ncol - # Set up the lists of names for the row and column facet variables col_names = list(darray[col].values) if col else [] row_names = list(darray[row].values) if row else [] @@ -125,7 +122,7 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): def __iter__(self): return self.axes.flat - def map_dataarray(self, plotfunc, *args, **kwargs): + def map_dataarray(self, plotfunc, x, y, *args, **kwargs): """Apply a plotting function to a 2d facet's subset of the data. This is more convenient and less general than the map method. @@ -135,6 +132,8 @@ def map_dataarray(self, plotfunc, *args, **kwargs): plotfunc : callable A plotting function with the same signature as a 2d xray plotting method such as `xray.plot.imshow` + x, y : string + Names of the coordinates to plot on x, y axes args : positional arguments to plotfunc kwargs : @@ -190,27 +189,16 @@ def map_dataarray(self, plotfunc, *args, **kwargs): # Handle the sentinel value if d is not None: subset = self.darray.loc[d] - plotfunc(subset, ax=ax, *args, **defaults) - - # Plot the last subset again to determine x and y values - try: - dummyfig = plt.figure() - dummyax = dummyfig.add_axes((0, 0, 0, 0)) - defaults['add_labels'] = True - - mappable = plotfunc(subset, - ax=dummyax, *args, **defaults) - - xlab, ylab = dummyax.get_xlabel(), dummyax.get_ylabel() - bottomleft = self.axes[-1, 0] - bottomleft.set_xlabel(xlab) - bottomleft.set_ylabel(ylab) - # Something to discuss- these labels could be centered on the - # whole figure instead of the bottom left axes - #self.fig.text(0.5, 0, xlab) - #self.fig.text(0, 0.5, ylab) - finally: - plt.close(dummyfig) + mappable = plotfunc(subset, x, y, ax=ax, *args, **defaults) + + bottomleft = self.axes[-1, 0] + bottomleft.set_xlabel(x) + bottomleft.set_ylabel(y) + + # Something to discuss- these labels could be centered on the + # whole figure instead of the bottom left axes + #self.fig.text(0.5, 0, x) + #self.fig.text(0, 0.5, y) # colorbar if kwargs.get('add_colorbar', True): diff --git a/xray/plot/plot.py b/xray/plot/plot.py index 13e52d0af83..3874f08e7c5 100644 --- a/xray/plot/plot.py +++ b/xray/plot/plot.py @@ -55,6 +55,47 @@ def _load_default_cmap(fname='default_colormap.csv'): return LinearSegmentedColormap.from_list('viridis', cm_data) +def _infer_xy_labels(plotfunc, darray, x, y): + ''' + Determine x and y labels when some are missing. For use in _plot2d + + darray is a 2 dimensional data array. + ''' + dims = list(darray.dims) + + if len(dims) != 2: + raise ValueError('{type} plots are for 2 dimensional DataArrays. ' + 'Passed DataArray has {ndim} dimensions' + .format(type=plotfunc.__name__, ndim=len(dims))) + + if x and x not in dims: + raise KeyError('{0} is not a dimension of this DataArray. Use ' + '{1} or {2} for x' + .format(x, *dims)) + + if y and y not in dims: + raise KeyError('{0} is not a dimension of this DataArray. Use ' + '{1} or {2} for y' + .format(y, *dims)) + + # Get label names + if x and y: + xlab = x + ylab = y + elif x and not y: + xlab = x + del dims[dims.index(x)] + ylab = dims.pop() + elif y and not x: + ylab = y + del dims[dims.index(y)] + xlab = dims.pop() + else: + ylab, xlab = dims + + return xlab, ylab + + def plot(darray, ax=None, rtol=0.01, **kwargs): """ Default plot of DataArray using matplotlib / pylab. @@ -477,37 +518,7 @@ def newplotfunc(darray, x=None, y=None, ax=None, xincrease=None, yincrease=None, if ax is None: ax = plt.gca() - dims = list(darray.dims) - - if len(dims) != 2: - raise ValueError('{type} plots are for 2 dimensional DataArrays. ' - 'Passed DataArray has {ndim} dimensions' - .format(type=plotfunc.__name__, ndim=len(dims))) - - if x and x not in darray: - raise KeyError('{0} is not a dimension of this DataArray. Use ' - '{1} or {2} for x' - .format(x, *dims)) - - if y and y not in darray: - raise KeyError('{0} is not a dimension of this DataArray. Use ' - '{1} or {2} for y' - .format(y, *dims)) - - # Get label names - if x and y: - xlab = x - ylab = y - elif x and not y: - xlab = x - del dims[dims.index(x)] - ylab = dims.pop() - elif y and not x: - ylab = y - del dims[dims.index(y)] - xlab = dims.pop() - else: - ylab, xlab = dims + xlab, ylab = _infer_xy_labels(plotfunc=plotfunc, darray=darray, x=x, y=y) # better to pass the ndarrays directly to plotting functions xval = darray[xlab].values diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index b582c9ba989..34579a067d4 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -448,6 +448,10 @@ def test_bad_x_string_exception(self): with self.assertRaisesRegexp(KeyError, r'y'): self.plotmethod('not_a_real_dim') + self.darray.coords['z'] = 100 + with self.assertRaisesRegexp(KeyError, r'y'): + self.plotmethod('z') + def test_default_title(self): a = DataArray(easy_array((4, 3, 2, 1)), dims=['a', 'b', 'c', 'd']) self.plotfunc(a.isel(c=1)) @@ -603,7 +607,7 @@ def setUp(self): self.g = xplt.FacetGrid(self.darray, col='z') def test_no_args(self): - self.g.map_dataarray(xplt.contourf) + self.g.map_dataarray(xplt.contourf, 'x', 'y') for ax in self.g: self.assertTrue(ax.has_data()) From 07e02bbcfbe9872ecf856d7c6c4237dec44bb6b8 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 1 Sep 2015 13:16:46 -0700 Subject: [PATCH 26/37] make axes invisible for ragged case --- doc/plotting.rst | 2 +- xray/plot/facetgrid.py | 5 ++++- xray/test/test_plot.py | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 268b982b28f..bcbcafa15c3 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -422,7 +422,7 @@ Faceted plotting supports other arguments common to xray 2d plots. g = xray.plot.FacetGrid(hasoutliers, col='time', col_wrap=2) @savefig plot_facet_robust.png height=12in - g.map_dataarray(xray.plot.contourf, robust=True, cmap='viridis') + g.map_dataarray(xray.plot.contourf, 'lon', 'lat', robust=True, cmap='viridis') Maps diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index 6ca90c46d6f..af9032542ac 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -186,7 +186,7 @@ def map_dataarray(self, plotfunc, x, y, *args, **kwargs): defaults.update(kwargs) for d, ax in zip(self.name_dicts.flat, self.axes.flat): - # Handle the sentinel value + # None is the sentinel value if d is not None: subset = self.darray.loc[d] mappable = plotfunc(subset, x, y, ax=ax, *args, **defaults) @@ -238,10 +238,13 @@ def set_titles(self, template="{coord} = {value}", maxchar=30, **kwargs): if self._single_group: for d, ax in zip(self.name_dicts.flat, self.axes.flat): + # Only plot the ones with data if d is not None: coord, value = list(d.items()).pop() title = nicetitle(coord, value, maxchar=maxchar) ax.set_title(title, **kwargs) + else: + ax.set_visible(False) else: # The row titles on the left edge of the grid for ax, row_name in zip(self.axes[:, -1], self.row_names): diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index 34579a067d4..138cf858a2b 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -658,6 +658,7 @@ def test_empty_cell(self): bottomright = g.axes[-1, -1] self.assertFalse(bottomright.has_data()) + self.assertFalse(bottomright.get_visible()) def test_row_and_col_shape(self): a = np.arange(10 * 15 * 3 * 2).reshape(10, 15, 3, 2) From ab02f416fca65191641d76b14b38112151d12677 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Wed, 2 Sep 2015 16:52:41 -0700 Subject: [PATCH 27/37] use ticker.maxNLocator to control ticks --- doc/plotting.rst | 21 ++++++++--- xray/plot/facetgrid.py | 83 ++++++++++++++++++++++++++++++++++-------- xray/test/test_plot.py | 75 +++++++++++++++++++++++++------------- 3 files changed, 132 insertions(+), 47 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index bcbcafa15c3..3737d1917f3 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -48,6 +48,7 @@ Imports # Use defaults so we don't get gridlines in generated docs import matplotlib as mpl mpl.rcdefaults() + mpl.rcParams.update({'figure.autolayout': True}) The following imports are necessary for all of the examples. @@ -346,7 +347,7 @@ We can use :py:meth:`xray.FacetGrid.map_dataarray` on a DataArray: .. ipython:: python - g = xray.plot.FacetGrid(t, col='time', col_wrap=2) + g = xray.plot.FacetGrid(t, col='time', col_wrap=3) @savefig plot_facet_dataarray.png height=12in g.map_dataarray(xray.plot.contourf, 'lon', 'lat') @@ -364,7 +365,7 @@ Pick out individual axes using the ``.axes`` attribute. .. ipython:: python - g = xray.plot.FacetGrid(t, col='time', col_wrap=2) + g = xray.plot.FacetGrid(t, col='time', col_wrap=3) g.map_dataarray(xray.plot.contourf, 'lon', 'lat') for i, ax in enumerate(g): @@ -381,7 +382,7 @@ Pick out individual axes using the ``.axes`` attribute. .. ipython:: python tds = t.to_dataset() - g = xray.plot.FacetGrid(tds, 'time', col_wrap=2) + g = xray.plot.FacetGrid(tds, 'time', col_wrap=3) @savefig plot_facet_mapds.png height=12in g.map(plt.contourf, 'lon', 'lat', 'air') @@ -403,10 +404,18 @@ one were 30 degrees hotter. g = xray.plot.FacetGrid(t4d, col='time', row='fourth_dim') + @savefig plot_facet_4d.png height=12in g.map_dataarray(xray.plot.imshow, 'lon', 'lat') - @savefig plot_facet_4d.png height=12in - plt.show() +Just for comparison, should remove this. + +.. ipython:: python + + g2 = xray.plot.FacetGrid(t4d, col='time', row='fourth_dim', + margin_titles=False) + + @savefig plot_facet_4d2.png height=12in + g2.map_dataarray(xray.plot.imshow, 'lon', 'lat') Other features ~~~~~~~~~~~~~~ @@ -419,7 +428,7 @@ Faceted plotting supports other arguments common to xray 2d plots. hasoutliers[0, 0, 0] = -100 hasoutliers[-1, -1, -1] = 400 - g = xray.plot.FacetGrid(hasoutliers, col='time', col_wrap=2) + g = xray.plot.FacetGrid(hasoutliers, col='time', col_wrap=3) @savefig plot_facet_robust.png height=12in g.map_dataarray(xray.plot.contourf, 'lon', 'lat', robust=True, cmap='viridis') diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index af9032542ac..9cb1c626af8 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -10,10 +10,18 @@ from ..core.formatting import format_item from .plot import _determine_cmap_params +import matplotlib as mpl # Using this over mpl.rcParams["axes.labelsize"] since there are many of # these strings, and they can get long -_TITLESIZE = 'small' +# Overrides axes.labelsize, xtick.major.size, ytick.major.size +# from mpl.rcParams +_FONTSIZE = 'small' + +# experimenting - lines below don't seem to have any effect on poorly +# formatted FacetGrid's +#import matplotlib +#matplotlib.rcParams.update({'figure.autolayout': True}) def _nicetitle(coord, value, maxchar, template): @@ -47,7 +55,11 @@ class FacetGrid(object): ''' - def __init__(self, darray, col=None, row=None, col_wrap=None): + def __init__(self, darray, col=None, row=None, col_wrap=None, + aspect=1, size=3, max_xticks=4, max_yticks=4, margin_titles=True): + ''' + TODO- fill these in + ''' import matplotlib.pyplot as plt @@ -80,6 +92,7 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): if col: # idea - could add heuristic for nice shapes like 3x4 self._ncol = self.nfacet + margin_titles = False if row: self._ncol = 1 if col_wrap is not None: @@ -87,8 +100,12 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): self._ncol = col_wrap self._nrow = int(np.ceil(self.nfacet / self._ncol)) + # Calculate the base figure size with extra horizontal space for a colorbar + self._cbar_space = 1 + figsize = (self._ncol * size * aspect + self._cbar_space, self._nrow * size) + self.fig, self.axes = plt.subplots(self._nrow, self._ncol, - sharex=True, sharey=True, squeeze=False) + sharex=True, sharey=True, squeeze=False, figsize=figsize) # Set up the lists of names for the row and column facet variables col_names = list(darray[col].values) if col else [] @@ -113,10 +130,12 @@ def __init__(self, darray, col=None, row=None, col_wrap=None): self.col_wrap = col_wrap # Next the private variables + self._margin_titles = margin_titles self._row_var = row self._col_var = col self._col_wrap = col_wrap + self.set_font() self.set_titles() def __iter__(self): @@ -191,22 +210,40 @@ def map_dataarray(self, plotfunc, x, y, *args, **kwargs): subset = self.darray.loc[d] mappable = plotfunc(subset, x, y, ax=ax, *args, **defaults) - bottomleft = self.axes[-1, 0] - bottomleft.set_xlabel(x) - bottomleft.set_ylabel(y) + #bottomleft = self.axes[-1, 0] + #bottomleft.set_xlabel(x) + #bottomleft.set_ylabel(y) + self.x = x + self.y = y + + # Left side labels + for ax in self.axes[:, 0]: + ax.set_ylabel(self.y) + + # Bottom labels + for ax in self.axes[-1, :]: + ax.set_xlabel(self.x) # Something to discuss- these labels could be centered on the # whole figure instead of the bottom left axes - #self.fig.text(0.5, 0, x) - #self.fig.text(0, 0.5, y) + #self.fig.text(0.3, 0, x, ha='center', va='top') + #self.fig.text(0.5, 0, x, va='bottom') + #self.fig.text(0, 0.3, y, rotation='vertical') # colorbar + # Must create new space for the colorbar, since resizing axes will + # make for bad formatting. if kwargs.get('add_colorbar', True): - cbar = plt.colorbar(mappable, ax=self.axes.ravel().tolist(), + + self.fig.subplots_adjust(right=0.8) + #cbar = plt.colorbar(mappable, ax=self.axes.ravel().tolist(), + cbar_ax = self.fig.add_axes([0.85, 0.15, 0.05, 0.7]) + cbar = self.fig.colorbar(mappable, cax=cbar_ax, extend=cmap_params['extend']) cbar.set_label(self.darray.name, rotation=270, verticalalignment='bottom') + return self @@ -231,7 +268,7 @@ def set_titles(self, template="{coord} = {value}", maxchar=30, **kwargs): ''' import matplotlib as mpl - kwargs.setdefault('size', _TITLESIZE) + kwargs.setdefault('size', _FONTSIZE) nicetitle = functools.partial(_nicetitle, maxchar=maxchar, template=template) @@ -246,11 +283,12 @@ def set_titles(self, template="{coord} = {value}", maxchar=30, **kwargs): else: ax.set_visible(False) else: - # The row titles on the left edge of the grid - for ax, row_name in zip(self.axes[:, -1], self.row_names): - title = nicetitle(coord=self.row, value=row_name, maxchar=maxchar) - ax.annotate(title, xy=(1.02, .5), xycoords="axes fraction", - rotation=270, ha="left", va="center", **kwargs) + # The row titles on the right edge of the grid + if self._margin_titles: + for ax, row_name in zip(self.axes[:, -1], self.row_names): + title = nicetitle(coord=self.row, value=row_name, maxchar=maxchar) + ax.annotate(title, xy=(1.02, .5), xycoords="axes fraction", + rotation=270, ha="left", va="center", **kwargs) # The column titles on the top row for ax, col_name in zip(self.axes[0, :], self.col_names): @@ -259,6 +297,21 @@ def set_titles(self, template="{coord} = {value}", maxchar=30, **kwargs): return self + def set_font(self, max_xticks=4, max_yticks=4, fontsize=_FONTSIZE): + ''' + + ''' + # Both are necessary + x_major_locator = mpl.ticker.MaxNLocator(nbins=max_xticks) + y_major_locator = mpl.ticker.MaxNLocator(nbins=max_yticks) + + for ax in self.axes.flat: + ax.xaxis.set_major_locator(x_major_locator) + ax.yaxis.set_major_locator(y_major_locator) + for tick in itertools.chain(ax.xaxis.get_major_ticks(), + ax.yaxis.get_major_ticks()): + tick.label.set_fontsize(fontsize) + def map(self, func, *args, **kwargs): """Apply a plotting function to each facet's subset of the data. diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index 138cf858a2b..0c4d03b8671 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -79,13 +79,13 @@ def contourf_called(self, plotmethod): class TestPlot(PlotTestCase): def setUp(self): - self.darray = DataArray(np.arange(2*3*4).reshape(2, 3, 4)) + self.darray = DataArray(easy_array((2, 3, 4))) def test1d(self): self.darray[:, 0, 0].plot() def test_2d_before_squeeze(self): - a = DataArray(np.arange(5).reshape(1, 5)) + a = DataArray(easy_array((1, 5))) a.plot() def test2d_uniform_calls_imshow(self): @@ -130,7 +130,7 @@ def test_ylabel_is_data_name(self): self.assertEqual(self.darray.name, plt.gca().get_ylabel()) def test_wrong_dims_raises_valueerror(self): - twodims = DataArray(np.arange(10).reshape(2, 5)) + twodims = DataArray(easy_array((2, 5))) with self.assertRaises(ValueError): twodims.plot.line() @@ -602,7 +602,7 @@ def test_seaborn_palette_needs_levels(self): class TestFacetGrid(PlotTestCase): def setUp(self): - d = np.arange(10 * 15 * 3).reshape(10, 15, 3) + d = easy_array((10, 15, 3)) self.darray = DataArray(d, dims=['y', 'x', 'z']) self.g = xplt.FacetGrid(self.darray, col='z') @@ -660,28 +660,6 @@ def test_empty_cell(self): self.assertFalse(bottomright.has_data()) self.assertFalse(bottomright.get_visible()) - def test_row_and_col_shape(self): - a = np.arange(10 * 15 * 3 * 2).reshape(10, 15, 3, 2) - d = DataArray(a, dims=['y', 'x', 'col', 'row']) - - d.coords['col'] = np.array(['col' + str(x) for x in - d.coords['col'].values]) - d.coords['row'] = np.array(['row' + str(x) for x in - d.coords['row'].values]) - - g = xplt.FacetGrid(d, col='col', row='row') - self.assertEqual((2, 3), g.axes.shape) - - g.map_dataarray(xplt.imshow, 'x', 'y') - - # Rightmost column should be labeled - for label, ax in zip(d.coords['row'].values, g.axes[:, -1]): - self.assertTrue(substring_in_axes(label, ax)) - - # Top row should be labeled - for label, ax in zip(d.coords['col'].values, g.axes[0, :]): - self.assertTrue(substring_in_axes(label, ax)) - def test_norow_nocol_error(self): with self.assertRaisesRegexp(ValueError, r'[Rr]ow'): xplt.FacetGrid(self.darray) @@ -735,3 +713,48 @@ def test_can_set_vmin_vmax(self): for image in plt.gcf().findobj(mpl.image.AxesImage): clim = np.array(image.get_clim()) self.assertTrue(np.allclose(expected, clim)) + + def test_figure_size(self): + + self.assertArrayEqual(self.g.fig.get_size_inches(), (10, 3)) + + g = xplt.FacetGrid(self.darray, col='z', size=6) + self.assertArrayEqual(g.fig.get_size_inches(), (19, 6)) + + g = xplt.FacetGrid(self.darray, col='z', size=4, aspect=0.5) + self.assertArrayEqual(g.fig.get_size_inches(), (7, 4)) + + +class TestFacetGrid4d(PlotTestCase): + + def setUp(self): + a = easy_array((10, 15, 3, 2)) + darray = DataArray(a, dims=['y', 'x', 'col', 'row']) + darray.coords['col'] = np.array(['col' + str(x) for x in + darray.coords['col'].values]) + darray.coords['row'] = np.array(['row' + str(x) for x in + darray.coords['row'].values]) + + self.darray = darray + + def test_default_labels(self): + g = xplt.FacetGrid(self.darray, col='col', row='row') + self.assertEqual((2, 3), g.axes.shape) + + g.map_dataarray(xplt.imshow, 'x', 'y') + + # Rightmost column should be labeled + for label, ax in zip(self.darray.coords['row'].values, g.axes[:, -1]): + self.assertTrue(substring_in_axes(label, ax)) + + # Top row should be labeled + for label, ax in zip(self.darray.coords['col'].values, g.axes[0, :]): + self.assertTrue(substring_in_axes(label, ax)) + + def test_margin_titles_false(self): + g = xplt.FacetGrid(self.darray, col='col', row='row', + margin_titles=False) + + # Rightmost column should not be labeled + for label, ax in zip(self.darray.coords['row'].values, g.axes[:, -1]): + self.assertFalse(substring_in_axes(label, ax)) From 5dccfb07b5bca3974644adb063bf4a84f3e58655 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Wed, 2 Sep 2015 17:05:20 -0700 Subject: [PATCH 28/37] take out global imports of maptplotlib --- xray/plot/facetgrid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index 9cb1c626af8..423bf4310e2 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -10,7 +10,6 @@ from ..core.formatting import format_item from .plot import _determine_cmap_params -import matplotlib as mpl # Using this over mpl.rcParams["axes.labelsize"] since there are many of # these strings, and they can get long @@ -301,6 +300,7 @@ def set_font(self, max_xticks=4, max_yticks=4, fontsize=_FONTSIZE): ''' ''' + import matplotlib as mpl # Both are necessary x_major_locator = mpl.ticker.MaxNLocator(nbins=max_xticks) y_major_locator = mpl.ticker.MaxNLocator(nbins=max_yticks) From 78c584d4800b2d63b360589d965df4e0462350d6 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 3 Sep 2015 14:59:00 -0700 Subject: [PATCH 29/37] add docs from seaborn, generate docs, set_font --- doc/api.rst | 12 ++++ doc/plotting.rst | 32 ++------- xray/plot/facetgrid.py | 155 ++++++++++++++++++++++++----------------- xray/test/test_plot.py | 23 +++++- 4 files changed, 134 insertions(+), 88 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index f141ee43378..0262eeada77 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -411,3 +411,15 @@ Plotting plot.imshow plot.line plot.pcolormesh + plot.FacetGrid + +FacetGrid methods +----------------- + +.. autosummary:: + :toctree: generated/ + + plot.FacetGrid.map_dataarray + plot.FacetGrid.set_titles + plot.FacetGrid.set_ticks + plot.FacetGrid.map diff --git a/doc/plotting.rst b/doc/plotting.rst index 3737d1917f3..0d8d5ccbd83 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -358,15 +358,17 @@ FacetGrid Objects :py:class:`xray.plot.FacetGrid` is used to control the behavior of the multiple plots. It borrows an API and code from `Seaborn -`_ +`_. Iterating over the FacetGrid iterates over the individual axes. Pick out individual axes using the ``.axes`` attribute. .. ipython:: python - g = xray.plot.FacetGrid(t, col='time', col_wrap=3) - g.map_dataarray(xray.plot.contourf, 'lon', 'lat') + g = (xray.plot + .FacetGrid(t, col='time', col_wrap=3) + .map_dataarray(xray.plot.contourf, 'lon', 'lat') + ) for i, ax in enumerate(g): ax.set_title('Air Temperature %d' % i) @@ -377,23 +379,13 @@ Pick out individual axes using the ``.axes`` attribute. @savefig plot_facet_iterator.png height=12in plt.show() -`map` is more general, but probably less convenient. - -.. ipython:: python - - tds = t.to_dataset() - g = xray.plot.FacetGrid(tds, 'time', col_wrap=3) - - @savefig plot_facet_mapds.png height=12in - g.map(plt.contourf, 'lon', 'lat', 'air') - 4 dimensional ~~~~~~~~~~~~~~ -For 4 dimensional arrays we can create use the rows and columns. +For 4 dimensional arrays we can use the rows and columns of the grids. Here we create a 4 dimensional array by taking the original data and adding a fixed amount. Now we can see how the temperature maps would compare if -one were 30 degrees hotter. +one were much hotter. .. ipython:: python @@ -407,16 +399,6 @@ one were 30 degrees hotter. @savefig plot_facet_4d.png height=12in g.map_dataarray(xray.plot.imshow, 'lon', 'lat') -Just for comparison, should remove this. - -.. ipython:: python - - g2 = xray.plot.FacetGrid(t4d, col='time', row='fourth_dim', - margin_titles=False) - - @savefig plot_facet_4d2.png height=12in - g2.map_dataarray(xray.plot.imshow, 'lon', 'lat') - Other features ~~~~~~~~~~~~~~ diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index 423bf4310e2..ed761a7feb1 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -11,21 +11,16 @@ from .plot import _determine_cmap_params -# Using this over mpl.rcParams["axes.labelsize"] since there are many of -# these strings, and they can get long # Overrides axes.labelsize, xtick.major.size, ytick.major.size # from mpl.rcParams _FONTSIZE = 'small' - -# experimenting - lines below don't seem to have any effect on poorly -# formatted FacetGrid's -#import matplotlib -#matplotlib.rcParams.update({'figure.autolayout': True}) +# For major ticks on x, y axes +_NTICKS = 5 def _nicetitle(coord, value, maxchar, template): ''' - Put coord, value in template and truncate + Put coord, value in template and truncate at maxchar ''' prettyvalue = format_item(value) title = template.format(coord=coord, value=prettyvalue) @@ -38,7 +33,27 @@ def _nicetitle(coord, value, maxchar, template): class FacetGrid(object): ''' - Mostly copied from Seaborn + Initialize the matplotlib figure and FacetGrid object. + + The :class:`FacetGrid` is an object that links a xray DataArray to + a matplotlib figure with a particular structure. + + In particular, :class:`FacetGrid` is used to draw plots with multiple + Axes where each Axes shows the same relationship conditioned on + different levels of some dimension. It's possible to condition on up to + two variables by assigning variables to the rows and columns of the + grid. + + The general approach to plotting here is called "small multiples", + where the same kind of plot is repeated multiple times, and the + specific use of small multiples to display the same relationship + conditioned on one ore more other variables is often called a "trellis + plot". + + The basic workflow is to initialize the :class:`FacetGrid` object with + the DataArray and the variable names that are used to structure the grid. Then + one or more plotting functions can be applied to each subset by calling + :meth:`FacetGrid.map_dataarray` or :meth:`FacetGrid.map`. Attributes ---------- @@ -46,18 +61,36 @@ class FacetGrid(object): Contains axes in corresponding position, as returned from plt.subplots fig : matplotlib.Figure - containing figure + The figure containing all the axes name_dicts : numpy object array Contains dictionaries mapping coordinate names to values. None is used as a sentinel value for axes which should remain empty, ie. - sometimes the bottom right + sometimes the bottom right grid ''' def __init__(self, darray, col=None, row=None, col_wrap=None, - aspect=1, size=3, max_xticks=4, max_yticks=4, margin_titles=True): + aspect=1, size=3, margin_titles=True): ''' - TODO- fill these in + Parameters + ---------- + darray : DataArray + xray DataArray to be plotted + row, col : strings + Dimesion names that define subsets of the data, which will be drawn on + separate facets in the grid. + col_wrap : int, optional + "Wrap" the column variable at this width, so that the column facets + aspect : scalar, optional + Aspect ratio of each facet, so that ``aspect * size`` gives the width + of each facet in inches + size : scalar, optional + Height (in inches) of each facet. See also: ``aspect`` + margin_titles : bool, optional + If ``True``, the titles for the row variable are drawn to the right of + the last column. This option is experimental and may not work in all + cases. + ''' import matplotlib.pyplot as plt @@ -134,16 +167,17 @@ def __init__(self, darray, col=None, row=None, col_wrap=None, self._col_var = col self._col_wrap = col_wrap - self.set_font() self.set_titles() def __iter__(self): return self.axes.flat - def map_dataarray(self, plotfunc, x, y, *args, **kwargs): - """Apply a plotting function to a 2d facet's subset of the data. + def map_dataarray(self, plotfunc, x, y, max_xticks=4, max_yticks=4, + fontsize=_FONTSIZE, **kwargs): + ''' + Apply a plotting function to a 2d facet's subset of the data. - This is more convenient and less general than the map method. + This is more convenient and less general than ``FacetGrid.map`` Parameters ---------- @@ -152,17 +186,20 @@ def map_dataarray(self, plotfunc, x, y, *args, **kwargs): plotting method such as `xray.plot.imshow` x, y : string Names of the coordinates to plot on x, y axes - args : - positional arguments to plotfunc + max_xticks, max_yticks : int, optional + Maximum number of labeled ticks to plot on x, y axes + max_yticks : int + Maximum number of tick marks to place on y axis + fontsize : string or int + Font size as used by matplotlib text kwargs : - keyword arguments to plotfunc + additional keyword arguments to plotfunc Returns ------- self : FacetGrid object - the same FacetGrid on which the method was called - """ + ''' import matplotlib.pyplot as plt # These should be consistent with xray.plot._plot2d @@ -207,11 +244,8 @@ def map_dataarray(self, plotfunc, x, y, *args, **kwargs): # None is the sentinel value if d is not None: subset = self.darray.loc[d] - mappable = plotfunc(subset, x, y, ax=ax, *args, **defaults) + mappable = plotfunc(subset, x, y, ax=ax, **defaults) - #bottomleft = self.axes[-1, 0] - #bottomleft.set_xlabel(x) - #bottomleft.set_ylabel(y) self.x = x self.y = y @@ -223,51 +257,50 @@ def map_dataarray(self, plotfunc, x, y, *args, **kwargs): for ax in self.axes[-1, :]: ax.set_xlabel(self.x) - # Something to discuss- these labels could be centered on the - # whole figure instead of the bottom left axes - #self.fig.text(0.3, 0, x, ha='center', va='top') - #self.fig.text(0.5, 0, x, va='bottom') - #self.fig.text(0, 0.3, y, rotation='vertical') - # colorbar - # Must create new space for the colorbar, since resizing axes will - # make for bad formatting. if kwargs.get('add_colorbar', True): self.fig.subplots_adjust(right=0.8) - #cbar = plt.colorbar(mappable, ax=self.axes.ravel().tolist(), + cbar_ax = self.fig.add_axes([0.85, 0.15, 0.05, 0.7]) cbar = self.fig.colorbar(mappable, cax=cbar_ax, extend=cmap_params['extend']) - cbar.set_label(self.darray.name, rotation=270, - verticalalignment='bottom') + if self.darray.name: + cbar.set_label(self.darray.name, rotation=270, + verticalalignment='bottom') + + # This happens here rather than __init__ since FacetGrid.map should + # use default ticks + self.set_ticks(max_xticks, max_yticks, fontsize) return self - - def set_titles(self, template="{coord} = {value}", maxchar=30, **kwargs): + def set_titles(self, template="{coord} = {value}", maxchar=30, + fontsize=_FONTSIZE, **kwargs): ''' Draw titles either above each facet or on the grid margins. Parameters ---------- template : string - Template for plot titles + Template for plot titles containing {coord} and {value} maxchar : int Truncate titles at maxchar + fontsize : string or int + Passed to matplotlib.text kwargs : keyword args additional arguments to matplotlib.text Returns ------- - self: object - Returns self. + self: FacetGrid object ''' + import matplotlib as mpl - kwargs.setdefault('size', _FONTSIZE) + kwargs['fontsize'] = fontsize nicetitle = functools.partial(_nicetitle, maxchar=maxchar, template=template) @@ -296,14 +329,17 @@ def set_titles(self, template="{coord} = {value}", maxchar=30, **kwargs): return self - def set_font(self, max_xticks=4, max_yticks=4, fontsize=_FONTSIZE): + def set_ticks(self, max_xticks=_NTICKS, max_yticks=_NTICKS, fontsize=_FONTSIZE): ''' - + Sets tick behavior. + + Refer to documentation in :meth:`FacetGrid.map_dataarray` ''' - import matplotlib as mpl + from matplotlib.ticker import MaxNLocator + # Both are necessary - x_major_locator = mpl.ticker.MaxNLocator(nbins=max_xticks) - y_major_locator = mpl.ticker.MaxNLocator(nbins=max_yticks) + x_major_locator = MaxNLocator(nbins=max_xticks) + y_major_locator = MaxNLocator(nbins=max_yticks) for ax in self.axes.flat: ax.xaxis.set_major_locator(x_major_locator) @@ -312,11 +348,8 @@ def set_font(self, max_xticks=4, max_yticks=4, fontsize=_FONTSIZE): ax.yaxis.get_major_ticks()): tick.label.set_fontsize(fontsize) - def map(self, func, *args, **kwargs): - """Apply a plotting function to each facet's subset of the data. - - True to Seaborn style + '''Apply a plotting function to each facet's subset of the data. Parameters ---------- @@ -334,18 +367,16 @@ def map(self, func, *args, **kwargs): Returns ------- - self : object - Returns self. + self : FacetGrid object - """ + ''' import matplotlib.pyplot as plt - for ax, (name, data) in zip(self, self.darray.groupby(self.col)): - - kwargs['add_colorbar'] = False - plt.sca(ax) - - innerargs = [data[a] for a in args] - func(*innerargs, **kwargs) + for ax, namedict in zip(self, self.name_dicts.flat): + if namedict is not None: + data = self.darray[namedict] + plt.sca(ax) + innerargs = [data[a].values for a in args] + func(*innerargs, **kwargs) return self diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index 0c4d03b8671..ed537f91cb5 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -608,10 +608,15 @@ def setUp(self): def test_no_args(self): self.g.map_dataarray(xplt.contourf, 'x', 'y') + + # Don't want colorbar labeled with 'None' + alltxt = text_in_fig() + self.assertNotIn('None', alltxt) + for ax in self.g: self.assertTrue(ax.has_data()) - # Font size should be small + # default font size should be small fontsize = ax.title.get_size() self.assertLessEqual(fontsize, 12) @@ -724,6 +729,22 @@ def test_figure_size(self): g = xplt.FacetGrid(self.darray, col='z', size=4, aspect=0.5) self.assertArrayEqual(g.fig.get_size_inches(), (7, 4)) + def test_num_ticks(self): + nticks = 100 + maxticks = nticks + 1 + self.g.map_dataarray(xplt.imshow, 'x', 'y', max_xticks=nticks, + max_yticks=nticks) + for ax in self.g: + xticks = len(ax.get_xticks()) + yticks = len(ax.get_yticks()) + self.assertLessEqual(xticks, maxticks) + self.assertLessEqual(yticks, maxticks) + self.assertGreaterEqual(xticks, nticks / 2.0) + self.assertGreaterEqual(yticks, nticks / 2.0) + + def test_map(self): + self.g.map(plt.contourf, 'x', 'y', Ellipsis) + class TestFacetGrid4d(PlotTestCase): From eab83a5f826be7a0c2dd4532dd5b70191cffe209 Mon Sep 17 00:00:00 2001 From: Stephan Hoyer Date: Thu, 3 Sep 2015 16:11:55 -0700 Subject: [PATCH 30/37] Colorbar positioning tweak and use tight_layout() --- xray/plot/facetgrid.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index ed761a7feb1..94cb10c2f77 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -53,7 +53,7 @@ class FacetGrid(object): The basic workflow is to initialize the :class:`FacetGrid` object with the DataArray and the variable names that are used to structure the grid. Then one or more plotting functions can be applied to each subset by calling - :meth:`FacetGrid.map_dataarray` or :meth:`FacetGrid.map`. + :meth:`FacetGrid.map_dataarray` or :meth:`FacetGrid.map`. Attributes ---------- @@ -78,7 +78,7 @@ def __init__(self, darray, col=None, row=None, col_wrap=None, xray DataArray to be plotted row, col : strings Dimesion names that define subsets of the data, which will be drawn on - separate facets in the grid. + separate facets in the grid. col_wrap : int, optional "Wrap" the column variable at this width, so that the column facets aspect : scalar, optional @@ -257,19 +257,23 @@ def map_dataarray(self, plotfunc, x, y, max_xticks=4, max_yticks=4, for ax in self.axes[-1, :]: ax.set_xlabel(self.x) - # colorbar - if kwargs.get('add_colorbar', True): + self.fig.tight_layout() - self.fig.subplots_adjust(right=0.8) + if self._single_group: + for d, ax in zip(self.name_dicts.flat, self.axes.flat): + if d is None: + ax.set_visible(False) - cbar_ax = self.fig.add_axes([0.85, 0.15, 0.05, 0.7]) - cbar = self.fig.colorbar(mappable, cax=cbar_ax, - extend=cmap_params['extend']) + # colorbar + if kwargs.get('add_colorbar', True): + cbar = self.fig.colorbar(mappable, + ax=list(self.axes.flat), + extend=cmap_params['extend']) if self.darray.name: cbar.set_label(self.darray.name, rotation=270, verticalalignment='bottom') - + # This happens here rather than __init__ since FacetGrid.map should # use default ticks self.set_ticks(max_xticks, max_yticks, fontsize) @@ -307,13 +311,11 @@ def set_titles(self, template="{coord} = {value}", maxchar=30, if self._single_group: for d, ax in zip(self.name_dicts.flat, self.axes.flat): - # Only plot the ones with data + # Only label the ones with data if d is not None: coord, value = list(d.items()).pop() title = nicetitle(coord, value, maxchar=maxchar) ax.set_title(title, **kwargs) - else: - ax.set_visible(False) else: # The row titles on the right edge of the grid if self._margin_titles: @@ -333,7 +335,7 @@ def set_ticks(self, max_xticks=_NTICKS, max_yticks=_NTICKS, fontsize=_FONTSIZE): ''' Sets tick behavior. - Refer to documentation in :meth:`FacetGrid.map_dataarray` + Refer to documentation in :meth:`FacetGrid.map_dataarray` ''' from matplotlib.ticker import MaxNLocator From 266b0bfa1809e3b770e0ac32977046719a085011 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 4 Sep 2015 10:25:56 -0700 Subject: [PATCH 31/37] address Stephans comments and pep8 cleanup --- xray/plot/facetgrid.py | 126 ++++++++++++++++++++--------------------- xray/plot/plot.py | 20 +++---- xray/test/test_plot.py | 10 ---- 3 files changed, 73 insertions(+), 83 deletions(-) diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index 94cb10c2f77..2a56aa621e0 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -5,7 +5,6 @@ import functools import numpy as np -import pandas as pd from ..core.formatting import format_item from .plot import _determine_cmap_params @@ -19,9 +18,9 @@ def _nicetitle(coord, value, maxchar, template): - ''' + """ Put coord, value in template and truncate at maxchar - ''' + """ prettyvalue = format_item(value) title = template.format(coord=coord, value=prettyvalue) @@ -32,7 +31,7 @@ def _nicetitle(coord, value, maxchar, template): class FacetGrid(object): - ''' + """ Initialize the matplotlib figure and FacetGrid object. The :class:`FacetGrid` is an object that links a xray DataArray to @@ -51,8 +50,8 @@ class FacetGrid(object): plot". The basic workflow is to initialize the :class:`FacetGrid` object with - the DataArray and the variable names that are used to structure the grid. Then - one or more plotting functions can be applied to each subset by calling + the DataArray and the variable names that are used to structure the grid. + Then plotting functions can be applied to each subset by calling :meth:`FacetGrid.map_dataarray` or :meth:`FacetGrid.map`. Attributes @@ -67,31 +66,27 @@ class FacetGrid(object): used as a sentinel value for axes which should remain empty, ie. sometimes the bottom right grid - ''' + """ def __init__(self, darray, col=None, row=None, col_wrap=None, - aspect=1, size=3, margin_titles=True): - ''' + aspect=1, size=3): + """ Parameters ---------- darray : DataArray xray DataArray to be plotted row, col : strings - Dimesion names that define subsets of the data, which will be drawn on - separate facets in the grid. + Dimesion names that define subsets of the data, which will be drawn + on separate facets in the grid. col_wrap : int, optional "Wrap" the column variable at this width, so that the column facets aspect : scalar, optional - Aspect ratio of each facet, so that ``aspect * size`` gives the width - of each facet in inches + Aspect ratio of each facet, so that ``aspect * size`` gives the + width of each facet in inches size : scalar, optional Height (in inches) of each facet. See also: ``aspect`` - margin_titles : bool, optional - If ``True``, the titles for the row variable are drawn to the right of - the last column. This option is experimental and may not work in all - cases. - ''' + """ import matplotlib.pyplot as plt @@ -116,7 +111,8 @@ def __init__(self, darray, col=None, row=None, col_wrap=None, elif not row and col: self._single_group = col else: - raise ValueError('Pass a coordinate name as an argument for row or col') + raise ValueError( + 'Pass a coordinate name as an argument for row or col') # Compute grid shape if self._single_group: @@ -124,7 +120,6 @@ def __init__(self, darray, col=None, row=None, col_wrap=None, if col: # idea - could add heuristic for nice shapes like 3x4 self._ncol = self.nfacet - margin_titles = False if row: self._ncol = 1 if col_wrap is not None: @@ -132,12 +127,15 @@ def __init__(self, darray, col=None, row=None, col_wrap=None, self._ncol = col_wrap self._nrow = int(np.ceil(self.nfacet / self._ncol)) - # Calculate the base figure size with extra horizontal space for a colorbar + # Calculate the base figure size with extra horizontal space for a + # colorbar self._cbar_space = 1 - figsize = (self._ncol * size * aspect + self._cbar_space, self._nrow * size) + figsize = (self._ncol * size * aspect + + self._cbar_space, self._nrow * size) self.fig, self.axes = plt.subplots(self._nrow, self._ncol, - sharex=True, sharey=True, squeeze=False, figsize=figsize) + sharex=True, sharey=True, + squeeze=False, figsize=figsize) # Set up the lists of names for the row and column facet variables col_names = list(darray[col].values) if col else [] @@ -145,7 +143,7 @@ def __init__(self, darray, col=None, row=None, col_wrap=None, if self._single_group: full = [{self._single_group: x} for x in - darray[self._single_group].values] + darray[self._single_group].values] empty = [None for x in range(self._nrow * self._ncol - len(full))] name_dicts = full + empty else: @@ -162,7 +160,6 @@ def __init__(self, darray, col=None, row=None, col_wrap=None, self.col_wrap = col_wrap # Next the private variables - self._margin_titles = margin_titles self._row_var = row self._col_var = col self._col_wrap = col_wrap @@ -173,8 +170,8 @@ def __iter__(self): return self.axes.flat def map_dataarray(self, plotfunc, x, y, max_xticks=4, max_yticks=4, - fontsize=_FONTSIZE, **kwargs): - ''' + fontsize=_FONTSIZE, **kwargs): + """ Apply a plotting function to a 2d facet's subset of the data. This is more convenient and less general than ``FacetGrid.map`` @@ -199,28 +196,28 @@ def map_dataarray(self, plotfunc, x, y, max_xticks=4, max_yticks=4, ------- self : FacetGrid object - ''' - import matplotlib.pyplot as plt + """ # These should be consistent with xray.plot._plot2d cmap_kwargs = {'plot_data': self.darray.values, - 'vmin': None, - 'vmax': None, - 'cmap': None, - 'center': None, - 'robust': False, - 'extend': None, - 'levels': 7 if 'contour' in plotfunc.__name__ else None, # MPL default - 'filled': plotfunc.__name__ != 'contour', - } + 'vmin': None, + 'vmax': None, + 'cmap': None, + 'center': None, + 'robust': False, + 'extend': None, + # MPL default + 'levels': 7 if 'contour' in plotfunc.__name__ else None, + 'filled': plotfunc.__name__ != 'contour', + } # Allow kwargs to override these defaults for param in kwargs: if param in cmap_kwargs: cmap_kwargs[param] = kwargs[param] - # colormap inference has to happen here since all the data in self.darray - # is required to make the right choice + # colormap inference has to happen here since all the data in + # self.darray is required to make the right choice cmap_params = _determine_cmap_params(**cmap_kwargs) if 'contour' in plotfunc.__name__: @@ -231,10 +228,10 @@ def map_dataarray(self, plotfunc, x, y, max_xticks=4, max_yticks=4, kwargs['levels'] = cmap_params['levels'] defaults = { - 'add_colorbar': False, - 'add_labels': False, - 'norm': cmap_params.pop('cnorm'), - } + 'add_colorbar': False, + 'add_labels': False, + 'norm': cmap_params.pop('cnorm'), + } # Order is important defaults.update(cmap_params) @@ -272,7 +269,7 @@ def map_dataarray(self, plotfunc, x, y, max_xticks=4, max_yticks=4, if self.darray.name: cbar.set_label(self.darray.name, rotation=270, - verticalalignment='bottom') + verticalalignment='bottom') # This happens here rather than __init__ since FacetGrid.map should # use default ticks @@ -281,8 +278,8 @@ def map_dataarray(self, plotfunc, x, y, max_xticks=4, max_yticks=4, return self def set_titles(self, template="{coord} = {value}", maxchar=30, - fontsize=_FONTSIZE, **kwargs): - ''' + fontsize=_FONTSIZE, **kwargs): + """ Draw titles either above each facet or on the grid margins. Parameters @@ -300,14 +297,12 @@ def set_titles(self, template="{coord} = {value}", maxchar=30, ------- self: FacetGrid object - ''' - - import matplotlib as mpl + """ kwargs['fontsize'] = fontsize nicetitle = functools.partial(_nicetitle, maxchar=maxchar, - template=template) + template=template) if self._single_group: for d, ax in zip(self.name_dicts.flat, self.axes.flat): @@ -318,25 +313,27 @@ def set_titles(self, template="{coord} = {value}", maxchar=30, ax.set_title(title, **kwargs) else: # The row titles on the right edge of the grid - if self._margin_titles: - for ax, row_name in zip(self.axes[:, -1], self.row_names): - title = nicetitle(coord=self.row, value=row_name, maxchar=maxchar) - ax.annotate(title, xy=(1.02, .5), xycoords="axes fraction", - rotation=270, ha="left", va="center", **kwargs) + for ax, row_name in zip(self.axes[:, -1], self.row_names): + title = nicetitle(coord=self.row, value=row_name, + maxchar=maxchar) + ax.annotate(title, xy=(1.02, .5), xycoords="axes fraction", + rotation=270, ha="left", va="center", **kwargs) # The column titles on the top row for ax, col_name in zip(self.axes[0, :], self.col_names): - title = nicetitle(coord=self.col, value=col_name, maxchar=maxchar) + title = nicetitle(coord=self.col, value=col_name, + maxchar=maxchar) ax.set_title(title, **kwargs) return self - def set_ticks(self, max_xticks=_NTICKS, max_yticks=_NTICKS, fontsize=_FONTSIZE): - ''' + def set_ticks(self, max_xticks=_NTICKS, max_yticks=_NTICKS, + fontsize=_FONTSIZE): + """ Sets tick behavior. Refer to documentation in :meth:`FacetGrid.map_dataarray` - ''' + """ from matplotlib.ticker import MaxNLocator # Both are necessary @@ -347,11 +344,14 @@ def set_ticks(self, max_xticks=_NTICKS, max_yticks=_NTICKS, fontsize=_FONTSIZE): ax.xaxis.set_major_locator(x_major_locator) ax.yaxis.set_major_locator(y_major_locator) for tick in itertools.chain(ax.xaxis.get_major_ticks(), - ax.yaxis.get_major_ticks()): + ax.yaxis.get_major_ticks()): tick.label.set_fontsize(fontsize) + return self + def map(self, func, *args, **kwargs): - '''Apply a plotting function to each facet's subset of the data. + """ + Apply a plotting function to each facet's subset of the data. Parameters ---------- @@ -371,7 +371,7 @@ def map(self, func, *args, **kwargs): ------- self : FacetGrid object - ''' + """ import matplotlib.pyplot as plt for ax, namedict in zip(self, self.name_dicts.flat): diff --git a/xray/plot/plot.py b/xray/plot/plot.py index 3874f08e7c5..ab18cae878f 100644 --- a/xray/plot/plot.py +++ b/xray/plot/plot.py @@ -56,11 +56,11 @@ def _load_default_cmap(fname='default_colormap.csv'): def _infer_xy_labels(plotfunc, darray, x, y): - ''' + """ Determine x and y labels when some are missing. For use in _plot2d darray is a 2 dimensional data array. - ''' + """ dims = list(darray.dims) if len(dims) != 2: @@ -272,8 +272,8 @@ def _determine_cmap_params(plot_data, vmin=None, vmax=None, cmap=None, ROBUST_PERCENTILE = 2.0 import matplotlib as mpl - #calc_data = np.ravel(plot_data[~pd.isnull(plot_data)]) - calc_data = plot_data[~pd.isnull(plot_data)] + calc_data = np.ravel(plot_data[~pd.isnull(plot_data)]) + if vmin is None: vmin = np.percentile(calc_data, ROBUST_PERCENTILE) if robust else calc_data.min() if vmax is None: @@ -405,10 +405,10 @@ def _build_discrete_cmap(cmap, levels, extend, filled): # MUST run before any 2d plotting functions are defined since # _plot2d decorator adds them as methods here. class _PlotMethods(object): - ''' + """ Enables use of xray.plot functions as attributes on a DataArray. For example, DataArray.plot.imshow - ''' + """ def __init__(self, DataArray_instance): self._da = DataArray_instance @@ -431,7 +431,7 @@ def _plot2d(plotfunc): Also adds the 2d plot method to class _PlotMethods """ - commondoc = ''' + commondoc = """ Parameters ---------- darray : DataArray @@ -486,7 +486,7 @@ def _plot2d(plotfunc): artist : The same type of primitive artist that the wrapped matplotlib function returns - ''' + """ # Build on the original docstring plotfunc.__doc__ = '\n'.join((plotfunc.__doc__, commondoc)) @@ -584,12 +584,12 @@ def plotmethod(_PlotMethods_obj, x=None, y=None, ax=None, xincrease=None, yincre add_colorbar=True, add_labels=True, vmin=None, vmax=None, cmap=None, colors=None, center=None, robust=False, extend=None, levels=None, **kwargs): - ''' + """ The method should have the same signature as the function. This just makes the method work on Plotmethods objects, and passes all the other arguments straight through. - ''' + """ allargs = locals() allargs['darray'] = _PlotMethods_obj._da allargs.update(kwargs) diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index ed537f91cb5..c055d536465 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -676,8 +676,6 @@ def test_groups(self): z0 = self.darray.isel(z=0) self.assertDataArrayEqual(upperleft_array, z0) - # Not sure if we need to expose this in this way - #self.assertDataArrayEqual(self.facet_data[0, 0], z0) def test_float_index(self): self.darray.coords['z'] = [0.1, 0.2, 0.4] @@ -771,11 +769,3 @@ def test_default_labels(self): # Top row should be labeled for label, ax in zip(self.darray.coords['col'].values, g.axes[0, :]): self.assertTrue(substring_in_axes(label, ax)) - - def test_margin_titles_false(self): - g = xplt.FacetGrid(self.darray, col='col', row='row', - margin_titles=False) - - # Rightmost column should not be labeled - for label, ax in zip(self.darray.coords['row'].values, g.axes[:, -1]): - self.assertFalse(substring_in_axes(label, ax)) From 29db0101b68ca1522a2cfe10f46d9df245222b91 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 4 Sep 2015 11:04:36 -0700 Subject: [PATCH 32/37] Use default axis labels and sizes --- xray/plot/facetgrid.py | 27 +++++++++++------------ xray/test/test_plot.py | 50 ++++++++++++++++++++---------------------- 2 files changed, 37 insertions(+), 40 deletions(-) diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index 2a56aa621e0..976e9042b94 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -169,8 +169,7 @@ def __init__(self, darray, col=None, row=None, col_wrap=None, def __iter__(self): return self.axes.flat - def map_dataarray(self, plotfunc, x, y, max_xticks=4, max_yticks=4, - fontsize=_FONTSIZE, **kwargs): + def map_dataarray(self, plotfunc, x, y, **kwargs): """ Apply a plotting function to a 2d facet's subset of the data. @@ -183,12 +182,6 @@ def map_dataarray(self, plotfunc, x, y, max_xticks=4, max_yticks=4, plotting method such as `xray.plot.imshow` x, y : string Names of the coordinates to plot on x, y axes - max_xticks, max_yticks : int, optional - Maximum number of labeled ticks to plot on x, y axes - max_yticks : int - Maximum number of tick marks to place on y axis - fontsize : string or int - Font size as used by matplotlib text kwargs : additional keyword arguments to plotfunc @@ -271,10 +264,6 @@ def map_dataarray(self, plotfunc, x, y, max_xticks=4, max_yticks=4, cbar.set_label(self.darray.name, rotation=270, verticalalignment='bottom') - # This happens here rather than __init__ since FacetGrid.map should - # use default ticks - self.set_ticks(max_xticks, max_yticks, fontsize) - return self def set_titles(self, template="{coord} = {value}", maxchar=30, @@ -330,9 +319,19 @@ def set_titles(self, template="{coord} = {value}", maxchar=30, def set_ticks(self, max_xticks=_NTICKS, max_yticks=_NTICKS, fontsize=_FONTSIZE): """ - Sets tick behavior. + Set and control tick behavior + + Parameters + ---------- + max_xticks, max_yticks : int, optional + Maximum number of labeled ticks to plot on x, y axes + fontsize : string or int + Font size as used by matplotlib text + + Returns + ------- + self : FacetGrid object - Refer to documentation in :meth:`FacetGrid.map_dataarray` """ from matplotlib.ticker import MaxNLocator diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index c055d536465..048d102671b 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -1,5 +1,3 @@ -import unittest - import numpy as np import pandas as pd @@ -207,6 +205,7 @@ def test_plot_nans(self): @requires_matplotlib class TestDetermineCmapParams(TestCase): + def setUp(self): self.data = np.linspace(0, 1, num=100) @@ -263,6 +262,7 @@ def test_list_levels(self): @requires_matplotlib class TestDiscreteColorMap(TestCase): + def setUp(self): x = np.arange(start=0, stop=10, step=2) y = np.arange(start=9, stop=-7, step=-3) @@ -347,8 +347,10 @@ class Common2dMixin: These tests assume that a staticmethod for `self.plotfunc` exists. Should have the same name as the method. """ + def setUp(self): - self.darray = DataArray(easy_array((10, 15), start=-1), dims=['y', 'x']) + self.darray = DataArray(easy_array( + (10, 15), start=-1), dims=['y', 'x']) self.plotmethod = getattr(self.darray.plot, self.plotfunc.__name__) def test_label_names(self): @@ -412,7 +414,7 @@ def test_seaborn_palette_as_cmap(self): try: import seaborn cmap_name = self.plotmethod( - levels=2, cmap='husl').get_cmap().name + levels=2, cmap='husl').get_cmap().name self.assertEqual('husl', cmap_name) except ImportError: pass @@ -452,12 +454,6 @@ def test_bad_x_string_exception(self): with self.assertRaisesRegexp(KeyError, r'y'): self.plotmethod('z') - def test_default_title(self): - a = DataArray(easy_array((4, 3, 2, 1)), dims=['a', 'b', 'c', 'd']) - self.plotfunc(a.isel(c=1)) - title = plt.gca().get_title() - self.assertEqual('c = 1, d = 0', title) - def test_default_title(self): a = DataArray(easy_array((4, 3, 2)), dims=['a', 'b', 'c']) a.coords['d'] = 10 @@ -535,21 +531,22 @@ def _color_as_tuple(c): return tuple(c[:3]) artist = self.plotmethod(colors='k') self.assertEqual( - _color_as_tuple(artist.cmap.colors[0]), - (0.0,0.0,0.0)) + _color_as_tuple(artist.cmap.colors[0]), + (0.0, 0.0, 0.0)) - artist = self.plotmethod(colors=['k','b']) + artist = self.plotmethod(colors=['k', 'b']) self.assertEqual( - _color_as_tuple(artist.cmap.colors[1]), - (0.0,0.0,1.0)) + _color_as_tuple(artist.cmap.colors[1]), + (0.0, 0.0, 1.0)) def test_cmap_and_color_both(self): - with self.assertRaises(ValueError): + with self.assertRaises(ValueError): self.plotmethod(colors='k', cmap='RdBu') def list_of_colors_in_cmap_deprecated(self): - with self.assertRaises(DeprecationError): - self.plotmethod(cmap=['k','b']) + with self.assertRaises(Exception): + self.plotmethod(cmap=['k', 'b']) + class TestPcolormesh(Common2dMixin, PlotTestCase): @@ -656,11 +653,11 @@ def test_colorbar(self): # There's only one colorbar cbar = plt.gcf().findobj(mpl.collections.QuadMesh) self.assertEqual(1, len(cbar)) - + def test_empty_cell(self): g = xplt.FacetGrid(self.darray, col='z', col_wrap=2) g.map_dataarray(xplt.imshow, 'x', 'y') - + bottomright = g.axes[-1, -1] self.assertFalse(bottomright.has_data()) self.assertFalse(bottomright.get_visible()) @@ -685,7 +682,7 @@ def test_float_index(self): def test_nonunique_index_error(self): self.darray.coords['z'] = [0.1, 0.2, 0.2] with self.assertRaisesRegexp(ValueError, r'[Uu]nique'): - g = xplt.FacetGrid(self.darray, col='z') + xplt.FacetGrid(self.darray, col='z') def test_robust(self): z = np.zeros((20, 20, 2)) @@ -730,8 +727,9 @@ def test_figure_size(self): def test_num_ticks(self): nticks = 100 maxticks = nticks + 1 - self.g.map_dataarray(xplt.imshow, 'x', 'y', max_xticks=nticks, - max_yticks=nticks) + self.g.map_dataarray(xplt.imshow, 'x', 'y') + self.g.set_ticks(max_xticks=nticks, max_yticks=nticks) + for ax in self.g: xticks = len(ax.get_xticks()) yticks = len(ax.get_yticks()) @@ -745,14 +743,14 @@ def test_map(self): class TestFacetGrid4d(PlotTestCase): - + def setUp(self): a = easy_array((10, 15, 3, 2)) darray = DataArray(a, dims=['y', 'x', 'col', 'row']) darray.coords['col'] = np.array(['col' + str(x) for x in - darray.coords['col'].values]) + darray.coords['col'].values]) darray.coords['row'] = np.array(['row' + str(x) for x in - darray.coords['row'].values]) + darray.coords['row'].values]) self.darray = darray From 56761afb2773a978e5b55b22edd2678d4934ee3d Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 4 Sep 2015 11:33:45 -0700 Subject: [PATCH 33/37] change from contourf to imshow for faceted plotting examples --- doc/plotting.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 0d8d5ccbd83..bd74e10e2a4 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -350,7 +350,7 @@ We can use :py:meth:`xray.FacetGrid.map_dataarray` on a DataArray: g = xray.plot.FacetGrid(t, col='time', col_wrap=3) @savefig plot_facet_dataarray.png height=12in - g.map_dataarray(xray.plot.contourf, 'lon', 'lat') + g.map_dataarray(xray.plot.imshow, 'lon', 'lat') FacetGrid Objects ~~~~~~~~~~~~~~~~~ @@ -367,7 +367,7 @@ Pick out individual axes using the ``.axes`` attribute. g = (xray.plot .FacetGrid(t, col='time', col_wrap=3) - .map_dataarray(xray.plot.contourf, 'lon', 'lat') + .map_dataarray(xray.plot.imshow, 'lon', 'lat') ) for i, ax in enumerate(g): From f6dc6a2bab3808ed81df14d8dc0fa182d1ef81a3 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 4 Sep 2015 12:32:35 -0700 Subject: [PATCH 34/37] darray now called data, remove iterable behavior on FacetGrid --- doc/plotting.rst | 20 +++++++++++++++++--- xray/plot/facetgrid.py | 39 ++++++++++++++++++--------------------- xray/test/test_plot.py | 8 ++++---- 3 files changed, 39 insertions(+), 28 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index bd74e10e2a4..4340b7c7495 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -359,9 +359,23 @@ FacetGrid Objects multiple plots. It borrows an API and code from `Seaborn `_. +The structure is contained within the ``axes`` and ``name_dicts`` +attributes, both 2d Numpy object arrays. -Iterating over the FacetGrid iterates over the individual axes. -Pick out individual axes using the ``.axes`` attribute. +.. ipython:: python + + g.axes + + g.name_dicts + +It's possible to select the :py:class:`xray.DataArray` corresponding to the FacetGrid +through the ``name_dicts``. + +.. ipython:: python + + g.data.loc[g.name_dicts[0, 0]] + +Here is an example of modifying the axes after they have been plotted. .. ipython:: python @@ -370,7 +384,7 @@ Pick out individual axes using the ``.axes`` attribute. .map_dataarray(xray.plot.imshow, 'lon', 'lat') ) - for i, ax in enumerate(g): + for i, ax in enumerate(g.axes.flat): ax.set_title('Air Temperature %d' % i) bottomright = g.axes[-1, -1] diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index 976e9042b94..36f1f1325c6 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -68,12 +68,12 @@ class FacetGrid(object): """ - def __init__(self, darray, col=None, row=None, col_wrap=None, + def __init__(self, data, col=None, row=None, col_wrap=None, aspect=1, size=3): """ Parameters ---------- - darray : DataArray + data : DataArray xray DataArray to be plotted row, col : strings Dimesion names that define subsets of the data, which will be drawn @@ -91,8 +91,8 @@ def __init__(self, darray, col=None, row=None, col_wrap=None, import matplotlib.pyplot as plt # Handle corner case of nonunique coordinates - rep_col = col is not None and not darray[col].to_index().is_unique - rep_row = row is not None and not darray[row].to_index().is_unique + rep_col = col is not None and not data[col].to_index().is_unique + rep_row = row is not None and not data[row].to_index().is_unique if rep_col or rep_row: raise ValueError('Coordinates used for faceting cannot ' 'contain repeated (nonunique) values.') @@ -100,8 +100,8 @@ def __init__(self, darray, col=None, row=None, col_wrap=None, # self._single_group is the grouping variable, if there is exactly one if col and row: self._single_group = False - self._nrow = len(darray[row]) - self._ncol = len(darray[col]) + self._nrow = len(data[row]) + self._ncol = len(data[col]) self.nfacet = self._nrow * self._ncol if col_wrap is not None: warnings.warn('Ignoring col_wrap since both col and row ' @@ -116,7 +116,7 @@ def __init__(self, darray, col=None, row=None, col_wrap=None, # Compute grid shape if self._single_group: - self.nfacet = len(darray[self._single_group]) + self.nfacet = len(data[self._single_group]) if col: # idea - could add heuristic for nice shapes like 3x4 self._ncol = self.nfacet @@ -138,12 +138,12 @@ def __init__(self, darray, col=None, row=None, col_wrap=None, squeeze=False, figsize=figsize) # Set up the lists of names for the row and column facet variables - col_names = list(darray[col].values) if col else [] - row_names = list(darray[row].values) if row else [] + col_names = list(data[col].values) if col else [] + row_names = list(data[row].values) if row else [] if self._single_group: full = [{self._single_group: x} for x in - darray[self._single_group].values] + data[self._single_group].values] empty = [None for x in range(self._nrow * self._ncol - len(full))] name_dicts = full + empty else: @@ -154,7 +154,7 @@ def __init__(self, darray, col=None, row=None, col_wrap=None, self.row_names = row_names self.col_names = col_names - self.darray = darray + self.data = data self.row = row self.col = col self.col_wrap = col_wrap @@ -166,9 +166,6 @@ def __init__(self, darray, col=None, row=None, col_wrap=None, self.set_titles() - def __iter__(self): - return self.axes.flat - def map_dataarray(self, plotfunc, x, y, **kwargs): """ Apply a plotting function to a 2d facet's subset of the data. @@ -192,7 +189,7 @@ def map_dataarray(self, plotfunc, x, y, **kwargs): """ # These should be consistent with xray.plot._plot2d - cmap_kwargs = {'plot_data': self.darray.values, + cmap_kwargs = {'plot_data': self.data.values, 'vmin': None, 'vmax': None, 'cmap': None, @@ -210,7 +207,7 @@ def map_dataarray(self, plotfunc, x, y, **kwargs): cmap_kwargs[param] = kwargs[param] # colormap inference has to happen here since all the data in - # self.darray is required to make the right choice + # self.data is required to make the right choice cmap_params = _determine_cmap_params(**cmap_kwargs) if 'contour' in plotfunc.__name__: @@ -233,7 +230,7 @@ def map_dataarray(self, plotfunc, x, y, **kwargs): for d, ax in zip(self.name_dicts.flat, self.axes.flat): # None is the sentinel value if d is not None: - subset = self.darray.loc[d] + subset = self.data.loc[d] mappable = plotfunc(subset, x, y, ax=ax, **defaults) self.x = x @@ -260,8 +257,8 @@ def map_dataarray(self, plotfunc, x, y, **kwargs): ax=list(self.axes.flat), extend=cmap_params['extend']) - if self.darray.name: - cbar.set_label(self.darray.name, rotation=270, + if self.data.name: + cbar.set_label(self.data.name, rotation=270, verticalalignment='bottom') return self @@ -373,9 +370,9 @@ def map(self, func, *args, **kwargs): """ import matplotlib.pyplot as plt - for ax, namedict in zip(self, self.name_dicts.flat): + for ax, namedict in zip(self.axes.flat, self.name_dicts.flat): if namedict is not None: - data = self.darray[namedict] + data = self.data[namedict] plt.sca(ax) innerargs = [data[a].values for a in args] func(*innerargs, **kwargs) diff --git a/xray/test/test_plot.py b/xray/test/test_plot.py index 048d102671b..c533b8e9bba 100644 --- a/xray/test/test_plot.py +++ b/xray/test/test_plot.py @@ -478,7 +478,7 @@ def test_facetgrid(self): d = DataArray(a, dims=['y', 'x', 'z']) g = xplt.FacetGrid(d, col='z') g.map_dataarray(self.plotfunc, 'x', 'y') - for ax in g: + for ax in g.axes.flat: self.assertTrue(ax.has_data()) @@ -610,7 +610,7 @@ def test_no_args(self): alltxt = text_in_fig() self.assertNotIn('None', alltxt) - for ax in self.g: + for ax in self.g.axes.flat: self.assertTrue(ax.has_data()) # default font size should be small @@ -620,7 +620,7 @@ def test_no_args(self): def test_names_appear_somewhere(self): self.darray.name = 'testvar' self.g.map_dataarray(xplt.contourf, 'x', 'y') - for i, ax in enumerate(self.g): + for i, ax in enumerate(self.g.axes.flat): self.assertEqual('z = {0}'.format(i), ax.get_title()) alltxt = text_in_fig() @@ -730,7 +730,7 @@ def test_num_ticks(self): self.g.map_dataarray(xplt.imshow, 'x', 'y') self.g.set_ticks(max_xticks=nticks, max_yticks=nticks) - for ax in self.g: + for ax in self.g.axes.flat: xticks = len(ax.get_xticks()) yticks = len(ax.get_yticks()) self.assertLessEqual(xticks, maxticks) From 15babf1456a92f08bcd6f9b5600de52e43caf65e Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 4 Sep 2015 14:01:02 -0700 Subject: [PATCH 35/37] attributes all set in bottom of __init__ --- xray/plot/facetgrid.py | 79 +++++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index 36f1f1325c6..ffe6b77b70e 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -97,72 +97,81 @@ def __init__(self, data, col=None, row=None, col_wrap=None, raise ValueError('Coordinates used for faceting cannot ' 'contain repeated (nonunique) values.') - # self._single_group is the grouping variable, if there is exactly one + # single_group is the grouping variable, if there is exactly one if col and row: - self._single_group = False - self._nrow = len(data[row]) - self._ncol = len(data[col]) - self.nfacet = self._nrow * self._ncol + single_group = False + nrow = len(data[row]) + ncol = len(data[col]) + nfacet = nrow * ncol if col_wrap is not None: warnings.warn('Ignoring col_wrap since both col and row ' 'were passed') elif row and not col: - self._single_group = row + single_group = row elif not row and col: - self._single_group = col + single_group = col else: raise ValueError( 'Pass a coordinate name as an argument for row or col') # Compute grid shape - if self._single_group: - self.nfacet = len(data[self._single_group]) + if single_group: + nfacet = len(data[single_group]) if col: # idea - could add heuristic for nice shapes like 3x4 - self._ncol = self.nfacet + ncol = nfacet if row: - self._ncol = 1 + ncol = 1 if col_wrap is not None: # Overrides previous settings - self._ncol = col_wrap - self._nrow = int(np.ceil(self.nfacet / self._ncol)) + ncol = col_wrap + nrow = int(np.ceil(nfacet / ncol)) # Calculate the base figure size with extra horizontal space for a # colorbar - self._cbar_space = 1 - figsize = (self._ncol * size * aspect + - self._cbar_space, self._nrow * size) + cbar_space = 1 + figsize = (ncol * size * aspect + + cbar_space, nrow * size) - self.fig, self.axes = plt.subplots(self._nrow, self._ncol, - sharex=True, sharey=True, - squeeze=False, figsize=figsize) + fig, axes = plt.subplots(nrow, ncol, + sharex=True, sharey=True, + squeeze=False, figsize=figsize) # Set up the lists of names for the row and column facet variables col_names = list(data[col].values) if col else [] row_names = list(data[row].values) if row else [] - if self._single_group: - full = [{self._single_group: x} for x in - data[self._single_group].values] - empty = [None for x in range(self._nrow * self._ncol - len(full))] + if single_group: + full = [{single_group: x} for x in + data[single_group].values] + empty = [None for x in range(nrow * ncol - len(full))] name_dicts = full + empty else: rowcols = itertools.product(row_names, col_names) name_dicts = [{row: r, col: c} for r, c in rowcols] - self.name_dicts = np.array(name_dicts).reshape(self._nrow, self._ncol) + name_dicts = np.array(name_dicts).reshape(nrow, ncol) + # Set up the class attributes + # --------------------------- + + # First the public API + self.data = data + self.name_dicts = name_dicts + self.fig = fig + self.axes = axes self.row_names = row_names self.col_names = col_names - self.data = data - self.row = row - self.col = col - self.col_wrap = col_wrap # Next the private variables + self._single_group = single_group + self._nrow = nrow self._row_var = row + self._ncol = ncol self._col_var = col self._col_wrap = col_wrap + self._x_var = None + self._y_var = None self.set_titles() @@ -233,16 +242,13 @@ def map_dataarray(self, plotfunc, x, y, **kwargs): subset = self.data.loc[d] mappable = plotfunc(subset, x, y, ax=ax, **defaults) - self.x = x - self.y = y - # Left side labels for ax in self.axes[:, 0]: - ax.set_ylabel(self.y) + ax.set_ylabel(y) # Bottom labels for ax in self.axes[-1, :]: - ax.set_xlabel(self.x) + ax.set_xlabel(x) self.fig.tight_layout() @@ -261,6 +267,9 @@ def map_dataarray(self, plotfunc, x, y, **kwargs): cbar.set_label(self.data.name, rotation=270, verticalalignment='bottom') + self._x_var = x + self._y_var = y + return self def set_titles(self, template="{coord} = {value}", maxchar=30, @@ -300,14 +309,14 @@ def set_titles(self, template="{coord} = {value}", maxchar=30, else: # The row titles on the right edge of the grid for ax, row_name in zip(self.axes[:, -1], self.row_names): - title = nicetitle(coord=self.row, value=row_name, + title = nicetitle(coord=self._row_var, value=row_name, maxchar=maxchar) ax.annotate(title, xy=(1.02, .5), xycoords="axes fraction", rotation=270, ha="left", va="center", **kwargs) # The column titles on the top row for ax, col_name in zip(self.axes[0, :], self.col_names): - title = nicetitle(coord=self.col, value=col_name, + title = nicetitle(coord=self._col_var, value=col_name, maxchar=maxchar) ax.set_title(title, **kwargs) From 99dbb5e441c621e91cac4ea333e4b9f7c2ea2238 Mon Sep 17 00:00:00 2001 From: Stephan Hoyer Date: Fri, 4 Sep 2015 15:17:05 -0700 Subject: [PATCH 36/37] don't show FacetGrid methods directly in API docs --- doc/api-hidden.rst | 5 +++++ doc/api.rst | 10 ---------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/doc/api-hidden.rst b/doc/api-hidden.rst index 4016eba65cd..a085970ee09 100644 --- a/doc/api-hidden.rst +++ b/doc/api-hidden.rst @@ -143,3 +143,8 @@ ufuncs.tan ufuncs.tanh ufuncs.trunc + + plot.FacetGrid.map_dataarray + plot.FacetGrid.set_titles + plot.FacetGrid.set_ticks + plot.FacetGrid.map diff --git a/doc/api.rst b/doc/api.rst index 0262eeada77..582df933d98 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -413,13 +413,3 @@ Plotting plot.pcolormesh plot.FacetGrid -FacetGrid methods ------------------ - -.. autosummary:: - :toctree: generated/ - - plot.FacetGrid.map_dataarray - plot.FacetGrid.set_titles - plot.FacetGrid.set_ticks - plot.FacetGrid.map From b48a50b6b1f3a2441481e5f7e23a6906f0132e74 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 4 Sep 2015 15:17:07 -0700 Subject: [PATCH 37/37] rename plotfunc arg to func --- doc/plotting.rst | 3 +-- xray/plot/facetgrid.py | 14 +++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 4340b7c7495..78de158e2f5 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -48,7 +48,6 @@ Imports # Use defaults so we don't get gridlines in generated docs import matplotlib as mpl mpl.rcdefaults() - mpl.rcParams.update({'figure.autolayout': True}) The following imports are necessary for all of the examples. @@ -343,7 +342,7 @@ Simple Example TODO - replace with the convenience method from plot -We can use :py:meth:`xray.FacetGrid.map_dataarray` on a DataArray: +We can use :py:meth:`xray.plot.FacetGrid.map_dataarray` on a DataArray: .. ipython:: python diff --git a/xray/plot/facetgrid.py b/xray/plot/facetgrid.py index ffe6b77b70e..4e335f3b1d1 100644 --- a/xray/plot/facetgrid.py +++ b/xray/plot/facetgrid.py @@ -175,7 +175,7 @@ def __init__(self, data, col=None, row=None, col_wrap=None, self.set_titles() - def map_dataarray(self, plotfunc, x, y, **kwargs): + def map_dataarray(self, func, x, y, **kwargs): """ Apply a plotting function to a 2d facet's subset of the data. @@ -183,13 +183,13 @@ def map_dataarray(self, plotfunc, x, y, **kwargs): Parameters ---------- - plotfunc : callable + func : callable A plotting function with the same signature as a 2d xray plotting method such as `xray.plot.imshow` x, y : string Names of the coordinates to plot on x, y axes kwargs : - additional keyword arguments to plotfunc + additional keyword arguments to func Returns ------- @@ -206,8 +206,8 @@ def map_dataarray(self, plotfunc, x, y, **kwargs): 'robust': False, 'extend': None, # MPL default - 'levels': 7 if 'contour' in plotfunc.__name__ else None, - 'filled': plotfunc.__name__ != 'contour', + 'levels': 7 if 'contour' in func.__name__ else None, + 'filled': func.__name__ != 'contour', } # Allow kwargs to override these defaults @@ -219,7 +219,7 @@ def map_dataarray(self, plotfunc, x, y, **kwargs): # self.data is required to make the right choice cmap_params = _determine_cmap_params(**cmap_kwargs) - if 'contour' in plotfunc.__name__: + if 'contour' in func.__name__: # extend is a keyword argument only for contour and contourf, but # passing it to the colorbar is sufficient for imshow and # pcolormesh @@ -240,7 +240,7 @@ def map_dataarray(self, plotfunc, x, y, **kwargs): # None is the sentinel value if d is not None: subset = self.data.loc[d] - mappable = plotfunc(subset, x, y, ax=ax, **defaults) + mappable = func(subset, x, y, ax=ax, **defaults) # Left side labels for ax in self.axes[:, 0]: