diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index d86de461efc8..4f539fcd362e 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -440,7 +440,7 @@ class SymmetricalLogScale(ScaleBase): """ name = 'symlog' - def __init__(self, axis, *, base=10, linthresh=2, subs=None, linscale=1): + def __init__(self, axis, *, base=10, linthresh=2, subs='auto', linscale=1): self._transform = SymmetricalLogTransform(base, linthresh, linscale) self.subs = subs @@ -454,7 +454,9 @@ def set_default_locators_and_formatters(self, axis): axis.set_major_formatter(LogFormatterSciNotation(self.base)) axis.set_minor_locator(SymmetricalLogLocator(self.get_transform(), self.subs)) - axis.set_minor_formatter(NullFormatter()) + axis.set_minor_formatter( + LogFormatterSciNotation(self.base, + labelOnlyBase=(self.subs != 'auto'))) def get_transform(self): """Return the `.SymmetricalLogTransform` associated with this scale.""" diff --git a/lib/matplotlib/scale.pyi b/lib/matplotlib/scale.pyi index 7fec8e68cc5a..0d72b273e9e8 100644 --- a/lib/matplotlib/scale.pyi +++ b/lib/matplotlib/scale.pyi @@ -108,7 +108,7 @@ class SymmetricalLogScale(ScaleBase): *, base: float = ..., linthresh: float = ..., - subs: Iterable[int] | None = ..., + subs: Iterable[int] | Literal["auto", "all"] | None = ..., linscale: float = ... ) -> None: ... @property diff --git a/lib/matplotlib/tests/baseline_images/test_axes/symlog.pdf b/lib/matplotlib/tests/baseline_images/test_axes/symlog.pdf index d3a109773d24..ed8a23d17b6f 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/symlog.pdf and b/lib/matplotlib/tests/baseline_images/test_axes/symlog.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/symlog2.pdf b/lib/matplotlib/tests/baseline_images/test_axes/symlog2.pdf index d5b93ce3bfbe..36fb9b1a144f 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/symlog2.pdf and b/lib/matplotlib/tests/baseline_images/test_axes/symlog2.pdf differ diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 961daaa1d167..831e2d4012bf 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -601,12 +601,12 @@ def test_set_params(self): class TestSymmetricalLogLocator: def test_set_params(self): """ - Create symmetrical log locator with default subs =[1.0] numticks = 15, + Create symmetrical log locator with default subs=[1.0] numticks='auto', and change it to something else. See if change was successful. Should not exception. """ - sym = mticker.SymmetricalLogLocator(base=10, linthresh=1) + sym = mticker.SymmetricalLogLocator(base=10, linthresh=1, linscale=1) sym.set_params(subs=[2.0], numticks=8) assert sym._subs == [2.0] assert sym.numticks == 8 @@ -614,32 +614,34 @@ def test_set_params(self): @pytest.mark.parametrize( 'vmin, vmax, expected', [ - (0, 1, [0, 1]), - (-1, 1, [-1, 0, 1]), + (0, 1, [-1, 0, 1, 10]), + (-1, 1, [-10, -1, 0, 1, 10]), ], ) def test_values(self, vmin, vmax, expected): # https://github.com/matplotlib/matplotlib/issues/25945 - sym = mticker.SymmetricalLogLocator(base=10, linthresh=1) + sym = mticker.SymmetricalLogLocator(base=10, linthresh=1, linscale=1) ticks = sym.tick_values(vmin=vmin, vmax=vmax) assert_array_equal(ticks, expected) def test_subs(self): - sym = mticker.SymmetricalLogLocator(base=10, linthresh=1, subs=[2.0, 4.0]) + sym = mticker.SymmetricalLogLocator(base=10, linthresh=1, linscale=1, + subs=[2.0, 4.0]) sym.create_dummy_axis() sym.axis.set_view_interval(-10, 10) - assert (sym() == [-20., -40., -2., -4., 0., 2., 4., 20., 40.]).all() + assert (sym() == [-400., -200., -40., -20., -4., -2., -0.4, -0.2, -0.1, 0., + 0.1, 0.2, 0.4, 2., 4., 20., 40., 200., 400.]).all() def test_extending(self): - sym = mticker.SymmetricalLogLocator(base=10, linthresh=1) + sym = mticker.SymmetricalLogLocator(base=10, linthresh=1, linscale=1) sym.create_dummy_axis() sym.axis.set_view_interval(8, 9) - assert (sym() == [1.0]).all() - sym.axis.set_view_interval(8, 12) assert (sym() == [1.0, 10.0]).all() - assert sym.view_limits(10, 10) == (1, 100) - assert sym.view_limits(-10, -10) == (-100, -1) - assert sym.view_limits(0, 0) == (-0.001, 0.001) + sym.axis.set_view_interval(8, 12) + assert (sym() == [1.0, 10.0, 100.0]).all() + assert sym.view_limits(10, 10) == (1.0, 100.0) + assert sym.view_limits(-10, -10) == (-100.0, -1.0) + assert sym.view_limits(0, 0) == (-1.0, 1.0) class TestAsinhLocator: diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 41114aafbf3e..b940affcc8ab 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -812,6 +812,92 @@ def _set_format(self): self.format = r'$\mathdefault{%s}$' % self.format +class _SymmetricalLogUtil: + """ + Helper class for working with symmetrical log scales. + + Parameters + ---------- + transform : `~.scale.SymmetricalLogTransform`, optional + If set, defines *base*, *linthresh* and *linscale* of the symlog transform. + base, linthresh, linscale : float, optional + The *base*, *linthresh* and *linscale* of the symlog transform, as + documented for `.SymmetricalLogScale`. These parameters are only used + if *transform* is not set. + """ + + def __init__(self, transform=None, base=None, linthresh=None, linscale=None): + if transform is not None: + self.base = transform.base + self.linthresh = transform.linthresh + self.linscale = transform.linscale + elif base is not None and linthresh is not None and linscale is not None: + self.base = base + self.linthresh = linthresh + self.linscale = linscale + else: + raise ValueError("Either transform, or all of base, linthresh and " + "linscale must be provided.") + + def pos(self, val): + """ + Calculate the normalized position of the value on the axis. + It is normalized such that the distance between two logarithmic decades + is 1 and the position of linthresh is linscale. + """ + sign, val = np.sign(val), np.abs(val) / self.linthresh + if val > 1: + val = self.linscale + np.log(val) / np.log(self.base) + else: + val *= self.linscale + return sign * val + + def unpos(self, val): + """The inverse of pos.""" + sign, val = np.sign(val), np.abs(val) + if val > self.linscale: + val = np.power(self.base, val - self.linscale) + else: + val /= self.linscale + return sign * val * self.linthresh + + def firstdec(self): + """ + Get the first decade (i.e. first positive major tick candidate). + It shall be at least half the width of a logarithmic decade from the + origin (i.e. its pos shall be at least 0.5). + """ + firstexp = np.ceil(np.log(self.unpos(0.5)) / np.log(self.base)) + firstpow = np.power(self.base, firstexp) + return firstexp, firstpow + + def dec(self, val): + """ + Calculate the decade number of the value. The first decade to have a + position (given by _pos) of at least 0.5 is given the number 1, the + value 0 is given the decade number 0. + """ + firstexp, firstpow = self.firstdec() + sign, val = np.sign(val), np.abs(val) + if val > firstpow: + val = np.log(val) / np.log(self.base) - firstexp + 1 + else: + # We scale linearly in order to get a monotonous mapping between + # 0 and 1, though the linear nature is arbitrary. + val /= firstpow + return sign * val + + def undec(self, val): + """The inverse of dec.""" + firstexp, firstpow = self.firstdec() + sign, val = np.sign(val), np.abs(val) + if val > 1: + val = np.power(self.base, val - 1 + firstexp) + else: + val *= firstpow + return sign * val + + class LogFormatter(Formatter): """ Base class for formatting ticks on a log or symlog scale. @@ -840,9 +926,9 @@ class LogFormatter(Formatter): avoid crowding. If ``numdec > subset`` then no minor ticks will be labeled. - linthresh : None or float, default: None - If a symmetric log scale is in use, its ``linthresh`` - parameter must be supplied here. + linthresh, linscale : None or float, default: None + If a symmetric log scale is in use, its ``linthresh`` and ``linscale`` + parameters must be supplied here. Notes ----- @@ -872,7 +958,7 @@ class LogFormatter(Formatter): def __init__(self, base=10.0, labelOnlyBase=False, minor_thresholds=None, - linthresh=None): + linthresh=None, linscale=None): self.set_base(base) self.set_label_minor(labelOnlyBase) @@ -884,6 +970,9 @@ def __init__(self, base=10.0, labelOnlyBase=False, self.minor_thresholds = minor_thresholds self._sublabels = None self._linthresh = linthresh + self._linscale = linscale + self._symlogutil = None + self._firstsublabels = None def set_base(self, base): """ @@ -905,6 +994,22 @@ def set_label_minor(self, labelOnlyBase): """ self.labelOnlyBase = labelOnlyBase + @property + def _symlog(self): + if self._symlogutil is not None: + return True + if self._linthresh is not None and self._linscale is not None: + self._symlogutil = _SymmetricalLogUtil(base=self._base, + linthresh=self._linthresh, + linscale=self._linscale) + return True + try: + self._symlogutil = _SymmetricalLogUtil(self.axis.get_transform()) + return True + except AttributeError: + pass + return False + def set_locs(self, locs=None): """ Use axis view limits to control which ticks are labeled. @@ -915,19 +1020,11 @@ def set_locs(self, locs=None): self._sublabels = None return - # Handle symlog case: - linthresh = self._linthresh - if linthresh is None: - try: - linthresh = self.axis.get_transform().linthresh - except AttributeError: - pass - vmin, vmax = self.axis.get_view_interval() if vmin > vmax: vmin, vmax = vmax, vmin - if linthresh is None and vmin <= 0: + if not self._symlog and vmin <= 0: # It's probably a colorbar with # a format kwarg setting a LogFormatter in the manner # that worked with 1.5.x, but that doesn't work now. @@ -935,16 +1032,8 @@ def set_locs(self, locs=None): return b = self._base - if linthresh is not None: # symlog - # Only compute the number of decades in the logarithmic part of the - # axis - numdec = 0 - if vmin < -linthresh: - rhs = min(vmax, -linthresh) - numdec += math.log(vmin / rhs) / math.log(b) - if vmax > linthresh: - lhs = max(vmin, linthresh) - numdec += math.log(vmax / lhs) / math.log(b) + if self._symlog: + numdec = self._symlogutil.dec(vmax) - self._symlogutil.dec(vmin) else: vmin = math.log(vmin) / math.log(b) vmax = math.log(vmax) / math.log(b) @@ -953,6 +1042,8 @@ def set_locs(self, locs=None): if numdec > self.minor_thresholds[0]: # Label only bases self._sublabels = {1} + if self._symlog: + self._firstsublabels = {0} elif numdec > self.minor_thresholds[1]: # Add labels between bases at log-spaced coefficients; # include base powers in case the locations include @@ -960,9 +1051,25 @@ def set_locs(self, locs=None): c = np.geomspace(1, b, int(b)//2 + 1) self._sublabels = set(np.round(c)) # For base 10, this yields (1, 2, 3, 4, 6, 10). + if self._symlog: + # For the linear part of the scale we use an analog selection. + c = np.linspace(2, b, int(b) // 2) + self._firstsublabels = set(np.round(c)) + # For base 10, this yields (0, 2, 4, 6, 8, 10). else: # Label all integer multiples of base**n. self._sublabels = set(np.arange(1, b + 1)) + if self._symlog: + self._firstsublabels = set(np.arange(0, b + 1)) + + if self._symlog: + _, firstpow = self._symlogutil.firstdec() + if self._firstsublabels == {0} and -firstpow < vmin < vmax < firstpow: + # No minor ticks are being labeled right now and the only major tick is + # at 0. This means the axis scaling cannot be read from the labels. + numsteps = int(np.ceil(firstpow / max(-vmin, vmax))) + step = int(b / numsteps) + self._firstsublabels = set(range(0, int(b) + 1, step)) def _num_to_string(self, x, vmin, vmax): if x > 10000: @@ -978,6 +1085,7 @@ def __call__(self, x, pos=None): if x == 0.0: # Symlog return '0' + sign = np.sign(x) x = abs(x) b = self._base # only label the decades @@ -986,14 +1094,21 @@ def __call__(self, x, pos=None): exponent = round(fx) if is_x_decade else np.floor(fx) coeff = round(b ** (fx - exponent)) - if self.labelOnlyBase and not is_x_decade: - return '' - if self._sublabels is not None and coeff not in self._sublabels: - return '' + _, firstpow = self._symlogutil.firstdec() if self._symlog else None, 0 + if x < firstpow: + if self.labelOnlyBase: + return '' + if self._firstsublabels is not None and coeff not in self._firstsublabels: + return '' + else: + if self.labelOnlyBase and not is_x_decade: + return '' + if self._sublabels is not None and coeff not in self._sublabels: + return '' vmin, vmax = self.axis.get_view_interval() vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05) - s = self._num_to_string(x, vmin, vmax) + s = self._num_to_string(sign * x, vmin, vmax) return self.fix_minus(s) def format_data(self, value): @@ -1067,10 +1182,17 @@ def __call__(self, x, pos=None): exponent = round(fx) if is_x_decade else np.floor(fx) coeff = round(b ** (fx - exponent)) - if self.labelOnlyBase and not is_x_decade: - return '' - if self._sublabels is not None and coeff not in self._sublabels: - return '' + _, firstpow = self._symlogutil.firstdec() if self._symlog else (None, 0) + if x < firstpow: + if self.labelOnlyBase: + return '' + if self._firstsublabels is not None and coeff not in self._firstsublabels: + return '' + else: + if self.labelOnlyBase and not is_x_decade: + return '' + if self._sublabels is not None and coeff not in self._sublabels: + return '' if is_x_decade: fx = round(fx) @@ -2246,7 +2368,6 @@ def _is_close_to_int(x): class LogLocator(Locator): """ - Determine the tick locations for log axes. Place ticks on the locations : ``subs[j] * base**i`` @@ -2272,7 +2393,6 @@ class LogLocator(Locator): of ``None`` will try to choose intelligently as long as this Locator has already been assigned to an axis using `~.axis.Axis.get_tick_space`, but otherwise falls back to 9. - """ @_api.delete_parameter("3.8", "numdecs") @@ -2445,155 +2565,189 @@ def nonsingular(self, vmin, vmax): class SymmetricalLogLocator(Locator): """ Determine the tick locations for symmetric log axes. + + Place ticks on the locations : ``subs[j] * base**i`` + + Parameters + ---------- + transform : `~.scale.SymmetricalLogTransform`, optional + If set, defines *base*, *linthresh* and *linscale* of the symlog transform. + subs : None or {'auto', 'all'} or sequence of float, default: None + Gives the multiples of integer powers of the base at which + to place ticks. The default of ``None`` is equivalent to ``(1.0, )``, + i.e. it places ticks only at integer powers of the base. + Permitted string values are ``'auto'`` and ``'all'``. + Both of these use an algorithm based on the axis view + limits to determine whether and how to put ticks between + integer powers of the base. With ``'auto'``, ticks are + placed only between integer powers; with ``'all'``, the + integer powers are included. + numticks : None or int, default: None + The maximum number of ticks to allow on a given axis. The default + of ``None`` will try to choose intelligently as long as this + Locator has already been assigned to an axis using + `~.axis.Axis.get_tick_space`, but otherwise falls back to 9. + base, linthresh, linscale : float, optional + The *base*, *linthresh* and *linscale* of the symlog transform, as + documented for `.SymmetricalLogScale`. These parameters are only used + if *transform* is not set. """ - def __init__(self, transform=None, subs=None, linthresh=None, base=None): - """ - Parameters - ---------- - transform : `~.scale.SymmetricalLogTransform`, optional - If set, defines the *base* and *linthresh* of the symlog transform. - base, linthresh : float, optional - The *base* and *linthresh* of the symlog transform, as documented - for `.SymmetricalLogScale`. These parameters are only used if - *transform* is not set. - subs : sequence of float, default: [1] - The multiples of integer powers of the base where ticks are placed, - i.e., ticks are placed at - ``[sub * base**i for i in ... for sub in subs]``. - - Notes - ----- - Either *transform*, or both *base* and *linthresh*, must be given. - """ - if transform is not None: - self._base = transform.base - self._linthresh = transform.linthresh - elif linthresh is not None and base is not None: - self._base = base - self._linthresh = linthresh - else: - raise ValueError("Either transform, or both linthresh " - "and base, must be provided.") - if subs is None: - self._subs = [1.0] - else: - self._subs = subs - self.numticks = 15 + def __init__(self, transform=None, subs=None, numticks=None, + base=None, linthresh=None, linscale=None): + """Place ticks on the locations : subs[j] * base**i.""" + self._symlogutil = _SymmetricalLogUtil(transform, base, linthresh, linscale) + self._set_subs(subs) + if numticks is None: + if mpl.rcParams['_internal.classic_mode']: + numticks = 15 + else: + numticks = 'auto' + self.numticks = numticks - def set_params(self, subs=None, numticks=None): + def set_params(self, subs=None, numticks=None, + base=None, linthresh=None, linscale=None): """Set parameters within this locator.""" + if subs is not None: + self._set_subs(subs) if numticks is not None: self.numticks = numticks - if subs is not None: + if base is not None: + self._symlogutil.base = float(base) + if linthresh is not None: + self._symlogutil.linthresh = float(linthresh) + if linscale is not None: + self._symlogutil.linscale = float(linscale) + + def _set_subs(self, subs): + """ + Set the minor ticks for the log scaling every ``base**i*subs[j]``. + """ + if subs is None: + self._subs = np.array([1.0]) + elif isinstance(subs, str): + _api.check_in_list(('all', 'auto'), subs=subs) self._subs = subs + else: + try: + self._subs = np.asarray(subs, dtype=float) + except ValueError as e: + raise ValueError("subs must be None, 'all', 'auto' or " + "a sequence of floats, not " + f"{subs}.") from e + if self._subs.ndim != 1: + raise ValueError("A sequence passed to subs must be " + "1-dimensional, not " + f"{self._subs.ndim}-dimensional.") def __call__(self): """Return the locations of the ticks.""" - # Note, these are untransformed coordinates vmin, vmax = self.axis.get_view_interval() return self.tick_values(vmin, vmax) def tick_values(self, vmin, vmax): - linthresh = self._linthresh + if self.numticks == 'auto': + if self.axis is not None: + numticks = np.clip(self.axis.get_tick_space(), 2, 9) + else: + numticks = 9 + else: + numticks = self.numticks + _log.debug('vmin %s vmax %s', vmin, vmax) if vmax < vmin: vmin, vmax = vmax, vmin - # The domain is divided into three sections, only some of - # which may actually be present. - # - # <======== -t ==0== t ========> - # aaaaaaaaa bbbbb ccccccccc - # - # a) and c) will have ticks at integral log positions. The - # number of ticks needs to be reduced if there are more - # than self.numticks of them. - # - # b) has a tick at 0 and only 0 (we assume t is a small - # number, and the linear segment is just an implementation - # detail and not interesting.) - # - # We could also add ticks at t, but that seems to usually be - # uninteresting. - # - # "simple" mode is when the range falls entirely within [-t, t] - # -- it should just display (vmin, 0, vmax) - if -linthresh <= vmin < vmax <= linthresh: - # only the linear range is present - return sorted({vmin, 0, vmax}) - - # Lower log range is present - has_a = (vmin < -linthresh) - # Upper log range is present - has_c = (vmax > linthresh) - - # Check if linear range is present - has_b = (has_a and vmax > -linthresh) or (has_c and vmin < linthresh) - - base = self._base - - def get_log_range(lo, hi): - lo = np.floor(np.log(lo) / np.log(base)) - hi = np.ceil(np.log(hi) / np.log(base)) - return lo, hi - - # Calculate all the ranges, so we can determine striding - a_lo, a_hi = (0, 0) - if has_a: - a_upper_lim = min(-linthresh, vmax) - a_lo, a_hi = get_log_range(abs(a_upper_lim), abs(vmin) + 1) - - c_lo, c_hi = (0, 0) - if has_c: - c_lower_lim = max(linthresh, vmin) - c_lo, c_hi = get_log_range(c_lower_lim, vmax + 1) - - # Calculate the total number of integer exponents in a and c ranges - total_ticks = (a_hi - a_lo) + (c_hi - c_lo) - if has_b: - total_ticks += 1 - stride = max(total_ticks // (self.numticks - 1), 1) - - decades = [] - if has_a: - decades.extend(-1 * (base ** (np.arange(a_lo, a_hi, - stride)[::-1]))) - - if has_b: - decades.append(0.0) - - if has_c: - decades.extend(base ** (np.arange(c_lo, c_hi, stride))) - - subs = np.asarray(self._subs) - - if len(subs) > 1 or subs[0] != 1.0: - ticklocs = [] - for decade in decades: - if decade == 0: - ticklocs.append(decade) + haszero = vmin <= 0 <= vmax + firstdec = np.ceil(self._symlogutil.dec(vmin)) + lastdec = np.floor(self._symlogutil.dec(vmax)) + maxdec = max(abs(firstdec), abs(lastdec)) + # Number of decades completely contained in the range. + numdec = lastdec - firstdec + + # Calculate the subs immediately, as we may return early. + if isinstance(self._subs, str): + # Either 'auto' or 'all'. + if numdec > 10: + # No minor ticks. + if self._subs == 'auto': + # No major ticks either. + return np.array([]) else: - ticklocs.extend(subs * decade) + subs = np.array([1.0]) + else: + _first = 2.0 if self._subs == 'auto' else 1.0 + subs = np.arange(_first, self._symlogutil.base) + else: + subs = self._subs + + # Get decades between major ticks. + stride = (max(math.ceil(numdec / (numticks - 1)), 1) + if mpl.rcParams['_internal.classic_mode'] + else numdec // numticks + 1) + # Avoid axes with a single tick. + if haszero: + # Zero always gets a major tick. + if stride > maxdec: + stride = max(1, maxdec - 1) + else: + if stride >= numdec: + stride = max(1, numdec - 1) + # Determine the major ticks. + if haszero: + # Make sure 0 is ticked. + decades = np.concatenate( + (np.flip(-np.arange(stride, -firstdec + 2 * stride, stride)), + np.arange(0, lastdec + 2 * stride, stride)) + ) + else: + decades = np.arange(firstdec - stride, lastdec + 2 * stride, stride) + + # Does subs include anything other than 1? Essentially a hack to know + # whether we're a major or a minor locator. + if len(subs) > 1 or (len(subs) == 1 and subs[0] != 1.0): + # Minor locator. + if stride == 1: + ticklocs = [] + for dec in decades: + if dec > 0: + ticklocs.append(subs * self._symlogutil.undec(dec)) + elif dec < 0: + ticklocs.append(np.flip(subs * self._symlogutil.undec(dec))) + else: + # We add the usual subs as well as the next lower decade. + zeropow = self._symlogutil.undec(1) / self._symlogutil.base + zeroticks = subs * zeropow + if subs[0] != 1.0: + zeroticks = np.concatenate(([zeropow], zeroticks)) + ticklocs.append(np.flip(-zeroticks)) + ticklocs.append([0.0]) + ticklocs.append(zeroticks) + ticklocs = np.concatenate(ticklocs) + else: + ticklocs = np.array([]) else: - ticklocs = decades + # Major locator. + ticklocs = np.array([self._symlogutil.undec(dec) for dec in decades]) - return self.raise_if_exceeds(np.array(ticklocs)) + _log.debug('ticklocs %r', ticklocs) + if (len(subs) > 1 + and stride == 1 + and ((vmin <= ticklocs) & (ticklocs <= vmax)).sum() <= 1): + # If we're a minor locator *that expects at least two ticks per + # decade* and the major locator stride is 1 and there's no more + # than one minor tick, switch to AutoLocator. + return AutoLocator().tick_values(vmin, vmax) + else: + return self.raise_if_exceeds(ticklocs) def view_limits(self, vmin, vmax): """Try to choose the view limits intelligently.""" - b = self._base - if vmax < vmin: - vmin, vmax = vmax, vmin - + vmin, vmax = self.nonsingular(vmin, vmax) if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers': - vmin = _decade_less_equal(vmin, b) - vmax = _decade_greater_equal(vmax, b) - if vmin == vmax: - vmin = _decade_less(vmin, b) - vmax = _decade_greater(vmax, b) - - return mtransforms.nonsingular(vmin, vmax) + vmin = self._symlogutil.undec(np.floor(self._symlogutil.dec(vmin))) + vmax = self._symlogutil.undec(np.ceil(self._symlogutil.dec(vmax))) + return vmin, vmax class AsinhLocator(Locator): diff --git a/lib/matplotlib/ticker.pyi b/lib/matplotlib/ticker.pyi index f026b4943c94..4056a5a728d8 100644 --- a/lib/matplotlib/ticker.pyi +++ b/lib/matplotlib/ticker.pyi @@ -89,6 +89,20 @@ class ScalarFormatter(Formatter): def format_data_short(self, value: float | np.ma.MaskedArray) -> str: ... def format_data(self, value: float) -> str: ... +class _SymmetricalLogUtil: + def __init__( + self, + transform: Transform | None = ..., + base: float | None = ..., + linthresh: float | None = ..., + linscale: float | None = ..., + ) -> None: ... + def pos(self, val: float) -> float: ... + def unpos(self, val: float) -> float: ... + def firstdec(self) -> tuple[float, float]: ... + def dec(self, val: float) -> float: ... + def undec(self, val: float) -> float: ... + class LogFormatter(Formatter): minor_thresholds: tuple[float, float] def __init__( @@ -97,6 +111,7 @@ class LogFormatter(Formatter): labelOnlyBase: bool = ..., minor_thresholds: tuple[float, float] | None = ..., linthresh: float | None = ..., + linscale: float | None = ..., ) -> None: ... def set_base(self, base: float) -> None: ... labelOnlyBase: bool @@ -253,12 +268,19 @@ class SymmetricalLogLocator(Locator): def __init__( self, transform: Transform | None = ..., - subs: Sequence[float] | None = ..., - linthresh: float | None = ..., + subs: Sequence[float] | Literal["auto", "all"] | None = ..., + numticks: float | None = ..., base: float | None = ..., + linthresh: float | None = ..., + linscale: float | None = ..., ) -> None: ... def set_params( - self, subs: Sequence[float] | None = ..., numticks: int | None = ... + self, + subs: Sequence[float] | None = ..., + numticks: int | None = ..., + base: float | None = ..., + linthresh: float | None = ..., + linscale : float | None = ... ) -> None: ... class AsinhLocator(Locator): pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy