Skip to content

Conversation

@sbinet
Copy link
Member

@sbinet sbinet commented Apr 22, 2016

Fixes #276

@kortschak
Copy link
Member

It's been 132 years. I think it's time to let go ;P

This does not fix the problem; UTC is just as fixed as Paris time. I think the issue is interplay with the locale that is used by formatting and the locale that is used during parsing, though I can't find a combinations of locales that fixes it.

@eaburns
Copy link
Member

eaburns commented Apr 22, 2016

You need to use the local timezone to create the original time values. The timezone is lost when you convert to unix seconds;time.Unixalways assumes its input is in the local timezone. If you create the initial times in the local zone thentime.Unix` will be correct in its assumption that the times are local, and you will get the correct time back.

@kortschak
Copy link
Member

I tried that when I reported the issue and it doesn't work:

$ git diff
diff --git a/plotter/timeseries_test.go b/plotter/timeseries_test.go
index 9954874..dbbfcf6 100644
--- a/plotter/timeseries_test.go
+++ b/plotter/timeseries_test.go
@@ -31,7 +31,7 @@ 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()
+                       date := time.Date(2007+i, month, day, hour, min, sec, nsec, time.Local).Unix()
                        pts[i].X = float64(date)
                        pts[i].Y = float64(pts[i].X+10*rand.Float64()) * 1e-9
                }
$ go test
--- FAIL: TestImagePlot (0.02s)
    general_test.go:74: image mismatch for image_plot.png
    general_test.go:94: golden image has been written out as image_plot_golden.png
--- FAIL: TestTimeSeries (0.02s)
    general_test.go:74: image mismatch for timeseries.png
    general_test.go:94: golden image has been written out as timeseries_golden.png
FAIL

There is another failure there, which I have not yet investigated.

@eaburns
Copy link
Member

eaburns commented Apr 22, 2016

Are those two separate thoughts: 1. that you tried it, and 2. that you
found another failure? Or is the second a consequence of the first?

Anyway, I'm still confused. How can it fail if you use local time? If you
create a date in your local timezone, convert it to unix seconds, convert
it back is it a different time? In other words, what does this program
print when you run it locally: http://play.golang.org/p/3AjCyKJsIX ? It
should print true. If so, then I don't see how doing the exact same thing
for the test would fail.

On Fri, Apr 22, 2016 at 6:56 AM Dan Kortschak notifications@github.com
wrote:

I tried that when I reported the issue and it doesn't work:

$ git diff
diff --git a/plotter/timeseries_test.go b/plotter/timeseries_test.go
index 9954874..dbbfcf6 100644
--- a/plotter/timeseries_test.go
+++ b/plotter/timeseries_test.go
@@ -31,7 +31,7 @@ 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()
    
  •                   date := time.Date(2007+i, month, day, hour, min, sec, nsec, time.Local).Unix()
                    pts[i].X = float64(date)
                    pts[i].Y = float64(pts[i].X+10*rand.Float64()) \* 1e-9
            }
    
    $ go test
    --- FAIL: TestImagePlot (0.02s)
    general_test.go:74: image mismatch for image_plot.png
    general_test.go:94: golden image has been written out as image_plot_golden.png
    --- FAIL: TestTimeSeries (0.02s)
    general_test.go:74: image mismatch for timeseries.png
    general_test.go:94: golden image has been written out as timeseries_golden.png
    FAIL

There is another failure there, which I have not yet investigated.


You are receiving this because you commented.

Reply to this email directly or view it on GitHub
#278 (comment)

@sbinet
Copy link
Member Author

sbinet commented Apr 22, 2016

so... the fix would actually be to modify:
https://github.com/gonum/plot/blob/master/axis.go#L483

to return a time.Time in UTC, right?

t := time.Unix(int64(tick.Value), 0).UTC()

then, perhaps the right fix would be to add another field to UnixTimeTicks:

type UnixTimeTicks 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

    // Convert takes a float64 value and converts it into a time.Time.
    // If nil, time.Unix will be used.
    Convert func(float64) time.Time
}

@kortschak
Copy link
Member

kortschak commented Apr 23, 2016 via email

@eaburns
Copy link
Member

eaburns commented Apr 23, 2016

@sbinet, another option is instead of having Convert, have the user specify a timezone in the ticker, defaulting to Local. However, I still want to know why using Local all of the way through doesn't work.

@kostya-sh
Copy link
Contributor

I have looked into this issue and found the following:

  • In order to produce a plot with the same date ticks as the golden image the same timestamps and location as were used to produce the golden image must be used. This means that timestamps should be created using UTC timezone but formatted using (I assume) Europe/Paris.
  • I also discovered that when I run tests locally (using the latest versions of all libraries and Go 1.7 from tip) all tests comparing images against the golden files fail. Visually test images look the same though.

I hope this helps.

As far as the code change is concerned I would prefer to have UnixTimeTicks.Location field instead of UnixTimeTicks.Convert function (as suggested by @eaburns).

@kostya-sh
Copy link
Contributor

With Go 1.6.2 all tests pass (apart from timestamp test as I am in UTC+8). I suspect that Go 1.7 produces different images comparing to Go 1.6 due to compression algorithms changes in compress/flate package.

@sbinet
Copy link
Member Author

sbinet commented May 24, 2016

@kostya-sh thanks for giving it a try (and raising the issue wrt tip.)

wrt having a Convert field or a Location field:
having a Convert func field allows the user to have complete control on how the 64bits of a time.Time are to be encoded and then interpreted. (perhaps you want UnixNano() instead of Unix(), perhaps you want UTC(), etc...)
You could imagine that instead of simply converting the returned int64 value into a float64 one, you may have a function that retrieves the int64 bits payload and encodes it directly (via math.Float64frombits) into a int64. No rounding issue for big int64 values.

Just for my own information, did my alternate PR (the #278 one with Convert) fix the UTC problem?

@kostya-sh
Copy link
Contributor

@sbinet,

Convert vs Location. There is only one way to convert unix time (number of seconds since epoch) and timezone (time.Location) to time.Time object. Any other conversion is no longer a unix time conversion. If more flexibility is required then maybe UnixTimeTicks should be renamed to TimeTicks and a few standard conversion functions are provided. E.g.:

type TimeTicks {
    ...
    Conversion: TimeConversion
}

type TimeConversion func(float64) time.Time

func UnixTime(loc *time.Location) TimeConversion {
    ....
}

?

Your PR with UTC fixes the problem, but if you use Local() both to create timestamps and to convert them then the test fails because the golden file has been created using different data. To fix it you can either:

  • regenerate the golden file (using Local() conversion)
  • Use the timezone that was used to create golden file in your example (return time.Unix(int64(v), 0).In(time.FixedZone("Europe/Paris", 120)))

@kortschak
Copy link
Member

If more flexibility is required then maybe UnixTimeTicks should be renamed to TimeTicks and a few standard conversion functions are provided.

This sounds good to me.

sbinet added a commit to sbinet-gonum/plot that referenced this pull request May 25, 2016
This CL introduces the TimeFloatConverter interface that describes how a
time.Time value is converted to a float64 (and back.)
A default TimeFloatConverter (used by TimeTicks) is also provided in the
shape of UnixTimeConverter.

Test updated to be resilient against timezones (tested with
'Europe/Paris', 'America/Los_Angeles' and 'Australia/Sydney')

Fixes gonum#278.
@sbinet sbinet force-pushed the time-loc branch 2 times, most recently from 8081980 to fd4b9e8 Compare May 25, 2016 10:00
sbinet added a commit to sbinet-gonum/plot that referenced this pull request May 25, 2016
This CL introduces the TimeFloatConverter interface that describes how a
time.Time value is converted to a float64 (and back.)
A default TimeFloatConverter (used by TimeTicks) is also provided in the
shape of UnixTimeConverter.

Test updated to be resilient against timezones (tested with
'Europe/Paris', 'America/Los_Angeles' and 'Australia/Sydney')

Fixes gonum#278.
@sbinet
Copy link
Member Author

sbinet commented May 25, 2016

PTAL.

@sbinet
Copy link
Member Author

sbinet commented May 25, 2016

interestingly, my first approach of (un)marshaling time.Time values into float64 via math.Float64frombits didn't work because (presumably, haven't checked) the Ticker interface expects a linear space to find ticks and adjacent time.Time values don't result in adjacent float64 values (generated via math.Float64frombits)

too bad.

@kostya-sh
Copy link
Contributor

kostya-sh commented May 25, 2016

@sbinet I think converting from time.Time to float64 (TimeFloatConverter.MarshalTime) should not be part of this package because:

  1. this functionality is not used in the pacakge itself
  2. it is cumbersome to use this functionality from outside of the package (t.Unix() is much simler than creating plot.UnixTimeConverter{} and calling converter.MarshalTime(t)). Ideally I want the time formatting to work reasonably just by setting Tick.Marker = plot.TimeTicks{}.

Secondly it is not possible to customize Location that is used to format dates. Currenly it is hardcoded to UTC.

See also some comments inline.

// 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.

@kostya-sh
Copy link
Contributor

I left a few comments however despite being the one who proposed the idea of having more flexible TimeTicks type I really like the simplicity of the original UnixTimeTicks type and still think that adding Location field to it was all that required ;)

@sbinet
Copy link
Member Author

sbinet commented May 25, 2016

@kostya-sh , yes, it is somewhat a bit less compact to write.
on the plus side, the protocol to encode/decode time.Time values to/from float64 ones is, IMHO, clearer, with less opportunities to introduce impedance mismatch or incorrect expectations.

I could introduce:

func UnixTimeTicks(format string, cnv TimeFloatConverter) TimeTicks {
    if format == "" {
        format = time.RFC3339
    }
    if cnv == nil {
        cnv = UnixTimeConverter{}
    }
    return TimeTicks{
        Format: format,
        Converter: cnv,
    }
}

to have a more direct way to have a properly configured TimeTicks

This CL introduces the TimeFloatConverter interface that describes how a
time.Time value is converted to a float64 (and back.)
A default TimeFloatConverter (used by TimeTicks) is also provided in the
shape of UnixTimeConverter.

Test updated to be resilient against timezones (tested with
'Europe/Paris', 'America/Los_Angeles' and 'Australia/Sydney')

Fixes gonum#276.
@kostya-sh
Copy link
Contributor

I had another look at ticker related functionality in plot package and I think it might be good idea to split Ticker interface into two: Ticker and Labeler.

type Ticker interface {
  func Ticks(min, max float64) []float64
}

type Labeler interface {
  func Label(tick float64) string
}

This will allow to specify ticker rules and formatting rules independently.

For example LogTicks and DefaultTicks and ConstantTicks use the same formatting rules but different ticker rules. UnixTimeTicks and DefaultTicks have the same ticker rules but different formatting rules.

Also if ConstantTicks is just a []float64 it is much easier to create in code.

It might be good idea in addition to the time specific formatter provide time specific ticker as well (that can prefer ticks at the beginning of major time intervals such as minutes or months depending on range).

Having a seprate Labeler interface will also make it easy to provide FloatLabeler with configurable precision if necessary.

Thoughts? Should I bring this question on the mainling list instead?

@sbinet
Copy link
Member Author

sbinet commented May 31, 2016

SGTM.
I suppose sending a PR wouldn't hurt to see how that would look like.
(small(er) interfaces are good, anyways)

@kostya-sh
Copy link
Contributor

Ok, I will send a PR for review soon.

@kostya-sh
Copy link
Contributor

See #286

@kortschak
Copy link
Member

Can we merge this in the interim. Having a failing case here is hampering what I can do.

@kortschak
Copy link
Member

ping

@kortschak
Copy link
Member

@sbinet Is there a particular hold up here that I can help with?

@kortschak
Copy link
Member

I'm going to close this. If it is going to be revived, please reopen.

@kortschak kortschak closed this Oct 24, 2016
@eaburns
Copy link
Member

eaburns commented Oct 24, 2016

I don't understand that logic. It certainly will not get reviewed if it's closed. Are you waiting for me or someone in particular to review?

@kortschak
Copy link
Member

kortschak commented Oct 25, 2016 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants