Skip to content

Commit bcf4fde

Browse files
committed
Fix missing +360 degree lon gridline issues
1 parent aa51512 commit bcf4fde

File tree

2 files changed

+48
-53
lines changed

2 files changed

+48
-53
lines changed

proplot/axes/geo.py

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -213,23 +213,22 @@ def _pad_ticks(ticks, vmin, vmax):
213213
# giant lists of 10,000 gridline locations.
214214
if len(ticks) == 0:
215215
return ticks
216-
range_ = max(ticks) - min(ticks)
216+
range_ = np.max(ticks) - np.min(ticks)
217217
vmin = max(vmin, ticks[0] - range_)
218218
vmax = min(vmax, ticks[-1] + range_)
219219

220220
# Pad the reported tick range up to specified range
221221
step = ticks[1] - ticks[0] # MaxNLocator/AutoMinorLocator steps are equal
222-
ticks_lo = np.arange(ticks[0], vmin, -step)[1:][::-1].tolist()
223-
ticks_hi = np.arange(ticks[-1], vmax, step)[1:].tolist()
224-
ticks = ticks_lo + ticks + ticks_hi
222+
ticks_lo = np.arange(ticks[0], vmin, -step)[1:][::-1]
223+
ticks_hi = np.arange(ticks[-1], vmax, step)[1:]
224+
ticks = np.concatenate((ticks_lo, ticks, ticks_hi))
225225
return ticks
226226

227227
def get_scale(self):
228228
return 'linear'
229229

230230
def get_tick_space(self):
231-
# Just use the long-standing default of nbins=9
232-
return 9
231+
return 9 # longstanding default of nbins=9
233232

234233
def get_major_formatter(self):
235234
return self.major.formatter
@@ -241,14 +240,14 @@ def get_minor_locator(self):
241240
return self.minor.locator
242241

243242
def get_majorticklocs(self):
244-
return self._get_sanitized_ticks(self.major.locator)
243+
return self._get_ticklocs(self.major.locator)
245244

246245
def get_minorticklocs(self):
247-
return self._get_sanitized_ticks(self.minor.locator)
246+
return self._get_ticklocs(self.minor.locator)
248247

249248
def set_major_formatter(self, formatter, default=False):
250-
# NOTE: Cartopy formatters check Formatter.axis.axes.projection and has
251-
# special projection-dependent behavior.
249+
# NOTE: Cartopy formatters check Formatter.axis.axes.projection
250+
# in order to implement special projection-dependent behavior.
252251
self.major.formatter = formatter
253252
formatter.set_axis(self)
254253
self.isDefault_majfmt = default
@@ -286,27 +285,41 @@ def __init__(self, axes):
286285
self.set_major_locator(constructor.Locator(locator), default=True)
287286
self.set_minor_locator(mticker.AutoMinorLocator(), default=True)
288287

289-
def _get_sanitized_ticks(self, locator):
288+
def _get_ticklocs(self, locator):
290289
# Prevent ticks from looping around
291-
eps = 5e-10 # more than 1e-10 because we use 1e-10 in _LongitudeLocator
292-
ticks = sorted(locator())
293-
while ticks and ticks[-1] - eps > ticks[0] + 360 + eps: # cut off looped ticks
294-
ticks = ticks[:-1]
290+
# NOTE: Cartopy 0.17 formats numbers offset by eps with the cardinal indicator
291+
# (e.g. 0 degrees for map centered on 180 degrees). So skip in that case.
292+
# NOTE: Common strange issue is e.g. MultipleLocator(60) starts out at
293+
# -60 degrees for a map from 0 to 360 degrees. If always trimmed circular
294+
# locations from right then would cut off rightmost gridline. Workaround is
295+
# to trim on the side closest to central longitude (in this case the left).
296+
eps = 1e-10
297+
lon0 = self.axes._get_lon0()
298+
ticks = np.sort(locator())
299+
while ticks.size:
300+
if np.isclose(ticks[0] + 360, ticks[-1]):
301+
if _version_cartopy >= '0.18' or not np.isclose(ticks[0] % 360, 0):
302+
ticks[-1] -= eps # ensure label appears on *right* not left
303+
break
304+
elif ticks[0] + 360 < ticks[-1]:
305+
idx = (1, None) if lon0 - ticks[0] > ticks[-1] - lon0 else (None, -1)
306+
ticks = ticks[slice(*idx)] # cut off ticks looped over globe
307+
else:
308+
break
295309

296310
# Append extra ticks in case longitude/latitude limits do not encompass
297311
# the entire view range of map, e.g. for Lambert Conformal sectors.
298312
# NOTE: Try to avoid making 10,000 element lists. Just wrap extra ticks
299313
# up to the width of *reported* longitude range.
300314
if isinstance(locator, (mticker.MaxNLocator, mticker.AutoMinorLocator)):
301-
lon0 = self.axes._get_lon0()
302315
ticks = self._pad_ticks(ticks, lon0 - 180 + eps, lon0 + 180 - eps)
303316

304317
return ticks
305318

306319
def get_view_interval(self):
307320
# NOTE: Proplot tries to set its *own* view intervals to avoid dateline
308-
# weirdness, but if geo.extent is 'auto' the interval will be unset, so
309-
# we are forced to use _get_extent().
321+
# weirdness, but if rc['geo.extent'] is 'auto' the interval will be unset.
322+
# In this case we use _get_extent() as a backup.
310323
interval = self._interval
311324
if interval is None:
312325
extent = self._get_extent()
@@ -332,12 +345,12 @@ def __init__(self, axes, latmax=90):
332345
self.set_major_locator(constructor.Locator(locator), default=True)
333346
self.set_minor_locator(mticker.AutoMinorLocator(), default=True)
334347

