Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5717116

Browse files
committed
Rewrite SymmetricalLogLocator
The new locator is based on LogLocator in such a way that it should give identical results if the data is only in a logarithmic part of the axis. It is extended to the linear part in a (hopefully) reasonable manner.
1 parent f73d048 commit 5717116

File tree

1 file changed

+225
-119
lines changed

1 file changed

+225
-119
lines changed

lib/matplotlib/ticker.py

Lines changed: 225 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -2444,157 +2444,263 @@ def nonsingular(self, vmin, vmax):
24442444

24452445
class SymmetricalLogLocator(Locator):
24462446
"""
2447+
24472448
Determine the tick locations for symmetric log axes.
2449+
2450+
Place ticks on the locations : ``subs[j] * base**i``
2451+
2452+
Parameters
2453+
----------
2454+
transform : `~.scale.SymmetricalLogTransform`, optional
2455+
If set, defines *base*, *linthresh* and *linscale* of the symlog transform.
2456+
subs : None or {'auto', 'all'} or sequence of float, default: None
2457+
Gives the multiples of integer powers of the base at which
2458+
to place ticks. The default of ``None`` is equivalent to ``(1.0, )``,
2459+
i.e. it places ticks only at integer powers of the base.
2460+
Permitted string values are ``'auto'`` and ``'all'``.
2461+
Both of these use an algorithm based on the axis view
2462+
limits to determine whether and how to put ticks between
2463+
integer powers of the base. With ``'auto'``, ticks are
2464+
placed only between integer powers; with ``'all'``, the
2465+
integer powers are included.
2466+
numticks : None or int, default: None
2467+
The maximum number of ticks to allow on a given axis. The default
2468+
of ``None`` will try to choose intelligently as long as this
2469+
Locator has already been assigned to an axis using
2470+
`~.axis.Axis.get_tick_space`, but otherwise falls back to 9.
2471+
base, linthresh, linscale : float, optional
2472+
The *base*, *linthresh* and *linscale* of the symlog transform, as
2473+
documented for `.SymmetricalLogScale`. These parameters are only used
2474+
if *transform* is not set.
2475+
24482476
"""
24492477

2450-
def __init__(self, transform=None, subs=None, linthresh=None, base=None):
2451-
"""
2452-
Parameters
2453-
----------
2454-
transform : `~.scale.SymmetricalLogTransform`, optional
2455-
If set, defines the *base* and *linthresh* of the symlog transform.
2456-
base, linthresh : float, optional
2457-
The *base* and *linthresh* of the symlog transform, as documented
2458-
for `.SymmetricalLogScale`. These parameters are only used if
2459-
*transform* is not set.
2460-
subs : sequence of float, default: [1]
2461-
The multiples of integer powers of the base where ticks are placed,
2462-
i.e., ticks are placed at
2463-
``[sub * base**i for i in ... for sub in subs]``.
2464-
2465-
Notes
2466-
-----
2467-
Either *transform*, or both *base* and *linthresh*, must be given.
2468-
"""
2478+
def __init__(self, transform=None, subs=None, numticks=None,
2479+
base=None, linthresh=None, linscale=None):
2480+
"""Place ticks on the locations : subs[j] * base**i."""
24692481
if transform is not None:
24702482
self._base = transform.base
24712483
self._linthresh = transform.linthresh
2472-
elif linthresh is not None and base is not None:
2484+
self._linscale = transform.linscale
2485+
elif base is not None and linthresh is not None and linscale is not None:
24732486
self._base = base
24742487
self._linthresh = linthresh
2488+
self._linscale = linscale
24752489
else:
2476-
raise ValueError("Either transform, or both linthresh "
2477-
"and base, must be provided.")
2478-
if subs is None:
2479-
self._subs = [1.0]
2480-
else:
2481-
self._subs = subs
2482-
self.numticks = 15
2490+
raise ValueError("Either transform, or all of base, linthresh and "
2491+
"linscale must be provided.")
2492+
self._set_subs(subs)
2493+
if numticks is None:
2494+
if mpl.rcParams['_internal.classic_mode']:
2495+
numticks = 15
2496+
else:
2497+
numticks = 'auto'
24832498

