Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 41 additions & 12 deletions axis.go
Original file line number Diff line number Diff line change
Expand Up @@ -451,41 +451,70 @@ func (ts ConstantTicks) Ticks(float64, float64) []Tick {
return ts
}

// UnixTimeTicks is suitable for axes representing time values.
// UnixTimeTicks expects values in Unix time seconds.
type UnixTimeTicks struct {
// TimeTicks is suitable for axes representing time values.
// By default, TimeTicks expects values in (UTC) Unix time seconds.
Copy link
Contributor

Choose a reason for hiding this comment

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

Unix time represents moment in time. It is time zone independent, so (UTC) Unix time seconds doesn't make sense.

type TimeTicks struct {
// Ticker is used to generate a set of ticks.
// If nil, DefaultTicks will be used.
Ticker Ticker

// Format is the textual representation of the time value.
// If empty, time.RFC3339 will be used
Format string

// Converter takes a float64 value and converts it into a time.Time.
// If nil, UnixTimeConverter will be used.
Converter TimeFloatConverter
}

var _ Ticker = UnixTimeTicks{}
var _ Ticker = TimeTicks{}

// Ticks implements plot.Ticker.
func (utt UnixTimeTicks) Ticks(min, max float64) []Tick {
if utt.Ticker == nil {
utt.Ticker = DefaultTicks{}
func (tt TimeTicks) Ticks(min, max float64) []Tick {
if tt.Ticker == nil {
tt.Ticker = DefaultTicks{}
}
if tt.Format == "" {
tt.Format = time.RFC3339
}
if utt.Format == "" {
utt.Format = time.RFC3339
if tt.Converter == nil {
tt.Converter = UnixTimeConverter{}
}

ticks := utt.Ticker.Ticks(min, max)
ticks := tt.Ticker.Ticks(min, max)
for i := range ticks {
tick := &ticks[i]
if tick.Label == "" {
continue
}
t := time.Unix(int64(tick.Value), 0)
tick.Label = t.Format(utt.Format)
t := tt.Converter.UnmarshalTime(tick.Value)
tick.Label = t.Format(tt.Format)
}
return ticks
}

// TimeFloatConverter converts back and forth a time.Time value into
// a float64.
type TimeFloatConverter interface {
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of using interface I would use

type TimeConverterFunc func(t float64) time.Time

and have

func UnixTimeIn(loc *time.Location) TimeConverterFunc {
    return func(t float64) time.Time {
        return time.Unix(int64(t), 0).In(loc)
    }
}

var UTCUnixTime = UnixTimeIn(time.UTC) // use this as default converter

MarshalTime(t time.Time) float64
UnmarshalTime(float64) time.Time
}

// UnixTimeConverter converts a time.Time value into the float64 representation
// of Unix time, using the UTC time location.
type UnixTimeConverter struct{}

// MarshalTime implements TimeFloatConverter.
func (UnixTimeConverter) MarshalTime(t time.Time) float64 {
v := t.UTC().Unix()
Copy link
Contributor

Choose a reason for hiding this comment

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

calling UTC() here is not necessary t.Unix() is always the same as t.UTC().Unix(). From https://golang.org/pkg/time/#Time:

Each Time has associated with it a Location, consulted when computing the presentation form of the time, such as in the Format, Hour, and Year methods. The methods Local, UTC, and In return a Time with a specific location. Changing the location in this way changes only the presentation; it does not change the instant in time being denoted and therefore does not affect the computations described in earlier paragraphs.

return float64(v)
}

// UnmarshalTime implements TimeFloatConverter.
func (UnixTimeConverter) UnmarshalTime(v float64) time.Time {
return time.Unix(int64(v), 0).UTC()
Copy link
Contributor

Choose a reason for hiding this comment

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

Using UTC as default here is fine, but it would be good to make it configurable.

}

// A Tick is a single tick mark on an axis.
type Tick struct {
// Value is the data value marked by this Tick.
Expand Down
Binary file modified plotter/testdata/timeseries_golden.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 10 additions & 3 deletions plotter/timeseries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ import (

// Example_timeSeries draws a time series.
func Example_timeSeries() {

// xticks defines how we convert and display time.Time values.
xticks := plot.TimeTicks{
Format: "2006-01-02\n15:04",
Converter: plot.UnixTimeConverter{},
}

// randomPoints returns some random x, y points
// with some interesting kind of trend.
randomPoints := func(n int) XYs {
Expand All @@ -31,8 +38,8 @@ func Example_timeSeries() {
)
pts := make(XYs, n)
for i := range pts {
date := time.Date(2007+i, month, day, hour, min, sec, nsec, time.UTC).Unix()
pts[i].X = float64(date)
date := time.Date(2007+i, month, day, hour, min, sec, nsec, time.UTC)
pts[i].X = xticks.Converter.MarshalTime(date)
pts[i].Y = float64(pts[i].X+10*rand.Float64()) * 1e-9
}
return pts
Expand All @@ -46,7 +53,7 @@ func Example_timeSeries() {
log.Panic(err)
}
p.Title.Text = "Time Series"
p.X.Tick.Marker = plot.UnixTimeTicks{Format: "2006-01-02"}
p.X.Tick.Marker = xticks
p.Y.Label.Text = "Number of Gophers\n(Billions)"
p.Add(NewGrid())

Expand Down