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:
Data Flow:
- Data Processing: Python/R scripts aggregate raw data into dashboard-ready JSON
- Static Integration: JSON files placed in Hugo’s
static/
directory - Template Integration: Hugo templates include Chart.js and dashboard JavaScript
- Client Rendering: Browsers fetch JSON and render interactive charts
Setting Up Chart.js in Hugo
Method 1: CDN Integration (Recommended)
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:
- Data-First Design: Structure your JSON for dashboard consumption
- Progressive Enhancement: Start simple, add complexity gradually
- Performance Focus: Optimize for fast loading and smooth interactions
- 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.