Skip to content

Commit a7d17f7

Browse files
committed
Merge branch 'master' of https://github.com/matplotlib/matplotlib into issue_10267
2 parents d805368 + 5e6ce5c commit a7d17f7

File tree

10 files changed

+170
-47
lines changed

10 files changed

+170
-47
lines changed
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
`Axes.imshow` clips RGB values to the valid range
2+
-------------------------------------------------
3+
4+
When `Axes.imshow` is passed an RGB or RGBA value with out-of-range
5+
values, it now logs a warning and clips them to the valid range.
6+
The old behaviour, wrapping back in to the range, often hid outliers
7+
and made interpreting RGB images unreliable.

doc/users/credits.rst

+1
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ Yu Feng,
386386
Yunfei Yang,
387387
Yuri D'Elia,
388388
Yuval Langer,
389+
Zac Hatfield-Dodds,
389390
Zach Pincus,
390391
Zair Mubashar,
391392
alex,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
`Axes.imshow` clips RGB values to the valid range
2+
-------------------------------------------------
3+
4+
When `Axes.imshow` is passed an RGB or RGBA value with out-of-range
5+
values, it now logs a warning and clips them to the valid range.
6+
The old behaviour, wrapping back in to the range, often hid outliers
7+
and made interpreting RGB images unreliable.

lib/matplotlib/_constrained_layout.py

+30-16
Original file line numberDiff line numberDiff line change
@@ -361,15 +361,19 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad,
361361
'bottom')
362362

363363
###########
364-
# Now we make the widths and heights similar.
364+
# Now we make the widths and heights of position boxes
365+
# similar. (i.e the spine locations)
365366
# This allows vertically stacked subplots to have
366-
# different sizes if they occupy different ammounts
367+
# different sizes if they occupy different amounts
367368
# of the gridspec: i.e.
368369
# gs = gridspec.GridSpec(3,1)
369370
# ax1 = gs[0,:]
370371
# ax2 = gs[1:,:]
371372
# then drows0 = 1, and drowsC = 2, and ax2
372373
# should be at least twice as large as ax1.
374+
# But it can be more than twice as large because
375+
# it needs less room for the labeling.
376+
#
373377
# For height, this only needs to be done if the
374378
# subplots share a column. For width if they
375379
# share a row.
@@ -387,31 +391,41 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad,
387391
dcolsC = (colnumCmax - colnumCmin + 1)
388392
dcols0 = (colnum0max - colnum0min + 1)
389393

390-
if drowsC > drows0:
394+
if height0 > heightC:
391395
if in_same_column(ss0, ssc):
392396
ax._poslayoutbox.constrain_height_min(
393-
axc._poslayoutbox.height * drows0 * height0
394-
/ drowsC / heightC)
395-
elif drowsC < drows0:
396-
if in_same_column(ss0, ssc):
397+
axc._poslayoutbox.height * height0 / heightC)
398+
# these constraints stop the smaller axes from
399+
# being allowed to go to zero height...
397400
axc._poslayoutbox.constrain_height_min(
398-
ax._poslayoutbox.height * drowsC * heightC
399-
/ drows0 / drowsC)
401+
ax._poslayoutbox.height * heightC /
402+
(height0*1.8))
400403
else:
404+
if in_same_column(ss0, ssc):
405+
axc._poslayoutbox.constrain_height_min(
406+
ax._poslayoutbox.height * heightC / height0)
407+
ax._poslayoutbox.constrain_height_min(
408+
ax._poslayoutbox.height * height0 /
409+
(heightC*1.8))
410+
if drows0 == drowsC:
401411
ax._poslayoutbox.constrain_height(
402412
axc._poslayoutbox.height * height0 / heightC)
403413
# widths...
404-
if dcolsC > dcols0:
414+
if width0 > widthC:
405415
if in_same_row(ss0, ssc):
406416
ax._poslayoutbox.constrain_width_min(
407-
axc._poslayoutbox.width * dcols0 * width0
408-
/ dcolsC / widthC)
409-
elif dcolsC < dcols0:
410-
if in_same_row(ss0, ssc):
417+
axc._poslayoutbox.width * width0 / widthC)
411418
axc._poslayoutbox.constrain_width_min(
412-
ax._poslayoutbox.width * dcolsC * widthC
413-
/ dcols0 / width0)
419+
ax._poslayoutbox.width * widthC /
420+
(width0*1.8))
414421
else:
422+
if in_same_row(ss0, ssc):
423+
axc._poslayoutbox.constrain_width_min(
424+
ax._poslayoutbox.width * widthC / width0)
425+
ax._poslayoutbox.constrain_width_min(
426+
axc._poslayoutbox.width * width0 /
427+
(widthC*1.8))
428+
if dcols0 == dcolsC:
415429
ax._poslayoutbox.constrain_width(
416430
axc._poslayoutbox.width * width0 / widthC)
417431

