Skip to content

Commit ed204cd

Browse files
committed
Move symlog helper functions to utility class
We will use it in the formatter, too.
1 parent 55b45cc commit ed204cd

File tree

1 file changed

+100
-82
lines changed

1 file changed

+100
-82
lines changed

lib/matplotlib/ticker.py

Lines changed: 100 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,92 @@ def _set_format(self):
812812
self.format = r'$\mathdefault{%s}$' % self.format
813813

814814

815+
class _SymmetricalLogUtil:
816+
"""
817+
Helper class for working with symmetrical log scales.
818+
819+
Parameters
820+
----------
821+
transform : `~.scale.SymmetricalLogTransform`, optional
822+
If set, defines *base*, *linthresh* and *linscale* of the symlog transform.
823+
base, linthresh, linscale : float, optional
824+
The *base*, *linthresh* and *linscale* of the symlog transform, as
825+
documented for `.SymmetricalLogScale`. These parameters are only used
826+
if *transform* is not set.
827+
"""
828+
829+
def __init__(self, transform=None, base=None, linthresh=None, linscale=None):
830+
if transform is not None:
831+
self.base = transform.base
832+
self.linthresh = transform.linthresh
833+
self.linscale = transform.linscale
834+
elif base is not None and linthresh is not None and linscale is not None:
835+
self.base = base
836+
self.linthresh = linthresh
837+
self.linscale = linscale
838+
else:
839+
raise ValueError("Either transform, or all of base, linthresh and "
840+
"linscale must be provided.")
841+
842+
def pos(self, val):
843+
"""
844+
Calculate the normalized position of the value on the axis.
845+
It is normalized such that the distance between two logarithmic decades
846+
is 1 and the position of linthresh is linscale.
847+
"""
848+
sign, val = np.sign(val), np.abs(val) / self.linthresh
849+
if val > 1:
850+
val = self.linscale + np.log(val) / np.log(self.base)
851+
else:
852+
val *= self.linscale
853+
return sign * val
854+
855+
def unpos(self, val):
856+
"""The inverse of _pos."""
857+
sign, val = np.sign(val), np.abs(val)
858+
if val > self.linscale:
859+
val = np.power(self.base, val - self.linscale)
860+
else:
861+
val /= self.linscale
862+
return sign * val * self.linthresh
863+
864+
def firstdec(self):
865+
"""
866+
Get the first decade (i.e. first positive major tick candidate).
867+
It shall be at least half the width of a logarithmic decade from the
868+
origin (i.e. its _pos shall be at least 0.5).
869+
"""
870+
firstexp = np.ceil(np.log(self.unpos(0.5)) / np.log(self.base))
871+
firstpow = np.power(self.base, firstexp)
872+
return firstexp, firstpow
873+
874+
def dec(self, val):
875+
"""
876+
Calculate the decade number of the value. The first decade to have a
877+
position (given by _pos) of at least 0.5 is given the number 1, the
878+
value 0 is given the decade number 0.
879+
"""
880+
firstexp, firstpow = self.firstdec()
881+
sign, val = np.sign(val), np.abs(val)
882+
if val > firstpow:
883+
val = np.log(val) / np.log(self.base) - firstexp + 1
884+
else:
885+
# We scale linearly in order to get a monotonous mapping between
886+
# 0 and 1, though the linear nature is arbitrary.
887+
val /= firstpow
888+
return sign * val
889+
890+
def undec(self, val):
891+
"""The inverse of _dec."""
892+
firstexp, firstpow = self.firstdec()
893+
sign, val = np.sign(val), np.abs(val)
894+
if val > 1:
895+
val = np.power(self.base, val - 1 + firstexp)
896+
else:
897+
val *= firstpow
898+
return sign * val
899+
900+
815901
class LogFormatter(Formatter):
816902
"""
817903
Base class for formatting ticks on a log or symlog scale.
@@ -2474,17 +2560,7 @@ class SymmetricalLogLocator(Locator):
24742560
def __init__(self, transform=None, subs=None, numticks=None,
24752561
base=None, linthresh=None, linscale=None):
24762562
"""Place ticks on the locations : subs[j] * base**i."""
2477-
if transform is not None:
2478-
self._base = transform.base
2479-
self._linthresh = transform.linthresh
2480-
self._linscale = transform.linscale
2481-
elif base is not None and linthresh is not None and linscale is not None:
2482-
self._base = base
2483-
self._linthresh = linthresh
2484-
self._linscale = linscale
2485-
else:
2486-
raise ValueError("Either transform, or all of base, linthresh and "
2487-
"linscale must be provided.")
2563+
self._symlogutil = _SymmetricalLogUtil(transform, base, linthresh, linscale)
24882564
self._set_subs(subs)
24892565
if numticks is None:
24902566
if mpl.rcParams['_internal.classic_mode']:
@@ -2501,11 +2577,11 @@ def set_params(self, subs=None, numticks=None,
25012577
if numticks is not None:
25022578
self.numticks = numticks
25032579
if base is not None:
2504-
self._base = float(base)
2580+
self._symlogutil.base = float(base)
25052581
if linthresh is not None:
2506-
self._linthresh = float(linthresh)
2582+
self._symlogutil.linthresh = float(linthresh)
25072583
if linscale is not None:
2508-
self._linscale = float(linscale)
2584+
self._symlogutil.linscale = float(linscale)
25092585

25102586
def _set_subs(self, subs):
25112587
"""
@@ -2547,8 +2623,8 @@ def tick_values(self, vmin, vmax):
25472623
vmin, vmax = vmax, vmin
25482624

