-
Notifications
You must be signed in to change notification settings - Fork 204
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
// 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doc comments here as complete grammatical sentences please. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This type does not need to exist. You can define |
||
durationPerInch time.Duration // use this rule for a maximum Duration per inch, it is also used as an interval per ticks. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no need for the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's there to distinguish from the function parameters |
||
maxT := time.Unix(int64(max), 0).UTC() | ||
durationPerInch := maxT.Sub(minT) / time.Duration(width/vg.Inch) | ||
|
||
lastElement := len(tickRules) - 1 | ||
rule := tickRules[lastElement] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just use |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/lonform/longform/ |
||
// 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. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,9 @@ import ( | |
"math" | ||
"reflect" | ||
"testing" | ||
"time" | ||
|
||
"github.com/gonum/plot/vg" | ||
) | ||
|
||
func TestAxisSmallTick(t *testing.T) { | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/graph/plot/