8000 Fix parasite_axes does not properly handle units by weiji-li · Pull Request #22789 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content

Fix parasite_axes does not properly handle units #22789

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

Closed
wants to merge 1 commit into from

Conversation

weiji-li
Copy link
Contributor
@weiji-li weiji-li commented Apr 5, 2022

PR Summary

Right now parasite_axes just use self._parent_axes._get_lines as self._get_lines, but it can't update the axes unit when there are twin axes. Therefore, we need to provide axes=self as an extra args to handle this. We also need to change the callees to use axes in kwargs when provided.

This new added test creates a plot with twin axes where both axes have units. It then checks whether units are appended correctly on the respective axes. The code base without the modification fails the unit test whereas the modification makes it pass the unit test.

PR Checklist

Tests and Styling

  • Has pytest style unit tests (and pytest passes).
  • Is Flake 8 compliant (install flake8-docstrings and run flake8 --docstring-convention=all).

Documentation

  • [n/a] New features are documented, with examples if plot related.
  • [n/a] New features have an entry in doc/users/next_whats_new/ (follow instructions in README.rst there).
  • [n/a] API changes documented in doc/api/next_api_changes/ (follow instructions in README.rst there).
  • [n/a] Documentation is sphinx and numpydoc compliant (the docs should build without error).

Fixes #22714

@weiji-li weiji-li changed the title Fix parasite_axes does not properly handle units #22714 Fix parasite_axes does not properly handle units Apr 5, 2022
@weiji-li weiji-li changed the title Fix parasite_axes does not properly handle units Fix parasite_axes does not properly handle units #22714 Apr 5, 2022
@weiji-li weiji-li changed the title Fix parasite_axes does not properly handle units #22714 Fixed #22714 parasite_axes does not properly handle units Apr 5, 2022
@weiji-li weiji-li changed the title Fixed #22714 parasite_axes does not properly handle units Fix parasite_axes does not properly handle units Apr 5, 2022
Copy link
@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Thank you for opening your first PR into Matplotlib!

If you have not heard from us in a while, please feel free to ping @matplotlib/developers or anyone who has commented on the PR. Most of our reviewers are volunteers and sometimes things fall through the cracks.

You can also join us on gitter for real-time discussion.

For details on testing, writing docs, and our review process, please see the developer guide

We strive to be a welcoming and open project. Please follow our Code of Conduct.

@weiji-li weiji-li force-pushed the fix-unit-twin-axes branch 2 times, most recently from d653434 to a622508 Compare April 5, 2022 20:52
@oscargus oscargus added status: needs workflow approval For PRs from new contributors, from which GitHub blocks workflows by default. topic: mpl_toolkit labels Apr 6, 2022
@weiji-li
Copy link
Contributor Author
weiji-li commented Apr 6, 2022

@oscargus What do I need to do in order to make this PR "workflow approved"?

@oscargus
Copy link
Member
oscargus commented Apr 6, 2022

It is a default setting for new contributors. So once you have your first PR merged, all tests will run automatically for upcoming PRs.

@oscargus
Copy link
Member
oscargus commented Apr 6, 2022

The label is just a reminder that this needs approval to run all tests. It doesn't affect anything else.

@weiji-li
Copy link
Contributor Author
weiji-li commented Apr 9, 2022

Thank you @oscargus ! Could you or any one review this PR?

@@ -84,6 +85,43 @@ def test_twin_axes_empty_and_removed():
plt.subplots_adjust(wspace=0.5, hspace=1)


@image_comparison(['twin_axes_both_with_units.png'])
Copy link
Member

Choose a reason for hiding this comment

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

You are changing the main library, but only testing the axes_grid toolkit. If this has ramifications for the normal Axes.twinx/y please test that as well, or even preferentially.

Copy link
Contributor Author
@weiji-li weiji-li Apr 12, 2022

Choose a reason for hiding this comment

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

We just added a new optional parameter to it, which should not affect the existing functionalities. All the regression tests for the main library have been passed as well.

@oscargus
Copy link
Member

Sorry, I do not have enough knowledge about this module to be able to review it. (Just helping out with the formalities.)

Copy link
Member
@jklymak jklymak left a comment

Choose a reason for hiding this comment

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

This had to be failing the main library as well, so this needs a test of units on twin axes in the main library.

