Creating interactive dashboards in static sites might seem contradictory, but Hugo’s flexibility combined with modern JavaScript libraries like Chart.js makes it not only possible but remarkably effective. This guide walks you through building professional, responsive dashboards that can handle complex data visualization while maintaining Hugo’s speed and simplicity. I used the Healthy Brain Network (HBN) EEG dashboard I recently made as an example for this guide. However, you should be able to use this guide to build dashboards for any dataset and integrate it into your Hugo site.

Why Dashboards in Static Sites?

Static sites offer compelling advantages for dashboard development:

  • Performance: No server-side processing means blazing-fast load times
  • Scalability: CDN-friendly and handles traffic spikes effortlessly
  • Cost: Hosting is often free or minimal cost
  • Security: No server vulnerabilities or database attacks
  • Reliability: Static files are inherently more stable

The key insight is that many dashboards are actually read-only data presentations rather than dynamic applications requiring real-time server interactions.

Architecture Overview

A Hugo dashboard follows this architecture:

flowchart TD A[Raw Data Sources] --> B[Data Processing Script] B --> C[JSON Data Files] C --> D[Hugo Static Site] D --> E[Chart.js Visualization] E --> F[Interactive Dashboard] G[Hugo Templates] --> D H[CSS Styling] --> D

Data Flow:

  1. Data Processing: Python/R scripts aggregate raw data into dashboard-ready JSON
  2. Static Integration: JSON files placed in Hugo’s static/ directory
  3. Template Integration: Hugo templates include Chart.js and dashboard JavaScript
  4. Client Rendering: Browsers fetch JSON and render interactive charts

Setting Up Chart.js in Hugo

Add Chart.js to your Hugo site using the rawhtml shortcode for maximum compatibility:

<!-- USE TWO BRACKETS, HERE WE USE ONE TO AVOID BREAKING THE PAGE -->
{< rawhtml >}

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

{< /rawhtml >}

Method 2: Hugo Partials (Advanced)

Create a partial at layouts/partials/dashboard_head.html:

{{- if .Params.dashboard -}}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.chart-container {
    background: #fff;
    border-radius: 8px;
    padding: 1.5rem;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    margin-bottom: 2rem;
}
.chart-canvas {
    position: relative;
    height: 400px;
}
</style>
{{- end -}}

Then include it in your theme’s head:

{{ partial "dashboard_head.html" . }}

Data Management Best Practices

JSON Structure Design

Design your JSON for dashboard efficiency, not database normalization. Here’s the actual structure we used for the HBN Dashboard:

{
  "metadata": {
    "generated_at": "2024-01-14",
    "total_participants": 3602,
    "total_releases": 11
  },
  "overview": {
    "ageDistribution": {
      "bins": ["5y", "6y", "7y", "8y", "9y", "10y"],
      "total": [234, 386, 441, 444, 435, 366],
      "male": [150, 247, 282, 283, 278, 235],
      "female": [84, 139, 159, 161, 157, 131]
    },
    "sexDistribution": {
      "male": 2306,
      "female": 1296
    },
    "taskAvailability": {
      "RestingState": 86.3,
      "DespicableMe": 70.2,
      "RestingState2": 82.1
    }
  },
  "releases": {
    "R1": {
      "participant_count": 578,
      "age_stats": {"mean": 10.7, "std": 3.7, "min": 5.0, "max": 21.8},
      "sex_distribution": {"M": 345, "F": 233},
      "task_status": {
        "RestingState": {
          "available": 557, "caution": 0, "unavailable": 21, "noevent": 0
        }
      }
    }
  }
}

Design Principles:

  • Pre-aggregated Data: Calculate percentages, totals, and derived metrics during processing
  • Chart-Ready Format: Structure data exactly as Chart.js expects it
  • Hierarchical Organization: Group related data for easy access
  • Metadata Inclusion: Add generation timestamps and data provenance

Data Processing Pipeline

Create a Python script to transform raw data into dashboard JSON. Based on our actual TSV processing:

import pandas as pd
import json
from datetime import datetime