2484-
def set_params(self, subs=None, numticks=None):
2499+
def set_params(self, subs=None, numticks=None,
2500+
base=None, linthresh=None, linscale=None):
24852501
"""Set parameters within this locator."""
2502+
if subs is not None:
2503+
self._set_subs(subs)
24862504
if numticks is not None:
24872505
self.numticks = numticks
2488-
if subs is not None:
2506+
if base is not None:
2507+
self._base = float(base)
2508+
if linthresh is not None:
2509+
self._linthresh = float(linthresh)
2510+
if linscale is not None:
2511+
self._linscale = float(linscale)
2512+
2513+
def _set_subs(self, subs):
2514+
"""
2515+
Set the minor ticks for the log scaling every ``base**i*subs[j]``.
2516+
"""
2517+
if subs is None: # consistency with previous bad API
2518+
self._subs = np.array([1.0])
2519+
elif isinstance(subs, str):
2520+
_api.check_in_list(('all', 'auto'), subs=subs)
24892521
self._subs = subs
2522+
else:
2523+
try:
2524+
self._subs = np.asarray(subs, dtype=float)
2525+
except ValueError as e:
2526+
raise ValueError("subs must be None, 'all', 'auto' or "
2527+
"a sequence of floats, not "
2528+
f"{subs}.") from e
2529+
if self._subs.ndim != 1:
2530+
raise ValueError("A sequence passed to subs must be "
2531+
"1-dimensional, not "
2532+
f"{self._subs.ndim}-dimensional.")
24902533

24912534
def __call__(self):
24922535
"""Return the locations of the ticks."""
2493-
# Note, these are untransformed coordinates
24942536
vmin, vmax = self.axis.get_view_interval()
24952537
return self.tick_values(vmin, vmax)
24962538

24972539
def tick_values(self, vmin, vmax):
2498-
linthresh = self._linthresh
2540+
if self.numticks == 'auto':
2541+
if self.axis is not None:
2542+
numticks = np.clip(self.axis.get_tick_space(), 2, 9)
2543+
else:
2544+
numticks = 9
2545+
else:
2546+
numticks = self.numticks
24992547

2548+
_log.debug('vmin %s vmax %s', vmin, vmax)
25002549
if vmax < vmin:
25012550
vmin, vmax = vmax, vmin
25022551

