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

Skip to content

Fix bar datetime#25667

Merged
QuLogic merged 1 commit intomatplotlib:mainfrom
jklymak:fix-bar-datetime
May 27, 2023
Merged

Fix bar datetime#25667
QuLogic merged 1 commit intomatplotlib:mainfrom
jklymak:fix-bar-datetime

Conversation

@jklymak
Copy link
Copy Markdown
Member

@jklymak jklymak commented Apr 12, 2023

PR Summary

Closes #25654

fig, ax = plt.subplots()
start = np.array([np.datetime64('2012-01-01'), np.datetime64('2012-02-01'), np.datetime64('2012-01-15')])
stop = np.array([np.datetime64('2012-02-07'), np.datetime64('2012-02-13'), np.datetime64('2012-02-12')])
ax.bar([0, 1, 3], height=stop-start, bottom=start)

used to not assign a AutoDateFormatter to the y axis. Now it does:

fixbar

PR Checklist

Documentation and Tests

  • Has pytest style unit tests (and pytest passes)
  • Documentation is sphinx and numpydoc compliant (the docs should build without error).
  • New plotting related features are documented with examples.

Release Notes

  • New features are marked with a .. versionadded:: directive in the docstring and documented in doc/users/next_whats_new/
  • API changes are marked with a .. versionchanged:: directive in the docstring and documented in doc/api/next_api_changes/
  • Release notes conform with instructions in next_whats_new/README.rst or next_api_changes/README.rst

Comment thread lib/matplotlib/axes/_axes.py Outdated
Comment on lines +2393 to +2394
self._process_unit_info(
[("x", x), ("y", height)], kwargs, convert=False)
[("x", x), ("y", height + y)], kwargs, convert=False)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we use _convert_dx for these?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think that happens directly below, but this is the step that sets the units on the axes, and it was not working properly with timedelta + datetime (it works fine with timedelta because its just a float with a unit attached).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Change y = 0 to y = [0] (similar for x) a few lines up to get rid of the errors?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Grrrr... our units handling is such a PITA. The problem is height = [1, 2, 4], and y=bottom = 0 of course cannot be added together. y=[0] won't help that, and indeed is wildly incorrect.

Trying by extracting the first safe element from x and y. This should maybe move into the units checker...

@jklymak
Copy link
Copy Markdown
Member Author

jklymak commented Apr 12, 2023

This doesn't work for categorical in "height". (0 + 'cat') has no meaning.

I'll be honest, I don't understand why height=['cat', 'dog', 'cat'] is something anyone would want.

@jklymak
Copy link
Copy Markdown
Member Author

jklymak commented Apr 12, 2023

OK, this works, but not happy with all the try/except clauses.

Just to summarize the issues:

  1. bottom = datetime, height = timedelta: the unit checker needs to check height+bottom (=datetime). Currently it just sees timedelta, which is converted to floats and dates are not used.
  2. bottom = 0, height = []: we can't add 0+[]: proposal here is just to pass bottom like before.
  3. bottom = 0, height = ['cat', 'dog']: can't add 0 + ['cat', 'dog']: proposal here is to keep the old behaviour.

@ksunden
Copy link
Copy Markdown
Member

ksunden commented Apr 12, 2023

diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py
index f80efddfe1..89e79d35a8 100644
--- a/lib/matplotlib/axes/_axes.py
+++ b/lib/matplotlib/axes/_axes.py
@@ -2391,12 +2391,12 @@ class Axes(_AxesBase):
 
         if orientation == 'vertical':
             self._process_unit_info(
-                [("x", x), ("y", height)], kwargs, convert=False)
+                [("x", x),  ("y", height), ("y", y)], kwargs, convert=False)
             if log:
                 self.set_yscale('log', nonpositive='clip')
         else:  # horizontal
             self._process_unit_info(
-                [("x", width), ("y", y)], kwargs, convert=False)
+                [("x", width), ("x", x), ("y", y)], kwargs, convert=False)
             if log:
                 self.set_xscale('log', nonpositive='clip')
 

