A previously-working code (that gets to plot well using other backends or matplotlib==3.2.2) fails to render a plot on Jupyter notebook when using matplotlib==3.3.0rc1. This has to do with np.nan input to plt.bar.
Some more digging, and I think the changes in #16832 are exposing a pre-existing bug in _path.cleanup_path. The code path to this error includes _path.cleanup_path being called with remove_nans=True on
which is odd, I would expect it to return something empty since the input was all NaNs. This then propagates to a point where matplotlib complains, because this path does not start with a MOVETO (ie. 1) code.
This appears to simply be a missed corner case in my rewrite of get_extents (tbh, I thought I checked the all-NaN's case, sorry...). I'll push a fix with a new test in a minute!
After some digging: in _path.h, the cleanup_path function removes NaN's using the algorithm in PathNanRemover's constructor. When this function encounters a NaN, it replaces that BezierSegment in the Path by a MOVETO (no-op) whose vertex is taken from the first control point of the next BezierSegment in the Path.
However, due to the way that PathNanRemover's logic is arranged, CLOSEPOLY commands are always passed thru as-is regardless of whether or non they are NaN (except in corner cases where there should not have been a CLOSEPOLY there in the first place, like coces=[1, 3, 79, 0])
Our C++ implementation of update_path_extents doesn't mind this, because it doesn't actually deal correctly with Path's that contain curves, so it can just go through the control points one by one and if it finds a CLOSEPOLY, it knows that by definition it has already seen that control point before, so it can just ignore it:
if ((code & agg::path_cmd_end_poly) == agg::path_cmd_end_poly) {
continue;
}
However, the new Path.get_extents does correctly deal with arbitrary Paths, but to do so it relies on Path.cleaned to return a valid Path, (which it currently does not, per the above).
As pointed out by David, this is definitely a bug in Path.cleaned. I initially was tempted to just patch Path.iter_bezier to look out for CLOSEPOLY commands that contain a NaN explicitly and just skip them (hence my comment above), but I think the correct solution is to just fix PathNanRemover. Thankfully, the algorithm it uses is very straightforward so this shouldn't be too hard....
Activity
dstansby commented on Jul 9, 2020
I can reproduce this with the current master branch in a jupyter notebook.
dstansby commented on Jul 9, 2020
Bisects to 95a530c
dstansby commented on Jul 9, 2020
Some more digging, and I think the changes in #16832 are exposing a pre-existing bug in _path.cleanup_path. The code path to this error includes _path.cleanup_path being called with remove_nans=True on
and it is returning
which is odd, I would expect it to return something empty since the input was all NaNs. This then propagates to a point where matplotlib complains, because this path does not start with a MOVETO (ie. 1) code.
QuLogic commented on Jul 9, 2020
cc @brunobeltran
brunobeltran commented on Jul 10, 2020
This appears to simply be a missed corner case in my rewrite of get_extents (tbh, I thought I checked the all-NaN's case, sorry...). I'll push a fix with a new test in a minute!
brunobeltran commented on Jul 11, 2020
After some digging: in _path.h, the cleanup_path function removes NaN's using the algorithm in PathNanRemover's constructor. When this function encounters a NaN, it replaces that BezierSegment in the Path by a MOVETO (no-op) whose vertex is taken from the first control point of the next BezierSegment in the Path.
However, due to the way that PathNanRemover's logic is arranged, CLOSEPOLY commands are always passed thru as-is regardless of whether or non they are NaN (except in corner cases where there should not have been a CLOSEPOLY there in the first place, like coces=[1, 3, 79, 0])
Our C++ implementation of update_path_extents doesn't mind this, because it doesn't actually deal correctly with Path's that contain curves, so it can just go through the control points one by one and if it finds a CLOSEPOLY, it knows that by definition it has already seen that control point before, so it can just ignore it:
However, the new Path.get_extents does correctly deal with arbitrary Paths, but to do so it relies on Path.cleaned to return a valid Path, (which it currently does not, per the above).
As pointed out by David, this is definitely a bug in Path.cleaned. I initially was tempted to just patch Path.iter_bezier to look out for CLOSEPOLY commands that contain a NaN explicitly and just skip them (hence my comment above), but I think the correct solution is to just fix PathNanRemover. Thankfully, the algorithm it uses is very straightforward so this shouldn't be too hard....