package server
import (
"fmt"
"html/template"
"net/http"
"strings"
"github.com/go-chi/chi/v5"
"git.hubris.network/dtoro/artifacto/internal/store"
)
type detailData struct {
BaseURL string
Artifact *store.Artifact
Series []store.DayPoint
Referrers []store.ReferrerCount
ChartSVG template.HTML
}
func (s *Server) getArtifactDetail(w http.ResponseWriter, r *http.Request) {
slug := chi.URLParam(r, "slug")
art, err := s.store.GetArtifact(slug)
if err != nil {
http.NotFound(w, r)
return
}
series, err := s.store.SeriesFor(slug, 30)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
refs, err := s.store.TopReferrers(slug, 10)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
s.render(w, "detail", detailData{
BaseURL: s.cfg.BaseURL,
Artifact: art,
Series: series,
Referrers: refs,
ChartSVG: template.HTML(renderChart(series)),
})
}
// renderChart produces a tiny SVG line+area chart with day labels.
func renderChart(series []store.DayPoint) string {
const w = 720
const h = 160
const padLeft = 30
const padBottom = 20
const padTop = 10
const padRight = 10
innerW := w - padLeft - padRight
innerH := h - padTop - padBottom
var max int64
for _, p := range series {
if p.Views > max {
max = p.Views
}
}
if max < 1 {
max = 1
}
var b strings.Builder
fmt.Fprintf(&b, ``)
return b.String()
}
step := float64(innerW) / float64(maxInt(n-1, 1))
// build polyline path
var pts strings.Builder
for i, p := range series {
x := float64(padLeft) + float64(i)*step
y := float64(padTop+innerH) - float64(p.Views)/float64(max)*float64(innerH)
if i > 0 {
pts.WriteByte(' ')
}
fmt.Fprintf(&pts, "%.1f,%.1f", x, y)
}
// area under curve
var area strings.Builder
fmt.Fprintf(&area, "M%d,%d ", padLeft, padTop+innerH)
for i, p := range series {
x := float64(padLeft) + float64(i)*step
y := float64(padTop+innerH) - float64(p.Views)/float64(max)*float64(innerH)
fmt.Fprintf(&area, "L%.1f,%.1f ", x, y)
}
fmt.Fprintf(&area, "L%.1f,%d Z", float64(padLeft)+float64(n-1)*step, padTop+innerH)
fmt.Fprintf(&b, ``, area.String())
fmt.Fprintf(&b, ``, pts.String())
// x-axis tick labels: first, middle, last day
labelIdx := []int{0, n / 2, n - 1}
for _, i := range labelIdx {
if i < 0 || i >= n {
continue
}
x := float64(padLeft) + float64(i)*step
fmt.Fprintf(&b, `%s`,
x, padTop+innerH+14, shortDay(series[i].Day))
}
b.WriteString(``)
return b.String()
}
func shortDay(iso string) string {
// yyyy-mm-dd → mm-dd
if len(iso) >= 10 {
return iso[5:10]
}
return iso
}