Skip to content

Commit 8520e36

Browse files
committed
Use AutoFormatter for deglat/lon/etc., improve AutoFormatter log-scale patch
1 parent 9b16473 commit 8520e36

3 files changed

Lines changed: 91 additions & 67 deletions

File tree

CHANGELOG.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,24 @@ ProPlot v0.3.0 (2020-01-##)
6060
with a default ``.proplotrc`` file, change the auto-generated user ``.proplotrc``
6161
(:pr:`50`).
6262

63+
ProPlot v0.2.6 (2019-12-09)
64+
===========================
65+
66+
.. rubric:: Bug fixes
67+
68+
- Fix issue where `~proplot.styletools.AutoFormatter` logarithmic scale
69+
points are incorrect (:commit:`9b164733`).
70+
71+
.. rubric:: Internals
72+
73+
- Remove `prefix`, `suffix`, and `negpos` keyword args from
74+
`~proplot.styletools.SimpleFormatter`, remove `precision` keyword arg from
75+
`~proplot.styletools.AutoFormatter` (it automatically figures out the
76+
necessary precision!).
77+
- Make ``'deglat'``, ``'deglon'``, ``'lat'``, ``'lon'``, and ``'deg'`` instances
78+
of `~proplot.styletools.AutoFormatter` instead of `~proplot.styletools.SimpleFormatter`.
79+
The latter should just be used for contours.
80+
6381
ProPlot v0.2.6 (2019-12-08)
6482
===========================
6583
.. rubric:: Bug fixes

docs/axis.ipynb

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@
8484
"ProPlot lets you easily change the axis formatter with `~proplot.axes.Axes.format` (keyword args `xformatter` and `yformatter`, or their aliases `xticklabels` and `yticklabels`). The builtin matplotlib formatters can be referenced by string name, and several new formatters have been introduced -- for example, you can now easily label your axes as fractions or as geographic coordinates. You can also just pass a list of strings or a ``%`` style format directive. See `~proplot.axes.XYAxes.format` and `~proplot.axistools.Formatter` for details."
8585
]
8686
},
87+
{
88+
"cell_type": "raw",
89+
"metadata": {
90+
"raw_mimetype": "text/restructuredtext"
91+
},
92+
"source": [
93+
"ProPlot also changes the default axis formatter. The new formatter trims trailing zeros by default, and can be used to *filter tick labels within some data range*, as demonstrated below. See `~proplot.axistools.AutoFormatter` for details."
94+
]
95+
},
8796
{
8897
"cell_type": "code",
8998
"execution_count": null,
@@ -109,15 +118,6 @@
109118
"plot.rc.reset()"
110119
]
111120
},
112-
{
113-
"cell_type": "raw",
114-
"metadata": {
115-
"raw_mimetype": "text/restructuredtext"
116-
},
117-
"source": [
118-
"ProPlot also changes the default axis formatter. The new formatter trims trailing zeros by default, and can be used to *filter tick labels within some data range*, as demonstrated below. See `~proplot.axistools.AutoFormatter` for details."
119-
]
120-
},
121121
{
122122
"cell_type": "code",
123123
"execution_count": null,
@@ -155,8 +155,8 @@
155155
"raw_mimetype": "text/restructuredtext"
156156
},
157157
"source": [
158-
"The axis scale can now be changed with `~proplot.axes.Axes.format` (keyword args `xscale` and `yscale`). You can also configure the ``'log'`` and ``'symlog'`` axis scales with the more sensible `base`, `linthresh`, `linscale`, and `subs`\n",
159-
"keyword args (i.e. you can omit the ``x`` and ``y``). See `~proplot.axes.XYAxes.format`, `~proplot.axistools.Scale`, `~proplot.axistools.LogScale` and `~proplot.axistools.SymmetricalLogScale` for details."
158+
"The axis scale can now be changed with `~proplot.axes.Axes.format` (keyword args `xscale` and `yscale`). You can now configure the ``'log'`` and ``'symlog'`` axis scales with the more sensible `base`, `linthresh`, `linscale`, and `subs`\n",
159+
"keyword args, rather than ``basex``, ``basey``, etc. Also, ProPlot's `~proplot.axistools.AutoFormatter` formatter is used for all axis scales by default; this can be changed e.g. by passing ``yformatter='log'`` to `~proplot.axes.XYAxes.format`. See `~proplot.axistools.Scale`, `~proplot.axistools.LogScale` and `~proplot.axistools.SymmetricalLogScale` for details."
160160
]
161161
},
162162
{
@@ -201,7 +201,7 @@
201201
"raw_mimetype": "text/restructuredtext"
202202
},
203203
"source": [
204-
"ProPlot also adds several new axis scales. The ``'cutoff'`` scale is great when you have weirdly distributed data (see `~proplot.axistools.CutoffScale`). The ``'sine'`` scale scales the axis as the sine of the coordinate, resulting in an \"area-weighted\" spherical latitude coordinate, and the ``'Mercator'`` scale scales the axis as with the Mercator projection latitude coordinate. The ``'inverse'`` scale is perfect for working with spectral data (this is more useful with `~proplot.axes.XYAxes.dualx` and `~proplot.axes.XYAxes.dualy`; see :ref:`Dual unit axes`)."
204+
"ProPlot adds several new axis scales. The ``'cutoff'`` scale is great when you have weirdly distributed data (see `~proplot.axistools.CutoffScale`). The ``'sine'`` scale scales the axis as the sine of the coordinate, resulting in an \"area-weighted\" spherical latitude coordinate, and the ``'Mercator'`` scale scales the axis as with the Mercator projection latitude coordinate. The ``'inverse'`` scale is perfect for working with spectral data (this is more useful with `~proplot.axes.XYAxes.dualx` and `~proplot.axes.XYAxes.dualy`; see :ref:`Dual unit axes`)."
205205
]
206206
},
207207
{

proplot/axistools.py

Lines changed: 61 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
'SymmetricalLogScale',
3131
]
3232

