Skip to content

Commit

Permalink
Merge pull request SciTools#1863 from stefraynaud/decimal_point
Browse files Browse the repository at this point in the history
Add cardinal_labels and decimal_point options to lon/lat formatters
  • Loading branch information
greglucas authored Sep 17, 2021
2 parents 6195c26 + 1927dc4 commit 127a226
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 22 deletions.
9 changes: 7 additions & 2 deletions lib/cartopy/mpl/geoaxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1423,7 +1423,8 @@ def gridlines(self, crs=None, draw_labels=False,
xformatter=None, yformatter=None, xlim=None, ylim=None,
rotate_labels=None, xlabel_style=None, ylabel_style=None,
labels_bbox_style=None, xpadding=5, ypadding=5,
offset_angle=25, auto_update=False, **kwargs):
offset_angle=25, auto_update=False, formatter_kwargs=None,
**kwargs):
"""
Automatically add gridlines to the axes, in the given coordinate
system, at draw time.
Expand Down Expand Up @@ -1530,6 +1531,10 @@ def gridlines(self, crs=None, draw_labels=False,
auto_update: bool
Whether to update the grilines and labels when the plot is
refreshed.
formatter_kwargs: dict, optional
Options passed to the default formatters.
See :class:`~cartopy.mpl.ticker.LongitudeFormatter` and
:class:`~cartopy.mpl.ticker.LatitudeFormatter`
Keyword Parameters
------------------
Expand Down Expand Up @@ -1563,7 +1568,7 @@ def gridlines(self, crs=None, draw_labels=False,
xlabel_style=xlabel_style, ylabel_style=ylabel_style,
labels_bbox_style=labels_bbox_style,
xpadding=xpadding, ypadding=ypadding, offset_angle=offset_angle,
auto_update=auto_update)
auto_update=auto_update, formatter_kwargs=formatter_kwargs)
self._gridliners.append(gl)
return gl

Expand Down
15 changes: 12 additions & 3 deletions lib/cartopy/mpl/gridliner.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def __init__(self, axes, crs, draw_labels=False, xlocator=None,
xlim=None, ylim=None, rotate_labels=None,
xlabel_style=None, ylabel_style=None, labels_bbox_style=None,
xpadding=5, ypadding=5, offset_angle=25,
auto_update=False):
auto_update=False, formatter_kwargs=None):
"""
Object used by :meth:`cartopy.mpl.geoaxes.GeoAxes.gridlines`
to add gridlines and tick labels to a map.
Expand Down Expand Up @@ -220,6 +220,10 @@ def __init__(self, axes, crs, draw_labels=False, xlocator=None,
auto_update: bool
Whether to redraw the gridlines and labels when the figure is
updated.
formatter_kwargs: dict, optional
Options passed to the default formatters.
See :class:`~cartopy.mpl.ticker.LongitudeFormatter` and
:class:`~cartopy.mpl.ticker.LatitudeFormatter`
Notes
-----
Expand Down Expand Up @@ -254,17 +258,22 @@ def __init__(self, axes, crs, draw_labels=False, xlocator=None,
else:
self.ylocator = classic_locator

formatter_kwargs = {
**(formatter_kwargs or {}),
"dms": dms,
}

if xformatter is None:
if isinstance(crs, PlateCarree):
xformatter = LongitudeFormatter(dms=dms)
xformatter = LongitudeFormatter(**formatter_kwargs)
else:
xformatter = classic_formatter()
#: The :class:`~matplotlib.ticker.Formatter` to use for the lon labels.
self.xformatter = xformatter

if yformatter is None:
if isinstance(crs, PlateCarree):
yformatter = LatitudeFormatter(dms=dms)
yformatter = LatitudeFormatter(**formatter_kwargs)
else:
yformatter = classic_formatter()
#: The :class:`~matplotlib.ticker.Formatter` to use for the lat labels.
Expand Down
49 changes: 41 additions & 8 deletions lib/cartopy/mpl/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""This module contains tools for handling tick marks in cartopy."""

import numpy as np
import matplotlib as mpl
from matplotlib.ticker import Formatter, MaxNLocator

import cartopy.crs as ccrs
Expand All @@ -25,7 +26,7 @@ def __init__(self, direction_label=True, degree_symbol='\u00B0',
number_format='g', transform_precision=1e-8, dms=False,
minute_symbol="'", second_symbol="''",
seconds_number_format='g',
auto_hide=True):
auto_hide=True, decimal_point=None, cardinal_labels=None):
"""
Base class for simpler implementation of specialised formatters
for latitude and longitude axes.
Expand All @@ -43,6 +44,14 @@ def __init__(self, direction_label=True, degree_symbol='\u00B0',
self._auto_hide_degrees = False
self._auto_hide_minutes = False
self._precision = 5 # locator precision
if (decimal_point is None and
mpl.rcParams['axes.formatter.use_locale']):
import locale
decimal_point = locale.localeconv()["decimal_point"]
if cardinal_labels is None:
cardinal_labels = {}
self._cardinal_labels = cardinal_labels
self._decimal_point = decimal_point

def __call__(self, value, pos=None):
if self.axis is not None and isinstance(self.axis.axes, GeoAxes):
Expand Down Expand Up @@ -158,7 +167,10 @@ def _format_degrees(self, deg):
number_format = 'd'
else:
number_format = self._degrees_number_format
return f'{abs(deg):{number_format}}{self._degree_symbol}'
value = f"{abs(deg):{number_format}}{self._degree_symbol}"
if self._decimal_point is not None:
value = value.replace(".", self._decimal_point)
return value

def _format_minutes(self, mn):
"""Format minutes as an integer"""
Expand Down Expand Up @@ -197,6 +209,7 @@ def __init__(self, direction_label=True,
transform_precision=1e-8, dms=False,
minute_symbol="'", second_symbol="''",
seconds_number_format='g', auto_hide=True,
decimal_point=None, cardinal_labels=None
):
"""
Tick formatter for latitudes.
Expand Down Expand Up @@ -237,6 +250,13 @@ def __init__(self, direction_label=True,
values. Defaults to 'g'.
auto_hide: bool, optional
Auto-hide degrees or minutes when redundant.
decimal_point: bool, optional
Decimal point character. If not provided and
``mpl.rcParams['axes.formatter.use_locale'] == True``,
the locale decimal point is used.
cardinal_labels: dict, optional
A dictionary with "south" and/or "north" keys to replace south and
north cardinal labels, which defaults to "S" and "N".
Note
----
Expand Down Expand Up @@ -282,6 +302,8 @@ def __init__(self, direction_label=True,
second_symbol=second_symbol,
seconds_number_format=seconds_number_format,
auto_hide=auto_hide,
decimal_point=decimal_point,
cardinal_labels=cardinal_labels
)

def _apply_transform(self, value, target_proj, source_crs):
Expand All @@ -290,9 +312,9 @@ def _apply_transform(self, value, target_proj, source_crs):
def _hemisphere(self, value, value_source_crs):

if value > 0:
hemisphere = 'N'
hemisphere = self._cardinal_labels.get('north', 'N')
elif value < 0:
hemisphere = 'S'
hemisphere = self._cardinal_labels.get('south', 'S')
else:
hemisphere = ''
return hemisphere
Expand All @@ -313,6 +335,8 @@ def __init__(self,
second_symbol="''",
seconds_number_format='g',
auto_hide=True,
decimal_point=None,
cardinal_labels=None
):
"""
Create a formatter for longitudes.
Expand Down Expand Up @@ -360,6 +384,13 @@ def __init__(self,
values. Defaults to 'g'.
auto_hide: bool, optional
Auto-hide degrees or minutes when redundant.
decimal_point: bool, optional
Decimal point character. If not provided and
``mpl.rcParams['axes.formatter.use_locale'] == True``,
the locale decimal point is used.
cardinal_labels: dict, optional
A dictionary with "west" and/or "east" keys to replace west and
east cardinal labels, which defaults to "W" and "E".
Note
----
Expand Down Expand Up @@ -406,6 +437,8 @@ def __init__(self,
second_symbol=second_symbol,
seconds_number_format=seconds_number_format,
auto_hide=auto_hide,
decimal_point=decimal_point,
cardinal_labels=cardinal_labels
)
self._zero_direction_labels = zero_direction_label
self._dateline_direction_labels = dateline_direction_label
Expand Down Expand Up @@ -444,18 +477,18 @@ def _hemisphere(self, value, value_source_crs):
value = self._fix_lons(value)
# Perform basic hemisphere detection.
if value < 0:
hemisphere = 'W'
hemisphere = self._cardinal_labels.get('west', 'W')
elif value > 0:
hemisphere = 'E'
hemisphere = self._cardinal_labels.get('east', 'E')
else:
hemisphere = ''
# Correct for user preferences:
if value == 0 and self._zero_direction_labels:
# Use the original tick value to determine the hemisphere.
if value_source_crs < 0:
hemisphere = 'E'
hemisphere = self._cardinal_labels.get('east', 'E')
else:
hemisphere = 'W'
hemisphere = self._cardinal_labels.get('west', 'W')
if value in (-180, 180) and not self._dateline_direction_labels:
hemisphere = ''
return hemisphere
Expand Down
29 changes: 20 additions & 9 deletions lib/cartopy/tests/mpl/test_gridliner.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,25 +381,24 @@ def test_gridliner_line_limits():
"draw_labels, result",
[
(True,
{'left': ['70°E', '40°N'],
'right': ['130°E', '40°N', '50°N'],
'top': ['130°E', '50°N', '100°E', '70°E'],
{'left': ['40°N'],
'right': ['40°N', '50°N'],
'top': ['70°E', '100°E', '130°E'],
'bottom': ['100°E']}),
(False,
{'left': [],
'right': [],
'top': [],
'bottom': []}),
(['top', 'left'],
{'left': ['70°E', '40°N'],
{'left': ['40°N'],
'right': [],
'top': ['130°E', '100°E', '50°N', '70°E'],
'top': ['70°E', '100°E', '130°E'],
'bottom': []}),
({'top': 'x', 'right': 'y'},
{'left': [],
'right': ['40°N', '50°N'],
'top': ['100°E', '130°E', '70°E'],
'top': ['70°E', '100°E', '130°E'],
'bottom': []}),
({'left': 'x'},
{'left': ['70°E'],
Expand All @@ -413,16 +412,28 @@ def test_gridliner_line_limits():
'bottom': []}),
])
def test_gridliner_draw_labels_param(draw_labels, result):
plt.figure()
fig = plt.figure()
lambert_crs = ccrs.LambertConformal(central_longitude=105)
ax = plt.axes(projection=lambert_crs)
ax.set_extent([75, 130, 18, 54], crs=ccrs.PlateCarree())
gl = ax.gridlines(draw_labels=draw_labels, rotate_labels=False, dms=True,
x_inline=False, y_inline=False)
gl.xlocator = mticker.FixedLocator([70, 100, 130])
gl.ylocator = mticker.FixedLocator([40, 50])
plt.show()
fig.canvas.draw()
res = {}
for loc in 'left', 'right', 'top', 'bottom':
artists = getattr(gl, loc+'_label_artists')
res[loc] = [a.get_text() for a in artists if a.get_visible()]
assert res == result


def test_gridliner_formatter_kwargs():
fig = plt.figure()
ax = plt.subplot(1, 1, 1, projection=ccrs.PlateCarree())
ax.set_extent([-80, -40.0, 10.0, -30.0])
gl = ax.gridlines(draw_labels=True, dms=False,
formatter_kwargs=dict(cardinal_labels={'west': 'O'}))
fig.canvas.draw()
labels = [a.get_text() for a in gl.bottom_label_artists if a.get_visible()]
assert labels == ['75°O', '70°O', '65°O', '60°O', '55°O', '50°O', '45°O']
14 changes: 14 additions & 0 deletions lib/cartopy/tests/mpl/test_ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,3 +302,17 @@ def test_LongitudeLocator(cls, vmin, vmax, expected):
locator = cls(dms=True)
result = locator.tick_values(vmin, vmax)
np.testing.assert_allclose(result, expected)


def test_lonlatformatter_decimal_point():
xticker = LongitudeFormatter(decimal_point=',', number_format='0.2f')
yticker = LatitudeFormatter(decimal_point=',', number_format='0.2f')
assert xticker(-10) == "10,00°W"
assert yticker(-10) == "10,00°S"


def test_lonlatformatter_cardinal_labels():
xticker = LongitudeFormatter(cardinal_labels={'west': 'O'})
yticker = LatitudeFormatter(cardinal_labels={'south': 'South'})
assert xticker(-10) == "10°O"
assert yticker(-10) == "10°South"

0 comments on commit 127a226

Please sign in to comment.