Skip to content
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

Surface plots #5101

Merged
merged 33 commits into from
May 3, 2021
Merged

Surface plots #5101

merged 33 commits into from
May 3, 2021

Conversation

johnomotani
Copy link
Contributor

@johnomotani johnomotani commented Mar 31, 2021

.plot.surface() method that wraps matplotlib's plot_surface() method from the 3d toolkit.

  • works with facet grids
  • disables all the automatic color-map, etc. because it doesn't necessarily make sense for surface plots - the default (in matplotlib, and as implemented now) is not to color the surface. xarray's auto-selection stuff would interfere with plot_surface()'s kwargs.

I'm not sure if there's somewhere good to note the new surface() method in the docs to make it more discoverable? Maybe in one of the examples?

  • Closes plot_surface() wrapper #5084
  • Tests added
  • Passes pre-commit run --all-files
  • User visible changes (including notable bug fixes) are documented in whats-new.rst
  • New functions/methods are listed in api.rst

Use broadcast_like if either `x` or `y` inputs are 2d to ensure that
both have dimensions in the same order as the DataArray being plotted.
Convert to numpy arrays after possibly using broadcast_like. Simplifies
code, and fixes pydata#5097 (bug when dimensions have the same size).
Wraps mpl_toolkits.mplot3d.axes3d.plot_surface
@johnomotani
Copy link
Contributor Author

Note, this PR includes the bugfix in #5099, because it needed to build on that change. #5099 is smaller, so I guess it will naturally be merged first.

@johnomotani
Copy link
Contributor Author

Seems like the way I've implemented surface plots requires a slightly newer version than the oldest supported matplotlib to use the "3d" projection. matplotlib-3.2.0 basically works, but some of the unit tests require matpltolib-3.3.0 to pass. I'll try to add a check and skip some tests where necessary.

@pep8speaks
Copy link

pep8speaks commented Apr 1, 2021

Hello @johnomotani! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found:

There are currently no PEP 8 issues detected in this Pull Request. Cheers! 🍻

Comment last updated at 2021-05-03 12:06:43 UTC

Too complicated to check matplotlib version is high enough just for
surface plots.
@johnomotani
Copy link
Contributor Author

Tests pass! Think this is ready for review now @pydata/xarray

Copy link
Collaborator

@mathause mathause left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, thanks for your patience! I left some comments but I think the implementation looks good overall. I suggest we merge #5099 first and then get this one under way. I would prefer to have the fix and the new feature in two different PRs.

xarray/plot/plot.py Outdated Show resolved Hide resolved
xarray/plot/plot.py Outdated Show resolved Hide resolved
Comment on lines 662 to 663
del LooseVersion
del mpl
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
del LooseVersion
del mpl

Not necessary.

Copy link
Contributor Author

@johnomotani johnomotani Apr 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these were necessary, because of this a bit later

xarray/xarray/plot/plot.py

Lines 672 to 673 in 648e13b

if row or col:
allargs = locals().copy()

If LooseVersion and mpl are not deleted, they are in the local environment, and end up in allargs, but are not valid keyword arguments. Moving the LooseVersion import to the top of the file fixes that, but I guess we don't want to do the same with mpl?

xarray/plot/plot.py Outdated Show resolved Hide resolved
xarray/plot/utils.py Show resolved Hide resolved
xarray/tests/test_plot.py Outdated Show resolved Hide resolved
xarray/tests/test_plot.py Outdated Show resolved Hide resolved
xarray/tests/test_plot.py Outdated Show resolved Hide resolved
pass

def test_seaborn_palette_as_cmap(self):
# seaborn does not work with mpl_toolkits.mplot3d
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not? I would not expect an interaction here? Or is it because you don't actually call the code determining the cmap?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the error. If I don't modify the test, I get

================================================================================================================== FAILURES ==================================================================================================================
__________________________________________________________________________________________________ TestSurface.test_seaborn_palette_as_cmap __________________________________________________________________________________________________

self = <xarray.tests.test_plot.TestSurface object at 0x7f5043826220>

    @requires_seaborn
    def test_seaborn_palette_as_cmap(self):
>       cmap_name = self.plotmethod(levels=2, cmap="husl").get_cmap().name

.../xarray/xarray/tests/test_plot.py:1232: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../xarray/xarray/plot/plot.py:873: in plotmethod
    return newplotfunc(**allargs)
