Skip to content

Update test figures for Matplotlib 3.11#2685

Open
QuLogic wants to merge 5 commits into
mainfrom
mpl311
Open

Update test figures for Matplotlib 3.11#2685
QuLogic wants to merge 5 commits into
mainfrom
mpl311

Conversation

@QuLogic

@QuLogic QuLogic commented Jun 26, 2026

Copy link
Copy Markdown
Member

Rationale

As we all know, Matplotlib changed text rendering, as well as some image resampling code, so tests are all failing when running against the latest version.

I went for replacing the images and adding a tolerance for older versions. This does result in quite high tolerances in some cases, so it is possible we may wish to ship two images for some time instead.

Implications

Compatibility with latest release of Matplotlib.

I also compressed the results with oxipng -o max, which saved:

Files processed: 57/57
Input size: 4.45 MiB (4663311 bytes)
Output size: 2.02 MiB (2121057 bytes)
Total saved: 2.42 MiB (54.52%)

Fixes #2666

@QuLogic QuLogic added this to the Next Release milestone Jun 26, 2026
@QuLogic

QuLogic commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

I've pushed a followup that changes tests that were already affected to use the mpl20 style. This is more modern and also minimizes the RMS tolerance we need in most cases. Probably these two commits can be squashed, but I kept them separate if you wish to review them by themselves.

@pytest.mark.mpl_image_compare(filename='xticks_no_transform.png', style='mpl20',
tolerance=5.20 if not _MPL_311 else 0.5)
def test_set_xticks_no_transform():
plt.rc('figure.subplot', left=0.125, right=0.9, bottom=0.1, top=0.9)

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.

Note in test_images, I went with changing the axes() call to add_axes with this rect:
https://github.com/SciTools/cartopy/pull/2685/changes#diff-ef0d6ab6699444c9c1bdbb5f5003c73a10781b3d70cec64f3009344a4d3a647dR129

I don't know if one way looks better than the other.

tolerance=5.42 if not _MPL_311 else 0.5)
def test_set_xyticks():
plt.rc('figure.subplot', left=0.125, right=0.9, bottom=0.1, top=0.9)
plt.rc('axes.formatter', limits=(-7, 7))

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.

This prevents the y-axis from changing to a smallish number with a 1e6 multiplier.

@QuLogic QuLogic marked this pull request as ready for review June 26, 2026 06:58

@rcomer rcomer left a comment

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.

Thanks for doing this @QuLogic.

I think we can get rid of the largest of these tolerances by using your text_placeholders fixture (see my comments on the images). I'm not sure about the others: I never got the hang of what counts as too high for a tolerance.

Comment thread lib/cartopy/mpl/__init__.py
@QuLogic QuLogic force-pushed the mpl311 branch 3 times, most recently from 721bba0 to 1121b71 Compare June 26, 2026 17:36
@QuLogic

QuLogic commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

Ah, it looks like the placeholders didn't handle rotation, so the boxes stick together a little differently on macOS. I'll try and see if I can get the placeholders to rotate as well.

@rcomer

rcomer commented Jun 27, 2026

Copy link
Copy Markdown
Member

Will we always need to vendor the fixture or will we be able to import it from Matplotlib when we require a high enough version?

@QuLogic

QuLogic commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

After a bit of CI debugging, I've found that on Ubuntu (x86_64) vs macOS (arm64), there are some small differences in the map boundary and subsequent gridline calculations. The map boundary difference is only somewhere below the 12th decimal place though, so that would seem not very relevant. What does differ more significantly is the calculated MultiLineString of the intersection of the latitude line with the map boundary.

On Ubuntu (x86_64), the 20°S, 0, and 40°N lines are split into 4 segments, whereas 20°N, 60°N, and 80°N are only 2 segments. On the hand, with macOS (arm64), the 60°N and 80°N lines are also split into 4. This occurs roughly at the ends +/-180° longitude, which on the LCC projection for the test is at about 60° from the top-left edge. You can see a comparison here:
projections

It may be a bit difficult to see, but there are a few extra colours along that split for the two innermost latitudes. For MultiLineString, the code then picks up to the eighth-last (not entirely sure why that limit; it should maybe be a distance long the path?) element as a possible position (see the last for loop below). When we have 4 parts, it picks a position in one of the small ones, and some slightly different points in the longer one than it would if there were only 2 parts. This causes the labels to seem to overlap, and get turned off.

