-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Support RGB[A] arrays in plot.imshow() #1796
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -258,12 +258,65 @@ def _determine_cmap_params(plot_data, vmin=None, vmax=None, cmap=None, | |
levels=levels, norm=norm) | ||
|
||
|
||
def _infer_xy_labels(darray, x, y): | ||
def _infer_xy_labels_3d(darray, x, y, rgb): | ||
""" | ||
Determine x and y labels for showing RGB images. | ||
|
||
Attempts to infer which dimension is RGB/RGBA by size and order of dims. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than all this complex logic with warnings in ambiguous cases, why not always treat the last and/or remaining (after explicit x/y labels) dimension as RGB? I think that solves the convenience use cases, without hard to understand/predict inference logic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
|
||
""" | ||
assert rgb is None or rgb != x | ||
assert rgb is None or rgb != y | ||
# Start by detecting and reporting invalid combinations of arguments | ||
assert darray.ndim == 3 | ||
not_none = [a for a in (x, y, rgb) if a is not None] | ||
if len(set(not_none)) < len(not_none): | ||
raise ValueError( | ||
'Dimension names must be None or unique strings, but imshow was ' | ||
'passed x=%r, y=%r, and rgb=%r.' % (x, y, rgb)) | ||
for label in not_none: | ||
if label not in darray.dims: | ||
raise ValueError('%r is not a dimension' % (label,)) | ||
|
||
# Then calculate rgb dimension if certain and check validity | ||
could_be_color = [label for label in darray.dims | ||
if darray[label].size in (3, 4) and label not in (x, y)] | ||
if rgb is None and not could_be_color: | ||
raise ValueError( | ||
'A 3-dimensional array was passed to imshow(), but there is no ' | ||
'dimension that could be color. At least one dimension must be ' | ||
'of size 3 (RGB) or 4 (RGBA), and not given as x or y.') | ||
if rgb is None and len(could_be_color) == 1: | ||
rgb = could_be_color[0] | ||
if rgb is not None and darray[rgb].size not in (3, 4): | ||
raise ValueError('Cannot interpret dim %r of size %s as RGB or RGBA.' | ||
% (rgb, darray[rgb].size)) | ||
|
||
# If rgb dimension is still unknown, there must be two or three dimensions | ||
# in could_be_color. We therefore warn, and use a heuristic to break ties. | ||
if rgb is None: | ||
assert len(could_be_color) in (2, 3) | ||
rgb = could_be_color[-1] | ||
warnings.warn( | ||
'Several dimensions of this array could be colors. Xarray ' | ||
'will use the last possible dimension (%r) to match ' | ||
'matplotlib.pyplot.imshow. You can pass names of x, y, ' | ||
'and/or rgb dimensions to override this guess.' % rgb) | ||
assert rgb is not None | ||
|
||
# Finally, we pick out the red slice and delegate to the 2D version: | ||
return _infer_xy_labels(darray.isel(**{rgb: 0}).squeeze(), x, y) | ||
|
||
|
||
def _infer_xy_labels(darray, x, y, imshow=False, rgb=None): | ||
""" | ||
Determine x and y labels. For use in _plot2d | ||
|
||
darray must be a 2 dimensional data array. | ||
darray must be a 2 dimensional data array, or 3d for imshow only. | ||
""" | ||
assert x is None or x != y | ||
if imshow and darray.ndim == 3: | ||
return _infer_xy_labels_3d(darray, x, y, rgb) | ||
|
||
if x is None and y is None: | ||
if darray.ndim != 2: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -619,6 +619,8 @@ def test_1d_raises_valueerror(self): | |
|
||
def test_3d_raises_valueerror(self): | ||
a = DataArray(easy_array((2, 3, 4))) | ||
if self.plotfunc.__name__ == 'imshow': | ||
pytest.skip() | ||
with raises_regex(ValueError, r'DataArray must be 2d'): | ||
self.plotfunc(a) | ||
|
||
|
@@ -670,6 +672,11 @@ def test_can_plot_axis_size_one(self): | |
if self.plotfunc.__name__ not in ('contour', 'contourf'): | ||
self.plotfunc(DataArray(np.ones((1, 1)))) | ||
|
||
def test_disallows_rgb_arg(self): | ||
with pytest.raises(ValueError): | ||
# Always invalid for most plots. Invalid for imshow with 2D data. | ||
self.plotfunc(DataArray(np.ones((2, 2))), rgb='not None') | ||
|
||
def test_viridis_cmap(self): | ||
cmap_name = self.plotmethod(cmap='viridis').get_cmap().name | ||
self.assertEqual('viridis', cmap_name) | ||
|
@@ -1062,6 +1069,52 @@ def test_2d_coord_names(self): | |
with raises_regex(ValueError, 'requires 1D coordinates'): | ||
self.plotmethod(x='x2d', y='y2d') | ||
|
||
def test_plot_rgb_image(self): | ||
DataArray( | ||
easy_array((10, 15, 3), start=0), | ||
dims=['y', 'x', 'band'], | ||
).plot.imshow() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to check that the colors have actually rendered correctly by introspecting deeper into the figures / axes that are generated by There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, it's almost certainly possible - but I have absolutely no idea where to start! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm okay relying on matplotlib to plot colors properly. One sane option would be to mock |
||
self.assertEqual(0, len(find_possible_colorbars())) | ||
|
||
def test_plot_rgb_image_explicit(self): | ||
DataArray( | ||
easy_array((10, 15, 3), start=0), | ||
dims=['y', 'x', 'band'], | ||
).plot.imshow(y='y', x='x', rgb='band') | ||
self.assertEqual(0, len(find_possible_colorbars())) | ||
|
||
def test_plot_rgb_faceted(self): | ||
DataArray( | ||
easy_array((2, 2, 10, 15, 3), start=0), | ||
dims=['a', 'b', 'y', 'x', 'band'], | ||
).plot.imshow(row='a', col='b') | ||
self.assertEqual(0, len(find_possible_colorbars())) | ||
|
||
def test_plot_rgba_image_transposed(self): | ||
# We can handle the color axis being in any position | ||
DataArray( | ||
easy_array((4, 10, 15), start=0), | ||
dims=['band', 'y', 'x'], | ||
).plot.imshow() | ||
|
||
def test_warns_ambigious_dim(self): | ||
arr = DataArray(easy_array((3, 3, 3)), dims=['y', 'x', 'band']) | ||
with pytest.warns(UserWarning): | ||
arr.plot.imshow() | ||
# but doesn't warn if dimensions specified | ||
arr.plot.imshow(rgb='band') | ||
arr.plot.imshow(x='x', y='y') | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add tests for errors related to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
def test_rgb_errors_too_many_dims(self): | ||
arr = DataArray(easy_array((3, 3, 3, 3)), dims=['y', 'x', 'z', 'band']) | ||
with pytest.raises(ValueError): | ||
arr.plot.imshow(rgb='band') | ||
|
||
def test_rgb_errors_bad_dim_sizes(self): | ||
arr = DataArray(easy_array((5, 5, 5)), dims=['y', 'x', 'band']) | ||
with pytest.raises(ValueError): | ||
arr.plot.imshow(rgb='band') | ||
|
||
|
||
class TestFacetGrid(PlotTestCase): | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add
rgb
as an actual argument inline, after x and y?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could, but this would be a breaking change for anyone using positional arguments. I'd therefore prefer not to, as it's just as easy to do the breaking bit later.
Please confirm if you'd like the breaking change; otherwise I'll leave it as-is.