forked from SciTools/cartopy
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Provide projection-aware tick formatters. Closes SciTools#204.
- Loading branch information
Showing
6 changed files
with
276 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
# (C) British Crown Copyright 2014, Met Office | ||
# | ||
# This file is part of cartopy. | ||
# | ||
# cartopy is free software: you can redistribute it and/or modify it under | ||
# the terms of the GNU Lesser General Public License as published by the | ||
# Free Software Foundation, either version 3 of the License, or | ||
# (at your option) any later version. | ||
# | ||
# cartopy is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU Lesser General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU Lesser General Public License | ||
# along with cartopy. If not, see <http://www.gnu.org/licenses/>. | ||
"""This module contains tools for handling tick marks in cartopy.""" | ||
from matplotlib.ticker import Formatter | ||
|
||
import cartopy.crs as ccrs | ||
from cartopy.mpl.geoaxes import GeoAxes | ||
|
||
|
||
class _GeoFormatter(Formatter): | ||
"""Base class for formatting ticks on geographical axes.""" | ||
|
||
def __init__(self, degree_symbol=u'\u00B0', number_format='g'): | ||
""" | ||
Create a formatter for geographical axis values. | ||
Kwargs: | ||
* degree_symbol (string): | ||
The character(s) used to represent the degree symbol in the | ||
tick labels. Defaults to u'\u00B0' which is the unicode | ||
degree symbol. Can be an empty string if no degree symbol is | ||
desired. | ||
* number_format (string): | ||
Format string to represent the tick values. Defaults to 'g'. | ||
""" | ||
self._degree_symbol = degree_symbol | ||
self._number_format = number_format | ||
|
||
def __call__(self, value, pos=None): | ||
if not isinstance(self.axis.axes, GeoAxes): | ||
raise TypeError("This formatter can only be " | ||
"used with cartopy axes.") | ||
# We want to produce labels for values in the familiar Plate Carree | ||
# projection, so convert the tick values from their own projection | ||
# before formatting them. | ||
source = self.axis.axes.projection | ||
if not isinstance(source, (ccrs._RectangularProjection, | ||
ccrs.Mercator)): | ||
raise TypeError("This formatter cannot be used with " | ||
"non-rectangular projections.") | ||
target = ccrs.PlateCarree() | ||
projected_value = self.extract_transform_result( | ||
target.transform_point(*self.make_transform_args(value, source))) | ||
# Round the transformed values to the nearest 0.1 degree for display | ||
# purposes (transforms can introduce minor rounding errors that make | ||
# the tick values look bad). | ||
projected_value = round(10 * projected_value) / 10 | ||
# Return the formatted values, the formatter has both the re-projected | ||
# tick value and the original axis value available to it. | ||
return self._format_value(projected_value, value) | ||
|
||
def _format_value(self, value, original_value): | ||
hemisphere = self.hemisphere(value, original_value) | ||
fmt_string = u'{value:{number_format}}{degree}{hemisphere}' | ||
return fmt_string.format(value=abs(value), | ||
number_format=self._number_format, | ||
degree=self._degree_symbol, | ||
hemisphere=hemisphere) | ||
|
||
def make_transform_args(self, value, source_crs): | ||
""" | ||
Given a single coordinate value and a source `CRS` returns a | ||
3-tuple of arguments suitable for use by `CRS.transform_point`. | ||
Must be over-ridden by the derived class. | ||
""" | ||
raise NotImplementedError("A subclass must implement this method.") | ||
|
||
def extract_transform_result(self, transform_result): | ||
""" | ||
Given a 2-tuple returned from `CRS.transform_point` returns the | ||
required element. | ||
Must be over-ridden by the derived class. | ||
""" | ||
raise NotImplementedError("A subclass must implement this method.") | ||
|
||
def hemisphere(self, value, value_source_crs): | ||
""" | ||
Given both a tick value in the Plate Carree projection and the | ||
same value in the source CRS returns a string indicating the | ||
hemisphere that the value is in. | ||
Must be over-ridden by the derived class. | ||
""" | ||
raise NotImplementedError("A subclass must implement this method.") | ||
|
||
|
||
class LatitudeFormatter(_GeoFormatter): | ||
"""Tick formatter for latitude axes.""" | ||
|
||
def make_transform_args(self, value, source_crs): | ||
return (0, value, source_crs) | ||
|
||
def extract_transform_result(self, transform_result): | ||
return transform_result[1] | ||
|
||
def hemisphere(self, value, value_source_crs): | ||
if value > 0: | ||
hemisphere = 'N' | ||
elif value < 0: | ||
hemisphere = 'S' | ||
else: | ||
hemisphere = '' | ||
return hemisphere | ||
|
||
|
||
class LongitudeFormatter(_GeoFormatter): | ||
"""Tick formatter for longitude axes.""" | ||
|
||
def __init__(self, | ||
zero_direction_label=False, | ||
dateline_direction_label=False, | ||
degree_symbol=u'\u00B0', | ||
number_format='g'): | ||
""" | ||
Create a formatter for longitude values. | ||
Kwargs: | ||
* zero_direction_label (False | True): | ||
If *True* a direction label (E or W) will be drawn next to | ||
longitude labels with the value 0. If *False* then these | ||
labels will not be drawn. Defaults to *False* (no direction | ||
labels). | ||
* dateline_direction_label (False | True): | ||
If *True* a direction label (E or W) will be drawn next to | ||
longitude labels with the value 180. If *False* then these | ||
labels will not be drawn. Defaults to *False* (no direction | ||
labels). | ||
* degree_symbol (string): | ||
The symbol used to represent degrees. Defaults to u'\u00B0' | ||
which is the unicode degree symbol. | ||
* number_format (string): | ||
Format string to represent the longitude values. Defaults to | ||
'g'. | ||
""" | ||
super(LongitudeFormatter, self).__init__(degree_symbol=degree_symbol, | ||
number_format=number_format) | ||
self._zero_direction_labels = zero_direction_label | ||
self._dateline_direction_labels = dateline_direction_label | ||
|
||
def make_transform_args(self, value, source_crs): | ||
return (value, 0, source_crs) | ||
|
||
def extract_transform_result(self, transform_result): | ||
return transform_result[0] | ||
|
||
def hemisphere(self, value, value_source_crs): | ||
# Perform basic hemisphere detection. | ||
if value < 0: | ||
hemisphere = 'W' | ||
elif value > 0: | ||
hemisphere = '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' | ||
else: | ||
hemisphere = 'W' | ||
if value in (-180, 180) and not self._dateline_direction_labels: | ||
hemisphere = '' | ||
return hemisphere |
Binary file added
BIN
+79.7 KB
...cartopy/tests/mpl/baseline_images/mpl/test_ticker/ticks_central_longitude_0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+84 KB
...rtopy/tests/mpl/baseline_images/mpl/test_ticker/ticks_central_longitude_120.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+80.2 KB
...rtopy/tests/mpl/baseline_images/mpl/test_ticker/ticks_central_longitude_180.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+100 KB
lib/cartopy/tests/mpl/baseline_images/mpl/test_ticker/ticks_mercator.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
# (C) British Crown Copyright 2014, Met Office | ||
# | ||
# This file is part of cartopy. | ||
# | ||
# cartopy is free software: you can redistribute it and/or modify it under | ||
# the terms of the GNU Lesser General Public License as published by the | ||
# Free Software Foundation, either version 3 of the License, or | ||
# (at your option) any later version. | ||
# | ||
# cartopy is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU Lesser General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU Lesser General Public License | ||
# along with cartopy. If not, see <http://www.gnu.org/licenses/>. | ||
from nose.tools import raises | ||
import matplotlib.pyplot as plt | ||
|
||
import cartopy.crs as ccrs | ||
from cartopy.mpl.ticker import LatitudeFormatter, LongitudeFormatter | ||
from cartopy.tests.mpl import ImageTesting | ||
|
||
|
||
def _run_test(projection, xticks, yticks, xformatter, yformatter): | ||
ax = plt.axes(projection=projection) | ||
ax.set_global() | ||
ax.coastlines() | ||
ax.set_xticks(xticks, crs=ccrs.PlateCarree()) | ||
ax.set_yticks(yticks, crs=ccrs.PlateCarree()) | ||
ax.xaxis.set_major_formatter(xformatter) | ||
ax.yaxis.set_major_formatter(yformatter) | ||
|
||
|
||
@ImageTesting(['ticks_central_longitude_0']) | ||
def test_central_longitude_0(): | ||
_run_test(ccrs.PlateCarree(central_longitude=0), | ||
[-180, -120, -60, 0, 60, 120, 180], | ||
[-90, -60, -30, 0, 30, 60, 90], | ||
LongitudeFormatter(dateline_direction_label=True), | ||
LatitudeFormatter()) | ||
|
||
|
||
@ImageTesting(['ticks_central_longitude_180']) | ||
def test_central_longitude_180(): | ||
_run_test(ccrs.PlateCarree(central_longitude=180), | ||
[0, 60, 120, 180, 240, 300, 360], | ||
[-90, -60, -30, 0, 30, 60, 90], | ||
LongitudeFormatter(zero_direction_label=True), | ||
LatitudeFormatter()) | ||
|
||
|
||
@ImageTesting(['ticks_central_longitude_120']) | ||
def test_central_longitude_120(): | ||
_run_test(ccrs.PlateCarree(central_longitude=120), | ||
[-60, 0, 60, 120, 180, 240, 300], | ||
[-90, -60, -30, 0, 30, 60, 90], | ||
LongitudeFormatter(degree_symbol='', number_format='.2f'), | ||
LatitudeFormatter(degree_symbol='', number_format='.2f')) | ||
|
||
|
||
@ImageTesting(['ticks_mercator']) | ||
def test_mercator(): | ||
_run_test(ccrs.Mercator(), | ||
[-180, -120, -60, 0, 60, 120, 180], | ||
[-80, -60, -30, 0, 30, 60, 80], | ||
LongitudeFormatter(dateline_direction_label=True), | ||
LatitudeFormatter()) | ||
|
||
|
||
@raises(TypeError) | ||
def test__GeoFormatter_invalid_axes(): | ||
ax = plt.axes() | ||
try: | ||
ax.xaxis.set_major_formatter(LongitudeFormatter) | ||
finally: | ||
plt.close() | ||
|
||
|
||
@raises(TypeError) | ||
def test__GeoFormatter_invalid_projection(): | ||
ax = plt.axes(projection=ccrs.Stereographic()) | ||
try: | ||
ax.xaxis.set_major_formatter(LongitudeFormatter) | ||
finally: | ||
plt.close() |