@@ -20,7 +21,8 @@ def __init__(self, parent_axes, aux_transform=None,
def cla(self):
super().cla()
martist.setp(self.get_children(), visible=False)
self._get_lines = self._parent_axes._get_lines
self._get_lines = functools.partial(
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand this change - can you explain why you are doing this?

Right now parasite_axes just use self._parent_axes._get_lines as self._get_lines, but it can't update the axes unit when there are twin axes. Therefore, we need to provide axes=self as an extra args to handle this. We also need to change the callees to use axes in kwargs when provided.

Add a unit test for missing unit in twin axes

This test creates a plot with twin axes where both axes have units. It then checks whether units are appended correctly on the respective axes. The code base without the modification fails the unit test whereas the modification makes it pass the unit test.t:q`
@ksunden
Copy link
Member
ksunden commented Dec 6, 2022

Took care of at least the rebase to keep up with changes in the library since this was opened.

I'm a little skeptical about the mechanism of using an undocumented argument to sneakily replace an instance attribute (at call time).

However, I do think I'm convinced that this is the most minimal change that addresses the problem at hand. It is at least only on an already private class.

Essentially the crux of the matter is that the _process_plot_var_args class manages a bit more of the state than perhaps it should (but extricating that without a change to behavior is a much larger change)

The most concrete thing that is kept as part of this object is the color cycle state.
Simply removing the line modified by this PR actually fixes the units behavior, but at the cost of using a separate color cycle for the parasite axes.
This is the behavior by twin[x,y] from standard axes, where the color cycle is separate but other behaviors tied to the axes such as units behavior work as expected.
If we were okay with that change in behavior, I think that would be my preferred fix for this issue, it has the benefit of being more consistent with twinx/y.

One alternative I explored, but think is fragile at best, was instead of replacing the self._get_lines object entirely, reaching in and taking the color cycle from the parent axes.
This actually works reasonably well in the short term, but would likely require quite a bit more machinery to behave as expected when set_color_cycle is called after the parasite axes are created.

The other direction that I think may be better, but haven't fully thought through is removing the axes reference from _process_plot_var_args entirely and shifting those calls into the base axes methods.

@anntzer
Copy link
Contributor
anntzer commented Jan 11, 2023

@ksunden See also #19484 re: sharing of the color cycle state. I don't think it's immediately usable in parasite_axes because right now the prop_cycle needs to be "declared" as shareable from the very beginning, but that's probably fixable, and using that in parasite_axes may be cleaner here.

On the other hand, your last suggestion (removing the axes reference from _process_plot_var_args) also seems to work well, i.e. the following patch seems good for me:

diff --git i/lib/matplotlib/axes/_axes.py w/lib/matplotlib/axes/_axes.py
index a6fecd374f..a827654100 100644
--- i/lib/matplotlib/axes/_axes.py
+++ w/lib/matplotlib/axes/_axes.py
@@ -1685,7 +1685,7 @@ class Axes(_AxesBase):
         (``'green'``) or hex strings (``'#008000'``).
         """
         kwargs = cbook.normalize_kwargs(kwargs, mlines.Line2D)
-        lines = [*self._get_lines(*args, data=data, **kwargs)]
+        lines = [*self._get_lines(self, *args, data=data, **kwargs)]
         for line in lines:
             self.add_line(line)
         if scalex:
@@ -3546,7 +3546,8 @@ class Axes(_AxesBase):
         # that would call self._process_unit_info again, and do other indirect
         # data processing.
         (data_line, base_style), = self._get_lines._plot_args(
-            (x, y) if fmt == '' else (x, y, fmt), kwargs, return_kwargs=True)
+            self, (x, y) if fmt == '' else (x, y, fmt), kwargs,
+            return_kwargs=True)
 
         # Do this after creating `data_line` to avoid modifying `base_style`.
         if barsabove:
@@ -5229,7 +5230,8 @@ default: :rc:`scatter.edgecolors`
         # For compatibility(!), get aliases from Line2D rather than Patch.
         kwargs = cbook.normalize_kwargs(kwargs, mlines.Line2D)
         # _get_patches_for_fill returns a generator, convert it to a list.
-        patches = [*self._get_patches_for_fill(*args, data=data, **kwargs)]
+        patches = [
+            *self._get_patches_for_fill(self, *args, data=data, **kwargs)]
         for poly in patches:
             self.add_patch(poly)
         self._request_autoscale_view()
diff --git i/lib/matplotlib/axes/_base.py w/lib/matplotlib/axes/_base.py
index 53f5607f41..52388a891b 100644
--- i/lib/matplotlib/axes/_base.py
+++ w/lib/matplotlib/axes/_base.py
@@ -219,14 +219,14 @@ class _process_plot_var_args:
 
     an arbitrary number of *x*, *y*, *fmt* are allowed
     """
-    def __init__(self, axes, command='plot'):
-        self.axes = axes
+
+    def __init__(self, command):
         self.command = command
         self.set_prop_cycle(None)
 
     def __getstate__(self):
         # note: it is not possible to pickle a generator (and thus a cycler).
-        return {'axes': self.axes, 'command': self.command}
+        return {'command': self.command}
 
     def __setstate__(self, state):
         self.__dict__ = state.copy()
@@ -238,8 +238,8 @@ class _process_plot_var_args:
         self.prop_cycler = itertools.cycle(cycler)
         self._prop_keys = cycler.keys  # This should make a copy
 
-    def __call__(self, *args, data=None, **kwargs):
-        self.axes._process_unit_info(kwargs=kwargs)
+    def __call__(self, axes, *args, data=None, **kwargs):
+        axes._process_unit_info(kwargs=kwargs)
 
         for pos_only in "xy":
             if pos_only in kwargs:
@@ -309,7 +309,8 @@ class _process_plot_var_args:
                 this += args[0],
                 args = args[1:]
             yield from self._plot_args(
-                this, kwargs, ambiguous_fmt_datakey=ambiguous_fmt_datakey)
+                axes, this, kwargs,
+                ambiguous_fmt_datakey=ambiguous_fmt_datakey)
 
     def get_next_color(self):
         """Return the next color in the cycle."""
@@ -344,17 +345,17 @@ class _process_plot_var_args:
             if kw.get(k, None) is None:
                 kw[k] = defaults[k]
 
-    def _makeline(self, x, y, kw, kwargs):
+    def _makeline(self, axes, x, y, kw, kwargs):
         kw = {**kw, **kwargs}  # Don't modify the original kw.
         default_dict = self._getdefaults(set(), kw)
         self._setdefaults(default_dict, kw)
         seg = mlines.Line2D(x, y, **kw)
         return seg, kw
 
-    def _makefill(self, x, y, kw, kwargs):
+    def _makefill(self, axes, x, y, kw, kwargs):
         # Polygon doesn't directly support unitized inputs.
-        x = self.axes.convert_xunits(x)
-        y = self.axes.convert_yunits(y)
+        x = axes.convert_xunits(x)
+        y = axes.convert_yunits(y)
 
         kw = kw.copy()  # Don't modify the original kw.
         kwargs = kwargs.copy()
@@ -403,7 +404,7 @@ class _process_plot_var_args:
         seg.set(**kwargs)
         return seg, kwargs
 
-    def _plot_args(self, tup, kwargs, *,
+    def _plot_args(self, axes, tup, kwargs, *,
                    return_kwargs=False, ambiguous_fmt_datakey=False):
         """
         Process the arguments of ``plot([x], y, [fmt], **kwargs)`` calls.
@@ -495,10 +496,10 @@ class _process_plot_var_args:
         else:
             x, y = index_of(xy[-1])
 
-        if self.axes.xaxis is not None:
-            self.axes.xaxis.update_units(x)
-        if self.axes.yaxis is not None:
-            self.axes.yaxis.update_units(y)
+        if axes.xaxis is not None:
+            axes.xaxis.update_units(x)
+        if axes.yaxis is not None:
+            axes.yaxis.update_units(y)
 
         if x.shape[0] != y.shape[0]:
             raise ValueError(f"x and y must have same first dimension, but "
@@ -534,7 +535,7 @@ class _process_plot_var_args:
         else:
             labels = [label] * n_datasets
 
-        result = (make_artist(x[:, j % ncx], y[:, j % ncy], kw,
+        result = (make_artist(axes, x[:, j % ncx], y[:, j % ncy], kw,
                               {**kwargs, 'label': label})
                   for j, label in enumerate(labels))
 
@@ -1295,8 +1296,8 @@ class _AxesBase(martist.Artist):
         self._tight = None
         self._use_sticky_edges = True
 
-        self._get_lines = _process_plot_var_args(self)
-        self._get_patches_for_fill = _process_plot_var_args(self, 'fill')
+        self._get_lines = _process_plot_var_args('plot')
+        self._get_patches_for_fill = _process_plot_var_args('fill')
 
         self._gridOn = mpl.rcParams['axes.grid']
         old_children, self._children = self._children, []
diff --git i/lib/mpl_toolkits/mplot3d/axes3d.py w/lib/mpl_toolkits/mplot3d/axes3d.py
index f9a4813628..a529bbc3a3 100644
--- i/lib/mpl_toolkits/mplot3d/axes3d.py
+++ w/lib/mpl_toolkits/mplot3d/axes3d.py
@@ -2993,7 +2993,8 @@ class Axes3D(Axes):
         # that would call self._process_unit_info again, and do other indirect
         # data processing.
         (data_line, base_style), = self._get_lines._plot_args(
-            (x, y) if fmt == '' else (x, y, fmt), kwargs, return_kwargs=True)
+            axes, (x, y) if fmt == '' else (x, y, fmt), kwargs,
+            return_kwargs=True)
         art3d.line_2d_to_3d(data_line, zs=z)
 
         # Do this after creating `data_line` to avoid modifying `base_style`.

@ksunden ksunden marked this pull request as draft February 22, 2023 22:40
@ksunden
Copy link
Member
ksunden commented Feb 22, 2023

Moved to draft because I think this is not fully ready and the diff proposed above by @anntzer may prove to be a better route.

@melissawm
Copy link
Member

Hello, @weiji-li - are you interested in moving this forward? Any chance you and @anntzer can collaborate on this?

@anntzer
Copy link
Contributor
anntzer commented Jun 6, 2023

Superseded by #26078. Thanks for the PR!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: needs workflow approval For PRs from new contributors, from which GitHub blocks workflows by default. topic: mpl_toolkit
Projects
Development

Successfully merging this pull request may close these issues.

[Bug]: parasite_axes does not properly handle units
6 participants
0