DEV Community

ahmet gedik
ahmet gedik

Posted on

Building a Video Analytics Dashboard with Go and Chart.js

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"`
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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))
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)