Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented an AutoUnixTimeTicks #313

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
123 changes: 123 additions & 0 deletions axis.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,129 @@ func (utt UnixTimeTicks) Ticks(min, max float64) []Tick {
return ticks
}

// TimeTicks is suitable for axes representing time values.
// TimeTicks expects values in Unix time seconds. It will
// adjust the number of ticks according to the specified Width. If
// not specified, Width defaults to 10 centimeters.
type TimeTicks struct {
// Width is the width of the underlying graph, used to calculate
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/graph/plot/

// the number of ticks that can fit properly with their time
// shown.
Width vg.Length
}

var _ Ticker = TimeTicks{}

// Inspired by https://github.com/d3/d3-scale/blob/master/src/time.js
var tickRules = []tickRule{
{time.Millisecond, "15:04:05.000", "15:04:05", ".000"},
{200 * time.Millisecond, "15:04:05.000", "15:04:05", ".000"},
{500 * time.Millisecond, "15:04:05.000", "15:04:05", ".000"},
{time.Second, "15:04:05", "15:04", ":05"},
{2 * time.Second, "15:04:05", "15:04", ":05"},
{5 * time.Second, "15:04:05", "15:04", ":05"},
{15 * time.Second, "Jan 02, 15:04", "Jan 02", "15:04"},
{30 * time.Second, "15:04:05", "15:04", ":05"},
{time.Minute, "15:04:05", "15:04", ":05"},
{2 * time.Minute, "Jan 02, 3:04pm", "Jan 02", "3:04pm"},
{5 * time.Minute, "Jan 02, 3:04pm", "Jan 02", "3:04pm"},
{15 * time.Minute, "Jan 02, 3:04pm", "Jan 02", "3:04pm"},
{30 * time.Minute, "Jan 02, 3:04pm", "Jan 02", "3:04pm"},
{time.Hour, "Jan 2, 3pm", "Jan 2", "3pm"},
{3 * time.Hour, "Jan 2, 3pm", "Jan 2", "3pm"},
{6 * time.Hour, "Jan 2, 3pm", "Jan 2", "3pm"},
{12 * time.Hour, "Jan 2", "Jan 2", "3pm"},
{24 * time.Hour, "Jan 2", "Jan", "2"},
{48 * time.Hour, "Jan 2", "Jan", "2"},
{7 * 24 * time.Hour, "Jan 2", "Jan", "2"},
{month, "Jan 2006", "2006", "Jan"},
{3 * month, "Jan 2006", "2006", "Jan"},
{6 * month, "Jan 2006", "2006", "Jan"},
{12 * month, "2006", "", "2006"},
{2 * year, "2006", "", "2006"},
{5 * year, "2006", "", "2006"},
{10 * year, "2006", "", "2006"},
}

const month = 31 * 24 * time.Hour
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These can be in a const block. Also, I'm sort of troubled by the choice of length of month; the marginally closer length is 30 days.

const year = 12 * month

// tickRule defines a time display format for a given time window (per
// inch).
//
// This assumes a tick about each `durationPerInch`. The long format is
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use single space after period throughout.

// shown each time the timestamp goes over a certain boundary
// (verified through `watchFormat`). This way you can show `Sep 2,
// 12pm` when you pass midnight after `11pm` on `Sep 1`.
type tickRule struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc comments here as complete grammatical sentences please.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type does not need to exist. You can define tickRules on the anonymous struct.

durationPerInch time.Duration // use this rule for a maximum Duration per inch, it is also used as an interval per ticks.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SI please.

longFormat string // longer format
watchFormat string // show long format when watchFormat changes between ticks
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs a better name, Given that we are talking about time, "watch" has two possible meanings.

shortFormat string // incremental format, shorter
}

