diff --git a/go.mod b/go.mod index 88bb85ec9..78ca04c1b 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.20 require ( gioui.org v0.2.0 + git.sr.ht/~sbinet/epok v0.1.0 git.sr.ht/~sbinet/go-arrow v0.2.0 github.com/astrogo/fitsio v0.3.0 github.com/campoy/embedmd v1.0.0 @@ -59,6 +60,7 @@ require ( github.com/posener/complete v1.2.3 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.4 // indirect + github.com/teambition/rrule-go v1.8.2 // indirect golang.org/x/exp/shiny v0.0.0-20230801115018-d63ba01acd4b // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/net v0.14.0 // indirect diff --git a/go.sum b/go.sum index 9bacc26f2..71f06ac3d 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= gioui.org/x v0.2.0 h1:/MbdjKH19F16auv19UiQxli2n6BYPw7eyh9XBOTgmEw= gioui.org/x v0.2.0/go.mod h1:rCGN2nZ8ZHqrtseJoQxCMZpt2xrZUrdZ2WuMRLBJmYs= git.sr.ht/~sbinet/cmpimg v0.1.0 h1:E0zPRk2muWuCqSKSVZIWsgtU9pjsw3eKHi8VmQeScxo= +git.sr.ht/~sbinet/epok v0.1.0 h1:gtcT69TjnNZl5NansTvzqWtIJ5W4ysMSyiZIjYDzORE= +git.sr.ht/~sbinet/epok v0.1.0/go.mod h1:fPYSc65tPhuTrm4kYvAZDx/vGaMsbIGg7owyUn+DD48= git.sr.ht/~sbinet/gg v0.5.0 h1:6V43j30HM623V329xA9Ntq+WJrMjDxRjuAB1LFWF5m8= git.sr.ht/~sbinet/gg v0.5.0/go.mod h1:G2C0eRESqlKhS7ErsNey6HHrqU1PwsnCQlekFi9Q2Oo= git.sr.ht/~sbinet/go-arrow v0.2.0 h1:QIiVPcEtMb2lzOWDgBrIz9Sa+7Xvs2EJ/Icxe6IcUSI= @@ -114,6 +116,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/teambition/rrule-go v1.8.2 h1:lIjpjvWTj9fFUZCmuoVDrKVOtdiyzbzc93qTmRVe/J8= +github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4= github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2 h1:zzrxE1FKn5ryBNl9eKOeqQ58Y/Qpo3Q9QNxKHX5uzzQ= diff --git a/hplot/testdata/timeseries_daily_golden.png b/hplot/testdata/timeseries_daily_golden.png new file mode 100644 index 000000000..00cd8ac13 Binary files /dev/null and b/hplot/testdata/timeseries_daily_golden.png differ diff --git a/hplot/testdata/timeseries_monthly_golden.png b/hplot/testdata/timeseries_monthly_golden.png new file mode 100644 index 000000000..d8674b2a9 Binary files /dev/null and b/hplot/testdata/timeseries_monthly_golden.png differ diff --git a/hplot/testdata/timeseries_yearly_golden.png b/hplot/testdata/timeseries_yearly_golden.png new file mode 100644 index 000000000..b2878745e Binary files /dev/null and b/hplot/testdata/timeseries_yearly_golden.png differ diff --git a/hplot/ticks_example_test.go b/hplot/ticks_example_test.go index 8fe9a081b..1f93db0d0 100644 --- a/hplot/ticks_example_test.go +++ b/hplot/ticks_example_test.go @@ -5,9 +5,13 @@ package hplot_test import ( + "image/color" "log" + "time" + "git.sr.ht/~sbinet/epok" "go-hep.org/x/hep/hplot" + "gonum.org/v1/plot/plotter" "gonum.org/v1/plot/vg" "gonum.org/v1/plot/vg/draw" ) @@ -75,3 +79,198 @@ func ExampleTicks() { log.Fatalf("error: %+v\n", err) } } + +func ExampleTicks_yearly() { + cnv := epok.UTCUnixTimeConverter{} + + p := hplot.New() + p.Title.Text = "Time series (yearly)" + p.Y.Label.Text = "Goroutines" + + p.Y.Min = 0 + p.Y.Max = 4 + p.X.AutoRescale = true + p.X.Tick.Marker = epok.Ticks{ + Ruler: epok.Rules{ + Major: epok.Rule{ + Freq: epok.Yearly, + Range: epok.Range{Step: 5}, + }, + }, + Format: "2006-01-02\n15:04:05", + Converter: cnv, + } + + xysFrom := func(vs ...float64) plotter.XYs { + o := make(plotter.XYs, len(vs)) + for i := range o { + o[i].X = vs[i] + o[i].Y = float64(i + 1) + } + return o + } + data := xysFrom( + cnv.FromTime(parse("2010-02-03 01:02:03")), + cnv.FromTime(parse("2011-03-04 11:22:33")), + cnv.FromTime(parse("2015-02-03 04:05:06")), + cnv.FromTime(parse("2020-02-03 07:08:09")), + ) + + line, pnts, err := hplot.NewLinePoints(data) + if err != nil { + log.Fatalf("could not create plotter: %+v", err) + } + + line.Color = color.RGBA{B: 255, A: 255} + pnts.Shape = draw.CircleGlyph{} + pnts.Color = color.RGBA{R: 255, A: 255} + + p.Add(line, pnts, hplot.NewGrid()) + + err = p.Save(20*vg.Centimeter, 10*vg.Centimeter, "testdata/timeseries_yearly.png") + if err != nil { + log.Fatalf("could not save plot: %+v", err) + } +} + +func ExampleTicks_monthly() { + cnv := epok.UTCUnixTimeConverter{} + + p := hplot.New() + p.Title.Text = "Time series (monthly)" + p.Y.Label.Text = "Goroutines" + + p.Y.Min = 0 + p.Y.Max = 4 + p.X.AutoRescale = true + p.X.Tick.Marker = epok.Ticks{ + Ruler: epok.Rules{ + Major: epok.Rule{ + Freq: epok.Monthly, + Range: epok.RangeFrom(1, 13, 2), + }, + }, + Format: "2006\nJan-02\n15:04:05", + Converter: cnv, + } + + xysFrom := func(vs ...float64) plotter.XYs { + o := make(plotter.XYs, len(vs)) + for i := range o { + o[i].X = vs[i] + o[i].Y = float64(i + 1) + } + return o + } + data := xysFrom( + cnv.FromTime(parse("2010-01-02 01:02:03")), + cnv.FromTime(parse("2010-02-01 01:02:03")), + cnv.FromTime(parse("2010-02-04 11:22:33")), + cnv.FromTime(parse("2010-03-04 01:02:03")), + cnv.FromTime(parse("2010-04-05 01:02:03")), + cnv.FromTime(parse("2010-04-05 01:02:03")), + cnv.FromTime(parse("2010-05-01 00:02:03")), + cnv.FromTime(parse("2010-05-04 04:04:04")), + cnv.FromTime(parse("2010-05-08 11:12:13")), + cnv.FromTime(parse("2010-06-15 01:02:03")), + cnv.FromTime(parse("2010-07-04 04:04:43")), + cnv.FromTime(parse("2010-07-14 14:17:09")), + cnv.FromTime(parse("2010-08-04 21:22:23")), + cnv.FromTime(parse("2010-08-15 11:12:13")), + cnv.FromTime(parse("2010-09-01 21:52:53")), + cnv.FromTime(parse("2010-10-25 01:19:23")), + cnv.FromTime(parse("2010-11-30 11:32:53")), + cnv.FromTime(parse("2010-12-24 23:59:59")), + cnv.FromTime(parse("2010-12-31 23:59:59")), + cnv.FromTime(parse("2011-01-12 01:02:03")), + ) + + line, pnts, err := hplot.NewLinePoints(data) + if err != nil { + log.Fatalf("could not create plotter: %+v", err) + } + + line.Color = color.RGBA{B: 255, A: 255} + pnts.Shape = draw.CircleGlyph{} + pnts.Color = color.RGBA{R: 255, A: 255} + + p.Add(line, pnts, hplot.NewGrid()) + + err = p.Save(20*vg.Centimeter, 10*vg.Centimeter, "testdata/timeseries_monthly.png") + if err != nil { + log.Fatalf("could not save plot: %+v", err) + } +} + +func ExampleTicks_daily() { + cnv := epok.UTCUnixTimeConverter{} + + p := hplot.New() + p.Title.Text = "Time series (daily)" + p.Y.Label.Text = "Goroutines" + + p.Y.Min = 0 + p.Y.Max = 4 + p.X.AutoRescale = true + p.X.Tick.Marker = epok.Ticks{ + Ruler: epok.Rules{ + Major: epok.Rule{ + Freq: epok.Daily, + Range: epok.RangeFrom(1, 29, 14), + }, + Minor: epok.Rule{ + Freq: epok.Daily, + Range: epok.RangeFrom(1, 32, 1), + }, + }, + Format: "2006\nJan-02\n15:04:05", + Converter: cnv, + } + + xysFrom := func(vs ...float64) plotter.XYs { + o := make(plotter.XYs, len(vs)) + for i := range o { + o[i].X = vs[i] + o[i].Y = float64(i + 1) + } + return o + } + data := xysFrom( + cnv.FromTime(parse("2020-01-01 01:02:03")), + cnv.FromTime(parse("2020-01-02 02:03:04")), + cnv.FromTime(parse("2020-01-12 03:04:05")), + cnv.FromTime(parse("2020-01-22 04:05:06")), + cnv.FromTime(parse("2020-02-03 05:06:07")), + cnv.FromTime(parse("2020-02-13 06:07:08")), + cnv.FromTime(parse("2020-02-23 07:08:09")), + cnv.FromTime(parse("2020-03-01 00:00:00")), + ) + + line, pnts, err := hplot.NewLinePoints(data) + if err != nil { + log.Fatalf("could not create plotter: %+v", err) + } + + line.Color = color.RGBA{B: 255, A: 255} + pnts.Shape = draw.CircleGlyph{} + pnts.Color = color.RGBA{R: 255, A: 255} + + p.Add(line, pnts, hplot.NewGrid()) + + err = p.Save(20*vg.Centimeter, 10*vg.Centimeter, "testdata/timeseries_daily.png") + if err != nil { + log.Fatalf("could not save plot: %+v", err) + } +} + +func parse(vs ...string) time.Time { + format := "2006-01-02 15:04:05" + if len(vs) > 1 { + format = vs[1] + } + t, err := time.Parse(format, vs[0]) + if err != nil { + panic(err) + } + return t +} diff --git a/hplot/ticks_test.go b/hplot/ticks_test.go index afedfac7e..8aa595b61 100644 --- a/hplot/ticks_test.go +++ b/hplot/ticks_test.go @@ -13,3 +13,15 @@ import ( func TestTicks(t *testing.T) { checkPlot(cmpimg.CheckPlot)(ExampleTicks, t, "ticks.png") } + +func TestTicksTimeYearly(t *testing.T) { + checkPlot(cmpimg.CheckPlot)(ExampleTicks_yearly, t, "timeseries_yearly.png") +} + +func TestTicksTimeMonthly(t *testing.T) { + checkPlot(cmpimg.CheckPlot)(ExampleTicks_monthly, t, "timeseries_monthly.png") +} + +func TestTicksTimeDaily(t *testing.T) { + checkPlot(cmpimg.CheckPlot)(ExampleTicks_daily, t, "timeseries_daily.png") +}