From e631f47a45b1a412f3349428f6463682b8510b81 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Fri, 6 Dec 2019 13:02:13 +0000 Subject: [PATCH 01/16] Copy colors for bad, under, and over values in _build_discrete_cmap() Copies the cmap._rgba_bad, cmap._rgba_under, and cmap._rgba_over values to new_cmap, in case they have been set to non-default values. Allows the user to customize plots more by using matplotlib methods on a cmap before passing as an argument to xarray's plotting methods. Previously these settings were overridden by defaults when creating the cmap actually used to make the plot. --- xarray/plot/utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 3b739197fea..26ca3de6a8a 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -98,6 +98,12 @@ def _build_discrete_cmap(cmap, levels, extend, filled): # copy the old cmap name, for easier testing new_cmap.name = getattr(cmap, "name", cmap) + # copy colors to use for bad, under, and over values in case they have been set to + # non-default values + new_cmap._rgba_bad = getattr(cmap, "_rgba_bad") + new_cmap._rgba_under = getattr(cmap, "_rgba_under") + new_cmap._rgba_over = getattr(cmap, "_rgba_over") + return new_cmap, cnorm From 92b066c2ac60ce21ef4230342a3ac7b8a3497810 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Fri, 6 Dec 2019 13:38:53 +0000 Subject: [PATCH 02/16] Tests that cmap.set_bad, cmap.set_under, cmap.set_over not overridden --- xarray/tests/test_plot.py | 46 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index a10f0d9a67e..7218c3e908a 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1,5 +1,6 @@ import inspect from datetime import datetime +from copy import copy import numpy as np import pandas as pd @@ -267,6 +268,51 @@ def test2d_1d_2d_coordinates_contourf(self): a.plot.contourf(x="time", y="depth") a.plot.contourf(x="depth", y="time") + def test_contourf_cmap_set_bad(self): + sz = (4, 4) + a = DataArray(easy_array(sz), dims=["z", "time"]) + + cmap = copy(mpl.cm.viridis) + cmap.set_bad("w") + + # check we actually changed the set_bad color + assert cmap._rgba_bad != mpl.cm.viridis._rgba_bad + + pl = a.plot.contourf(cmap=copy(cmap)) + + # check the set_bad color has been kept + assert pl.cmap._rgba_bad == cmap._rgba_bad + + def test_contourf_cmap_set_under(self): + sz = (4, 4) + a = DataArray(easy_array(sz), dims=["z", "time"]) + + cmap = copy(mpl.cm.viridis) + cmap.set_under("w") + + # check we actually changed the set_under color + assert cmap._rgba_under != mpl.cm.viridis._rgba_under + + pl = a.plot.contourf(cmap=copy(cmap)) + + # check the set_under color has been kept + assert pl.cmap._rgba_under == cmap._rgba_under + + def test_contourf_cmap_set_over(self): + sz = (4, 4) + a = DataArray(easy_array(sz), dims=["z", "time"]) + + cmap = copy(mpl.cm.viridis) + cmap.set_over("w") + + # check we actually changed the set_over color + assert cmap._rgba_over != mpl.cm.viridis._rgba_over + + pl = a.plot.contourf(cmap=copy(cmap)) + + # check the set_over color has been kept + assert pl.cmap._rgba_over == cmap._rgba_over + def test3d(self): self.darray.plot() From ae6609b025d62a0d863869cbd16db167a8855167 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Fri, 6 Dec 2019 18:57:25 +0000 Subject: [PATCH 03/16] Add defaults in case cmap is a str in _build_discrete_cmap If the input cmap is a str, getting _rgba_bad, _rgba_under, or _rgba_over attributes from it will fail. To fix this, provide defaults taken from new_cmap. --- xarray/plot/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 26ca3de6a8a..5b43c03bfc1 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -100,9 +100,9 @@ def _build_discrete_cmap(cmap, levels, extend, filled): # copy colors to use for bad, under, and over values in case they have been set to # non-default values - new_cmap._rgba_bad = getattr(cmap, "_rgba_bad") - new_cmap._rgba_under = getattr(cmap, "_rgba_under") - new_cmap._rgba_over = getattr(cmap, "_rgba_over") + new_cmap._rgba_bad = getattr(cmap, "_rgba_bad", new_cmap._rgba_bad) + new_cmap._rgba_under = getattr(cmap, "_rgba_under", new_cmap._rgba_under) + new_cmap._rgba_over = getattr(cmap, "_rgba_over", new_cmap._rgba_over) return new_cmap, cnorm From 75fc288fec35fc769181fd050c8245ab60abf293 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Wed, 22 Jan 2020 21:54:05 +0000 Subject: [PATCH 04/16] Consolidate tests of cmap.set_bad() cmap.set_under() and cmap.set_over() Make one unit test test all three properties, rather than having a separate test for each. --- xarray/tests/test_plot.py | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 7218c3e908a..5b95295ca68 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -268,48 +268,32 @@ def test2d_1d_2d_coordinates_contourf(self): a.plot.contourf(x="time", y="depth") a.plot.contourf(x="depth", y="time") - def test_contourf_cmap_set_bad(self): + def test_contourf_cmap_set(self): sz = (4, 4) a = DataArray(easy_array(sz), dims=["z", "time"]) cmap = copy(mpl.cm.viridis) - cmap.set_bad("w") + cmap.set_bad("w") # check we actually changed the set_bad color assert cmap._rgba_bad != mpl.cm.viridis._rgba_bad - pl = a.plot.contourf(cmap=copy(cmap)) - - # check the set_bad color has been kept - assert pl.cmap._rgba_bad == cmap._rgba_bad - - def test_contourf_cmap_set_under(self): - sz = (4, 4) - a = DataArray(easy_array(sz), dims=["z", "time"]) - - cmap = copy(mpl.cm.viridis) cmap.set_under("w") - # check we actually changed the set_under color assert cmap._rgba_under != mpl.cm.viridis._rgba_under - pl = a.plot.contourf(cmap=copy(cmap)) - - # check the set_under color has been kept - assert pl.cmap._rgba_under == cmap._rgba_under - - def test_contourf_cmap_set_over(self): - sz = (4, 4) - a = DataArray(easy_array(sz), dims=["z", "time"]) - - cmap = copy(mpl.cm.viridis) cmap.set_over("w") - # check we actually changed the set_over color assert cmap._rgba_over != mpl.cm.viridis._rgba_over pl = a.plot.contourf(cmap=copy(cmap)) + # check the set_bad color has been kept + assert pl.cmap._rgba_bad == cmap._rgba_bad + + # check the set_under color has been kept + assert pl.cmap._rgba_under == cmap._rgba_under + # check the set_over color has been kept assert pl.cmap._rgba_over == cmap._rgba_over From de6ffeb537b0549244356694eb1cdabd3ed63345 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Tue, 4 Feb 2020 11:24:48 +0000 Subject: [PATCH 05/16] Do not read/modify private members for bad, under and over colors --- xarray/plot/utils.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 5b43c03bfc1..bc17e36b90a 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -98,11 +98,22 @@ def _build_discrete_cmap(cmap, levels, extend, filled): # copy the old cmap name, for easier testing new_cmap.name = getattr(cmap, "name", cmap) - # copy colors to use for bad, under, and over values in case they have been set to - # non-default values - new_cmap._rgba_bad = getattr(cmap, "_rgba_bad", new_cmap._rgba_bad) - new_cmap._rgba_under = getattr(cmap, "_rgba_under", new_cmap._rgba_under) - new_cmap._rgba_over = getattr(cmap, "_rgba_over", new_cmap._rgba_over) + # copy colors to use for bad, under, and over values in case they have been + # set to non-default values + try: + # matplotlib<3.2 only uses bad color for masked values + bad = cmap(np.ma.masked_invalid([np.nan]))[0] + except TypeError: + # cmap was a str or list rather than a color-map object, so there are + # no bad, under or over values to check or copy + pass + else: + under = cmap(-np.inf) + over = cmap(np.inf) + + new_cmap.set_bad(bad) + new_cmap.set_under(under) + new_cmap.set_over(over) return new_cmap, cnorm From 47a6fc0c9b796997996d79d32358585d08021b9c Mon Sep 17 00:00:00 2001 From: John Omotani Date: Tue, 4 Feb 2020 11:26:47 +0000 Subject: [PATCH 06/16] Only change under, over colors if they were set by user When modifying a colormap, we only want to preserve the under and over colors of the original colormap if they were explicitly set by the user. In _build_discrete_cmap this makes no difference, as the new_cmap returned by mpl.colors.from_levels_and_colors has the same minimum and maximum colors as its input, so the default under and over colors would not change anyway and could be copied regardless. However, for clarity and in case the same pattern is needed in future elsewhere, it is nicer to add a checks for: whether the under color is the same as cmap(0), only setting under for new_cmap if it is not; and whether the over color is the same as cmap(cmap.N - 1), only setting over for new_cmap if it is not. --- xarray/plot/utils.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index bc17e36b90a..b5bd96ab127 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -112,8 +112,15 @@ def _build_discrete_cmap(cmap, levels, extend, filled): over = cmap(np.inf) new_cmap.set_bad(bad) - new_cmap.set_under(under) - new_cmap.set_over(over) + + # Only update under and over if they were explicitly changed by the user + # (i.e. are different from the lowest or highest values in cmap). Otherwise + # leave unchanged so new_cmap uses its default values (its own lowest and + # highest values). + if under != cmap(0): + new_cmap.set_under(under) + if over != cmap(cmap.N - 1): + new_cmap.set_over(over) return new_cmap, cnorm From 55fa27789aec413138f9614423baebfbbe303e5f Mon Sep 17 00:00:00 2001 From: John Omotani Date: Tue, 4 Feb 2020 15:33:15 +0000 Subject: [PATCH 07/16] Remove temporary variable that is only used once --- xarray/tests/test_plot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 5b95295ca68..eb185d519cd 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -269,8 +269,7 @@ def test2d_1d_2d_coordinates_contourf(self): a.plot.contourf(x="depth", y="time") def test_contourf_cmap_set(self): - sz = (4, 4) - a = DataArray(easy_array(sz), dims=["z", "time"]) + a = DataArray(easy_array((4, 4)), dims=["z", "time"]) cmap = copy(mpl.cm.viridis) From 784560db3e65719985fa54a65efbb48774504c1f Mon Sep 17 00:00:00 2001 From: John Omotani Date: Tue, 4 Feb 2020 15:58:32 +0000 Subject: [PATCH 08/16] Use deepcopy instead of copy for cmaps cmaps contain tuples member variables, so safer to deepcopy instead of just copy to make sure we never change the copied variable. --- xarray/tests/test_plot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index eb185d519cd..b57720af9f2 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1,6 +1,6 @@ import inspect from datetime import datetime -from copy import copy +from copy import deepcopy import numpy as np import pandas as pd @@ -271,7 +271,7 @@ def test2d_1d_2d_coordinates_contourf(self): def test_contourf_cmap_set(self): a = DataArray(easy_array((4, 4)), dims=["z", "time"]) - cmap = copy(mpl.cm.viridis) + cmap = deepcopy(mpl.cm.viridis) cmap.set_bad("w") # check we actually changed the set_bad color @@ -285,7 +285,7 @@ def test_contourf_cmap_set(self): # check we actually changed the set_over color assert cmap._rgba_over != mpl.cm.viridis._rgba_over - pl = a.plot.contourf(cmap=copy(cmap)) + pl = a.plot.contourf(cmap=deepcopy(cmap)) # check the set_bad color has been kept assert pl.cmap._rgba_bad == cmap._rgba_bad From fb60960891b83e69d1fbf97834ded0555b350a90 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Tue, 4 Feb 2020 16:03:25 +0000 Subject: [PATCH 09/16] Set different colors for bad, under and over when testing --- xarray/tests/test_plot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index b57720af9f2..2258d65f964 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -277,11 +277,11 @@ def test_contourf_cmap_set(self): # check we actually changed the set_bad color assert cmap._rgba_bad != mpl.cm.viridis._rgba_bad - cmap.set_under("w") + cmap.set_under("r") # check we actually changed the set_under color assert cmap._rgba_under != mpl.cm.viridis._rgba_under - cmap.set_over("w") + cmap.set_over("g") # check we actually changed the set_over color assert cmap._rgba_over != mpl.cm.viridis._rgba_over From ef0d15edb1eb53e3145db0fa83cc764af5dfeabc Mon Sep 17 00:00:00 2001 From: John Omotani Date: Tue, 4 Feb 2020 16:05:33 +0000 Subject: [PATCH 10/16] Extra test checking bad, under and over colors when not set explicitly --- xarray/tests/test_plot.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 2258d65f964..e23efeb1a2c 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -273,6 +273,22 @@ def test_contourf_cmap_set(self): cmap = deepcopy(mpl.cm.viridis) + pl = a.plot.contourf(cmap=deepcopy(cmap)) + + # check the set_bad color + assert pl.cmap._rgba_bad == cmap._rgba_bad + + # check the set_under color + assert pl.cmap._rgba_under == cmap._rgba_under + + # check the set_over color + assert pl.cmap._rgba_over == cmap._rgba_over + + def test_contourf_cmap_set_with_bad_under_over(self): + a = DataArray(easy_array((4, 4)), dims=["z", "time"]) + + cmap = deepcopy(mpl.cm.viridis) + cmap.set_bad("w") # check we actually changed the set_bad color assert cmap._rgba_bad != mpl.cm.viridis._rgba_bad From 8a42c0fb528e949fd6d92adfbf1d03389cc24759 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Wed, 5 Feb 2020 16:32:58 +0000 Subject: [PATCH 11/16] Comment on why test uses deepcopy --- xarray/tests/test_plot.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index e23efeb1a2c..28e9f15af58 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -271,8 +271,9 @@ def test2d_1d_2d_coordinates_contourf(self): def test_contourf_cmap_set(self): a = DataArray(easy_array((4, 4)), dims=["z", "time"]) - cmap = deepcopy(mpl.cm.viridis) + cmap = mpl.cm.viridis + # deepcopy to ensure cmap is not changed by contourf() pl = a.plot.contourf(cmap=deepcopy(cmap)) # check the set_bad color @@ -287,6 +288,9 @@ def test_contourf_cmap_set(self): def test_contourf_cmap_set_with_bad_under_over(self): a = DataArray(easy_array((4, 4)), dims=["z", "time"]) + # Make a copy here because we want a local cmap that we will modify. + # Use deepcopy because matplotlib Colormap objects have tuple members + # and we want to ensure we do not change the original. cmap = deepcopy(mpl.cm.viridis) cmap.set_bad("w") @@ -301,6 +305,7 @@ def test_contourf_cmap_set_with_bad_under_over(self): # check we actually changed the set_over color assert cmap._rgba_over != mpl.cm.viridis._rgba_over + # deepcopy to ensure cmap is not changed by contourf() pl = a.plot.contourf(cmap=deepcopy(cmap)) # check the set_bad color has been kept From f0ec3f2376bd595e16739f5fc9eb328d69f1ff72 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Wed, 5 Feb 2020 17:54:19 +0000 Subject: [PATCH 12/16] Pass vmin and vmax in test_contourf_cmap_set() Set vmin and vmax so that _build_discrete_colormap is called with extend='both'. extend is passed to mpl.colors.from_levels_and_colors(), which returns a result with sensible under and over values if extend='both', but not if extend='neither' (but if extend='neither' the under and over values would not be used because the data would all be within the plotted range) --- xarray/tests/test_plot.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 28e9f15af58..26daf417037 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -274,7 +274,14 @@ def test_contourf_cmap_set(self): cmap = mpl.cm.viridis # deepcopy to ensure cmap is not changed by contourf() - pl = a.plot.contourf(cmap=deepcopy(cmap)) + # Set vmin and vmax so that _build_discrete_colormap is called with + # extend='both'. extend is passed to + # mpl.colors.from_levels_and_colors(), which returns a result with + # sensible under and over values if extend='both', but not if + # extend='neither' (but if extend='neither' the under and over values + # would not be used because the data would all be within the plotted + # range) + pl = a.plot.contourf(cmap=deepcopy(cmap), vmin=0.1, vmax=0.9) # check the set_bad color assert pl.cmap._rgba_bad == cmap._rgba_bad From 061ddf2b68f30a2fe036cb1ad672bd4ff81a0dd4 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Wed, 5 Feb 2020 17:55:52 +0000 Subject: [PATCH 13/16] Don't use private members in tests --- xarray/tests/test_plot.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 26daf417037..330c975b999 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -284,13 +284,16 @@ def test_contourf_cmap_set(self): pl = a.plot.contourf(cmap=deepcopy(cmap), vmin=0.1, vmax=0.9) # check the set_bad color - assert pl.cmap._rgba_bad == cmap._rgba_bad + assert np.all( + pl.cmap(np.ma.masked_invalid([np.nan]))[0] + == cmap(np.ma.masked_invalid([np.nan]))[0] + ) # check the set_under color - assert pl.cmap._rgba_under == cmap._rgba_under + assert pl.cmap(-np.inf) == cmap(-np.inf) # check the set_over color - assert pl.cmap._rgba_over == cmap._rgba_over + assert pl.cmap(np.inf) == cmap(np.inf) def test_contourf_cmap_set_with_bad_under_over(self): a = DataArray(easy_array((4, 4)), dims=["z", "time"]) @@ -302,27 +305,33 @@ def test_contourf_cmap_set_with_bad_under_over(self): cmap.set_bad("w") # check we actually changed the set_bad color - assert cmap._rgba_bad != mpl.cm.viridis._rgba_bad + assert np.all( + cmap(np.ma.masked_invalid([np.nan]))[0] + != mpl.cm.viridis(np.ma.masked_invalid([np.nan]))[0] + ) cmap.set_under("r") # check we actually changed the set_under color - assert cmap._rgba_under != mpl.cm.viridis._rgba_under + assert cmap(-np.inf) != mpl.cm.viridis(-np.inf) cmap.set_over("g") # check we actually changed the set_over color - assert cmap._rgba_over != mpl.cm.viridis._rgba_over + assert cmap(np.inf) != mpl.cm.viridis(-np.inf) # deepcopy to ensure cmap is not changed by contourf() pl = a.plot.contourf(cmap=deepcopy(cmap)) # check the set_bad color has been kept - assert pl.cmap._rgba_bad == cmap._rgba_bad + assert np.all( + pl.cmap(np.ma.masked_invalid([np.nan]))[0] + == cmap(np.ma.masked_invalid([np.nan]))[0] + ) # check the set_under color has been kept - assert pl.cmap._rgba_under == cmap._rgba_under + assert pl.cmap(-np.inf) == cmap(-np.inf) # check the set_over color has been kept - assert pl.cmap._rgba_over == cmap._rgba_over + assert pl.cmap(np.inf) == cmap(np.inf) def test3d(self): self.darray.plot() From 7dc6ed87b070207b96f8276594cb47ed79544ed0 Mon Sep 17 00:00:00 2001 From: dcherian Date: Mon, 24 Feb 2020 12:12:17 -0700 Subject: [PATCH 14/16] Add whats-new --- doc/whats-new.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 1d7c425e554..bb4116d0d11 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -41,6 +41,9 @@ Bug fixes - :py:func:`concat` can now handle coordinate variables only present in one of the objects to be concatenated when ``coords="different"``. By `Deepak Cherian `_. +- xarray now respects the over, under and bad colors if set on a provided colormap. + (:issue:`3590`, :pull:`3601`) + By `John Motani `_. Documentation ~~~~~~~~~~~~~ From cdb230577555b213ecaa4ffa4c88353a0b6f7f90 Mon Sep 17 00:00:00 2001 From: dcherian Date: Mon, 24 Feb 2020 12:52:37 -0700 Subject: [PATCH 15/16] Fix isort --- xarray/tests/test_plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index c9ed3706c58..9ffbcd9c85e 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1,6 +1,6 @@ import inspect -from datetime import datetime from copy import deepcopy +from datetime import datetime import numpy as np import pandas as pd From 4fa6f8ecce70c427ca2c7c26fb3eb1c1d28a959b Mon Sep 17 00:00:00 2001 From: Deepak Cherian Date: Mon, 24 Feb 2020 12:19:54 -0800 Subject: [PATCH 16/16] Update doc/whats-new.rst --- doc/whats-new.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index bb4116d0d11..8a2666d25fa 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -43,7 +43,7 @@ Bug fixes By `Deepak Cherian `_. - xarray now respects the over, under and bad colors if set on a provided colormap. (:issue:`3590`, :pull:`3601`) - By `John Motani `_. + By `johnomotani `_. Documentation ~~~~~~~~~~~~~