Skip to content

Commit 71d6a0e

Browse files
authored
Integer levels and vmin/vmax (#1191)
1 parent feed922 commit 71d6a0e

File tree

4 files changed

+53
-15
lines changed

4 files changed

+53
-15
lines changed

doc/whats-new.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ Breaking changes
7070
- Coordinates used to index a dimension are now loaded eagerly into
7171
:py:class:`pandas.Index` objects, instead of loading the values lazily.
7272
By `Guido Imperiale <https://github.com/crusaderky>`_.
73+
- Automatic levels for 2d plots are now guaranteed to land on ``vmin`` and
74+
``vmax`` when these kwargs are explicitly provided (:issue:`1191`). The
75+
automated level selection logic also slightly changed.
76+
By `Fabien Maussion <https://github.com/fmaussion>`_.
7377
- xarray no longer supports python 3.3 or versions of dask prior to v0.9.0.
7478

7579
Deprecations

xarray/plot/plot.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,11 @@ def _plot2d(plotfunc):
358358
How to draw arrows extending the colorbar beyond its limits. If not
359359
provided, extend is inferred from vmin, vmax and the data limits.
360360
levels : int or list-like object, optional
361-
Split the colormap (cmap) into discrete color intervals.
361+
Split the colormap (cmap) into discrete color intervals. If an integer
362+
is provided, "nice" levels are chosen based on the data range: this can
363+
imply that the final number of levels is not exactly the expected one.
364+
Setting ``vmin`` and/or ``vmax`` with ``levels=N`` is equivalent to
365+
setting ``levels=np.linspace(vmin, vmax, N)``.
362366
infer_intervals : bool, optional
363367
Only applies to pcolormesh. If True, the coordinate intervals are
364368
passed to pcolormesh. If False, the original coordinates are used

xarray/plot/utils.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import pandas as pd
88

99
from ..core.pycompat import basestring
10+
from ..core.utils import is_scalar
1011

1112

1213
def _load_default_cmap(fname='default_colormap.csv'):
@@ -139,6 +140,9 @@ def _determine_cmap_params(plot_data, vmin=None, vmax=None, cmap=None,
139140
if (vmin is not None) and (vmax is not None):
140141
possibly_divergent = False
141142

143+
# Setting vmin or vmax implies linspaced levels
144+
user_minmax = (vmin is not None) or (vmax is not None)
145+
142146
# vlim might be computed below
143147
vlim = None
144148

@@ -187,9 +191,13 @@ def _determine_cmap_params(plot_data, vmin=None, vmax=None, cmap=None,
187191

188192
# Handle discrete levels
189193
if levels is not None:
190-
if isinstance(levels, int):
191-
ticker = mpl.ticker.MaxNLocator(levels)
192-
levels = ticker.tick_values(vmin, vmax)
194+
if is_scalar(levels):
195+
if user_minmax or levels == 1:
196+
levels = np.linspace(vmin, vmax, levels)
197+
else:
198+
# N in MaxNLocator refers to bins, not ticks
199+
ticker = mpl.ticker.MaxNLocator(levels-1)
200+
levels = ticker.tick_values(vmin, vmax)
193201
vmin, vmax = levels[0], levels[-1]
194202

195203
if extend is None:

xarray/test/test_plot.py

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
from __future__ import division
33
from __future__ import print_function
44

5+
# import mpl and change the backend before other mpl imports
6+
try:
7+
import matplotlib as mpl
8+
# Using a different backend makes Travis CI work
9+
mpl.use('Agg')
10+
# Order of imports is important here.
11+
import matplotlib.pyplot as plt
12+
except ImportError:
13+
pass
14+
515
import inspect
616

717
import numpy as np
@@ -17,15 +27,6 @@
1727

1828
from . import TestCase, requires_matplotlib
1929

20-
try:
21-
import matplotlib as mpl
22-
# Using a different backend makes Travis CI work.
23-
mpl.use('Agg')
24-
# Order of imports is important here.
25-
import matplotlib.pyplot as plt
26-
except ImportError:
27-
pass
28-
2930

3031
def text_in_fig():
3132
'''
@@ -322,20 +323,41 @@ def test_center(self):
322323

323324
def test_integer_levels(self):
324325
data = self.data + 1
326+
327+
# default is to cover full data range but with no guarantee on Nlevels
328+
for level in np.arange(2, 10, dtype=int):
329+
cmap_params = _determine_cmap_params(data, levels=level)
330+
self.assertEqual(cmap_params['vmin'], cmap_params['levels'][0])
331+
self.assertEqual(cmap_params['vmax'], cmap_params['levels'][-1])
332+
self.assertEqual(cmap_params['extend'], 'neither')
333+
334+
# with min max we are more strict
325335
cmap_params = _determine_cmap_params(data, levels=5, vmin=0, vmax=5,
326336
cmap='Blues')
337+
self.assertEqual(cmap_params['vmin'], 0)
338+
self.assertEqual(cmap_params['vmax'], 5)
327339
self.assertEqual(cmap_params['vmin'], cmap_params['levels'][0])
328340
self.assertEqual(cmap_params['vmax'], cmap_params['levels'][-1])
329341
self.assertEqual(cmap_params['cmap'].name, 'Blues')
330342
self.assertEqual(cmap_params['extend'], 'neither')
331-
self.assertEqual(cmap_params['cmap'].N, 5)
332-
self.assertEqual(cmap_params['norm'].N, 6)
343+
self.assertEqual(cmap_params['cmap'].N, 4)
344+
self.assertEqual(cmap_params['norm'].N, 5)
333345

334346
cmap_params = _determine_cmap_params(data, levels=5,
335347
vmin=0.5, vmax=1.5)
336348
self.assertEqual(cmap_params['cmap'].name, 'viridis')
337349
self.assertEqual(cmap_params['extend'], 'max')
338350

351+
cmap_params = _determine_cmap_params(data, levels=5,
352+
vmin=1.5)
353+
self.assertEqual(cmap_params['cmap'].name, 'viridis')
354+
self.assertEqual(cmap_params['extend'], 'min')
355+
356+
cmap_params = _determine_cmap_params(data, levels=5,
357+
vmin=1.3, vmax=1.5)
358+
self.assertEqual(cmap_params['cmap'].name, 'viridis')
359+
self.assertEqual(cmap_params['extend'], 'both')
360+
339361
def test_list_levels(self):
340362
data = self.data + 1
341363

0 commit comments

Comments
 (0)