lib/matplotlib/axes/_axes.py

+64-29
Original file line numberDiff line numberDiff line change
@@ -1928,37 +1928,63 @@ def step(self, x, y, *args, **kwargs):
19281928
"""
19291929
Make a step plot.
19301930
1931+
Call signatures::
1932+
1933+
step(x, y, [fmt], *, data=None, where='pre', **kwargs)
1934+
step(x, y, [fmt], x2, y2, [fmt2], ..., *, where='pre', **kwargs)
1935+
1936+
This is just a thin wrapper around `.plot` which changes some
1937+
formatting options. Most of the concepts and parameters of plot can be
1938+
used here as well.
1939+
19311940
Parameters
19321941
----------
19331942
x : array_like
1934-
1-D sequence, and it is assumed, but not checked,
1935-
that it is uniformly increasing.
1943+
1-D sequence of x positions. It is assumed, but not checked, that
1944+
it is uniformly increasing.
19361945
19371946
y : array_like
1938-
1-D sequence.
1947+
1-D sequence of y levels.
1948+
1949+
fmt : str, optional
1950+
A format string, e.g. 'g' for a green line. See `.plot` for a more
1951+
detailed description.
1952+
1953+
Note: While full format strings are accepted, it is recommended to
1954+
only specify the color. Line styles are currently ignored (use
1955+
the keyword argument *linestyle* instead). Markers are accepted
1956+
and plotted on the given positions, however, this is a rarely
1957+
needed feature for step plots.
1958+
1959+
data : indexable object, optional
1960+
An object with labelled data. If given, provide the label names to
1961+
plot in *x* and *y*.
1962+
1963+
where : {'pre', 'post', 'mid'}, optional, default 'pre'
1964+
Define where the steps should be placed:
1965+
1966+
- 'pre': The y value is continued constantly to the left from
1967+
every *x* position, i.e. the interval ``(x[i-1], x[i]]`` has the
1968+
value ``y[i]``.
1969+
- 'post': The y value is continued constantly to the right from
1970+
every *x* position, i.e. the interval ``[x[i], x[i+1])`` has the
1971+
value ``y[i]``.
1972+
- 'mid': Steps occur half-way between the *x* positions.
19391973
19401974
Returns
19411975
-------
1942-
list
1943-
List of lines that were added.
1976+
lines
1977+
A list of `.Line2D` objects representing the plotted data.
19441978
19451979
Other Parameters
19461980
----------------
1947-
where : [ 'pre' | 'post' | 'mid' ]
1948-
If 'pre' (the default), the interval from
1949-
``x[i]`` to ``x[i+1]`` has level ``y[i+1]``.
1950-
1951-
If 'post', that interval has level ``y[i]``.
1952-
1953-
If 'mid', the jumps in *y* occur half-way between the
1954-
*x*-values.
1981+
**kwargs
1982+
Additional parameters are the same as those for `.plot`.
19551983
19561984
Notes
19571985
-----
1958-
Additional parameters are the same as those for
1959-
:func:`~matplotlib.pyplot.plot`.
1986+
.. [notes section required to get data note injection right]
19601987
"""
1961-
19621988
where = kwargs.pop('where', 'pre')
19631989
if where not in ('pre', 'post', 'mid'):
19641990
raise ValueError("'where' argument to step must be "
@@ -4994,10 +5020,12 @@ def fill_between(self, x, y1, y2=0, where=None, interpolate=False,
49945020
step will occur:
49955021
49965022
- 'pre': The y value is continued constantly to the left from
4997-
every *x* position.
5023+
every *x* position, i.e. the interval ``(x[i-1], x[i]]`` has the
5024+
value ``y[i]``.
49985025
- 'post': The y value is continued constantly to the right from
4999-
every *x* position.
5000-
- 'mid': Steps occur in the middle between the *x* positions.
5026+
every *x* position, i.e. the interval ``[x[i], x[i+1])`` has the
5027+
value ``y[i]``.
5028+
- 'mid': Steps occur half-way between the *x* positions.
50015029
50025030
Other Parameters
50035031
----------------
@@ -5175,11 +5203,13 @@ def fill_betweenx(self, y, x1, x2=0, where=None,
51755203
i.e. constant in between *y*. The value determines where the
51765204
step will occur:
51775205
5178-
- 'pre': The y value is continued constantly below every *y*
5179-
position.
5180-
- 'post': The y value is continued constantly above every *y*
5181-
position.
5182-
- 'mid': Steps occur in the middle between the *y* positions.
5206+
- 'pre': The y value is continued constantly to the left from
5207+
every *x* position, i.e. the interval ``(x[i-1], x[i]]`` has the
5208+
value ``y[i]``.
5209+
- 'post': The y value is continued constantly to the right from
5210+
every *x* position, i.e. the interval ``[x[i], x[i+1])`` has the
5211+
value ``y[i]``.
5212+
- 'mid': Steps occur half-way between the *x* positions.
51835213
51845214
Other Parameters
51855215
----------------
@@ -5319,10 +5349,14 @@ def imshow(self, X, cmap=None, norm=None, aspect=None,
53195349
- MxNx3 -- RGB (float or uint8)
53205350
- MxNx4 -- RGBA (float or uint8)
53215351
5322-
The value for each component of MxNx3 and MxNx4 float arrays
5323-
should be in the range 0.0 to 1.0. MxN arrays are mapped
5324-
to colors based on the `norm` (mapping scalar to scalar)
5325-
and the `cmap` (mapping the normed scalar to a color).
5352+
MxN arrays are mapped to colors based on the `norm` (mapping
5353+
scalar to scalar) and the `cmap` (mapping the normed scalar to
5354+
a color).
5355+
5356+
Elements of RGB and RGBA arrays represent pixels of an MxN image.
5357+
All values should be in the range [0 .. 1] for floats or
5358+
[0 .. 255] for integers. Out-of-range values will be clipped to
5359+
these bounds.
53265360
53275361
cmap : `~matplotlib.colors.Colormap`, optional, default: None
53285362
If None, default to rc `image.cmap` value. `cmap` is ignored
@@ -5364,7 +5398,8 @@ def imshow(self, X, cmap=None, norm=None, aspect=None,
53645398
settings for `vmin` and `vmax` will be ignored.
53655399
53665400
alpha : scalar, optional, default: None
5367-
The alpha blending value, between 0 (transparent) and 1 (opaque)
5401+
The alpha blending value, between 0 (transparent) and 1 (opaque).
5402+
The ``alpha`` argument is ignored for RGBA input data.
53685403
53695404
origin : ['upper' | 'lower'], optional, default: None
53705405
Place the [0,0] index of the array in the upper left or lower left

lib/matplotlib/cm.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ def to_rgba(self, x, alpha=None, bytes=False, norm=True):
259259
xx = (xx * 255).astype(np.uint8)
260260
elif xx.dtype == np.uint8:
261261
if not bytes:
262-
xx = xx.astype(float) / 255
262+
xx = xx.astype(np.float32) / 255
263263
else:
264264
raise ValueError("Image RGB array must be uint8 or "
265265
"floating point; found %s" % xx.dtype)

lib/matplotlib/image.py

+20
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from math import ceil
1515
import os
16+
import logging
1617

1718
import numpy as np
1819

@@ -34,6 +35,8 @@
3435
from matplotlib.transforms import (Affine2D, BboxBase, Bbox, BboxTransform,
3536
IdentityTransform, TransformedBbox)
3637

38+
_log = logging.getLogger(__name__)
39+
3740
# map interpolation strings to module constants
3841
_interpd_ = {
3942
'none': _image.NEAREST, # fall back to nearest when not supported
@@ -623,6 +626,23 @@ def set_data(self, A):
623626
or self._A.ndim == 3 and self._A.shape[-1] in [3, 4]):
624627
raise TypeError("Invalid dimensions for image data")
625628

629+
if self._A.ndim == 3:
630+
# If the input data has values outside the valid range (after
631+
# normalisation), we issue a warning and then clip X to the bounds
632+
# - otherwise casting wraps extreme values, hiding outliers and
633+
# making reliable interpretation impossible.
634+
high = 255 if np.issubdtype(self._A.dtype, np.integer) else 1
635+
if self._A.min() < 0 or high < self._A.max():
636+
_log.warning(
637+
'Clipping input data to the valid range for imshow with '
638+
'RGB data ([0..1] for floats or [0..255] for integers).'
639+
)
640+
self._A = np.clip(self._A, 0, high)
641+
# Cast unsupported integer types to uint8
642+
if self._A.dtype != np.uint8 and np.issubdtype(self._A.dtype,
643+
np.integer):
644+
self._A = self._A.astype(np.uint8)
645+
626646
self._imcache = None
627647
self._rgbacache = None
628648
self.stale = True

lib/matplotlib/tests/test_constrainedlayout.py

+18
Original file line numberDiff line numberDiff line change
@@ -345,3 +345,21 @@ def test_constrained_layout16():
345345
fig, ax = plt.subplots(constrained_layout=True)
346346
example_plot(ax, fontsize=12)
347347
ax2 = fig.add_axes([0.2, 0.2, 0.4, 0.4])
348+
349+
350+
@image_comparison(baseline_images=['constrained_layout17'],
351+
extensions=['png'])
352+
def test_constrained_layout17():
353+
'Test uneven gridspecs'
354+
fig = plt.figure(constrained_layout=True)
355+
gs = gridspec.GridSpec(3, 3, figure=fig)
356+
357+
ax1 = fig.add_subplot(gs[0, 0])
358+
ax2 = fig.add_subplot(gs[0, 1:])
359+
ax3 = fig.add_subplot(gs[1:, 0:2])
360+
ax4 = fig.add_subplot(gs[1:, -1])
361+
362+
example_plot(ax1)
363+
example_plot(ax2)
364+
example_plot(ax3)
365+
example_plot(ax4)

lib/matplotlib/tests/test_image.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,7 @@ def test_minimized_rasterized():
620620
def test_load_from_url():
621621
req = six.moves.urllib.request.urlopen(
622622
"http://matplotlib.org/_static/logo_sidebar_horiz.png")
623-
Z = plt.imread(req)
623+
plt.imread(req)
624624

625625

626626
@image_comparison(baseline_images=['log_scale_image'],
@@ -813,6 +813,27 @@ def test_imshow_no_warn_invalid():
813813
assert len(warns) == 0
814814

815815

816+
@pytest.mark.parametrize(
817+
'dtype', [np.dtype(s) for s in 'u2 u4 i2 i4 i8 f4 f8'.split()])
818+
def test_imshow_clips_rgb_to_valid_range(dtype):
819+
arr = np.arange(300, dtype=dtype).reshape((10, 10, 3))
820+
if dtype.kind != 'u':
821+
arr -= 10
822+
too_low = arr < 0
823+
too_high = arr > 255
824+
if dtype.kind == 'f':
825+
arr = arr / 255
826+
_, ax = plt.subplots()
827+
out = ax.imshow(arr).get_array()
828+
assert (out[too_low] == 0).all()
829+
if dtype.kind == 'f':
830+
assert (out[too_high] == 1).all()
831+
assert out.dtype.kind == 'f'
832+
else:
833+
assert (out[too_high] == 255).all()
834+
assert out.dtype == np.uint8
835+
836+
816837
@image_comparison(baseline_images=['imshow_flatfield'],
817838
remove_text=True, style='mpl20',
818839
extensions=['png'])

0 commit comments

Comments
 (0)