/usr/lib/python3/dist-packages/leather/ticks/score.py is in python3-leather 0.3.3-1.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | #!/usr/bin/env python
from decimal import Decimal, ROUND_CEILING, ROUND_FLOOR
import math
import sys
from leather.ticks.base import Ticker
from leather.utils import isclose
# Shorthand
ZERO = Decimal('0')
TEN = Decimal('10')
#: Normalized intervals to be tested for ticks
INTERVALS = [
Decimal('0.1'),
Decimal('0.15'),
Decimal('0.2'),
Decimal('0.25'),
Decimal('0.5'),
Decimal('1.0')
]
#: The default number of ticks to produce
DEFAULT_TICKS = 5
#: The minimum length of a viable tick sequence
MIN_TICK_COUNT = 4
#: The maximum length of a viable tick sequence
MAX_TICK_COUNT = 10
#: Most preferred tick intervals
BEST_INTERVALS = [Decimal('0.1'), Decimal('1.0')]
#: Least preferred tick intervals
WORST_INTERVALS = [Decimal('0.15')]
class ScoreTicker(Ticker):
"""
Attempt to find an optimal series of ticks by generating many possible
sequences and scoring them based on several criteria. Only the best
tick sequence is returned.
Based an algorithm described by Austin Clemens:
http://austinclemens.com/blog/2016/01/09/an-algorithm-for-creating-a-graphs-axes/
See :meth:`.ScoreTicker.score` for scoring implementation.
:param domain_min:
Minimum value of the data series.
:param domain_max:
Maximum value of the data series.
"""
def __init__(self, domain_min, domain_max):
self._domain_min = domain_min
self._domain_max = domain_max
self._ticks = self._find_ticks()
self._min = self._ticks[0]
self._max = self._ticks[-1]
@property
def ticks(self):
return self._ticks
@property
def min(self):
return self._min
@property
def max(self):
return self._max
def _find_ticks(self):
"""
Implements the tick-finding algorithm.
"""
force_zero = self._domain_min < ZERO and self._domain_max > ZERO
interval_guess = abs(self._domain_max - self._domain_min) / (DEFAULT_TICKS - 1)
magnitude = interval_guess.log10().to_integral_exact(rounding=ROUND_CEILING)
candidate_intervals = []
for interval in INTERVALS:
candidate_intervals.append((interval, interval * pow(TEN, magnitude)))
candidate_intervals.append((interval, interval * pow(TEN, magnitude - 1)))
candidate_intervals.append((interval, interval * pow(TEN, magnitude + 1)))
candidate_ticks = []
for base_interval, interval in candidate_intervals:
ticks = []
if force_zero:
min_steps = (abs(self._domain_min) / interval).to_integral_exact(rounding=ROUND_CEILING)
ticks.append(self._round_tick(-min_steps * interval))
else:
ticks.append(self._round_tick((self._domain_min / interval).to_integral_exact(rounding=ROUND_FLOOR) * interval))
tick_num = 1
while ticks[tick_num - 1] < self._domain_max:
t = self._round_tick(ticks[0] + (interval * tick_num))
ticks.append(t)
tick_num += 1
# Throw out sequences that are too short or too long
if len(ticks) < MIN_TICK_COUNT or len(ticks) > MAX_TICK_COUNT:
continue
candidate_ticks.append({
'base_interval': base_interval,
'interval': interval,
'ticks': ticks,
'score': self._score(base_interval, interval, ticks)
})
# Order by best score, using number of ticks as a tie-breaker
best = sorted(candidate_ticks, key=lambda c: (c['score']['total'], len(c['ticks'])))
return best[0]['ticks']
def _score(self, base_interval, interval, ticks):
"""
Score a given tick sequence based on several criteria. This method returns
discrete scoring components for easier debugging.
"""
s = {
'pct_waste': 0,
'interval_penalty': 0,
'len_penalty': 0,
'total': 0
}
# Penalty for wasted scale space
waste = (self._domain_min - ticks[0]) + (ticks[-1] - self._domain_max)
pct_waste = waste / (self._domain_max - self._domain_min)
s['pct_waste'] = pow(10, pct_waste)
# Penalty for choosing less optimal tick intervals
if base_interval in BEST_INTERVALS:
pass
elif base_interval in WORST_INTERVALS:
s['interval_penalty'] = 2
else:
s['interval_penalty'] = 1
# Penalty for too many ticks
if len(ticks) > 5:
s['len_penalty'] = (len(ticks) - 5)
s['total'] = s['pct_waste'] + s['interval_penalty'] + s['len_penalty']
return s
def _round_tick(self, t):
"""
Round a tick to 0-3 decimal places, if the remaining digits do not
appear to be significant.
"""
for r in range(0, 4):
exp = pow(Decimal(10), Decimal(-r))
quantized = t.quantize(exp)
if isclose(t, quantized):
return quantized
return t
|