Skip to content

Commit 5847a84

Browse files
committed
logitscale: rewrite of LogitLocator
1 parent e736500 commit 5847a84

File tree

1 file changed

+96
-44
lines changed

1 file changed

+96
-44
lines changed

lib/matplotlib/ticker.py

Lines changed: 96 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2502,64 +2502,116 @@ def view_limits(self, vmin, vmax):
25022502
return result
25032503

25042504

2505-
class LogitLocator(Locator):
2505+
class LogitLocator(MaxNLocator):
25062506
"""
25072507
Determine the tick locations for logit axes
25082508
"""
25092509

2510-
def __init__(self, minor=False):
2511-
"""Place ticks on the logit locations."""
2512-
self.minor = minor
2510+
def __init__(self, minor=False, *, nbins="auto"):
2511+
"""
2512+
Place ticks on the logit locations
2513+
2514+
Parameters
2515+
----------
2516+
nbins : int or 'auto', optional
2517+
Number of ticks. Only used if minor is False.
2518+
minor : bool, default: False
2519+
Indicate if this locator is for minor ticks or not.
2520+
"""
2521+
2522+
self._minor = minor
2523+
MaxNLocator.__init__(self, nbins=nbins, steps=[1, 2, 5, 10])
25132524

2514-
def set_params(self, minor=None):
2525+
def set_params(self, minor=None, **kwargs):
25152526
"""Set parameters within this locator."""
25162527
if minor is not None:
2517-
self.minor = minor
2528+
self._minor = minor
2529+
MaxNLocator.set_params(self, **kwargs)
25182530

2519-
def __call__(self):
2520-
"""Return the locations of the ticks."""
2521-
vmin, vmax = self.axis.get_view_interval()
2522-
return self.tick_values(vmin, vmax)
2531+
@property
2532+
def minor(self):
2533+
return self._minor
2534+
2535+
@minor.setter
2536+
def minor(self, value):
2537+
self.set_params(minor=value)
25232538

25242539
def tick_values(self, vmin, vmax):
25252540
# dummy axis has no axes attribute
2526-
if hasattr(self.axis, 'axes') and self.axis.axes.name == 'polar':
2527-
raise NotImplementedError('Polar axis cannot be logit scaled yet')
2541+
if hasattr(self.axis, "axes") and self.axis.axes.name == "polar":
2542+
raise NotImplementedError("Polar axis cannot be logit scaled yet")
25282543

2529-
vmin, vmax = self.nonsingular(vmin, vmax)
2530-
vmin = np.log10(vmin / (1 - vmin))
2531-
vmax = np.log10(vmax / (1 - vmax))
2532-
2533-
decade_min = np.floor(vmin)
2534-
decade_max = np.ceil(vmax)
2535-
2536-
# major ticks
2537-
if not self.minor:
2538-
ticklocs = []
2539-
if decade_min <= -1:
2540-
expo = np.arange(decade_min, min(0, decade_max + 1))
2541-
ticklocs.extend(10**expo)
2542-
if decade_min <= 0 <= decade_max:
2543-
ticklocs.append(0.5)
2544-
if decade_max >= 1:
2545-
expo = -np.arange(max(1, decade_min), decade_max + 1)
2546-
ticklocs.extend(1 - 10**expo)
2547-
2548-
# minor ticks
2544+
if self._nbins == "auto":
2545+
if self.axis is not None:
2546+
nbins = self.axis.get_tick_space()
2547+
if nbins < 2:
2548+
nbins = 2
2549+
else:
2550+
nbins = 9
25492551
else:
2550-
ticklocs = []
2551-
if decade_min <= -2:
2552-
expo = np.arange(decade_min, min(-1, decade_max))
2553-
newticks = np.outer(np.arange(2, 10), 10**expo).ravel()
2554-
ticklocs.extend(newticks)
2555-
if decade_min <= 0 <= decade_max:
2556-
ticklocs.extend([0.2, 0.3, 0.4, 0.6, 0.7, 0.8])
2557-
if decade_max >= 2:
2558-
expo = -np.arange(max(2, decade_min), decade_max + 1)
2559-
newticks = 1 - np.outer(np.arange(2, 10), 10**expo).ravel()
2560-
ticklocs.extend(newticks)
2552+
nbins = self._nbins
25612553

2562-
return self.raise_if_exceeds(np.array(ticklocs))
2554+
# We define ideal ticks with their index:
2555+
# linscale: ... 1e-3 1e-2 1e-1 1/2 1-1e-1 1-1e-2 1-1e-3 ...
2556+
# b-scale : ... -3 -2 -1 0 1 2 3 ...
2557+
def ideal_ticks(x):
2558+
return 10 ** x if x < 0 else 1 - (10 ** (-x)) if x > 0 else 1 / 2
2559+
2560+
vmin, vmax = self.nonsingular(vmin, vmax)
2561+
binf = int(
2562+
np.floor(np.log10(vmin))
2563+
if vmin < 0.5
2564+
else 0
2565+
if vmin < 0.9
2566+
else -np.ceil(np.log10(1 - vmin))
2567+
)
2568+
bsup = int(
2569+
np.ceil(np.log10(vmax))
2570+
if vmax <= 0.5
2571+
else 1
2572+
if vmax <= 0.9
2573+
else -np.floor(np.log10(1 - vmax))
2574+
)
2575+
numideal = bsup - binf - 1
2576+
if numideal >= 2:
2577+
# have 2 or more wanted ideal ticks, so use them as major ticks
2578+
if numideal > nbins:
2579+
# to many ideal ticks, subsampling ideals for major ticks, and
2580+
# take others for minor ticks
2581+
subsampling_factor = math.ceil(numideal / nbins)
2582+
if self._minor:
2583+
ticklocs = [
2584+
ideal_ticks(b)
2585+
for b in range(binf, bsup + 1)
2586+
if (b % subsampling_factor) != 0
2587+
]
2588+
else:
2589+
ticklocs = [
2590+
ideal_ticks(b)
2591+
for b in range(binf, bsup + 1)
2592+
if (b % subsampling_factor) == 0
2593+
]
2594+
return self.raise_if_exceeds(np.array(ticklocs))
2595+
if self._minor:
2596+
ticklocs = []
2597+
for b in range(binf, bsup):
2598+
if b < -1:
2599+
ticklocs.extend(np.arange(2, 10) * 10 ** b)
2600+
elif b == -1:
2601+
ticklocs.extend(np.arange(2, 5) / 10)
2602+
elif b == 0:
2603+
ticklocs.extend(np.arange(6, 9) / 10)
2604+
else:
2605+
ticklocs.extend(
2606+
1 - np.arange(2, 10)[::-1] * 10 ** (-b - 1)
2607+
)
2608+
return self.raise_if_exceeds(np.array(ticklocs))
2609+
ticklocs = [ideal_ticks(b) for b in range(binf, bsup + 1)]
2610+
return self.raise_if_exceeds(np.array(ticklocs))
2611+
# the scale is zoomed so same ticks as linear scale can be used
2612+
if self._minor:
2613+
return []
2614+
return MaxNLocator.tick_values(self, vmin, vmax)
25632615

25642616
def nonsingular(self, vmin, vmax):
25652617
initial_range = (1e-7, 1 - 1e-7)

0 commit comments

Comments
 (0)