Skip to content

Commit

Permalink
Merge pull request #655 from fmaussion/plot-params
Browse files Browse the repository at this point in the history
More control on cmap params
  • Loading branch information
shoyer committed Nov 15, 2015
2 parents 5109f4f + 9bf93f7 commit 4f430cd
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 10 deletions.
3 changes: 3 additions & 0 deletions doc/whats-new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ Enhancements
:ref:`io.pynio` for more details.
- Better error message when a variable is supplied with the same name as
one of its dimensions.
- Plotting: more control on colormap parameters (:issue:`642`). ``vmin`` and
``vmax`` will not be silently ignored anymore. Setting ``center=False``
prevents automatic selection of a divergent colormap.

Bug fixes
~~~~~~~~~
Expand Down
9 changes: 6 additions & 3 deletions xray/plot/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,8 +290,10 @@ def _plot2d(plotfunc):
vmin, vmax : floats, optional
Values to anchor the colormap, otherwise they are inferred from the
data and other keyword arguments. When a diverging dataset is inferred,
one of these values may be ignored. If discrete levels are provided as
an explicit list, both of these values are ignored.
setting one of these values will fix the other by symmetry around
``center``. Setting both values prevents use of a diverging colormap.
If discrete levels are provided as an explicit list, both of these
values are ignored.
cmap : matplotlib colormap name or object, optional
The mapping from data values to color space. If not provided, this
will be either be ``viridis`` (if the function infers a sequential
Expand All @@ -304,7 +306,8 @@ def _plot2d(plotfunc):
or ``contourf``, the ``levels`` argument is required.
center : float, optional
The value at which to center the colormap. Passing this value implies
use of a diverging colormap.
use of a diverging colormap. Setting it to ``False`` prevents use of a
diverging colormap.
robust : bool, optional
If True and ``vmin`` or ``vmax`` are absent, the colormap range is
computed with 2nd and 98th percentiles instead of the extreme values.
Expand Down
34 changes: 27 additions & 7 deletions xray/plot/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,28 +123,48 @@ def _determine_cmap_params(plot_data, vmin=None, vmax=None, cmap=None,

calc_data = np.ravel(plot_data[~pd.isnull(plot_data)])

# Setting center=False prevents a divergent cmap
possibly_divergent = center is not False

# Set center to 0 so math below makes sense but remember its state
center_is_none = False
if center is None:
center = 0
center_is_none = True

# Setting both vmin and vmax prevents a divergent cmap
if (vmin is not None) and (vmax is not None):
possibly_divergent = False

# vlim might be computed below
vlim = None

if vmin is None:
if robust:
vmin = np.percentile(calc_data, ROBUST_PERCENTILE)
else:
vmin = calc_data.min()
elif possibly_divergent:
vlim = abs(vmin - center)

if vmax is None:
if robust:
vmax = np.percentile(calc_data, 100 - ROBUST_PERCENTILE)
else:
vmax = calc_data.max()
elif possibly_divergent:
vlim = abs(vmax - center)

# Simple heuristics for whether these data should have a divergent map
divergent = ((vmin < 0) and (vmax > 0)) or center is not None

# Now set center to 0 so math below makes sense
if center is None:
center = 0
if possibly_divergent:
# kwargs not specific about divergent or not: infer defaults from data
divergent = ((vmin < 0) and (vmax > 0)) or not center_is_none
else:
divergent = False

# A divergent map should be symmetric around the center value
if divergent:
vlim = max(abs(vmin - center), abs(vmax - center))
if vlim is None:
vlim = max(abs(vmin - center), abs(vmax - center))
vmin, vmax = -vlim, vlim

# Now add in the centering value and set the limits
Expand Down
72 changes: 72 additions & 0 deletions xray/test/test_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,78 @@ def test_list_levels(self):
data, levels=wrap_levels(orig_levels))
self.assertArrayEqual(cmap_params['levels'], orig_levels)

def test_divergentcontrol(self):
neg = self.data - 0.1
pos = self.data

# Default with positive data will be a normal cmap
cmap_params = _determine_cmap_params(pos)
self.assertEqual(cmap_params['vmin'], 0)
self.assertEqual(cmap_params['vmax'], 1)
self.assertEqual(cmap_params['cmap'].name, "viridis")

# Default with negative data will be a divergent cmap
cmap_params = _determine_cmap_params(neg)
self.assertEqual(cmap_params['vmin'], -0.9)
self.assertEqual(cmap_params['vmax'], 0.9)
self.assertEqual(cmap_params['cmap'], "RdBu_r")

# Setting vmin or vmax should prevent this only if center is false
cmap_params = _determine_cmap_params(neg, vmin=-0.1, center=False)
self.assertEqual(cmap_params['vmin'], -0.1)
self.assertEqual(cmap_params['vmax'], 0.9)
self.assertEqual(cmap_params['cmap'].name, "viridis")
cmap_params = _determine_cmap_params(neg, vmax=0.5, center=False)
self.assertEqual(cmap_params['vmin'], -0.1)
self.assertEqual(cmap_params['vmax'], 0.5)
self.assertEqual(cmap_params['cmap'].name, "viridis")

# Setting center=False too
cmap_params = _determine_cmap_params(neg, center=False)
self.assertEqual(cmap_params['vmin'], -0.1)
self.assertEqual(cmap_params['vmax'], 0.9)
self.assertEqual(cmap_params['cmap'].name, "viridis")

# However, I should still be able to set center and have a div cmap
cmap_params = _determine_cmap_params(neg, center=0)
self.assertEqual(cmap_params['vmin'], -0.9)
self.assertEqual(cmap_params['vmax'], 0.9)
self.assertEqual(cmap_params['cmap'], "RdBu_r")

# Setting vmin or vmax alone will force symetric bounds around center
cmap_params = _determine_cmap_params(neg, vmin=-0.1)
self.assertEqual(cmap_params['vmin'], -0.1)
self.assertEqual(cmap_params['vmax'], 0.1)
self.assertEqual(cmap_params['cmap'], "RdBu_r")
cmap_params = _determine_cmap_params(neg, vmax=0.5)
self.assertEqual(cmap_params['vmin'], -0.5)
self.assertEqual(cmap_params['vmax'], 0.5)
self.assertEqual(cmap_params['cmap'], "RdBu_r")
cmap_params = _determine_cmap_params(neg, vmax=0.6, center=0.1)
self.assertEqual(cmap_params['vmin'], -0.4)
self.assertEqual(cmap_params['vmax'], 0.6)
self.assertEqual(cmap_params['cmap'], "RdBu_r")

# But this is only true if vmin or vmax are negative
cmap_params = _determine_cmap_params(pos, vmin=-0.1)
self.assertEqual(cmap_params['vmin'], -0.1)
self.assertEqual(cmap_params['vmax'], 0.1)
self.assertEqual(cmap_params['cmap'], "RdBu_r")
cmap_params = _determine_cmap_params(pos, vmin=0.1)
self.assertEqual(cmap_params['vmin'], 0.1)
self.assertEqual(cmap_params['vmax'], 1)
self.assertEqual(cmap_params['cmap'].name, "viridis")
cmap_params = _determine_cmap_params(pos, vmax=0.5)
self.assertEqual(cmap_params['vmin'], 0)
self.assertEqual(cmap_params['vmax'], 0.5)
self.assertEqual(cmap_params['cmap'].name, "viridis")

# If both vmin and vmax are provided, output is non-divergent
cmap_params = _determine_cmap_params(neg, vmin=-0.2, vmax=0.6)
self.assertEqual(cmap_params['vmin'], -0.2)
self.assertEqual(cmap_params['vmax'], 0.6)
self.assertEqual(cmap_params['cmap'].name, "viridis")


@requires_matplotlib
class TestDiscreteColorMap(TestCase):
Expand Down

0 comments on commit 4f430cd

Please sign in to comment.