Skip to content

Units handling different with plot than other functions... #9713

Open

Description

@jklymak

Bug report

Units always get reset for plot whereas scatter (for instance) doesn't reset units...

Code for reproduction

This leads to inconsistent behaviour between plot and (for instance) scatter:

import matplotlib
matplotlib.use('Qt5Agg')
import numpy as np
import matplotlib.pyplot as plt
from basic_units import secs, hertz, minutes

# create masked array
data = (1, 2, 3, 4, 5, 6, 7, 8)
mask = (1, 0, 1, 0, 0, 0, 1, 0)
xsecs = secs * np.ma.MaskedArray(data, mask, float)
xsecs2 = secs * np.ma.MaskedArray(np.array(data)+2, mask, float)
fig, (ax0, ax1, ax2, ax3) = plt.subplots(4, 1)

ax0.plot(xsecs, xsecs2, yunits=secs)

ax1.plot(xsecs, xsecs2, yunits=secs)
ax1.plot(xsecs, xsecs, yunits=minutes)

ax2.scatter(xsecs, xsecs2, yunits=secs)

ax3.scatter(xsecs, xsecs2, yunits=secs)
ax3.scatter(xsecs, xsecs, yunits=minutes)

plt.setp([ax0, ax1, ax2, ax3], 'ylim', [0, 12])
plt.show()

Actual outcome

  1. Neither case makes much sense
  2. The cases don't make sense in different ways.

figure_1

Expected outcome

The fact that neither case makes sense is maybe the fault of basic_units.py. But that asside, the fact that they are wrong in different ways is because plot and scatter handle units differently.

There is a discussion here: #9705 (comment) but basically plot updates units no matter if the units have already been set for an axis, whereas scatter and other functions do not updat ethe units if they have already been set.

This was the fundamental cause of the error in #9705 not being caught.

I think plot should be made consistent w/ the other plotting functions and only update the units if they have not been set.

I think future unit handlers should decide what to do if the new axis has different units.

Matplotlib version

  • Operating system:
  • Matplotlib version:
  • Matplotlib backend (print(matplotlib.get_backend())):
  • Python version:
  • Jupyter version (if applicable):
  • Other libraries:

