diff options
author | Felix Hanley <felix@userspace.com.au> | 2020-02-20 05:37:36 +0000 |
---|---|---|
committer | Felix Hanley <felix@userspace.com.au> | 2020-02-20 05:37:36 +0000 |
commit | 8e53f314cab7e96fb11e4a8d1dfe537e58bcc9ed (patch) | |
tree | e481cec1d836d560c2f52e02f348d90e87753fd2 | |
parent | 7d18480b3e1df4233e0b540909931a99d4fe3b71 (diff) | |
download | sws-8e53f314cab7e96fb11e4a8d1dfe537e58bcc9ed.tar.gz sws-8e53f314cab7e96fb11e4a8d1dfe537e58bcc9ed.tar.bz2 |
WIP generic charting
-rw-r--r-- | charts.go | 22 | ||||
-rw-r--r-- | cmd/server/sites.go | 2 | ||||
-rw-r--r-- | templates/charts.tmpl | 18 | ||||
-rw-r--r-- | templates/site.tmpl | 17 | ||||
-rw-r--r-- | time_buckets.go | 58 | ||||
-rw-r--r-- | time_buckets_test.go | 12 | ||||
-rw-r--r-- | user_agent.go | 58 |
7 files changed, 137 insertions, 50 deletions
@@ -9,6 +9,28 @@ import ( "github.com/wcharczuk/go-chart/drawing" ) +type Chartable interface { + YMax() int + XSeries() []Countable +} + +type Countable interface { + Label() string + YValue() int +} + +/* +type TimeChartable interface { + XMax() int + Series() []TimeCountable +} + +type TimeCountable interface { + XValue() time.Time + YValue() int +} +*/ + type chart struct { width, height int data TimeBuckets diff --git a/cmd/server/sites.go b/cmd/server/sites.go index d59eb1c..efc447b 100644 --- a/cmd/server/sites.go +++ b/cmd/server/sites.go @@ -58,7 +58,7 @@ func handleSite(db sws.SiteStore, rndr Renderer) http.HandlerFunc { payload := struct { Site *sws.Site Pages map[string]*sws.Page - UserAgents map[string]*sws.UserAgent + UserAgents sws.UserAgentSummary Hits sws.TimeBuckets }{ Site: site, diff --git a/templates/charts.tmpl b/templates/charts.tmpl index f864263..d0d973f 100644 --- a/templates/charts.tmpl +++ b/templates/charts.tmpl @@ -1,7 +1,7 @@ -{{ define "barChart" }} +{{ define "timeBarChart" }} <ul class="chart bar"> {{ $max := .CountMax }} - {{ range .Buckets }} + {{ range .Data }} <li class="slot{{ if eq .Time.Hour 0 }} midnight{{ end }}" data-date="{{ .Time|timeHour }}" data-count="{{ .Count }}" data-percent="{{ percent .Count $max }}"> <div class="bar" style="height:{{ percent .Count $max }}%" /> </li> @@ -9,10 +9,22 @@ </ul> {{ end }} +{{ define "barChart" }} + <ul class="chart bar"> + {{ $max := .YMax }} + {{ range .XSeries }} + <li class="slot" data-x="{{ .Label }}" data-y="{{ .YValue }}" data-percent="{{ percent .YValue $max }}"> + <div class="bar" style="height:{{ percent .YValue $max }}%" /> + </li> + {{ else }} + {{ end }} + </ul> +{{ end }} + {{ define "stackedBarChart" }} <ul class="chart bar stacked"> {{ $max := .CountMax }} - {{ range .Buckets }} + {{ range .Data }} <li class="slot{{ if eq .Time.Hour 0 }} midnight{{ end }}" data-date="{{ .Time|timeHour }}" data-count="{{ .Count }}" data-percent="{{ percent .Count $max }}"> <div class="bar" style="height:{{ percent .Count $max }}%" /> </li> diff --git a/templates/site.tmpl b/templates/site.tmpl index bf1a8af..5562dd4 100644 --- a/templates/site.tmpl +++ b/templates/site.tmpl @@ -7,7 +7,7 @@ {{ end }} </header> <fig> - {{ template "barChart" .Hits }} + {{ template "timeBarChart" .Hits }} </fig> <h2>Popular pages</h2> <ul class="pages"> @@ -16,11 +16,16 @@ {{ end }} </ul> <h2>User Agents</h2> - <ul class="agents"> - {{ range .UserAgents }} - {{ template "uaForList" . }} - {{ end }} - </ul> + {{ if .UserAgents }} + {{ template "barChart" .UserAgents }} + <ul class="agents"> + {{ range .UserAgents }} + {{ template "uaForList" . }} + {{ end }} + </ul> + {{ else }} + <p>No user agents</p> + {{ end }} </main> {{ end }} diff --git a/time_buckets.go b/time_buckets.go index f17ee38..ebb25e6 100644 --- a/time_buckets.go +++ b/time_buckets.go @@ -10,7 +10,7 @@ type TimeBuckets struct { Duration time.Duration TimeMin, TimeMax time.Time CountMin, CountMax int - Buckets []Bucket + Data []Bucket } type Bucket struct { @@ -18,12 +18,30 @@ type Bucket struct { Count int } +/* +func (tb TimeBuckets) YMax() int { + return tb.CountMax +} + +func (tb TimeBuckets) Next() TimeCountable { + return tb.Data +} + +func (b Bucket) XValue() time.Time { + return b.Time +} + +func (b Bucket) YValue() int { + return b.Count +} +*/ + // XYValues splits the buckets into two data series, one with the times // and the other with the values. func (tb TimeBuckets) XYValues() ([]time.Time, []float64) { - x := make([]time.Time, len(tb.Buckets)) - y := make([]float64, len(tb.Buckets)) - for i, b := range tb.Buckets { + x := make([]time.Time, len(tb.Data)) + y := make([]float64, len(tb.Data)) + for i, b := range tb.Data { x[i] = b.Time y[i] = float64(b.Count) } @@ -38,7 +56,7 @@ func (b Bucket) String() string { func HitsToTimeBuckets(hits []*Hit, d time.Duration) TimeBuckets { out := TimeBuckets{ Duration: d, - Buckets: make([]Bucket, 0), + Data: make([]Bucket, 0), } for j, h := range hits { k := h.CreatedAt.Truncate(d) @@ -49,14 +67,14 @@ func HitsToTimeBuckets(hits []*Hit, d time.Duration) TimeBuckets { out.TimeMax = k } var found bool - for i, tb := range out.Buckets { + for i, tb := range out.Data { if tb.Time.Equal(k) { - out.Buckets[i].Count++ + out.Data[i].Count++ found = true } } if !found { - out.Buckets = append(out.Buckets, Bucket{Time: k, Count: 1}) + out.Data = append(out.Data, Bucket{Time: k, Count: 1}) } } out.Sort() @@ -66,8 +84,8 @@ func HitsToTimeBuckets(hits []*Hit, d time.Duration) TimeBuckets { // Sort order the buckets in ascending order by time. func (tb *TimeBuckets) Sort() { - sort.Slice(tb.Buckets, func(i, j int) bool { - return tb.Buckets[i].Time.Before(tb.Buckets[j].Time) + sort.Slice(tb.Data, func(i, j int) bool { + return tb.Data[i].Time.Before(tb.Data[j].Time) }) } @@ -92,31 +110,31 @@ func (tb *TimeBuckets) Fill(b, e *time.Time) { var idx int for n := begin; idx < total && !n.After(end); n = n.Add(tb.Duration) { switch { - case existing >= len(tb.Buckets): + case existing >= len(tb.Data): newBuckets[idx] = Bucket{Time: n, Count: 0} - case n.Before(tb.Buckets[existing].Time): + case n.Before(tb.Data[existing].Time): newBuckets[idx] = Bucket{Time: n, Count: 0} default: - newBuckets[idx] = tb.Buckets[existing] + newBuckets[idx] = tb.Data[existing] existing++ } idx++ } tb.updateMinMax() - tb.Buckets = newBuckets + tb.Data = newBuckets } func (tb *TimeBuckets) updateMinMax() { - if len(tb.Buckets) < 1 { + if len(tb.Data) < 1 { return } - minC := tb.Buckets[0].Count - maxC := tb.Buckets[0].Count - minT := tb.Buckets[0].Time - maxT := tb.Buckets[0].Time - for _, b := range tb.Buckets { + minC := tb.Data[0].Count + maxC := tb.Data[0].Count + minT := tb.Data[0].Time + maxT := tb.Data[0].Time + for _, b := range tb.Data { if b.Count < minC { minC = b.Count } diff --git a/time_buckets_test.go b/time_buckets_test.go index a94d49a..c97d1f1 100644 --- a/time_buckets_test.go +++ b/time_buckets_test.go @@ -25,7 +25,7 @@ func TestHitsToTimeBuckets(t *testing.T) { Duration: time.Second, TimeMin: now.Add(-5 * time.Second).Round(time.Second), TimeMax: now.Add(-2 * time.Second).Round(time.Second), - Buckets: []Bucket{ + Data: []Bucket{ {Time: now.Add(-5 * time.Second).Round(time.Second), Count: 1}, {Time: now.Add(-4 * time.Second).Round(time.Second), Count: 3}, {Time: now.Add(-3 * time.Second).Round(time.Second), Count: 1}, @@ -57,7 +57,7 @@ func TestXYValues(t *testing.T) { { TimeBuckets{ Duration: time.Minute, - Buckets: []Bucket{ + Data: []Bucket{ {Time: now.Add(-3 * time.Minute), Count: 1}, {Time: now.Add(-2 * time.Minute), Count: 2}, {Time: now.Add(-1 * time.Minute), Count: 1}, @@ -101,7 +101,7 @@ func TestTimeBucketsFill(t *testing.T) { Duration: time.Second, TimeMin: now.Add(-5 * time.Second), TimeMax: now.Add(-5 * time.Second), - Buckets: []Bucket{{Time: now.Add(-5 * time.Second), Count: 1}}, + Data: []Bucket{{Time: now.Add(-5 * time.Second), Count: 1}}, }, nil, ptrTime(now), @@ -120,7 +120,7 @@ func TestTimeBucketsFill(t *testing.T) { Duration: time.Second, TimeMin: now.Add(-5 * time.Second), TimeMax: now.Add(-5 * time.Second), - Buckets: []Bucket{{Time: now.Add(-5 * time.Second), Count: 1}}, + Data: []Bucket{{Time: now.Add(-5 * time.Second), Count: 1}}, }, ptrTime(now.Add(-6 * time.Second)), nil, @@ -135,7 +135,7 @@ func TestTimeBucketsFill(t *testing.T) { Duration: time.Second, TimeMin: now.Add(-5 * time.Second), TimeMax: now.Add(-5 * time.Second), - Buckets: []Bucket{{Time: now.Add(-5 * time.Second), Count: 1}}, + Data: []Bucket{{Time: now.Add(-5 * time.Second), Count: 1}}, }, nil, nil, @@ -147,7 +147,7 @@ func TestTimeBucketsFill(t *testing.T) { for i, tt := range tests { tt.in.Fill(tt.begin, tt.end) - for j, b := range tt.in.Buckets { + for j, b := range tt.in.Data { if b.Time != tt.expected[j].Time { t.Errorf("%d => [%d] expected %s, got %s", i, j, tt.expected[j].Time, b.Time) } diff --git a/user_agent.go b/user_agent.go index a259538..dbfd1bf 100644 --- a/user_agent.go +++ b/user_agent.go @@ -54,28 +54,58 @@ func UserAgentFromRequest(r *http.Request) (*UserAgent, error) { } // UserAgentsFromHits collects the browsers from provided hits. -func UserAgentsFromHits(hits []*Hit) map[string]*UserAgent { +func UserAgentsFromHits(hits []*Hit) UserAgentSummary { out := make(map[string]*UserAgent) for _, h := range hits { - if h.UserAgentHash != nil { - b, ok := out[*h.UserAgentHash] - if !ok { - b = &UserAgent{ - Name: h.UserAgent.Name, - LastSeenAt: h.CreatedAt, - ua: detector.New(h.UserAgent.Name), - } - } - if b.LastSeenAt.Before(h.CreatedAt) { - b.LastSeenAt = h.CreatedAt + if h.UserAgentHash == nil { + continue + } + b, ok := out[*h.UserAgentHash] + if !ok { + b = &UserAgent{ + Name: h.UserAgent.Name, + LastSeenAt: h.CreatedAt, + ua: detector.New(h.UserAgent.Name), } - b.Count++ - out[*h.UserAgentHash] = b } + if b.LastSeenAt.Before(h.CreatedAt) { + b.LastSeenAt = h.CreatedAt + } + b.Count++ + out[*h.UserAgentHash] = b + } + return UserAgentSummary(out) +} + +type UserAgentSummary map[string]*UserAgent + +func (s UserAgentSummary) YMax() int { + max := 0 + for _, v := range s { + if v.Count > max { + max = v.Count + } + } + return max +} +func (s UserAgentSummary) XSeries() []*UserAgent { + out := make([]*UserAgent, len(s)) + i := 0 + for _, v := range s { + out[i] = v + i++ } return out } +func (ua UserAgent) Label() string { + return ua.Browser() +} + +func (ua UserAgent) YValue() int { + return ua.Count +} + func (ua UserAgent) IsBot() bool { return ua.ua.Bot() } |