Why Build Your Own Dashboard
Third-party analytics tools are great, but they don't understand your domain. For DailyWatch, we needed dashboards showing views by region, category popularity over time, and fetch pipeline health -- metrics that Google Analytics simply doesn't track. A Go backend serving JSON to Chart.js gives us exactly that.
The Data Models
First, define the structs for our analytics data:
package analytics
import "time"
type RegionStats struct {
Region string `json:"region"`
VideoCount int `json:"video_count"`
TotalViews int64 `json:"total_views"`
AvgViews int64 `json:"avg_views"`
}
type CategoryTrend struct {
Category string `json:"category"`
Date time.Time `json:"date"`
Count int `json:"count"`
Views int64 `json:"views"`
}
type DashboardData struct {
RegionBreakdown []RegionStats `json:"region_breakdown"`
CategoryTrends []CategoryTrend `json:"category_trends"`
TotalVideos int `json:"total_videos"`
FetchedToday int `json:"fetched_today"`
LastFetchAt *time.Time `json:"last_fetch_at"`
}
The Analytics Repository
Query SQLite for the metrics we need:
package analytics
import (
"database/sql"
"time"
_ "github.com/mattn/go-sqlite3"
)
type Repository struct {
db *sql.DB
}
func NewRepository(dbPath string) (*Repository, error) {
db, err := sql.Open("sqlite3", dbPath+"?mode=ro&_journal_mode=WAL")
if err != nil {
return nil, err
}
return &Repository{db: db}, nil
}
func (r *Repository) GetRegionStats() ([]RegionStats, error) {
rows, err := r.db.Query(`
SELECT region,
COUNT(*) as video_count,
COALESCE(SUM(view_count), 0) as total_views,
COALESCE(AVG(view_count), 0) as avg_views
FROM videos
WHERE region IS NOT NULL
GROUP BY region
ORDER BY total_views DESC
`)
if err != nil {
return nil, err
}
defer rows.Close()
var stats []RegionStats
for rows.Next() {
var s RegionStats
if err := rows.Scan(&s.Region, &s.VideoCount, &s.TotalViews, &s.AvgViews); err != nil {
return nil, err
}
stats = append(stats, s)
}
return stats, nil
}
func (r *Repository) GetCategoryTrends(days int) ([]CategoryTrend, error) {
rows, err := r.db.Query(`
SELECT c.name, DATE(v.fetched_at) as fetch_date,
COUNT(*) as count,
COALESCE(SUM(v.view_count), 0) as views
FROM videos v
JOIN categories c ON v.category_id = c.id
WHERE v.fetched_at >= DATE('now', ? || ' days')
GROUP BY c.name, fetch_date
ORDER BY fetch_date, views DESC
`, -days)
if err != nil {
return nil, err
}
defer rows.Close()
var trends []CategoryTrend
for rows.Next() {
var t CategoryTrend
var dateStr string
if err := rows.Scan(&t.Category, &dateStr, &t.Count, &t.Views); err != nil {
return nil, err
}
t.Date, _ = time.Parse("2006-01-02", dateStr)
trends = append(trends, t)
}
return trends, nil
}
The HTTP Handler
Serve the dashboard data as JSON:
package main
import (
"encoding/json"
"log"
"net/http"
"myapp/analytics"
)
func main() {
repo, err := analytics.NewRepository("./data/videos.db")
if err != nil {
log.Fatal(err)
}
http.HandleFunc("/api/dashboard", func(w http.ResponseWriter, r *http.Request) {
regions, _ := repo.GetRegionStats()
trends, _ := repo.GetCategoryTrends(14)
data := analytics.DashboardData{
RegionBreakdown: regions,
CategoryTrends: trends,
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "max-age=300")
json.NewEncoder(w).Encode(data)
})
log.Println("Dashboard API on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Chart.js Frontend
Now the fun part -- rendering the charts:
<canvas id="regionChart" width="600" height="300"></canvas>
<canvas id="trendChart" width="600" height="300"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
async function loadDashboard() {
const res = await fetch('/api/dashboard');
const data = await res.json();
// Region breakdown - horizontal bar chart
new Chart(document.getElementById('regionChart'), {
type: 'bar',
data: {
labels: data.region_breakdown.map(r => r.region),
datasets: [{
label: 'Total Views',
data: data.region_breakdown.map(r => r.total_views),
backgroundColor: [
'#3b82f6', '#ef4444', '#10b981', '#f59e0b',
'#8b5cf6', '#ec4899', '#06b6d4', '#84cc16'
],
}]
},
options: {
indexAxis: 'y',
plugins: { title: { display: true, text: 'Views by Region' } },
}
});
// Category trends - line chart
const categories = [...new Set(data.category_trends.map(t => t.category))].slice(0, 5);
const dates = [...new Set(data.category_trends.map(t => t.date.split('T')[0]))];
new Chart(document.getElementById('trendChart'), {
type: 'line',
data: {
labels: dates,
datasets: categories.map((cat, i) => ({
label: cat,
data: dates.map(d => {
const point = data.category_trends.find(t => t.category === cat && t.date.startsWith(d));
return point ? point.views : 0;
}),
borderColor: ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6'][i],
fill: false,
tension: 0.3,
}))
},
options: {
plugins: { title: { display: true, text: 'Category Trends (14 days)' } },
}
});
}
loadDashboard();
</script>
What We Learned
Running this dashboard at DailyWatch revealed patterns we never expected. Music videos dominate in Brazil and India, while tech content leads in Germany and Australia. Category trends shift predictably on weekends. These insights directly influenced how we weight content in the trending algorithm.
Go's minimal overhead means the dashboard API responds in under 5ms even on shared hosting, and Chart.js renders beautiful interactive charts with zero build tooling.
This article is part of the Building DailyWatch series. Check out DailyWatch to see these techniques in action.
Top comments (0)