335-
def _get_sanitized_ticks(self, locator):
348+
def _get_ticklocs(self, locator):
336349
# Adjust latitude ticks to fix bug in some projections. Harmless for basemap.
337-
# NOTE: Maybe this is fixed by cartopy 0.18?
338-
eps = 5e-10
339-
ticks = sorted(locator())
340-
if ticks:
350+
# NOTE: Maybe this was fixed by cartopy 0.18?
351+
eps = 1e-10
352+
ticks = np.sort(locator())
353+
if ticks.size:
341354
if ticks[0] == -90:
342355
ticks[0] += eps
343356
if ticks[-1] == 90:
@@ -350,7 +363,7 @@ def _get_sanitized_ticks(self, locator):
350363

351364
# Filter ticks to latmax range
352365
latmax = self.get_latmax()
353-
ticks = [l for l in ticks if -latmax <= l <= latmax]
366+
ticks = ticks[(ticks >= -latmax) & (ticks <= latmax)]
354367

355368
return ticks
356369

proplot/ticker.py

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -243,9 +243,11 @@ def tick_values(self, vmin, vmax): # noqa: U100
243243

244244
class DegreeLocator(mticker.MaxNLocator):
245245
"""
246-
Locate geographic gridlines with degree-minute-second support.
247-
Adapted from cartopy.
246+
Locate geographic gridlines with degree-minute-second support. Adapted from cartopy.
248247
"""
248+
# NOTE: This is identical to cartopy except they only define LongitutdeLocator
249+
# for common methods whereas we use DegreeLocator. More intuitive this way in
250+
# case users need degree-minute-seconds for non-specific degree axis.
249251
# NOTE: Locator implementation is weird AF. __init__ just calls set_params with all
250252
# keyword args and fills in missing params with default_params class attribute.
251253
# Unknown params result in warning instead of error.
@@ -280,14 +282,13 @@ def _raw_ticks(self, vmin, vmax):
280282
self._guess_steps(vmin, vmax)
281283
return super()._raw_ticks(vmin, vmax)
282284

283-
def bin_boundaries(self, vmin, vmax): # matplotlib <2.2.0
284-
return self._raw_ticks(vmin, vmax) # may call Latitude/LongitudeLocator copies
285+
def bin_boundaries(self, vmin, vmax): # matplotlib < 2.2.0
286+
return self._raw_ticks(vmin, vmax) # may call Latitude/Longitude Locator copies
285287

286288

287289
class LongitudeLocator(DegreeLocator):
288290
"""
289-
Locate longitude gridlines with degree-minute-second support.
290-
Adapted from cartopy.
291+
Locate longitude gridlines with degree-minute-second support. Adapted from cartopy.
291292
"""
292293
@docstring._snippet_manager
293294
def __init__(self, *args, **kwargs):
@@ -296,28 +297,10 @@ def __init__(self, *args, **kwargs):
296297
"""
297298
super().__init__(*args, **kwargs)
298299

299-
def tick_values(self, vmin, vmax):
300-
# NOTE: Proplot ensures vmin, vmax are always the *actual* longitude range
301-
# accounting for central longitude position.
302-
ticks = super().tick_values(vmin, vmax)
303-
if np.isclose(ticks[0] + 360, ticks[-1]):
304-
eps = 1e-10
305-
if ticks[-1] % 360 > 0:
306-
# Make sure the label appears on *right*, not on
307-
# top of the leftmost label.
308-
ticks[-1] -= eps
309-
else:
310-
# Formatter formats label as 1e-10... so there is simply no way to
311-
# put label on right. Just shift this location off the map edge so
312-
# parallels still extend all the way to the edge, but label disappears.
313-
ticks[-1] += eps
314-
return ticks
315-
316300

317301
class LatitudeLocator(DegreeLocator):
318302
"""
319-
Locate latitude gridlines with degree-minute-second support.
320-
Adapted from cartopy.
303+
Locate latitude gridlines with degree-minute-second support. Adapted from cartopy.
321304
"""
322305
@docstring._snippet_manager
323306
def __init__(self, *args, **kwargs):
@@ -741,7 +724,7 @@ def __init__(self, symbol='', number=1):
741724
----------
742725
symbol : str, default: ''
743726
The constant symbol, e.g. ``r'$\pi$'``.
744-
number : float, default: 1`
727+
number : float, default: 1
745728
The constant value, e.g. `numpy.pi`.
746729
"""
747730
self._symbol = symbol
@@ -782,7 +765,7 @@ class _CartopyFormatter(object):
782765
# NOTE: Cartopy formatters pre 0.18 required axis, and *always* translated
783766
# input values from map projection coordinates to Plate Carrée coordinates.
784767
# After 0.18 you can avoid this behavior by not setting axis but really
785-
# dislike that inconsistency. Solution is temporarily change projection.
768+
# dislike that inconsistency. Solution is temporarily assign PlateCarre().
786769
def __init__(self, *args, **kwargs):
787770
import cartopy # noqa: F401 (ensure available)
788771
super().__init__(*args, **kwargs)
@@ -797,8 +780,7 @@ def __call__(self, value, pos=None):
797780

798781
class DegreeFormatter(_CartopyFormatter, _PlateCarreeFormatter):
799782
"""
800-
Format longitude and latitude gridline labels.
801-
Adapted from cartopy.
783+
Formatter for longitude and latitude gridline labels. Adapted from cartopy.
802784
"""
803785
@docstring._snippet_manager
804786
def __init__(self, *args, **kwargs):

0 commit comments

Comments
 (0)