Skip to content

Commit 5094004

Browse files
authored
Fix choropleth horizontal line artifacts on projected maps (#668)
* Fix choropleth horizontal line artifacts on projected maps Use Cartopy's project_geometry() instead of transform_points() for choropleth polygon projection. transform_points() projects vertices independently without geometric awareness, so polygons crossing the antimeridian (e.g. Russia) produce path segments that jump across the entire map, rendering as visible horizontal lines. project_geometry() properly splits and clips polygons at projection boundaries before converting to matplotlib paths, eliminating the artifacts on all projections (Robinson, Mercator, Mollweide, etc.). * Black
1 parent f229363 commit 5094004

File tree

2 files changed

+88
-0
lines changed

2 files changed

+88
-0
lines changed

ultraplot/axes/geo.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3725,6 +3725,22 @@ def _choropleth_geometry_path(
37253725
"""
37263726
Convert a polygon geometry to a projected matplotlib path.
37273727
"""
3728+
if ax._name == "cartopy":
3729+
src = transform
3730+
if src is None:
3731+
if ccrs is None:
3732+
raise RuntimeError("choropleth() requires cartopy for cartopy GeoAxes.")
3733+
src = ccrs.PlateCarree()
3734+
projected_geom = ax.projection.project_geometry(geometry, src)
3735+
paths = []
3736+
for ring in _choropleth_iter_rings(projected_geom):
3737+
path = _choropleth_close_path(np.asarray(ring, dtype=float))
3738+
if path is not None:
3739+
paths.append(path)
3740+
if not paths:
3741+
return None
3742+
return mpath.Path.make_compound_path(*paths)
3743+
37283744
paths = []
37293745
for ring in _choropleth_iter_rings(geometry):
37303746
projected = _choropleth_project_vertices(ax, ring, transform=transform)

ultraplot/tests/test_geographic.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,6 +1116,78 @@ def test_choropleth_country_mapping_with_explicit_values_raises():
11161116
uplt.close(fig)
11171117

11181118

1119+
def test_choropleth_antimeridian_no_horizontal_artifacts():
1120+
"""
1121+
Polygons crossing the antimeridian (e.g. Russia) must be split by
1122+
project_geometry so that no path vertex jumps across the map.
1123+
"""
1124+
sgeom = pytest.importorskip("shapely.geometry")
1125+
ccrs = pytest.importorskip("cartopy.crs")
1126+
1127+
# A box that crosses the antimeridian: 170E to 190E (= 170W)
1128+
box = sgeom.box(170, 50, 190, 70)
1129+
fig, ax = uplt.subplots(proj="robin")
1130+
geo = ax[0]
1131+
coll = geo.choropleth([box], [1.0])
1132+
fig.canvas.draw()
1133+
1134+
# After project_geometry splits the box, the compound path should
1135+
# have multiple sub-paths (MOVETO codes) rather than one continuous ring
1136+
paths = coll.get_paths()
1137+
assert len(paths) >= 1
1138+
codes = paths[0].codes
1139+
moveto_count = (codes == 1).sum() # Path.MOVETO == 1
1140+
assert (
1141+
moveto_count >= 2
1142+
), "Antimeridian-crossing polygon should be split into multiple sub-paths"
1143+
uplt.close(fig)
1144+
1145+
1146+
def test_choropleth_project_geometry_non_cylindrical():
1147+
"""
1148+
Choropleth on non-cylindrical projections (Robinson, Mollweide, etc.)
1149+
should render without errors for geometries that span wide longitudes.
1150+
"""
1151+
sgeom = pytest.importorskip("shapely.geometry")
1152+
pytest.importorskip("cartopy.crs")
1153+
1154+
# Wide-spanning box (like Russia or Canada)
1155+
box = sgeom.box(-170, 40, 170, 75)
1156+
for proj in ("robin", "moll", "merc"):
1157+
fig, ax = uplt.subplots(proj=proj)
1158+
coll = ax[0].choropleth([box], [42.0])
1159+
fig.canvas.draw()
1160+
1161+
paths = coll.get_paths()
1162+
assert len(paths) >= 1
1163+
# Verify no inf/nan in projected vertices
1164+
for path in paths:
1165+
verts = path.vertices
1166+
assert np.all(
1167+
np.isfinite(verts)
1168+
), f"Projected path has non-finite vertices on {proj!r} projection"
1169+
uplt.close(fig)
1170+
1171+
1172+
def test_choropleth_country_antimeridian_renders():
1173+
"""
1174+
Country-level choropleth for Russia (crosses antimeridian) should
1175+
produce valid paths with finite vertices on multiple projections.
1176+
"""
1177+
pytest.importorskip("cartopy.crs")
1178+
for proj in ("robin", "merc", "moll"):
1179+
fig, ax = uplt.subplots(proj=proj)
1180+
coll = ax[0].choropleth({"Russia": 1.0}, country=True, cmap="Glacial")
1181+
fig.canvas.draw()
1182+
1183+
for path in coll.get_paths():
1184+
verts = path.vertices
1185+
assert np.all(
1186+
np.isfinite(verts)
1187+
), f"Russia choropleth path has non-finite vertices on {proj!r}"
1188+
uplt.close(fig)
1189+
1190+
11191191
def test_check_tricontourf():
11201192
"""
11211193
Ensure transform defaults are applied only when appropriate for tri-plots.

0 commit comments

Comments
 (0)