From 3ab7ed11f71ed400b58d328889a98ccbd9a7cdf3 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 26 Jun 2015 16:29:01 -0700 Subject: [PATCH 01/61] plotmethod decorator to import matplotlib --- xray/core/dataarray.py | 10 +++++++++- xray/core/utils.py | 12 ++++++++++++ xray/test/test_plotting.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 xray/test/test_plotting.py diff --git a/xray/core/dataarray.py b/xray/core/dataarray.py index d8dfee7ca09..0ae2df68f5a 100644 --- a/xray/core/dataarray.py +++ b/xray/core/dataarray.py @@ -14,7 +14,7 @@ from .coordinates import DataArrayCoordinates, Indexes from .dataset import Dataset from .pycompat import iteritems, basestring, OrderedDict, zip -from .utils import FrozenOrderedDict +from .utils import FrozenOrderedDict, plotmethod from .variable import as_variable, _as_compatible_data, Coordinate @@ -1073,6 +1073,14 @@ def func(self, other): return self return func + @plotmethod + def plot(self): + """Plot DataArray using Matplotlib + + """ + plt.scatter + pass + # priority most be higher than Variable to properly work with binary ufuncs ops.inject_all_ops_and_reduce_methods(DataArray, priority=60) diff --git a/xray/core/utils.py b/xray/core/utils.py index 67e4f445a39..ec5229f687e 100644 --- a/xray/core/utils.py +++ b/xray/core/utils.py @@ -391,3 +391,15 @@ def close_on_error(f): def is_remote_uri(path): return bool(re.search('^https?\://', path)) + + +def plotmethod(func): + """Decorator to lazily import matplotlib. + """ + import matplotlib.pyplot as plt + + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py new file mode 100644 index 00000000000..269b19883c5 --- /dev/null +++ b/xray/test/test_plotting.py @@ -0,0 +1,35 @@ +import sys + +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt + +from xray import (Dataset, DataArray) + +from . import TestCase + + +class PlotTestCase(TestCase): + + def setUp(self): + d = [0, 1, 0, 2] + self.dv = DataArray(d, coords={'period': range(len(d))}) + + def tearDown(self): + # Remove all matplotlib figures + pass + + +class TestBasics(PlotTestCase): + + # Not sure how to test this + def test_matplotlib_not_imported(self): + # Doesn't work. Keeping so I remember to change it. + #self.assertFalse('matplotlib' in sys.modules) + pass + + +class TestDataArray(PlotTestCase): + + def test_plot_exists_and_callable(self): + self.assertTrue(callable(self.dv.plot)) From e8671f5d60e8ec1caaeff4409ba1d893d2885ecb Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 26 Jun 2015 17:28:32 -0700 Subject: [PATCH 02/61] #185 organizing structure for plotting --- xray/core/dataarray.py | 10 +++------- xray/core/plotting.py | 5 +++++ xray/core/utils.py | 12 ------------ 3 files changed, 8 insertions(+), 19 deletions(-) create mode 100644 xray/core/plotting.py diff --git a/xray/core/dataarray.py b/xray/core/dataarray.py index 0ae2df68f5a..5838adfe08c 100644 --- a/xray/core/dataarray.py +++ b/xray/core/dataarray.py @@ -13,8 +13,9 @@ from .common import AbstractArray, BaseDataObject from .coordinates import DataArrayCoordinates, Indexes from .dataset import Dataset +from .plotting import _plot from .pycompat import iteritems, basestring, OrderedDict, zip -from .utils import FrozenOrderedDict, plotmethod +from .utils import FrozenOrderedDict from .variable import as_variable, _as_compatible_data, Coordinate @@ -1073,13 +1074,8 @@ def func(self, other): return self return func - @plotmethod - def plot(self): - """Plot DataArray using Matplotlib - """ - plt.scatter - pass +DataArray.plot = _plot # priority most be higher than Variable to properly work with binary ufuncs diff --git a/xray/core/plotting.py b/xray/core/plotting.py new file mode 100644 index 00000000000..9f8735d7d1b --- /dev/null +++ b/xray/core/plotting.py @@ -0,0 +1,5 @@ +def _plot(darray, *args, **kwargs): + """Plot a DataArray + """ + import matplotlib.pyplot as plt + return plt.plot(darray, *args, **kwargs) diff --git a/xray/core/utils.py b/xray/core/utils.py index ec5229f687e..67e4f445a39 100644 --- a/xray/core/utils.py +++ b/xray/core/utils.py @@ -391,15 +391,3 @@ def close_on_error(f): def is_remote_uri(path): return bool(re.search('^https?\://', path)) - - -def plotmethod(func): - """Decorator to lazily import matplotlib. - """ - import matplotlib.pyplot as plt - - @functools.wraps(func) - def wrapper(*args, **kwargs): - return func(*args, **kwargs) - - return wrapper From bec3054039506afb26c0dec37fc38182d44cc6e5 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Mon, 29 Jun 2015 11:05:47 -0700 Subject: [PATCH 03/61] begin adding docs for plotting --- doc/index.rst | 1 + doc/plotting.rst | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 doc/plotting.rst diff --git a/doc/index.rst b/doc/index.rst index e313698008f..5e775c262c3 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -41,6 +41,7 @@ Documentation pandas io dask + plotting api faq whats-new diff --git a/doc/plotting.rst b/doc/plotting.rst new file mode 100644 index 00000000000..03a0f9705a5 --- /dev/null +++ b/doc/plotting.rst @@ -0,0 +1,26 @@ +Plotting +-------- + +Examples +~~~~~~~~ + +Here are some quick examples of plotting in xray. + +To begin, import numpy, pandas and xray: + +.. ipython:: python + + import numpy as np + import pandas as pd + import xray + import matplotlib.pyplot as plt + + plt.plot((0, 1), (0, 1)) + +Rules +~~~~~ + +xray tries to create reasonable plots based on metadata and the array +dimensions. + +'In the face of ambiguity, refuse the temptation to guess.' From bd5109fd8f8b36220a125b88ce7d7d9ccd38432f Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Mon, 29 Jun 2015 11:11:44 -0700 Subject: [PATCH 04/61] include png output from plot --- doc/plotting.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/plotting.rst b/doc/plotting.rst index 03a0f9705a5..ea9499083d0 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -15,6 +15,7 @@ To begin, import numpy, pandas and xray: import xray import matplotlib.pyplot as plt + @savefig plotting_example1.png plt.plot((0, 1), (0, 1)) Rules From 00bed5071324e5c71cafb787013b13f89e9146e5 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Mon, 29 Jun 2015 12:43:44 -0700 Subject: [PATCH 05/61] multivariate normal example --- doc/plotting.rst | 56 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index ea9499083d0..887b60306a4 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -1,10 +1,17 @@ Plotting --------- +======== -Examples -~~~~~~~~ +xray tries to create reasonable plots based on metadata and the array +dimensions. -Here are some quick examples of plotting in xray. +But it's not always obvious what to plot. A wise man once said: +'In the face of ambiguity, refuse the temptation to guess.' +So try to use the ``plot`` methods, and if you see +a ``ValueError`` then +hopefully the error message will point you in the right direction. + +Examples +-------- To begin, import numpy, pandas and xray: @@ -15,13 +22,42 @@ To begin, import numpy, pandas and xray: import xray import matplotlib.pyplot as plt - @savefig plotting_example1.png +Simple +~~~~~~ + +This is as basic as it comes. xray uses the coordinate name to label the x +axis. + +.. ipython:: python + + @savefig plotting_example_simple.png plt.plot((0, 1), (0, 1)) -Rules -~~~~~ +Multivariate Normal +~~~~~~~~~~~~~~~~~~~ -xray tries to create reasonable plots based on metadata and the array -dimensions. +Consider the density for a two dimensional normal distribution +evaluated on a square grid. -'In the face of ambiguity, refuse the temptation to guess.' +.. ipython:: python + + from scipy.stats import multivariate_normal + + g = np.linspace(-3, 3) + xy = np.dstack(np.meshgrid(g, g)) + + # 2d Normal distribution centered at 1, 0 + rv = multivariate_normal(mean=(1, 0)) + + normal = xray.DataArray(rv.pdf(xy), {'x': g, 'y': g}) + + # TODO- use xray method + @savefig plotting_example_2dnormal.png + plt.contourf(normal.x, normal.y, normal.data) + + +Rules +----- + +The following is a more complete description of how xray determines what +and how to plot. From edcf55c79a033ef2295fb53753d6b63d4070b31a Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Mon, 29 Jun 2015 14:24:14 -0700 Subject: [PATCH 06/61] fleshing out docs a bit --- doc/plotting.rst | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 887b60306a4..af22334613c 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -1,14 +1,14 @@ Plotting ======== -xray tries to create reasonable plots based on metadata and the array +xray tries to create reasonable labeled plots based on metadata and the array dimensions. But it's not always obvious what to plot. A wise man once said: 'In the face of ambiguity, refuse the temptation to guess.' -So try to use the ``plot`` methods, and if you see -a ``ValueError`` then -hopefully the error message will point you in the right direction. +So don't be scared if you see some ``ValueError``'s when +trying to plot, it just means you may need to get the data into a form +where plotting is more natural. Examples -------- @@ -22,19 +22,23 @@ To begin, import numpy, pandas and xray: import xray import matplotlib.pyplot as plt -Simple -~~~~~~ +Sin Function +~~~~~~~~~~~~ -This is as basic as it comes. xray uses the coordinate name to label the x -axis. +Here is a simple example of plotting. +Xray uses the coordinate name to label the x axis. .. ipython:: python + x = np.linspace(0, 2*np.pi) + a = xray.DataArray(np.sin(x), {'x': x}, name='sin(x)') + + # TODO- use xray method @savefig plotting_example_simple.png plt.plot((0, 1), (0, 1)) -Multivariate Normal -~~~~~~~~~~~~~~~~~~~ +Multivariate Normal Density +~~~~~~~~~~~~~~~~~~~~~~~~~~~ Consider the density for a two dimensional normal distribution evaluated on a square grid. From 26b35f19f33d78c3ae597c44a0d1acc7156c3b9c Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Mon, 29 Jun 2015 16:30:38 -0700 Subject: [PATCH 07/61] test for axis label --- xray/core/dataarray.py | 5 +++-- xray/core/plotting.py | 17 ++++++++++++++--- xray/test/test_plotting.py | 25 +++++++++---------------- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/xray/core/dataarray.py b/xray/core/dataarray.py index 5838adfe08c..db4ceb992ac 100644 --- a/xray/core/dataarray.py +++ b/xray/core/dataarray.py @@ -13,7 +13,7 @@ from .common import AbstractArray, BaseDataObject from .coordinates import DataArrayCoordinates, Indexes from .dataset import Dataset -from .plotting import _plot +from .plotting import _plot_dataarray from .pycompat import iteritems, basestring, OrderedDict, zip from .utils import FrozenOrderedDict from .variable import as_variable, _as_compatible_data, Coordinate @@ -1075,8 +1075,9 @@ def func(self, other): return func -DataArray.plot = _plot +# Add plotting methods +DataArray.plot = _plot_dataarray # priority most be higher than Variable to properly work with binary ufuncs ops.inject_all_ops_and_reduce_methods(DataArray, priority=60) diff --git a/xray/core/plotting.py b/xray/core/plotting.py index 9f8735d7d1b..1b6ac4b1ae1 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -1,5 +1,16 @@ -def _plot(darray, *args, **kwargs): - """Plot a DataArray +def _plot_dataarray(darray, *args, **kwargs): + """ + Plot a DataArray """ import matplotlib.pyplot as plt - return plt.plot(darray, *args, **kwargs) + + xlabel = darray.indexes.keys()[0] + x = darray.indexes[xlabel].values + y = darray.values + + # Probably should be using the lower level matplotlib API + plt.plot(x, y, *args, **kwargs) + ax = plt.gca() + ax.set_xlabel(xlabel) + + return ax diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index 269b19883c5..76cf0baaf48 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -9,27 +9,20 @@ from . import TestCase -class PlotTestCase(TestCase): +class TestDataArray(TestCase): def setUp(self): d = [0, 1, 0, 2] - self.dv = DataArray(d, coords={'period': range(len(d))}) + self.darray = DataArray(d, coords={'period': range(len(d))}) def tearDown(self): # Remove all matplotlib figures - pass - - -class TestBasics(PlotTestCase): - - # Not sure how to test this - def test_matplotlib_not_imported(self): - # Doesn't work. Keeping so I remember to change it. - #self.assertFalse('matplotlib' in sys.modules) - pass - - -class TestDataArray(PlotTestCase): + plt.close('all') def test_plot_exists_and_callable(self): - self.assertTrue(callable(self.dv.plot)) + self.assertTrue(callable(self.darray.plot)) + + def test_xlabel_is_coordinate_name(self): + self.darray.plot() + xlabel = plt.gca().get_xlabel() + self.assertEqual(xlabel, 'period') From 723be1c872d814351195e63d4ec6f63776ec2554 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Mon, 29 Jun 2015 16:43:36 -0700 Subject: [PATCH 08/61] plotting works in docs for sin example --- doc/plotting.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index af22334613c..0098ae573a5 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -34,8 +34,8 @@ Xray uses the coordinate name to label the x axis. a = xray.DataArray(np.sin(x), {'x': x}, name='sin(x)') # TODO- use xray method - @savefig plotting_example_simple.png - plt.plot((0, 1), (0, 1)) + @savefig plotting_example_sin.png + a.plot() Multivariate Normal Density ~~~~~~~~~~~~~~~~~~~~~~~~~~~ From cb100f9915366eadcda820dbca567852cc4b46f1 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Mon, 29 Jun 2015 18:04:20 -0700 Subject: [PATCH 09/61] beginning to set up auto docs --- xray/core/plotting.py | 23 +++++++++++++++++++++++ xray/test/test_plotting.py | 30 ++++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/xray/core/plotting.py b/xray/core/plotting.py index 1b6ac4b1ae1..d8507819c59 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -1,3 +1,8 @@ +""" +Plotting functions are implemented here and then monkeypatched in to +DataArray and DataSet classes +""" + def _plot_dataarray(darray, *args, **kwargs): """ Plot a DataArray @@ -14,3 +19,21 @@ def _plot_dataarray(darray, *args, **kwargs): ax.set_xlabel(xlabel) return ax + + +def _plot_contourf(dset, *args, **kwargs): + """ + Plot a Dataset + """ + import matplotlib.pyplot as plt + + xlabel = darray.indexes.keys()[0] + x = darray.indexes[xlabel].values + y = darray.values + + # Probably should be using the lower level matplotlib API + plt.plot(x, y, *args, **kwargs) + ax = plt.gca() + ax.set_xlabel(xlabel) + + return ax diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index 76cf0baaf48..e52c8237ede 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -9,20 +9,34 @@ from . import TestCase -class TestDataArray(TestCase): - - def setUp(self): - d = [0, 1, 0, 2] - self.darray = DataArray(d, coords={'period': range(len(d))}) +class PlotTestCase(TestCase): def tearDown(self): # Remove all matplotlib figures plt.close('all') - def test_plot_exists_and_callable(self): - self.assertTrue(callable(self.darray.plot)) - def test_xlabel_is_coordinate_name(self): +class TestSimpleDataArray(PlotTestCase): + + def setUp(self): + d = [0, 1, 0, 2] + self.darray = DataArray(d, coords={'period': range(len(d))}) + + def test_xlabel_is_index_name(self): self.darray.plot() xlabel = plt.gca().get_xlabel() self.assertEqual(xlabel, 'period') + + +class Test2dDataArray(PlotTestCase): + + def setUp(self): + self.darray = DataArray(np.random.randn(10, 15), + dims=['long', 'lat']) + + def test_label_names(self): + self.darray.plot_contourf() + xlabel = plt.gca().get_xlabel() + ylabel = plt.gca().get_ylabel() + self.assertEqual(xlabel, 'long') + self.assertEqual(ylabel, 'lat') From a18ac2384c9cac6e079b47c4d445d70d8870a434 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 30 Jun 2015 09:29:08 -0700 Subject: [PATCH 10/61] test for ylabel --- xray/test/test_plotting.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index e52c8237ede..d7687294d2a 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -27,6 +27,12 @@ def test_xlabel_is_index_name(self): xlabel = plt.gca().get_xlabel() self.assertEqual(xlabel, 'period') + def test_ylabel_is_data_name(self): + self.darray.name = 'temperature' + self.darray.plot() + ylabel = plt.gca().get_ylabel() + self.assertEqual(ylabel, self.darray.name) + class Test2dDataArray(PlotTestCase): From 2ce0bd9136a6a6b94333bfee9ee1e095f538b846 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 30 Jun 2015 10:55:15 -0700 Subject: [PATCH 11/61] decorator to skip matplotlib tests --- xray/test/__init__.py | 11 +++++++++++ xray/test/test_plotting.py | 16 +++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/xray/test/__init__.py b/xray/test/__init__.py index 1e52fafbd4e..9fb3a702ff8 100644 --- a/xray/test/__init__.py +++ b/xray/test/__init__.py @@ -48,6 +48,13 @@ has_dask = False +try: + import matplotlib + has_matplotlib = True +except ImportError: + has_matplotlib = False + + def requires_scipy(test): return test if has_scipy else unittest.skip('requires scipy')(test) @@ -73,6 +80,10 @@ def requires_dask(test): return test if has_dask else unittest.skip('requires dask')(test) +def requires_matplotlib(test): + return test if has_matplotlib else unittest.skip('requires matplotlib')(test) + + def decode_string_data(data): if data.dtype.kind == 'S': return np.core.defchararray.decode(data, 'utf-8', 'replace') diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index d7687294d2a..1043bea09dc 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -2,12 +2,19 @@ import numpy as np import pandas as pd -import matplotlib.pyplot as plt -from xray import (Dataset, DataArray) +from xray import Dataset, DataArray -from . import TestCase +from . import TestCase, requires_matplotlib +try: + import matplotlib.pyplot as plt +except ImportError: + pass + +# TODO - Every test in this file requires matplotlib +# Hence it's redundant to have to use the decorator on every test +# How to refactor? class PlotTestCase(TestCase): @@ -22,11 +29,13 @@ def setUp(self): d = [0, 1, 0, 2] self.darray = DataArray(d, coords={'period': range(len(d))}) + @requires_matplotlib def test_xlabel_is_index_name(self): self.darray.plot() xlabel = plt.gca().get_xlabel() self.assertEqual(xlabel, 'period') + @requires_matplotlib def test_ylabel_is_data_name(self): self.darray.name = 'temperature' self.darray.plot() @@ -40,6 +49,7 @@ def setUp(self): self.darray = DataArray(np.random.randn(10, 15), dims=['long', 'lat']) + @requires_matplotlib def test_label_names(self): self.darray.plot_contourf() xlabel = plt.gca().get_xlabel() From fabbb915419ec5dc57c44b12112711b9c5102919 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 30 Jun 2015 11:19:14 -0700 Subject: [PATCH 12/61] simple tests passing, working on build --- xray/core/dataarray.py | 3 ++- xray/core/plotting.py | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/xray/core/dataarray.py b/xray/core/dataarray.py index db4ceb992ac..a401155b8ef 100644 --- a/xray/core/dataarray.py +++ b/xray/core/dataarray.py @@ -13,7 +13,7 @@ from .common import AbstractArray, BaseDataObject from .coordinates import DataArrayCoordinates, Indexes from .dataset import Dataset -from .plotting import _plot_dataarray +from .plotting import _plot_dataarray, _plot_contourf from .pycompat import iteritems, basestring, OrderedDict, zip from .utils import FrozenOrderedDict from .variable import as_variable, _as_compatible_data, Coordinate @@ -1078,6 +1078,7 @@ def func(self, other): # Add plotting methods DataArray.plot = _plot_dataarray +DataArray.plot_contourf = _plot_contourf # priority most be higher than Variable to properly work with binary ufuncs ops.inject_all_ops_and_reduce_methods(DataArray, priority=60) diff --git a/xray/core/plotting.py b/xray/core/plotting.py index d8507819c59..f1149ac17cb 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -11,29 +11,32 @@ def _plot_dataarray(darray, *args, **kwargs): xlabel = darray.indexes.keys()[0] x = darray.indexes[xlabel].values - y = darray.values # Probably should be using the lower level matplotlib API - plt.plot(x, y, *args, **kwargs) + plt.plot(x, darray.values, *args, **kwargs) ax = plt.gca() ax.set_xlabel(xlabel) + ax.set_ylabel(darray.name) return ax -def _plot_contourf(dset, *args, **kwargs): +def _plot_contourf(darray, *args, **kwargs): """ - Plot a Dataset + Contour plot """ import matplotlib.pyplot as plt - xlabel = darray.indexes.keys()[0] + xlabel, ylabel = darray.indexes.keys()[0:2] x = darray.indexes[xlabel].values - y = darray.values + y = darray.indexes[ylabel].values - # Probably should be using the lower level matplotlib API - plt.plot(x, y, *args, **kwargs) + # Assume 2d matrix with x on dim_0, y on dim_1 + z = darray.values.T + + plt.contourf(x, y, z, *args, **kwargs) ax = plt.gca() ax.set_xlabel(xlabel) + ax.set_ylabel(ylabel) return ax From 86b032065823c19ebb25acce326d74cafeb09521 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 30 Jun 2015 11:47:00 -0700 Subject: [PATCH 13/61] working on making the build work --- .travis.yml | 4 ++-- doc/plotting.rst | 17 +++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 55ecfff36a1..c736aada335 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ matrix: env: UPDATE_ENV="conda install unittest2 pandas==0.15.0 h5py cython && 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 && pip install cyordereddict h5netcdf" + env: UPDATE_ENV="conda install -c scitools cdat-lite h5py cython matplotlib && pip install cyordereddict h5netcdf" - python: 2.7 # nb. we have to remove scipy because conda install pandas brings it in: # https://github.com/ContinuumIO/anaconda-issues/issues/145 @@ -19,7 +19,7 @@ matrix: - python: 3.3 env: UPDATE_ENV="conda remove netCDF4" - python: 3.4 - env: UPDATE_ENV="conda install -c pandas bottleneck h5py cython dask && pip install cyordereddict h5netcdf" + env: UPDATE_ENV="conda install -c pandas bottleneck h5py cython dask matplotlib && pip install cyordereddict h5netcdf" # don't require pydap tests to pass because the dap test server is unreliable - python: 2.7 env: UPDATE_ENV="pip install pydap" diff --git a/doc/plotting.rst b/doc/plotting.rst index 0098ae573a5..bab40658788 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -22,6 +22,13 @@ To begin, import numpy, pandas and xray: import xray import matplotlib.pyplot as plt +The following line is not necessary, but it makes for a nice style. + +.. ipython:: python + + plt.style.use('ggplot') + + Sin Function ~~~~~~~~~~~~ @@ -31,9 +38,8 @@ Xray uses the coordinate name to label the x axis. .. ipython:: python x = np.linspace(0, 2*np.pi) - a = xray.DataArray(np.sin(x), {'x': x}, name='sin(x)') + a = xray.DataArray(np.sin(x), {'time': x}, name='sin(x)') - # TODO- use xray method @savefig plotting_example_sin.png a.plot() @@ -41,9 +47,9 @@ Multivariate Normal Density ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Consider the density for a two dimensional normal distribution -evaluated on a square grid. - -.. ipython:: python +evaluated on a square grid:: + + # TODO this requires scipy as a dependency for docs to build from scipy.stats import multivariate_normal @@ -59,7 +65,6 @@ evaluated on a square grid. @savefig plotting_example_2dnormal.png plt.contourf(normal.x, normal.y, normal.data) - Rules ----- From eb79aa04fd672c90f7595c8c8968dc1335f5dde5 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 30 Jun 2015 12:52:03 -0700 Subject: [PATCH 14/61] using Agg backend for matplotlib on Travis --- xray/test/test_plotting.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index 1043bea09dc..0c7fb4a994b 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -8,6 +8,9 @@ from . import TestCase, requires_matplotlib try: + import matplotlib + # Allows use of Travis CI. Order of imports is important here + matplotlib.use('Agg') import matplotlib.pyplot as plt except ImportError: pass From a16d0ad133d95dc9ea2c43945e55395e309ebf26 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 30 Jun 2015 15:20:05 -0700 Subject: [PATCH 15/61] using slices to pick correct indexes for axis labels --- xray/core/dataarray.py | 4 ++-- xray/core/plotting.py | 23 ++++++++++++----------- xray/test/test_plotting.py | 9 +++++---- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/xray/core/dataarray.py b/xray/core/dataarray.py index a401155b8ef..948caaabf2c 100644 --- a/xray/core/dataarray.py +++ b/xray/core/dataarray.py @@ -13,7 +13,7 @@ from .common import AbstractArray, BaseDataObject from .coordinates import DataArrayCoordinates, Indexes from .dataset import Dataset -from .plotting import _plot_dataarray, _plot_contourf +from .plotting import _plot_line, _plot_contourf from .pycompat import iteritems, basestring, OrderedDict, zip from .utils import FrozenOrderedDict from .variable import as_variable, _as_compatible_data, Coordinate @@ -1077,7 +1077,7 @@ def func(self, other): # Add plotting methods -DataArray.plot = _plot_dataarray +DataArray.plot = _plot_line DataArray.plot_contourf = _plot_contourf # priority most be higher than Variable to properly work with binary ufuncs diff --git a/xray/core/plotting.py b/xray/core/plotting.py index f1149ac17cb..e8ece4cfdab 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -3,14 +3,17 @@ DataArray and DataSet classes """ -def _plot_dataarray(darray, *args, **kwargs): +# TODO - Is there a better way to import matplotlib in the function? +# Decorators don't preserve the argument names + + +def _plot_line(darray, *args, **kwargs): """ - Plot a DataArray + Line plot """ import matplotlib.pyplot as plt - xlabel = darray.indexes.keys()[0] - x = darray.indexes[xlabel].values + xlabel, x = darray.indexes.items()[0] # Probably should be using the lower level matplotlib API plt.plot(x, darray.values, *args, **kwargs) @@ -27,14 +30,12 @@ def _plot_contourf(darray, *args, **kwargs): """ import matplotlib.pyplot as plt - xlabel, ylabel = darray.indexes.keys()[0:2] - x = darray.indexes[xlabel].values - y = darray.indexes[ylabel].values - - # Assume 2d matrix with x on dim_0, y on dim_1 - z = darray.values.T + # x axis is by default the one corresponding to the 0th axis + xlabel, x = darray[0].indexes.items()[0] + ylabel, y = darray[:, 0].indexes.items()[0] - plt.contourf(x, y, z, *args, **kwargs) + # TODO - revisit needing the transpose here + plt.contourf(x, y, darray.values, *args, **kwargs) ax = plt.gca() ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index 0c7fb4a994b..ee1182785d7 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -9,8 +9,9 @@ try: import matplotlib - # Allows use of Travis CI. Order of imports is important here + # Using a different backend makes Travis CI work. matplotlib.use('Agg') + # Order of imports is important here. import matplotlib.pyplot as plt except ImportError: pass @@ -50,12 +51,12 @@ class Test2dDataArray(PlotTestCase): def setUp(self): self.darray = DataArray(np.random.randn(10, 15), - dims=['long', 'lat']) + dims=['y', 'x']) @requires_matplotlib def test_label_names(self): self.darray.plot_contourf() xlabel = plt.gca().get_xlabel() ylabel = plt.gca().get_ylabel() - self.assertEqual(xlabel, 'long') - self.assertEqual(ylabel, 'lat') + self.assertEqual(xlabel, 'x') + self.assertEqual(ylabel, 'y') From 9c1a268e3ac72c109898e12c8d1dcb6479f828c6 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 30 Jun 2015 15:22:23 -0700 Subject: [PATCH 16/61] import matplotlib for appveyor windows build --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index abdef2b40e5..51abf03748d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -26,7 +26,7 @@ install: - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" # install xray and depenencies - - "conda install --yes --quiet pip nose numpy pandas scipy netCDF4" + - "conda install --yes --quiet pip nose numpy pandas scipy netCDF4 matplotlib" - "python setup.py install" build: false From 05c17c833e1ae449a540b481814b203136ff343d Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 30 Jun 2015 15:50:53 -0700 Subject: [PATCH 17/61] fix python3 bug from itemsview --- doc/api.rst | 10 ++++++++++ xray/core/plotting.py | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index fa04d8c00aa..0628ec450da 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -264,6 +264,16 @@ Comparisons DataArray.identical DataArray.broadcast_equals +Plotting +-------- + +.. autosummary:: + :toctree: generated/ + + DataArray.plot + DataArray.plot_contourf + + .. _api.ufuncs: Universal functions diff --git a/xray/core/plotting.py b/xray/core/plotting.py index e8ece4cfdab..6b4dc5ebd26 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -13,7 +13,7 @@ def _plot_line(darray, *args, **kwargs): """ import matplotlib.pyplot as plt - xlabel, x = darray.indexes.items()[0] + xlabel, x = list(darray.indexes.items())[0] # Probably should be using the lower level matplotlib API plt.plot(x, darray.values, *args, **kwargs) @@ -31,8 +31,8 @@ def _plot_contourf(darray, *args, **kwargs): import matplotlib.pyplot as plt # x axis is by default the one corresponding to the 0th axis - xlabel, x = darray[0].indexes.items()[0] - ylabel, y = darray[:, 0].indexes.items()[0] + xlabel, x = list(darray[0].indexes.items())[0] + ylabel, y = list(darray[:, 0].indexes.items())[0] # TODO - revisit needing the transpose here plt.contourf(x, y, darray.values, *args, **kwargs) From 1994f9a3ace5f487c741bfd6b0bd3ca05bde8f50 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 30 Jun 2015 16:08:36 -0700 Subject: [PATCH 18/61] empty tests with todo for notes on pull requests --- xray/test/test_plotting.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index ee1182785d7..e20666c605a 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -26,8 +26,13 @@ def tearDown(self): # Remove all matplotlib figures plt.close('all') + @requires_matplotlib + def test_can_pass_in_axis(self): + # TODO + pass + -class TestSimpleDataArray(PlotTestCase): +class TestPlot(PlotTestCase): def setUp(self): d = [0, 1, 0, 2] @@ -47,7 +52,19 @@ def test_ylabel_is_data_name(self): self.assertEqual(ylabel, self.darray.name) -class Test2dDataArray(PlotTestCase): +class TestPlotLine(PlotTestCase): + + def setUp(self): + d = [0, 1, 0, 2] + self.darray = DataArray(d, coords={'period': range(len(d))}) + + @requires_matplotlib + def test_wrong_dims_raises_valueerror(self): + # TODO + pass + + +class TestPlotContourf(PlotTestCase): def setUp(self): self.darray = DataArray(np.random.randn(10, 15), From 2311507a66d6a88cf82b72370d318e100735cfa1 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 2 Jul 2015 16:53:49 -0700 Subject: [PATCH 19/61] add FacetGrid class --- xray/__init__.py | 1 + xray/core/plotting.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/xray/__init__.py b/xray/__init__.py index 65fa7719608..5ad5b232c8b 100644 --- a/xray/__init__.py +++ b/xray/__init__.py @@ -3,6 +3,7 @@ from .core.dataset import Dataset from .core.dataarray import DataArray from .core.options import set_options +from .core import plotting from .backends.api import open_dataset, open_mfdataset from .conventions import decode_cf diff --git a/xray/core/plotting.py b/xray/core/plotting.py index 6b4dc5ebd26..5aa6de2b65c 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -1,5 +1,5 @@ """ -Plotting functions are implemented here and then monkeypatched in to +Plotting functions are implemented here and also monkeypatched in to DataArray and DataSet classes """ @@ -7,6 +7,10 @@ # Decorators don't preserve the argument names +class FacetGrid(): + pass + + def _plot_line(darray, *args, **kwargs): """ Line plot From a76858320eb86b91aa508f053b16796e84705c0c Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Mon, 6 Jul 2015 11:48:49 -0700 Subject: [PATCH 20/61] starting on histogram --- doc/api.rst | 19 ++++++----- doc/plotting.rst | 10 ++++-- xray/core/dataarray.py | 7 ++-- xray/core/plotting.py | 69 +++++++++++++++++++++++++++++++++----- xray/test/test_plotting.py | 17 ++++++---- 5 files changed, 93 insertions(+), 29 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 0628ec450da..02987218d79 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -264,15 +264,6 @@ Comparisons DataArray.identical DataArray.broadcast_equals -Plotting --------- - -.. autosummary:: - :toctree: generated/ - - DataArray.plot - DataArray.plot_contourf - .. _api.ufuncs: @@ -393,3 +384,13 @@ arguments for the ``from_store`` and ``dump_to_store`` Dataset methods. backends.H5NetCDFStore backends.PydapDataStore backends.ScipyDataStore + + +Plotting +======== + +.. autosummary:: + :toctree: generated/ + + DataArray.plot + DataArray.plot_contourf diff --git a/doc/plotting.rst b/doc/plotting.rst index bab40658788..47b372aadd8 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -1,6 +1,11 @@ Plotting ======== +xray plotting functionality is a thin wrapper around the popular +`matplotlib `__ library. Hence matplotlib is a +dependency for plotting. The metadata is used to +add informative labels. + xray tries to create reasonable labeled plots based on metadata and the array dimensions. @@ -38,10 +43,11 @@ Xray uses the coordinate name to label the x axis. .. ipython:: python x = np.linspace(0, 2*np.pi) - a = xray.DataArray(np.sin(x), {'time': x}, name='sin(x)') + sinpts = xray.DataArray(np.sin(x), {'time': x}, name='sin(x)') @savefig plotting_example_sin.png - a.plot() + sinpts.plot() + Multivariate Normal Density ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/xray/core/dataarray.py b/xray/core/dataarray.py index 948caaabf2c..d9e05b9893d 100644 --- a/xray/core/dataarray.py +++ b/xray/core/dataarray.py @@ -13,7 +13,7 @@ from .common import AbstractArray, BaseDataObject from .coordinates import DataArrayCoordinates, Indexes from .dataset import Dataset -from .plotting import _plot_line, _plot_contourf +from .plotting import plot, plot_line, plot_contourf from .pycompat import iteritems, basestring, OrderedDict, zip from .utils import FrozenOrderedDict from .variable import as_variable, _as_compatible_data, Coordinate @@ -1077,8 +1077,9 @@ def func(self, other): # Add plotting methods -DataArray.plot = _plot_line -DataArray.plot_contourf = _plot_contourf +DataArray.plot = plot +DataArray.plot_line = plot_line +DataArray.plot_contourf = plot_contourf # priority most be higher than Variable to properly work with binary ufuncs ops.inject_all_ops_and_reduce_methods(DataArray, priority=60) diff --git a/xray/core/plotting.py b/xray/core/plotting.py index 5aa6de2b65c..6ed626a64ab 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -11,37 +11,90 @@ class FacetGrid(): pass -def _plot_line(darray, *args, **kwargs): +def plot(darray, ax=None, *args, **kwargs): """ - Line plot + Default plot of DataArray using matplotlib / pylab. + + Parameters + ---------- + darray : DataArray + Must be 1 dimensional + ax : matplotlib axes object + If not passed, uses plt.gca() + args, kwargs + Additional arguments to matplotlib + """ + return plot_line(darray, ax) + + +def plot_line(darray, ax=None, *args, **kwargs): + """ + Line plot of DataArray using matplotlib / pylab. + + Parameters + ---------- + darray : DataArray + Must be 1 dimensional + ax : matplotlib axes object + If not passed, uses plt.gca() """ import matplotlib.pyplot as plt + ndims = len(darray.dims) + if ndims != 1: + raise ValueError('Line plots are for 1 dimensional DataArrays. ' + 'Passed DataArray has {} dimensions'.format(ndims)) + + if not ax: + ax = plt.gca() + xlabel, x = list(darray.indexes.items())[0] - # Probably should be using the lower level matplotlib API - plt.plot(x, darray.values, *args, **kwargs) - ax = plt.gca() + ax.plot(x, darray.values, *args, **kwargs) + ax.set_xlabel(xlabel) ax.set_ylabel(darray.name) return ax -def _plot_contourf(darray, *args, **kwargs): +def plot_contourf(darray, ax=None, *args, **kwargs): """ Contour plot """ import matplotlib.pyplot as plt + if not ax: + ax = plt.gca() + # x axis is by default the one corresponding to the 0th axis xlabel, x = list(darray[0].indexes.items())[0] ylabel, y = list(darray[:, 0].indexes.items())[0] # TODO - revisit needing the transpose here - plt.contourf(x, y, darray.values, *args, **kwargs) - ax = plt.gca() + ax.contourf(x, y, darray.values, *args, **kwargs) ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) return ax + + +def plot_hist(darray, ax=None, *args, **kwargs): + """ + Histogram of DataArray using matplotlib / pylab. + + Parameters + ---------- + darray : DataArray + Can be + ax : matplotlib axes object + If not passed, uses plt.gca() + """ + import matplotlib.pyplot as plt + + if not ax: + ax = plt.gca() + + ax.hist(np.ravel(darray)) + + return ax diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index e20666c605a..a7161101a4f 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -26,11 +26,6 @@ def tearDown(self): # Remove all matplotlib figures plt.close('all') - @requires_matplotlib - def test_can_pass_in_axis(self): - # TODO - pass - class TestPlot(PlotTestCase): @@ -51,6 +46,13 @@ def test_ylabel_is_data_name(self): ylabel = plt.gca().get_ylabel() self.assertEqual(ylabel, self.darray.name) + @requires_matplotlib + def test_can_pass_in_axis(self): + # TODO - add this test to for other plotting methods + fig, axes = plt.subplots(ncols=2) + self.darray.plot(axes[0]) + self.assertTrue(axes[0].has_data()) + class TestPlotLine(PlotTestCase): @@ -60,8 +62,9 @@ def setUp(self): @requires_matplotlib def test_wrong_dims_raises_valueerror(self): - # TODO - pass + twodims = DataArray(np.arange(10).reshape(2, 5)) + with self.assertRaises(ValueError): + twodims.plot_line() class TestPlotContourf(PlotTestCase): From d0b27616962bd305805146f9db87ed67a119f57d Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Mon, 6 Jul 2015 15:54:41 -0700 Subject: [PATCH 21/61] imshow --- doc/plotting.rst | 43 ++++++++++++++++++++- xray/core/dataarray.py | 12 ++++-- xray/core/plotting.py | 76 +++++++++++++++++++++++++++++++++----- xray/test/test_plotting.py | 45 +++++++++++++++++----- 4 files changed, 152 insertions(+), 24 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 47b372aadd8..f35294f2089 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -45,10 +45,50 @@ Xray uses the coordinate name to label the x axis. x = np.linspace(0, 2*np.pi) sinpts = xray.DataArray(np.sin(x), {'time': x}, name='sin(x)') - @savefig plotting_example_sin.png + @savefig plotting_example_sin.png width=4in sinpts.plot() +Histogram +~~~~~~~~~ + +A histogram of the same data. + +.. ipython:: python + + @savefig plotting_example_hist.png width=4in + sinpts.plot_hist() + + +Contour Plot +~~~~~~~~~~~~ + +We can compute the distance from the origin for some two dimensional data. + +.. ipython:: python + + g = np.linspace(-3, 3) + xy = np.dstack(np.meshgrid(g, g)) + + distance = np.linalg.norm(xy, axis=2) + + distance = xray.DataArray(distance, {'x': g, 'y': g}) + + @savefig plotting_example_contour.png width=4in + distance.plot_contourf() + + +Image Plot +~~~~~~~~~~ + +This data can also be visualized using the `image` function. + +.. ipython:: python + + @savefig plotting_example_image.png width=4in + distance.plot_image() + + Multivariate Normal Density ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -71,6 +111,7 @@ evaluated on a square grid:: @savefig plotting_example_2dnormal.png plt.contourf(normal.x, normal.y, normal.data) + Rules ----- diff --git a/xray/core/dataarray.py b/xray/core/dataarray.py index d9e05b9893d..f1d9fe11cc9 100644 --- a/xray/core/dataarray.py +++ b/xray/core/dataarray.py @@ -9,11 +9,13 @@ from . import ops from . import utils from . import variable +from . import plotting from .alignment import align from .common import AbstractArray, BaseDataObject from .coordinates import DataArrayCoordinates, Indexes from .dataset import Dataset -from .plotting import plot, plot_line, plot_contourf +from .plotting import (plot, plot_line, plot_contourf, plot_hist, + plot_imshow) from .pycompat import iteritems, basestring, OrderedDict, zip from .utils import FrozenOrderedDict from .variable import as_variable, _as_compatible_data, Coordinate @@ -1077,9 +1079,11 @@ def func(self, other): # Add plotting methods -DataArray.plot = plot -DataArray.plot_line = plot_line -DataArray.plot_contourf = plot_contourf +DataArray.plot = plotting.plot +DataArray.plot_line = plotting.plot_line +DataArray.plot_contourf = plotting.plot_contourf +DataArray.plot_hist = plotting.plot_hist +DataArray.plot_imshow = plotting.plot_imshow # priority most be higher than Variable to properly work with binary ufuncs ops.inject_all_ops_and_reduce_methods(DataArray, priority=60) diff --git a/xray/core/plotting.py b/xray/core/plotting.py index 6ed626a64ab..b88f036a693 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -3,6 +3,9 @@ DataArray and DataSet classes """ +import numpy as np + + # TODO - Is there a better way to import matplotlib in the function? # Decorators don't preserve the argument names @@ -58,7 +61,49 @@ def plot_line(darray, ax=None, *args, **kwargs): return ax -def plot_contourf(darray, ax=None, *args, **kwargs): +def plot_imshow(darray, ax=None, add_colorbar=True, *args, **kwargs): + """ + Image plot of 2d DataArray using matplotlib / pylab. + + Parameters + ---------- + darray : DataArray + Must be 1 dimensional + ax : matplotlib axes object + If not passed, uses plt.gca() + add_colorbar : Boolean + Adds colorbar to axis + args, kwargs + Additional arguments to matplotlib + + """ + import matplotlib.pyplot as plt + + if not ax: + ax = plt.gca() + + # Seems strange that ylab comes first + ylab, xlab = darray.dims + + # Need these as Numpy arrays + x = darray[xlab].values + y = darray[ylab].values + z = darray.values + + ax.imshow(x, y, z, *args, **kwargs) + ax.set_xlabel(xlab) + ax.set_ylabel(ylab) + + if add_colorbar: + # Contains color mapping + mesh = ax.pcolormesh(x, y, z) + plt.colorbar(mesh, ax=ax) + + return ax + + +# TODO - Could refactor this to avoid duplicating plot_image logic above +def plot_contourf(darray, ax=None, add_colorbar=True, *args, **kwargs): """ Contour plot """ @@ -67,14 +112,22 @@ def plot_contourf(darray, ax=None, *args, **kwargs): if not ax: ax = plt.gca() - # x axis is by default the one corresponding to the 0th axis - xlabel, x = list(darray[0].indexes.items())[0] - ylabel, y = list(darray[:, 0].indexes.items())[0] + # Seems strange that ylab comes first + ylab, xlab = darray.dims - # TODO - revisit needing the transpose here - ax.contourf(x, y, darray.values, *args, **kwargs) - ax.set_xlabel(xlabel) - ax.set_ylabel(ylabel) + # Need these as Numpy arrays + x = darray[xlab].values + y = darray[ylab].values + z = darray.values + + ax.contourf(x, y, z, *args, **kwargs) + ax.set_xlabel(xlab) + ax.set_ylabel(ylab) + + if add_colorbar: + # Contains color mapping + mesh = ax.pcolormesh(x, y, z) + plt.colorbar(mesh, ax=ax) return ax @@ -82,11 +135,13 @@ def plot_contourf(darray, ax=None, *args, **kwargs): def plot_hist(darray, ax=None, *args, **kwargs): """ Histogram of DataArray using matplotlib / pylab. + + Uses numpy.ravel to first flatten the array. Parameters ---------- darray : DataArray - Can be + Can be any dimensions ax : matplotlib axes object If not passed, uses plt.gca() """ @@ -97,4 +152,7 @@ def plot_hist(darray, ax=None, *args, **kwargs): ax.hist(np.ravel(darray)) + ax.set_ylabel('Count') + ax.set_title('Histogram of {}'.format(darray.name)) + return ax diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index a7161101a4f..4f5ca77dea5 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -27,7 +27,7 @@ def tearDown(self): plt.close('all') -class TestPlot(PlotTestCase): +class TestPlot1D(PlotTestCase): def setUp(self): d = [0, 1, 0, 2] @@ -53,13 +53,6 @@ def test_can_pass_in_axis(self): self.darray.plot(axes[0]) self.assertTrue(axes[0].has_data()) - -class TestPlotLine(PlotTestCase): - - def setUp(self): - d = [0, 1, 0, 2] - self.darray = DataArray(d, coords={'period': range(len(d))}) - @requires_matplotlib def test_wrong_dims_raises_valueerror(self): twodims = DataArray(np.arange(10).reshape(2, 5)) @@ -67,16 +60,48 @@ def test_wrong_dims_raises_valueerror(self): twodims.plot_line() -class TestPlotContourf(PlotTestCase): +class TestPlot2D(PlotTestCase): def setUp(self): self.darray = DataArray(np.random.randn(10, 15), dims=['y', 'x']) @requires_matplotlib - def test_label_names(self): + def test_contour_label_names(self): self.darray.plot_contourf() xlabel = plt.gca().get_xlabel() ylabel = plt.gca().get_ylabel() self.assertEqual(xlabel, 'x') self.assertEqual(ylabel, 'y') + + @requires_matplotlib + def test_imshow_label_names(self): + self.darray.plot_imshow() + xlabel = plt.gca().get_xlabel() + ylabel = plt.gca().get_ylabel() + self.assertEqual(xlabel, 'x') + self.assertEqual(ylabel, 'y') + + +class TestPlotHist(PlotTestCase): + + def setUp(self): + self.darray = DataArray(np.random.randn(2, 3, 4)) + + @requires_matplotlib + def test_3d_array(self): + self.darray.plot_hist() + + @requires_matplotlib + def test_title_uses_name(self): + nm = 'randompoints' + self.darray.name = nm + self.darray.plot_hist() + title = plt.gca().get_title() + self.assertIn(nm, title) + + @requires_matplotlib + def test_ylabel_is_count(self): + self.darray.plot_hist() + ylabel = plt.gca().get_ylabel() + self.assertEqual(ylabel, 'Count') From 9b717af487db24b635e59e192bb2d0287b55171a Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Mon, 6 Jul 2015 17:27:26 -0700 Subject: [PATCH 22/61] default plot method --- doc/plotting.rst | 16 +++++++++++++-- xray/core/plotting.py | 40 +++++++++++++++++++++++++++----------- xray/test/test_plotting.py | 30 ++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 13 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index f35294f2089..1bdfa3e5d97 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -77,16 +77,17 @@ We can compute the distance from the origin for some two dimensional data. @savefig plotting_example_contour.png width=4in distance.plot_contourf() +TODO- This is the same plot as ``imshow``. Image Plot ~~~~~~~~~~ -This data can also be visualized using the `image` function. +This data can also be visualized using the ``imshow`` method. .. ipython:: python @savefig plotting_example_image.png width=4in - distance.plot_image() + distance.plot_imshow() Multivariate Normal Density @@ -117,3 +118,14 @@ Rules The following is a more complete description of how xray determines what and how to plot. + +The method :py:meth:`xray.DataArray.plot` dispatches to an appropriate +plotting function based on the dimensions of the ``DataArray``. + +=============== ====================================== +Dimensions Plotting function +--------------- -------------------------------------- +1 :py:meth:`xray.DataArray.plot_line` +2 :py:meth:`xray.DataArray.plot_imshow` +Anything else :py:meth:`xray.DataArray.plot_hist` +=============== ====================================== diff --git a/xray/core/plotting.py b/xray/core/plotting.py index b88f036a693..59408e3976f 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -8,6 +8,7 @@ # TODO - Is there a better way to import matplotlib in the function? # Decorators don't preserve the argument names +# But if all the plotting methods have same signature... class FacetGrid(): @@ -27,7 +28,15 @@ def plot(darray, ax=None, *args, **kwargs): args, kwargs Additional arguments to matplotlib """ - return plot_line(darray, ax) + defaults = {1: plot_line, 2: plot_image} + ndims = len(darray.dims) + + if ndims in defaults: + plotfunc = defaults[ndims] + else: + plotfunc = plot_hist + + return plotfunc(darray, ax, *args, **kwargs) def plot_line(darray, ax=None, *args, **kwargs): @@ -48,7 +57,7 @@ def plot_line(darray, ax=None, *args, **kwargs): raise ValueError('Line plots are for 1 dimensional DataArrays. ' 'Passed DataArray has {} dimensions'.format(ndims)) - if not ax: + if ax is None: ax = plt.gca() xlabel, x = list(darray.indexes.items())[0] @@ -79,23 +88,28 @@ def plot_imshow(darray, ax=None, add_colorbar=True, *args, **kwargs): """ import matplotlib.pyplot as plt - if not ax: + if ax is None: ax = plt.gca() # Seems strange that ylab comes first - ylab, xlab = darray.dims + try: + ylab, xlab = darray.dims + except ValueError: + raise ValueError('Line plots are for 2 dimensional DataArrays. ' + 'Passed DataArray has {} dimensions'.format(len(darray.dims))) - # Need these as Numpy arrays + # Need these as Numpy arrays for colormesh x = darray[xlab].values y = darray[ylab].values z = darray.values - ax.imshow(x, y, z, *args, **kwargs) + ax.imshow(z, extent=[x.min(), x.max(), y.min(), y.max()], + *args, **kwargs) ax.set_xlabel(xlab) ax.set_ylabel(ylab) if add_colorbar: - # Contains color mapping + # mesh contains color mapping mesh = ax.pcolormesh(x, y, z) plt.colorbar(mesh, ax=ax) @@ -109,13 +123,17 @@ def plot_contourf(darray, ax=None, add_colorbar=True, *args, **kwargs): """ import matplotlib.pyplot as plt - if not ax: + if ax is None: ax = plt.gca() # Seems strange that ylab comes first - ylab, xlab = darray.dims + try: + ylab, xlab = darray.dims + except ValueError: + raise ValueError('Contour plots are for 2 dimensional DataArrays. ' + 'Passed DataArray has {} dimensions'.format(len(darray.dims))) - # Need these as Numpy arrays + # Need these as Numpy arrays for colormesh x = darray[xlab].values y = darray[ylab].values z = darray.values @@ -147,7 +165,7 @@ def plot_hist(darray, ax=None, *args, **kwargs): """ import matplotlib.pyplot as plt - if not ax: + if ax is None: ax = plt.gca() ax.hist(np.ravel(darray)) diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index 4f5ca77dea5..c0d06257e8c 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -27,6 +27,25 @@ def tearDown(self): plt.close('all') +class TestPlot(PlotTestCase): + + def setUp(self): + d = np.arange(24).reshape(2, 3, 4) + self.darray = DataArray(d) + + @requires_matplotlib + def test3d(self): + self.darray[0, 0, :].plot() + + @requires_matplotlib + def test2d(self): + self.darray[0, :, :].plot() + + @requires_matplotlib + def test3d(self): + self.darray.plot() + + class TestPlot1D(PlotTestCase): def setUp(self): @@ -82,6 +101,17 @@ def test_imshow_label_names(self): self.assertEqual(xlabel, 'x') self.assertEqual(ylabel, 'y') + @requires_matplotlib + def test_too_few_dims_raises_valueerror(self): + with self.assertRaisesRegexp(ValueError, r'[Dd]im'): + self.darray[0, :].plot_imshow() + + @requires_matplotlib + def test_too_many_dims_raises_valueerror(self): + da = DataArray(np.random.randn(2, 3, 4)) + with self.assertRaisesRegexp(ValueError, r'[Dd]im'): + da.plot_imshow() + class TestPlotHist(PlotTestCase): From ad1cab68f235c42009533775a12b15a2302e0a1d Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 7 Jul 2015 10:31:51 -0700 Subject: [PATCH 23/61] cleaner plotting tests by assigning to axis object --- xray/core/plotting.py | 8 +++-- xray/test/test_plotting.py | 74 +++++++++++++++----------------------- 2 files changed, 34 insertions(+), 48 deletions(-) diff --git a/xray/core/plotting.py b/xray/core/plotting.py index 59408e3976f..a8f353255f4 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -28,7 +28,7 @@ def plot(darray, ax=None, *args, **kwargs): args, kwargs Additional arguments to matplotlib """ - defaults = {1: plot_line, 2: plot_image} + defaults = {1: plot_line, 2: plot_imshow} ndims = len(darray.dims) if ndims in defaults: @@ -168,9 +168,11 @@ def plot_hist(darray, ax=None, *args, **kwargs): if ax is None: ax = plt.gca() - ax.hist(np.ravel(darray)) + ax.hist(np.ravel(darray), *args, **kwargs) ax.set_ylabel('Count') - ax.set_title('Histogram of {}'.format(darray.name)) + + if darray.name is not None: + ax.set_title('Histogram of {}'.format(darray.name)) return ax diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index c0d06257e8c..ad9b5940447 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -16,10 +16,8 @@ except ImportError: pass -# TODO - Every test in this file requires matplotlib -# Hence it's redundant to have to use the decorator on every test -# How to refactor? +@requires_matplotlib class PlotTestCase(TestCase): def tearDown(self): @@ -33,15 +31,12 @@ def setUp(self): d = np.arange(24).reshape(2, 3, 4) self.darray = DataArray(d) - @requires_matplotlib - def test3d(self): + def test1d(self): self.darray[0, 0, :].plot() - @requires_matplotlib def test2d(self): self.darray[0, :, :].plot() - @requires_matplotlib def test3d(self): self.darray.plot() @@ -52,27 +47,21 @@ def setUp(self): d = [0, 1, 0, 2] self.darray = DataArray(d, coords={'period': range(len(d))}) - @requires_matplotlib def test_xlabel_is_index_name(self): - self.darray.plot() - xlabel = plt.gca().get_xlabel() - self.assertEqual(xlabel, 'period') + ax = self.darray.plot() + self.assertEqual('period', ax.get_xlabel()) - @requires_matplotlib def test_ylabel_is_data_name(self): self.darray.name = 'temperature' - self.darray.plot() - ylabel = plt.gca().get_ylabel() - self.assertEqual(ylabel, self.darray.name) + ax = self.darray.plot() + self.assertEqual(self.darray.name, ax.get_ylabel()) - @requires_matplotlib def test_can_pass_in_axis(self): # TODO - add this test to for other plotting methods fig, axes = plt.subplots(ncols=2) self.darray.plot(axes[0]) self.assertTrue(axes[0].has_data()) - @requires_matplotlib def test_wrong_dims_raises_valueerror(self): twodims = DataArray(np.arange(10).reshape(2, 5)) with self.assertRaises(ValueError): @@ -85,29 +74,21 @@ def setUp(self): self.darray = DataArray(np.random.randn(10, 15), dims=['y', 'x']) - @requires_matplotlib def test_contour_label_names(self): - self.darray.plot_contourf() - xlabel = plt.gca().get_xlabel() - ylabel = plt.gca().get_ylabel() - self.assertEqual(xlabel, 'x') - self.assertEqual(ylabel, 'y') + ax = self.darray.plot_contourf() + self.assertEqual('x', ax.get_xlabel()) + self.assertEqual('y', ax.get_ylabel()) - @requires_matplotlib def test_imshow_label_names(self): - self.darray.plot_imshow() - xlabel = plt.gca().get_xlabel() - ylabel = plt.gca().get_ylabel() - self.assertEqual(xlabel, 'x') - self.assertEqual(ylabel, 'y') - - @requires_matplotlib - def test_too_few_dims_raises_valueerror(self): + ax = self.darray.plot_imshow() + self.assertEqual('x', ax.get_xlabel()) + self.assertEqual('y', ax.get_ylabel()) + + def test_1d_raises_valueerror(self): with self.assertRaisesRegexp(ValueError, r'[Dd]im'): self.darray[0, :].plot_imshow() - @requires_matplotlib - def test_too_many_dims_raises_valueerror(self): + def test_3d_raises_valueerror(self): da = DataArray(np.random.randn(2, 3, 4)) with self.assertRaisesRegexp(ValueError, r'[Dd]im'): da.plot_imshow() @@ -118,20 +99,23 @@ class TestPlotHist(PlotTestCase): def setUp(self): self.darray = DataArray(np.random.randn(2, 3, 4)) - @requires_matplotlib def test_3d_array(self): self.darray.plot_hist() - @requires_matplotlib + def test_title_no_name(self): + ax = self.darray.plot_hist() + self.assertEqual('', ax.get_title()) + def test_title_uses_name(self): - nm = 'randompoints' - self.darray.name = nm - self.darray.plot_hist() - title = plt.gca().get_title() - self.assertIn(nm, title) + self.darray.name = 'randompoints' + ax = self.darray.plot_hist() + self.assertIn(self.darray.name, ax.get_title()) - @requires_matplotlib def test_ylabel_is_count(self): - self.darray.plot_hist() - ylabel = plt.gca().get_ylabel() - self.assertEqual(ylabel, 'Count') + ax = self.darray.plot_hist() + self.assertEqual('Count', ax.get_ylabel()) + + def test_can_pass_in_kwargs(self): + nbins = 5 + ax = self.darray.plot_hist(bins=nbins) + self.assertEqual(nbins, len(ax.patches)) From ad4ec3185b9b166be3948ba03107c2da2f1f0286 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 7 Jul 2015 11:46:58 -0700 Subject: [PATCH 24/61] reorganizing plotting docs --- doc/plotting.rst | 95 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 27 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 1bdfa3e5d97..45c253be088 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -1,10 +1,17 @@ Plotting ======== -xray plotting functionality is a thin wrapper around the popular -`matplotlib `__ library. Hence matplotlib is a -dependency for plotting. The metadata is used to -add informative labels. +Introduction +------------ + +Xray plotting functionality is a thin wrapper around the popular +`matplotlib `__ library. +The metadata from :py:class:`xray.DataArray` objects are used to add +informative labels. +We copy matplotlib syntax and function names as much as possible. + +Hence matplotlib is a +dependency for plotting. xray tries to create reasonable labeled plots based on metadata and the array dimensions. @@ -15,9 +22,6 @@ So don't be scared if you see some ``ValueError``'s when trying to plot, it just means you may need to get the data into a form where plotting is more natural. -Examples --------- - To begin, import numpy, pandas and xray: .. ipython:: python @@ -33,9 +37,8 @@ The following line is not necessary, but it makes for a nice style. plt.style.use('ggplot') - -Sin Function -~~~~~~~~~~~~ +1 Dimension +----------- Here is a simple example of plotting. Xray uses the coordinate name to label the x axis. @@ -43,7 +46,7 @@ Xray uses the coordinate name to label the x axis. .. ipython:: python x = np.linspace(0, 2*np.pi) - sinpts = xray.DataArray(np.sin(x), {'time': x}, name='sin(x)') + sinpts = xray.DataArray(np.sin(x), {'t': x}, name='sin(t)') @savefig plotting_example_sin.png width=4in sinpts.plot() @@ -59,36 +62,74 @@ A histogram of the same data. @savefig plotting_example_hist.png width=4in sinpts.plot_hist() +Additional arguments are passed directly to ``matplotlib.pyplot.hist``, +which handles the plotting. -Contour Plot -~~~~~~~~~~~~ +.. ipython:: python + + @savefig plotting_example_hist2.png width=4in + sinpts.plot_hist(bins=3) -We can compute the distance from the origin for some two dimensional data. +2 Dimensions +------------ + +For these examples we generate two dimensional data by computing the distance +from a 2d grid point to the origin .. ipython:: python - g = np.linspace(-3, 3) - xy = np.dstack(np.meshgrid(g, g)) + x = np.linspace(-5, 10, num=6) + y = np.logspace(0, 1.2, num=7) + xy = np.dstack(np.meshgrid(x, y)) distance = np.linalg.norm(xy, axis=2) - distance = xray.DataArray(distance, {'x': g, 'y': g}) + distance = xray.DataArray(distance, {'x': x, 'y': y}) + distance - @savefig plotting_example_contour.png width=4in - distance.plot_contourf() - -TODO- This is the same plot as ``imshow``. +The default :py:meth:`xray.DataArray.plot` sees that the data is 2 dimenstional +and calls :py:meth:`xray.DataArray.plot_imshow`. This was chosen as a +default +since it does not perform any smoothing or interpolation; it just shows the +raw data. + +.. ipython:: python -Image Plot -~~~~~~~~~~ + @savefig plotting_example_2d.png width=4in + distance.plot() -This data can also be visualized using the ``imshow`` method. +The y grid points were generated from a log scale, so we can use matplotlib +to adjust the scale on y: .. ipython:: python - @savefig plotting_example_image.png width=4in - distance.plot_imshow() - + plt.yscale('log') + + @savefig plotting_example_2d3.png width=4in + distance.plot() + +Swap the variables plotted on vertical and horizontal axes by transposing the array. + +TODO: This is easy, but is it better to have an argument for which variable +should appear on x and y axis? + +.. ipython:: python + + @savefig plotting_example_2d2.png width=4in + distance.T.plot() + + +Contour Plot +~~~~~~~~~~~~ + +Visualization is + +.. ipython:: python + + @savefig plotting_example_contour.png width=4in + distance.plot_contourf() + +TODO- This is the same plot as ``imshow``. Multivariate Normal Density ~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 13b5a1bccef5367549bca5ca400621cdd05be406 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 7 Jul 2015 17:30:56 -0700 Subject: [PATCH 25/61] testing all plotting funcs take axes arg --- .gitignore | 4 +- doc/api.rst | 3 + doc/plotting.rst | 118 ++++++++++++++++--------------------- xray/core/dataarray.py | 3 + xray/core/plotting.py | 87 ++++++++++++++++++++------- xray/test/test_plotting.py | 34 ++++++++--- 6 files changed, 152 insertions(+), 97 deletions(-) diff --git a/.gitignore b/.gitignore index 5c75dccd9ac..b60dd7d07ed 100644 --- a/.gitignore +++ b/.gitignore @@ -34,10 +34,12 @@ nosetests.xml .project .pydevproject -# PyCharm +# PyCharm and Vim .idea +*.swp # xray specific doc/_build doc/generated +doc/_static/*.png xray/version.py diff --git a/doc/api.rst b/doc/api.rst index 02987218d79..baf2bbc2f26 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -394,3 +394,6 @@ Plotting DataArray.plot DataArray.plot_contourf + DataArray.plot_hist + DataArray.plot_imshow + DataArray.plot_line diff --git a/doc/plotting.rst b/doc/plotting.rst index 45c253be088..30c7e0ae992 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -6,28 +6,16 @@ Introduction Xray plotting functionality is a thin wrapper around the popular `matplotlib `__ library. -The metadata from :py:class:`xray.DataArray` objects are used to add -informative labels. -We copy matplotlib syntax and function names as much as possible. +Metadata from :py:class:`xray.DataArray` objects are used to add +informative labels. Matplotlib is required for plotting with xray. +Matplotlib syntax and function names were copied as much as possible, which +makes for an easy transition between the two. -Hence matplotlib is a -dependency for plotting. - -xray tries to create reasonable labeled plots based on metadata and the array -dimensions. - -But it's not always obvious what to plot. A wise man once said: -'In the face of ambiguity, refuse the temptation to guess.' -So don't be scared if you see some ``ValueError``'s when -trying to plot, it just means you may need to get the data into a form -where plotting is more natural. - -To begin, import numpy, pandas and xray: +Begin by importing the necessary modules: .. ipython:: python import numpy as np - import pandas as pd import xray import matplotlib.pyplot as plt @@ -37,41 +25,60 @@ The following line is not necessary, but it makes for a nice style. plt.style.use('ggplot') -1 Dimension ------------ +One Dimension +------------- -Here is a simple example of plotting. -Xray uses the coordinate name to label the x axis. +Here is a simple example of plotting. +Xray uses the coordinate name to label the x axis: .. ipython:: python - x = np.linspace(0, 2*np.pi) - sinpts = xray.DataArray(np.sin(x), {'t': x}, name='sin(t)') + t = np.linspace(0, 2*np.pi) + sinpts = xray.DataArray(np.sin(t), {'t': t}, name='sin(t)') @savefig plotting_example_sin.png width=4in sinpts.plot() +Additional Arguments +~~~~~~~~~~~~~~~~~~~~~ -Histogram -~~~~~~~~~ - -A histogram of the same data. +Additional arguments are passed directly to the matplotlib function which +does the work. For example, +for a plot with blue triangles marking the data points one can use a +matplotlib format string: .. ipython:: python - @savefig plotting_example_hist.png width=4in - sinpts.plot_hist() + @savefig plotting_example_sin2.png width=4in + sinpts.plot('b-^') + +Keyword arguments work the same way. -Additional arguments are passed directly to ``matplotlib.pyplot.hist``, -which handles the plotting. +Adding to Existing Axis +~~~~~~~~~~~~~~~~~~~~~~~ + +To add the plot to an existing axis pass in the axis as a keyword argument +``ax``. This works for all xray plotting methods. +In this example ``axes`` is a tuple consisting of the left and right +axes created by ``plt.subplots``. .. ipython:: python - @savefig plotting_example_hist2.png width=4in - sinpts.plot_hist(bins=3) + fig, axes = plt.subplots(ncols=2) -2 Dimensions ------------- + axes + + sinpts.plot(ax=axes[0]) + sinpts.plot_hist(ax=axes[1]) + + @savefig plotting_example_existing_axes.png width=6in + plt.show() + +Instead of using the default :py:meth:`xray.DataArray.plot` we see a +histogram created by :py:meth:`xray.DataArray.plot_hist`. + +Two Dimensions +-------------- For these examples we generate two dimensional data by computing the distance from a 2d grid point to the origin @@ -88,10 +95,7 @@ from a 2d grid point to the origin distance The default :py:meth:`xray.DataArray.plot` sees that the data is 2 dimenstional -and calls :py:meth:`xray.DataArray.plot_imshow`. This was chosen as a -default -since it does not perform any smoothing or interpolation; it just shows the -raw data. +and calls :py:meth:`xray.DataArray.plot_imshow`. .. ipython:: python @@ -131,37 +135,19 @@ Visualization is TODO- This is the same plot as ``imshow``. -Multivariate Normal Density -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Consider the density for a two dimensional normal distribution -evaluated on a square grid:: - - # TODO this requires scipy as a dependency for docs to build - - from scipy.stats import multivariate_normal - - g = np.linspace(-3, 3) - xy = np.dstack(np.meshgrid(g, g)) - - # 2d Normal distribution centered at 1, 0 - rv = multivariate_normal(mean=(1, 0)) - - normal = xray.DataArray(rv.pdf(xy), {'x': g, 'y': g}) - - # TODO- use xray method - @savefig plotting_example_2dnormal.png - plt.contourf(normal.x, normal.y, normal.data) +Details +------- +There are two ways to use the xray plotting functionality: -Rules ------ +1. Use the ``plot`` convenience methods of :py:class:`xray.DataArray` +2. Directly from the xray plotting submodule:: -The following is a more complete description of how xray determines what -and how to plot. + import xray.plotting as xplt -The method :py:meth:`xray.DataArray.plot` dispatches to an appropriate -plotting function based on the dimensions of the ``DataArray``. +The convenience method :py:meth:`xray.DataArray.plot` dispatches to an appropriate +plotting function based on the dimensions of the ``DataArray``. This table +describes what gets plotted: =============== ====================================== Dimensions Plotting function diff --git a/xray/core/dataarray.py b/xray/core/dataarray.py index f1d9fe11cc9..28d31afc87f 100644 --- a/xray/core/dataarray.py +++ b/xray/core/dataarray.py @@ -1078,6 +1078,9 @@ def func(self, other): # Add plotting methods +# Alternatively these could be added using a Mixin +# Wondering if it's better to only expose plot and plot_hist here, since +# those always work. DataArray.plot = plotting.plot DataArray.plot_line = plotting.plot_line diff --git a/xray/core/plotting.py b/xray/core/plotting.py index a8f353255f4..b0fcd1f94d6 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -7,6 +7,7 @@ # TODO - Is there a better way to import matplotlib in the function? +# Other piece of duplicated logic is the checking for axes. # Decorators don't preserve the argument names # But if all the plotting methods have same signature... @@ -15,16 +16,26 @@ class FacetGrid(): pass -def plot(darray, ax=None, *args, **kwargs): +def plot(darray, *args, **kwargs): """ Default plot of DataArray using matplotlib / pylab. + Calls a plotting function based on the dimensions of + the array: + + =============== ====================================== + Dimensions Plotting function + --------------- -------------------------------------- + 1 :py:meth:`xray.DataArray.plot_line` + 2 :py:meth:`xray.DataArray.plot_imshow` + Anything else :py:meth:`xray.DataArray.plot_hist` + =============== ====================================== + Parameters ---------- darray : DataArray - Must be 1 dimensional ax : matplotlib axes object - If not passed, uses plt.gca() + If not passed, uses the current axis args, kwargs Additional arguments to matplotlib """ @@ -36,19 +47,27 @@ def plot(darray, ax=None, *args, **kwargs): else: plotfunc = plot_hist - return plotfunc(darray, ax, *args, **kwargs) + return plotfunc(darray, *args, **kwargs) -def plot_line(darray, ax=None, *args, **kwargs): +def plot_line(darray, *args, **kwargs): """ - Line plot of DataArray using matplotlib / pylab. + Line plot of 1 dimensional darray index against values + + Wraps matplotlib.pyplot.plot Parameters ---------- darray : DataArray Must be 1 dimensional ax : matplotlib axes object - If not passed, uses plt.gca() + If not passed, uses the current axis + args, kwargs + Additional arguments to matplotlib.pyplot.plot + + Examples + -------- + """ import matplotlib.pyplot as plt @@ -57,7 +76,10 @@ def plot_line(darray, ax=None, *args, **kwargs): raise ValueError('Line plots are for 1 dimensional DataArrays. ' 'Passed DataArray has {} dimensions'.format(ndims)) - if ax is None: + # Was an axis passed in? + try: + ax = kwargs.pop('ax') + except KeyError: ax = plt.gca() xlabel, x = list(darray.indexes.items())[0] @@ -70,25 +92,33 @@ def plot_line(darray, ax=None, *args, **kwargs): return ax -def plot_imshow(darray, ax=None, add_colorbar=True, *args, **kwargs): +def plot_imshow(darray, add_colorbar=True, *args, **kwargs): """ Image plot of 2d DataArray using matplotlib / pylab. + Wraps matplotlib.pyplot.imshow + Parameters ---------- darray : DataArray - Must be 1 dimensional + Must be 2 dimensional ax : matplotlib axes object - If not passed, uses plt.gca() + If not passed, uses the current axis + args, kwargs + Additional arguments to matplotlib.pyplot.imshow add_colorbar : Boolean Adds colorbar to axis - args, kwargs - Additional arguments to matplotlib + + Examples + -------- """ import matplotlib.pyplot as plt - if ax is None: + # Was an axis passed in? + try: + ax = kwargs.pop('ax') + except KeyError: ax = plt.gca() # Seems strange that ylab comes first @@ -117,13 +147,16 @@ def plot_imshow(darray, ax=None, add_colorbar=True, *args, **kwargs): # TODO - Could refactor this to avoid duplicating plot_image logic above -def plot_contourf(darray, ax=None, add_colorbar=True, *args, **kwargs): +def plot_contourf(darray, add_colorbar=True, *args, **kwargs): """ Contour plot """ import matplotlib.pyplot as plt - if ax is None: + # Was an axis passed in? + try: + ax = kwargs.pop('ax') + except KeyError: ax = plt.gca() # Seems strange that ylab comes first @@ -150,22 +183,32 @@ def plot_contourf(darray, ax=None, add_colorbar=True, *args, **kwargs): return ax -def plot_hist(darray, ax=None, *args, **kwargs): +def plot_hist(darray, *args, **kwargs): """ Histogram of DataArray using matplotlib / pylab. - - Uses numpy.ravel to first flatten the array. + Plots N dimensional arrays by first flattening the array. + + Wraps matplotlib.pyplot.hist Parameters ---------- darray : DataArray - Can be any dimensions + Must be 2 dimensional ax : matplotlib axes object - If not passed, uses plt.gca() + If not passed, uses the current axis + args, kwargs + Additional arguments to matplotlib.pyplot.imshow + + Examples + -------- + """ import matplotlib.pyplot as plt - if ax is None: + # Was an axis passed in? + try: + ax = kwargs.pop('ax') + except KeyError: ax = plt.gca() ax.hist(np.ravel(darray), *args, **kwargs) diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index ad9b5940447..8c3dc205e7d 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -1,5 +1,3 @@ -import sys - import numpy as np import pandas as pd @@ -24,6 +22,11 @@ def tearDown(self): # Remove all matplotlib figures plt.close('all') + def pass_in_axis(self, plotfunc): + fig, axes = plt.subplots(ncols=2) + plotfunc(ax=axes[0]) + self.assertTrue(axes[0].has_data()) + class TestPlot(PlotTestCase): @@ -40,6 +43,12 @@ def test2d(self): def test3d(self): self.darray.plot() + def test_format_string(self): + self.darray[0, 0, :].plot('ro') + + def test_can_pass_in_axis(self): + self.pass_in_axis(self.darray.plot) + class TestPlot1D(PlotTestCase): @@ -56,17 +65,14 @@ def test_ylabel_is_data_name(self): ax = self.darray.plot() self.assertEqual(self.darray.name, ax.get_ylabel()) - def test_can_pass_in_axis(self): - # TODO - add this test to for other plotting methods - fig, axes = plt.subplots(ncols=2) - self.darray.plot(axes[0]) - self.assertTrue(axes[0].has_data()) - def test_wrong_dims_raises_valueerror(self): twodims = DataArray(np.arange(10).reshape(2, 5)) with self.assertRaises(ValueError): twodims.plot_line() + def test_can_pass_in_axis(self): + self.pass_in_axis(self.darray.plot_line) + class TestPlot2D(PlotTestCase): @@ -93,6 +99,10 @@ def test_3d_raises_valueerror(self): with self.assertRaisesRegexp(ValueError, r'[Dd]im'): da.plot_imshow() + def test_can_pass_in_axis(self): + self.pass_in_axis(self.darray.plot_imshow) + self.pass_in_axis(self.darray.plot_contourf) + class TestPlotHist(PlotTestCase): @@ -119,3 +129,11 @@ def test_can_pass_in_kwargs(self): nbins = 5 ax = self.darray.plot_hist(bins=nbins) self.assertEqual(nbins, len(ax.patches)) + + def test_can_pass_in_positional_args(self): + nbins = 5 + ax = self.darray.plot_hist(nbins) + self.assertEqual(nbins, len(ax.patches)) + + def test_can_pass_in_axis(self): + self.pass_in_axis(self.darray.plot_hist) From 62981b7be1f3be91280a7784f795605788c278ab Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Wed, 8 Jul 2015 10:45:40 -0700 Subject: [PATCH 26/61] add links to other plotting libs --- doc/installing.rst | 6 ++++++ doc/plotting.rst | 37 ++++++++++++++++++++++++++++++------- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/doc/installing.rst b/doc/installing.rst index cfb6eacc51e..e3ad73a8e8e 100644 --- a/doc/installing.rst +++ b/doc/installing.rst @@ -34,6 +34,12 @@ For parallel computing - `dask.array `__: required for :ref:`dask`. +For plotting +~~~~~~~~~~~~ + +- `matplotlib `__: required for :ref:`plotting`. + + Instructions ------------ diff --git a/doc/plotting.rst b/doc/plotting.rst index 30c7e0ae992..9b6b464f726 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -4,13 +4,27 @@ Plotting Introduction ------------ +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. + Xray plotting functionality is a thin wrapper around the popular `matplotlib `__ library. -Metadata from :py:class:`xray.DataArray` objects are used to add -informative labels. Matplotlib is required for plotting with xray. Matplotlib syntax and function names were copied as much as possible, which makes for an easy transition between the two. +For more specialized plotting applications consider the following packages: + +- `Seaborn `__: "provides + a high-level interface for drawing attractive statistical graphics." + Integrates well with pandas. + +- `Cartopy `__: provides cartographic + tools + +Imports +~~~~~~~ + Begin by importing the necessary modules: .. ipython:: python @@ -43,16 +57,25 @@ Additional Arguments ~~~~~~~~~~~~~~~~~~~~~ Additional arguments are passed directly to the matplotlib function which -does the work. For example, -for a plot with blue triangles marking the data points one can use a -matplotlib format string: +does the work. +For example, for a 1 dimensional DataArray, :py:meth:`xray.DataArray.plot_line` calls ``plt.plot``, +passing in the index and the array values as x and y, respectively. +So to make a line plot with blue triangles a `matplotlib format string +`__ +can be used: .. ipython:: python @savefig plotting_example_sin2.png width=4in - sinpts.plot('b-^') + sinpts.plot_line('b-^') + +Keyword arguments work the same way: + +.. ipython:: python + + @savefig plotting_example_sin3.png width=4in + sinpts.plot_line(color='purple', marker='o') -Keyword arguments work the same way. Adding to Existing Axis ~~~~~~~~~~~~~~~~~~~~~~~ From 573723f50fb884d1aee8073cfa1057aa3ae11fc6 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Wed, 8 Jul 2015 15:24:15 -0700 Subject: [PATCH 27/61] only plot image plots when the data is coordinates are uniform and sorted --- doc/plotting.rst | 89 ++++++++++++++++++++++++++++---------- xray/core/plotting.py | 18 +++++--- xray/core/utils.py | 5 +++ xray/test/test_plotting.py | 4 ++ xray/test/test_utils.py | 9 ++++ 5 files changed, 96 insertions(+), 29 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 9b6b464f726..175c5931d9f 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -42,6 +42,9 @@ The following line is not necessary, but it makes for a nice style. One Dimension ------------- +Simple Example +~~~~~~~~~~~~~~ + Here is a simple example of plotting. Xray uses the coordinate name to label the x axis: @@ -100,16 +103,54 @@ axes created by ``plt.subplots``. Instead of using the default :py:meth:`xray.DataArray.plot` we see a histogram created by :py:meth:`xray.DataArray.plot_hist`. +Time Series +~~~~~~~~~~~ + +The index may be a time series. + +.. ipython:: python + + import pandas as pd + npts = 50 + time = pd.date_range('2015-01-01', periods=npts) + noise = xray.DataArray(np.random.randn(npts), {'time': time}) + + @savefig plotting_example_time.png width=6in + noise.plot_line() + + Two Dimensions -------------- -For these examples we generate two dimensional data by computing the distance -from a 2d grid point to the origin +Simple Example +~~~~~~~~~~~~~~ + +The default :py:meth:`xray.DataArray.plot` sees that the data is 2 dimensional +and calls :py:meth:`xray.DataArray.plot_imshow`. + +.. ipython:: python + + a = np.zeros((5, 3)) + a[0, 0] = 1 + xa = xray.DataArray(a) + xa + + @savefig plotting_example_2d.png width=4in + xa.plot() + +The top left pixel is 1, and the others are 0. + +Simulated Data +~~~~~~~~~~~~~~ + +For further examples we generate two dimensional data by computing the distance +from a 2d grid point to the origin. +It's not necessary for the grid to be evenly spaced. .. ipython:: python x = np.linspace(-5, 10, num=6) - y = np.logspace(0, 1.2, num=7) + y = np.logspace(1.2, 0, num=7) xy = np.dstack(np.meshgrid(x, y)) distance = np.linalg.norm(xy, axis=2) @@ -117,35 +158,37 @@ from a 2d grid point to the origin distance = xray.DataArray(distance, {'x': x, 'y': y}) distance -The default :py:meth:`xray.DataArray.plot` sees that the data is 2 dimenstional -and calls :py:meth:`xray.DataArray.plot_imshow`. +Note the coordinate ``y`` here is decreasing. +This makes the axes of the image plot in the expected way. -.. ipython:: python +# TODO- Edge case- what if the coordinates are not sorted? Is this +possible? What if coordinates increasing? - @savefig plotting_example_2d.png width=4in - distance.plot() +Calling Matplotlib +~~~~~~~~~~~~~~~~~~ -The y grid points were generated from a log scale, so we can use matplotlib +Use matplotlib to adjust plot parameters. For example, the +y grid points were generated from a log scale, so we can use matplotlib to adjust the scale on y: .. ipython:: python - plt.yscale('log') + #plt.yscale('log') @savefig plotting_example_2d3.png width=4in distance.plot() -Swap the variables plotted on vertical and horizontal axes by transposing the array. +Changing Axes +~~~~~~~~~~~~~ -TODO: This is easy, but is it better to have an argument for which variable -should appear on x and y axis? +Two dimensional plotting in xray uses the +Swap the variables plotted on vertical and horizontal axes by transposing the array. .. ipython:: python @savefig plotting_example_2d2.png width=4in distance.T.plot() - Contour Plot ~~~~~~~~~~~~ @@ -169,13 +212,15 @@ There are two ways to use the xray plotting functionality: import xray.plotting as xplt The convenience method :py:meth:`xray.DataArray.plot` dispatches to an appropriate -plotting function based on the dimensions of the ``DataArray``. This table +plotting function based on the dimensions of the ``DataArray`` and whether +the coordinates are sorted and uniformly spaced. This table describes what gets plotted: -=============== ====================================== -Dimensions Plotting function ---------------- -------------------------------------- -1 :py:meth:`xray.DataArray.plot_line` -2 :py:meth:`xray.DataArray.plot_imshow` -Anything else :py:meth:`xray.DataArray.plot_hist` -=============== ====================================== +=============== =========== =========================== +Dimensions Coordinates Plotting function +--------------- ----------- --------------------------- +1 :py:meth:`xray.DataArray.plot_line` +2 Uniform :py:meth:`xray.DataArray.plot_imshow` +2 Irregular :py:meth:`xray.DataArray.plot_contourf` +Anything else :py:meth:`xray.DataArray.plot_hist` +=============== =========== =========================== diff --git a/xray/core/plotting.py b/xray/core/plotting.py index b0fcd1f94d6..f0c9ede2eec 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -87,7 +87,9 @@ def plot_line(darray, *args, **kwargs): ax.plot(x, darray.values, *args, **kwargs) ax.set_xlabel(xlabel) - ax.set_ylabel(darray.name) + + if darray.name is not None: + ax.set_ylabel(darray.name) return ax @@ -128,20 +130,22 @@ def plot_imshow(darray, add_colorbar=True, *args, **kwargs): raise ValueError('Line plots are for 2 dimensional DataArrays. ' 'Passed DataArray has {} dimensions'.format(len(darray.dims))) - # Need these as Numpy arrays for colormesh - x = darray[xlab].values - y = darray[ylab].values - z = darray.values + x = darray[xlab] + y = darray[ylab] + + image = ax.imshow(darray, extent=[x.min(), x.max(), y.min(), y.max()], + interpolation='nearest', *args, **kwargs) - ax.imshow(z, extent=[x.min(), x.max(), y.min(), y.max()], - *args, **kwargs) ax.set_xlabel(xlab) ax.set_ylabel(ylab) + plt.colorbar(image, ax=ax) + ''' if add_colorbar: # mesh contains color mapping mesh = ax.pcolormesh(x, y, z) plt.colorbar(mesh, ax=ax) + ''' return ax diff --git a/xray/core/utils.py b/xray/core/utils.py index 67e4f445a39..7b72893be67 100644 --- a/xray/core/utils.py +++ b/xray/core/utils.py @@ -391,3 +391,8 @@ def close_on_error(f): def is_remote_uri(path): return bool(re.search('^https?\://', path)) + +def is_uniform_spaced(arr): + """Return True if values of an array are uniformly spaced and sorted + """ + pass diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index 8c3dc205e7d..916d837ad22 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -60,6 +60,10 @@ def test_xlabel_is_index_name(self): ax = self.darray.plot() self.assertEqual('period', ax.get_xlabel()) + def test_no_label_name_on_y_axis(self): + ax = self.darray.plot() + self.assertEqual('', ax.get_ylabel()) + def test_ylabel_is_data_name(self): self.darray.name = 'temperature' ax = self.darray.plot() diff --git a/xray/test/test_utils.py b/xray/test/test_utils.py index a86cf914c28..02c2a915524 100644 --- a/xray/test/test_utils.py +++ b/xray/test/test_utils.py @@ -117,3 +117,12 @@ def test_chain_map(self): self.assertEqual(m['x'], 100) self.assertEqual(m.maps[0]['x'], 100) self.assertItemsEqual(['x', 'y', 'z'], m) + + +class Test_is_uniform_and_sorted(TestCase): + + def test_range_sorted(self): + self.assertTrue(utils.is_uniform_spaced(np.arange(5))) + + def test_not_sorted(self): + self.assertFalse(utils.is_uniform_spaced([4, 1, 89])) From 3f135238627c7529a1764a75ed41231856070bc2 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 9 Jul 2015 09:29:37 -0700 Subject: [PATCH 28/61] add is_uniform_spaced function to utils --- xray/core/utils.py | 16 +++++++++++++--- xray/test/test_utils.py | 18 +++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/xray/core/utils.py b/xray/core/utils.py index 7b72893be67..3e2becf13ac 100644 --- a/xray/core/utils.py +++ b/xray/core/utils.py @@ -392,7 +392,17 @@ def close_on_error(f): def is_remote_uri(path): return bool(re.search('^https?\://', path)) -def is_uniform_spaced(arr): - """Return True if values of an array are uniformly spaced and sorted + +def is_uniform_spaced(arr, **kwargs): + """Return True if values of an array are uniformly spaced and sorted. + + >>> is_uniform_spaced(range(5)) + True + >>> is_uniform_spaced([-4, 0, 100]) + False + + kwargs are additional arguments to ``np.isclose`` """ - pass + arr = np.array(arr) + diffs = np.diff(arr) + return np.isclose(diffs.min(), diffs.max(), **kwargs) diff --git a/xray/test/test_utils.py b/xray/test/test_utils.py index 02c2a915524..6b2d8f003cc 100644 --- a/xray/test/test_utils.py +++ b/xray/test/test_utils.py @@ -121,8 +121,20 @@ def test_chain_map(self): class Test_is_uniform_and_sorted(TestCase): - def test_range_sorted(self): + def test_sorted_uniform(self): self.assertTrue(utils.is_uniform_spaced(np.arange(5))) - def test_not_sorted(self): - self.assertFalse(utils.is_uniform_spaced([4, 1, 89])) + def test_sorted_not_uniform(self): + self.assertEqual(False, utils.is_uniform_spaced([-2, 1, 89])) + + def test_not_sorted_uniform(self): + self.assertEqual(False, utils.is_uniform_spaced([1, -1, 3])) + + def test_not_sorted_not_uniform(self): + self.assertEqual(False, utils.is_uniform_spaced([4, 1, 89])) + + def test_two_numbers(self): + self.assertTrue(utils.is_uniform_spaced([0, 1.7])) + + def test_relative_tolerance(self): + self.assertTrue(utils.is_uniform_spaced([0, 0.97, 2], rtol=0.1)) From 886452780597a749ee6e1302afb8b2f2ce996ecb Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 9 Jul 2015 11:49:04 -0700 Subject: [PATCH 29/61] add link to holoviews --- doc/plotting.rst | 51 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 175c5931d9f..3e0ac56004e 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -19,6 +19,10 @@ For more specialized plotting applications consider the following packages: a high-level interface for drawing attractive statistical graphics." Integrates well with pandas. +- `Holoviews `__: provides cartographic tools @@ -125,20 +129,53 @@ Two Dimensions Simple Example ~~~~~~~~~~~~~~ -The default :py:meth:`xray.DataArray.plot` sees that the data is 2 dimensional -and calls :py:meth:`xray.DataArray.plot_imshow`. +The default :py:meth:`xray.DataArray.plot` sees that the data is +2 dimensional. If the coordinates are uniformly spaced then it +calls :py:meth:`xray.DataArray.plot_imshow`. + +.. ipython:: python + + a = xray.DataArray(np.zeros((4, 3)), ('xaxis', 'yaxis')) + a[0, 0] = 1 + a + + @savefig plotting_example_2d_simple.png width=4in + a.plot() + +The top left pixel is 1, and the others are 0. This corresponds to the +printed array. It may seem unintuitive that +the the values on the y axis are decreasing with 0 on the top. This is because the +axis labels and ranges correspond to the values of the +coordinates. + +An `extended slice` ` +can be used to reverse the order of the rows, producing a +more conventional plot where the coordinates increase in the y axis. .. ipython:: python - a = np.zeros((5, 3)) + a = xray.DataArray(np.zeros((4, 3)), ('xaxis', 'yaxis')) a[0, 0] = 1 - xa = xray.DataArray(a) - xa + a + + @savefig plotting_example_2d_simple.png width=4in + a.plot() + + +Nonuniform Coordinates +~~~~~~~~~~~~~~~~~~~~~~ + +If the coordinates are not uniformly spaced then +:py:meth:`xray.DataArray.plot` produces a filled contour plot by calling +:py:meth:`xray.DataArray.plot_contourf`. + +.. ipython:: python + + xa.coords['dim_0'] = [0, 1, 4] - @savefig plotting_example_2d.png width=4in + @savefig plotting_example_2d_nonuniform.png width=4in xa.plot() -The top left pixel is 1, and the others are 0. Simulated Data ~~~~~~~~~~~~~~ From 3bf237538edb8b3bc7ba40f4ad6234250201e9c8 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 9 Jul 2015 13:26:09 -0700 Subject: [PATCH 30/61] plotting docs provide a nice spec now --- doc/plotting.rst | 24 ++++++++---------------- xray/test/test_plotting.py | 2 ++ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 3e0ac56004e..1dafd81ea56 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -37,12 +37,6 @@ Begin by importing the necessary modules: import xray import matplotlib.pyplot as plt -The following line is not necessary, but it makes for a nice style. - -.. ipython:: python - - plt.style.use('ggplot') - One Dimension ------------- @@ -110,12 +104,12 @@ histogram created by :py:meth:`xray.DataArray.plot_hist`. Time Series ~~~~~~~~~~~ -The index may be a time series. +The index may be a date. .. ipython:: python import pandas as pd - npts = 50 + npts = 20 time = pd.date_range('2015-01-01', periods=npts) noise = xray.DataArray(np.random.randn(npts), {'time': time}) @@ -135,7 +129,7 @@ calls :py:meth:`xray.DataArray.plot_imshow`. .. ipython:: python - a = xray.DataArray(np.zeros((4, 3)), ('xaxis', 'yaxis')) + a = xray.DataArray(np.zeros((4, 3)), dims=('y', 'x')) a[0, 0] = 1 a @@ -148,18 +142,16 @@ the the values on the y axis are decreasing with 0 on the top. This is because t axis labels and ranges correspond to the values of the coordinates. -An `extended slice` ` +An `extended slice ` __ can be used to reverse the order of the rows, producing a more conventional plot where the coordinates increase in the y axis. .. ipython:: python - a = xray.DataArray(np.zeros((4, 3)), ('xaxis', 'yaxis')) - a[0, 0] = 1 - a + a[::-1, :] @savefig plotting_example_2d_simple.png width=4in - a.plot() + a[::-1, :].plot() Nonuniform Coordinates @@ -171,10 +163,10 @@ If the coordinates are not uniformly spaced then .. ipython:: python - xa.coords['dim_0'] = [0, 1, 4] + a.coords['x'] = [0, 1, 4] @savefig plotting_example_2d_nonuniform.png width=4in - xa.plot() + a.plot() Simulated Data diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index 916d837ad22..59b24b03c47 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -37,6 +37,8 @@ def setUp(self): def test1d(self): self.darray[0, 0, :].plot() + # TODO - test for 2d dispatching to imshow versus contourf + # Can use mock for this def test2d(self): self.darray[0, :, :].plot() From 66b033217057a5d700296a9c3e0a305418bc2c31 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 9 Jul 2015 14:21:38 -0700 Subject: [PATCH 31/61] test for uniform and nonuniform 2d plots --- xray/test/test_plotting.py | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index 59b24b03c47..b6ce365cefa 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -6,9 +6,9 @@ from . import TestCase, requires_matplotlib try: - import matplotlib + import matplotlib as mpl # Using a different backend makes Travis CI work. - matplotlib.use('Agg') + mpl.use('Agg') # Order of imports is important here. import matplotlib.pyplot as plt except ImportError: @@ -27,6 +27,16 @@ def pass_in_axis(self, plotfunc): plotfunc(ax=axes[0]) self.assertTrue(axes[0].has_data()) + def imshow_called(self, plotfunc): + ax = plotfunc() + images = ax.findobj(mpl.image.AxesImage) + return len(images) > 0 + + def contourf_called(self, plotfunc): + ax = plotfunc() + paths = ax.findobj(mpl.collections.PathCollection) + return len(paths) > 0 + class TestPlot(PlotTestCase): @@ -37,10 +47,14 @@ def setUp(self): def test1d(self): self.darray[0, 0, :].plot() - # TODO - test for 2d dispatching to imshow versus contourf - # Can use mock for this - def test2d(self): - self.darray[0, :, :].plot() + def test2d_uniform_calls_imshow(self): + a = self.darray[0, :, :] + self.assertTrue(self.imshow_called(a.plot)) + + def test2d_nonuniform_calls_contourf(self): + a = self.darray[0, :, :] + a.coords['dim_1'] = [0, 10, 2] + self.assertTrue(self.contourf_called(a.plot)) def test3d(self): self.darray.plot() @@ -109,6 +123,16 @@ def test_can_pass_in_axis(self): self.pass_in_axis(self.darray.plot_imshow) self.pass_in_axis(self.darray.plot_contourf) + def test_imshow_called(self): + # Having both statements ensures the test works properly + self.assertFalse(self.imshow_called(self.darray.plot_contourf)) + self.assertTrue(self.imshow_called(self.darray.plot_imshow)) + + def test_contourf_called(self): + # Having both statements ensures the test works properly + self.assertFalse(self.contourf_called(self.darray.plot_imshow)) + self.assertTrue(self.contourf_called(self.darray.plot_contourf)) + class TestPlotHist(PlotTestCase): From 6f48a01715a8516264394a55883d687c764ac6ea Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 9 Jul 2015 15:22:27 -0700 Subject: [PATCH 32/61] remove positional args from most plot funcs --- doc/plotting.rst | 8 ++--- xray/core/plotting.py | 79 +++++++++++++++++++++---------------------- 2 files changed, 43 insertions(+), 44 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 1dafd81ea56..84b3648a82e 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -19,9 +19,9 @@ For more specialized plotting applications consider the following packages: a high-level interface for drawing attractive statistical graphics." Integrates well with pandas. -- `Holoviews `__: "Composable, declarative data structures for building even complex visualizations easily." - Also works for higher dimensional datasets. + Works for 2d datasets. - `Cartopy `__: provides cartographic tools @@ -137,12 +137,12 @@ calls :py:meth:`xray.DataArray.plot_imshow`. a.plot() The top left pixel is 1, and the others are 0. This corresponds to the -printed array. It may seem unintuitive that +printed array. It may seem strange that the the values on the y axis are decreasing with 0 on the top. This is because the axis labels and ranges correspond to the values of the coordinates. -An `extended slice ` __ +An `extended slice `__ can be used to reverse the order of the rows, producing a more conventional plot where the coordinates increase in the y axis. diff --git a/xray/core/plotting.py b/xray/core/plotting.py index f0c9ede2eec..ec1ac8f5785 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -3,8 +3,10 @@ DataArray and DataSet classes """ +import functools import numpy as np +from .utils import is_uniform_spaced # TODO - Is there a better way to import matplotlib in the function? # Other piece of duplicated logic is the checking for axes. @@ -16,7 +18,7 @@ class FacetGrid(): pass -def plot(darray, *args, **kwargs): +def plot(darray, ax=None, rtol=0.01, **kwargs): """ Default plot of DataArray using matplotlib / pylab. @@ -35,21 +37,31 @@ def plot(darray, *args, **kwargs): ---------- darray : DataArray ax : matplotlib axes object - If not passed, uses the current axis + If None, uses the current axis + rtol : relative tolerance + Relative tolerance used to determine if the indexes + are uniformly spaced args, kwargs Additional arguments to matplotlib """ - defaults = {1: plot_line, 2: plot_imshow} + kwargs['ax'] = ax ndims = len(darray.dims) - if ndims in defaults: - plotfunc = defaults[ndims] + if ndims == 1: + plotfunc = plot_line + elif ndims == 2: + if all(is_uniform_spaced(i, rtol=rtol) for i in darray.indexes.values()): + plotfunc = plot_imshow + else: + plotfunc = plot_contourf else: plotfunc = plot_hist return plotfunc(darray, *args, **kwargs) +# This function signature should not change so that it can pass format +# strings def plot_line(darray, *args, **kwargs): """ Line plot of 1 dimensional darray index against values @@ -94,7 +106,7 @@ def plot_line(darray, *args, **kwargs): return ax -def plot_imshow(darray, add_colorbar=True, *args, **kwargs): +def plot_imshow(darray, ax=None, add_colorbar=True, *args, **kwargs): """ Image plot of 2d DataArray using matplotlib / pylab. @@ -105,7 +117,7 @@ def plot_imshow(darray, add_colorbar=True, *args, **kwargs): darray : DataArray Must be 2 dimensional ax : matplotlib axes object - If not passed, uses the current axis + If None, uses the current axis args, kwargs Additional arguments to matplotlib.pyplot.imshow add_colorbar : Boolean @@ -117,17 +129,14 @@ def plot_imshow(darray, add_colorbar=True, *args, **kwargs): """ import matplotlib.pyplot as plt - # Was an axis passed in? - try: - ax = kwargs.pop('ax') - except KeyError: + if ax is None: ax = plt.gca() # Seems strange that ylab comes first try: ylab, xlab = darray.dims except ValueError: - raise ValueError('Line plots are for 2 dimensional DataArrays. ' + raise ValueError('Image plots are for 2 dimensional DataArrays. ' 'Passed DataArray has {} dimensions'.format(len(darray.dims))) x = darray[xlab] @@ -140,54 +149,47 @@ def plot_imshow(darray, add_colorbar=True, *args, **kwargs): ax.set_ylabel(ylab) plt.colorbar(image, ax=ax) - ''' - if add_colorbar: - # mesh contains color mapping - mesh = ax.pcolormesh(x, y, z) - plt.colorbar(mesh, ax=ax) - ''' return ax # TODO - Could refactor this to avoid duplicating plot_image logic above -def plot_contourf(darray, add_colorbar=True, *args, **kwargs): +def plot_contourf(darray, ax=None, add_colorbar=True, **kwargs): """ Contour plot """ import matplotlib.pyplot as plt - # Was an axis passed in? - try: - ax = kwargs.pop('ax') - except KeyError: + if ax is None: ax = plt.gca() - # Seems strange that ylab comes first try: ylab, xlab = darray.dims except ValueError: raise ValueError('Contour plots are for 2 dimensional DataArrays. ' 'Passed DataArray has {} dimensions'.format(len(darray.dims))) - # Need these as Numpy arrays for colormesh - x = darray[xlab].values - y = darray[ylab].values - z = darray.values + # Need arrays here? + #x = darray[xlab].values + #y = darray[ylab].values + #z = darray.values + + #ax.contourf(x, y, z, *args, **kwargs) + + x = darray[xlab] + y = darray[ylab] - ax.contourf(x, y, z, *args, **kwargs) + ax.contourf(x, y, darray, **kwargs) ax.set_xlabel(xlab) ax.set_ylabel(ylab) if add_colorbar: - # Contains color mapping - mesh = ax.pcolormesh(x, y, z) - plt.colorbar(mesh, ax=ax) + ax.colorbar() return ax -def plot_hist(darray, *args, **kwargs): +def plot_hist(darray, ax=None, **kwargs): """ Histogram of DataArray using matplotlib / pylab. Plots N dimensional arrays by first flattening the array. @@ -200,8 +202,8 @@ def plot_hist(darray, *args, **kwargs): Must be 2 dimensional ax : matplotlib axes object If not passed, uses the current axis - args, kwargs - Additional arguments to matplotlib.pyplot.imshow + kwargs + Additional arguments to matplotlib.pyplot.hist Examples -------- @@ -209,13 +211,10 @@ def plot_hist(darray, *args, **kwargs): """ import matplotlib.pyplot as plt - # Was an axis passed in? - try: - ax = kwargs.pop('ax') - except KeyError: + if ax is None: ax = plt.gca() - ax.hist(np.ravel(darray), *args, **kwargs) + ax.hist(np.ravel(darray), **kwargs) ax.set_ylabel('Count') From d0192dac5be66600031c6d354aea2fab0e7cad22 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 9 Jul 2015 15:34:01 -0700 Subject: [PATCH 33/61] colorbar is working by passing in mapping --- xray/core/plotting.py | 10 +++++----- xray/test/test_plotting.py | 5 ----- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/xray/core/plotting.py b/xray/core/plotting.py index ec1ac8f5785..daec2dd1b52 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -41,8 +41,8 @@ def plot(darray, ax=None, rtol=0.01, **kwargs): rtol : relative tolerance Relative tolerance used to determine if the indexes are uniformly spaced - args, kwargs - Additional arguments to matplotlib + kwargs + Additional keyword arguments to matplotlib """ kwargs['ax'] = ax ndims = len(darray.dims) @@ -57,7 +57,7 @@ def plot(darray, ax=None, rtol=0.01, **kwargs): else: plotfunc = plot_hist - return plotfunc(darray, *args, **kwargs) + return plotfunc(darray, **kwargs) # This function signature should not change so that it can pass format @@ -179,12 +179,12 @@ def plot_contourf(darray, ax=None, add_colorbar=True, **kwargs): x = darray[xlab] y = darray[ylab] - ax.contourf(x, y, darray, **kwargs) + contours = ax.contourf(x, y, darray, **kwargs) ax.set_xlabel(xlab) ax.set_ylabel(ylab) if add_colorbar: - ax.colorbar() + plt.colorbar(contours) return ax diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index b6ce365cefa..3394b3ae564 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -160,10 +160,5 @@ def test_can_pass_in_kwargs(self): ax = self.darray.plot_hist(bins=nbins) self.assertEqual(nbins, len(ax.patches)) - def test_can_pass_in_positional_args(self): - nbins = 5 - ax = self.darray.plot_hist(nbins) - self.assertEqual(nbins, len(ax.patches)) - def test_can_pass_in_axis(self): self.pass_in_axis(self.darray.plot_hist) From 2d0c893170a0e8c62086a330132162d7cf39b2bb Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 9 Jul 2015 15:52:05 -0700 Subject: [PATCH 34/61] tests passing after removing position args --- xray/core/plotting.py | 9 ++++++--- xray/test/test_plotting.py | 6 +++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/xray/core/plotting.py b/xray/core/plotting.py index daec2dd1b52..aaa9f4a35c7 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -44,7 +44,6 @@ def plot(darray, ax=None, rtol=0.01, **kwargs): kwargs Additional keyword arguments to matplotlib """ - kwargs['ax'] = ax ndims = len(darray.dims) if ndims == 1: @@ -57,6 +56,7 @@ def plot(darray, ax=None, rtol=0.01, **kwargs): else: plotfunc = plot_hist + kwargs['ax'] = ax return plotfunc(darray, **kwargs) @@ -88,10 +88,13 @@ def plot_line(darray, *args, **kwargs): raise ValueError('Line plots are for 1 dimensional DataArrays. ' 'Passed DataArray has {} dimensions'.format(ndims)) - # Was an axis passed in? + # Ensures consistency with .plot method try: ax = kwargs.pop('ax') except KeyError: + ax = None + + if ax is None: ax = plt.gca() xlabel, x = list(darray.indexes.items())[0] @@ -184,7 +187,7 @@ def plot_contourf(darray, ax=None, add_colorbar=True, **kwargs): ax.set_ylabel(ylab) if add_colorbar: - plt.colorbar(contours) + plt.colorbar(contours, ax=ax) return ax diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index 3394b3ae564..07fba7ddd64 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -59,9 +59,6 @@ def test2d_nonuniform_calls_contourf(self): def test3d(self): self.darray.plot() - def test_format_string(self): - self.darray[0, 0, :].plot('ro') - def test_can_pass_in_axis(self): self.pass_in_axis(self.darray.plot) @@ -90,6 +87,9 @@ def test_wrong_dims_raises_valueerror(self): with self.assertRaises(ValueError): twodims.plot_line() + def test_format_string(self): + self.darray.plot_line('ro') + def test_can_pass_in_axis(self): self.pass_in_axis(self.darray.plot_line) From 9b6be1d05833d241fa1bfc5e95eb3e0ead7423cc Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 9 Jul 2015 17:31:37 -0700 Subject: [PATCH 35/61] More docs in simple 2d example --- doc/plotting.rst | 71 ++++++++++++++++++++++++-------------- xray/core/plotting.py | 19 ++++++++-- xray/test/test_plotting.py | 4 +++ 3 files changed, 66 insertions(+), 28 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 84b3648a82e..8fa26c3abce 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -59,7 +59,7 @@ Additional Arguments Additional arguments are passed directly to the matplotlib function which does the work. -For example, for a 1 dimensional DataArray, :py:meth:`xray.DataArray.plot_line` calls ``plt.plot``, +For example, :py:meth:`xray.DataArray.plot_line` calls ``plt.plot``, passing in the index and the array values as x and y, respectively. So to make a line plot with blue triangles a `matplotlib format string `__ @@ -70,6 +70,12 @@ can be used: @savefig plotting_example_sin2.png width=4in sinpts.plot_line('b-^') +.. warning:: + Not all xray plotting methods support passing positional arguments + to the underlying matplotlib functions, but they do all + support keyword arguments. Check the documentation for each + function to make sure. + Keyword arguments work the same way: .. ipython:: python @@ -77,7 +83,6 @@ Keyword arguments work the same way: @savefig plotting_example_sin3.png width=4in sinpts.plot_line(color='purple', marker='o') - Adding to Existing Axis ~~~~~~~~~~~~~~~~~~~~~~~ @@ -133,14 +138,21 @@ calls :py:meth:`xray.DataArray.plot_imshow`. a[0, 0] = 1 a +The plot will produce an image corresponding to the values of the array. +Hence the top left pixel will be a different color than the others. +Before reading on, you may want to look at the coordinates and +think carefully about what the limits, labels, and orientation for +each of the axes should be. + +.. ipython:: python + @savefig plotting_example_2d_simple.png width=4in a.plot() -The top left pixel is 1, and the others are 0. This corresponds to the -printed array. It may seem strange that +It may seem strange that the the values on the y axis are decreasing with 0 on the top. This is because the axis labels and ranges correspond to the values of the -coordinates. +coordinates. The pixels are centered over their coordinates. An `extended slice `__ can be used to reverse the order of the rows, producing a @@ -150,36 +162,19 @@ more conventional plot where the coordinates increase in the y axis. a[::-1, :] - @savefig plotting_example_2d_simple.png width=4in + @savefig plotting_example_2d_simple_reversed.png width=4in a[::-1, :].plot() - -Nonuniform Coordinates -~~~~~~~~~~~~~~~~~~~~~~ - -If the coordinates are not uniformly spaced then -:py:meth:`xray.DataArray.plot` produces a filled contour plot by calling -:py:meth:`xray.DataArray.plot_contourf`. - -.. ipython:: python - - a.coords['x'] = [0, 1, 4] - - @savefig plotting_example_2d_nonuniform.png width=4in - a.plot() - - Simulated Data ~~~~~~~~~~~~~~ For further examples we generate two dimensional data by computing the distance from a 2d grid point to the origin. -It's not necessary for the grid to be evenly spaced. .. ipython:: python x = np.linspace(-5, 10, num=6) - y = np.logspace(1.2, 0, num=7) + y = np.linspace(1.2, 0, num=7) xy = np.dstack(np.meshgrid(x, y)) distance = np.linalg.norm(xy, axis=2) @@ -190,8 +185,32 @@ It's not necessary for the grid to be evenly spaced. Note the coordinate ``y`` here is decreasing. This makes the axes of the image plot in the expected way. -# TODO- Edge case- what if the coordinates are not sorted? Is this -possible? What if coordinates increasing? +TODO- need proper scaling for y axis. + +.. ipython:: python + + @savefig plotting_2d_simulated.png width=4in + distance.plot() + +Nonuniform Coordinates +~~~~~~~~~~~~~~~~~~~~~~ + +It's not necessary for the coordinates to be evenly spaced. If not, then +:py:meth:`xray.DataArray.plot` produces a filled contour plot by calling +:py:meth:`xray.DataArray.plot_contourf`. This example demonstrates that by +using coordinates with logarithmic spacing. + +.. ipython:: python + + x = np.logspace(0, 2, num=3) + y = np.logspace(0, 2, num=4) + xy = np.dstack(np.meshgrid(x, y)) + d2 = np.linalg.norm(xy, axis=2) + d2 = xray.DataArray(d2, {'x': x, 'y': y}) + d2 + + @savefig plotting_nonuniform_coords.png width=4in + d2.plot() Calling Matplotlib ~~~~~~~~~~~~~~~~~~ diff --git a/xray/core/plotting.py b/xray/core/plotting.py index aaa9f4a35c7..d701385bb1b 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -113,6 +113,9 @@ def plot_imshow(darray, ax=None, add_colorbar=True, *args, **kwargs): """ Image plot of 2d DataArray using matplotlib / pylab. + Warning: This function needs sorted, uniformly spaced coordinates to + properly label the axes. + Wraps matplotlib.pyplot.imshow Parameters @@ -126,6 +129,11 @@ def plot_imshow(darray, ax=None, add_colorbar=True, *args, **kwargs): add_colorbar : Boolean Adds colorbar to axis + Details + ------- + The pixels are centered on the coordinates values. Ie, if the coordinate + value is 3.2 then the pixel for that data point will be centered on 3.2. + Examples -------- @@ -145,13 +153,19 @@ def plot_imshow(darray, ax=None, add_colorbar=True, *args, **kwargs): x = darray[xlab] y = darray[ylab] - image = ax.imshow(darray, extent=[x.min(), x.max(), y.min(), y.max()], + # Use to center the pixels- Assumes uniform spacing + xstep = (x[1] - x[0]) / 2.0 + ystep = (y[1] - y[0]) / 2.0 + left, right = x[0] - xstep, x[-1] + xstep + bottom, top = y[-1] + ystep, y[0] - ystep + + ax.imshow(darray, extent=[left, right, bottom, top], interpolation='nearest', *args, **kwargs) ax.set_xlabel(xlab) ax.set_ylabel(ylab) - plt.colorbar(image, ax=ax) + #plt.colorbar(image, ax=ax) return ax @@ -183,6 +197,7 @@ def plot_contourf(darray, ax=None, add_colorbar=True, **kwargs): y = darray[ylab] contours = ax.contourf(x, y, darray, **kwargs) + ax.set_xlabel(xlab) ax.set_ylabel(ylab) diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index 07fba7ddd64..e4dacf1de38 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -133,6 +133,10 @@ def test_contourf_called(self): self.assertFalse(self.contourf_called(self.darray.plot_imshow)) self.assertTrue(self.contourf_called(self.darray.plot_contourf)) + def test_imshow_xy_pixel_centered(self): + ax = self.darray.plot_contourf() + self.assertTrue(np.allclose([-0.5, 14.5], ax.get_xlim())) + class TestPlotHist(PlotTestCase): From 09c242cc8369f24af46126c86fbd3a70d645b761 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 10 Jul 2015 09:21:02 -0700 Subject: [PATCH 36/61] auto aspect ratio for imshow --- xray/core/plotting.py | 20 ++++++++++++++------ xray/test/test_plotting.py | 11 ++++++++++- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/xray/core/plotting.py b/xray/core/plotting.py index d701385bb1b..58c80631aad 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -124,15 +124,15 @@ def plot_imshow(darray, ax=None, add_colorbar=True, *args, **kwargs): Must be 2 dimensional ax : matplotlib axes object If None, uses the current axis - args, kwargs - Additional arguments to matplotlib.pyplot.imshow add_colorbar : Boolean Adds colorbar to axis + kwargs + Additional arguments to matplotlib.pyplot.imshow Details ------- The pixels are centered on the coordinates values. Ie, if the coordinate - value is 3.2 then the pixel for that data point will be centered on 3.2. + value is 3.2 then the pixels for those coordinates will be centered on 3.2. Examples -------- @@ -159,13 +159,21 @@ def plot_imshow(darray, ax=None, add_colorbar=True, *args, **kwargs): left, right = x[0] - xstep, x[-1] + xstep bottom, top = y[-1] + ystep, y[0] - ystep - ax.imshow(darray, extent=[left, right, bottom, top], - interpolation='nearest', *args, **kwargs) + defaults = {'extent': [left, right, bottom, top], + 'aspect': 'auto', + 'interpolation': 'nearest', + } + + # Allow user to override these defaults + defaults.update(kwargs) + + image = ax.imshow(darray, **defaults) ax.set_xlabel(xlab) ax.set_ylabel(ylab) - #plt.colorbar(image, ax=ax) + if add_colorbar: + plt.colorbar(image, ax=ax) return ax diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index e4dacf1de38..7d60d922903 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -134,8 +134,17 @@ def test_contourf_called(self): self.assertTrue(self.contourf_called(self.darray.plot_contourf)) def test_imshow_xy_pixel_centered(self): - ax = self.darray.plot_contourf() + ax = self.darray.plot_imshow() self.assertTrue(np.allclose([-0.5, 14.5], ax.get_xlim())) + self.assertTrue(np.allclose([9.5, -0.5], ax.get_ylim())) + + def test_default_aspect_is_auto(self): + ax = self.darray.plot_imshow() + self.assertEqual('auto', ax.get_aspect()) + + def test_can_change_aspect(self): + ax = self.darray.plot_imshow(aspect='equal') + self.assertEqual('equal', ax.get_aspect()) class TestPlotHist(PlotTestCase): From 2451c47ae91076eb45ebd01d1f1de33606d4ae10 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 10 Jul 2015 11:52:45 -0700 Subject: [PATCH 37/61] examples with color maps --- doc/plotting.rst | 97 +++++++++++++++++++++++++++++++----------------- 1 file changed, 62 insertions(+), 35 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 8fa26c3abce..963f21d0477 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -121,6 +121,7 @@ The index may be a date. @savefig plotting_example_time.png width=6in noise.plot_line() +TODO- rotate dates printed on x axis. Two Dimensions -------------- @@ -128,7 +129,7 @@ Two Dimensions Simple Example ~~~~~~~~~~~~~~ -The default :py:meth:`xray.DataArray.plot` sees that the data is +The default method :py:meth:`xray.DataArray.plot` sees that the data is 2 dimensional. If the coordinates are uniformly spaced then it calls :py:meth:`xray.DataArray.plot_imshow`. @@ -150,9 +151,10 @@ each of the axes should be. a.plot() It may seem strange that -the the values on the y axis are decreasing with 0 on the top. This is because the +the the values on the y axis are decreasing with -0.5 on the top. This is because +the pixels are centered over their coordinates, and the axis labels and ranges correspond to the values of the -coordinates. The pixels are centered over their coordinates. +coordinates. An `extended slice `__ can be used to reverse the order of the rows, producing a @@ -173,8 +175,8 @@ from a 2d grid point to the origin. .. ipython:: python - x = np.linspace(-5, 10, num=6) - y = np.linspace(1.2, 0, num=7) + x = np.arange(start=0, stop=10, step=2) + y = np.arange(start=9, stop=-7, step=-3) xy = np.dstack(np.meshgrid(x, y)) distance = np.linalg.norm(xy, axis=2) @@ -183,14 +185,36 @@ from a 2d grid point to the origin. distance Note the coordinate ``y`` here is decreasing. -This makes the axes of the image plot in the expected way. - -TODO- need proper scaling for y axis. +This makes the y axes appear in the conventional way. .. ipython:: python @savefig plotting_2d_simulated.png width=4in distance.plot() + +Changing Axes +~~~~~~~~~~~~~ + +To swap the variables plotted on vertical and horizontal axes one can +transpose the array. + +.. ipython:: python + + @savefig plotting_changing_axes.png width=4in + distance.T.plot() + +TODO: Feedback here please. This requires the user to put the array into +the order they want for plotting. To plot with sorted coordinates they +would have to write something +like this: ``distance.T[::-1, ::-1].plot()``. +This requires the user to be aware of how the array is organized. + +Alternatively, this could be implemented in +xray plotting as: ``distance.plot(xvar='y', sortx=True, +sorty=True)``. +This allows the use of the dimension +name to describe which coordinate should appear as the x variable on the +plot, and is probably more convenient. Nonuniform Coordinates ~~~~~~~~~~~~~~~~~~~~~~ @@ -198,56 +222,59 @@ Nonuniform Coordinates It's not necessary for the coordinates to be evenly spaced. If not, then :py:meth:`xray.DataArray.plot` produces a filled contour plot by calling :py:meth:`xray.DataArray.plot_contourf`. This example demonstrates that by -using coordinates with logarithmic spacing. +using one coordinate with logarithmic spacing. .. ipython:: python - x = np.logspace(0, 2, num=3) - y = np.logspace(0, 2, num=4) + x = np.linspace(0, 500) + y = np.logspace(0, 3) xy = np.dstack(np.meshgrid(x, y)) - d2 = np.linalg.norm(xy, axis=2) - d2 = xray.DataArray(d2, {'x': x, 'y': y}) - d2 + d_ylog = np.linalg.norm(xy, axis=2) + d_ylog = xray.DataArray(d_ylog, {'x': x, 'y': y}) @savefig plotting_nonuniform_coords.png width=4in - d2.plot() + d_ylog.plot() Calling Matplotlib ~~~~~~~~~~~~~~~~~~ -Use matplotlib to adjust plot parameters. For example, the -y grid points were generated from a log scale, so we can use matplotlib -to adjust the scale on y: +Since this is a thin wrapper around matplotlib, all the functionality of +matplotlib is available. For example, use a different color map and add a title. .. ipython:: python - #plt.yscale('log') + d_ylog.plot(cmap=plt.cm.Blues) + plt.title('Euclidean distance from point to origin') - @savefig plotting_example_2d3.png width=4in - distance.plot() + @savefig plotting_2d_call_matplotlib.png width=4in + plt.show() -Changing Axes -~~~~~~~~~~~~~ +Colormaps +~~~~~~~~~ -Two dimensional plotting in xray uses the -Swap the variables plotted on vertical and horizontal axes by transposing the array. +Suppose we want two plots to share the same color scale. This can be +achieved by passing in a color map. + +TODO- Don't actually know how to do this yet. Will probably want it for the +Faceting .. ipython:: python - @savefig plotting_example_2d2.png width=4in - distance.T.plot() + colors = plt.cm.Blues -Contour Plot -~~~~~~~~~~~~ + fig, axes = plt.subplots(ncols=2) -Visualization is + distance.plot(ax=axes[0], cmap=colors, ) -.. ipython:: python + halfd = distance / 2 + halfd.plot(ax=axes[1], cmap=colors) + + @savefig plotting_same_color_scale.png width=6in + plt.show() + +Maps +---- - @savefig plotting_example_contour.png width=4in - distance.plot_contourf() - -TODO- This is the same plot as ``imshow``. Details ------- From 8cb6403f7eaa4fb7920af5500a158d3760493d13 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 10 Jul 2015 13:04:15 -0700 Subject: [PATCH 38/61] adding maps to docs --- doc/plotting.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/doc/plotting.rst b/doc/plotting.rst index 963f21d0477..121d282a7a5 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -275,6 +275,26 @@ Faceting Maps ---- +To follow this section you'll need to have Cartopy installed and working. + +Plot an image over the Atlantic ocean. + +.. ipython:: python + + import cartopy.crs as ccrs + + n = 30 + atlantic = xray.DataArray(np.random.randn(n, n), + coords = (np.linspace(50, 20, n), np.linspace(10, 60, n)), + dims = ('latitude', 'longitude')) + + ax = plt.axes(projection=ccrs.PlateCarree()) + ax.stock_img() + + atlantic.plot(ax=ax) + + @savefig simple_map.png width=6in + plt.show() Details ------- From c39d766ea7ce0ee010e8e84ecfed3eb6b1798366 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 10 Jul 2015 13:35:32 -0700 Subject: [PATCH 39/61] first plotting example works --- doc/plotting.rst | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 121d282a7a5..7828587eceb 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -283,16 +283,21 @@ Plot an image over the Atlantic ocean. import cartopy.crs as ccrs - n = 30 - atlantic = xray.DataArray(np.random.randn(n, n), - coords = (np.linspace(50, 20, n), np.linspace(10, 60, n)), + nlat = 15 + nlon = 5 + atlantic = xray.DataArray(np.random.randn(nlat, nlon), + coords = (np.linspace(50, 20, nlat), np.linspace(-60, -20, nlon)), dims = ('latitude', 'longitude')) ax = plt.axes(projection=ccrs.PlateCarree()) - ax.stock_img() atlantic.plot(ax=ax) + ax.set_ylim(0, 90) + ax.set_xlim(-180, 30) + + ax.coastlines() + @savefig simple_map.png width=6in plt.show() From 89e5db0e7102008bb525324c6795e5a91a698faf Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 10 Jul 2015 14:24:23 -0700 Subject: [PATCH 40/61] preparing to check through code with pep8 and pyflakes --- xray/core/dataarray.py | 3 - xray/core/plotting.py | 111 +++++++++++++++++++++---------------- xray/test/test_plotting.py | 1 + 3 files changed, 63 insertions(+), 52 deletions(-) diff --git a/xray/core/dataarray.py b/xray/core/dataarray.py index 28d31afc87f..7f90673e7ad 100644 --- a/xray/core/dataarray.py +++ b/xray/core/dataarray.py @@ -1079,9 +1079,6 @@ def func(self, other): # Add plotting methods # Alternatively these could be added using a Mixin -# Wondering if it's better to only expose plot and plot_hist here, since -# those always work. - DataArray.plot = plotting.plot DataArray.plot_line = plotting.plot_line DataArray.plot_contourf = plotting.plot_contourf diff --git a/xray/core/plotting.py b/xray/core/plotting.py index 58c80631aad..28355704f70 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -1,9 +1,8 @@ """ -Plotting functions are implemented here and also monkeypatched in to -DataArray and DataSet classes +Plotting functions are implemented here and also monkeypatched into +the DataArray class """ -import functools import numpy as np from .utils import is_uniform_spaced @@ -14,6 +13,7 @@ # But if all the plotting methods have same signature... +# TODO - implement this class FacetGrid(): pass @@ -22,16 +22,17 @@ def plot(darray, ax=None, rtol=0.01, **kwargs): """ Default plot of DataArray using matplotlib / pylab. - Calls a plotting function based on the dimensions of + Calls xray plotting function based on the dimensions of the array: - =============== ====================================== - Dimensions Plotting function - --------------- -------------------------------------- - 1 :py:meth:`xray.DataArray.plot_line` - 2 :py:meth:`xray.DataArray.plot_imshow` - Anything else :py:meth:`xray.DataArray.plot_hist` - =============== ====================================== + =============== =========== =========================== + Dimensions Coordinates Plotting function + --------------- ----------- --------------------------- + 1 :py:meth:`xray.DataArray.plot_line` + 2 Uniform :py:meth:`xray.DataArray.plot_imshow` + 2 Irregular :py:meth:`xray.DataArray.plot_contourf` + Anything else :py:meth:`xray.DataArray.plot_hist` + =============== =========== =========================== Parameters ---------- @@ -43,6 +44,7 @@ def plot(darray, ax=None, rtol=0.01, **kwargs): are uniformly spaced kwargs Additional keyword arguments to matplotlib + """ ndims = len(darray.dims) @@ -60,11 +62,11 @@ def plot(darray, ax=None, rtol=0.01, **kwargs): return plotfunc(darray, **kwargs) -# This function signature should not change so that it can pass format -# strings +# This function signature should not change so that it can use +# matplotlib format strings def plot_line(darray, *args, **kwargs): """ - Line plot of 1 dimensional darray index against values + Line plot of 1 dimensional DataArray index against values Wraps matplotlib.pyplot.plot @@ -80,6 +82,16 @@ def plot_line(darray, *args, **kwargs): Examples -------- + >>> from numpy import sin, linspace + >>> a = DataArray(sin(linspace(0, 10))) + + @savefig plotting_plot_line_doc1.png width=4in + >>> a.plot_line() + + Use matplotlib arguments: + @savefig plotting_plot_line_doc2.png width=4in + >>> a.plot_line(color='purple', shape='x') + """ import matplotlib.pyplot as plt @@ -99,7 +111,7 @@ def plot_line(darray, *args, **kwargs): xlabel, x = list(darray.indexes.items())[0] - ax.plot(x, darray.values, *args, **kwargs) + ax.plot(x, darray, *args, **kwargs) ax.set_xlabel(xlabel) @@ -109,15 +121,17 @@ def plot_line(darray, *args, **kwargs): return ax -def plot_imshow(darray, ax=None, add_colorbar=True, *args, **kwargs): +def plot_imshow(darray, ax=None, add_colorbar=True, **kwargs): """ - Image plot of 2d DataArray using matplotlib / pylab. - - Warning: This function needs sorted, uniformly spaced coordinates to - properly label the axes. + Image plot of 2d DataArray using matplotlib / pylab Wraps matplotlib.pyplot.imshow + Warning:: + + This function needs sorted, uniformly spaced coordinates to + properly label the axes. + Parameters ---------- darray : DataArray @@ -126,7 +140,7 @@ def plot_imshow(darray, ax=None, add_colorbar=True, *args, **kwargs): If None, uses the current axis add_colorbar : Boolean Adds colorbar to axis - kwargs + kwargs : Additional arguments to matplotlib.pyplot.imshow Details @@ -134,16 +148,12 @@ def plot_imshow(darray, ax=None, add_colorbar=True, *args, **kwargs): The pixels are centered on the coordinates values. Ie, if the coordinate value is 3.2 then the pixels for those coordinates will be centered on 3.2. - Examples - -------- - """ import matplotlib.pyplot as plt if ax is None: ax = plt.gca() - # Seems strange that ylab comes first try: ylab, xlab = darray.dims except ValueError: @@ -153,7 +163,7 @@ def plot_imshow(darray, ax=None, add_colorbar=True, *args, **kwargs): x = darray[xlab] y = darray[ylab] - # Use to center the pixels- Assumes uniform spacing + # Centering the pixels- Assumes uniform spacing xstep = (x[1] - x[0]) / 2.0 ystep = (y[1] - y[0]) / 2.0 left, right = x[0] - xstep, x[-1] + xstep @@ -162,7 +172,7 @@ def plot_imshow(darray, ax=None, add_colorbar=True, *args, **kwargs): defaults = {'extent': [left, right, bottom, top], 'aspect': 'auto', 'interpolation': 'nearest', - } + } # Allow user to override these defaults defaults.update(kwargs) @@ -178,10 +188,25 @@ def plot_imshow(darray, ax=None, add_colorbar=True, *args, **kwargs): return ax -# TODO - Could refactor this to avoid duplicating plot_image logic above +# TODO - Could refactor this to avoid duplicating plot_imshow logic. +# There's also some similar tests for the two. def plot_contourf(darray, ax=None, add_colorbar=True, **kwargs): """ - Contour plot + Filled contour plot of 2d DataArray + + Wraps matplotlib.pyplot.contourf + + Parameters + ---------- + darray : DataArray + Must be 2 dimensional + ax : matplotlib axes object + If None, uses the current axis + add_colorbar : Boolean + Adds colorbar to axis + kwargs : + Additional arguments to matplotlib.pyplot.imshow + """ import matplotlib.pyplot as plt @@ -194,17 +219,7 @@ def plot_contourf(darray, ax=None, add_colorbar=True, **kwargs): raise ValueError('Contour plots are for 2 dimensional DataArrays. ' 'Passed DataArray has {} dimensions'.format(len(darray.dims))) - # Need arrays here? - #x = darray[xlab].values - #y = darray[ylab].values - #z = darray.values - - #ax.contourf(x, y, z, *args, **kwargs) - - x = darray[xlab] - y = darray[ylab] - - contours = ax.contourf(x, y, darray, **kwargs) + contours = ax.contourf(darray[xlab], darray[ylab], darray, **kwargs) ax.set_xlabel(xlab) ax.set_ylabel(ylab) @@ -217,22 +232,20 @@ def plot_contourf(darray, ax=None, add_colorbar=True, **kwargs): def plot_hist(darray, ax=None, **kwargs): """ - Histogram of DataArray using matplotlib / pylab. - Plots N dimensional arrays by first flattening the array. - + Histogram of DataArray + Wraps matplotlib.pyplot.hist + Plots N dimensional arrays by first flattening the array. + Parameters ---------- darray : DataArray - Must be 2 dimensional + Can be any dimension ax : matplotlib axes object If not passed, uses the current axis - kwargs - Additional arguments to matplotlib.pyplot.hist - - Examples - -------- + kwargs : + Additional keyword arguments to matplotlib.pyplot.hist """ import matplotlib.pyplot as plt diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index 7d60d922903..8d01958ba14 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -93,6 +93,7 @@ def test_format_string(self): def test_can_pass_in_axis(self): self.pass_in_axis(self.darray.plot_line) +# TODO - Add NaN handling and tests class TestPlot2D(PlotTestCase): From 36bb79c63df7439dd3d2b5e0b15a127483b7a17a Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 10 Jul 2015 14:34:54 -0700 Subject: [PATCH 41/61] pep8 and pyflakes fixes --- xray/core/plotting.py | 31 +++++++++++++++++-------------- xray/test/test_plotting.py | 10 +++++----- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/xray/core/plotting.py b/xray/core/plotting.py index 28355704f70..e2d9a4bdc21 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -28,10 +28,10 @@ def plot(darray, ax=None, rtol=0.01, **kwargs): =============== =========== =========================== Dimensions Coordinates Plotting function --------------- ----------- --------------------------- - 1 :py:meth:`xray.DataArray.plot_line` - 2 Uniform :py:meth:`xray.DataArray.plot_imshow` - 2 Irregular :py:meth:`xray.DataArray.plot_contourf` - Anything else :py:meth:`xray.DataArray.plot_hist` + 1 :py:meth:`xray.DataArray.plot_line` + 2 Uniform :py:meth:`xray.DataArray.plot_imshow` + 2 Irregular :py:meth:`xray.DataArray.plot_contourf` + Anything else :py:meth:`xray.DataArray.plot_hist` =============== =========== =========================== Parameters @@ -51,7 +51,8 @@ def plot(darray, ax=None, rtol=0.01, **kwargs): if ndims == 1: plotfunc = plot_line elif ndims == 2: - if all(is_uniform_spaced(i, rtol=rtol) for i in darray.indexes.values()): + indexes = darray.indexes.values() + if all(is_uniform_spaced(i, rtol=rtol) for i in indexes): plotfunc = plot_imshow else: plotfunc = plot_contourf @@ -98,7 +99,7 @@ def plot_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 {} dimensions'.format(ndims)) # Ensures consistency with .plot method try: @@ -128,7 +129,7 @@ def plot_imshow(darray, ax=None, add_colorbar=True, **kwargs): Wraps matplotlib.pyplot.imshow Warning:: - + This function needs sorted, uniformly spaced coordinates to properly label the axes. @@ -158,7 +159,8 @@ def plot_imshow(darray, ax=None, add_colorbar=True, **kwargs): ylab, xlab = darray.dims except ValueError: raise ValueError('Image plots are for 2 dimensional DataArrays. ' - 'Passed DataArray has {} dimensions'.format(len(darray.dims))) + 'Passed DataArray has {} dimensions' + .format(len(darray.dims))) x = darray[xlab] y = darray[ylab] @@ -170,9 +172,9 @@ def plot_imshow(darray, ax=None, add_colorbar=True, **kwargs): bottom, top = y[-1] + ystep, y[0] - ystep defaults = {'extent': [left, right, bottom, top], - 'aspect': 'auto', - 'interpolation': 'nearest', - } + 'aspect': 'auto', + 'interpolation': 'nearest', + } # Allow user to override these defaults defaults.update(kwargs) @@ -217,7 +219,8 @@ def plot_contourf(darray, ax=None, add_colorbar=True, **kwargs): ylab, xlab = darray.dims except ValueError: raise ValueError('Contour plots are for 2 dimensional DataArrays. ' - 'Passed DataArray has {} dimensions'.format(len(darray.dims))) + 'Passed DataArray has {} dimensions' + .format(len(darray.dims))) contours = ax.contourf(darray[xlab], darray[ylab], darray, **kwargs) @@ -232,8 +235,8 @@ def plot_contourf(darray, ax=None, add_colorbar=True, **kwargs): def plot_hist(darray, ax=None, **kwargs): """ - Histogram of DataArray - + Histogram of DataArray + Wraps matplotlib.pyplot.hist Plots N dimensional arrays by first flattening the array. diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index 8d01958ba14..e62751ae744 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -1,7 +1,6 @@ import numpy as np -import pandas as pd -from xray import Dataset, DataArray +from xray import DataArray from . import TestCase, requires_matplotlib @@ -15,6 +14,8 @@ pass +# TODO - Add NaN handling and tests + @requires_matplotlib class PlotTestCase(TestCase): @@ -93,13 +94,12 @@ def test_format_string(self): def test_can_pass_in_axis(self): self.pass_in_axis(self.darray.plot_line) -# TODO - Add NaN handling and tests class TestPlot2D(PlotTestCase): def setUp(self): - self.darray = DataArray(np.random.randn(10, 15), - dims=['y', 'x']) + self.darray = DataArray(np.random.randn(10, 15), + dims=['y', 'x']) def test_contour_label_names(self): ax = self.darray.plot_contourf() From abb728151a6d58b22e509cf8b48587ac5dcc3b55 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 10 Jul 2015 15:52:22 -0700 Subject: [PATCH 42/61] add cartopy example --- doc/examples/atlantic_noise.png | Bin 0 -> 85534 bytes doc/examples/cartopy_atlantic.py | 20 +++++ doc/plotting.rst | 129 +++++++++++++++---------------- xray/core/plotting.py | 19 +---- 4 files changed, 85 insertions(+), 83 deletions(-) create mode 100644 doc/examples/atlantic_noise.png create mode 100644 doc/examples/cartopy_atlantic.py diff --git a/doc/examples/atlantic_noise.png b/doc/examples/atlantic_noise.png new file mode 100644 index 0000000000000000000000000000000000000000..3ed287e7a1a7da119040414bf75f73c1ff318b3c GIT binary patch literal 85534 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 library. +`matplotlib `_ library. Matplotlib syntax and function names were copied as much as possible, which makes for an easy transition between the two. +Matplotlib must be installed and working before trying to plot with xray. -For more specialized plotting applications consider the following packages: +For more extensive plotting applications consider the following projects: -- `Seaborn `__: "provides +- `Seaborn `_: "provides a high-level interface for drawing attractive statistical graphics." Integrates well with pandas. -- `Holoviews `__: "Composable, declarative +- `Holoviews `_: "Composable, declarative data structures for building even complex visualizations easily." Works for 2d datasets. -- `Cartopy `__: provides cartographic - tools +- `Cartopy `_: Provides cartographic + tools. Imports ~~~~~~~ -Begin by importing the necessary modules: +These imports are necessary for all of the examples. .. ipython:: python import numpy as np - import xray import matplotlib.pyplot as plt + import xray One Dimension ------------- @@ -43,40 +44,40 @@ One Dimension Simple Example ~~~~~~~~~~~~~~ -Here is a simple example of plotting. Xray uses the coordinate name to label the x axis: .. ipython:: python - t = np.linspace(0, 2*np.pi) + t = np.linspace(0, np.pi, num=20) sinpts = xray.DataArray(np.sin(t), {'t': t}, name='sin(t)') + sinpts @savefig plotting_example_sin.png width=4in sinpts.plot() -Additional Arguments +Additional Arguments ~~~~~~~~~~~~~~~~~~~~~ Additional arguments are passed directly to the matplotlib function which -does the work. -For example, :py:meth:`xray.DataArray.plot_line` calls ``plt.plot``, -passing in the index and the array values as x and y, respectively. -So to make a line plot with blue triangles a `matplotlib format string -`__ +does the work. +For example, :py:meth:`xray.DataArray.plot_line` calls +matplotlib.pyplot.plot_ passing in the index and the array values as x and y, respectively. +So to make a line plot with blue triangles a matplotlib format string can be used: +.. _matplotlib.pyplot.plot: http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.plot + .. ipython:: python @savefig plotting_example_sin2.png width=4in sinpts.plot_line('b-^') -.. warning:: - Not all xray plotting methods support passing positional arguments - to the underlying matplotlib functions, but they do all - support keyword arguments. Check the documentation for each - function to make sure. +.. warning:: + Not all xray plotting methods support passing positional arguments + to the wrapped matplotlib functions, but they do all + support keyword arguments. -Keyword arguments work the same way: +Keyword arguments work the same way, and are more explicit. .. ipython:: python @@ -103,8 +104,7 @@ axes created by ``plt.subplots``. @savefig plotting_example_existing_axes.png width=6in plt.show() -Instead of using the default :py:meth:`xray.DataArray.plot` we see a -histogram created by :py:meth:`xray.DataArray.plot_hist`. +On the right is a histogram created by :py:meth:`xray.DataArray.plot_hist`. Time Series ~~~~~~~~~~~ @@ -123,15 +123,16 @@ The index may be a date. TODO- rotate dates printed on x axis. + Two Dimensions -------------- Simple Example ~~~~~~~~~~~~~~ -The default method :py:meth:`xray.DataArray.plot` sees that the data is +The default method :py:meth:`xray.DataArray.plot` sees that the data is 2 dimensional. If the coordinates are uniformly spaced then it -calls :py:meth:`xray.DataArray.plot_imshow`. +calls :py:meth:`xray.DataArray.plot_imshow`. .. ipython:: python @@ -139,10 +140,10 @@ calls :py:meth:`xray.DataArray.plot_imshow`. a[0, 0] = 1 a -The plot will produce an image corresponding to the values of the array. -Hence the top left pixel will be a different color than the others. -Before reading on, you may want to look at the coordinates and -think carefully about what the limits, labels, and orientation for +The plot will produce an image corresponding to the values of the array. +Hence the top left pixel will be a different color than the others. +Before reading on, you may want to look at the coordinates and +think carefully about what the limits, labels, and orientation for each of the axes should be. .. ipython:: python @@ -151,10 +152,10 @@ each of the axes should be. a.plot() It may seem strange that -the the values on the y axis are decreasing with -0.5 on the top. This is because +the the values on the y axis are decreasing with -0.5 on the top. This is because the pixels are centered over their coordinates, and the axis labels and ranges correspond to the values of the -coordinates. +coordinates. An `extended slice `__ can be used to reverse the order of the rows, producing a @@ -170,8 +171,8 @@ more conventional plot where the coordinates increase in the y axis. Simulated Data ~~~~~~~~~~~~~~ -For further examples we generate two dimensional data by computing the distance -from a 2d grid point to the origin. +For further examples we generate two dimensional data by computing the Euclidean +distance from a 2d grid point to the origin. .. ipython:: python @@ -184,18 +185,18 @@ from a 2d grid point to the origin. distance = xray.DataArray(distance, {'x': x, 'y': y}) distance -Note the coordinate ``y`` here is decreasing. +Note the coordinate ``y`` here is decreasing. This makes the y axes appear in the conventional way. .. ipython:: python @savefig plotting_2d_simulated.png width=4in distance.plot() - + Changing Axes ~~~~~~~~~~~~~ -To swap the variables plotted on vertical and horizontal axes one can +To swap the variables plotted on vertical and horizontal axes one can transpose the array. .. ipython:: python @@ -206,12 +207,12 @@ transpose the array. TODO: Feedback here please. This requires the user to put the array into the order they want for plotting. To plot with sorted coordinates they would have to write something -like this: ``distance.T[::-1, ::-1].plot()``. +like this: ``distance.T[::-1, ::-1].plot()``. This requires the user to be aware of how the array is organized. Alternatively, this could be implemented in xray plotting as: ``distance.plot(xvar='y', sortx=True, -sorty=True)``. +sorty=True)``. This allows the use of the dimension name to describe which coordinate should appear as the x variable on the plot, and is probably more convenient. @@ -238,7 +239,7 @@ using one coordinate with logarithmic spacing. Calling Matplotlib ~~~~~~~~~~~~~~~~~~ -Since this is a thin wrapper around matplotlib, all the functionality of +Since this is a thin wrapper around matplotlib, all the functionality of matplotlib is available. For example, use a different color map and add a title. .. ipython:: python @@ -277,36 +278,20 @@ Maps To follow this section you'll need to have Cartopy installed and working. -Plot an image over the Atlantic ocean. - -.. ipython:: python - - import cartopy.crs as ccrs - - nlat = 15 - nlon = 5 - atlantic = xray.DataArray(np.random.randn(nlat, nlon), - coords = (np.linspace(50, 20, nlat), np.linspace(-60, -20, nlon)), - dims = ('latitude', 'longitude')) +This script will plot an image over the Atlantic ocean. - ax = plt.axes(projection=ccrs.PlateCarree()) +.. literalinclude:: examples/cartopy_atlantic.py - atlantic.plot(ax=ax) +Here is the resulting image: - ax.set_ylim(0, 90) - ax.set_xlim(-180, 30) - - ax.coastlines() - - @savefig simple_map.png width=6in - plt.show() +.. image:: examples/atlantic_noise.png Details ------- There are two ways to use the xray plotting functionality: -1. Use the ``plot`` convenience methods of :py:class:`xray.DataArray` +1. Use the ``plot`` convenience methods of :py:class:`xray.DataArray` 2. Directly from the xray plotting submodule:: import xray.plotting as xplt @@ -319,8 +304,20 @@ describes what gets plotted: =============== =========== =========================== Dimensions Coordinates Plotting function --------------- ----------- --------------------------- -1 :py:meth:`xray.DataArray.plot_line` -2 Uniform :py:meth:`xray.DataArray.plot_imshow` -2 Irregular :py:meth:`xray.DataArray.plot_contourf` -Anything else :py:meth:`xray.DataArray.plot_hist` +1 :py:meth:`xray.DataArray.plot_line` +2 Uniform :py:meth:`xray.DataArray.plot_imshow` +2 Irregular :py:meth:`xray.DataArray.plot_contourf` +Anything else :py:meth:`xray.DataArray.plot_hist` =============== =========== =========================== + +Non Numeric Indexes +~~~~~~~~~~~~~~~~~~~ + +If the coordinates are not numeric. + +.. ipython:: python + + a = xray.DataArray([1, 2, 3], {'letter': ['a', 'b', 'c']}) + + @savefig plotting_nonnumeric.png width=4in + a.plot_line() diff --git a/xray/core/plotting.py b/xray/core/plotting.py index e2d9a4bdc21..f2efa0e1305 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -80,19 +80,6 @@ def plot_line(darray, *args, **kwargs): args, kwargs Additional arguments to matplotlib.pyplot.plot - Examples - -------- - - >>> from numpy import sin, linspace - >>> a = DataArray(sin(linspace(0, 10))) - - @savefig plotting_plot_line_doc1.png width=4in - >>> a.plot_line() - - Use matplotlib arguments: - @savefig plotting_plot_line_doc2.png width=4in - >>> a.plot_line(color='purple', shape='x') - """ import matplotlib.pyplot as plt @@ -128,10 +115,8 @@ def plot_imshow(darray, ax=None, add_colorbar=True, **kwargs): Wraps matplotlib.pyplot.imshow - Warning:: - - This function needs sorted, uniformly spaced coordinates to - properly label the axes. + WARNING: This function needs uniformly spaced coordinates to + properly label the axes. Call DataArray.plot() to check. Parameters ---------- From 6ccd06e5ad97dc09b813a2ecba32a86beace60fe Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 10 Jul 2015 17:11:24 -0700 Subject: [PATCH 43/61] add failing tests for informative TypeErrors --- doc/plotting.rst | 12 ------------ xray/core/plotting.py | 23 ++++++++++++++++++++++- xray/test/test_plotting.py | 12 ++++++++++++ 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index f05890df4d8..3e9c0a0c46e 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -309,15 +309,3 @@ Dimensions Coordinates Plotting function 2 Irregular :py:meth:`xray.DataArray.plot_contourf` Anything else :py:meth:`xray.DataArray.plot_hist` =============== =========== =========================== - -Non Numeric Indexes -~~~~~~~~~~~~~~~~~~~ - -If the coordinates are not numeric. - -.. ipython:: python - - a = xray.DataArray([1, 2, 3], {'letter': ['a', 'b', 'c']}) - - @savefig plotting_nonnumeric.png width=4in - a.plot_line() diff --git a/xray/core/plotting.py b/xray/core/plotting.py index f2efa0e1305..86c4ab12d57 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -18,6 +18,19 @@ class FacetGrid(): pass +def _ensure_numeric(*args): + """ + Raise exception if there is anything in args that can't be plotted on + an axis. + """ + #plottypes = [np.number, np.datetime64] + + # TODO come back here- this just tests for complex arrays + if not all(np.isrealobj(x) for x in args): + raise TypeError('Plotting requires coordinates to be numeric ' + 'or dates. Try DataArray.reindex() to convert.') + + def plot(darray, ax=None, rtol=0.01, **kwargs): """ Default plot of DataArray using matplotlib / pylab. @@ -99,6 +112,8 @@ def plot_line(darray, *args, **kwargs): xlabel, x = list(darray.indexes.items())[0] + #_ensure_numeric([x]) + ax.plot(x, darray, *args, **kwargs) ax.set_xlabel(xlabel) @@ -150,6 +165,8 @@ def plot_imshow(darray, ax=None, add_colorbar=True, **kwargs): x = darray[xlab] y = darray[ylab] + #_ensure_numeric(x, y) + # Centering the pixels- Assumes uniform spacing xstep = (x[1] - x[0]) / 2.0 ystep = (y[1] - y[0]) / 2.0 @@ -207,7 +224,11 @@ def plot_contourf(darray, ax=None, add_colorbar=True, **kwargs): 'Passed DataArray has {} dimensions' .format(len(darray.dims))) - contours = ax.contourf(darray[xlab], darray[ylab], darray, **kwargs) + x = darray[xlab] + y = darray[ylab] + #_ensure_numeric(x, y) + + contours = ax.contourf(x, y, darray, **kwargs) ax.set_xlabel(xlab) ax.set_ylabel(ylab) diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index e62751ae744..c06a40b6eed 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -94,6 +94,11 @@ def test_format_string(self): def test_can_pass_in_axis(self): self.pass_in_axis(self.darray.plot_line) + def test_nonnumeric_index_raises_typeerror(self): + a = DataArray([1, 2, 3], {'letter': ['a', 'b', 'c']}) + with self.assertRaisesRegexp(TypeError, r'[Ii]ndex'): + a.plot_line() + class TestPlot2D(PlotTestCase): @@ -120,6 +125,13 @@ def test_3d_raises_valueerror(self): with self.assertRaisesRegexp(ValueError, r'[Dd]im'): da.plot_imshow() + def test_nonnumeric_index_raises_typeerror(self): + a = DataArray(np.random.randn(3, 2), coords=[['a', 'b', 'c'], + ['d', 'e']]) + with self.assertRaisesRegexp(TypeError, r'[Ii]ndex'): + a.plot_imshow() + a.plot_contourf() + def test_can_pass_in_axis(self): self.pass_in_axis(self.darray.plot_imshow) self.pass_in_axis(self.darray.plot_contourf) From efa584433452cfbac520b27593bba98f152f9f56 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Mon, 13 Jul 2015 10:15:49 -0700 Subject: [PATCH 44/61] fix colorbar with multiple axes --- doc/plotting.rst | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 3e9c0a0c46e..2e3dea17b01 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -35,6 +35,7 @@ These imports are necessary for all of the examples. .. ipython:: python import numpy as np + import matplotlib as mpl import matplotlib.pyplot as plt import xray @@ -254,21 +255,25 @@ Colormaps ~~~~~~~~~ Suppose we want two plots to share the same color scale. This can be -achieved by passing in a color map. +achieved by passing in the appropriate arguments and adding the color bar +later. -TODO- Don't actually know how to do this yet. Will probably want it for the -Faceting +TODO: All xray plot methods return axes for consistency- is this necessary? +Now to make this particular plot we need to access ``.images[0]`` to get +the color mapping. .. ipython:: python - colors = plt.cm.Blues - fig, axes = plt.subplots(ncols=2) - distance.plot(ax=axes[0], cmap=colors, ) + kwargs = {'cmap': plt.cm.Blues, 'vmin': distance.min(), 'vmax': distance.max(), 'add_colorbar': False} + + distance.plot(ax=axes[0], **kwargs) halfd = distance / 2 - halfd.plot(ax=axes[1], cmap=colors) + im = halfd.plot(ax=axes[1], **kwargs) + + plt.colorbar(im.images[0], ax=axes.tolist()) @savefig plotting_same_color_scale.png width=6in plt.show() From be1caa90b4e412d9d87719afd71720c9c12f7408 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Mon, 13 Jul 2015 16:08:41 -0700 Subject: [PATCH 45/61] pass tests for typeerror for index w wrong type --- xray/core/dataarray.py | 2 -- xray/core/plotting.py | 17 ++++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/xray/core/dataarray.py b/xray/core/dataarray.py index 7f90673e7ad..c248c0f68ea 100644 --- a/xray/core/dataarray.py +++ b/xray/core/dataarray.py @@ -14,8 +14,6 @@ from .common import AbstractArray, BaseDataObject from .coordinates import DataArrayCoordinates, Indexes from .dataset import Dataset -from .plotting import (plot, plot_line, plot_contourf, plot_hist, - plot_imshow) from .pycompat import iteritems, basestring, OrderedDict, zip from .utils import FrozenOrderedDict from .variable import as_variable, _as_compatible_data, Coordinate diff --git a/xray/core/plotting.py b/xray/core/plotting.py index 86c4ab12d57..e19fbe60647 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -23,10 +23,12 @@ def _ensure_numeric(*args): Raise exception if there is anything in args that can't be plotted on an axis. """ - #plottypes = [np.number, np.datetime64] + plottypes = [np.floating, np.integer, np.timedelta64, np.datetime64] - # TODO come back here- this just tests for complex arrays - if not all(np.isrealobj(x) for x in args): + righttype = lambda x: any(np.issubdtype(x.dtype, t) for t in plottypes) + + # Lists need to be converted to np.arrays here. + if not any(righttype(np.array(x)) for x in args): raise TypeError('Plotting requires coordinates to be numeric ' 'or dates. Try DataArray.reindex() to convert.') @@ -112,7 +114,7 @@ def plot_line(darray, *args, **kwargs): xlabel, x = list(darray.indexes.items())[0] - #_ensure_numeric([x]) + _ensure_numeric([x]) ax.plot(x, darray, *args, **kwargs) @@ -130,7 +132,8 @@ def plot_imshow(darray, ax=None, add_colorbar=True, **kwargs): Wraps matplotlib.pyplot.imshow - WARNING: This function needs uniformly spaced coordinates to +..warning:: + This function needs uniformly spaced coordinates to properly label the axes. Call DataArray.plot() to check. Parameters @@ -165,7 +168,7 @@ def plot_imshow(darray, ax=None, add_colorbar=True, **kwargs): x = darray[xlab] y = darray[ylab] - #_ensure_numeric(x, y) + _ensure_numeric(x, y) # Centering the pixels- Assumes uniform spacing xstep = (x[1] - x[0]) / 2.0 @@ -226,7 +229,7 @@ def plot_contourf(darray, ax=None, add_colorbar=True, **kwargs): x = darray[xlab] y = darray[ylab] - #_ensure_numeric(x, y) + _ensure_numeric(x, y) contours = ax.contourf(x, y, darray, **kwargs) From 3dc16c1a8a9db40cac8781965b33ea1ec0a820da Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Mon, 13 Jul 2015 16:33:02 -0700 Subject: [PATCH 46/61] address easy feedback from github PR --- doc/plotting.rst | 4 ++-- xray/core/plotting.py | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 2e3dea17b01..0270963b986 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -183,7 +183,7 @@ distance from a 2d grid point to the origin. distance = np.linalg.norm(xy, axis=2) - distance = xray.DataArray(distance, {'x': x, 'y': y}) + distance = xray.DataArray(distance, zip(('y', 'x'), (y, x))) distance Note the coordinate ``y`` here is decreasing. @@ -232,7 +232,7 @@ using one coordinate with logarithmic spacing. y = np.logspace(0, 3) xy = np.dstack(np.meshgrid(x, y)) d_ylog = np.linalg.norm(xy, axis=2) - d_ylog = xray.DataArray(d_ylog, {'x': x, 'y': y}) + d_ylog = xray.DataArray(d_ylog, zip(('y', 'x'), (y, x))) @savefig plotting_nonuniform_coords.png width=4in d_ylog.plot() diff --git a/xray/core/plotting.py b/xray/core/plotting.py index e19fbe60647..c6beec6168f 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -104,10 +104,7 @@ def plot_line(darray, *args, **kwargs): 'Passed DataArray has {} dimensions'.format(ndims)) # Ensures consistency with .plot method - try: - ax = kwargs.pop('ax') - except KeyError: - ax = None + ax = kwargs.pop('ax', None) if ax is None: ax = plt.gca() From f033a2031dec5108a22a98c4d18700a8314f8523 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Mon, 13 Jul 2015 17:12:10 -0700 Subject: [PATCH 47/61] remove gridlines in generated docs --- doc/plotting.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/plotting.rst b/doc/plotting.rst index 0270963b986..5ecc314e61a 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -36,9 +36,13 @@ These imports are necessary for all of the examples. import numpy as np import matplotlib as mpl + # Avoids gridlines in generated docs + #mpl.rcParams['axes.grid'] = False + mpl.rcdefaults() import matplotlib.pyplot as plt import xray + One Dimension ------------- From b92f426063b18cd8b50790072d1bf8903ffef0a0 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Mon, 13 Jul 2015 17:45:25 -0700 Subject: [PATCH 48/61] beginning on 2d plot function decorator --- doc/plotting.rst | 1 + xray/core/plotting.py | 48 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 5ecc314e61a..e0622c2e32f 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -39,6 +39,7 @@ These imports are necessary for all of the examples. # Avoids gridlines in generated docs #mpl.rcParams['axes.grid'] = False mpl.rcdefaults() + import matplotlib.pyplot as plt import xray diff --git a/xray/core/plotting.py b/xray/core/plotting.py index c6beec6168f..335c42f7cb5 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -3,6 +3,8 @@ the DataArray class """ +import functools + import numpy as np from .utils import is_uniform_spaced @@ -18,7 +20,7 @@ class FacetGrid(): pass -def _ensure_numeric(*args): +def _ensure_plottable(*args): """ Raise exception if there is anything in args that can't be plotted on an axis. @@ -111,7 +113,7 @@ def plot_line(darray, *args, **kwargs): xlabel, x = list(darray.indexes.items())[0] - _ensure_numeric([x]) + _ensure_plottable([x]) ax.plot(x, darray, *args, **kwargs) @@ -123,6 +125,44 @@ def plot_line(darray, *args, **kwargs): return ax +def _plot2d(plotfunc): + """ + Decorator for common 2d plotting logic. + + All 2d plots in xray will share the function signature below. + """ + @functools.wraps + def wrapped(darray, ax=None, add_colorbar=True, **kwargs): + + import matplotlib.pyplot as plt + + if ax is None: + ax = plt.gca() + + try: + ylab, xlab = darray.dims + except ValueError: + raise ValueError('{} plots are for 2 dimensional DataArrays. ' + 'Passed DataArray has {} dimensions' + .format(plotfunc.name, len(darray.dims))) + + x = darray[xlab] + y = darray[ylab] + + _ensure_plottable(x, y) + + ax = plotfunc(x, y, z, ax=ax, add_colorbar=add_colorbar, **kwargs): + + ax.set_xlabel(xlab) + ax.set_ylabel(ylab) + + if add_colorbar: + plt.colorbar(image, ax=ax) + + return ax + return wrapped + + def plot_imshow(darray, ax=None, add_colorbar=True, **kwargs): """ Image plot of 2d DataArray using matplotlib / pylab @@ -165,7 +205,7 @@ def plot_imshow(darray, ax=None, add_colorbar=True, **kwargs): x = darray[xlab] y = darray[ylab] - _ensure_numeric(x, y) + _ensure_plottable(x, y) # Centering the pixels- Assumes uniform spacing xstep = (x[1] - x[0]) / 2.0 @@ -226,7 +266,7 @@ def plot_contourf(darray, ax=None, add_colorbar=True, **kwargs): x = darray[xlab] y = darray[ylab] - _ensure_numeric(x, y) + _ensure_plottable(x, y) contours = ax.contourf(x, y, darray, **kwargs) From ced072947afdd217761da04b42724630e560694a Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 14 Jul 2015 08:28:43 -0700 Subject: [PATCH 49/61] beginning to use mixins for 2d plot tests --- doc/plotting.rst | 3 +- xray/core/plotting.py | 2 +- xray/test/test_plotting.py | 91 +++++++++++++++++++++----------------- 3 files changed, 52 insertions(+), 44 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index e0622c2e32f..8187912abc8 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -36,8 +36,7 @@ These imports are necessary for all of the examples. import numpy as np import matplotlib as mpl - # Avoids gridlines in generated docs - #mpl.rcParams['axes.grid'] = False + # Use defaults so we don't get gridlines in generated docs mpl.rcdefaults() import matplotlib.pyplot as plt diff --git a/xray/core/plotting.py b/xray/core/plotting.py index 335c42f7cb5..b935e8a103a 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -151,7 +151,7 @@ def wrapped(darray, ax=None, add_colorbar=True, **kwargs): _ensure_plottable(x, y) - ax = plotfunc(x, y, z, ax=ax, add_colorbar=add_colorbar, **kwargs): + ax = plotfunc(x, y, z, ax=ax, add_colorbar=add_colorbar, **kwargs) ax.set_xlabel(xlab) ax.set_ylabel(ylab) diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index c06a40b6eed..1b0d358660b 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -13,7 +13,6 @@ except ImportError: pass - # TODO - Add NaN handling and tests @requires_matplotlib @@ -42,19 +41,17 @@ def contourf_called(self, plotfunc): class TestPlot(PlotTestCase): def setUp(self): - d = np.arange(24).reshape(2, 3, 4) - self.darray = DataArray(d) + self.darray = DataArray(np.random.randn(2, 3, 4)) def test1d(self): - self.darray[0, 0, :].plot() + self.darray[:, 0, 0].plot() def test2d_uniform_calls_imshow(self): - a = self.darray[0, :, :] - self.assertTrue(self.imshow_called(a.plot)) + self.assertTrue(self.imshow_called(self.darray[:, :, 0].plot)) def test2d_nonuniform_calls_contourf(self): - a = self.darray[0, :, :] - a.coords['dim_1'] = [0, 10, 2] + a = self.darray[:, :, 0] + a.coords['dim_1'] = [2, 1, 89] self.assertTrue(self.contourf_called(a.plot)) def test3d(self): @@ -100,12 +97,53 @@ def test_nonnumeric_index_raises_typeerror(self): a.plot_line() -class TestPlot2D(PlotTestCase): +class TestPlotHistogram(PlotTestCase): def setUp(self): - self.darray = DataArray(np.random.randn(10, 15), - dims=['y', 'x']) + self.darray = DataArray(np.random.randn(2, 3, 4)) + + def test_3d_array(self): + self.darray.plot_hist() + + def test_title_no_name(self): + ax = self.darray.plot_hist() + self.assertEqual('', ax.get_title()) + + def test_title_uses_name(self): + self.darray.name = 'randompoints' + ax = self.darray.plot_hist() + self.assertIn(self.darray.name, ax.get_title()) + + def test_ylabel_is_count(self): + ax = self.darray.plot_hist() + self.assertEqual('Count', ax.get_ylabel()) + + def test_can_pass_in_kwargs(self): + nbins = 5 + ax = self.darray.plot_hist(bins=nbins) + self.assertEqual(nbins, len(ax.patches)) + def test_can_pass_in_axis(self): + self.pass_in_axis(self.darray.plot_hist) + +class Common2dMixin: + """ + Common tests for 2d plotting go here + """ + def test_label_names(self): + ax = self.plotfunc() + self.assertEqual('x', ax.get_xlabel()) + self.assertEqual('y', ax.get_ylabel()) + + +class TestContourf(Common2dMixin, PlotTestCase): + def setUp(self): + self.darray = DataArray(np.random.randn(3, 4), dims=['y', 'x']) + self.plotfunc = self.darray.plot_contourf + + +''' +class TestPlot2D(Common2dMixin, PlotTestCase): def test_contour_label_names(self): ax = self.darray.plot_contourf() self.assertEqual('x', ax.get_xlabel()) @@ -158,33 +196,4 @@ def test_default_aspect_is_auto(self): def test_can_change_aspect(self): ax = self.darray.plot_imshow(aspect='equal') self.assertEqual('equal', ax.get_aspect()) - - -class TestPlotHist(PlotTestCase): - - def setUp(self): - self.darray = DataArray(np.random.randn(2, 3, 4)) - - def test_3d_array(self): - self.darray.plot_hist() - - def test_title_no_name(self): - ax = self.darray.plot_hist() - self.assertEqual('', ax.get_title()) - - def test_title_uses_name(self): - self.darray.name = 'randompoints' - ax = self.darray.plot_hist() - self.assertIn(self.darray.name, ax.get_title()) - - def test_ylabel_is_count(self): - ax = self.darray.plot_hist() - self.assertEqual('Count', ax.get_ylabel()) - - def test_can_pass_in_kwargs(self): - nbins = 5 - ax = self.darray.plot_hist(bins=nbins) - self.assertEqual(nbins, len(ax.patches)) - - def test_can_pass_in_axis(self): - self.pass_in_axis(self.darray.plot_hist) +''' From cfeaeca6fe84a731a6f4334b02f5196037b6f14d Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 14 Jul 2015 09:27:49 -0700 Subject: [PATCH 50/61] Common2dMixin for plotting tests complete and working --- xray/test/test_plotting.py | 83 ++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index 1b0d358660b..dd900732857 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -1,6 +1,9 @@ +import functools import numpy as np from xray import DataArray +# Shouldn't need the core here +from xray.core.plotting import plot_imshow, plot_contourf from . import TestCase, requires_matplotlib @@ -22,18 +25,18 @@ def tearDown(self): # Remove all matplotlib figures plt.close('all') - def pass_in_axis(self, plotfunc): + def pass_in_axis(self, plotmethod): fig, axes = plt.subplots(ncols=2) - plotfunc(ax=axes[0]) + plotmethod(ax=axes[0]) self.assertTrue(axes[0].has_data()) - def imshow_called(self, plotfunc): - ax = plotfunc() + def imshow_called(self, plotmethod): + ax = plotmethod() images = ax.findobj(mpl.image.AxesImage) return len(images) > 0 - def contourf_called(self, plotfunc): - ax = plotfunc() + def contourf_called(self, plotmethod): + ax = plotmethod() paths = ax.findobj(mpl.collections.PathCollection) return len(paths) > 0 @@ -126,65 +129,66 @@ def test_can_pass_in_kwargs(self): def test_can_pass_in_axis(self): self.pass_in_axis(self.darray.plot_hist) + class Common2dMixin: """ - Common tests for 2d plotting go here + Common tests for 2d plotting go here. These tests assume that the + following attributes exist (define them in setUp): + + darray | 2 dimensional DataArray + plotfunc | plot as a function that takes DataArray as an arg + plotmethod | the method on DataArray """ def test_label_names(self): - ax = self.plotfunc() - self.assertEqual('x', ax.get_xlabel()) - self.assertEqual('y', ax.get_ylabel()) - - -class TestContourf(Common2dMixin, PlotTestCase): - def setUp(self): - self.darray = DataArray(np.random.randn(3, 4), dims=['y', 'x']) - self.plotfunc = self.darray.plot_contourf - - -''' -class TestPlot2D(Common2dMixin, PlotTestCase): - def test_contour_label_names(self): - ax = self.darray.plot_contourf() - self.assertEqual('x', ax.get_xlabel()) - self.assertEqual('y', ax.get_ylabel()) - - def test_imshow_label_names(self): - ax = self.darray.plot_imshow() + ax = self.plotmethod() self.assertEqual('x', ax.get_xlabel()) self.assertEqual('y', ax.get_ylabel()) def test_1d_raises_valueerror(self): with self.assertRaisesRegexp(ValueError, r'[Dd]im'): - self.darray[0, :].plot_imshow() + self.plotfunc(self.darray[0, :]) def test_3d_raises_valueerror(self): - da = DataArray(np.random.randn(2, 3, 4)) + a = DataArray(np.random.randn(2, 3, 4)) with self.assertRaisesRegexp(ValueError, r'[Dd]im'): - da.plot_imshow() + self.plotfunc(a) def test_nonnumeric_index_raises_typeerror(self): a = DataArray(np.random.randn(3, 2), coords=[['a', 'b', 'c'], ['d', 'e']]) with self.assertRaisesRegexp(TypeError, r'[Ii]ndex'): - a.plot_imshow() - a.plot_contourf() + self.plotfunc(a) def test_can_pass_in_axis(self): - self.pass_in_axis(self.darray.plot_imshow) - self.pass_in_axis(self.darray.plot_contourf) + self.pass_in_axis(self.plotmethod) - def test_imshow_called(self): - # Having both statements ensures the test works properly - self.assertFalse(self.imshow_called(self.darray.plot_contourf)) - self.assertTrue(self.imshow_called(self.darray.plot_imshow)) + +class TestContourf(Common2dMixin, PlotTestCase): + + def setUp(self): + self.darray = DataArray(np.random.randn(3, 4), dims=['y', 'x']) + self.plotfunc = plot_contourf + self.plotmethod = getattr(self.darray, self.plotfunc.__name__) def test_contourf_called(self): # Having both statements ensures the test works properly self.assertFalse(self.contourf_called(self.darray.plot_imshow)) self.assertTrue(self.contourf_called(self.darray.plot_contourf)) - def test_imshow_xy_pixel_centered(self): + +class TestImshow(Common2dMixin, PlotTestCase): + + def setUp(self): + self.darray = DataArray(np.random.randn(10, 15), dims=['y', 'x']) + self.plotfunc = plot_imshow + self.plotmethod = getattr(self.darray, self.plotfunc.__name__) + + def test_imshow_called(self): + # Having both statements ensures the test works properly + self.assertFalse(self.imshow_called(self.darray.plot_contourf)) + self.assertTrue(self.imshow_called(self.darray.plot_imshow)) + + def test_xy_pixel_centered(self): ax = self.darray.plot_imshow() self.assertTrue(np.allclose([-0.5, 14.5], ax.get_xlim())) self.assertTrue(np.allclose([9.5, -0.5], ax.get_ylim())) @@ -196,4 +200,3 @@ def test_default_aspect_is_auto(self): def test_can_change_aspect(self): ax = self.darray.plot_imshow(aspect='equal') self.assertEqual('equal', ax.get_aspect()) -''' From 2dc914d3a1acd39d5341e063ebb85eb945fcf2a6 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 14 Jul 2015 11:45:01 -0700 Subject: [PATCH 51/61] 2d plotting decorator works for imshow --- xray/core/plotting.py | 63 +++++++++++++++++++++++++++++++++----- xray/test/test_plotting.py | 3 +- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/xray/core/plotting.py b/xray/core/plotting.py index b935e8a103a..c53ab6fb06d 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -131,8 +131,8 @@ def _plot2d(plotfunc): All 2d plots in xray will share the function signature below. """ - @functools.wraps - def wrapped(darray, ax=None, add_colorbar=True, **kwargs): + @functools.wraps(plotfunc) + def wrapper(darray, ax=None, xincrease=None, yincrease=None, add_colorbar=True, **kwargs): import matplotlib.pyplot as plt @@ -144,26 +144,74 @@ def wrapped(darray, ax=None, add_colorbar=True, **kwargs): except ValueError: raise ValueError('{} plots are for 2 dimensional DataArrays. ' 'Passed DataArray has {} dimensions' - .format(plotfunc.name, len(darray.dims))) + .format(plotfunc.__name__, len(darray.dims))) x = darray[xlab] y = darray[ylab] + z = darray _ensure_plottable(x, y) - ax = plotfunc(x, y, z, ax=ax, add_colorbar=add_colorbar, **kwargs) + ax, cmap = plotfunc(x, y, z, ax=ax, **kwargs) ax.set_xlabel(xlab) ax.set_ylabel(ylab) if add_colorbar: - plt.colorbar(image, ax=ax) + plt.colorbar(cmap, ax=ax) return ax - return wrapped + return wrapper -def plot_imshow(darray, ax=None, add_colorbar=True, **kwargs): +@_plot2d +def plot_imshow(x, y, z, ax, **kwargs): + """ + Image plot of 2d DataArray using matplotlib / pylab + + Wraps matplotlib.pyplot.imshow + + ..warning:: + This function needs uniformly spaced coordinates to + properly label the axes. Call DataArray.plot() to check. + + Parameters + ---------- + darray : DataArray + Must be 2 dimensional + ax : matplotlib axes object + If None, uses the current axis + add_colorbar : Boolean + Adds colorbar to axis + kwargs : + Additional arguments to matplotlib.pyplot.imshow + + Details + ------- + The pixels are centered on the coordinates values. Ie, if the coordinate + value is 3.2 then the pixels for those coordinates will be centered on 3.2. + + """ + # Centering the pixels- Assumes uniform spacing + xstep = (x[1] - x[0]) / 2.0 + ystep = (y[1] - y[0]) / 2.0 + left, right = x[0] - xstep, x[-1] + xstep + bottom, top = y[-1] + ystep, y[0] - ystep + + defaults = {'extent': [left, right, bottom, top], + 'aspect': 'auto', + 'interpolation': 'nearest', + } + + # Allow user to override these defaults + defaults.update(kwargs) + + cmap = ax.imshow(z, **defaults) + + return ax, cmap + + +def old_plot_imshow(darray, ax=None, add_colorbar=True, **kwargs): """ Image plot of 2d DataArray using matplotlib / pylab @@ -234,6 +282,7 @@ def plot_imshow(darray, ax=None, add_colorbar=True, **kwargs): # TODO - Could refactor this to avoid duplicating plot_imshow logic. # There's also some similar tests for the two. +#def old_plot_contourf(darray, ax=None, add_colorbar=True, **kwargs): def plot_contourf(darray, ax=None, add_colorbar=True, **kwargs): """ Filled contour plot of 2d DataArray diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index dd900732857..24df12a913f 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -1,8 +1,7 @@ -import functools import numpy as np from xray import DataArray -# Shouldn't need the core here +# Shouldn't need the core here? from xray.core.plotting import plot_imshow, plot_contourf from . import TestCase, requires_matplotlib From 6ba054c0b85d0675b963f03faf5192ded6e13e30 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 14 Jul 2015 12:28:17 -0700 Subject: [PATCH 52/61] _plot2d decorator working well. Deleting old functions --- xray/core/plotting.py | 49 +++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/xray/core/plotting.py b/xray/core/plotting.py index c53ab6fb06d..92876384db7 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -128,11 +128,28 @@ def plot_line(darray, *args, **kwargs): def _plot2d(plotfunc): """ Decorator for common 2d plotting logic. - - All 2d plots in xray will share the function signature below. """ + commondoc = ''' + Parameters + ---------- + darray : DataArray + Must be 2 dimensional + ax : matplotlib axes object + If None, uses the current axis + xincrease : None, True, or False + If None, uses + add_colorbar : Boolean + Adds colorbar to axis + kwargs : + Additional arguments to wrapped matplotlib function + ''' + + # Build on the original docstring + plotfunc.__doc__ = '\n'.join((plotfunc.__doc__, commondoc)) + @functools.wraps(plotfunc) def wrapper(darray, ax=None, xincrease=None, yincrease=None, add_colorbar=True, **kwargs): + # All 2d plots in xray share this function signature import matplotlib.pyplot as plt @@ -175,22 +192,8 @@ def plot_imshow(x, y, z, ax, **kwargs): This function needs uniformly spaced coordinates to properly label the axes. Call DataArray.plot() to check. - Parameters - ---------- - darray : DataArray - Must be 2 dimensional - ax : matplotlib axes object - If None, uses the current axis - add_colorbar : Boolean - Adds colorbar to axis - kwargs : - Additional arguments to matplotlib.pyplot.imshow - - Details - ------- The pixels are centered on the coordinates values. Ie, if the coordinate value is 3.2 then the pixels for those coordinates will be centered on 3.2. - """ # Centering the pixels- Assumes uniform spacing xstep = (x[1] - x[0]) / 2.0 @@ -211,6 +214,17 @@ def plot_imshow(x, y, z, ax, **kwargs): return ax, cmap +@_plot2d +def plot_contourf(x, y, z, ax, **kwargs): + """ + Filled contour plot of 2d DataArray + + Wraps matplotlib.pyplot.contourf + """ + cmap = ax.contourf(x, y, z, **kwargs) + return ax, cmap + + def old_plot_imshow(darray, ax=None, add_colorbar=True, **kwargs): """ Image plot of 2d DataArray using matplotlib / pylab @@ -282,8 +296,7 @@ def old_plot_imshow(darray, ax=None, add_colorbar=True, **kwargs): # TODO - Could refactor this to avoid duplicating plot_imshow logic. # There's also some similar tests for the two. -#def old_plot_contourf(darray, ax=None, add_colorbar=True, **kwargs): -def plot_contourf(darray, ax=None, add_colorbar=True, **kwargs): +def old_plot_contourf(darray, ax=None, add_colorbar=True, **kwargs): """ Filled contour plot of 2d DataArray From f65bbfdbc93f20b46c90a7be3e453e879ec1d668 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 14 Jul 2015 15:27:05 -0700 Subject: [PATCH 53/61] use super to finish 2d plot setup in test mixin --- doc/plotting.rst | 24 +---- xray/core/plotting.py | 216 +++++++++++-------------------------- xray/test/test_plotting.py | 24 ++++- 3 files changed, 90 insertions(+), 174 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 8187912abc8..2fb3d1b8780 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -162,16 +162,15 @@ the pixels are centered over their coordinates, and the axis labels and ranges correspond to the values of the coordinates. -An `extended slice `__ -can be used to reverse the order of the rows, producing a +All 2d plots in xray allow the use of the keyword arguments ``yincrease=True`` +to produce a more conventional plot where the coordinates increase in the y axis. +``xincrease`` works similarly. .. ipython:: python - a[::-1, :] - - @savefig plotting_example_2d_simple_reversed.png width=4in - a[::-1, :].plot() + @savefig 2d_simple_yincrease.png width=4in + a.plot(yincrease=True) Simulated Data ~~~~~~~~~~~~~~ @@ -209,19 +208,6 @@ transpose the array. @savefig plotting_changing_axes.png width=4in distance.T.plot() -TODO: Feedback here please. This requires the user to put the array into -the order they want for plotting. To plot with sorted coordinates they -would have to write something -like this: ``distance.T[::-1, ::-1].plot()``. -This requires the user to be aware of how the array is organized. - -Alternatively, this could be implemented in -xray plotting as: ``distance.plot(xvar='y', sortx=True, -sorty=True)``. -This allows the use of the dimension -name to describe which coordinate should appear as the x variable on the -plot, and is probably more convenient. - Nonuniform Coordinates ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/xray/core/plotting.py b/xray/core/plotting.py index 92876384db7..50f0bad49a8 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -125,6 +125,59 @@ def plot_line(darray, *args, **kwargs): return ax +def plot_hist(darray, ax=None, **kwargs): + """ + Histogram of DataArray + + Wraps matplotlib.pyplot.hist + + Plots N dimensional arrays by first flattening the array. + + Parameters + ---------- + darray : DataArray + Can be any dimension + ax : matplotlib axes object + If not passed, uses the current axis + kwargs : + Additional keyword arguments to matplotlib.pyplot.hist + + """ + import matplotlib.pyplot as plt + + if ax is None: + ax = plt.gca() + + ax.hist(np.ravel(darray), **kwargs) + + ax.set_ylabel('Count') + + if darray.name is not None: + ax.set_title('Histogram of {}'.format(darray.name)) + + return ax + + +def _update_axes_limits(ax, xincrease, yincrease): + """ + Update axes in place to increase or decrease + For use in _plot2d + """ + if xincrease is None: + pass + elif xincrease: + ax.set_xlim(sorted(ax.get_xlim())) + elif not xincrease: + ax.set_xlim(sorted(ax.get_xlim(), reverse=True)) + + if yincrease is None: + pass + elif yincrease: + ax.set_ylim(sorted(ax.get_ylim())) + elif not yincrease: + ax.set_ylim(sorted(ax.get_ylim(), reverse=True)) + + def _plot2d(plotfunc): """ Decorator for common 2d plotting logic. @@ -136,12 +189,20 @@ def _plot2d(plotfunc): Must be 2 dimensional ax : matplotlib axes object If None, uses the current axis - xincrease : None, True, or False - If None, uses + xincrease : None (default), True, or False + Should the values on the x axes be increasing from left to right? + if None, use the default for the matplotlib function + yincrease : None (default), True, or False + Should the values on the y axes be increasing from top to bottom? + if None, use the default for the matplotlib function add_colorbar : Boolean Adds colorbar to axis kwargs : Additional arguments to wrapped matplotlib function + + Returns + ------- + ax : plotted matplotlib axis object ''' # Build on the original docstring @@ -177,6 +238,8 @@ def wrapper(darray, ax=None, xincrease=None, yincrease=None, add_colorbar=True, if add_colorbar: plt.colorbar(cmap, ax=ax) + _update_axes_limits(ax, xincrease, yincrease) + return ax return wrapper @@ -223,152 +286,3 @@ def plot_contourf(x, y, z, ax, **kwargs): """ cmap = ax.contourf(x, y, z, **kwargs) return ax, cmap - - -def old_plot_imshow(darray, ax=None, add_colorbar=True, **kwargs): - """ - Image plot of 2d DataArray using matplotlib / pylab - - Wraps matplotlib.pyplot.imshow - -..warning:: - This function needs uniformly spaced coordinates to - properly label the axes. Call DataArray.plot() to check. - - Parameters - ---------- - darray : DataArray - Must be 2 dimensional - ax : matplotlib axes object - If None, uses the current axis - add_colorbar : Boolean - Adds colorbar to axis - kwargs : - Additional arguments to matplotlib.pyplot.imshow - - Details - ------- - The pixels are centered on the coordinates values. Ie, if the coordinate - value is 3.2 then the pixels for those coordinates will be centered on 3.2. - - """ - import matplotlib.pyplot as plt - - if ax is None: - ax = plt.gca() - - try: - ylab, xlab = darray.dims - except ValueError: - raise ValueError('Image plots are for 2 dimensional DataArrays. ' - 'Passed DataArray has {} dimensions' - .format(len(darray.dims))) - - x = darray[xlab] - y = darray[ylab] - - _ensure_plottable(x, y) - - # Centering the pixels- Assumes uniform spacing - xstep = (x[1] - x[0]) / 2.0 - ystep = (y[1] - y[0]) / 2.0 - left, right = x[0] - xstep, x[-1] + xstep - bottom, top = y[-1] + ystep, y[0] - ystep - - defaults = {'extent': [left, right, bottom, top], - 'aspect': 'auto', - 'interpolation': 'nearest', - } - - # Allow user to override these defaults - defaults.update(kwargs) - - image = ax.imshow(darray, **defaults) - - ax.set_xlabel(xlab) - ax.set_ylabel(ylab) - - if add_colorbar: - plt.colorbar(image, ax=ax) - - return ax - - -# TODO - Could refactor this to avoid duplicating plot_imshow logic. -# There's also some similar tests for the two. -def old_plot_contourf(darray, ax=None, add_colorbar=True, **kwargs): - """ - Filled contour plot of 2d DataArray - - Wraps matplotlib.pyplot.contourf - - Parameters - ---------- - darray : DataArray - Must be 2 dimensional - ax : matplotlib axes object - If None, uses the current axis - add_colorbar : Boolean - Adds colorbar to axis - kwargs : - Additional arguments to matplotlib.pyplot.imshow - - """ - import matplotlib.pyplot as plt - - if ax is None: - ax = plt.gca() - - try: - ylab, xlab = darray.dims - except ValueError: - raise ValueError('Contour plots are for 2 dimensional DataArrays. ' - 'Passed DataArray has {} dimensions' - .format(len(darray.dims))) - - x = darray[xlab] - y = darray[ylab] - _ensure_plottable(x, y) - - contours = ax.contourf(x, y, darray, **kwargs) - - ax.set_xlabel(xlab) - ax.set_ylabel(ylab) - - if add_colorbar: - plt.colorbar(contours, ax=ax) - - return ax - - -def plot_hist(darray, ax=None, **kwargs): - """ - Histogram of DataArray - - Wraps matplotlib.pyplot.hist - - Plots N dimensional arrays by first flattening the array. - - Parameters - ---------- - darray : DataArray - Can be any dimension - ax : matplotlib axes object - If not passed, uses the current axis - kwargs : - Additional keyword arguments to matplotlib.pyplot.hist - - """ - import matplotlib.pyplot as plt - - if ax is None: - ax = plt.gca() - - ax.hist(np.ravel(darray), **kwargs) - - ax.set_ylabel('Count') - - if darray.name is not None: - ax.set_title('Histogram of {}'.format(darray.name)) - - return ax diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index 24df12a913f..5ab1ab3ed13 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -138,6 +138,10 @@ class Common2dMixin: plotfunc | plot as a function that takes DataArray as an arg plotmethod | the method on DataArray """ + def setUp(self): + self.darray = DataArray(np.random.randn(10, 15), dims=['y', 'x']) + self.plotmethod = getattr(self.darray, self.plotfunc.__name__) + def test_label_names(self): ax = self.plotmethod() self.assertEqual('x', ax.get_xlabel()) @@ -161,13 +165,26 @@ def test_nonnumeric_index_raises_typeerror(self): def test_can_pass_in_axis(self): self.pass_in_axis(self.plotmethod) + def test_xyincrease_false_changes_axes(self): + ax = self.plotmethod(xincrease=False, yincrease=False) + xlim = ax.get_xlim() + ylim = ax.get_ylim() + diffs = xlim[0] - 14, xlim[1] - 0, ylim[0] - 9, ylim[1] - 0 + self.assertTrue(all(abs(x) < 1 for x in diffs)) + + def test_xyincrease_true_changes_axes(self): + ax = self.plotmethod(xincrease=True, yincrease=True) + xlim = ax.get_xlim() + ylim = ax.get_ylim() + diffs = xlim[0] - 0, xlim[1] - 14, ylim[0] - 0, ylim[1] - 9 + self.assertTrue(all(abs(x) < 1 for x in diffs)) + class TestContourf(Common2dMixin, PlotTestCase): def setUp(self): - self.darray = DataArray(np.random.randn(3, 4), dims=['y', 'x']) self.plotfunc = plot_contourf - self.plotmethod = getattr(self.darray, self.plotfunc.__name__) + super(TestContourf, self).setUp() def test_contourf_called(self): # Having both statements ensures the test works properly @@ -178,9 +195,8 @@ def test_contourf_called(self): class TestImshow(Common2dMixin, PlotTestCase): def setUp(self): - self.darray = DataArray(np.random.randn(10, 15), dims=['y', 'x']) self.plotfunc = plot_imshow - self.plotmethod = getattr(self.darray, self.plotfunc.__name__) + super(TestImshow, self).setUp() def test_imshow_called(self): # Having both statements ensures the test works properly From 77c19dddc70f72ff409f318bc0dbb0c4dd1b6752 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Tue, 14 Jul 2015 15:37:01 -0700 Subject: [PATCH 54/61] updated transpose example plot with xincrease --- doc/plotting.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 2fb3d1b8780..bc5d3637840 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -160,7 +160,7 @@ It may seem strange that the the values on the y axis are decreasing with -0.5 on the top. This is because the pixels are centered over their coordinates, and the axis labels and ranges correspond to the values of the -coordinates. +coordinates. All 2d plots in xray allow the use of the keyword arguments ``yincrease=True`` to produce a @@ -208,6 +208,13 @@ transpose the array. @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) + Nonuniform Coordinates ~~~~~~~~~~~~~~~~~~~~~~ From 78f6804d614d57530a9e716d88e05c634b5885b2 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 16 Jul 2015 11:21:18 -0700 Subject: [PATCH 55/61] tests no longer require to assign axes --- xray/test/test_plotting.py | 68 +++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index 5ab1ab3ed13..e3c2a263c79 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -30,13 +30,13 @@ def pass_in_axis(self, plotmethod): self.assertTrue(axes[0].has_data()) def imshow_called(self, plotmethod): - ax = plotmethod() - images = ax.findobj(mpl.image.AxesImage) + plotmethod() + images = plt.gca().findobj(mpl.image.AxesImage) return len(images) > 0 def contourf_called(self, plotmethod): - ax = plotmethod() - paths = ax.findobj(mpl.collections.PathCollection) + plotmethod() + paths = plt.gca().findobj(mpl.collections.PathCollection) return len(paths) > 0 @@ -70,17 +70,17 @@ def setUp(self): self.darray = DataArray(d, coords={'period': range(len(d))}) def test_xlabel_is_index_name(self): - ax = self.darray.plot() - self.assertEqual('period', ax.get_xlabel()) + self.darray.plot() + self.assertEqual('period', plt.gca().get_xlabel()) def test_no_label_name_on_y_axis(self): - ax = self.darray.plot() - self.assertEqual('', ax.get_ylabel()) + self.darray.plot() + self.assertEqual('', plt.gca().get_ylabel()) def test_ylabel_is_data_name(self): self.darray.name = 'temperature' - ax = self.darray.plot() - self.assertEqual(self.darray.name, ax.get_ylabel()) + self.darray.plot() + self.assertEqual(self.darray.name, plt.gca().get_ylabel()) def test_wrong_dims_raises_valueerror(self): twodims = DataArray(np.arange(10).reshape(2, 5)) @@ -108,22 +108,22 @@ def test_3d_array(self): self.darray.plot_hist() def test_title_no_name(self): - ax = self.darray.plot_hist() - self.assertEqual('', ax.get_title()) + self.darray.plot_hist() + self.assertEqual('', plt.gca().get_title()) def test_title_uses_name(self): self.darray.name = 'randompoints' - ax = self.darray.plot_hist() - self.assertIn(self.darray.name, ax.get_title()) + self.darray.plot_hist() + self.assertIn(self.darray.name, plt.gca().get_title()) def test_ylabel_is_count(self): - ax = self.darray.plot_hist() - self.assertEqual('Count', ax.get_ylabel()) + self.darray.plot_hist() + self.assertEqual('Count', plt.gca().get_ylabel()) def test_can_pass_in_kwargs(self): nbins = 5 - ax = self.darray.plot_hist(bins=nbins) - self.assertEqual(nbins, len(ax.patches)) + self.darray.plot_hist(bins=nbins) + self.assertEqual(nbins, len(plt.gca().patches)) def test_can_pass_in_axis(self): self.pass_in_axis(self.darray.plot_hist) @@ -143,9 +143,9 @@ def setUp(self): self.plotmethod = getattr(self.darray, self.plotfunc.__name__) def test_label_names(self): - ax = self.plotmethod() - self.assertEqual('x', ax.get_xlabel()) - self.assertEqual('y', ax.get_ylabel()) + self.plotmethod() + self.assertEqual('x', plt.gca().get_xlabel()) + self.assertEqual('y', plt.gca().get_ylabel()) def test_1d_raises_valueerror(self): with self.assertRaisesRegexp(ValueError, r'[Dd]im'): @@ -166,16 +166,16 @@ def test_can_pass_in_axis(self): self.pass_in_axis(self.plotmethod) def test_xyincrease_false_changes_axes(self): - ax = self.plotmethod(xincrease=False, yincrease=False) - xlim = ax.get_xlim() - ylim = ax.get_ylim() + self.plotmethod(xincrease=False, yincrease=False) + xlim = plt.gca().get_xlim() + ylim = plt.gca().get_ylim() diffs = xlim[0] - 14, xlim[1] - 0, ylim[0] - 9, ylim[1] - 0 self.assertTrue(all(abs(x) < 1 for x in diffs)) def test_xyincrease_true_changes_axes(self): - ax = self.plotmethod(xincrease=True, yincrease=True) - xlim = ax.get_xlim() - ylim = ax.get_ylim() + self.plotmethod(xincrease=True, yincrease=True) + xlim = plt.gca().get_xlim() + ylim = plt.gca().get_ylim() diffs = xlim[0] - 0, xlim[1] - 14, ylim[0] - 0, ylim[1] - 9 self.assertTrue(all(abs(x) < 1 for x in diffs)) @@ -204,14 +204,14 @@ def test_imshow_called(self): self.assertTrue(self.imshow_called(self.darray.plot_imshow)) def test_xy_pixel_centered(self): - ax = self.darray.plot_imshow() - self.assertTrue(np.allclose([-0.5, 14.5], ax.get_xlim())) - self.assertTrue(np.allclose([9.5, -0.5], ax.get_ylim())) + self.darray.plot_imshow() + self.assertTrue(np.allclose([-0.5, 14.5], plt.gca().get_xlim())) + self.assertTrue(np.allclose([9.5, -0.5], plt.gca().get_ylim())) def test_default_aspect_is_auto(self): - ax = self.darray.plot_imshow() - self.assertEqual('auto', ax.get_aspect()) + self.darray.plot_imshow() + self.assertEqual('auto', plt.gca().get_aspect()) def test_can_change_aspect(self): - ax = self.darray.plot_imshow(aspect='equal') - self.assertEqual('equal', ax.get_aspect()) + self.darray.plot_imshow(aspect='equal') + self.assertEqual('equal', plt.gca().get_aspect()) From 7d67344a18dace75b0decac3585754f3a65734bf Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 16 Jul 2015 12:03:46 -0700 Subject: [PATCH 56/61] breaking tests for returning primitives --- doc/plotting.rst | 6 +----- xray/core/plotting.py | 5 +++-- xray/test/test_plotting.py | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index bc5d3637840..a5514816484 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -255,10 +255,6 @@ Suppose we want two plots to share the same color scale. This can be achieved by passing in the appropriate arguments and adding the color bar later. -TODO: All xray plot methods return axes for consistency- is this necessary? -Now to make this particular plot we need to access ``.images[0]`` to get -the color mapping. - .. ipython:: python fig, axes = plt.subplots(ncols=2) @@ -270,7 +266,7 @@ the color mapping. halfd = distance / 2 im = halfd.plot(ax=axes[1], **kwargs) - plt.colorbar(im.images[0], ax=axes.tolist()) + plt.colorbar(im, ax=axes.tolist()) @savefig plotting_same_color_scale.png width=6in plt.show() diff --git a/xray/core/plotting.py b/xray/core/plotting.py index 50f0bad49a8..c3c5e933124 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -230,16 +230,17 @@ def wrapper(darray, ax=None, xincrease=None, yincrease=None, add_colorbar=True, _ensure_plottable(x, y) - ax, cmap = plotfunc(x, y, z, ax=ax, **kwargs) + ax, primitive = plotfunc(x, y, z, ax=ax, **kwargs) ax.set_xlabel(xlab) ax.set_ylabel(ylab) if add_colorbar: - plt.colorbar(cmap, ax=ax) + plt.colorbar(primitive, ax=ax) _update_axes_limits(ax, xincrease, yincrease) + #return primitive return ax return wrapper diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index e3c2a263c79..61c7eebb2ff 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -98,6 +98,10 @@ def test_nonnumeric_index_raises_typeerror(self): with self.assertRaisesRegexp(TypeError, r'[Ii]ndex'): a.plot_line() + def test_primitive_returned(self): + p = self.darray.plot_line() + self.assertTrue(isinstance(p, mpl.lines.Line2D)) + class TestPlotHistogram(PlotTestCase): @@ -128,6 +132,10 @@ def test_can_pass_in_kwargs(self): def test_can_pass_in_axis(self): self.pass_in_axis(self.darray.plot_hist) + def test_primitive_returned(self): + h = self.darray.plot_hist() + self.assertTrue(isinstance(h[-1][0], plt.patches.Rectangle)) + class Common2dMixin: """ @@ -191,6 +199,10 @@ def test_contourf_called(self): self.assertFalse(self.contourf_called(self.darray.plot_imshow)) self.assertTrue(self.contourf_called(self.darray.plot_contourf)) + def test_primitive_artist_returned(self): + artist = self.plotmethod() + self.assertTrue(isinstance(artist, mpl.contour.QuadContourSet)) + class TestImshow(Common2dMixin, PlotTestCase): @@ -215,3 +227,7 @@ def test_default_aspect_is_auto(self): def test_can_change_aspect(self): self.darray.plot_imshow(aspect='equal') self.assertEqual('equal', plt.gca().get_aspect()) + + def test_primitive_artist_returned(self): + artist = self.plotmethod() + self.assertTrue(isinstance(artist, mpl.image.AxesImage)) From fd0cad20ff95b3f64710b3565abce5cbf1174bac Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 16 Jul 2015 12:12:27 -0700 Subject: [PATCH 57/61] tests for primitive objects are passing --- xray/core/plotting.py | 19 +++++++++---------- xray/test/test_plotting.py | 12 +++++------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/xray/core/plotting.py b/xray/core/plotting.py index c3c5e933124..6321e09c2bd 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -115,14 +115,14 @@ def plot_line(darray, *args, **kwargs): _ensure_plottable([x]) - ax.plot(x, darray, *args, **kwargs) + primitive = ax.plot(x, darray, *args, **kwargs) ax.set_xlabel(xlabel) if darray.name is not None: ax.set_ylabel(darray.name) - return ax + return primitive def plot_hist(darray, ax=None, **kwargs): @@ -148,14 +148,14 @@ def plot_hist(darray, ax=None, **kwargs): if ax is None: ax = plt.gca() - ax.hist(np.ravel(darray), **kwargs) + primitive = ax.hist(np.ravel(darray), **kwargs) ax.set_ylabel('Count') if darray.name is not None: ax.set_title('Histogram of {}'.format(darray.name)) - return ax + return primitive def _update_axes_limits(ax, xincrease, yincrease): @@ -240,8 +240,7 @@ def wrapper(darray, ax=None, xincrease=None, yincrease=None, add_colorbar=True, _update_axes_limits(ax, xincrease, yincrease) - #return primitive - return ax + return primitive return wrapper @@ -273,9 +272,9 @@ def plot_imshow(x, y, z, ax, **kwargs): # Allow user to override these defaults defaults.update(kwargs) - cmap = ax.imshow(z, **defaults) + primitive = ax.imshow(z, **defaults) - return ax, cmap + return ax, primitive @_plot2d @@ -285,5 +284,5 @@ def plot_contourf(x, y, z, ax, **kwargs): Wraps matplotlib.pyplot.contourf """ - cmap = ax.contourf(x, y, z, **kwargs) - return ax, cmap + primitive = ax.contourf(x, y, z, **kwargs) + return ax, primitive diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index 61c7eebb2ff..8ce260ea7a3 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -100,7 +100,7 @@ def test_nonnumeric_index_raises_typeerror(self): def test_primitive_returned(self): p = self.darray.plot_line() - self.assertTrue(isinstance(p, mpl.lines.Line2D)) + self.assertTrue(isinstance(p[0], mpl.lines.Line2D)) class TestPlotHistogram(PlotTestCase): @@ -134,17 +134,15 @@ def test_can_pass_in_axis(self): def test_primitive_returned(self): h = self.darray.plot_hist() - self.assertTrue(isinstance(h[-1][0], plt.patches.Rectangle)) + self.assertTrue(isinstance(h[-1][0], mpl.patches.Rectangle)) class Common2dMixin: """ - Common tests for 2d plotting go here. These tests assume that the - following attributes exist (define them in setUp): + Common tests for 2d plotting go here. - darray | 2 dimensional DataArray - plotfunc | plot as a function that takes DataArray as an arg - plotmethod | the method on DataArray + These tests assume that `self.plotfunc` exists and is defined in the + setUp. Should have the same name as the method. """ def setUp(self): self.darray = DataArray(np.random.randn(10, 15), dims=['y', 'x']) From 7fc9707affff332168eeed4dc792695010194501 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 16 Jul 2015 13:05:09 -0700 Subject: [PATCH 58/61] docs and tests for np.nans --- doc/plotting.rst | 57 ++++++++++++++++++++++++++++++++++++-- xray/core/plotting.py | 4 ++- xray/test/test_plotting.py | 4 +++ 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index a5514816484..0af7524ebd6 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -172,6 +172,21 @@ more conventional plot where the coordinates increase in the y axis. @savefig 2d_simple_yincrease.png width=4in a.plot(yincrease=True) +Missing Values +~~~~~~~~~~~~~~ + +Xray plots data with missing values. +Xray uses ``np.nan`` for missing values. +TODO link. + +.. ipython:: python + + # This data has holes in it! + a[1, 1] = np.nan + + @savefig plotting_missing_values.png width=6in + a.plot() + Simulated Data ~~~~~~~~~~~~~~ @@ -238,16 +253,52 @@ Calling Matplotlib ~~~~~~~~~~~~~~~~~~ Since this is a thin wrapper around matplotlib, all the functionality of -matplotlib is available. For example, use a different color map and add a title. +matplotlib is available. .. ipython:: python d_ylog.plot(cmap=plt.cm.Blues) plt.title('Euclidean distance from point to origin') + plt.xlabel('temperature (C)') @savefig plotting_2d_call_matplotlib.png width=4in plt.show() +.. warning:: + + Xray methods update label information and generally play around with the + axes. So any kind of updates to the plot + should be done *after* the call to the xray's plot. + In the example below, ``plt.xlabel`` effectively does nothing, since + ``d_ylog.plot()`` updates the xlabel. + +.. ipython:: python + + plt.xlabel('temperature (C)') + d_ylog.plot() + + @savefig plotting_2d_call_matplotlib2.png width=4in + plt.show() + +Contour plots can have missing values also. + +.. ipython:: python + + d_ylog[30:48, 10:30] = np.nan + + d_ylog.plot() + + plt.text(100, 600, 'So common...') + + @savefig plotting_nonuniform_coords_missing.png width=4in + plt.show() + +Return Values +~~~~~~~~~~~~~ + +Xray's plotting functions all return the same objects that the equivalent +matplotlib functions return. + Colormaps ~~~~~~~~~ @@ -261,10 +312,10 @@ later. kwargs = {'cmap': plt.cm.Blues, 'vmin': distance.min(), 'vmax': distance.max(), 'add_colorbar': False} - distance.plot(ax=axes[0], **kwargs) + im = distance.plot(ax=axes[0], **kwargs) halfd = distance / 2 - im = halfd.plot(ax=axes[1], **kwargs) + halfd.plot(ax=axes[1], **kwargs) plt.colorbar(im, ax=axes.tolist()) diff --git a/xray/core/plotting.py b/xray/core/plotting.py index 6321e09c2bd..4ed99c3aed5 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -202,7 +202,9 @@ def _plot2d(plotfunc): Returns ------- - ax : plotted matplotlib axis object + artist : + The same type of primitive artist that the wrapped matplotlib + function returns ''' # Build on the original docstring diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index 8ce260ea7a3..dcaa4e46105 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -185,6 +185,10 @@ def test_xyincrease_true_changes_axes(self): diffs = xlim[0] - 0, xlim[1] - 14, ylim[0] - 0, ylim[1] - 9 self.assertTrue(all(abs(x) < 1 for x in diffs)) + def test_plot_nans(self): + self.darray[0, 0] = np.nan + self.plotmethod() + class TestContourf(Common2dMixin, PlotTestCase): From 021f81f78bf6c11bca0cca8e67d360977b4812a5 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 16 Jul 2015 13:29:51 -0700 Subject: [PATCH 59/61] nans work for all plotting methods now --- xray/core/plotting.py | 4 +++- xray/test/test_plotting.py | 10 +++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/xray/core/plotting.py b/xray/core/plotting.py index 4ed99c3aed5..def48256a38 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -148,7 +148,9 @@ def plot_hist(darray, ax=None, **kwargs): if ax is None: ax = plt.gca() - primitive = ax.hist(np.ravel(darray), **kwargs) + no_nan = np.ravel(darray) + no_nan = no_nan[np.logical_not(np.isnan(no_nan))] + primitive = ax.hist(no_nan, **kwargs) ax.set_ylabel('Count') diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index dcaa4e46105..3410bbb6b9a 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -66,7 +66,7 @@ def test_can_pass_in_axis(self): class TestPlot1D(PlotTestCase): def setUp(self): - d = [0, 1, 0, 2] + d = [0, 1.1, 0, 2] self.darray = DataArray(d, coords={'period': range(len(d))}) def test_xlabel_is_index_name(self): @@ -102,6 +102,10 @@ def test_primitive_returned(self): p = self.darray.plot_line() self.assertTrue(isinstance(p[0], mpl.lines.Line2D)) + def test_plot_nans(self): + self.darray[1] = np.nan + self.darray.plot_line() + class TestPlotHistogram(PlotTestCase): @@ -136,6 +140,10 @@ def test_primitive_returned(self): h = self.darray.plot_hist() self.assertTrue(isinstance(h[-1][0], mpl.patches.Rectangle)) + def test_plot_nans(self): + self.darray[0, 0, :] = np.nan + self.darray.plot_hist() + class Common2dMixin: """ From 28047e43a1fa875d6f88e7b6faaa7c1ced6dc26f Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Thu, 16 Jul 2015 13:42:16 -0700 Subject: [PATCH 60/61] some pep8 fixes and reorganization --- xray/core/plotting.py | 25 ++++++++++++++----------- xray/test/test_plotting.py | 7 +++---- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/xray/core/plotting.py b/xray/core/plotting.py index def48256a38..a4d4c17da29 100644 --- a/xray/core/plotting.py +++ b/xray/core/plotting.py @@ -9,17 +9,20 @@ from .utils import is_uniform_spaced -# TODO - Is there a better way to import matplotlib in the function? -# Other piece of duplicated logic is the checking for axes. -# Decorators don't preserve the argument names -# But if all the plotting methods have same signature... - # TODO - implement this class FacetGrid(): pass +# Maybe more appropriate to keep this in .utils +def _right_dtype(x, types): + """ + Is x a sub dtype of anything in types? + """ + return any(np.issubdtype(x.dtype, t) for t in types) + + def _ensure_plottable(*args): """ Raise exception if there is anything in args that can't be plotted on @@ -27,10 +30,8 @@ def _ensure_plottable(*args): """ plottypes = [np.floating, np.integer, np.timedelta64, np.datetime64] - righttype = lambda x: any(np.issubdtype(x.dtype, t) for t in plottypes) - # Lists need to be converted to np.arrays here. - if not any(righttype(np.array(x)) for x in args): + if not any(_right_dtype(np.array(x), plottypes) for x in args): raise TypeError('Plotting requires coordinates to be numeric ' 'or dates. Try DataArray.reindex() to convert.') @@ -150,6 +151,7 @@ def plot_hist(darray, ax=None, **kwargs): no_nan = np.ravel(darray) no_nan = no_nan[np.logical_not(np.isnan(no_nan))] + primitive = ax.hist(no_nan, **kwargs) ax.set_ylabel('Count') @@ -182,7 +184,7 @@ def _update_axes_limits(ax, xincrease, yincrease): def _plot2d(plotfunc): """ - Decorator for common 2d plotting logic. + Decorator for common 2d plotting logic. """ commondoc = ''' Parameters @@ -205,7 +207,7 @@ def _plot2d(plotfunc): Returns ------- artist : - The same type of primitive artist that the wrapped matplotlib + The same type of primitive artist that the wrapped matplotlib function returns ''' @@ -213,7 +215,8 @@ def _plot2d(plotfunc): plotfunc.__doc__ = '\n'.join((plotfunc.__doc__, commondoc)) @functools.wraps(plotfunc) - def wrapper(darray, ax=None, xincrease=None, yincrease=None, add_colorbar=True, **kwargs): + def wrapper(darray, ax=None, xincrease=None, yincrease=None, + add_colorbar=True, **kwargs): # All 2d plots in xray share this function signature import matplotlib.pyplot as plt diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index 3410bbb6b9a..0d0be86edb0 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -15,7 +15,6 @@ except ImportError: pass -# TODO - Add NaN handling and tests @requires_matplotlib class PlotTestCase(TestCase): @@ -147,7 +146,7 @@ def test_plot_nans(self): class Common2dMixin: """ - Common tests for 2d plotting go here. + Common tests for 2d plotting go here. These tests assume that `self.plotfunc` exists and is defined in the setUp. Should have the same name as the method. @@ -171,8 +170,8 @@ def test_3d_raises_valueerror(self): self.plotfunc(a) def test_nonnumeric_index_raises_typeerror(self): - a = DataArray(np.random.randn(3, 2), coords=[['a', 'b', 'c'], - ['d', 'e']]) + a = DataArray(np.random.randn(3, 2), + coords=[['a', 'b', 'c'], ['d', 'e']]) with self.assertRaisesRegexp(TypeError, r'[Ii]ndex'): self.plotfunc(a) From 9a54bedee07643d7ada5f13bab64696cadbab587 Mon Sep 17 00:00:00 2001 From: Clark Fitzgerald Date: Fri, 17 Jul 2015 17:02:05 -0700 Subject: [PATCH 61/61] incorporate Stephans feedback --- doc/api.rst | 1 + doc/computation.rst | 2 ++ doc/plotting.rst | 44 ++++++++++++++++------------- xray/__init__.py | 1 - xray/core/dataarray.py | 12 ++++---- xray/{core => }/plotting.py | 55 ++++++++++++++++++++++++------------- xray/test/test_plotting.py | 34 ++++++++++++++--------- 7 files changed, 91 insertions(+), 58 deletions(-) rename xray/{core => }/plotting.py (86%) diff --git a/doc/api.rst b/doc/api.rst index 34f239703ae..e8b8a06ef57 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -395,6 +395,7 @@ Plotting DataArray.plot DataArray.plot_contourf + DataArray.plot_contour DataArray.plot_hist DataArray.plot_imshow DataArray.plot_line diff --git a/doc/computation.rst b/doc/computation.rst index e5bccf494e1..69002ea5b17 100644 --- a/doc/computation.rst +++ b/doc/computation.rst @@ -46,6 +46,8 @@ Data arrays also implement many :py:class:`numpy.ndarray` methods: arr.round(2) arr.T +.. _missing_values: + Missing values ============== diff --git a/doc/plotting.rst b/doc/plotting.rst index 0af7524ebd6..b2b3a795d39 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -12,7 +12,7 @@ Xray plotting functionality is a thin wrapper around the popular `matplotlib `_ library. Matplotlib syntax and function names were copied as much as possible, which makes for an easy transition between the two. -Matplotlib must be installed and working before trying to plot with xray. +Matplotlib must be installed and working before plotting with xray. For more extensive plotting applications consider the following projects: @@ -21,8 +21,8 @@ For more extensive plotting applications consider the following projects: Integrates well with pandas. - `Holoviews `_: "Composable, declarative - data structures for building even complex visualizations easily." - Works for 2d datasets. + data structures for building even complex visualizations easily." Works + for 2d datasets. - `Cartopy `_: Provides cartographic tools. @@ -30,19 +30,20 @@ For more extensive plotting applications consider the following projects: Imports ~~~~~~~ -These imports are necessary for all of the examples. - .. ipython:: python - import numpy as np - import matplotlib as mpl # Use defaults so we don't get gridlines in generated docs + import matplotlib as mpl mpl.rcdefaults() +The following imports are necessary for all of the examples. + +.. ipython:: python + + import numpy as np import matplotlib.pyplot as plt import xray - One Dimension ------------- @@ -157,7 +158,7 @@ each of the axes should be. a.plot() It may seem strange that -the the values on the y axis are decreasing with -0.5 on the top. This is because +the values on the y axis are decreasing with -0.5 on the top. This is because the pixels are centered over their coordinates, and the axis labels and ranges correspond to the values of the coordinates. @@ -175,16 +176,14 @@ more conventional plot where the coordinates increase in the y axis. Missing Values ~~~~~~~~~~~~~~ -Xray plots data with missing values. -Xray uses ``np.nan`` for missing values. -TODO link. +Xray plots data with :ref:`missing_values`. .. ipython:: python # This data has holes in it! a[1, 1] = np.nan - @savefig plotting_missing_values.png width=6in + @savefig plotting_missing_values.png width=4in a.plot() Simulated Data @@ -293,12 +292,6 @@ Contour plots can have missing values also. @savefig plotting_nonuniform_coords_missing.png width=4in plt.show() -Return Values -~~~~~~~~~~~~~ - -Xray's plotting functions all return the same objects that the equivalent -matplotlib functions return. - Colormaps ~~~~~~~~~ @@ -322,6 +315,19 @@ later. @savefig plotting_same_color_scale.png width=6in plt.show() +Here we've used the object returned by :py:meth:`xray.DataArray.plot` to +pass in as an argument to +`plt.colorbar `_. +Take a closer look: + +.. ipython:: python + + im + +In general xray's plotting functions modify the axes and +return the same objects that the wrapped +matplotlib functions return. + Maps ---- diff --git a/xray/__init__.py b/xray/__init__.py index a0d61bfbd3a..6261721673f 100644 --- a/xray/__init__.py +++ b/xray/__init__.py @@ -4,7 +4,6 @@ from .core.dataset import Dataset from .core.dataarray import DataArray from .core.options import set_options -from .core import plotting from .backends.api import open_dataset, open_mfdataset, save_mfdataset from .conventions import decode_cf diff --git a/xray/core/dataarray.py b/xray/core/dataarray.py index 1c2974aa944..509d5fadb57 100644 --- a/xray/core/dataarray.py +++ b/xray/core/dataarray.py @@ -4,12 +4,13 @@ import pandas as pd +from .. import plotting + from . import indexing from . import groupby from . import ops from . import utils from . import variable -from . import plotting from .alignment import align from .common import AbstractArray, BaseDataObject from .coordinates import DataArrayCoordinates, Indexes @@ -1054,11 +1055,10 @@ def func(self, other): # Add plotting methods # Alternatively these could be added using a Mixin -DataArray.plot = plotting.plot -DataArray.plot_line = plotting.plot_line -DataArray.plot_contourf = plotting.plot_contourf -DataArray.plot_hist = plotting.plot_hist -DataArray.plot_imshow = plotting.plot_imshow +for name in ('plot', 'plot_line', 'plot_contourf', 'plot_contour', + 'plot_hist', 'plot_imshow'): + setattr(DataArray, name, getattr(plotting, name)) + # priority most be higher than Variable to properly work with binary ufuncs ops.inject_all_ops_and_reduce_methods(DataArray, priority=60) diff --git a/xray/core/plotting.py b/xray/plotting.py similarity index 86% rename from xray/core/plotting.py rename to xray/plotting.py index a4d4c17da29..ade41d78675 100644 --- a/xray/core/plotting.py +++ b/xray/plotting.py @@ -6,8 +6,9 @@ import functools import numpy as np +import pandas as pd -from .utils import is_uniform_spaced +from .core.utils import is_uniform_spaced # TODO - implement this @@ -16,11 +17,11 @@ class FacetGrid(): # Maybe more appropriate to keep this in .utils -def _right_dtype(x, types): +def _right_dtype(arr, types): """ - Is x a sub dtype of anything in types? + Is the numpy array a sub dtype of anything in types? """ - return any(np.issubdtype(x.dtype, t) for t in types) + return any(np.issubdtype(arr.dtype, t) for t in types) def _ensure_plottable(*args): @@ -33,7 +34,7 @@ def _ensure_plottable(*args): # Lists need to be converted to np.arrays here. if not any(_right_dtype(np.array(x), plottypes) for x in args): raise TypeError('Plotting requires coordinates to be numeric ' - 'or dates. Try DataArray.reindex() to convert.') + 'or dates.') def plot(darray, ax=None, rtol=0.01, **kwargs): @@ -55,12 +56,12 @@ def plot(darray, ax=None, rtol=0.01, **kwargs): Parameters ---------- darray : DataArray - ax : matplotlib axes object + ax : matplotlib axes, optional If None, uses the current axis - rtol : relative tolerance + rtol : number, optional Relative tolerance used to determine if the indexes - are uniformly spaced - kwargs + are uniformly spaced. Usually a small positive number. + **kwargs : optional Additional keyword arguments to matplotlib """ @@ -93,9 +94,9 @@ def plot_line(darray, *args, **kwargs): ---------- darray : DataArray Must be 1 dimensional - ax : matplotlib axes object + ax : matplotlib axes, optional If not passed, uses the current axis - args, kwargs + *args, **kwargs : optional Additional arguments to matplotlib.pyplot.plot """ @@ -123,6 +124,11 @@ def plot_line(darray, *args, **kwargs): if darray.name is not None: ax.set_ylabel(darray.name) + # Rotate dates on xlabels + if np.issubdtype(x.dtype, np.datetime64): + for label in ax.get_xticklabels(): + label.set_rotation(35) + return primitive @@ -138,9 +144,9 @@ def plot_hist(darray, ax=None, **kwargs): ---------- darray : DataArray Can be any dimension - ax : matplotlib axes object + ax : matplotlib axes, optional If not passed, uses the current axis - kwargs : + **kwargs : optional Additional keyword arguments to matplotlib.pyplot.hist """ @@ -150,7 +156,7 @@ def plot_hist(darray, ax=None, **kwargs): ax = plt.gca() no_nan = np.ravel(darray) - no_nan = no_nan[np.logical_not(np.isnan(no_nan))] + no_nan = no_nan[pd.notnull(no_nan)] primitive = ax.hist(no_nan, **kwargs) @@ -191,17 +197,17 @@ def _plot2d(plotfunc): ---------- darray : DataArray Must be 2 dimensional - ax : matplotlib axes object + ax : matplotlib axes object, optional If None, uses the current axis - xincrease : None (default), True, or False + xincrease : None, True, or False, optional Should the values on the x axes be increasing from left to right? if None, use the default for the matplotlib function - yincrease : None (default), True, or False + yincrease : None, True, or False, optional Should the values on the y axes be increasing from top to bottom? if None, use the default for the matplotlib function - add_colorbar : Boolean + add_colorbar : Boolean, optional Adds colorbar to axis - kwargs : + **kwargs : optional Additional arguments to wrapped matplotlib function Returns @@ -284,6 +290,17 @@ def plot_imshow(x, y, z, ax, **kwargs): return ax, primitive +@_plot2d +def plot_contour(x, y, z, ax, **kwargs): + """ + Contour plot of 2d DataArray + + Wraps matplotlib.pyplot.contour + """ + primitive = ax.contour(x, y, z, **kwargs) + return ax, primitive + + @_plot2d def plot_contourf(x, y, z, ax, **kwargs): """ diff --git a/xray/test/test_plotting.py b/xray/test/test_plotting.py index 0d0be86edb0..10d6bc0c137 100644 --- a/xray/test/test_plotting.py +++ b/xray/test/test_plotting.py @@ -1,8 +1,8 @@ import numpy as np +import pandas as pd from xray import DataArray -# Shouldn't need the core here? -from xray.core.plotting import plot_imshow, plot_contourf +from xray.plotting import plot_imshow, plot_contourf, plot_contour from . import TestCase, requires_matplotlib @@ -94,7 +94,7 @@ def test_can_pass_in_axis(self): def test_nonnumeric_index_raises_typeerror(self): a = DataArray([1, 2, 3], {'letter': ['a', 'b', 'c']}) - with self.assertRaisesRegexp(TypeError, r'[Ii]ndex'): + with self.assertRaisesRegexp(TypeError, r'[Pp]lot'): a.plot_line() def test_primitive_returned(self): @@ -105,6 +105,13 @@ def test_plot_nans(self): self.darray[1] = np.nan self.darray.plot_line() + def test_x_ticks_are_rotated_for_time(self): + time = pd.date_range('2000-01-01', '2000-01-10') + a = DataArray(np.arange(len(time)), {'t': time}) + a.plot_line() + rotation = plt.gca().get_xticklabels()[0].get_rotation() + self.assertFalse(rotation == 0) + class TestPlotHistogram(PlotTestCase): @@ -140,7 +147,7 @@ def test_primitive_returned(self): self.assertTrue(isinstance(h[-1][0], mpl.patches.Rectangle)) def test_plot_nans(self): - self.darray[0, 0, :] = np.nan + self.darray[0, 0, 0] = np.nan self.darray.plot_hist() @@ -148,8 +155,8 @@ class Common2dMixin: """ Common tests for 2d plotting go here. - These tests assume that `self.plotfunc` exists and is defined in the - setUp. Should have the same name as the method. + These tests assume that a staticmethod for `self.plotfunc` exists. + Should have the same name as the method. """ def setUp(self): self.darray = DataArray(np.random.randn(10, 15), dims=['y', 'x']) @@ -172,7 +179,7 @@ def test_3d_raises_valueerror(self): def test_nonnumeric_index_raises_typeerror(self): a = DataArray(np.random.randn(3, 2), coords=[['a', 'b', 'c'], ['d', 'e']]) - with self.assertRaisesRegexp(TypeError, r'[Ii]ndex'): + with self.assertRaisesRegexp(TypeError, r'[Pp]lot'): self.plotfunc(a) def test_can_pass_in_axis(self): @@ -199,9 +206,7 @@ def test_plot_nans(self): class TestContourf(Common2dMixin, PlotTestCase): - def setUp(self): - self.plotfunc = plot_contourf - super(TestContourf, self).setUp() + plotfunc = staticmethod(plot_contourf) def test_contourf_called(self): # Having both statements ensures the test works properly @@ -213,11 +218,14 @@ def test_primitive_artist_returned(self): self.assertTrue(isinstance(artist, mpl.contour.QuadContourSet)) +class TestContour(Common2dMixin, PlotTestCase): + + plotfunc = staticmethod(plot_contour) + + class TestImshow(Common2dMixin, PlotTestCase): - def setUp(self): - self.plotfunc = plot_imshow - super(TestImshow, self).setUp() + plotfunc = staticmethod(plot_imshow) def test_imshow_called(self): # Having both statements ensures the test works properly