Closed
Labels
Release criticalFor bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.status: confirmed bugtopic: contour
Milestone
Description
Bug summary
This worked up to and including Matplotlib 3.7.3 and broke in Matplotlib 3.8.0.
Data files for sample code below:
x.txt
y.txt
z.txt
Code for reproduction
import matplotlib
matplotlib.rcParams['figure.dpi'] = matplotlib.rcParams['savefig.dpi'] = 4
from matplotlib import pyplot as plt
import numpy as np
# See data files attached above: x.txt, y.txt, z.txt
x = np.loadtxt('x.txt')
y = np.loadtxt('y.txt')
z = np.loadtxt('z.txt')
ax = plt.axes()
contourset = ax.contour(x, y, z, levels=[90.0], linewidths=0.5)
ax.clabel(contourset)
plt.savefig('test.pdf')Actual outcome
Traceback (most recent call last):
File "/private/tmp/test.py", line 13, in <module>
ax.clabel(contourset)
File "/Users/lpsinger/Library/Python/3.11/lib/python/site-packages/matplotlib/axes/_axes.py", line 6550, in clabel
return CS.clabel(levels, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/lpsinger/Library/Python/3.11/lib/python/site-packages/matplotlib/contour.py", line 222, in clabel
self.labels(inline, inline_spacing)
File "/Users/lpsinger/Library/Python/3.11/lib/python/site-packages/matplotlib/contour.py", line 622, in labels
rotation, path = self._split_path_and_get_label_rotation(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/lpsinger/Library/Python/3.11/lib/python/site-packages/matplotlib/contour.py", line 436, in _split_path_and_get_label_rotation
return angle, Path(xys, codes)
^^^^^^^^^^^^^^^^
File "/Users/lpsinger/Library/Python/3.11/lib/python/site-packages/matplotlib/path.py", line 135, in __init__
raise ValueError("'codes' must be a 1D list or array with the "
ValueError: 'codes' must be a 1D list or array with the same length of 'vertices'. Your vertices have shape (2, 2) but your codes have shape (1,)
Expected outcome
Additional information
I discovered this regression in the CI pipeline from one of my own projects, in a test that sets DPI to an extremely low value for performance purposes. https://git.ligo.org/lscsoft/ligo.skymap/-/jobs/2961694
Operating system
macOS
Matplotlib Version
3.8.0
Matplotlib Backend
MacOSX
Python version
3.11.5
Jupyter version
N/A
Installation
pip
Metadata
Metadata
Assignees
Labels
Release criticalFor bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.status: confirmed bugtopic: contour
Activity
Work around regression in Matplotlib
ksunden commentedon Oct 2, 2023
One odd thing here is that pdf is a vector format and all of the items are drawn as vectors here, so it is mildly unexpected to me that the dpi affects this at all.
Now, My guess is that there is logic on how big to make the break in the contour that is based on the dpi to some extent, and that the vector formats use the same computation to make sure that placement of text is the same for them as it is for e.g. png.
The error actually occurs on the clabel call itself, so before the
savefig.When I use the pdf backend, it does not error (it also does not error at 5 dpi, for the record, and at 3 dpi gets a different error that it can't set the font size). I will also note that the positioning of the clabel text does differ between using pdf backend when calling
clabeland usingagg(or other agg based, including macos) when calling clabel and then swapping out for pdf at save time. This is because the calculations for where to put the text and break the paths is done atclabelcall time rather than draw time. (Which I think is probably fine, but either choice would lead to some potential for unexpected behavior, this way it is that changing the backend changes the figure, if it was done at draw time, it would be that changing the dpi/save format moves text around. I tend to think the latter is worse, so current behavior is correct, but it is a bit odd either way.)That said, we are generating invalid paths which should not be possible.
This does come back to #25247, which changed the inheritance of ContourSet
The
_split_path_and_get_label_rotationmethod was introduced in that PR.@anntzer, thoughts?
ksunden commentedon Oct 2, 2023
The following diff makes it so it at least does not error (debugging prints left in for right now):
Essentially what was happening was that
i0was computed as less thani1, but is used as the later index, resulting in one place where a<one point> + cc_xys[i1:i0+1] + <second_point>was appended to to the points, which was the two endpoints, because i0 was two less than i1, so the middle term was getting[5:4](i.e. empty list), but then the codes asked for[Path.LINETO] * (i0 + 2 - i1)], which gave none of those, so it only had the move to, not the line to.Ensuring that i0 is greater fixes this.
The output is not quite identical to what you had previously, though quite close:
test.pdf
@anntzer do you think this is the correct fix? perhaps the swap should happen after the floor/ceiling calls (I lean towards before, but wasn't 100% sure)?
Increase DPI in test to work around Matplotlib issue upstream
anntzer commentedon Oct 2, 2023
From a quick look I think this basically occurs if
is_closed_path and lw+2*spacing >= path_cpls[-1] - path_cpls[0](i.e. the length of the text plus the surrounding spacing is longer than the closed contour itself, leading to the indices "looping over" the whole path); I suspect it is cleaner to just explicitly test for that condition and directly return an empty path in that case, something likeAs a side point, this whole "remove parts of the contour path to draw text over it" logic is really terrible; we should really have something like knockout groups in matplotlib (i.e. in a temporary buffer, render the (unbroken) contour and draw the text over it with a surrounding box that resets the buffer to transparent, and then draw the temporary buffer to the main one). But that's a much bigger ask, of course.
ksunden commentedon Oct 3, 2023
This appears to give identical behavior to 3.7.3 (the file linked in the original post), though I think that removing is "more correct" (if a bit odd looking as there is a label with no contour at all) as I do think this is the result of the expansion from the label actually eliminating all points, just this adds two of them back.
I think flipping i0 and i1 (my first suggestion) has the same endpoints, but brings back the point in between, whereas this (and pre-3.8) ignore that point between the two endpoints it sees.
I do agree that this path manipulation/breaking is brittle at the very least, but that a more comprehensive solution is out of scope for this particular issue.
anntzer commentedon Oct 4, 2023
The last patch seems fine to me as a stopgap.