Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

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

Open
jklymak opened this issue Nov 7, 2017 · 11 comments
Open

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

jklymak opened this issue Nov 7, 2017 · 11 comments
Labels
keep Items to be ignored by the “Stale” Github Action topic: units and array ducktypes

Comments

@jklymak
Copy link
Member

jklymak commented Nov 7, 2017

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:
@jklymak
Copy link
Member Author

jklymak commented Nov 7, 2017

Crossref #9336

@jklymak
Copy link
Member Author

jklymak commented Nov 9, 2017

@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>

@story645
Copy link
Member

story645 commented Nov 9, 2017

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

@anntzer
Copy link
Contributor

anntzer commented Nov 9, 2017

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

@jklymak
Copy link
Member Author

jklymak commented Nov 9, 2017

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.

@jklymak
Copy link
Member Author

jklymak commented Nov 9, 2017

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.

@jklymak
Copy link
Member Author

jklymak commented May 6, 2022

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

@github-actions
Copy link

github-actions bot commented May 7, 2023

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!

@github-actions github-actions bot added the status: inactive Marked by the “Stale” Github Action label May 7, 2023
@story645 story645 removed the status: inactive Marked by the “Stale” Github Action label May 8, 2023
@QuLogic
Copy link
Member

QuLogic commented May 8, 2023

This is probably something for @ksunden?

@ksunden
Copy link
Member

ksunden commented May 9, 2023

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)

Copy link

github-actions bot commented Jun 5, 2024

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!

@github-actions github-actions bot added the status: inactive Marked by the “Stale” Github Action label Jun 5, 2024
@story645 story645 removed the status: inactive Marked by the “Stale” Github Action label Jun 5, 2024
@story645 story645 added the keep Items to be ignored by the “Stale” Github Action label Jun 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
keep Items to be ignored by the “Stale” Github Action topic: units and array ducktypes
Projects
None yet
Development

No branches or pull requests

6 participants