Skip to content

Commit 48abfc2

Browse files
committed
MNT: update contour for mpl v3.8
1 parent c4858e4 commit 48abfc2

File tree

5 files changed

+104
-65
lines changed

5 files changed

+104
-65
lines changed

lib/cartopy/crs.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1249,7 +1249,10 @@ def quick_vertices_transform(self, vertices, src_crs):
12491249
"""
12501250
return_value = None
12511251

1252-
if self == src_crs:
1252+
if vertices.size == 0:
1253+
return_value = vertices
1254+
1255+
elif self == src_crs:
12531256
x = vertices[:, 0]
12541257
y = vertices[:, 1]
12551258
# Extend the limits a tiny amount to allow for precision mistakes

lib/cartopy/mpl/contour.py

Lines changed: 76 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
# See COPYING and COPYING.LESSER in the root of the repository for full
55
# licensing details.
66

7-
7+
import matplotlib as mpl
88
from matplotlib.contour import QuadContourSet
99
import matplotlib.path as mpath
1010
import numpy as np
11+
import packaging
1112

1213

1314
class GeoContourSet(QuadContourSet):
@@ -20,66 +21,91 @@ class GeoContourSet(QuadContourSet):
2021
# fiddling with instance.__class__.
2122

2223
def clabel(self, *args, **kwargs):
23-
# nb: contour labelling does not work very well for filled
24-
# contours - it is recommended to only label line contours.
25-
# This is especially true when inline=True.
26-
27-
# This wrapper exist because mpl does not properly transform
28-
# paths. Instead it simply assumes one path represents one polygon
29-
# (not necessarily the case), and it assumes that
30-
# transform(path.verts) is equivalent to transform_path(path).
31-
# Unfortunately there is no way to easily correct this error,
32-
# so we are forced to pre-transform the ContourSet's paths from
33-
# the source coordinate system to the axes' projection.
34-
# The existing mpl code then has a much simpler job of handling
35-
# pre-projected paths (which can now effectively be transformed
36-
# naively).
37-
38-
for col in self.collections:
39-
# Snaffle the collection's path list. We will change the
40-
# list in-place (as the contour label code does in mpl).
41-
paths = col.get_paths()
24+
if packaging.version.parse(mpl.__version__).release[:2] < (3, 8):
25+
# nb: contour labelling does not work very well for filled
26+
# contours - it is recommended to only label line contours.
27+
# This is especially true when inline=True.
28+
29+
# This wrapper exist because mpl does not properly transform
30+
# paths. Instead it simply assumes one path represents one polygon
31+
# (not necessarily the case), and it assumes that
32+
# transform(path.verts) is equivalent to transform_path(path).
33+
# Unfortunately there is no way to easily correct this error,
34+
# so we are forced to pre-transform the ContourSet's paths from
35+
# the source coordinate system to the axes' projection.
36+
# The existing mpl code then has a much simpler job of handling
37+
# pre-projected paths (which can now effectively be transformed
38+
# naively).
39+
40+
for col in self.collections:
41+
# Snaffle the collection's path list. We will change the
42+
# list in-place (as the contour label code does in mpl).
43+
paths = col.get_paths()
44+
45+
# Define the transform that will take us from collection
46+
# coordinates through to axes projection coordinates.
47+
data_t = self.axes.transData
48+
col_to_data = col.get_transform() - data_t
49+
50+
# Now that we have the transform, project all of this
51+
# collection's paths.
52+
new_paths = [col_to_data.transform_path(path)
53+
for path in paths]
54+
new_paths = [path for path in new_paths
55+
if path.vertices.size >= 1]
56+
57+
# The collection will now be referenced in axes projection
58+
# coordinates.
59+
col.set_transform(data_t)
60+
61+
# Clear the now incorrectly referenced paths.
62+
del paths[:]
63+
64+
for path in new_paths:
65+
if path.vertices.size == 0:
66+
# Don't persist empty paths. Let's get rid of them.
67+
continue
68+
69+
# Split the path if it has multiple MOVETO statements.
70+
codes = np.array(
71+
path.codes if path.codes is not None else [0])
72+
moveto = codes == mpath.Path.MOVETO
73+
if moveto.sum() <= 1:
74+
# This is only one path, so add it to the collection.
75+
paths.append(path)
76+
else:
77+
# The first MOVETO doesn't need cutting-out.
78+
moveto[0] = False
79+
split_locs = np.flatnonzero(moveto)
80+
81+
split_verts = np.split(path.vertices, split_locs)
82+
split_codes = np.split(path.codes, split_locs)
83+
84+
for verts, codes in zip(split_verts, split_codes):
85+
# Add this path to the collection's list of paths.
86+
paths.append(mpath.Path(verts, codes))
87+
88+
else:
89+
# Where contour paths exist at the edge of the globe, sometimes a
90+
# complete path in data space will become multiple paths when
91+
# transformed into axes or screen space. Matplotlib's contour
92+
# labelling does not account for this so we need to give it the
93+
# pre-transformed paths to work with.
4294

4395
# Define the transform that will take us from collection
4496
# coordinates through to axes projection coordinates.
4597
data_t = self.axes.transData
46-
col_to_data = col.get_transform() - data_t
98+
col_to_data = self.get_transform() - data_t
4799

48100
# Now that we have the transform, project all of this
49101
# collection's paths.
102+
paths = self.get_paths()
50103
new_paths = [col_to_data.transform_path(path) for path in paths]
51-
new_paths = [path for path in new_paths if path.vertices.size >= 1]
104+
paths[:] = new_paths
52105

53106
# The collection will now be referenced in axes projection
54107
# coordinates.
55-
col.set_transform(data_t)
56-
57-
# Clear the now incorrectly referenced paths.
58-
del paths[:]
59-
60-
for path in new_paths:
61-
if path.vertices.size == 0:
62-
# Don't persist empty paths. Let's get rid of them.
63-
continue
64-
65-
# Split the path if it has multiple MOVETO statements.
66-
codes = np.array(
67-
path.codes if path.codes is not None else [0])
68-
moveto = codes == mpath.Path.MOVETO
69-
if moveto.sum() <= 1:
70-
# This is only one path, so add it to the collection.
71-
paths.append(path)
72-
else:
73-
# The first MOVETO doesn't need cutting-out.
74-
moveto[0] = False
75-
split_locs = np.flatnonzero(moveto)
76-
77-
split_verts = np.split(path.vertices, split_locs)
78-
split_codes = np.split(path.codes, split_locs)
79-
80-
for verts, codes in zip(split_verts, split_codes):
81-
# Add this path to the collection's list of paths.
82-
paths.append(mpath.Path(verts, codes))
108+
self.set_transform(data_t)
83109

84110
# Now that we have prepared the collection paths, call on
85111
# through to the underlying implementation.

lib/cartopy/mpl/geoaxes.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@
4545
from cartopy.mpl.slippy_image_artist import SlippyImageArtist
4646

4747

48-
assert packaging.version.parse(mpl.__version__).release[:2] >= (3, 4), \
48+
_MPL_VERSION = packaging.version.parse(mpl.__version__)
49+
assert _MPL_VERSION.release >= (3, 4), \
4950
'Cartopy is only supported with Matplotlib 3.4 or greater.'
5051

5152
# A nested mapping from path, source CRS, and target projection to the
@@ -1602,12 +1603,15 @@ def contour(self, *args, **kwargs):
16021603
result = super().contour(*args, **kwargs)
16031604

16041605
# We need to compute the dataLim correctly for contours.
1605-
bboxes = [col.get_datalim(self.transData)
1606-
for col in result.collections
1607-
if col.get_paths()]
1608-
if bboxes:
1609-
extent = mtransforms.Bbox.union(bboxes)
1610-
self.update_datalim(extent.get_points())
1606+
if _MPL_VERSION.release[:2] < (3, 8):
1607+
bboxes = [col.get_datalim(self.transData)
1608+
for col in result.collections
1609+
if col.get_paths()]
1610+
if bboxes:
1611+
extent = mtransforms.Bbox.union(bboxes)
1612+
self.update_datalim(extent.get_points())
1613+
else:
1614+
self.update_datalim(result.get_datalim(self.transData))
16111615

16121616
self.autoscale_view()
16131617

@@ -1650,12 +1654,15 @@ def contourf(self, *args, **kwargs):
16501654
result = super().contourf(*args, **kwargs)
16511655

16521656
# We need to compute the dataLim correctly for contours.
1653-
bboxes = [col.get_datalim(self.transData)
1654-
for col in result.collections
1655-
if col.get_paths()]
1656-
if bboxes:
1657-
extent = mtransforms.Bbox.union(bboxes)
1658-
self.update_datalim(extent.get_points())
1657+
if _MPL_VERSION.release[:2] < (3, 8):
1658+
bboxes = [col.get_datalim(self.transData)
1659+
for col in result.collections
1660+
if col.get_paths()]
1661+
if bboxes:
1662+
extent = mtransforms.Bbox.union(bboxes)
1663+
self.update_datalim(extent.get_points())
1664+
else:
1665+
self.update_datalim(result.get_datalim(self.transData))
16591666

16601667
self.autoscale_view()
16611668

Loading

lib/cartopy/tests/mpl/test_examples.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import pytest
99

1010
import cartopy.crs as ccrs
11+
from cartopy.tests.mpl import MPL_VERSION
1112

1213

1314
@pytest.mark.natural_earth
@@ -31,7 +32,9 @@ def test_global_map():
3132

3233

3334
@pytest.mark.natural_earth
34-
@pytest.mark.mpl_image_compare(filename='contour_label.png', tolerance=0.5)
35+
@pytest.mark.mpl_image_compare(
36+
filename='contour_label.png',
37+
tolerance=3.9 if MPL_VERSION.release[:2] < (3, 8) else 0.5)
3538
def test_contour_label():
3639
from cartopy.tests.mpl.test_caching import sample_data
3740
fig = plt.figure()

0 commit comments

Comments
 (0)