.../xarray/xarray/plot/plot.py:775: in newplotfunc
    primitive = plotfunc(
.../xarray/xarray/plot/plot.py:1035: in surface
    primitive = ax.plot_surface(x, y, z, **kwargs)
.../python3.8/site-packages/matplotlib/_api/deprecation.py:431: in wrapper
    return func(*inner_args, **inner_kwargs)
.../python3.8/site-packages/mpl_toolkits/mplot3d/axes3d.py:1742: in plot_surface
    polyc = art3d.Poly3DCollection(polys, *args, **kwargs)
.../python3.8/site-packages/mpl_toolkits/mplot3d/art3d.py:715: in __init__
    super().__init__(verts, *args, **kwargs)
.../python3.8/site-packages/matplotlib/collections.py:1206: in __init__
    super().__init__(**kwargs)
.../python3.8/site-packages/matplotlib/_api/deprecation.py:431: in wrapper
    return func(*inner_args, **inner_kwargs)
.../python3.8/site-packages/matplotlib/collections.py:164: in __init__
    cm.ScalarMappable.__init__(self, norm, cmap)
.../python3.8/site-packages/matplotlib/cm.py:263: in __init__
    self.set_cmap(cmap)  # The Colormap instance of this ScalarMappable.
.../python3.8/site-packages/matplotlib/cm.py:432: in set_cmap
    cmap = get_cmap(cmap)
.../python3.8/site-packages/matplotlib/cm.py:190: in get_cmap
    _api.check_in_list(sorted(_cmap_registry), name=name)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

_values = ['Accent', 'Accent_r', 'Blues', 'Blues_r', 'BrBG', 'BrBG_r', ...], _print_supported_values = True, kwargs = {'name': 'husl'}, values = ['Accent', 'Accent_r', 'Blues', 'Blues_r', 'BrBG', 'BrBG_r', ...], key = 'name', val = 'husl'

    def check_in_list(_values, *, _print_supported_values=True, **kwargs):
        """
        For each *key, value* pair in *kwargs*, check that *value* is in *_values*.
    
        Parameters
        ----------
        _values : iterable
            Sequence of values to check on.
        _print_supported_values : bool, default: True
            Whether to print *_values* when raising ValueError.
        **kwargs : dict
            *key, value* pairs as keyword arguments to find in *_values*.
    
        Raises
        ------
        ValueError
            If any *value* in *kwargs* is not found in *_values*.
    
        Examples
        --------
        >>> _api.check_in_list(["foo", "bar"], arg=arg, other_arg=other_arg)
        """
        values = _values
        for key, val in kwargs.items():
            if val not in values:
                if _print_supported_values:
>                   raise ValueError(
                        f"{val!r} is not a valid value for {key}; "
                        f"supported values are {', '.join(map(repr, values))}")
E                   ValueError: 'husl' is not a valid value for name; supported values are 'Accent', 'Accent_r', 'Blues', 'Blues_r', 'BrBG', 'BrBG_r', 'BuGn', 'BuGn_r', 'BuPu', 'BuPu_r', 'CMRmap', 'CMRmap_r', 'Dark2', 'Dark2_r', 'GnBu', 'GnBu_r', 'Greens', 'Greens_r', 'Greys', 'Greys_r', 'OrRd', 'OrRd_r', 'Oranges', 'Oranges_r', 'PRGn', 'PRGn_r', 'Paired', 'Paired_r', 'Pastel1', 'Pastel1_r', 'Pastel2', 'Pastel2_r', 'PiYG', 'PiYG_r', 'PuBu', 'PuBuGn', 'PuBuGn_r', 'PuBu_r', 'PuOr', 'PuOr_r', 'PuRd', 'PuRd_r', 'Purples', 'Purples_r', 'RdBu', 'RdBu_r', 'RdGy', 'RdGy_r', 'RdPu', 'RdPu_r', 'RdYlBu', 'RdYlBu_r', 'RdYlGn', 'RdYlGn_r', 'Reds', 'Reds_r', 'Set1', 'Set1_r', 'Set2', 'Set2_r', 'Set3', 'Set3_r', 'Spectral', 'Spectral_r', 'Wistia', 'Wistia_r', 'YlGn', 'YlGnBu', 'YlGnBu_r', 'YlGn_r', 'YlOrBr', 'YlOrBr_r', 'YlOrRd', 'YlOrRd_r', 'afmhot', 'afmhot_r', 'autumn', 'autumn_r', 'binary', 'binary_r', 'bone', 'bone_r', 'brg', 'brg_r', 'bwr', 'bwr_r', 'cividis', 'cividis_r', 'cool', 'cool_r', 'coolwarm', 'coolwarm_r', 'copper', 'copper_r', 'crest', 'crest_r', 'cubehelix', 'cubehelix_r', 'flag', 'flag_r', 'flare', 'flare_r', 'gist_earth', 'gist_earth_r', 'gist_gray', 'gist_gray_r', 'gist_heat', 'gist_heat_r', 'gist_ncar', 'gist_ncar_r', 'gist_rainbow', 'gist_rainbow_r', 'gist_stern', 'gist_stern_r', 'gist_yarg', 'gist_yarg_r', 'gnuplot', 'gnuplot2', 'gnuplot2_r', 'gnuplot_r', 'gray', 'gray_r', 'hot', 'hot_r', 'hsv', 'hsv_r', 'icefire', 'icefire_r', 'inferno', 'inferno_r', 'jet', 'jet_r', 'magma', 'magma_r', 'mako', 'mako_r', 'nipy_spectral', 'nipy_spectral_r', 'ocean', 'ocean_r', 'pink', 'pink_r', 'plasma', 'plasma_r', 'prism', 'prism_r', 'rainbow', 'rainbow_r', 'rocket', 'rocket_r', 'seismic', 'seismic_r', 'spring', 'spring_r', 'summer', 'summer_r', 'tab10', 'tab10_r', 'tab20', 'tab20_r', 'tab20b', 'tab20b_r', 'tab20c', 'tab20c_r', 'terrain', 'terrain_r', 'turbo', 'turbo_r', 'twilight', 'twilight_r', 'twilight_shifted', 'twilight_shifted_r', 'viridis', 'viridis_r', 'vlag', 'vlag_r', 'winter', 'winter_r'

.../python3.8/site-packages/matplotlib/_api/__init__.py:126: ValueError

xarray/tests/test_plot.py Outdated Show resolved Hide resolved
@Illviljan
Copy link
Contributor

Illviljan commented Apr 20, 2021

I think you can remove all those matplotlib checks if you do something like this:

    subplot_kws = dict()
    if ax is None:
        # TODO: Importing Axes3D is not necessary in matplotlib >= 3.2.
        # Remove when minimum requirement of matplotlib is 3.2:
        from mpl_toolkits.mplot3d import Axes3D  # type: ignore # noqa

        subplot_kws.update(projection="3d")

I got stuck with the same thing in #4909. Importing Axes3D was the the standard way of turning on the 3d plotting functionalities. Now with newer releases it's not necessary anymore, but it's a backwards friendly way of doing it for a while longer.

Copy link
Collaborator

@mathause mathause left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you merge master so we don't get the changes from #5099. I can then have another look.

xarray/plot/plot.py Show resolved Hide resolved
@johnomotani
Copy link
Contributor Author

Thanks @mathause - master merged now.

Copy link
Collaborator

@mathause mathause left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more minor comments. It's good to go from my side afterwards. Maybe @dcherian wants to have a look?

doc/whats-new.rst Outdated Show resolved Hide resolved
xarray/plot/plot.py Outdated Show resolved Hide resolved
xarray/plot/plot.py Outdated Show resolved Hide resolved
xarray/tests/test_plot.py Outdated Show resolved Hide resolved
xarray/tests/test_plot.py Outdated Show resolved Hide resolved
Copy link
Collaborator

@mathause mathause left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! I can merge in a few days unless someone else has comments. One thing that could be nice is an example in http://xarray.pydata.org/en/stable/plotting.html - up to you.

@johnomotani
Copy link
Contributor Author

johnomotani commented Apr 29, 2021

One thing that could be nice is an example in http://xarray.pydata.org/en/stable/plotting.html - up to you.

It seemed odd to add a surface example when contour and countourf weren't shown - so I've added a new subsection for 'Other types of plot' and shown all three.

@TomNicholas TomNicholas mentioned this pull request Apr 29, 2021
13 tasks
@mathause mathause merged commit 9f11862 into pydata:master May 3, 2021
@mathause
Copy link
Collaborator

mathause commented May 3, 2021

Once again, thanks a lot for your contribution @johnomotani! (the test failure is unrelated)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

plot_surface() wrapper
4 participants