def process_hbn_data(tsv_path, output_path):
    """Transform HBN TSV data into dashboard-ready JSON"""
    
    # Load the master participants list
    df = pd.read_csv(tsv_path, sep='\t')
    
    # Overview statistics
    overview = {
        "ageDistribution": {
            "bins": [f"{age}y" for age in range(5, 22)],
            "total": df.groupby('Age')['EID'].count().tolist(),
            "male": df[df['Sex'] == 'M'].groupby('Age')['EID'].count().tolist(),
            "female": df[df['Sex'] == 'F'].groupby('Age')['EID'].count().tolist()
        },
        "sexDistribution": {
            "male": len(df[df['Sex'] == 'M']),
            "female": len(df[df['Sex'] == 'F'])
        }
    }
    
    # Per-release analysis
    releases = {}
    for release in df['Release'].unique():
        release_df = df[df['Release'] == release]
        releases[f"R{release}"] = {
            "participant_count": len(release_df),
            "age_stats": {
                "mean": round(release_df['Age'].mean(), 1),
                "std": round(release_df['Age'].std(), 1),
                "min": release_df['Age'].min(),
                "max": release_df['Age'].max()
            },
            "sex_distribution": {
                "M": len(release_df[release_df['Sex'] == 'M']),
                "F": len(release_df[release_df['Sex'] == 'F'])
            }
        }
    
    # Assemble final structure
    dashboard_data = {
        "metadata": {
            "generated_at": datetime.now().isoformat(),
            "total_participants": len(df),
            "total_releases": df['Release'].nunique()
        },
        "overview": overview,
        "releases": releases
    }
    
    # Write to static directory
    with open(output_path, 'w') as f:
        json.dump(dashboard_data, f, indent=2)
    
    print(f"Dashboard data generated: {output_path}")

# Usage - based on our actual implementation
process_hbn_data(
    'PATH/TO/FILE/participants.tsv',
    'static/hbn_dashboard_data.json'
)

Chart.js Integration Patterns

Basic Chart Setup

// Global dashboard configuration - matches our actual implementation
const chartConfig = {
    responsive: true,
    maintainAspectRatio: false,
    plugins: {
        legend: { display: true, position: 'bottom' },
        tooltip: { mode: 'index', intersect: false }
    }
};

// Load dashboard data
let hbnData = null;

fetch('/hbn_dashboard_data.json')
    .then(response => response.json())
    .then(data => {
        hbnData = data;
        initializeDashboard();
    })
    .catch(error => console.error('Data loading failed:', error));

function initializeDashboard() {
    if (!hbnData) return;
    
    createOverviewCharts();
    createReleaseAnalysis();
    updateMainTaskCharts();
}

Chart Creation Functions

function createOverviewCharts() {
    // Age distribution chart - actual implementation
    const ageCtx = document.getElementById('ageChart').getContext('2d');
    new Chart(ageCtx, {
        type: 'bar',
        data: {
            labels: hbnData.overview.ageDistribution.bins,
            datasets: [{
                label: 'Total Participants',
                data: hbnData.overview.ageDistribution.total,
                backgroundColor: 'rgba(54, 162, 235, 0.8)',
                borderColor: 'rgba(54, 162, 235, 1)',
                borderWidth: 1
            }]
        },
        options: {
            ...chartConfig,
            scales: {
                y: { beginAtZero: true, title: { display: true, text: 'Count' } },
                x: { title: { display: true, text: 'Age (years)' } }
            }
        }
    });
}

function createReleaseAnalysis() {
    // Task pie charts - per our actual implementation
    const releases = Object.keys(hbnData.releases);
    releases.forEach(release => {
        createTaskPieCharts(release);
    });
}

Advanced Features

Interactive Controls

Add dynamic filtering and selection to your dashboard:

<!-- Control Panel -->
<div class="dashboard-controls">
    <div class="control-group">
        <label for="releaseSelector">Select Release:</label>
        <select id="releaseSelector" onchange="updateMainTaskCharts()">
            <option value="R1">Release 1</option>
            <option value="R2">Release 2</option>
            <option value="R3">Release 3</option>
        </select>
    </div>
    
    <div class="control-group">
        <label for="sexHueToggle">Show Sex Breakdown:</label>
        <button id="sexHueToggle" onclick="toggleSexHue()">Show Sex Breakdown</button>
    </div>
</div>
function updateMainTaskCharts() {
    const selectedRelease = document.getElementById('releaseSelector').value || 'R1';
    console.log('Creating main task charts for:', selectedRelease);
    
    if (!hbnData || !hbnData.releases) {
        console.log('Data not ready yet');
        return;
    }
    
    updateReleaseInsights(selectedRelease);
    createMainTaskPieCharts(selectedRelease);
}