Activity

  1. jklymak commented on Nov 7, 2017

    @jklymak
    MemberAuthor

    Crossref #9336

  2. jklymak commented on Nov 9, 2017

    @jklymak
    MemberAuthor

    @story645 Here is what I found most confusing: that we bother setting the axis units at all. We set the x-axis units, but then the next call to plot ignores whatever it is set to and re-translates the new values for x into floats using whatever the registry says to use. I guess thats good so we can plot different types of date objects on the same axis. But then why set the x-axis to have certain units?

    Consider

    import pandas
    import numpy as np
    import matplotlib.pyplot as plt
    import matplotlib.dates as mdates
    import datetime
    # import pandas.plotting
    
    base = datetime.datetime.today()
    time = [base - datetime.timedelta(days=x) for x in range(0, 3)]
    data = np.random.rand( 3)
    
    fig, ax = plt.subplots(tight_layout=True)
    ax.plot(time, data)
    print('xaxis converter: ',ax.xaxis.converter)
    ax.plot(['a', 'b'], [1., 2.])
    print('xaxis converter: ',ax.xaxis.converter)
    plt.show()

    It yields a nonsense plot and

    xaxis converter:  <pandas.plotting._converter.DatetimeConverter object at 0x10ab8b0b8>
    xaxis converter:  <matplotlib.category.StrCategoryConverter object at 0x10c04c6a0>
    

    If I don't import pandas:

    xaxis converter:  <matplotlib.dates.DateConverter object at 0x10c4bae48>
    xaxis converter:  <matplotlib.category.StrCategoryConverter object at 0x10c454940>
    
  3. story645 commented on Nov 9, 2017

    @story645
    Member

    I think that's the point @anntzer was also making that once units are set, all plots on that axis should always be set.

  4. anntzer commented on Nov 9, 2017

    @anntzer
    Contributor

    (including when we are using the implicit, "pass-through" scalar converter -- I think this should disable setting another converter)

  5. jklymak commented on Nov 9, 2017

    @jklymak
    MemberAuthor

    That’d make a lot more sense modolo maybe always being able to plot an array of floats. If you want to mix types, either your converter can handle all the types you want to mix or you do the conversion by hand.

    I think the only hang up is the converter gets called a bunch of times that don’t have real data in them.

  6. jklymak commented on Nov 9, 2017

    @jklymak
    MemberAuthor

    As above, _base.cla() calls set_xlim(0, 1) which calls _process_unit_data(xdata=(0, 1) ) which of course calls self.xaxis.update_units(xdata). So, before the user has even had a chance to register a converter with the axis it has been attempted to set it (though it gets set to None, which is the pass through).

    We could:

    1. make the cla calls be less intrusive (either with a flag or?) and then make the first real call to get_units set the units.
    2. Allow None passthrough to always work, but lockout other converters.

    I really don't understand the code in

    def _process_unit_info(self, xdata=None, ydata=None, kwargs=None):
    which seems very circular.

  7. added this to the milestone on Jan 23, 2018
  8. modified the milestones: v3.0, v3.1 on Aug 11, 2018
  9. modified the milestones: v3.1.0, v3.2.0 on Mar 18, 2019
  10. jklymak commented on May 6, 2022

    @jklymak
    MemberAuthor

    Here is another test based on our Quantities test in test_units.py; basically the take home is that unit conversion is still almost completely broken:

    import matplotlib.pyplot as plt
    import matplotlib.units as munits
    import numpy as np
    import matplotlib as mpl
    import xarray as xr
    
    
    
    class Quantity:
        def __init__(self, data, units):
            self.magnitude = data
            self.units = units
    
        def to(self, new_units):
            factors = {('hours', 'seconds'): 3600, ('minutes', 'hours'): 1 / 60,
                       ('minutes', 'seconds'): 60, ('feet', 'miles'): 1 / 5280.,
                       ('feet', 'inches'): 12, ('miles', 'inches'): 12 * 5280}
            if self.units != new_units:
                mult = factors[self.units, new_units]
                return Quantity(mult * self.magnitude, new_units)
            else:
                return Quantity(self.magnitude, self.units)
    
        def __copy__(self):
            return Quantity(self.magnitude, self.units)
    
        def __getattr__(self, attr):
            return getattr(self.magnitude, attr)
    
        def __getitem__(self, item):
            if np.iterable(self.magnitude):
                return Quantity(self.magnitude[item], self.units)
            else:
                return Quantity(self.magnitude, self.units)
    
        def __array__(self):
            return np.asarray(self.magnitude)
    
    
    class quantity_converter(munits.ConversionInterface):
        def convert(value, unit, axis):
            if hasattr(value, 'units'):
                return value.to(unit).magnitude
            elif np.iterable(value):
                try:
                    return [v.to(unit).magnitude for v in value]
                except AttributeError:
                    return [Quantity(v, axis.get_units()).to(unit).magnitude
                            for v in value]
            else:
                return Quantity(value, axis.get_units()).to(unit).magnitude
    
        def default_units(value, axis):
            if hasattr(value, 'units'):
                return value.units
            elif np.iterable(value):
                for v in value:
                    if hasattr(v, 'units'):
                        return v.units
                return None
    
    
    munits.registry[Quantity] = quantity_converter
    
    # Simple test
    y = Quantity(np.linspace(0, 30), 'miles')
    x = Quantity(np.linspace(0, 5), 'hours')
    
    fig, axs = plt.subplots(2, 1)
    ax = axs[0]
    ax.plot(x, y, 'd', color='tab:blue')
    ax.yaxis.set_units('inches')
    ax.xaxis.set_units('seconds')
    ax.set_xlim([0, 17500])
    ax.set_ylim([0, 2e6])
    
    ax = axs[1]
    ax.scatter(x, y)
    ax.yaxis.set_units('inches')
    ax.xaxis.set_units('seconds')
    ax.set_xlim([0, 17500])
    ax.set_ylim([0, 2e6])
    plt.show()

    Figure_1

  11. github-actions commented on May 7, 2023

    @github-actions

    This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear. Thanks for your help!

  12. QuLogic commented on May 8, 2023

    @QuLogic
    Member

    This is probably something for @ksunden?

  13. ksunden commented on May 9, 2023

    @ksunden
    Member

    Resolving this kind of discrepancy in the API is one of the overarching goals of the matplotlib/data-prototype work.

    Resolving in the shorter term is likely a challenge to do without unintended side-effects. At its crux, I think this is due to some artists (scatter in particular, though I believe there are several others) which use the offsets feature of PathCollection and that gets resolved to floats early (and thus even if it passes through a convert_[xy]units call at draw time, no conversion will actually happen)

  14. github-actions commented on Jun 5, 2024

    @github-actions

    This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear. Thanks for your help!

  15. added
    keepItems to be ignored by the “Stale” Github Action
    and removed
    status: inactiveMarked by the “Stale” Github Action
    on Jun 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions