From c43f7f915f82a56cfa146d5e61b2cc57151788d4 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Wed, 20 May 2020 03:56:44 -0600 Subject: [PATCH 1/7] Add and register SciFormatter --- proplot/constructor.py | 10 ++++----- proplot/ticker.py | 49 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/proplot/constructor.py b/proplot/constructor.py index 5da3737db..3d007f8fa 100644 --- a/proplot/constructor.py +++ b/proplot/constructor.py @@ -114,6 +114,7 @@ FORMATTERS = { # note default LogFormatter uses ugly e+00 notation 'auto': pticker.AutoFormatter, 'frac': pticker.FracFormatter, + 'sci': pticker.SciFormatter, 'sigfig': pticker.SigFigFormatter, 'simple': pticker.SimpleFormatter, 'date': mdates.AutoDateFormatter, @@ -124,9 +125,7 @@ 'func': mticker.FuncFormatter, 'strmethod': mticker.StrMethodFormatter, 'formatstr': mticker.FormatStrFormatter, - 'log': mticker.LogFormatterSciNotation, - 'sci': mticker.LogFormatterSciNotation, - 'math': mticker.LogFormatterMathtext, + 'log': mticker.LogFormatterSciNotation, # NOTE: this is subclass of Mathtext class 'logit': mticker.LogitFormatter, 'eng': mticker.EngFormatter, 'percent': mticker.PercentFormatter, @@ -140,6 +139,7 @@ 'deglon': partial(pticker.SimpleFormatter, negpos='WE', suffix='\N{DEGREE SIGN}', wraprange=(-180, 180)), # noqa: E501 'dmslon': partial(pticker._LongitudeFormatter, dms=True), 'dmslat': partial(pticker._LatitudeFormatter, dms=True), + 'math': mticker.LogFormatterMathtext, # deprecated (use SciNotation subclass) } if hasattr(mdates, 'ConciseDateFormatter'): FORMATTERS['concise'] = mdates.ConciseDateFormatter @@ -1007,6 +1007,7 @@ def Formatter(formatter, *args, date=False, index=False, **kwargs): ====================== ============================================== =============================================================== ``'null'``, ``'none'`` `~matplotlib.ticker.NullFormatter` No tick labels ``'auto'`` `~proplot.ticker.AutoFormatter` New default tick labels for axes + ``'sci'`` `~proplot.ticker.SciFormatter` Format ticks with scientific notation. ``'simple'`` `~proplot.ticker.SimpleFormatter` New default tick labels for e.g. contour labels ``'sigfig'`` `~proplot.ticker.SigFigFormatter` Format labels using the first ``N`` significant digits ``'frac'`` `~proplot.ticker.FracFormatter` Rational fractions @@ -1018,9 +1019,8 @@ def Formatter(formatter, *args, date=False, index=False, **kwargs): ``'formatstr'`` `~matplotlib.ticker.FormatStrFormatter` From C-style ``string % format`` notation ``'func'`` `~matplotlib.ticker.FuncFormatter` Use an arbitrary function ``'index'`` `~matplotlib.ticker.IndexFormatter` List of strings corresponding to non-negative integer positions - ``'log'``, ``'sci'`` `~matplotlib.ticker.LogFormatterSciNotation` For log-scale axes with scientific notation + ``'log'`` `~matplotlib.ticker.LogFormatterSciNotation` For log-scale axes with scientific notation ``'logit'`` `~matplotlib.ticker.LogitFormatter` For logistic-scale axes - ``'math'`` `~matplotlib.ticker.LogFormatterMathtext` For log-scale axes with math text ``'percent'`` `~matplotlib.ticker.PercentFormatter` Trailing percent sign ``'scalar'`` `~matplotlib.ticker.ScalarFormatter` Old default tick labels for axes ``'strmethod'`` `~matplotlib.ticker.StrMethodFormatter` From the ``string.format`` method diff --git a/proplot/ticker.py b/proplot/ticker.py index 29cd14126..f2619a28b 100644 --- a/proplot/ticker.py +++ b/proplot/ticker.py @@ -22,6 +22,7 @@ 'FracFormatter', 'LongitudeLocator', 'LatitudeLocator', + 'SciFormatter', 'SigFigFormatter', 'SimpleFormatter', ] @@ -384,6 +385,54 @@ def _wrap_tick_range(x, wraprange): return (x - base) % modulus + base +def SciFormatter(precision=None, zerotrim=None): + """ + Return a `~matplotlib.ticker.FuncFormatter` that formats + the number with scientific notation. + + Parameters + ---------- + precision : int, optional + The maximum number of digits after the decimal point. Default is ``6`` + when `zerotrim` is ``True`` and ``2`` otherwise. + zerotrim : bool, optional + Whether to trim trailing zeros. Default is + """ + from .config import rc + zerotrim = _not_none(zerotrim, rc['formatter.zerotrim']) + if precision is None: + precision = 6 if zerotrim else 2 + + def func(x, pos): + # Get string + decimal_point = AutoFormatter._get_default_decimal_point() + string = ('{:.%de}' % precision).format(x) + parts = string.split('e') + + # Trim trailing zeros + significand = parts[0].rstrip(decimal_point) + if zerotrim: + significand = AutoFormatter._trim_trailing_zeros(significand, decimal_point) + + # Get sign and exponent + sign = parts[1][0].replace('+', '') + exponent = parts[1][1:].lstrip('0') + if exponent: + exponent = f'10^{{{sign}{exponent}}}' + if significand and exponent: + string = rf'{significand}{{\times}}{exponent}' + else: + string = rf'{significand}{exponent}' + + # Ensure unicode minus sign + string = AutoFormatter._minus_format(string) + + # Return TeX string + return f'${string}$' + + return mticker.FuncFormatter(func) + + def SigFigFormatter(sigfig=1, zerotrim=None): """ Return a `~matplotlib.ticker.FuncFormatter` that rounds numbers From 1b49c72e07cd9332b0aeb2fe429580931192f6ed Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Wed, 20 May 2020 03:57:08 -0600 Subject: [PATCH 2/7] Always format ScalarFormatter exponent with mathtext --- proplot/ticker.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/proplot/ticker.py b/proplot/ticker.py index f2619a28b..fad5862c0 100644 --- a/proplot/ticker.py +++ b/proplot/ticker.py @@ -251,6 +251,13 @@ def __call__(self, x, pos=None): string = string + tail # add negative-positive indicator return string + def get_offset(self): + """ + Get the offset but *always* use math text. + """ + with _set_state(self, _useMathText=True): + return super().get_offset() + @staticmethod def _add_prefix_suffix(string, prefix=None, suffix=None): """ From 9bd3dec8ff171d05705a76d46ae0acbd9d27513f Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Wed, 20 May 2020 03:57:33 -0600 Subject: [PATCH 3/7] Increase default SigFigFormatter precision to 3 --- proplot/ticker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proplot/ticker.py b/proplot/ticker.py index fad5862c0..0eee397c7 100644 --- a/proplot/ticker.py +++ b/proplot/ticker.py @@ -440,7 +440,7 @@ def func(x, pos): return mticker.FuncFormatter(func) -def SigFigFormatter(sigfig=1, zerotrim=None): +def SigFigFormatter(sigfig=3, zerotrim=None): """ Return a `~matplotlib.ticker.FuncFormatter` that rounds numbers to the specified number of *significant digits*. From f1ebd5e1055f636d667b927383c8a88a358af845 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Wed, 20 May 2020 03:58:07 -0600 Subject: [PATCH 4/7] Better default SimpleFormatter behavior when zerotrim=False --- proplot/ticker.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/proplot/ticker.py b/proplot/ticker.py index 0eee397c7..620c02ea6 100644 --- a/proplot/ticker.py +++ b/proplot/ticker.py @@ -478,7 +478,7 @@ def func(x, pos): @docstring.add_snippets def SimpleFormatter( - precision=6, zerotrim=None, tickrange=None, wraprange=None, + precision=None, zerotrim=None, tickrange=None, wraprange=None, prefix=None, suffix=None, negpos=None, ): """ @@ -490,11 +490,14 @@ def SimpleFormatter( Parameters ---------- precision : int, optional - The maximum number of digits after the decimal point. Default is ``6``. + The maximum number of digits after the decimal point. Default is ``6`` + when `zerotrim` is ``True`` and ``2`` otherwise. %(formatter.params)s """ from .config import rc zerotrim = _not_none(zerotrim, rc['formatter.zerotrim']) + if precision is None: + precision = 6 if zerotrim else 2 tickrange = tickrange or (-np.inf, np.inf) prefix = prefix or '' suffix = suffix or '' From 2db49185e33be321baec946d9cf0734b12316fa4 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Wed, 20 May 2020 03:59:07 -0600 Subject: [PATCH 5/7] Permit changing 'wraprange' with format() keyword args --- proplot/axes/cartesian.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/proplot/axes/cartesian.py b/proplot/axes/cartesian.py index 4bae14cbc..ce71739f6 100644 --- a/proplot/axes/cartesian.py +++ b/proplot/axes/cartesian.py @@ -45,7 +45,7 @@ _alt_kwargs = ( # TODO: More systematic approach? 'label', 'locator', 'formatter', 'ticks', 'ticklabels', 'minorlocator', 'minorticks', 'tickminor', - 'ticklen', 'tickrange', 'tickdir', 'ticklabeldir', 'tickrotation', + 'ticklen', 'tickrange', 'tickdir', 'ticklabeldir', 'tickrotation', 'wraprange', 'bounds', 'margin', 'color', 'linewidth', 'grid', 'gridminor', 'gridcolor', 'locator_kw', 'formatter_kw', 'minorlocator_kw', 'label_kw', ) @@ -477,6 +477,7 @@ def format( xtickminor=None, ytickminor=None, xticklabeldir=None, yticklabeldir=None, xtickrange=None, ytickrange=None, + xwraprange=None, ywraprange=None, xreverse=None, yreverse=None, xlabel=None, ylabel=None, xlim=None, ylim=None, @@ -595,9 +596,15 @@ def format( *x* axes. xtickrange, ytickrange : (float, float), optional The *x* and *y* axis data ranges within which major tick marks - are labelled. For example, the tick range ``(-1,1)`` with - axis range ``(-5,5)`` and a tick interval of 1 will only - label the ticks marks at -1, 0, and 1. + are labelled. For example, the tick range ``(-1, 1)`` with + axis range ``(-5, 5)`` and a tick interval of 1 will only + label the ticks marks at -1, 0, and 1. See + `~proplot.ticker.AutoFormatter` for details. + xwraprange, ywraprange : (float, float), optional + The *x* and *y* axis data ranges with which major tick mark + values are *wrapped*. For example, the wrap range ``(0, 3)`` + causes the values 0 through 9 to be formatted as 0, 1, 2, + 0, 1, 2, 0, 1, 2, 0. See `~proplot.ticker.AutoFormatter` for details. xmargin, ymargin : float, optional The default margin between plotted content and the *x* and *y* axis spines. Value is proportional to the width, height of the axes. @@ -759,6 +766,7 @@ def format( tickminor, minorlocator, lim, reverse, scale, locator, tickrange, + wraprange, formatter, tickdir, ticklabeldir, rotation, label_kw, scale_kw, @@ -776,6 +784,7 @@ def format( (xtickminor, ytickminor), (xminorlocator, yminorlocator), (xlim, ylim), (xreverse, yreverse), (xscale, yscale), (xlocator, ylocator), (xtickrange, ytickrange), + (xwraprange, ywraprange), (xformatter, yformatter), (xtickdir, ytickdir), (xticklabeldir, yticklabeldir), (xrotation, yrotation), (xlabel_kw, ylabel_kw), (xscale_kw, yscale_kw), @@ -1073,17 +1082,24 @@ def format( # NOTE: The only reliable way to disable ticks labels and then # restore them is by messing with the *formatter*, rather than # setting labelleft=False, labelright=False, etc. - if formatter is not None or tickrange is not None: + if ( + formatter is not None + or tickrange is not None + or wraprange is not None + ): # Tick range - if tickrange is not None: + if tickrange is not None or wraprange is not None: if formatter not in (None, 'auto'): warnings._warn_proplot( - 'The tickrange feature requires ' + 'The tickrange and autorange features require ' 'proplot.AutoFormatter formatter. Overriding ' 'input formatter.' ) formatter = 'auto' - formatter_kw.setdefault('tickrange', tickrange) + if tickrange is not None: + formatter_kw.setdefault('tickrange', tickrange) + if wraprange is not None: + formatter_kw.setdefault('wraprange', wraprange) # Set the formatter # Note some formatters require 'locator' as keyword arg From bd5f338e69ff1452dab52e2352f76e8000784b5f Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Wed, 20 May 2020 04:10:38 -0600 Subject: [PATCH 6/7] Update axis examples --- docs/axis.py | 59 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/docs/axis.py b/docs/axis.py index b7bd8ebc9..b24872970 100644 --- a/docs/axis.py +++ b/docs/axis.py @@ -112,16 +112,17 @@ # # These keyword arguments can be used to apply built-in matplotlib # `~matplotlib.ticker.Formatter`\ s by their "registered" names (e.g. -# ``xformatter='log'``), to apply new "preset" axis formatters (e.g. -# ``xformatter='deglat'`` to label ticks as the geographic latitude or -# ``xformatter='pi'`` to label ticks as fractions of :math:`\pi`), to apply a -# ``%``-style format directive with `~matplotlib.ticker.FormatStrFormatter` -# (e.g. ``xformatter='%.0f'``), or to apply custom tick labels with -# `~matplotlib.ticker.FixedFormatter` (just like -# `~matplotlib.axes.Axes.set_xticklabels` and -# `~matplotlib.axes.Axes.set_yticklabels`). See -# `~proplot.axes.CartesianAxes.format` and `~proplot.constructor.Formatter` -# for details. +# ``xformatter='log'``), to apply a ``%``-style format directive with +# `~matplotlib.ticker.FormatStrFormatter` (e.g. ``xformatter='%.0f'``), or +# to apply custom tick labels with `~matplotlib.ticker.FixedFormatter` (just +# like `~matplotlib.axes.Axes.set_xticklabels` and +# `~matplotlib.axes.Axes.set_yticklabels`). They can also be used +# to apply one of ProPlot's new axis formatters -- for example, +# ``xformatter='deglat'`` to label ticks as the geographic latitude, +# ``xformatter='pi'`` to label ticks as fractions of :math:`\pi`, +# or ``xformatter='sci'`` to label ticks with scientific notation. +# See `~proplot.axes.CartesianAxes.format` and +# `~proplot.constructor.Formatter` for details. # # ProPlot also changes the default axis formatter to # `~proplot.ticker.AutoFormatter`. This class trims trailing zeros by @@ -137,36 +138,48 @@ suptitlecolor='w', gridcolor='w', color='w', titleloc='upper center', titlecolor='w', titleborder=False, ) -fig, axs = plot.subplots(nrows=6, axwidth=5, aspect=(8, 1), share=0) +fig, axs = plot.subplots(nrows=8, axwidth=5, aspect=(8, 1), share=0) -# Fraction formatters +# Scientific notation axs[0].format( + xlim=(0, 1e20), + xformatter='sci', title='SciFormatter' +) + +# N significant figures for ticks at specific values +axs[1].format( + xlim=(0, 20), xlocator=(0.0034, 3.233, 9.2, 15.2344, 7.2343, 19.58), + xformatter=('sigfig', 2), title='SigFigFormatter', # 2 significant digits +) + +# Fraction formatters +axs[2].format( xlim=(0, 3 * np.pi), xlocator=plot.arange(0, 4, 0.25) * np.pi, xformatter='pi', title='FracFormatter', ) -axs[1].format( +axs[3].format( xlim=(0, 2 * np.e), xlocator=plot.arange(0, 2, 0.5) * np.e, xticklabels='e', title='FracFormatter', ) # Geographic formatter -axs[2].format( +axs[4].format( xlim=(-90, 90), xlocator=plot.arange(-90, 90, 30), - xformatter='deglat', title='Geographic preset' + xformatter='deglat', title='Geographic Formatter' ) # User input labels -axs[3].format( +axs[5].format( xlim=(-1.01, 1), xlocator=0.5, xticklabels=['a', 'b', 'c', 'd', 'e'], title='FixedFormatter', ) # Custom style labels -axs[4].format( +axs[6].format( xlim=(0, 0.001), xlocator=0.0001, xformatter='%.E', title='FormatStrFormatter', ) -axs[5].format( +axs[7].format( xlim=(0, 100), xtickminor=False, xlocator=20, xformatter='{x:.1f}', title='StrMethodFormatter', ) @@ -178,7 +191,7 @@ plot.rc.linewidth = 2 plot.rc.fontsize = 11 locator = [0, 0.25, 0.5, 0.75, 1] -fig, axs = plot.subplots([[1, 1, 2, 2], [0, 3, 3, 0]], axwidth=1.5, share=0) +fig, axs = plot.subplots(ncols=2, nrows=2, axwidth=1.5, share=0) # Formatter comparison axs[0].format( @@ -187,11 +200,17 @@ axs[1].format(yticklabelloc='both', title='ProPlot formatter') axs[:2].format(xlocator=locator, ylocator=locator) -# Limiting the formatter tick range +# Limiting the tick range axs[2].format( title='Omitting tick labels', ticklen=5, xlim=(0, 5), ylim=(0, 5), xtickrange=(0, 2), ytickrange=(0, 2), xlocator=1, ylocator=1 ) + +# Setting the wrap range +axs[3].format( + title='Wrapping the tick range', ticklen=5, xlim=(0, 7), ylim=(0, 6), + xwraprange=(0, 5), ywraprange=(0, 3), xlocator=1, ylocator=1 +) axs.format( ytickloc='both', yticklabelloc='both', titlepad='0.5em', suptitle='Default formatters demo' From fb2ff210a9adad86d6738976b219328794c43f0b Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Wed, 20 May 2020 04:12:47 -0600 Subject: [PATCH 7/7] Update changelog --- CHANGELOG.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1db4872b8..309193cd8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -123,6 +123,9 @@ There are quite a lot of deprecations for this release. .. rubric:: Features +- Add `~proplot.ticker.SigFigFormatter` (:pr:`149`, :commit:`da6105d2`) + and `~proplot.ticker.SciFormatter` (:pr:`175`, :commit:`c43f7f91`) + axis formatters. - Use `_LonAxis` and `_LatAxis` dummy axes with custom `LongitudeLocator` and `LatitudeLocator` to control geographic gridlines (:pr:`168`). - Add ``'dmslat'`` and ``'dmslon'`` as formatters for cartopy projections, @@ -165,8 +168,6 @@ There are quite a lot of deprecations for this release. (:commit:`f801852b`). - Change default line style for geographic gridlines from ``':'`` to ``'-'`` and match style from primary gridlines (:commit:`f801852b`). -- Support cartopy inline meridian and parallel gridlines and support - changing the gridline padding (:commit:`###`). - Support `cartopy 0.18 `__ locators, formatters, deprecations, and new labelling features (:pr:`158`). - Support building a colormap and `DiscreteNorm` inside `~matplotlib.axes.Axes.scatter`, @@ -216,7 +217,7 @@ There are quite a lot of deprecations for this release. - Fix various issues with axis label sharing and axis sharing for twinned axes and panel axes (:pr:`164`). - Permit modifying existing cartopy geographic features with successive - calls to `~proplot.axes.GeoAxes.format` (:commit:`###`). + calls to `~proplot.axes.GeoAxes.format` (:pr:`168`). - Fix issue drawing bar plots with datetime *x* axes (:pr:`156`). - Fix issue where `~proplot.ticker.AutoFormatter` tools were not locale-aware, i.e. use comma as decimal point sometimes (:commit:`c7636296`).