aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFelix Hanley <felix@userspace.com.au>2020-03-18 13:25:09 +0000
committerFelix Hanley <felix@userspace.com.au>2020-03-18 13:25:09 +0000
commitac6468f9eac688a2fb379ea332fd45490cf80cf4 (patch)
tree581fe758fb471e22cf1b1ef85726bc4064e510a8
parent2248b4d7e1d083a103e94985ee4b373d689ae0e8 (diff)
downloadsws-ac6468f9eac688a2fb379ea332fd45490cf80cf4.tar.gz
sws-ac6468f9eac688a2fb379ea332fd45490cf80cf4.tar.bz2
Unify chart handler, add pie charts, better caching
-rw-r--r--browser_set.go42
-rw-r--r--charts.go49
-rw-r--r--cmd/server/charts.go127
-rw-r--r--cmd/server/helpers.go14
-rw-r--r--cmd/server/hits.go3
-rw-r--r--cmd/server/routes.go11
-rw-r--r--hit_set.go21
-rw-r--r--store/sqlite3.go4
-rw-r--r--tmpl/site.tmpl1
9 files changed, 178 insertions, 94 deletions
diff --git a/browser_set.go b/browser_set.go
index 4d3107c..c4b9865 100644
--- a/browser_set.go
+++ b/browser_set.go
@@ -1,6 +1,7 @@
package sws
import (
+ "sort"
"time"
detector "github.com/mssola/user_agent"
@@ -54,6 +55,17 @@ func NewBrowserSet(hs *HitSet) BrowserSet {
}
return BrowserSet(out)
}
+func (bs *BrowserSet) SortByName() {
+ sort.Slice(*bs, func(i, j int) bool {
+ return (*bs)[i].Label() < (*bs)[j].Label()
+ })
+}
+
+func (bs *BrowserSet) SortByHits() {
+ sort.Slice(*bs, func(i, j int) bool {
+ return (*bs)[i].hitSet.Count() > (*bs)[j].hitSet.Count()
+ })
+}
func (b Browser) Label() string {
return b.Name
@@ -66,7 +78,37 @@ func (b Browser) Count() int {
func (bs BrowserSet) Count() int {
return len(bs)
}
+func (bs BrowserSet) Labels() []string {
+ out := make([]string, len(bs))
+ for i := 0; i < len(bs); i++ {
+ out[i] = bs[i].Label()
+ }
+ return out
+}
+func (bs BrowserSet) Counts() []int {
+ out := make([]int, len(bs))
+ for i := 0; i < len(bs); i++ {
+ out[i] = bs[i].Count()
+ }
+ return out
+}
+/*
+func (bs BrowserSet) Ratios() []float64 {
+ out := make([]float64, len(bs))
+ max := 0.0
+ for i := 0; i < len(bs); i++ {
+ out[i] = float64(bs[i].Count())
+ if out[i] > max {
+ max = out[i]
+ }
+ }
+ for i := 0; i < len(out); i++ {
+ out[i] = out[i] / max
+ }
+ return out
+}
+*/
func (bs BrowserSet) YMax() int {
max := 0
for _, b := range bs {
diff --git a/charts.go b/charts.go
index 4b0a0e2..fe90bb1 100644
--- a/charts.go
+++ b/charts.go
@@ -9,27 +9,23 @@ import (
"github.com/wcharczuk/go-chart/drawing"
)
-type Chartable interface {
- YMax() int
- XSeries() []Countable
+type counted interface {
+ Counts() []int
}
-
-type Countable interface {
- Label() string
- Count() int
+type labelled interface {
+ Labels() []string
}
-
-/*
-type TimeChartable interface {
- XMax() int
- Series() []TimeCountable
+type timed interface {
+ Times() []time.Time
}
-
-type TimeCountable interface {
- XValue() time.Time
- YValue() int
+type timeCounted interface {
+ counted
+ timed
+}
+type labelCounted interface {
+ counted
+ labelled
}
-*/
type chart struct {
width, height int
@@ -181,3 +177,22 @@ func HitChartSVG(w io.Writer, data *HitSet, d time.Duration) error {
graph.Render(gochart.SVG, w)
return nil
}
+
+func PieChartSVG(w io.Writer, data labelCounted) error {
+ labels := data.Labels()
+ counts := data.Counts()
+ values := make([]gochart.Value, len(labels))
+ for i := 0; i < len(labels); i++ {
+ values[i] = gochart.Value{
+ Value: float64(counts[i]),
+ Label: labels[i],
+ }
+ }
+ pie := gochart.PieChart{
+ Width: 400,
+ Height: 400,
+ Values: values,
+ }
+ pie.Render(gochart.SVG, w)
+ return nil
+}
diff --git a/cmd/server/charts.go b/cmd/server/charts.go
index 875b528..fce5c86 100644
--- a/cmd/server/charts.go
+++ b/cmd/server/charts.go
@@ -1,15 +1,18 @@
package main
import (
+ "crypto/sha1"
+ "fmt"
"net/http"
"strconv"
+ "strings"
"time"
"github.com/go-chi/chi"
"src.userspace.com.au/sws"
)
-func sparklineHandler(db sws.HitStore) http.HandlerFunc {
+func chartHandler(db sws.HitStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
site, ok := ctx.Value("site").(*sws.Site)
@@ -19,20 +22,34 @@ func sparklineHandler(db sws.HitStore) http.HandlerFunc {
return
}
- beginSecs, err := strconv.ParseInt(chi.URLParam(r, "b"), 10, 64)
+ chartType := chi.URLParam(r, "type")
+ dataType := chi.URLParam(r, "data")
+ beginSecs, err := strconv.ParseInt(chi.URLParam(r, "begin"), 10, 64)
if err != nil {
- log(err)
- http.Error(w, http.StatusText(404), 404)
+ httpError(w, http.StatusNotFound, err.Error())
return
}
- endSecs, err := strconv.ParseInt(chi.URLParam(r, "e"), 10, 64)
+ endSecs, err := strconv.ParseInt(chi.URLParam(r, "end"), 10, 64)
if err != nil {
- log(err)
- http.Error(w, http.StatusText(404), 404)
+ httpError(w, http.StatusNotFound, err.Error())
return
}
begin := time.Unix(beginSecs, 0)
end := time.Unix(endSecs, 0)
+
+ var b strings.Builder
+ b.WriteString(r.URL.Path)
+ // FIXME
+ b.WriteString(time.Now().Truncate(30 * time.Minute).String())
+ etag := fmt.Sprintf(`"%x"`, sha1.Sum([]byte(b.String())))
+
+ if match := r.Header.Get("If-None-Match"); match != "" {
+ if strings.Contains(match, etag) {
+ w.WriteHeader(http.StatusNotModified)
+ return
+ }
+ }
+
filter := map[string]interface{}{
"begin": begin,
"end": end,
@@ -40,76 +57,56 @@ func sparklineHandler(db sws.HitStore) http.HandlerFunc {
hits, err := db.GetHits(*site, filter)
if err != nil {
- log(err)
- http.Error(w, http.StatusText(404), 404)
- return
- }
- debug("retrieved", len(hits), "hits")
- data, err := sws.NewHitSet(sws.FromHits(hits))
- if err != nil {
- log(err)
- http.Error(w, http.StatusText(500), 500)
+ httpError(w, http.StatusInternalServerError, err.Error())
return
}
- // Ensure the buckets start and end at the right time
- data.Fill(&begin, &end)
- w.Header().Set("Content-Type", "image/svg+xml")
- w.Header().Set("Cache-Control", "public")
- sws.SparklineSVG(w, data, time.Hour)
- }
-}
-
-func svgChartHandler(db sws.HitStore) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- beginSecs, err := strconv.ParseInt(chi.URLParam(r, "b"), 10, 64)
+ hitSet, err := sws.NewHitSet(sws.FromHits(hits))
if err != nil {
- log(err)
- http.Error(w, http.StatusText(404), 404)
+ httpError(w, http.StatusInternalServerError, err.Error())
return
}
- endSecs, err := strconv.ParseInt(chi.URLParam(r, "e"), 10, 64)
- if err != nil {
- log(err)
- http.Error(w, http.StatusText(404), 404)
- return
- }
- begin := time.Unix(beginSecs, 0)
- end := time.Unix(endSecs, 0)
-
- ctx := r.Context()
- site, ok := ctx.Value("site").(*sws.Site)
- if !ok {
- http.Error(w, http.StatusText(422), 422)
+ if hitSet == nil {
+ httpError(w, http.StatusInternalServerError, "missing hitset")
return
}
- // width := 400
- // height := 200
- // if w := q.Get("width"); w != "" {
- // width, _ = strconv.Atoi(w)
- // }
- // if h := q.Get("height"); h != "" {
- // height, _ = strconv.Atoi(h)
- // }
+ hitSet.Fill(&begin, &end)
+ hitSet.SortByDate()
- hits, err := db.GetHits(*site, map[string]interface{}{
- "begin": begin, "end": end,
- })
- if err != nil {
- panic(err)
- }
- debug("retrieved", len(hits), "hits")
+ w.Header().Set("Etag", etag)
+ w.Header().Set("Cache-Control", "no-cache")
- data, err := sws.NewHitSet(sws.FromHits(hits))
- if err != nil {
- log(err)
- http.Error(w, http.StatusText(500), 500)
+ switch dataType {
+ case "h":
+ w.Header().Set("Content-Type", "image/svg+xml")
+ switch chartType {
+ case "b":
+ sws.HitChartSVG(w, hitSet, time.Minute)
+ case "s":
+ sws.SparklineSVG(w, hitSet, time.Hour)
+ }
+ case "p":
+ pages := sws.NewBrowserSet(hitSet)
+ pages.SortByHits()
+ w.Header().Set("Content-Type", "image/svg+xml")
+ switch chartType {
+ case "p":
+ sws.PieChartSVG(w, pages)
+ }
+ case "b":
+ browsers := sws.NewBrowserSet(hitSet)
+ browsers.SortByHits()
+ w.Header().Set("Content-Type", "image/svg+xml")
+ switch chartType {
+ case "p":
+ sws.PieChartSVG(w, browsers)
+ }
+ case "c":
+ default:
+ log("invalid chart data type:", dataType)
+ http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
- data.Fill(&begin, &end)
-
- w.Header().Set("Content-Type", "image/svg+xml")
- sws.HitChartSVG(w, data, time.Minute)
}
}
diff --git a/cmd/server/helpers.go b/cmd/server/helpers.go
index 6c498e5..17e519f 100644
--- a/cmd/server/helpers.go
+++ b/cmd/server/helpers.go
@@ -10,14 +10,13 @@ import (
)
var funcMap = template.FuncMap{
+ "piechart": func(siteID int, dataType string, begin, end time.Time) string {
+ return fmt.Sprintf("/sites/%d/charts/p-%s-%d-%d.svg", siteID, dataType, begin.Unix(), end.Unix())
+ },
"sparkline": func(id int) string {
- // This will enable "caching" for an hour
- now := time.Now() //.Truncate(time.Hour)
- //then := now.Add(-720 * time.Hour)
+ now := time.Now().Truncate(time.Hour)
then := now.Add(-168 * time.Hour) // 7 days
- //then := now.Add(-24 * time.Hour)
- //then := now.Add(-1 * time.Hour)
- return fmt.Sprintf("/sites/%d/sparklines/%d-%d.svg", id, then.Unix(), now.Unix())
+ return fmt.Sprintf("/sites/%d/charts/s-h-%d-%d.svg", id, then.Unix(), now.Unix())
},
"tz": func(s string, t time.Time) time.Time {
tz, _ := time.LoadLocation(s)
@@ -76,8 +75,9 @@ func httpError(w http.ResponseWriter, code int, msg string) {
}
func extractTimeRange(r *http.Request) (*time.Time, *time.Time) {
+ // Default to 1 week ago
begin := timePtr(time.Now().Truncate(time.Hour).Add(-168 * time.Hour))
- end := timePtr(time.Now().Truncate(time.Hour))
+ end := timePtr(time.Now().Truncate(30 * time.Minute))
q := r.URL.Query()
if b := q.Get("begin"); b != "" {
if bs, err := strconv.ParseInt(b, 10, 64); err == nil {
diff --git a/cmd/server/hits.go b/cmd/server/hits.go
index e5aaf73..3a76bea 100644
--- a/cmd/server/hits.go
+++ b/cmd/server/hits.go
@@ -84,6 +84,9 @@ func handleHitCounter(db sws.CounterStore, mmdbPath string) http.HandlerFunc {
//http.Error(w, err.Error(), http.StatusInternalServerError)
//return
}
+
+ // TODO restrict to site sites
+ w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "image/gif")
w.Write(gifBytes)
return
diff --git a/cmd/server/routes.go b/cmd/server/routes.go
index 5b93713..b9979fe 100644
--- a/cmd/server/routes.go
+++ b/cmd/server/routes.go
@@ -65,7 +65,10 @@ func createRouter(db sws.Store, mmdbPath string) (chi.Router, error) {
// For counter
r.Get("/sws.js", handleCounter(addr))
- r.Get("/sws.gif", handleHitCounter(db, mmdbPath))
+ r.Group(func(r chi.Router) {
+ r.Use(middleware.NoCache)
+ r.Get("/sws.gif", handleHitCounter(db, mmdbPath))
+ })
//r.Get("/hits", handleHits(db))
r.Group(func(r chi.Router) {
@@ -134,11 +137,9 @@ func createRouter(db sws.Store, mmdbPath string) (chi.Router, error) {
r.Get("/", siteHandler)
r.Post("/", siteHandler)
r.Get("/edit", handleSiteEdit(db, rndr))
- r.Route("/sparklines", func(r chi.Router) {
- r.Get("/{b:\\d+}-{e:\\d+}.svg", sparklineHandler(db))
- })
+
r.Route("/charts", func(r chi.Router) {
- r.Get("/{b:\\d+}-{e:\\d+}.svg", svgChartHandler(db))
+ r.Get("/{type:(p|s|b)}-{data:(h|b|c)}-{begin:\\d+}-{end:\\d+}.svg", chartHandler(db))
//r.Get("/{b:\\d+}-{e:\\d+}.png", svgChartHandler(db))
})
})
diff --git a/hit_set.go b/hit_set.go
index cd9210f..3ad0701 100644
--- a/hit_set.go
+++ b/hit_set.go
@@ -239,6 +239,27 @@ func (hs HitSet) XYValues() ([]time.Time, []float64) {
}
return x, y
}
+func (hs HitSet) Labels() []string {
+ out := make([]string, len(hs.buckets))
+ for i := 0; i < len(hs.buckets); i++ {
+ out[i] = hs.buckets[i].Label()
+ }
+ return out
+}
+func (hs HitSet) Counts() []int {
+ out := make([]int, len(hs.buckets))
+ for i := 0; i < len(hs.buckets); i++ {
+ out[i] = hs.buckets[i].Count()
+ }
+ return out
+}
+func (hs HitSet) Times() []time.Time {
+ out := make([]time.Time, len(hs.buckets))
+ for i := 0; i < len(hs.buckets); i++ {
+ out[i] = hs.buckets[i].Time()
+ }
+ return out
+}
func (hs HitSet) YMax() int {
hs.updateMinMax()
diff --git a/store/sqlite3.go b/store/sqlite3.go
index a889547..9525158 100644
--- a/store/sqlite3.go
+++ b/store/sqlite3.go
@@ -68,6 +68,10 @@ func (s *Sqlite3) SaveSite(d *sws.Site) error {
func (s *Sqlite3) GetHits(d sws.Site, filter map[string]interface{}) ([]*sws.Hit, error) {
hits := make([]*sws.Hit, 0)
+ if filter == nil {
+ filter = make(map[string]interface{})
+ }
+
sql := stmts["hits"]
filter["site_id"] = *d.ID
processFilter(&sql, filter)
diff --git a/tmpl/site.tmpl b/tmpl/site.tmpl
index 923cbc3..7ca2cdf 100644
--- a/tmpl/site.tmpl
+++ b/tmpl/site.tmpl
@@ -131,6 +131,7 @@
</header>
{{ if .Browsers }}
<figure class="figure figure--graph">
+ <img src="{{ piechart .Site.ID "b" .Begin .End }}" />
{{ template "barChart" .Browsers }}
</figure>
{{ $sum := .Browsers.YSum }}