From c3a066aecd488d7e534afcf57469d0767b83ac50 Mon Sep 17 00:00:00 2001 From: Will Charczuk Date: Sun, 31 Jul 2016 16:54:09 -0700 Subject: [PATCH] tweaks to make ticks not be terrible --- annotation_series.go | 4 +- axis.go | 15 ++-- bollinger_band_series.go | 4 +- continuous_series.go | 4 +- date/util.go => date.go | 155 ++++++++++++++++++++-------------- date/util_test.go | 98 --------------------- date_test.go | 122 ++++++++++++++++++++++++++ ema_series.go | 4 +- examples/market_hours/main.go | 45 ++++++++++ histogram_series.go | 4 +- linear_regression_series.go | 4 +- macd_series.go | 12 +-- market_hours_range.go | 84 ++++++++++++------ market_hours_range_test.go | 35 ++++---- sequence.go | 71 ++++++++++++++++ sequence_test.go | 9 ++ series.go | 2 +- sma_series.go | 4 +- style.go | 19 +++-- text.go | 40 ++++----- tick.go | 2 +- time_series.go | 4 +- xaxis.go | 6 +- yaxis.go | 4 +- 24 files changed, 480 insertions(+), 271 deletions(-) rename date/util.go => date.go (62%) delete mode 100644 date/util_test.go create mode 100644 date_test.go create mode 100644 examples/market_hours/main.go diff --git a/annotation_series.go b/annotation_series.go index 2ea3761..01d8569 100644 --- a/annotation_series.go +++ b/annotation_series.go @@ -6,7 +6,7 @@ import "math" type AnnotationSeries struct { Name string Style Style - YAxis yAxisType + YAxis YAxisType Annotations []Value2 } @@ -21,7 +21,7 @@ func (as AnnotationSeries) GetStyle() Style { } // GetYAxis returns which YAxis the series draws on. -func (as AnnotationSeries) GetYAxis() yAxisType { +func (as AnnotationSeries) GetYAxis() YAxisType { return as.YAxis } diff --git a/axis.go b/axis.go index e607cea..6fd0a60 100644 --- a/axis.go +++ b/axis.go @@ -1,24 +1,25 @@ package chart -type tickPosition int +// TickPosition is an enumeration of possible tick drawing positions. +type TickPosition int const ( // TickPositionUnset means to use the default tick position. - TickPositionUnset tickPosition = 0 + TickPositionUnset TickPosition = 0 // TickPositionBetweenTicks draws the labels for a tick between the previous and current tick. - TickPositionBetweenTicks tickPosition = 1 + TickPositionBetweenTicks TickPosition = 1 // TickPositionUnderTick draws the tick below the tick. - TickPositionUnderTick tickPosition = 2 + TickPositionUnderTick TickPosition = 2 ) // YAxisType is a type of y-axis; it can either be primary or secondary. -type yAxisType int +type YAxisType int const ( // YAxisPrimary is the primary axis. - YAxisPrimary yAxisType = 0 + YAxisPrimary YAxisType = 0 // YAxisSecondary is the secondary axis. - YAxisSecondary yAxisType = 1 + YAxisSecondary YAxisType = 1 ) // Axis is a chart feature detailing what values happen where. diff --git a/bollinger_band_series.go b/bollinger_band_series.go index 6981d11..f74b489 100644 --- a/bollinger_band_series.go +++ b/bollinger_band_series.go @@ -7,7 +7,7 @@ import "math" type BollingerBandsSeries struct { Name string Style Style - YAxis yAxisType + YAxis YAxisType Period int K float64 @@ -27,7 +27,7 @@ func (bbs BollingerBandsSeries) GetStyle() Style { } // GetYAxis returns which YAxis the series draws on. -func (bbs BollingerBandsSeries) GetYAxis() yAxisType { +func (bbs BollingerBandsSeries) GetYAxis() YAxisType { return bbs.YAxis } diff --git a/continuous_series.go b/continuous_series.go index f3a00cf..fe8d068 100644 --- a/continuous_series.go +++ b/continuous_series.go @@ -5,7 +5,7 @@ type ContinuousSeries struct { Name string Style Style - YAxis yAxisType + YAxis YAxisType XValues []float64 YValues []float64 @@ -44,7 +44,7 @@ func (cs ContinuousSeries) GetValueFormatters() (x, y ValueFormatter) { } // GetYAxis returns which YAxis the series draws on. -func (cs ContinuousSeries) GetYAxis() yAxisType { +func (cs ContinuousSeries) GetYAxis() YAxisType { return cs.YAxis } diff --git a/date/util.go b/date.go similarity index 62% rename from date/util.go rename to date.go index 48fb5a4..2611ea3 100644 --- a/date/util.go +++ b/date.go @@ -1,4 +1,4 @@ -package date +package chart import ( "sync" @@ -52,33 +52,40 @@ var ( var ( // NYSEOpen is when the NYSE opens. - NYSEOpen = ClockTime(9, 30, 0, 0, Eastern()) + NYSEOpen = Date.ClockTime(9, 30, 0, 0, Date.Eastern()) // NYSEClose is when the NYSE closes. - NYSEClose = ClockTime(16, 0, 0, 0, Eastern()) + NYSEClose = Date.ClockTime(16, 0, 0, 0, Date.Eastern()) // NASDAQOpen is when NASDAQ opens. - NASDAQOpen = ClockTime(9, 30, 0, 0, Eastern()) + NASDAQOpen = Date.ClockTime(9, 30, 0, 0, Date.Eastern()) // NASDAQClose is when NASDAQ closes. - NASDAQClose = ClockTime(16, 0, 0, 0, Eastern()) + NASDAQClose = Date.ClockTime(16, 0, 0, 0, Date.Eastern()) // NYSEArcaOpen is when NYSEARCA opens. - NYSEArcaOpen = ClockTime(4, 0, 0, 0, Eastern()) + NYSEArcaOpen = Date.ClockTime(4, 0, 0, 0, Date.Eastern()) // NYSEArcaClose is when NYSEARCA closes. - NYSEArcaClose = ClockTime(20, 0, 0, 0, Eastern()) + NYSEArcaClose = Date.ClockTime(20, 0, 0, 0, Date.Eastern()) ) // HolidayProvider is a function that returns if a given time falls on a holiday. type HolidayProvider func(time.Time) bool -// DefaultHolidayProvider implements `HolidayProvider` and just returns false. -func DefaultHolidayProvider(_ time.Time) bool { return false } +// defaultHolidayProvider implements `HolidayProvider` and just returns false. +func defaultHolidayProvider(_ time.Time) bool { return false } + +var ( + // Date contains utility functions that operate on dates. + Date = &date{} +) + +type date struct{} // IsNYSEHoliday returns if a date was/is on a nyse holiday day. -func IsNYSEHoliday(t time.Time) bool { - te := t.In(Eastern()) +func (d date) IsNYSEHoliday(t time.Time) bool { + te := t.In(d.Eastern()) if te.Year() == 2013 { if te.Month() == 1 { return te.Day() == 1 || te.Day() == 21 @@ -192,17 +199,17 @@ func IsNYSEHoliday(t time.Time) bool { } // IsNYSEArcaHoliday returns that returns if a given time falls on a holiday. -func IsNYSEArcaHoliday(t time.Time) bool { - return IsNYSEHoliday(t) +func (d date) IsNYSEArcaHoliday(t time.Time) bool { + return d.IsNYSEHoliday(t) } // IsNASDAQHoliday returns if a date was a NASDAQ holiday day. -func IsNASDAQHoliday(t time.Time) bool { - return IsNYSEHoliday(t) +func (d date) IsNASDAQHoliday(t time.Time) bool { + return d.IsNYSEHoliday(t) } // Eastern returns the eastern timezone. -func Eastern() *time.Location { +func (d date) Eastern() *time.Location { if _eastern == nil { _easternLock.Lock() defer _easternLock.Unlock() @@ -214,32 +221,37 @@ func Eastern() *time.Location { } // ClockTime returns a new time.Time for the given clock components. -func ClockTime(hour, min, sec, nsec int, loc *time.Location) time.Time { +func (d date) ClockTime(hour, min, sec, nsec int, loc *time.Location) time.Time { return time.Date(0, 0, 0, hour, min, sec, nsec, loc) } // On returns the clock components of clock (hour,minute,second) on the date components of d. -func On(clock, d time.Time) time.Time { - return time.Date(d.Year(), d.Month(), d.Day(), clock.Hour(), clock.Minute(), clock.Second(), clock.Nanosecond(), clock.Location()) +func (d date) On(clock, cd time.Time) time.Time { + return time.Date(cd.Year(), cd.Month(), cd.Day(), clock.Hour(), clock.Minute(), clock.Second(), clock.Nanosecond(), clock.Location()) +} + +// NoonOn is a shortcut for On(ClockTime(12,0,0), cd) a.k.a. noon on a given date. +func (d date) NoonOn(cd time.Time) time.Time { + return time.Date(cd.Year(), cd.Month(), cd.Day(), 12, 0, 0, 0, cd.Location()) } // Optional returns a pointer reference to a given time. -func Optional(t time.Time) *time.Time { +func (d date) Optional(t time.Time) *time.Time { return &t } // IsWeekDay returns if the day is a monday->friday. -func IsWeekDay(day time.Weekday) bool { - return !IsWeekendDay(day) +func (d date) IsWeekDay(day time.Weekday) bool { + return !d.IsWeekendDay(day) } // IsWeekendDay returns if the day is a monday->friday. -func IsWeekendDay(day time.Weekday) bool { +func (d date) IsWeekendDay(day time.Weekday) bool { return day == time.Saturday || day == time.Sunday } -// BeforeDate returns if a timestamp is strictly before another date (ignoring hours, minutes etc.) -func BeforeDate(before, reference time.Time) bool { +// Before returns if a timestamp is strictly before another date (ignoring hours, minutes etc.) +func (d date) Before(before, reference time.Time) bool { if before.Year() < reference.Year() { return true } @@ -250,41 +262,40 @@ func BeforeDate(before, reference time.Time) bool { } // NextMarketOpen returns the next market open after a given time. -func NextMarketOpen(after, openTime time.Time, isHoliday HolidayProvider) time.Time { - afterEastern := after.In(Eastern()) - todaysOpen := On(openTime, afterEastern) +func (d date) NextMarketOpen(after, openTime time.Time, isHoliday HolidayProvider) time.Time { + afterEastern := after.In(d.Eastern()) + todaysOpen := d.On(openTime, afterEastern) if isHoliday == nil { - isHoliday = DefaultHolidayProvider + isHoliday = defaultHolidayProvider } - if afterEastern.Before(todaysOpen) && IsWeekDay(todaysOpen.Weekday()) && !isHoliday(todaysOpen) { + todayIsValidTradingDay := d.IsWeekDay(todaysOpen.Weekday()) && !isHoliday(todaysOpen) + + if (afterEastern.Equal(todaysOpen) || afterEastern.Before(todaysOpen)) && todayIsValidTradingDay { return todaysOpen } - if afterEastern.Equal(todaysOpen) { //rare but it might happen. - return todaysOpen - } - - for cursorDay := 1; cursorDay < 6; cursorDay++ { + for cursorDay := 1; cursorDay < 7; cursorDay++ { newDay := todaysOpen.AddDate(0, 0, cursorDay) - if IsWeekDay(newDay.Weekday()) && !isHoliday(afterEastern) { - return On(openTime, newDay) + isValidTradingDay := d.IsWeekDay(newDay.Weekday()) && !isHoliday(newDay) + if isValidTradingDay { + return d.On(openTime, newDay) } } - return Epoch //we should never reach this. + panic("Have exhausted day window looking for next market open.") } // NextMarketClose returns the next market close after a given time. -func NextMarketClose(after, closeTime time.Time, isHoliday HolidayProvider) time.Time { - afterEastern := after.In(Eastern()) +func (d date) NextMarketClose(after, closeTime time.Time, isHoliday HolidayProvider) time.Time { + afterEastern := after.In(d.Eastern()) if isHoliday == nil { - isHoliday = DefaultHolidayProvider + isHoliday = defaultHolidayProvider } - todaysClose := On(closeTime, afterEastern) - if afterEastern.Before(todaysClose) && IsWeekDay(todaysClose.Weekday()) && !isHoliday(todaysClose) { + todaysClose := d.On(closeTime, afterEastern) + if afterEastern.Before(todaysClose) && d.IsWeekDay(todaysClose.Weekday()) && !isHoliday(todaysClose) { return todaysClose } @@ -294,22 +305,22 @@ func NextMarketClose(after, closeTime time.Time, isHoliday HolidayProvider) time for cursorDay := 1; cursorDay < 6; cursorDay++ { newDay := todaysClose.AddDate(0, 0, cursorDay) - if IsWeekDay(newDay.Weekday()) && !isHoliday(newDay) { - return On(closeTime, newDay) + if d.IsWeekDay(newDay.Weekday()) && !isHoliday(newDay) { + return d.On(closeTime, newDay) } } - return Epoch //we should never reach this. + panic("Have exhausted day window looking for next market close.") } // CalculateMarketSecondsBetween calculates the number of seconds the market was open between two dates. -func CalculateMarketSecondsBetween(start, end, marketOpen, marketClose time.Time, isHoliday HolidayProvider) (seconds int64) { - startEastern := start.In(Eastern()) - endEastern := end.In(Eastern()) +func (d date) CalculateMarketSecondsBetween(start, end, marketOpen, marketClose time.Time, isHoliday HolidayProvider) (seconds int64) { + startEastern := start.In(d.Eastern()) + endEastern := end.In(d.Eastern()) - startMarketOpen := On(marketOpen, startEastern) - startMarketClose := On(marketClose, startEastern) + startMarketOpen := d.On(marketOpen, startEastern) + startMarketClose := d.On(marketClose, startEastern) - if !IsWeekendDay(startMarketOpen.Weekday()) && !isHoliday(startMarketOpen) { + if !d.IsWeekendDay(startMarketOpen.Weekday()) && !isHoliday(startMarketOpen) { if (startEastern.Equal(startMarketOpen) || startEastern.After(startMarketOpen)) && startEastern.Before(startMarketClose) { if endEastern.Before(startMarketClose) { seconds += int64(endEastern.Sub(startEastern) / time.Second) @@ -319,17 +330,17 @@ func CalculateMarketSecondsBetween(start, end, marketOpen, marketClose time.Time } } - cursor := NextMarketOpen(startMarketClose, marketOpen, isHoliday) - for BeforeDate(cursor, endEastern) { - if IsWeekDay(cursor.Weekday()) && !isHoliday(cursor) { - close := NextMarketClose(cursor, marketClose, isHoliday) + cursor := d.NextMarketOpen(startMarketClose, marketOpen, isHoliday) + for d.Before(cursor, endEastern) { + if d.IsWeekDay(cursor.Weekday()) && !isHoliday(cursor) { + close := d.NextMarketClose(cursor, marketClose, isHoliday) seconds += int64(close.Sub(cursor) / time.Second) } cursor = cursor.AddDate(0, 0, 1) } - finalMarketOpen := NextMarketOpen(cursor, marketOpen, isHoliday) - finalMarketClose := NextMarketClose(cursor, marketClose, isHoliday) + finalMarketOpen := d.NextMarketOpen(cursor, marketOpen, isHoliday) + finalMarketClose := d.NextMarketClose(cursor, marketClose, isHoliday) if endEastern.After(finalMarketOpen) { if endEastern.Before(finalMarketClose) { seconds += int64(endEastern.Sub(finalMarketOpen) / time.Second) @@ -341,13 +352,27 @@ func CalculateMarketSecondsBetween(start, end, marketOpen, marketClose time.Time return } -// Format returns a string representation of a date. -func format(t time.Time) string { - return t.Format("2006-01-02") +const ( + _secondsPerDay = 60 * 60 * 24 +) + +func (d date) Diff(t1, t2 time.Time) (days int64) { + t1n := t1.Unix() + t2n := t2.Unix() + diff := t1n - t2n + return diff / (_secondsPerDay) } -// Parse parses a date from a string. -func parse(str string) time.Time { - res, _ := time.Parse("2006-01-02", str) - return res +// NextDay returns the timestamp advanced a day. +func (d date) NextDay(ts time.Time) time.Time { + return ts.AddDate(0, 0, 1) +} + +// NextHour returns the next timestamp on the hour. +func (d date) NextHour(ts time.Time) time.Time { + //advance a full hour ... + advanced := ts.Add(time.Hour) + minutes := time.Duration(advanced.Minute()) * time.Minute + final := advanced.Add(-minutes) + return time.Date(final.Year(), final.Month(), final.Day(), final.Hour(), 0, 0, 0, final.Location()) } diff --git a/date/util_test.go b/date/util_test.go deleted file mode 100644 index 8538053..0000000 --- a/date/util_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package date - -import ( - "testing" - "time" - - assert "github.com/blendlabs/go-assert" -) - -func TestBeforeDate(t *testing.T) { - assert := assert.New(t) - - assert.True(BeforeDate(parse("2015-07-02"), parse("2016-07-01"))) - assert.True(BeforeDate(parse("2016-06-01"), parse("2016-07-01"))) - assert.True(BeforeDate(parse("2016-07-01"), parse("2016-07-02"))) - - assert.False(BeforeDate(parse("2016-07-01"), parse("2016-07-01"))) - assert.False(BeforeDate(parse("2016-07-03"), parse("2016-07-01"))) - assert.False(BeforeDate(parse("2016-08-03"), parse("2016-07-01"))) - assert.False(BeforeDate(parse("2017-08-03"), parse("2016-07-01"))) -} - -func TestNextMarketOpen(t *testing.T) { - assert := assert.New(t) - - beforeOpen := time.Date(2016, 07, 18, 9, 0, 0, 0, Eastern()) - todayOpen := time.Date(2016, 07, 18, 9, 30, 0, 0, Eastern()) - - afterOpen := time.Date(2016, 07, 18, 9, 31, 0, 0, Eastern()) - tomorrowOpen := time.Date(2016, 07, 19, 9, 30, 0, 0, Eastern()) - - afterFriday := time.Date(2016, 07, 22, 9, 31, 0, 0, Eastern()) - mondayOpen := time.Date(2016, 07, 25, 9, 30, 0, 0, Eastern()) - - weekend := time.Date(2016, 07, 23, 9, 31, 0, 0, Eastern()) - - assert.True(todayOpen.Equal(NextMarketOpen(beforeOpen, NYSEOpen, IsNYSEHoliday))) - assert.True(tomorrowOpen.Equal(NextMarketOpen(afterOpen, NYSEOpen, IsNYSEHoliday))) - assert.True(mondayOpen.Equal(NextMarketOpen(afterFriday, NYSEOpen, IsNYSEHoliday))) - assert.True(mondayOpen.Equal(NextMarketOpen(weekend, NYSEOpen, IsNYSEHoliday))) - - testRegression := time.Date(2016, 07, 18, 16, 0, 0, 0, Eastern()) - shouldbe := time.Date(2016, 07, 19, 9, 30, 0, 0, Eastern()) - - assert.True(shouldbe.Equal(NextMarketOpen(testRegression, NYSEOpen, IsNYSEHoliday))) -} - -func TestNextMarketClose(t *testing.T) { - assert := assert.New(t) - - beforeClose := time.Date(2016, 07, 18, 15, 0, 0, 0, Eastern()) - todayClose := time.Date(2016, 07, 18, 16, 00, 0, 0, Eastern()) - - afterClose := time.Date(2016, 07, 18, 16, 1, 0, 0, Eastern()) - tomorrowClose := time.Date(2016, 07, 19, 16, 00, 0, 0, Eastern()) - - afterFriday := time.Date(2016, 07, 22, 16, 1, 0, 0, Eastern()) - mondayClose := time.Date(2016, 07, 25, 16, 0, 0, 0, Eastern()) - - weekend := time.Date(2016, 07, 23, 9, 31, 0, 0, Eastern()) - - assert.True(todayClose.Equal(NextMarketClose(beforeClose, NYSEClose, IsNYSEHoliday))) - assert.True(tomorrowClose.Equal(NextMarketClose(afterClose, NYSEClose, IsNYSEHoliday))) - assert.True(mondayClose.Equal(NextMarketClose(afterFriday, NYSEClose, IsNYSEHoliday))) - assert.True(mondayClose.Equal(NextMarketClose(weekend, NYSEClose, IsNYSEHoliday))) -} - -func TestCalculateMarketSecondsBetween(t *testing.T) { - assert := assert.New(t) - - start := time.Date(2016, 07, 18, 9, 30, 0, 0, Eastern()) - end := time.Date(2016, 07, 22, 16, 00, 0, 0, Eastern()) - - shouldbe := 5 * 6.5 * 60 * 60 - - assert.Equal(shouldbe, CalculateMarketSecondsBetween(start, end, NYSEOpen, NYSEClose, IsNYSEHoliday)) -} - -func TestCalculateMarketSecondsBetween1D(t *testing.T) { - assert := assert.New(t) - - start := time.Date(2016, 07, 22, 9, 45, 0, 0, Eastern()) - end := time.Date(2016, 07, 22, 15, 45, 0, 0, Eastern()) - - shouldbe := 6 * 60 * 60 - - assert.Equal(shouldbe, CalculateMarketSecondsBetween(start, end, NYSEOpen, NYSEClose, IsNYSEHoliday)) -} - -func TestCalculateMarketSecondsBetweenLTM(t *testing.T) { - assert := assert.New(t) - - start := time.Date(2015, 07, 01, 9, 30, 0, 0, Eastern()) - end := time.Date(2016, 07, 01, 9, 30, 0, 0, Eastern()) - - shouldbe := 253 * 6.5 * 60 * 60 //253 full market days since this date last year. - assert.Equal(shouldbe, CalculateMarketSecondsBetween(start, end, NYSEOpen, NYSEClose, IsNYSEHoliday)) -} diff --git a/date_test.go b/date_test.go new file mode 100644 index 0000000..f2b76ae --- /dev/null +++ b/date_test.go @@ -0,0 +1,122 @@ +package chart + +import ( + "testing" + "time" + + assert "github.com/blendlabs/go-assert" +) + +func parse(v string) time.Time { + ts, _ := time.Parse("2006-01-02", v) + return ts +} + +func TestDateBefore(t *testing.T) { + assert := assert.New(t) + + assert.True(Date.Before(parse("2015-07-02"), parse("2016-07-01"))) + assert.True(Date.Before(parse("2016-06-01"), parse("2016-07-01"))) + assert.True(Date.Before(parse("2016-07-01"), parse("2016-07-02"))) + + assert.False(Date.Before(parse("2016-07-01"), parse("2016-07-01"))) + assert.False(Date.Before(parse("2016-07-03"), parse("2016-07-01"))) + assert.False(Date.Before(parse("2016-08-03"), parse("2016-07-01"))) + assert.False(Date.Before(parse("2017-08-03"), parse("2016-07-01"))) +} + +func TestNextMarketOpen(t *testing.T) { + assert := assert.New(t) + + beforeOpen := time.Date(2016, 07, 18, 9, 0, 0, 0, Date.Eastern()) + todayOpen := time.Date(2016, 07, 18, 9, 30, 0, 0, Date.Eastern()) + + afterOpen := time.Date(2016, 07, 18, 9, 31, 0, 0, Date.Eastern()) + tomorrowOpen := time.Date(2016, 07, 19, 9, 30, 0, 0, Date.Eastern()) + + afterFriday := time.Date(2016, 07, 22, 9, 31, 0, 0, Date.Eastern()) + mondayOpen := time.Date(2016, 07, 25, 9, 30, 0, 0, Date.Eastern()) + + weekend := time.Date(2016, 07, 23, 9, 31, 0, 0, Date.Eastern()) + + assert.True(todayOpen.Equal(Date.NextMarketOpen(beforeOpen, NYSEOpen, Date.IsNYSEHoliday))) + assert.True(tomorrowOpen.Equal(Date.NextMarketOpen(afterOpen, NYSEOpen, Date.IsNYSEHoliday))) + assert.True(mondayOpen.Equal(Date.NextMarketOpen(afterFriday, NYSEOpen, Date.IsNYSEHoliday))) + assert.True(mondayOpen.Equal(Date.NextMarketOpen(weekend, NYSEOpen, Date.IsNYSEHoliday))) + + testRegression := time.Date(2016, 07, 18, 16, 0, 0, 0, Date.Eastern()) + shouldbe := time.Date(2016, 07, 19, 9, 30, 0, 0, Date.Eastern()) + + assert.True(shouldbe.Equal(Date.NextMarketOpen(testRegression, NYSEOpen, Date.IsNYSEHoliday))) +} + +func TestNextMarketClose(t *testing.T) { + assert := assert.New(t) + + beforeClose := time.Date(2016, 07, 18, 15, 0, 0, 0, Date.Eastern()) + todayClose := time.Date(2016, 07, 18, 16, 00, 0, 0, Date.Eastern()) + + afterClose := time.Date(2016, 07, 18, 16, 1, 0, 0, Date.Eastern()) + tomorrowClose := time.Date(2016, 07, 19, 16, 00, 0, 0, Date.Eastern()) + + afterFriday := time.Date(2016, 07, 22, 16, 1, 0, 0, Date.Eastern()) + mondayClose := time.Date(2016, 07, 25, 16, 0, 0, 0, Date.Eastern()) + + weekend := time.Date(2016, 07, 23, 9, 31, 0, 0, Date.Eastern()) + + assert.True(todayClose.Equal(Date.NextMarketClose(beforeClose, NYSEClose, Date.IsNYSEHoliday))) + assert.True(tomorrowClose.Equal(Date.NextMarketClose(afterClose, NYSEClose, Date.IsNYSEHoliday))) + assert.True(mondayClose.Equal(Date.NextMarketClose(afterFriday, NYSEClose, Date.IsNYSEHoliday))) + assert.True(mondayClose.Equal(Date.NextMarketClose(weekend, NYSEClose, Date.IsNYSEHoliday))) +} + +func TestCalculateMarketSecondsBetween(t *testing.T) { + assert := assert.New(t) + + start := time.Date(2016, 07, 18, 9, 30, 0, 0, Date.Eastern()) + end := time.Date(2016, 07, 22, 16, 00, 0, 0, Date.Eastern()) + + shouldbe := 5 * 6.5 * 60 * 60 + + assert.Equal(shouldbe, Date.CalculateMarketSecondsBetween(start, end, NYSEOpen, NYSEClose, Date.IsNYSEHoliday)) +} + +func TestCalculateMarketSecondsBetween1D(t *testing.T) { + assert := assert.New(t) + + start := time.Date(2016, 07, 22, 9, 45, 0, 0, Date.Eastern()) + end := time.Date(2016, 07, 22, 15, 45, 0, 0, Date.Eastern()) + + shouldbe := 6 * 60 * 60 + + assert.Equal(shouldbe, Date.CalculateMarketSecondsBetween(start, end, NYSEOpen, NYSEClose, Date.IsNYSEHoliday)) +} + +func TestCalculateMarketSecondsBetweenLTM(t *testing.T) { + assert := assert.New(t) + + start := time.Date(2015, 07, 01, 9, 30, 0, 0, Date.Eastern()) + end := time.Date(2016, 07, 01, 9, 30, 0, 0, Date.Eastern()) + + shouldbe := 253 * 6.5 * 60 * 60 //253 full market days since this date last year. + assert.Equal(shouldbe, Date.CalculateMarketSecondsBetween(start, end, NYSEOpen, NYSEClose, Date.IsNYSEHoliday)) +} + +func TestDateNextHour(t *testing.T) { + assert := assert.New(t) + + start := time.Date(2015, 07, 01, 9, 30, 0, 0, Date.Eastern()) + next := Date.NextHour(start) + assert.Equal(2015, next.Year()) + assert.Equal(07, next.Month()) + assert.Equal(01, next.Day()) + assert.Equal(10, next.Hour()) + assert.Equal(00, next.Minute()) + + next = Date.NextHour(next) + assert.Equal(11, next.Hour()) + + next = Date.NextHour(next) + assert.Equal(12, next.Hour()) + +} diff --git a/ema_series.go b/ema_series.go index 991359f..affadc1 100644 --- a/ema_series.go +++ b/ema_series.go @@ -9,7 +9,7 @@ const ( type EMASeries struct { Name string Style Style - YAxis yAxisType + YAxis YAxisType Period int InnerSeries ValueProvider @@ -28,7 +28,7 @@ func (ema EMASeries) GetStyle() Style { } // GetYAxis returns which YAxis the series draws on. -func (ema EMASeries) GetYAxis() yAxisType { +func (ema EMASeries) GetYAxis() YAxisType { return ema.YAxis } diff --git a/examples/market_hours/main.go b/examples/market_hours/main.go new file mode 100644 index 0000000..e168dea --- /dev/null +++ b/examples/market_hours/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "net/http" + "time" + + "github.com/wcharczuk/go-chart" +) + +func drawChart(res http.ResponseWriter, req *http.Request) { + start := time.Date(2016, 07, 04, 12, 0, 0, 0, chart.Date.Eastern()) + end := time.Date(2016, 07, 06, 12, 0, 0, 0, chart.Date.Eastern()) + xv := chart.Sequence.MarketHours(start, end, chart.NYSEOpen, chart.NYSEClose, chart.Date.IsNYSEHoliday) + yv := chart.Sequence.RandomWithAverage(len(xv), 200, 10) + + graph := chart.Chart{ + XAxis: chart.XAxis{ + Style: chart.StyleShow(), + TickPosition: chart.TickPositionBetweenTicks, + ValueFormatter: chart.TimeHourValueFormatter, + Range: &chart.MarketHoursRange{ + MarketOpen: chart.NYSEOpen, + MarketClose: chart.NYSEClose, + HolidayProvider: chart.Date.IsNYSEHoliday, + }, + }, + YAxis: chart.YAxis{ + Style: chart.StyleShow(), + }, + Series: []chart.Series{ + chart.TimeSeries{ + XValues: xv, + YValues: yv, + }, + }, + } + + res.Header().Set("Content-Type", "image/png") + graph.Render(chart.PNG, res) +} + +func main() { + http.HandleFunc("/", drawChart) + http.ListenAndServe(":8080", nil) +} diff --git a/histogram_series.go b/histogram_series.go index 351fe94..0542c1a 100644 --- a/histogram_series.go +++ b/histogram_series.go @@ -6,7 +6,7 @@ package chart type HistogramSeries struct { Name string Style Style - YAxis yAxisType + YAxis YAxisType InnerSeries ValueProvider } @@ -21,7 +21,7 @@ func (hs HistogramSeries) GetStyle() Style { } // GetYAxis returns which yaxis the series is mapped to. -func (hs HistogramSeries) GetYAxis() yAxisType { +func (hs HistogramSeries) GetYAxis() YAxisType { return hs.YAxis } diff --git a/linear_regression_series.go b/linear_regression_series.go index 06c8c4e..a33a0b1 100644 --- a/linear_regression_series.go +++ b/linear_regression_series.go @@ -5,7 +5,7 @@ package chart type LinearRegressionSeries struct { Name string Style Style - YAxis yAxisType + YAxis YAxisType Window int Offset int @@ -28,7 +28,7 @@ func (lrs LinearRegressionSeries) GetStyle() Style { } // GetYAxis returns which YAxis the series draws on. -func (lrs LinearRegressionSeries) GetYAxis() yAxisType { +func (lrs LinearRegressionSeries) GetYAxis() YAxisType { return lrs.YAxis } diff --git a/macd_series.go b/macd_series.go index d0aa51c..b3b80c0 100644 --- a/macd_series.go +++ b/macd_series.go @@ -14,7 +14,7 @@ const ( type MACDSeries struct { Name string Style Style - YAxis yAxisType + YAxis YAxisType InnerSeries ValueProvider PrimaryPeriod int @@ -56,7 +56,7 @@ func (macd MACDSeries) GetStyle() Style { } // GetYAxis returns which YAxis the series draws on. -func (macd MACDSeries) GetYAxis() yAxisType { +func (macd MACDSeries) GetYAxis() YAxisType { return macd.YAxis } @@ -109,7 +109,7 @@ func (macd *MACDSeries) ensureChildSeries() { type MACDSignalSeries struct { Name string Style Style - YAxis yAxisType + YAxis YAxisType InnerSeries ValueProvider PrimaryPeriod int @@ -150,7 +150,7 @@ func (macds MACDSignalSeries) GetStyle() Style { } // GetYAxis returns which YAxis the series draws on. -func (macds MACDSignalSeries) GetYAxis() yAxisType { +func (macds MACDSignalSeries) GetYAxis() YAxisType { return macds.YAxis } @@ -200,7 +200,7 @@ func (macds *MACDSignalSeries) Render(r Renderer, canvasBox Box, xrange, yrange type MACDLineSeries struct { Name string Style Style - YAxis yAxisType + YAxis YAxisType InnerSeries ValueProvider PrimaryPeriod int @@ -223,7 +223,7 @@ func (macdl MACDLineSeries) GetStyle() Style { } // GetYAxis returns which YAxis the series draws on. -func (macdl MACDLineSeries) GetYAxis() yAxisType { +func (macdl MACDLineSeries) GetYAxis() YAxisType { return macdl.YAxis } diff --git a/market_hours_range.go b/market_hours_range.go index 7ad2e52..19a29d6 100644 --- a/market_hours_range.go +++ b/market_hours_range.go @@ -3,8 +3,6 @@ package chart import ( "fmt" "time" - - "github.com/wcharczuk/go-chart/date" ) // MarketHoursRange is a special type of range that compresses a time range into just the @@ -16,7 +14,7 @@ type MarketHoursRange struct { MarketOpen time.Time MarketClose time.Time - HolidayProvider date.HolidayProvider + HolidayProvider HolidayProvider ValueFormatter ValueFormatter @@ -40,7 +38,7 @@ func (mhr MarketHoursRange) GetMax() float64 { // GetEffectiveMax gets either the close on the max, or the max itself. func (mhr MarketHoursRange) GetEffectiveMax() time.Time { - maxClose := date.On(mhr.MarketClose, mhr.Max) + maxClose := Date.On(mhr.MarketClose, mhr.Max) if maxClose.After(mhr.Max) { return maxClose } @@ -75,40 +73,70 @@ func (mhr *MarketHoursRange) SetDomain(domain int) { } // GetHolidayProvider coalesces a userprovided holiday provider and the date.DefaultHolidayProvider. -func (mhr MarketHoursRange) GetHolidayProvider() date.HolidayProvider { +func (mhr MarketHoursRange) GetHolidayProvider() HolidayProvider { if mhr.HolidayProvider == nil { - return date.DefaultHolidayProvider + return defaultHolidayProvider } return mhr.HolidayProvider } +// GetMarketOpen returns the market open time. +func (mhr MarketHoursRange) GetMarketOpen() time.Time { + if mhr.MarketOpen.IsZero() { + return NYSEOpen + } + return mhr.MarketOpen +} + +// GetMarketClose returns the market close time. +func (mhr MarketHoursRange) GetMarketClose() time.Time { + if mhr.MarketClose.IsZero() { + return NYSEClose + } + return mhr.MarketClose +} + // GetTicks returns the ticks for the range. // This is to override the default continous ticks that would be generated for the range. -func (mhr *MarketHoursRange) GetTicks(vf ValueFormatter) []Tick { - var ticks []Tick +func (mhr *MarketHoursRange) GetTicks(r Renderer, defaults Style, vf ValueFormatter) []Tick { + println("GetTicks() domain:", mhr.Domain) + times := Sequence.MarketHours(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) + timesWidth := mhr.measureTimes(r, defaults, vf, times) + if timesWidth <= mhr.Domain { + return mhr.makeTicks(vf, times) + } + times = Sequence.MarketHourQuarters(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) + timesWidth = mhr.measureTimes(r, defaults, vf, times) + if timesWidth <= mhr.Domain { + return mhr.makeTicks(vf, times) + } + return mhr.makeTicks(vf, Sequence.MarketDayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())) +} - cursor := date.On(mhr.MarketClose, mhr.Min) - maxClose := date.On(mhr.MarketClose, mhr.Max) +func (mhr *MarketHoursRange) measureTimes(r Renderer, defaults Style, vf ValueFormatter, times []time.Time) int { + defaults.GetTextOptions().WriteToRenderer(r) + var total int + for index, t := range times { + timeLabel := vf(t) - for date.BeforeDate(cursor, maxClose) { - if date.IsWeekDay(cursor.Weekday()) && !mhr.GetHolidayProvider()(cursor) { - ticks = append(ticks, Tick{ - Value: TimeToFloat64(cursor), - Label: vf(cursor), - }) + labelBox := r.MeasureText(timeLabel) + total += labelBox.Width() + if index > 0 { + total += DefaultMinimumTickHorizontalSpacing } - - cursor = cursor.AddDate(0, 0, 1) } + return total +} - endMarketClose := date.On(mhr.MarketClose, cursor) - if date.IsWeekDay(endMarketClose.Weekday()) && !mhr.GetHolidayProvider()(endMarketClose) { - ticks = append(ticks, Tick{ - Value: TimeToFloat64(endMarketClose), - Label: vf(endMarketClose), - }) +func (mhr *MarketHoursRange) makeTicks(vf ValueFormatter, times []time.Time) []Tick { + ticks := make([]Tick, len(times)) + for index, t := range times { + ticks[index] = Tick{ + Value: TimeToFloat64(t), + Label: vf(t), + } + println("make tick =>", vf(t)) } - return ticks } @@ -119,9 +147,9 @@ func (mhr MarketHoursRange) String() string { // Translate maps a given value into the ContinuousRange space. func (mhr MarketHoursRange) Translate(value float64) int { valueTime := Float64ToTime(value) - valueTimeEastern := valueTime.In(date.Eastern()) - totalSeconds := date.CalculateMarketSecondsBetween(mhr.Min, mhr.GetEffectiveMax(), mhr.MarketOpen, mhr.MarketClose, mhr.HolidayProvider) - valueDelta := date.CalculateMarketSecondsBetween(mhr.Min, valueTimeEastern, mhr.MarketOpen, mhr.MarketClose, mhr.HolidayProvider) + valueTimeEastern := valueTime.In(Date.Eastern()) + totalSeconds := Date.CalculateMarketSecondsBetween(mhr.Min, mhr.GetEffectiveMax(), mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.HolidayProvider) + valueDelta := Date.CalculateMarketSecondsBetween(mhr.Min, valueTimeEastern, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.HolidayProvider) translated := int((float64(valueDelta) / float64(totalSeconds)) * float64(mhr.Domain)) return translated } diff --git a/market_hours_range_test.go b/market_hours_range_test.go index 8698b36..7c66589 100644 --- a/market_hours_range_test.go +++ b/market_hours_range_test.go @@ -5,18 +5,17 @@ import ( "time" assert "github.com/blendlabs/go-assert" - "github.com/wcharczuk/go-chart/date" ) func TestMarketHoursRangeGetDelta(t *testing.T) { assert := assert.New(t) r := &MarketHoursRange{ - Min: time.Date(2016, 07, 19, 9, 30, 0, 0, date.Eastern()), - Max: time.Date(2016, 07, 22, 16, 00, 0, 0, date.Eastern()), - MarketOpen: date.NYSEOpen, - MarketClose: date.NYSEClose, - HolidayProvider: date.IsNYSEHoliday, + Min: time.Date(2016, 07, 19, 9, 30, 0, 0, Date.Eastern()), + Max: time.Date(2016, 07, 22, 16, 00, 0, 0, Date.Eastern()), + MarketOpen: NYSEOpen, + MarketClose: NYSEClose, + HolidayProvider: Date.IsNYSEHoliday, } assert.NotZero(r.GetDelta()) @@ -26,15 +25,15 @@ func TestMarketHoursRangeTranslate(t *testing.T) { assert := assert.New(t) r := &MarketHoursRange{ - Min: time.Date(2016, 07, 18, 9, 30, 0, 0, date.Eastern()), - Max: time.Date(2016, 07, 22, 16, 00, 0, 0, date.Eastern()), - MarketOpen: date.NYSEOpen, - MarketClose: date.NYSEClose, - HolidayProvider: date.IsNYSEHoliday, + Min: time.Date(2016, 07, 18, 9, 30, 0, 0, Date.Eastern()), + Max: time.Date(2016, 07, 22, 16, 00, 0, 0, Date.Eastern()), + MarketOpen: NYSEOpen, + MarketClose: NYSEClose, + HolidayProvider: Date.IsNYSEHoliday, Domain: 1000, } - weds := time.Date(2016, 07, 20, 9, 30, 0, 0, date.Eastern()) + weds := time.Date(2016, 07, 20, 9, 30, 0, 0, Date.Eastern()) assert.Equal(0, r.Translate(TimeToFloat64(r.Min))) assert.Equal(400, r.Translate(TimeToFloat64(weds))) @@ -45,17 +44,17 @@ func TestMarketHoursRangeGetTicks(t *testing.T) { assert := assert.New(t) r := &MarketHoursRange{ - Min: time.Date(2016, 07, 18, 9, 30, 0, 0, date.Eastern()), - Max: time.Date(2016, 07, 22, 16, 00, 0, 0, date.Eastern()), - MarketOpen: date.NYSEOpen, - MarketClose: date.NYSEClose, - HolidayProvider: date.IsNYSEHoliday, + Min: time.Date(2016, 07, 18, 9, 30, 0, 0, Date.Eastern()), + Max: time.Date(2016, 07, 22, 16, 00, 0, 0, Date.Eastern()), + MarketOpen: NYSEOpen, + MarketClose: NYSEClose, + HolidayProvider: Date.IsNYSEHoliday, Domain: 1000, } ticks := r.GetTicks(TimeValueFormatter) assert.NotEmpty(ticks) - assert.Len(ticks, 5) + assert.Len(ticks, 24) assert.NotEqual(TimeToFloat64(r.Min), ticks[0].Value) assert.NotEmpty(ticks[0].Label) } diff --git a/sequence.go b/sequence.go index c04ec4d..461aa4d 100644 --- a/sequence.go +++ b/sequence.go @@ -45,6 +45,19 @@ func (s sequence) Random(samples int, scale float64) []float64 { return values } +// Random generates a fixed length sequence of random values with a given average, above and below that average by (-scale, scale) +func (s sequence) RandomWithAverage(samples int, average, scale float64) []float64 { + rnd := rand.New(rand.NewSource(time.Now().Unix())) + values := make([]float64, samples) + + for x := 0; x < samples; x++ { + jitter := scale - (rnd.Float64() * (2 * scale)) + values[x] = average + jitter + } + + return values +} + // Days generates a sequence of timestamps by day, from -days to today. func (s sequence) Days(days int) []time.Time { var values []time.Time @@ -53,3 +66,61 @@ func (s sequence) Days(days int) []time.Time { } return values } + +func (s sequence) MarketHours(from, to time.Time, marketOpen, marketClose time.Time, isHoliday HolidayProvider) []time.Time { + var times []time.Time + cursor := Date.On(marketOpen, from) + toClose := Date.On(marketClose, to) + for cursor.Before(toClose) || cursor.Equal(toClose) { + todayOpen := Date.On(marketOpen, cursor) + todayClose := Date.On(marketClose, cursor) + isValidTradingDay := !isHoliday(cursor) && Date.IsWeekDay(cursor.Weekday()) + + if (cursor.Equal(todayOpen) || cursor.After(todayOpen)) && (cursor.Equal(todayClose) || cursor.Before(todayClose)) && isValidTradingDay { + times = append(times, cursor) + } + if cursor.After(todayClose) { + cursor = Date.NextMarketOpen(cursor, marketOpen, isHoliday) + } else { + cursor = Date.NextHour(cursor) + } + } + return times +} + +func (s sequence) MarketHourQuarters(from, to time.Time, marketOpen, marketClose time.Time, isHoliday HolidayProvider) []time.Time { + var times []time.Time + cursor := Date.On(marketOpen, from) + toClose := Date.On(marketClose, to) + for cursor.Before(toClose) || cursor.Equal(toClose) { + + isValidTradingDay := !isHoliday(cursor) && Date.IsWeekDay(cursor.Weekday()) + + if isValidTradingDay { + todayOpen := Date.On(marketOpen, cursor) + todayNoon := Date.NoonOn(cursor) + today2pm := Date.On(Date.ClockTime(2, 0, 0, 0, cursor.Location()), cursor) + todayClose := Date.On(marketClose, cursor) + times = append(times, todayOpen, todayNoon, today2pm, todayClose) + } + + cursor = Date.NextDay(cursor) + } + return times +} + +func (s sequence) MarketDayCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday HolidayProvider) []time.Time { + var times []time.Time + cursor := Date.On(marketOpen, from) + toClose := Date.On(marketClose, to) + for cursor.Before(toClose) || cursor.Equal(toClose) { + isValidTradingDay := !isHoliday(cursor) && Date.IsWeekDay(cursor.Weekday()) + if isValidTradingDay { + todayClose := Date.On(marketClose, cursor) + times = append(times, todayClose) + } + + cursor = Date.NextDay(cursor) + } + return times +} diff --git a/sequence_test.go b/sequence_test.go index 91e4965..b39b3e3 100644 --- a/sequence_test.go +++ b/sequence_test.go @@ -2,6 +2,7 @@ package chart import ( "testing" + "time" assert "github.com/blendlabs/go-assert" ) @@ -15,3 +16,11 @@ func TestSequenceFloat64(t *testing.T) { desc := Sequence.Float64(10.0, 1.0) assert.Len(desc, 10) } + +func TestSequenceMarketHours(t *testing.T) { + assert := assert.New(t) + + today := time.Date(2016, 07, 01, 12, 0, 0, 0, Date.Eastern()) + mh := Sequence.MarketHours(today, today, NYSEOpen, NYSEClose, Date.IsNYSEHoliday) + assert.Len(mh, 7) +} diff --git a/series.go b/series.go index 879f24b..6145dcb 100644 --- a/series.go +++ b/series.go @@ -3,7 +3,7 @@ package chart // Series is an alias to Renderable. type Series interface { GetName() string - GetYAxis() yAxisType + GetYAxis() YAxisType GetStyle() Style Render(r Renderer, canvasBox Box, xrange, yrange Range, s Style) } diff --git a/sma_series.go b/sma_series.go index dd8aed9..a7e0034 100644 --- a/sma_series.go +++ b/sma_series.go @@ -9,7 +9,7 @@ const ( type SMASeries struct { Name string Style Style - YAxis yAxisType + YAxis YAxisType Period int InnerSeries ValueProvider @@ -26,7 +26,7 @@ func (sma SMASeries) GetStyle() Style { } // GetYAxis returns which YAxis the series draws on. -func (sma SMASeries) GetYAxis() yAxisType { +func (sma SMASeries) GetYAxis() YAxisType { return sma.YAxis } diff --git a/style.go b/style.go index a9eb1c5..9563066 100644 --- a/style.go +++ b/style.go @@ -8,6 +8,13 @@ import ( "github.com/wcharczuk/go-chart/drawing" ) +// StyleShow is a prebuilt style with the `Show` property set to true. +func StyleShow() Style { + return Style{ + Show: true, + } +} + // Style is a simple style set. type Style struct { Show bool @@ -22,9 +29,9 @@ type Style struct { FontColor drawing.Color Font *truetype.Font - TextHorizontalAlign textHorizontalAlign - TextVerticalAlign textVerticalAlign - TextWrap textWrap + TextHorizontalAlign TextHorizontalAlign + TextVerticalAlign TextVerticalAlign + TextWrap TextWrap TextLineSpacing int } @@ -191,7 +198,7 @@ func (s Style) GetPadding(defaults ...Box) Box { } // GetTextHorizontalAlign returns the horizontal alignment. -func (s Style) GetTextHorizontalAlign(defaults ...textHorizontalAlign) textHorizontalAlign { +func (s Style) GetTextHorizontalAlign(defaults ...TextHorizontalAlign) TextHorizontalAlign { if s.TextHorizontalAlign == TextHorizontalAlignUnset { if len(defaults) > 0 { return defaults[0] @@ -202,7 +209,7 @@ func (s Style) GetTextHorizontalAlign(defaults ...textHorizontalAlign) textHoriz } // GetTextVerticalAlign returns the vertical alignment. -func (s Style) GetTextVerticalAlign(defaults ...textVerticalAlign) textVerticalAlign { +func (s Style) GetTextVerticalAlign(defaults ...TextVerticalAlign) TextVerticalAlign { if s.TextVerticalAlign == TextVerticalAlignUnset { if len(defaults) > 0 { return defaults[0] @@ -213,7 +220,7 @@ func (s Style) GetTextVerticalAlign(defaults ...textVerticalAlign) textVerticalA } // GetTextWrap returns the word wrap. -func (s Style) GetTextWrap(defaults ...textWrap) textWrap { +func (s Style) GetTextWrap(defaults ...TextWrap) TextWrap { if s.TextWrap == TextWrapUnset { if len(defaults) > 0 { return defaults[0] diff --git a/text.go b/text.go index 6e45c1c..156d5b3 100644 --- a/text.go +++ b/text.go @@ -3,51 +3,51 @@ package chart import "strings" // TextHorizontalAlign is an enum for the horizontal alignment options. -type textHorizontalAlign int +type TextHorizontalAlign int const ( // TextHorizontalAlignUnset is the unset state for text horizontal alignment. - TextHorizontalAlignUnset textHorizontalAlign = 0 + TextHorizontalAlignUnset TextHorizontalAlign = 0 // TextHorizontalAlignLeft aligns a string horizontally so that it's left ligature starts at horizontal pixel 0. - TextHorizontalAlignLeft textHorizontalAlign = 1 + TextHorizontalAlignLeft TextHorizontalAlign = 1 // TextHorizontalAlignCenter left aligns a string horizontally so that there are equal pixels // to the left and to the right of a string within a box. - TextHorizontalAlignCenter textHorizontalAlign = 2 + TextHorizontalAlignCenter TextHorizontalAlign = 2 // TextHorizontalAlignRight right aligns a string horizontally so that the right ligature ends at the right-most pixel // of a box. - TextHorizontalAlignRight textHorizontalAlign = 3 + TextHorizontalAlignRight TextHorizontalAlign = 3 ) // TextWrap is an enum for the word wrap options. -type textWrap int +type TextWrap int const ( // TextWrapUnset is the unset state for text wrap options. - TextWrapUnset textWrap = 0 + TextWrapUnset TextWrap = 0 // TextWrapNone will spill text past horizontal boundaries. - TextWrapNone textWrap = 1 + TextWrapNone TextWrap = 1 // TextWrapWord will split a string on words (i.e. spaces) to fit within a horizontal boundary. - TextWrapWord textWrap = 2 + TextWrapWord TextWrap = 2 // TextWrapRune will split a string on a rune (i.e. utf-8 codepage) to fit within a horizontal boundary. - TextWrapRune textWrap = 3 + TextWrapRune TextWrap = 3 ) // TextVerticalAlign is an enum for the vertical alignment options. -type textVerticalAlign int +type TextVerticalAlign int const ( // TextVerticalAlignUnset is the unset state for vertical alignment options. - TextVerticalAlignUnset textVerticalAlign = 0 + TextVerticalAlignUnset TextVerticalAlign = 0 // TextVerticalAlignBaseline aligns text according to the "baseline" of the string, or where a normal ascender begins. - TextVerticalAlignBaseline textVerticalAlign = 1 + TextVerticalAlignBaseline TextVerticalAlign = 1 // TextVerticalAlignBottom aligns the text according to the lowers pixel of any of the ligatures (ex. g or q both extend below the baseline). - TextVerticalAlignBottom textVerticalAlign = 2 + TextVerticalAlignBottom TextVerticalAlign = 2 // TextVerticalAlignMiddle aligns the text so that there is an equal amount of space above and below the top and bottom of the ligatures. - TextVerticalAlignMiddle textVerticalAlign = 3 + TextVerticalAlignMiddle TextVerticalAlign = 3 // TextVerticalAlignMiddleBaseline aligns the text veritcally so that there is an equal number of pixels above and below the baseline of the string. - TextVerticalAlignMiddleBaseline textVerticalAlign = 4 + TextVerticalAlignMiddleBaseline TextVerticalAlign = 4 // TextVerticalAlignTop alignts the text so that the top of the ligatures are at y-pixel 0 in the container. - TextVerticalAlignTop textVerticalAlign = 5 + TextVerticalAlignTop TextVerticalAlign = 5 ) var ( @@ -57,9 +57,9 @@ var ( // TextStyle encapsulates text style options. type TextStyle struct { - HorizontalAlign textHorizontalAlign - VerticalAlign textVerticalAlign - Wrap textWrap + HorizontalAlign TextHorizontalAlign + VerticalAlign TextVerticalAlign + Wrap TextWrap } type text struct{} diff --git a/tick.go b/tick.go index dd1d161..274d070 100644 --- a/tick.go +++ b/tick.go @@ -4,7 +4,7 @@ import "math" // TicksProvider is a type that provides ticks. type TicksProvider interface { - GetTicks(vf ValueFormatter) []Tick + GetTicks(r Renderer, defaults Style, vf ValueFormatter) []Tick } // Tick represents a label on an axis. diff --git a/time_series.go b/time_series.go index 8869cac..df779eb 100644 --- a/time_series.go +++ b/time_series.go @@ -7,7 +7,7 @@ type TimeSeries struct { Name string Style Style - YAxis yAxisType + YAxis YAxisType XValues []time.Time YValues []float64 @@ -50,7 +50,7 @@ func (ts TimeSeries) GetValueFormatters() (x, y ValueFormatter) { } // GetYAxis returns which YAxis the series draws on. -func (ts TimeSeries) GetYAxis() yAxisType { +func (ts TimeSeries) GetYAxis() YAxisType { return ts.YAxis } diff --git a/xaxis.go b/xaxis.go index 5855c12..6c5cb58 100644 --- a/xaxis.go +++ b/xaxis.go @@ -13,7 +13,7 @@ type XAxis struct { Range Range Ticks []Tick - TickPosition tickPosition + TickPosition TickPosition GridLines []GridLine GridMajorStyle Style @@ -31,7 +31,7 @@ func (xa XAxis) GetStyle() Style { } // GetTickPosition returns the tick position option for the axis. -func (xa XAxis) GetTickPosition(defaults ...tickPosition) tickPosition { +func (xa XAxis) GetTickPosition(defaults ...TickPosition) TickPosition { if xa.TickPosition == TickPositionUnset { if len(defaults) > 0 { return defaults[0] @@ -51,7 +51,7 @@ func (xa XAxis) GetTicks(r Renderer, ra Range, defaults Style, vf ValueFormatter return xa.Ticks } if tp, isTickProvider := ra.(TicksProvider); isTickProvider { - return tp.GetTicks(vf) + return tp.GetTicks(r, defaults, vf) } tickStyle := xa.Style.InheritFrom(defaults) return GenerateContinuousTicks(r, ra, false, tickStyle, vf) diff --git a/yaxis.go b/yaxis.go index 8b79904..7012e21 100644 --- a/yaxis.go +++ b/yaxis.go @@ -13,7 +13,7 @@ type YAxis struct { Zero GridLine - AxisType yAxisType + AxisType YAxisType ValueFormatter ValueFormatter Range Range @@ -45,7 +45,7 @@ func (ya YAxis) GetTicks(r Renderer, ra Range, defaults Style, vf ValueFormatter return ya.Ticks } if tp, isTickProvider := ra.(TicksProvider); isTickProvider { - return tp.GetTicks(vf) + return tp.GetTicks(r, defaults, vf) } tickStyle := ya.Style.InheritFrom(defaults) return GenerateContinuousTicks(r, ra, true, tickStyle, vf)