aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFelix Hanley <felix@userspace.com.au>2020-02-20 05:37:36 +0000
committerFelix Hanley <felix@userspace.com.au>2020-02-20 05:37:36 +0000
commit8e53f314cab7e96fb11e4a8d1dfe537e58bcc9ed (patch)
treee481cec1d836d560c2f52e02f348d90e87753fd2
parent7d18480b3e1df4233e0b540909931a99d4fe3b71 (diff)
downloadsws-8e53f314cab7e96fb11e4a8d1dfe537e58bcc9ed.tar.gz
sws-8e53f314cab7e96fb11e4a8d1dfe537e58bcc9ed.tar.bz2
WIP generic charting
-rw-r--r--charts.go22
-rw-r--r--cmd/server/sites.go2
-rw-r--r--templates/charts.tmpl18
-rw-r--r--templates/site.tmpl17
-rw-r--r--time_buckets.go58
-rw-r--r--time_buckets_test.go12
-rw-r--r--user_agent.go58
7 files changed, 137 insertions, 50 deletions
diff --git a/charts.go b/charts.go
index 1ca245b..b90f347 100644
--- a/charts.go
+++ b/charts.go
@@ -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()
}