DEV Community

ahmet gedik
ahmet gedik

Posted on

Building a Video Analytics Dashboard with Go and Chart.js

Analytics for Asia-Pacific Video Trends

Understanding which regions drive traffic, which categories trend at which hours, and how K-pop compares to J-drama in engagement — these are the questions that shape content strategy for TopVideoHub. A lightweight Go backend serving pre-aggregated analytics to Chart.js covers all of it.

Go Analytics Server

package main

import (
    "database/sql"
    "encoding/json"
    "log"
    "net/http"

    _ "github.com/mattn/go-sqlite3"
)

type Server struct {
    db *sql.DB
}

func main() {
    db, err := sql.Open("sqlite3", "./data/topvideohub.db?_journal_mode=WAL")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    s := &Server{db: db}
    mux := http.NewServeMux()
    mux.HandleFunc("/api/analytics/views-by-region",    s.viewsByRegion)
    mux.HandleFunc("/api/analytics/trending-categories", s.trendingCategories)
    mux.HandleFunc("/api/analytics/peak-hours",          s.peakHours)

    log.Println("Analytics server on :8080")
    log.Fatal(http.ListenAndServe(":8080", corsMiddleware(mux)))
}

func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "https://topvideohub.com")
        w.Header().Set("Content-Type", "application/json")
        next.ServeHTTP(w, r)
    })
}
Enter fullscreen mode Exit fullscreen mode

Views by Region Endpoint

type RegionStat struct {
    Region     string `json:"region"`
    VideoCount int    `json:"video_count"`
    TotalViews int64  `json:"total_views"`
}

func (s *Server) viewsByRegion(w http.ResponseWriter, r *http.Request) {
    rows, err := s.db.Query(`
        SELECT vr.region,
               COUNT(DISTINCT vr.video_id) AS video_count,
               COALESCE(SUM(v.view_count), 0) AS total_views
        FROM video_regions vr
        JOIN videos v ON v.id = vr.video_id
        GROUP BY vr.region
        ORDER BY total_views DESC
    `)
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    defer rows.Close()

    var stats []RegionStat
    for rows.Next() {
        var st RegionStat
        rows.Scan(&st.Region, &st.VideoCount, &st.TotalViews)
        stats = append(stats, st)
    }
    json.NewEncoder(w).Encode(stats)
}
Enter fullscreen mode Exit fullscreen mode

Trending Categories Endpoint

type CategoryStat struct {
    Name       string  `json:"name"`
    VideoCount int     `json:"video_count"`
    SharePct   float64 `json:"share_pct"`
}

func (s *Server) trendingCategories(w http.ResponseWriter, r *http.Request) {
    region := r.URL.Query().Get("region")
    query := `
        SELECT c.name, COUNT(v.id) AS cnt,
               ROUND(COUNT(v.id) * 100.0 / SUM(COUNT(v.id)) OVER (), 1) AS share
        FROM videos v
        JOIN categories c ON c.id = v.category_id`
    args := []any{}
    if region != "" {
        query += " JOIN video_regions vr ON vr.video_id = v.id WHERE vr.region = ?"
        args = append(args, region)
    }
    query += " GROUP BY c.name ORDER BY cnt DESC LIMIT 10"

    rows, err := s.db.Query(query, args...)
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    defer rows.Close()

    var stats []CategoryStat
    for rows.Next() {
        var st CategoryStat
        rows.Scan(&st.Name, &st.VideoCount, &st.SharePct)
        stats = append(stats, st)
    }
    json.NewEncoder(w).Encode(stats)
}
Enter fullscreen mode Exit fullscreen mode

Peak Hours Endpoint

type HourStat struct {
    Hour  int `json:"hour"`
    Count int `json:"count"`
}

func (s *Server) peakHours(w http.ResponseWriter, r *http.Request) {
    rows, err := s.db.Query(`
        SELECT CAST(strftime('%H', fetched_at) AS INTEGER) AS hour, COUNT(*) AS cnt
        FROM videos WHERE fetched_at > datetime('now', '-7 days')
        GROUP BY hour ORDER BY hour
    `)
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    defer rows.Close()

    stats := make([]HourStat, 0, 24)
    for rows.Next() {
        var h HourStat
        rows.Scan(&h.Hour, &h.Count)
        stats = append(stats, h)
    }
    json.NewEncoder(w).Encode(stats)
}
Enter fullscreen mode Exit fullscreen mode

Chart.js Dashboard

async function loadAnalytics() {
  const BASE = 'https://analytics.topvideohub.com';
  const [regions, categories, hours] = await Promise.all([
    fetch(`${BASE}/api/analytics/views-by-region`).then(r => r.json()),
    fetch(`${BASE}/api/analytics/trending-categories?region=JP`).then(r => r.json()),
    fetch(`${BASE}/api/analytics/peak-hours`).then(r => r.json()),
  ]);

  new Chart(document.getElementById('regionChart'), {
    type: 'bar',
    data: {
      labels: regions.map(r => r.region),
      datasets: [{ label: 'Total Views', data: regions.map(r => r.total_views),
        backgroundColor: 'rgba(99,102,241,0.7)' }],
    },
    options: { indexAxis: 'y' },
  });

  new Chart(document.getElementById('categoryChart'), {
    type: 'doughnut',
    data: { labels: categories.map(c => c.name),
      datasets: [{ data: categories.map(c => c.share_pct) }] },
  });

  new Chart(document.getElementById('peakChart'), {
    type: 'line',
    data: { labels: hours.map(h => `${h.hour}:00`),
      datasets: [{ label: 'Fetched Videos', data: hours.map(h => h.count),
        tension: 0.3, fill: true }] },
  });
}
loadAnalytics();
Enter fullscreen mode Exit fullscreen mode

For TopVideoHub, the peak hours chart consistently shows spikes at 19-22 JST as Japanese audiences come home and browse trending content.


This article is part of the Building TopVideoHub series. Check out TopVideoHub to see these techniques in action.

Top comments (0)