25492625
haszero = vmin <= 0 <= vmax
2550-
firstdec = np.ceil(self._dec(vmin))
2551-
lastdec = np.floor(self._dec(vmax))
2626+
firstdec = np.ceil(self._symlogutil.dec(vmin))
2627+
lastdec = np.floor(self._symlogutil.dec(vmax))
25522628
maxdec = max(abs(firstdec), abs(lastdec))
25532629
# Number of decades completely contained in the range.
25542630
numdec = lastdec - firstdec
@@ -2565,7 +2641,7 @@ def tick_values(self, vmin, vmax):
25652641
subs = np.array([1.0])
25662642
else:
25672643
_first = 2.0 if self._subs == 'auto' else 1.0
2568-
subs = np.arange(_first, self._base)
2644+
subs = np.arange(_first, self._symlogutil.base)
25692645
else:
25702646
subs = self._subs
25712647

@@ -2599,16 +2675,16 @@ def tick_values(self, vmin, vmax):
25992675
ticklocs = []
26002676
for dec in decades:
26012677
if dec > 0:
2602-
ticklocs.append(subs * self._undec(dec))
2678+
ticklocs.append(subs * self._symlogutil.undec(dec))
26032679
elif dec < 0:
2604-
ticklocs.append(np.flip(subs * self._undec(dec)))
2680+
ticklocs.append(np.flip(subs * self._symlogutil.undec(dec)))
26052681
else:
2606-
if self._linscale < 0.5:
2682+
if self._symlogutil.linscale < 0.5:
26072683
# Don't add minor ticks around 0, it's too camped.
26082684
zeroticks = np.array([])
26092685
else:
26102686
# We add the usual subs as well as the next lower decade.
2611-
zeropow = self._undec(1) / self._base
2687+
zeropow = self._symlogutil.undec(1) / self._symlogutil.base
26122688
zeroticks = subs * zeropow
26132689
if subs[0] != 1.0:
26142690
zeroticks = np.concatenate(([zeropow], zeroticks))
@@ -2620,7 +2696,7 @@ def tick_values(self, vmin, vmax):
26202696
ticklocs = np.array([])
26212697
else:
26222698
# Major locator.
2623-
ticklocs = np.array([self._undec(dec) for dec in decades])
2699+
ticklocs = np.array([self._symlogutil.undec(dec) for dec in decades])
26242700

26252701
_log.debug('ticklocs %r', ticklocs)
26262702
if (len(subs) > 1
@@ -2633,70 +2709,12 @@ def tick_values(self, vmin, vmax):
26332709
else:
26342710
return self.raise_if_exceeds(ticklocs)
26352711

2636-
def _pos(self, val):
2637-
"""
2638-
Calculate the normalized position of the value on the axis.
2639-
It is normalized such that the distance between two logarithmic decades
2640-
is 1 and the position of linthresh is linscale.
2641-
"""
2642-
sign, val = np.sign(val), np.abs(val) / self._linthresh
2643-
if val > 1:
2644-
val = self._linscale + np.log(val) / np.log(self._base)
2645-
else:
2646-
val *= self._linscale
2647-
return sign * val
2648-
2649-
def _unpos(self, val):
2650-
"""The inverse of _pos."""
2651-
sign, val = np.sign(val), np.abs(val)
2652-
if val > self._linscale:
2653-
val = np.power(self._base, val - self._linscale)
2654-
else:
2655-
val /= self._linscale
2656-
return sign * val * self._linthresh
2657-
2658-
def _firstdec(self):
2659-
"""
2660-
Get the first decade (i.e. first positive major tick candidate).
2661-
It shall be at least half the width of a logarithmic decade from the
2662-
origin (i.e. its _pos shall be at least 0.5).
2663-
"""
2664-
firstexp = np.ceil(np.log(self._unpos(0.5)) / np.log(self._base))
2665-
firstpow = np.power(self._base, firstexp)
2666-
return firstexp, firstpow
2667-
2668-
def _dec(self, val):
2669-
"""
2670-
Calculate the decade number of the value. The first decade to have a
2671-
position (given by _pos) of at least 0.5 is given the number 1, the
2672-
value 0 is given the decade number 0.
2673-
"""
2674-
firstexp, firstpow = self._firstdec()
2675-
sign, val = np.sign(val), np.abs(val)
2676-
if val > firstpow:
2677-
val = np.log(val) / np.log(self._base) - firstexp + 1
2678-
else:
2679-
# We scale linearly in order to get a monotonous mapping between
2680-
# 0 and 1, though the linear nature is arbitrary.
2681-
val /= firstpow
2682-
return sign * val
2683-
2684-
def _undec(self, val):
2685-
"""The inverse of _dec."""
2686-
firstexp, firstpow = self._firstdec()
2687-
sign, val = np.sign(val), np.abs(val)
2688-
if val > 1:
2689-
val = np.power(self._base, val - 1 + firstexp)
2690-
else:
2691-
val *= firstpow
2692-
return sign * val
2693-
26942712
def view_limits(self, vmin, vmax):
26952713
"""Try to choose the view limits intelligently."""
26962714
vmin, vmax = self.nonsingular(vmin, vmax)
26972715
if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers':
2698-
vmin = self._undec(np.floor(self._dec(vmin)))
2699-
vmax = self._undec(np.ceil(self._dec(vmax)))
2716+
vmin = self._symlogutil.undec(np.floor(self._symlogutil.dec(vmin)))
2717+
vmax = self._symlogutil.undec(np.ceil(self._symlogutil.dec(vmax)))
27002718
return vmin, vmax
27012719

27022720
class AsinhLocator(Locator):

0 commit comments

Comments
 (0)