@@ -2444,157 +2444,263 @@ def nonsingular(self, vmin, vmax):
2444
2444
2445
2445
class SymmetricalLogLocator (Locator ):
2446
2446
"""
2447
+
2447
2448
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
+
2448
2476
"""
2449
2477
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."""
2469
2481
if transform is not None :
2470
2482
self ._base = transform .base
2471
2483
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 :
2473
2486
self ._base = base
2474
2487
self ._linthresh = linthresh
2488
+ self ._linscale = linscale
2475
2489
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'
2483
2498
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 ):
2485
2501
"""Set parameters within this locator."""
2502
+ if subs is not None :
2503
+ self ._set_subs (subs )
2486
2504
if numticks is not None :
2487
2505
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 )
2489
2521
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." )
2490
2533
2491
2534
def __call__ (self ):
2492
2535
"""Return the locations of the ticks."""
2493
- # Note, these are untransformed coordinates
2494
2536
vmin , vmax = self .axis .get_view_interval ()
2495
2537
return self .tick_values (vmin , vmax )
2496
2538
2497
2539
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
2499
2547
2548
+ _log .debug ('vmin %s vmax %s' , vmin , vmax )
2500
2549
if vmax < vmin :
2501
2550
vmin , vmax = vmax , vmin
2502
2551
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 ([])
2576
2567
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 )
2578
2572
else :
2579
- ticklocs = decades
2573
+ subs = self . _subs
2580
2574
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 )
2582
2596
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 )
2588
2627
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 )
2595
2638
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
2597
2670
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
2598
2704
2599
2705
class AsinhLocator (Locator ):
2600
2706
"""
0 commit comments