// Ticks implements plot.Ticker and displays appropriately spaced and
// formatted time labels.
func (t TimeTicks) Ticks(min, max float64) []Tick {
width := t.Width
if width == 0 {
width = 10 * vg.Centimeter
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the basis for this?

}

minT := time.Unix(int64(min), 0).UTC()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need for the T suffix, just min and max.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's there to distinguish from the function parameters min and max, shortly indicating they're time.Time instead of a float64. Would you prefer some other names ?

maxT := time.Unix(int64(max), 0).UTC()
durationPerInch := maxT.Sub(minT) / time.Duration(width/vg.Inch)

lastElement := len(tickRules) - 1
rule := tickRules[lastElement]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just use rule := tickRules[len(tickRules)-1] and similarly in the range.

for idx, tickRule := range tickRules[:lastElement] {
if durationPerInch < tickRules[idx+1].durationPerInch {
rule = tickRule
break
}
}

timeWindow := rule.durationPerInch
delta := time.Month(timeWindow / month) // in months
start := minT.Truncate(timeWindow)
var lastWatch string
var ticks []Tick
for {
if delta > 0 {
// Count in Months now
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full stop.

start = time.Date(start.Year(), start.Month()+delta, 1, 0, 0, 0, 0, time.UTC)
} else {
start = start.Add(timeWindow)
}

if start.Before(minT) {
continue
}
if start.After(maxT) {
break
}

var label string
newWatch := start.Format(rule.watchFormat)
if lastWatch == newWatch {
label = start.Format(rule.shortFormat)
} else {
//TODO: overwrite the first tick with the long form if we
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s|//TODO: o|// TODO(name): O|

// haven't shown a lonform at all.. instead of always
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/lonform/longform/
s/../,/

// showing the longform first.
label = start.Format(rule.longFormat)
}
lastWatch = newWatch

ticks = append(ticks, Tick{
Value: float64(start.UnixNano()) / float64(time.Second),
Label: label,
})
}

return ticks
}

