Skip to content

Commit 620232e

Browse files
committed
scale: make tick levels useful and explicit
Currently tick level selection is essentially black box. As a result, it's difficult for a caller to affect the optimization in interesting ways. The only way to do this now is to pass a predicate function down into the optimizer, but that's fairly limited. Swap the role of the optimizer and the scale: make the scale API expose ticks at any level, and make the optimizer a (useful) public API that takes a scale and computes a tick level. The existing Ticks method remains, but now callers that need more sophisticated optimization can invoke the optimizer directly. This also eliminates the need for the predicate since the caller can easily tweak the tick level returned by the optimizer according to whatever criteria it has and then pass the updated tick level back into the scale.
1 parent 24ddaf7 commit 620232e

File tree

4 files changed

+128
-132
lines changed

4 files changed

+128
-132
lines changed

scale/linear.go

+31-27
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,34 @@ func (s *Linear) spacingAtLevel(level int, roundOut bool) (firstN, lastN, spacin
105105
return
106106
}
107107

108+
// CountTicks returns the number of ticks in [s.Min, s.Max] at the
109+
// given tick level.
110+
func (s Linear) CountTicks(level int) int {
111+
return linearTicker{&s, false}.CountTicks(level)
112+
}
113+
114+
// TicksAtLevel returns the tick locations in [s.Min, s.Max] as a
115+
// []float64 at the given tick level in ascending order.
116+
func (s Linear) TicksAtLevel(level int) interface{} {
117+
return linearTicker{&s, false}.TicksAtLevel(level)
118+
}
119+
120+
type linearTicker struct {
121+
s *Linear
122+
roundOut bool
123+
}
124+
125+
func (t linearTicker) CountTicks(level int) int {
126+
firstN, lastN, _ := t.s.spacingAtLevel(level, t.roundOut)
127+
return int(lastN - firstN + 1)
128+
}
129+
130+
func (t linearTicker) TicksAtLevel(level int) interface{} {
131+
firstN, lastN, spacing := t.s.spacingAtLevel(level, t.roundOut)
132+
n := int(lastN - firstN + 1)
133+
return vec.Linspace(firstN*spacing, lastN*spacing, n)
134+
}
135+
108136
func (s Linear) Ticks(o TickOptions) (major, minor []float64) {
109137
if o.Max <= 0 {
110138
return nil, nil
@@ -114,24 +142,11 @@ func (s Linear) Ticks(o TickOptions) (major, minor []float64) {
114142
s.Min, s.Max = s.Max, s.Min
115143
}
116144

117-
// nticksAtLevel returns the number of ticks in [s.Min, s.Max]
118-
// at the given level.
119-
nticksAtLevel := func(level int) int {
120-
firstN, lastN, _ := s.spacingAtLevel(level, false)
121-
return int(lastN - firstN + 1)
122-
}
123-
124-
ticksAtLevel := func(level int) []float64 {
125-
firstN, lastN, spacing := s.spacingAtLevel(level, false)
126-
n := int(lastN - firstN + 1)
127-
return vec.Linspace(firstN*spacing, lastN*spacing, n)
128-
}
129-
130-
level, ok := o.FindLevel(nticksAtLevel, ticksAtLevel, s.guessLevel())
145+
level, ok := o.FindLevel(linearTicker{&s, false}, s.guessLevel())
131146
if !ok {
132147
return nil, nil
133148
}
134-
return ticksAtLevel(level), ticksAtLevel(level - 1)
149+
return s.TicksAtLevel(level).([]float64), s.TicksAtLevel(level - 1).([]float64)
135150
}
136151

137152
func (s *Linear) Nice(o TickOptions) {
@@ -142,18 +157,7 @@ func (s *Linear) Nice(o TickOptions) {
142157
s.Min, s.Max = s.Max, s.Min
143158
}
144159

145-
nticksAtLevel := func(level int) int {
146-
firstN, lastN, _ := s.spacingAtLevel(level, true)
147-
return int(lastN - firstN + 1)
148-
}
149-
150-
ticksAtLevel := func(level int) []float64 {
151-
firstN, lastN, spacing := s.spacingAtLevel(level, true)
152-
n := int(lastN - firstN + 1)
153-
return vec.Linspace(firstN*spacing, lastN*spacing, n)
154-
}
155-
156-
level, ok := o.FindLevel(nticksAtLevel, ticksAtLevel, s.guessLevel())
160+
level, ok := o.FindLevel(linearTicker{s, true}, s.guessLevel())
157161
if !ok {
158162
return
159163
}

scale/log.go

+54-48
Original file line numberDiff line numberDiff line change
@@ -130,58 +130,64 @@ func (s *Log) spacingAtLevel(level int, roundOut bool) (firstN, lastN, ebase flo
130130
return
131131
}
132132

133-
func (s *Log) tickFuncs(roundOut bool) (func(level int) int, func(level int) []float64) {
134-
neg, min, max := s.ebounds()
133+
func (s *Log) CountTicks(level int) int {
134+
return logTicker{s, false}.CountTicks(level)
135+
}
135136

136-
// nticksAtLevel returns the number of ticks in [min, max] at
137-
// the given level.
138-
nticksAtLevel := func(level int) int {
139-
if level < 0 {
140-
const maxInt = int(^uint(0) >> 1)
141-
return maxInt
142-
}
137+
func (s *Log) TicksAtLevel(level int) interface{} {
138+
return logTicker{s, false}.TicksAtLevel(level)
139+
}
143140

144-
firstN, lastN, _ := s.spacingAtLevel(level, roundOut)
145-
return int(lastN - firstN + 1)
146-
}
147-
148-
ticksAtLevel := func(level int) []float64 {
149-
ticks := []float64{}
150-
151-
if level < 0 {
152-
// Minor ticks for level 0. Get the major
153-
// ticks, but round out so we can fill in
154-
// minor ticks outside of the major ticks.
155-
firstN, lastN, _ := s.spacingAtLevel(0, true)
156-
for n := firstN; n <= lastN; n++ {
157-
tick := math.Pow(float64(s.Base), n)
158-
step := tick
159-
for i := 0; i < s.Base-1; i++ {
160-
if min <= tick && tick <= max {
161-
ticks = append(ticks, tick)
162-
}
163-
tick += step
141+
type logTicker struct {
142+
s *Log
143+
roundOut bool
144+
}
145+
146+
func (t logTicker) CountTicks(level int) int {
147+
if level < 0 {
148+
const maxInt = int(^uint(0) >> 1)
149+
return maxInt
150+
}
151+
152+
firstN, lastN, _ := t.s.spacingAtLevel(level, t.roundOut)
153+
return int(lastN - firstN + 1)
154+
}
155+
156+
func (t logTicker) TicksAtLevel(level int) interface{} {
157+
neg, min, max := t.s.ebounds()
158+
ticks := []float64{}
159+
160+
if level < 0 {
161+
// Minor ticks for level 0. Get the major
162+
// ticks, but round out so we can fill in
163+
// minor ticks outside of the major ticks.
164+
firstN, lastN, _ := t.s.spacingAtLevel(0, true)
165+
for n := firstN; n <= lastN; n++ {
166+
tick := math.Pow(float64(t.s.Base), n)
167+
step := tick
168+
for i := 0; i < t.s.Base-1; i++ {
169+
if min <= tick && tick <= max {
170+
ticks = append(ticks, tick)
164171
}
165-
}
166-
} else {
167-
firstN, lastN, base := s.spacingAtLevel(level, roundOut)
168-
for n := firstN; n <= lastN; n++ {
169-
ticks = append(ticks, math.Pow(base, n))
172+
tick += step
170173
}
171174
}
172-
173-
if neg {
174-
// Negate and reverse order of ticks.
175-
for i := 0; i < (len(ticks)+1)/2; i++ {
176-
j := len(ticks) - i - 1
177-
ticks[i], ticks[j] = -ticks[j], -ticks[i]
178-
}
175+
} else {
176+
firstN, lastN, base := t.s.spacingAtLevel(level, t.roundOut)
177+
for n := firstN; n <= lastN; n++ {
178+
ticks = append(ticks, math.Pow(base, n))
179179
}
180+
}
180181

181-
return ticks
182+
if neg {
183+
// Negate and reverse order of ticks.
184+
for i := 0; i < (len(ticks)+1)/2; i++ {
185+
j := len(ticks) - i - 1
186+
ticks[i], ticks[j] = -ticks[j], -ticks[i]
187+
}
182188
}
183189

184-
return nticksAtLevel, ticksAtLevel
190+
return ticks
185191
}
186192

187193
func (s Log) Ticks(o TickOptions) (major, minor []float64) {
@@ -190,23 +196,23 @@ func (s Log) Ticks(o TickOptions) (major, minor []float64) {
190196
} else if s.Min == s.Max {
191197
return []float64{s.Min}, []float64{s.Max}
192198
}
193-
count, ticks := s.tickFuncs(false)
199+
t := logTicker{&s, false}
194200

195-
level, ok := o.FindLevel(count, ticks, 0)
201+
level, ok := o.FindLevel(t, 0)
196202
if !ok {
197203
return nil, nil
198204
}
199-
return ticks(level), ticks(level - 1)
205+
return t.TicksAtLevel(level).([]float64), t.TicksAtLevel(level - 1).([]float64)
200206
}
201207

202208
func (s *Log) Nice(o TickOptions) {
203209
if s.Min == s.Max {
204210
return
205211
}
206212
neg, _, _ := s.ebounds()
207-
count, ticks := s.tickFuncs(true)
213+
t := logTicker{s, true}
208214

209-
level, ok := o.FindLevel(count, ticks, 0)
215+
level, ok := o.FindLevel(t, 0)
210216
if !ok {
211217
return
212218
}

scale/ticks.go

+25-35
Original file line numberDiff line numberDiff line change
@@ -20,37 +20,40 @@ type TickOptions struct {
2020
// levels to accept, respectively. If they are both 0, there is
2121
// no limit on acceptable tick levels.
2222
MinLevel, MaxLevel int
23+
}
24+
25+
// A Ticker computes tick marks for a scale. The "level" of the ticks
26+
// controls how many ticks there are and how closely they are spaced.
27+
// Higher levels have fewer ticks, while lower levels have more ticks.
28+
// For example, on a numerical scale, one could have ticks at every
29+
// n*(10^level).
30+
type Ticker interface {
31+
// CountTicks returns the number of ticks at level in this
32+
// scale's input range. This is equivalent to
33+
// len(TicksAtLevel(level)), but should be much more
34+
// efficient. CountTicks is a weakly monotonically decreasing
35+
// function of level.
36+
CountTicks(level int) int
2337

24-
// Pred returns true if ticks is an acceptable set of major
25-
// ticks. ticks will be in increasing order. Pred must be
26-
// "monotonic" in level in the following sense: if Pred is
27-
// false for level l (or ticks t), it must be false for all l'
28-
// < l (or len(t') > len(t)), and if Pred is true for level l
29-
// (or ticks t), it must be true for all l' > l (or len(t') <
30-
// len(t)). In other words, Pred should return false if there
31-
// are "too many" ticks or they are "too close together".
32-
//
33-
// If Pred is nil, it is assumed to always be satisfied.
34-
Pred func(ticks []float64, level int) bool
38+
// TicksAtLevel returns a slice of "nice" tick values in
39+
// increasing order at level in this scale's input range.
40+
// Typically, TicksAtLevel(l+1) is a subset of
41+
// TicksAtLevel(l). That is, higher levels remove ticks from
42+
// lower levels.
43+
TicksAtLevel(level int) interface{}
3544
}
3645

3746
// FindLevel returns the lowest level that satisfies the constraints
3847
// given by o:
3948
//
40-
// * count(level) <= o.Max
49+
// * ticker.CountTicks(level) <= o.Max
4150
//
4251
// * o.MinLevel <= level <= o.MaxLevel (if MinLevel and MaxLevel != 0).
4352
//
44-
// * o.Pred(ticks(level), level) is true (if o.Pred != nil).
45-
//
4653
// If the constraints cannot be satisfied, it returns 0, false.
4754
//
48-
// ticks(level) must return the tick marks at level in increasing
49-
// order. count(level) must return len(ticks(level)), but should do so
50-
// without constructing the ticks array because it may be very large.
51-
// count must be a weakly monotonically decreasing function of level.
5255
// guess is the level to start the optimization at.
53-
func (o *TickOptions) FindLevel(count func(level int) int, ticks func(level int) []float64, guess int) (int, bool) {
56+
func (o *TickOptions) FindLevel(ticker Ticker, guess int) (int, bool) {
5457
minLevel, maxLevel := o.MinLevel, o.MaxLevel
5558
if minLevel == 0 && maxLevel == 0 {
5659
minLevel, maxLevel = -1000, 1000
@@ -70,20 +73,20 @@ func (o *TickOptions) FindLevel(count func(level int) int, ticks func(level int)
7073
}
7174

7275
// Optimize count against o.Max.
73-
if count(l) <= o.Max {
76+
if ticker.CountTicks(l) <= o.Max {
7477
// We're satisfying the o.Max and min/maxLevel
7578
// constraints. count is monotonically decreasing, so
7679
// decrease level to increase the count until we
7780
// violate either o.Max or minLevel.
78-
for l--; l >= minLevel && count(l) <= o.Max; l-- {
81+
for l--; l >= minLevel && ticker.CountTicks(l) <= o.Max; l-- {
7982
}
8083
// We went one too far.
8184
l++
8285
} else {
8386
// We're over o.Max. Increase level to decrease the
8487
// count until we go below o.Max. This may cause us to
8588
// violate maxLevel.
86-
for l++; l <= maxLevel && count(l) > o.Max; l++ {
89+
for l++; l <= maxLevel && ticker.CountTicks(l) > o.Max; l++ {
8790
}
8891
if l > maxLevel {
8992
// We can't satisfy both o.Max and maxLevel.
@@ -94,18 +97,5 @@ func (o *TickOptions) FindLevel(count func(level int) int, ticks func(level int)
9497
// At this point l is the lowest value that satisfies the
9598
// o.Max, minLevel, and maxLevel constraints.
9699

97-
// Optimize ticks against o.Pred.
98-
if o.Pred != nil {
99-
// Increase level until Pred is satisfied. This may
100-
// cause us to violate maxLevel.
101-
for l <= maxLevel && !o.Pred(ticks(l), l) {
102-
l++
103-
}
104-
if l > maxLevel {
105-
// We can't satisfy both maxLevel and Pred.
106-
return 0, false
107-
}
108-
}
109-
110100
return l, true
111101
}

scale/ticks_test.go

+18-22
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,32 @@ package scale
66

77
import "testing"
88

9-
func TestTicks(t *testing.T) {
10-
count := func(level int) int {
11-
c := 10 - level
12-
if c < 1 {
13-
c = 1
14-
}
15-
return c
9+
type testTicker struct{}
10+
11+
func (testTicker) CountTicks(level int) int {
12+
c := 10 - level
13+
if c < 1 {
14+
c = 1
1615
}
17-
ticks := func(level int) []float64 {
18-
m := make([]float64, count(level))
19-
for i := 0; i < len(m); i++ {
20-
m[i] = float64(i)
21-
}
22-
return m
16+
return c
17+
}
18+
19+
func (t testTicker) TicksAtLevel(level int) interface{} {
20+
m := make([]float64, t.CountTicks(level))
21+
for i := 0; i < len(m); i++ {
22+
m[i] = float64(i)
2323
}
24+
return m
25+
}
26+
27+
func TestTicks(t *testing.T) {
2428
check := func(o TickOptions, want int) {
2529
wantL, wantOK := want, true
2630
if want == -999 {
2731
wantL, wantOK = 0, false
2832
}
2933
for _, guess := range []int{0, -50, 50} {
30-
l, ok := o.FindLevel(count, ticks, guess)
34+
l, ok := o.FindLevel(testTicker{}, guess)
3135
if l != wantL || ok != wantOK {
3236
t.Errorf("%+v.FindLevel with guess %v returned %v, %v; wanted %v, %v", o, guess, l, ok, wantL, wantOK)
3337
}
@@ -52,12 +56,4 @@ func TestTicks(t *testing.T) {
5256
check(TickOptions{Max: 6, MaxLevel: 9}, 4)
5357
check(TickOptions{Max: 6, MaxLevel: 3}, -999)
5458
check(TickOptions{Max: 6, MinLevel: 10, MaxLevel: 11}, 10)
55-
56-
// Predicate always matches.
57-
check(TickOptions{Max: 6, Pred: func(t []float64, level int) bool { return true }}, 4)
58-
// Predicate matches in the middle of the satisfiable region.
59-
check(TickOptions{Max: 6, Pred: func(t []float64, level int) bool { return level >= 6 }}, 6)
60-
check(TickOptions{Max: 6, MinLevel: 5, MaxLevel: 1000, Pred: func(t []float64, level int) bool { return level >= 6 }}, 6)
61-
// Predicate does not match in the satisfiable region.
62-
check(TickOptions{Max: 6, MaxLevel: 5, Pred: func(t []float64, level int) bool { return level >= 6 }}, -999)
6359
}

0 commit comments

Comments
 (0)