diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c74087906..295448915 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -60,6 +60,24 @@ ProPlot v0.3.0 (2020-01-##) with a default ``.proplotrc`` file, change the auto-generated user ``.proplotrc`` (:pr:`50`). +ProPlot v0.2.6 (2019-12-09) +=========================== + +.. rubric:: Bug fixes + +- Fix issue where `~proplot.styletools.AutoFormatter` logarithmic scale + points are incorrect (:commit:`9b164733`). + +.. rubric:: Internals + +- Remove `prefix`, `suffix`, and `negpos` keyword args from + `~proplot.styletools.SimpleFormatter`, remove `precision` keyword arg from + `~proplot.styletools.AutoFormatter` (it automatically figures out the + necessary precision!). +- Make ``'deglat'``, ``'deglon'``, ``'lat'``, ``'lon'``, and ``'deg'`` instances + of `~proplot.styletools.AutoFormatter` instead of `~proplot.styletools.SimpleFormatter`. + The latter should just be used for contours. + ProPlot v0.2.6 (2019-12-08) =========================== .. rubric:: Bug fixes diff --git a/docs/axis.ipynb b/docs/axis.ipynb index 92c95f761..55c13e031 100644 --- a/docs/axis.ipynb +++ b/docs/axis.ipynb @@ -84,6 +84,15 @@ "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." ] }, + { + "cell_type": "raw", + "metadata": { + "raw_mimetype": "text/restructuredtext" + }, + "source": [ + "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." + ] + }, { "cell_type": "code", "execution_count": null, @@ -109,15 +118,6 @@ "plot.rc.reset()" ] }, - { - "cell_type": "raw", - "metadata": { - "raw_mimetype": "text/restructuredtext" - }, - "source": [ - "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." - ] - }, { "cell_type": "code", "execution_count": null, @@ -155,8 +155,8 @@ "raw_mimetype": "text/restructuredtext" }, "source": [ - "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", - "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." + "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", + "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." ] }, { @@ -201,7 +201,7 @@ "raw_mimetype": "text/restructuredtext" }, "source": [ - "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`)." + "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`)." ] }, { diff --git a/proplot/axistools.py b/proplot/axistools.py index 2cf49fba7..46e06c42c 100644 --- a/proplot/axistools.py +++ b/proplot/axistools.py @@ -30,7 +30,7 @@ 'SymmetricalLogScale', ] -# Scale preset names and positional args +MAX_DIGITS = 32 # do not draw 1000 digits when LogScale limits include zero! SCALE_PRESETS = { 'quadratic': ('power', 2,), 'cubic': ('power', 3,), @@ -194,11 +194,11 @@ def Formatter(formatter, *args, date=False, index=False, **kwargs): ``'theta'`` `~matplotlib.projections.polar.ThetaFormatter` Formats radians as degrees, with a degree symbol ``'pi'`` `FracFormatter` preset Fractions of :math:`\\pi` ``'e'`` `FracFormatter` preset Fractions of *e* - ``'deg'`` `SimpleFormatter` preset Trailing degree symbol - ``'deglon'`` `SimpleFormatter` preset Trailing degree symbol and cardinal "WE" indicator - ``'deglat'`` `SimpleFormatter` preset Trailing degree symbol and cardinal "SN" indicator - ``'lon'`` `SimpleFormatter` preset Cardinal "WE" indicator - ``'lat'`` `SimpleFormatter` preset Cardinal "SN" indicator + ``'deg'`` `AutoFormatter` preset Trailing degree symbol + ``'deglon'`` `AutoFormatter` preset Trailing degree symbol and cardinal "WE" indicator + ``'deglat'`` `AutoFormatter` preset Trailing degree symbol and cardinal "SN" indicator + ``'lon'`` `AutoFormatter` preset Cardinal "WE" indicator + ``'lat'`` `AutoFormatter` preset Cardinal "SN" indicator ====================== ============================================== =================================================================================================================================== date : bool, optional @@ -255,7 +255,7 @@ def Formatter(formatter, *args, date=False, index=False, **kwargs): negpos = 'WE' kwargs.setdefault('suffix', suffix) kwargs.setdefault('negpos', negpos) - formatter = 'simple' + formatter = 'auto' # Lookup if formatter not in formatters: raise ValueError( @@ -348,6 +348,15 @@ def Scale(scale, *args, **kwargs): return scale(*args, **kwargs) +def _zerofix(x, string, precision=6): + """ + Try to fix non-zero tick labels formatted as ``'0'``. + """ + if string.rstrip('0').rstrip('.') == '0' and x != 0: + string = ('{:.%df}' % precision).format(x) + return string + + class AutoFormatter(mticker.ScalarFormatter): """ The new default formatter, a simple wrapper around @@ -362,30 +371,40 @@ class AutoFormatter(mticker.ScalarFormatter): """ def __init__(self, *args, zerotrim=None, precision=None, tickrange=None, - prefix=None, suffix=None, **kwargs): + prefix=None, suffix=None, negpos=None, **kwargs): """ Parameters ---------- zerotrim : bool, optional Whether to trim trailing zeros. Default is :rc:`axes.formatter.zerotrim`. - precision : float, optional - The maximum number of digits after the decimal point. tickrange : (float, float), optional Range within which major tick marks are labelled. prefix, suffix : str, optional - Optional prefix and suffix for all strings. + Prefix and suffix for all strings. + negpos : str, optional + Length-2 string indicating the suffix for "negative" and "positive" + numbers, meant to replace the minus sign. This is useful for + indicating cardinal geographic coordinates. *args, **kwargs - Passed to `matplotlib.ticker.ScalarFormatter`. + Passed to `~matplotlib.ticker.ScalarFormatter`. + + Warning + ------- + The matplotlib `~matplotlib.ticker.ScalarFormatter` determines the + number of significant digits based on the axis limits, and therefore + may *truncate* digits while formatting ticks on highly non-linear + axis scales like `~proplot.axistools.LogScale`. We try to correct + this behavior with a patch. """ tickrange = tickrange or (-np.inf, np.inf) super().__init__(*args, **kwargs) zerotrim = _notNone(zerotrim, rc.get('axes.formatter.zerotrim')) - self._maxprecision = precision self._zerotrim = zerotrim self._tickrange = tickrange self._prefix = prefix or '' self._suffix = suffix or '' + self._negpos = negpos or '' def __call__(self, x, pos=None): """ @@ -403,75 +422,62 @@ def __call__(self, x, pos=None): tickrange = self._tickrange if (x + eps) < tickrange[0] or (x - eps) > tickrange[1]: return '' # avoid some ticks - # Normal formatting + # Negative positive handling + if not self._negpos: + tail = '' + elif x > 0: + tail = self._negpos[1] + else: + x *= -1 + tail = self._negpos[0] + # Format the string string = super().__call__(x, pos) - if string == '0' and x != 0: # weird LogScale issue - string = ('{:.%df}' % (self._maxprecision or 6)).format(x) - if self._maxprecision is not None and '.' in string: - head, tail = string.split('.') - string = head + '.' + tail[:self._maxprecision] - if self._zerotrim and '.' in string: - string = string.rstrip('0').rstrip('.') - if string == '-0' or string == '\N{MINUS SIGN}0': - string = '0' + for i in range(2): + # Try to fix non-zero values formatted as zero + if self._zerotrim and '.' in string: + string = string.rstrip('0').rstrip('.') + if string == '-0' or string == '\N{MINUS SIGN}0': + string = '0' + if i == 0 and string == '0' and x != 0: + # Hard limit of 10 sigfigs + string = ('{:.%df}' % min( + abs(np.log10(x) // 1), MAX_DIGITS)).format(x) + continue + break # Prefix and suffix sign = '' string = string.replace('-', '\N{MINUS SIGN}') if string and string[0] == '\N{MINUS SIGN}': sign, string = string[0], string[1:] - # if 0 < x < 1: - # print(x, repr(string)) - return sign + self._prefix + string + self._suffix + if tail: + sign = '' + return sign + self._prefix + string + self._suffix + tail -def SimpleFormatter(*args, precision=6, - prefix=None, suffix=None, negpos=None, zerotrim=True, - **kwargs): +def SimpleFormatter(*args, precision=6, zerotrim=True, **kwargs): """ - Replicates features of `AutoFormatter`, but as a simpler + Replicates the `zerotrim` feature from `AutoFormatter`, but as a simpler `~matplotlib.ticker.FuncFormatter` instance. This is more suitable for arbitrary number formatting not necessarily associated with any - `~matplotlib.axis.Axis` instance, e.g. labelling contours. + `~matplotlib.axis.Axis` instance, e.g. labeling contours. Parameters ---------- precision : int, optional - Maximum number of digits after the decimal point. - prefix, suffix : str, optional - Optional prefix and suffix for all strings. - negpos : str, optional - Length-2 string that indicates suffix for "negative" and "positive" - numbers, meant to replace the minus sign. This is useful for - indicating cardinal geographic coordinates. + The maximum number of digits after the decimal point. zerotrim : bool, optional Whether to trim trailing zeros. Default is :rc:`axes.formatter.zerotrim`. """ - prefix = prefix or '' - suffix = suffix or '' zerotrim = _notNone(zerotrim, rc['axes.formatter.zerotrim']) def f(x, pos): - # Apply suffix if not on equator/prime meridian - if not negpos: - tail = '' - elif x > 0: - tail = negpos[1] - else: - x *= -1 - tail = negpos[0] - # Finally use default formatter string = ('{:.%df}' % precision).format(x) if zerotrim and '.' in string: string = string.rstrip('0').rstrip('.') if string == '-0' or string == '\N{MINUS SIGN}0': string = '0' - # Prefix and suffix - sign = '' - string = string.replace('-', '\N{MINUS SIGN}') - if string and string[0] == '\N{MINUS SIGN}': - sign, string = string[0], string[1:] - return sign + prefix + string + suffix + tail + return string.replace('-', '\N{MINUS SIGN}') return mticker.FuncFormatter(f)