// A Tick is a single tick mark on an axis.
type Tick struct {
// Value is the data value marked by this Tick.
Expand Down
176 changes: 176 additions & 0 deletions axis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import (
"math"
"reflect"
"testing"
"time"

"github.com/gonum/plot/vg"
)

func TestAxisSmallTick(t *testing.T) {
Expand Down Expand Up @@ -59,3 +62,176 @@ func labelsOf(ticks []Tick) []string {
}
return labels
}

func allLabelsOf(ticks []Tick) []string {
var labels []string
for _, t := range ticks {
labels = append(labels, t.Label)
}
return labels
}

func TestTimeTicks(t *testing.T) {
d := TimeTicks{Width: 4 * vg.Inch}
for _, test := range []struct {
min, max string
want []string
}{
{
min: "2016-01-01 12:56:30",
max: "2016-01-01 12:56:31",
want: []string{"12:56:30.200", ".400", ".600", ".800", "12:56:31.000"},
},
{
min: "2016-01-01 12:56:01",
max: "2016-01-01 12:56:59",
want: []string{"12:56:05", ":10", ":15", ":20", ":25", ":30", ":35", ":40", ":45", ":50", ":55"},
},
{
min: "2016-01-01 12:56:30",
max: "2016-01-01 12:57:29",
want: []string{"12:56:35", ":40", ":45", ":50", ":55", "12:57:00", ":05", ":10", ":15", ":20", ":25"},
},
{
min: "2016-01-01 12:01:05",
max: "2016-01-01 12:07:00",
want: []string{"12:02:00", "12:03:00", "12:04:00", "12:05:00", "12:06:00", "12:07:00"},
},
{
min: "2016-01-01 12:01:05",
max: "2016-01-01 12:17:00",
want: []string{"Jan 01, 12:02pm", "12:04pm", "12:06pm", "12:08pm", "12:10pm", "12:12pm", "12:14pm", "12:16pm"},
},
{
min: "2016-01-01 12:01:05",
max: "2016-01-01 12:28:00",
want: []string{"Jan 01, 12:05pm", "12:10pm", "12:15pm", "12:20pm", "12:25pm"},
},
{
min: "2016-01-01 12:01:05",
max: "2016-01-01 12:35:00",
want: []string{"Jan 01, 12:05pm", "12:10pm", "12:15pm", "12:20pm", "12:25pm", "12:30pm", "12:35pm"},
},
{
min: "2016-01-01 12:01:05",
max: "2016-01-01 12:40:00",
want: []string{"Jan 01, 12:05pm", "12:10pm", "12:15pm", "12:20pm", "12:25pm", "12:30pm", "12:35pm", "12:40pm"},
},
{
min: "2016-01-01 12:01:05",
max: "2016-01-01 12:45:00",
want: []string{"Jan 01, 12:05pm", "12:10pm", "12:15pm", "12:20pm", "12:25pm", "12:30pm", "12:35pm", "12:40pm", "12:45pm"},
},
{
min: "2016-01-01 12:01:05",
max: "2016-01-01 13:05:00",
want: []string{"Jan 01, 12:15pm", "12:30pm", "12:45pm", "1:00pm"},
},
{
min: "2016-01-01 12:01:05",
max: "2016-01-01 13:05:00",
want: []string{"Jan 01, 12:15pm", "12:30pm", "12:45pm", "1:00pm"},
},
{
min: "2016-01-01 12:01:05",
max: "2016-01-01 16:05:00",
want: []string{"Jan 1, 1pm", "2pm", "3pm", "4pm"},
},
{
min: "2016-01-01 20:01:05",
max: "2016-01-02 07:59:00",
want: []string{"Jan 1, 9pm", "10pm", "11pm", "Jan 2, 12am", "1am", "2am", "3am", "4am", "5am", "6am", "7am"},
},
{
min: "2016-01-01 12:01:05",
max: "2016-01-02 13:59:00",
want: []string{"Jan 1, 6pm", "Jan 2, 12am", "6am", "12pm"},
},
{
min: "2016-01-01 12:01:05",
max: "2016-01-04 13:59:00",
want: []string{"Jan 2", "12pm", "Jan 3", "12pm", "Jan 4", "12pm"},
},
{
min: "2016-01-01 12:01:05",
max: "2016-01-06 13:59:00",
want: []string{"Jan 2", "3", "4", "5", "6"},
},
{
min: "2016-01-01 12:01:05",
max: "2016-01-09 13:59:00",
want: []string{"Jan 2", "4", "6", "8"},
},
{
min: "2016-01-01 12:01:05",
max: "2016-01-25 13:59:00",
want: []string{"Jan 2", "4", "6", "8", "10", "12", "14", "16", "18", "20", "22", "24"},
},
{
min: "2016-01-01 12:01:05",
max: "2016-02-06 13:59:00",
want: []string{"Jan 4", "11", "18", "25", "Feb 1"},
},
{
min: "2016-01-01 12:01:05",
max: "2016-02-28 13:59:00",
want: []string{"Jan 4", "11", "18", "25", "Feb 1", "8", "15", "22"},
},
{
min: "2016-01-01 12:01:05",
max: "2016-04-28 13:59:00",
want: []string{"Jan 4", "11", "18", "25", "Feb 1", "8", "15", "22", "29", "Mar 7", "14", "21", "28", "Apr 4", "11", "18", "25"},
},
{
min: "2016-01-01 12:01:05",
max: "2016-09-28 13:59:00",
want: []string{"Feb 2016", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep"},
},
{
min: "2016-01-01 12:01:05",
max: "2016-12-28 13:59:00",
want: []string{"Feb 2016", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"},
},
{
min: "2016-01-01 12:01:05",
max: "2017-02-28 13:59:00",
want: []string{"Feb 2016", "May", "Aug", "Nov", "Feb 2017"},
},
{
min: "2016-01-01 12:01:05",
max: "2017-08-28 13:59:00",
want: []string{"Feb 2016", "May", "Aug", "Nov", "Feb 2017", "May", "Aug"},
},
{
min: "2016-01-01 12:01:05",
max: "2018-08-28 13:59:00",
want: []string{"Feb 2016", "Aug", "Feb 2017", "Aug", "Feb 2018", "Aug"},
},
{
min: "2016-01-01 12:01:05",
max: "2020-08-28 13:59:00",
want: []string{"2016", "2017", "2018", "2019", "2020"},
},
{
min: "2016-01-01 12:01:05",
max: "2048-08-28 13:59:00",
want: []string{"2017", "2022", "2027", "2032", "2037", "2042", "2047"},
},
} {
//fmt.Println("For dates", test.min, test.max)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete this line.

ticks := d.Ticks(dateToFloat64(test.min), dateToFloat64(test.max))
got := allLabelsOf(ticks)
if !reflect.DeepEqual(got, test.want) {
t.Errorf("tick labels mismatch:\ndate1: %s\ndate2: %s\ngot: %#v\nwant:%q", test.min, test.max, got, test.want)
}
}
}

func dateToFloat64(date string) float64 {
t, err := time.Parse("2006-01-02 15:04:05", date)
if err != nil {
panic(err)
}

return float64(t.UTC().UnixNano()) / float64(time.Second)
}