elif isinstance(intersection, (sgeom.LineString,
sgeom.MultiLineString)):
if isinstance(intersection, sgeom.LineString):
intersection = [intersection]
elif len(intersection.geoms) > 4:
# If lines are parallel, there will be many intersections
# merge them to get only one for the calculations below
merged_line = shapely.line_merge(intersection)
if isinstance(merged_line, sgeom.MultiLineString):
# our merge still produced a multilinestring, so
# manually concatenate the original coordinates
xy = np.concatenate(
[inter.coords for inter in intersection.geoms], axis=0)
merged_line = shapely.LineString(xy)
intersection = [merged_line]
else:
intersection = intersection.geoms
tails = []
heads = []
for inter in intersection:
if len(inter.coords) < 2:
continue
n2 = min(len(inter.coords), 8)
tails.append(inter.coords[:n2:n2 - 1])
heads.append(inter.coords[-1:-n2 - 1:-n2 + 1])
if not tails:
continue

Now, it turns out that when we get 4 parts, each smaller part is just two elements long and an end is coincident with an end of a longer part. I'm uncertain why we end up with these extra parts (as at least the transformed latitude line is the same on both platforms, and the split is nowhere near the map boundary), but if we merge them together, then we can get consistent potential-label-positions on all platforms and they end up all being visible in the same way. As you can see in the above code, we already merge the lines if there are more than 4 parts.

I do not have a strong idea of whether we should always have one LineString, so I have only applied the merge everywhere, and not the coalescence into a single one. The only loss is to Robinson where the top 180 labels have gone missing.

@QuLogic

QuLogic commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

Will we always need to vendor the fixture or will we be able to import it from Matplotlib when we require a high enough version?

I didn't end up applying any rotation changes, so yes, we can import it when we require a high enough version.

@QuLogic

QuLogic commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

The only loss is to Robinson where the top 180 labels have gone missing.

Sigh, and now macOS produces a different result for that one... Let me see what the calculations are like.

@QuLogic

QuLogic commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

So I guess I see this Robinson case is the reason why we have that merging code for MultiLineString. The 180 lines are nearly coincident with the boundary and produce ~90-part MultiLineString of mostly two-point LineStrings. Unfortunately, the line_merge appears to sort them in a different order, which is in line with the "mild" canonical form that Shapely produces, but causes us to pick slightly different spots to attempt to label. I have tried calling normalize on the result before the manual concatenation, but it's still inconsistent. I'm beginning to wonder if there's a Shapely or GEOS bug across platforms now.

@QuLogic

QuLogic commented Jul 6, 2026

Copy link
Copy Markdown
Member Author

OK, I've had to isolate the changes in the last commit, so that if after line merging we still have to combine all the sub-geometries manually, then do that on the pre-line-merged intersection. This avoids the platform-specific re-ordering that Shapely/GEOS seems to be doing. If I can make a reproducer, I will report this to whichever upstream, but in the meantime, this will allow us to get tests working.

@rcomer

rcomer commented Jul 10, 2026

Copy link
Copy Markdown
Member

Thanks for the detailed explanations @QuLogic. These changes all makes sense to me. I think we just need to decide what to do with the packaging import and then this is good to go.

QuLogic added 5 commits July 10, 2026 12:30
I also compressed the results with `oxipng -o max`, which saved:
```
Files processed: 57/57
Input size: 4.45 MiB (4663311 bytes)
Output size: 2.02 MiB (2121057 bytes)
Total saved: 2.42 MiB (54.52%)
```
Also, use the RdBu colour map in `test_contour_label` instead of the
default (previously jet, now viridis), which fits the data better.
This seems to be different on macOS.
Sometimes, Shapely will produce a `MultiLineString` with multiple parts
that coincide (most notably on macOS for `LambertConformal`), causing
potential labels to be placed in slightly different places, and then
different ones to be visible due to overlap.

By merging the intersection of the lines and map boundary, we can ensure
that potential labels are placed in consistent spots, and there should
not be any platform-specific changes to lable visibility.

Unfortunately, there does still appear to be a bit of inconsistency
between platforms as far as normalization goes, so we need to fall back
to the original intersection result if we are merging a large number of
sub-geometries.

@rcomer rcomer left a comment

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.

Please handle the merge however you would like.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Nightlies failures: text issues

2 participants