I think this works just as well (and perhaps can even be simplified to remove the horizontal/vertical dependence on the difference for unit behavior.

It would perhaps be odd if we were using the returns here, but this should, I belive, use the units from the first of the two with the same axis that has an associated Converter (_process_unit_info will not override units/converter if they are set, and won't set it if raw floats are passed)

There is nothing preventing passing multiple tuples for the same axis, as far as I can tell.

@jklymak
Copy link
Copy Markdown
Member Author

jklymak commented Apr 12, 2023

That does seem to work, and passes all the tests.

OTOH I worry about the axis latching onto the wrong unit (eg in height) if for instance someone makes a timedelta converter, which has been talked about quite a bit. In that case, the timedelta converter would be used because height is passed in first, but we really meant to check datetime+timedelta.

The point is "height" really should be in delta units, whereas bottom should be in absolute units. However, we have allowed 0 + 'cat', which is clearly in categorical units - or at least we have tests that do this, whether they are sensible or not is open to intepretation (and we allow empty lists, which we could probably just short circuit at the at top, so thats not really a problem)

@ksunden
Copy link
Copy Markdown
Member

ksunden commented Apr 13, 2023

If we swapped the order (i.e. did ("x", x), ("x", width) instead of ("x", width), ("x", x)) is that sufficient?

I think it may still have delta problems, but not sure it would have any more delta problems than than the try/excepts implemented here...

With try/except paths as implemented here:

If y is float (e.g. 0) and height is timedelta, then the try except will try to do 0 + <timedelta> and fail (unsupported operand types) and pass the timedelta directly to process_unit_info.

With passing both separately to process_unit_info:

The raw float value will not assign units, the timedelta will. This is the same end result.

As for categoricals, I can't think of a sensible way to have your categories be a "delta", as is needed for height with non-zero baseline.
I think allowing the ax.bar([1,2,3], ["a", "b", "c"]) case that is in the tests is relatively reasonable. A bar graph where you are cartoonizing your y axis with string labels that have no formal mathematical relationship (but perhaps have an order).

Current main already breaks if you try to pass categoricals to both bottom and height, though not in _process_unit_info (rather in _convert_units or somehow set_linewidth (‽)) I don't think either of the suggestions would change that being disallowed (and don't really think that is a problem)

@jklymak
Copy link
Copy Markdown
Member Author

jklymak commented Apr 13, 2023

OK - note that 0 + np.timedelta64(10, 'h') is OK though.

@ksunden
Copy link
Copy Markdown
Member

ksunden commented Apr 13, 2023

Even so, it still returns a timedelta64, not a datetime, so even that is still in delta form.

@jklymak jklymak force-pushed the fix-bar-datetime branch 2 times, most recently from 8226ad7 to e3bd2fb Compare April 13, 2023 21:53
@jklymak
Copy link
Copy Markdown
Member Author

jklymak commented Apr 14, 2023

New implementation follows @ksunden suggestion, which works well, and as discussed on the call, I agree is semantically correct.

Comment thread lib/matplotlib/axes/_axes.py
Comment thread lib/matplotlib/cbook.py Outdated
Comment on lines +1660 to +1664
try:
return next(val for val in obj if safe_isfinite(val))
except TypeError:
# not an iterable...
return obj
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this still necessary? It looks like the calls to _safe_first_finite were reverted.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thats true... OTOH, if we pass _safe_first_element(12) do we want it to error?

Copy link
Copy Markdown
Member

@ksunden ksunden left a comment

Choose a reason for hiding this comment

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

Modulo rebase...

FIX: bar handle datetime
FIX: more checks for empty bars, and single-entry bars
FIX: more checks for categorical
DOC: fix
@QuLogic QuLogic merged commit 042b886 into matplotlib:main May 27, 2023
@QuLogic QuLogic added this to the v3.8.0 milestone May 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: bar/barh don't trigger datetime units

4 participants