33-
# Scale preset names and positional args
33+
MAX_DIGITS = 32 # do not draw 1000 digits when LogScale limits include zero!
3434
SCALE_PRESETS = {
3535
'quadratic': ('power', 2,),
3636
'cubic': ('power', 3,),
@@ -194,11 +194,11 @@ def Formatter(formatter, *args, date=False, index=False, **kwargs):
194194
``'theta'`` `~matplotlib.projections.polar.ThetaFormatter` Formats radians as degrees, with a degree symbol
195195
``'pi'`` `FracFormatter` preset Fractions of :math:`\\pi`
196196
``'e'`` `FracFormatter` preset Fractions of *e*
197-
``'deg'`` `SimpleFormatter` preset Trailing degree symbol
198-
``'deglon'`` `SimpleFormatter` preset Trailing degree symbol and cardinal "WE" indicator
199-
``'deglat'`` `SimpleFormatter` preset Trailing degree symbol and cardinal "SN" indicator
200-
``'lon'`` `SimpleFormatter` preset Cardinal "WE" indicator
201-
``'lat'`` `SimpleFormatter` preset Cardinal "SN" indicator
197+
``'deg'`` `AutoFormatter` preset Trailing degree symbol
198+
``'deglon'`` `AutoFormatter` preset Trailing degree symbol and cardinal "WE" indicator
199+
``'deglat'`` `AutoFormatter` preset Trailing degree symbol and cardinal "SN" indicator
200+
``'lon'`` `AutoFormatter` preset Cardinal "WE" indicator
201+
``'lat'`` `AutoFormatter` preset Cardinal "SN" indicator
202202
====================== ============================================== ===================================================================================================================================
203203
204204
date : bool, optional
@@ -255,7 +255,7 @@ def Formatter(formatter, *args, date=False, index=False, **kwargs):
255255
negpos = 'WE'
256256
kwargs.setdefault('suffix', suffix)
257257
kwargs.setdefault('negpos', negpos)
258-
formatter = 'simple'
258+
formatter = 'auto'
259259
# Lookup
260260
if formatter not in formatters:
261261
raise ValueError(
@@ -348,6 +348,15 @@ def Scale(scale, *args, **kwargs):
348348
return scale(*args, **kwargs)
349349

350350

351+
def _zerofix(x, string, precision=6):
352+
"""
353+
Try to fix non-zero tick labels formatted as ``'0'``.
354+
"""
355+
if string.rstrip('0').rstrip('.') == '0' and x != 0:
356+
string = ('{:.%df}' % precision).format(x)
357+
return string
358+
359+
351360
class AutoFormatter(mticker.ScalarFormatter):
352361
"""
353362
The new default formatter, a simple wrapper around
@@ -362,30 +371,40 @@ class AutoFormatter(mticker.ScalarFormatter):
362371
"""
363372
def __init__(self, *args,
364373
zerotrim=None, precision=None, tickrange=None,
365-
prefix=None, suffix=None, **kwargs):
374+
prefix=None, suffix=None, negpos=None, **kwargs):
366375
"""
367376
Parameters
368377
----------
369378
zerotrim : bool, optional
370379
Whether to trim trailing zeros.
371380
Default is :rc:`axes.formatter.zerotrim`.
372-
precision : float, optional
373-
The maximum number of digits after the decimal point.
374381
tickrange : (float, float), optional
375382
Range within which major tick marks are labelled.
376383
prefix, suffix : str, optional
377-
Optional prefix and suffix for all strings.
384+
Prefix and suffix for all strings.
385+
negpos : str, optional
386+
Length-2 string indicating the suffix for "negative" and "positive"
387+
numbers, meant to replace the minus sign. This is useful for
388+
indicating cardinal geographic coordinates.
378389
*args, **kwargs
379-
Passed to `matplotlib.ticker.ScalarFormatter`.
390+
Passed to `~matplotlib.ticker.ScalarFormatter`.
391+
392+
Warning
393+
-------
394+
The matplotlib `~matplotlib.ticker.ScalarFormatter` determines the
395+
number of significant digits based on the axis limits, and therefore
396+
may *truncate* digits while formatting ticks on highly non-linear
397+
axis scales like `~proplot.axistools.LogScale`. We try to correct
398+
this behavior with a patch.
380399
"""
381400
tickrange = tickrange or (-np.inf, np.inf)
382401
super().__init__(*args, **kwargs)
383402
zerotrim = _notNone(zerotrim, rc.get('axes.formatter.zerotrim'))
384-
self._maxprecision = precision
385403
self._zerotrim = zerotrim
386404
self._tickrange = tickrange
387405
self._prefix = prefix or ''
388406
self._suffix = suffix or ''
407+
self._negpos = negpos or ''
389408

390409
def __call__(self, x, pos=None):
391410
"""
@@ -403,75 +422,62 @@ def __call__(self, x, pos=None):
403422
tickrange = self._tickrange
404423
if (x + eps) < tickrange[0] or (x - eps) > tickrange[1]:
405424
return '' # avoid some ticks
406-
# Normal formatting
425+
# Negative positive handling
426+
if not self._negpos:
427+
tail = ''
428+
elif x > 0:
429+
tail = self._negpos[1]
430+
else:
431+
x *= -1
432+
tail = self._negpos[0]
433+
# Format the string
407434
string = super().__call__(x, pos)
408-
if string == '0' and x != 0: # weird LogScale issue
409-
string = ('{:.%df}' % (self._maxprecision or 6)).format(x)
410-
if self._maxprecision is not None and '.' in string:
411-
head, tail = string.split('.')
412-
string = head + '.' + tail[:self._maxprecision]
413-
if self._zerotrim and '.' in string:
414-
string = string.rstrip('0').rstrip('.')
415-
if string == '-0' or string == '\N{MINUS SIGN}0':
416-
string = '0'
435+
for i in range(2):
436+
# Try to fix non-zero values formatted as zero
437+
if self._zerotrim and '.' in string:
438+
string = string.rstrip('0').rstrip('.')
439+
if string == '-0' or string == '\N{MINUS SIGN}0':
440+
string = '0'
441+
if i == 0 and string == '0' and x != 0:
442+
# Hard limit of 10 sigfigs
443+
string = ('{:.%df}' % min(
444+
abs(np.log10(x) // 1), MAX_DIGITS)).format(x)
445+
continue
446+
break
417447
# Prefix and suffix
418448
sign = ''
419449
string = string.replace('-', '\N{MINUS SIGN}')
420450
if string and string[0] == '\N{MINUS SIGN}':
421451
sign, string = string[0], string[1:]
422-
# if 0 < x < 1:
423-
# print(x, repr(string))
424-
return sign + self._prefix + string + self._suffix
452+
if tail:
453+
sign = ''
454+
return sign + self._prefix + string + self._suffix + tail
425455

426456

427-
def SimpleFormatter(*args, precision=6,
428-
prefix=None, suffix=None, negpos=None, zerotrim=True,
429-
**kwargs):
457+
def SimpleFormatter(*args, precision=6, zerotrim=True, **kwargs):
430458
"""
431-
Replicates features of `AutoFormatter`, but as a simpler
459+
Replicates the `zerotrim` feature from `AutoFormatter`, but as a simpler
432460
`~matplotlib.ticker.FuncFormatter` instance. This is more suitable for
433461
arbitrary number formatting not necessarily associated with any
434-
`~matplotlib.axis.Axis` instance, e.g. labelling contours.
462+
`~matplotlib.axis.Axis` instance, e.g. labeling contours.
435463
436464
Parameters
437465
----------
438466
precision : int, optional
439-
Maximum number of digits after the decimal point.
440-
prefix, suffix : str, optional
441-
Optional prefix and suffix for all strings.
442-
negpos : str, optional
443-
Length-2 string that indicates suffix for "negative" and "positive"
444-
numbers, meant to replace the minus sign. This is useful for
445-
indicating cardinal geographic coordinates.
467+
The maximum number of digits after the decimal point.
446468
zerotrim : bool, optional
447469
Whether to trim trailing zeros.
448470
Default is :rc:`axes.formatter.zerotrim`.
449471
"""
450-
prefix = prefix or ''
451-
suffix = suffix or ''
452472
zerotrim = _notNone(zerotrim, rc['axes.formatter.zerotrim'])
453473

454474
def f(x, pos):
455-
# Apply suffix if not on equator/prime meridian
456-
if not negpos:
457-
tail = ''
458-
elif x > 0:
459-
tail = negpos[1]
460-
else:
461-
x *= -1
462-
tail = negpos[0]
463-
# Finally use default formatter
464475
string = ('{:.%df}' % precision).format(x)
465476
if zerotrim and '.' in string:
466477
string = string.rstrip('0').rstrip('.')
467478
if string == '-0' or string == '\N{MINUS SIGN}0':
468479
string = '0'
469-
# Prefix and suffix
470-
sign = ''
471-
string = string.replace('-', '\N{MINUS SIGN}')
472-
if string and string[0] == '\N{MINUS SIGN}':
473-
sign, string = string[0], string[1:]
474-
return sign + prefix + string + suffix + tail
480+
return string.replace('-', '\N{MINUS SIGN}')
475481
return mticker.FuncFormatter(f)
476482

477483

0 commit comments

Comments
 (0)