2503-
# The domain is divided into three sections, only some of
2504-
# which may actually be present.
2505-
#
2506-
# <======== -t ==0== t ========>
2507-
# aaaaaaaaa bbbbb ccccccccc
2508-
#
2509-
# a) and c) will have ticks at integral log positions. The
2510-
# number of ticks needs to be reduced if there are more
2511-
# than self.numticks of them.
2512-
#
2513-
# b) has a tick at 0 and only 0 (we assume t is a small
2514-
# number, and the linear segment is just an implementation
2515-
# detail and not interesting.)
2516-
#
2517-
# We could also add ticks at t, but that seems to usually be
2518-
# uninteresting.
2519-
#
2520-
# "simple" mode is when the range falls entirely within [-t, t]
2521-
# -- it should just display (vmin, 0, vmax)
2522-
if -linthresh <= vmin < vmax <= linthresh:
2523-
# only the linear range is present
2524-
return sorted({vmin, 0, vmax})
2525-
2526-
# Lower log range is present
2527-
has_a = (vmin < -linthresh)
2528-
# Upper log range is present
2529-
has_c = (vmax > linthresh)
2530-
2531-
# Check if linear range is present
2532-
has_b = (has_a and vmax > -linthresh) or (has_c and vmin < linthresh)
2533-
2534-
base = self._base
2535-
2536-
def get_log_range(lo, hi):
2537-
lo = np.floor(np.log(lo) / np.log(base))
2538-
hi = np.ceil(np.log(hi) / np.log(base))
2539-
return lo, hi
2540-
2541-
# Calculate all the ranges, so we can determine striding
2542-
a_lo, a_hi = (0, 0)
2543-
if has_a:
2544-
a_upper_lim = min(-linthresh, vmax)
2545-
a_lo, a_hi = get_log_range(abs(a_upper_lim), abs(vmin) + 1)
2546-
2547-
c_lo, c_hi = (0, 0)
2548-
if has_c:
2549-
c_lower_lim = max(linthresh, vmin)
2550-
c_lo, c_hi = get_log_range(c_lower_lim, vmax + 1)
2551-
2552-
# Calculate the total number of integer exponents in a and c ranges
2553-
total_ticks = (a_hi - a_lo) + (c_hi - c_lo)
2554-
if has_b:
2555-
total_ticks += 1
2556-
stride = max(total_ticks // (self.numticks - 1), 1)
2557-
2558-
decades = []
2559-
if has_a:
2560-
decades.extend(-1 * (base ** (np.arange(a_lo, a_hi,
2561-
stride)[::-1])))
2562-
2563-
if has_b:
2564-
decades.append(0.0)
2565-
2566-
if has_c:
2567-
decades.extend(base ** (np.arange(c_lo, c_hi, stride)))
2568-
2569-
subs = np.asarray(self._subs)
2570-
2571-
if len(subs) > 1 or subs[0] != 1.0:
2572-
ticklocs = []
2573-
for decade in decades:
2574-
if decade == 0:
2575-
ticklocs.append(decade)
2552+
haszero = vmin <= 0 <= vmax
2553+
firstdec = np.ceil(self._dec(vmin))
2554+
lastdec = np.floor(self._dec(vmax))
2555+
maxdec = max(abs(firstdec), abs(lastdec))
2556+
# Number of decades completely contained in the range.
2557+
numdec = lastdec - firstdec
2558+
2559+
# Calculate the subs immediately, as we may return early.
2560+
if isinstance(self._subs, str):
2561+
# Either 'auto' or 'all'.
2562+
if numdec > 10:
2563+
# No minor ticks.
2564+
if self._subs == 'auto':
2565+
# No major ticks either.
2566+
return np.array([])
25762567
else:
2577-
ticklocs.extend(subs * decade)
2568+
subs = np.array([1.0])
2569+
else:
2570+
_first = 2.0 if self._subs == 'auto' else 1.0
2571+
subs = np.arange(_first, self._base)
25782572
else:
2579-
ticklocs = decades
2573+
subs = self._subs
25802574

2581-
return self.raise_if_exceeds(np.array(ticklocs))
2575+
# Get decades between major ticks.
2576+
stride = (max(math.ceil(numdec / (numticks - 1)), 1)
2577+
if mpl.rcParams['_internal.classic_mode']
2578+
else numdec // numticks + 1)
2579+
# Avoid axes with a single tick.
2580+
if haszero:
2581+
# Zero always gets a major tick.
2582+
if stride > maxdec:
2583+
stride = max(1, maxdec - 1)
2584+
else:
2585+
if stride >= numdec:
2586+
stride = max(1, numdec - 1)
2587+
# Determine the major ticks.
2588+
if haszero:
2589+
# Make sure 0 is ticked.
2590+
decades = np.concatenate(
2591+
(np.flip(np.arange(stride, -firstdec + 2 * stride, stride)),
2592+
np.arange(0, lastdec + 2 * stride, stride))
2593+
)
2594+
else:
2595+
decades = np.arange(firstdec - stride, lastdec + 2 * stride, stride)
25822596

2583-
def view_limits(self, vmin, vmax):
2584-
"""Try to choose the view limits intelligently."""
2585-
b = self._base
2586-
if vmax < vmin:
2587-
vmin, vmax = vmax, vmin
2597+
# Does subs include anything other than 1? Essentially a hack to know
2598+
# whether we're a major or a minor locator.
2599+
if len(subs) > 1 or (len(subs) == 1 and subs[0] != 1.0):
2600+
# Minor locator.
2601+
if stride == 1:
2602+
ticklocs = []
2603+
for dec in decades:
2604+
if dec > 0:
2605+
ticklocs.append(subs * self._undec(dec))
2606+
elif dec < 0:
2607+
ticklocs.append(np.flip(subs * self._undec(dec)))
2608+
else:
2609+
if self._linscale < 0.5:
2610+
# Don't add minor ticks around 0, it's too camped.
2611+
zeroticks = np.array([])
2612+
else:
2613+
# We add the usual subs as well as the next lower decade.
2614+
zeropow = self._undec(1) / self._base
2615+
zeroticks = subs * zeropow
2616+
if subs[0] != 1.0:
2617+
zeroticks = np.concatenate(([zeropow], zeroticks))
2618+
ticklocs.append(np.flip(-zeroticks))
2619+
ticklocs.append([0.0])
2620+
ticklocs.append(zeroticks)
2621+
ticklocs = np.concatenate(ticklocs)
2622+
else:
2623+
ticklocs = np.array([])
2624+
else:
2625+
# Major locator.
2626+
ticklocs = np.power(self._base, decades)
25882627

2589-
if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers':
2590-
vmin = _decade_less_equal(vmin, b)
2591-
vmax = _decade_greater_equal(vmax, b)
2592-
if vmin == vmax:
2593-
vmin = _decade_less(vmin, b)
2594-
vmax = _decade_greater(vmax, b)
2628+
_log.debug('ticklocs %r', ticklocs)
2629+
if (len(subs) > 1
2630+
and stride == 1
2631+
and ((vmin <= ticklocs) & (ticklocs <= vmax)).sum() <= 1):
2632+
# If we're a minor locator *that expects at least two ticks per
2633+
# decade* and the major locator stride is 1 and there's no more
2634+
# than one minor tick, switch to AutoLocator.
2635+
return AutoLocator().tick_values(vmin, vmax)
2636+
else:
2637+
return self.raise_if_exceeds(ticklocs)
25952638

2596-
return mtransforms.nonsingular(vmin, vmax)
2639+
def _pos(self, val):
2640+
"""
2641+
Calculate the normalized position of the value on the axis.
2642+
It is normalized such that the distance between two logarithmic decades
2643+
is 1 and the position of linthresh is linscale.
2644+
"""
2645+
sign, val = np.sign(val), np.abs(val) / self._linthresh
2646+
if val > 1:
2647+
val = self._linscale + np.log(val) / np.log(self._base)
2648+
else:
2649+
val *= self._linscale
2650+
return sign * val
2651+
2652+
def _unpos(self, val):
2653+
"""The inverse of _pos."""
2654+
sign, val = np.sign(val), np.abs(val)
2655+
if val > self._linscale:
2656+
val = np.power(self._base, val - self._linscale)
2657+
else:
2658+
val /= self._linscale
2659+
return sign * val * self._linthresh
2660+
2661+
def _firstdec(self):
2662+
"""
2663+
Get the first decade (i.e. first positive major tick candidate).
2664+
It shall be at least half the width of a logarithmic decade from the
2665+
origin (i.e. its _pos shall be at least 0.5).
2666+
"""
2667+
firstexp = np.ceil(np.log(self._unpos(0.5)) / np.log(self._base))
2668+
firstpow = np.power(self._base, firstexp)
2669+
return firstexp, firstpow
25972670

2671+
def _dec(self, val):
2672+
"""
2673+
Calculate the decade number of the value. The first decade to have a
2674+
position (given by _pos) of at least 0.5 is given the number 1, the
2675+
value 0 is given the decade number 0.
2676+
"""
2677+
firstexp, firstpow = self._firstdec()
2678+
sign, val = np.sign(val), np.abs(val)
2679+
if val > firstpow:
2680+
val = np.log(val) / np.log(self._base) - firstexp + 1
2681+
else:
2682+
# We scale linearly in order to get a monotonous mapping between
2683+
# 0 and 1, though the linear nature is arbitrary.
2684+
val /= firstpow
2685+
return sign * val
2686+
2687+
def _undec(self, val):
2688+
"""The inverse of _dec."""
2689+
firstexp, firstpow = self._firstdec()
2690+
sign, val = np.sign(val), np.abs(val)
2691+
if val > 1:
2692+
val = np.power(self._base, val - 1 + firstexp)
2693+
else:
2694+
val *= firstpow
2695+
return sign * val
2696+
2697+
def view_limits(self, vmin, vmax):
2698+
"""Try to choose the view limits intelligently."""
2699+
vmin, vmax = self.nonsingular(vmin, vmax)
2700+
if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers':
2701+
vmin = self._undec(np.floor(self._dec(vmin)))
2702+
vmax = self._undec(np.ceil(self._dec(vmax)))
2703+
return vmin, vmax
25982704

25992705
class AsinhLocator(Locator):
26002706
"""

0 commit comments

Comments
 (0)