function updateReleaseInsights(release) {
    const releaseData = hbnData.releases[release];
    if (!releaseData) return;
    
    // Update participant count
    document.getElementById('releaseParticipantCount').textContent = 
        releaseData.participant_count.toLocaleString();
    
    // Update gender distribution
    const malePercent = ((releaseData.sex_distribution.M / releaseData.participant_count) * 100).toFixed(1);
    const femalePercent = ((releaseData.sex_distribution.F / releaseData.participant_count) * 100).toFixed(1);
    document.getElementById('releaseGenderSplit').textContent = `${malePercent}% M / ${femalePercent}% F`;
    
    // Update age range
    document.getElementById('releaseAgeRange').textContent = 
        `${releaseData.age_stats.min}-${releaseData.age_stats.max}`;
}

Responsive Design

Ensure your dashboard works across devices:

.dashboard-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 1.5rem;
    margin: 2rem 0;
}

.chart-container {
    background: #fff;
    border-radius: 8px;
    padding: 1.5rem;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    min-height: 400px;
}

.per-release-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 1.5rem;
    margin-top: 1rem;
}

@media (max-width: 768px) {
    .dashboard-grid {
        grid-template-columns: 1fr;
        gap: 1rem;
    }
    
    .chart-container {
        padding: 1rem;
        min-height: 300px;
    }
}

Performance Optimization

Data Loading Strategies:

// Progressive enhancement - based on our implementation
document.addEventListener('DOMContentLoaded', function() {
    // Load critical charts first
    if (hbnData) {
        initializeDashboard();
    }
    
    // Initialize main task charts immediately
    setTimeout(() => {
        const mainSelector = document.getElementById('mainReleaseSelector');
        if (mainSelector) {
            mainSelector.value = 'R1';
            console.log('Main selector set to R1');
        }
        console.log('Initializing release insights and charts...');
        updateMainTaskCharts();
    }, 100);
});

Deployment and Maintenance

Automation Pipeline

# GitHub Actions example for dashboard updates
name: Update Dashboard Data
on:
  schedule:
    - cron: '0 6 * * *'  # Daily at 6 AM
  
jobs:
  update-data:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Setup Python
        uses: actions/setup-python@v2
        with:
          python-version: '3.9'
      - name: Install dependencies
        run: pip install pandas numpy
      - name: Generate dashboard data
        run: python scripts/generate_dashboard_data.py
      - name: Build Hugo site
        run: hugo --minify
      - name: Deploy
        run: # Your deployment command

Monitoring and Analytics

Track dashboard usage and performance:

// Basic analytics tracking
function trackChartInteraction(chartType, action) {
    if (typeof gtag !== 'undefined') {
        gtag('event', 'chart_interaction', {
            'chart_type': chartType,
            'action': action,
            'page_title': document.title
        });
    }
}

// Performance monitoring
function measureChartRenderTime(chartType, startTime) {
    const renderTime = performance.now() - startTime;
    console.log(`${chartType} rendered in ${renderTime.toFixed(2)}ms`);
    
    // Send to analytics if needed
    trackChartInteraction(chartType, 'render_complete');
}

Troubleshooting Common Issues

Chart Not Rendering:

  • Verify Chart.js is loaded before your dashboard scripts
  • Check that canvas elements exist in the DOM
  • Ensure data is loaded before chart initialization

Performance Issues:

  • Minimize JSON file sizes through data aggregation
  • Use Chart.js datasets efficiently
  • Implement lazy loading for complex dashboards

Mobile Responsiveness:

  • Set maintainAspectRatio: false in Chart.js options
  • Use CSS Grid or Flexbox for responsive layouts
  • Test on various screen sizes

Hugo dashboards combine the best of static site performance with modern data visualization capabilities. By following these patterns—structured JSON data, Chart.js integration, and responsive design—you can create professional dashboards that are fast, maintainable, and highly effective for data communication.

The key success factors are:

  1. Data-First Design: Structure your JSON for dashboard consumption
  2. Progressive Enhancement: Start simple, add complexity gradually
  3. Performance Focus: Optimize for fast loading and smooth interactions
  4. Mobile Responsiveness: Ensure dashboards work across all devices

This approach has proven effective for research data visualization, business metrics, and any scenario where you need to present complex data in an accessible, interactive format.

© 2025 Seyed Yahya Shirazi. All rights reserved.