diff --git a/axis.go b/axis.go index d1f13743..69ec8aed 100644 --- a/axis.go +++ b/axis.go @@ -5,6 +5,7 @@ package plot import ( + "fmt" "image/color" "math" "strconv" @@ -504,6 +505,98 @@ func (t TimeTicks) Ticks(min, max float64) []Tick { return ticks } +// IntTicks is suitable for axes representing integer. +type IntTicks struct { + // AddMinTick adds tick to the min value when it is a true. + AddMinTick bool + + // AddMaxTick adds tick to the max value when it is a true. + AddMaxTick bool +} + +// Ticks returns Ticks in a specified range +func (it IntTicks) Ticks(min, max float64) (ticks []Tick) { + const SuggestedTicks = 3 + if max < min { + panic("illegal range") + } + tens := math.Pow10(int(math.Floor(math.Log10(max - min)))) + n := (max - min) / tens + for n < SuggestedTicks { + tens /= 10 + n = (max - min) / tens + } + + majorMult := int(n / SuggestedTicks) + switch majorMult { + case 7: + majorMult = 6 + case 9: + majorMult = 8 + } + majorDelta := float64(majorMult) * tens + val := math.Floor(min/majorDelta) * majorDelta + + // Add tick to min value + if it.AddMinTick { + if val != min { + ticks = append(ticks, Tick{Value: min, Label: fmt.Sprintf("%.f", min)}) + } + } + + for val <= max { + if val >= min && val <= max { + ticks = append(ticks, Tick{Value: val, Label: fmt.Sprintf("%.f", val)}) + } + if math.Nextafter(val, val+majorDelta) == val { + break + } + val += majorDelta + } + + minorDelta := majorDelta / 2 + switch majorMult { + case 3, 6: + minorDelta = majorDelta / 3 + case 5: + minorDelta = majorDelta / 5 + } + + val = math.Floor(min/minorDelta) * minorDelta + for val <= max { + found := false + for _, t := range ticks { + if t.Value == val { + found = true + } + } + if val >= min && val <= max && !found { + ticks = append(ticks, Tick{Value: val}) + } + if math.Nextafter(val, val+minorDelta) == val { + break + } + val += minorDelta + } + // Add tick to max value + if it.AddMaxTick { + found := false + for _, t := range ticks { + if t.Value == max { + if t.Label == "" { + t.Label = fmt.Sprintf("%.f", t.Value) + } + found = true + break + } + } + if found == false { + ticks = append(ticks, Tick{Value: max, Label: fmt.Sprintf("%.f", max)}) + } + } + return +} + // A Tick is a single tick mark on an axis. type Tick struct { // Value is the data value marked by this Tick. diff --git a/axis_test.go b/axis_test.go index 32f49b2f..3aaae88c 100644 --- a/axis_test.go +++ b/axis_test.go @@ -50,6 +50,46 @@ func TestAxisSmallTick(t *testing.T) { } } +func TestIntTick(t *testing.T) { + i := IntTicks{} + for _, test := range []struct { + addMinTick bool + addMaxTick bool + min, max float64 + want []string + }{ + { + addMinTick: false, + addMaxTick: false, + min: 1.0, + max: 99999.0, + want: []string{"30000", "60000", "90000"}, + }, + { + addMinTick: true, + addMaxTick: false, + min: 1.0, + max: 99999.0, + want: []string{"1", "30000", "60000", "90000"}, + }, + { + addMinTick: true, + addMaxTick: true, + min: 1.0, + max: 99999.0, + want: []string{"1", "30000", "60000", "90000", "99999"}, + }, + } { + i.AddMinTick = test.addMinTick + i.AddMaxTick = test.addMaxTick + ticks := i.Ticks(test.min, test.max) + got := labelsOf(ticks) + if !reflect.DeepEqual(got, test.want) { + t.Errorf("tick labels mismatch:\ngot: %q\nwant:%q", got, test.want) + } + } +} + func labelsOf(ticks []Tick) []string { var labels []string for _, t := range ticks {