/build-dashboard - Build Interactive Dashboards
If you see unfamiliar placeholders or need to check which tools are connected, see CONNECTORS.md.
Build a self-contained interactive HTML dashboard with charts, filters, tables, and professional styling. Opens directly in a browser -- no server or dependencies required.
Usage
/build-dashboard <description of dashboard> [data source]
Workflow
- Understand the Dashboard Requirements
Determine:
-
Purpose: Executive overview, operational monitoring, deep-dive analysis, team reporting
-
Audience: Who will use this dashboard?
-
Key metrics: What numbers matter most?
-
Dimensions: What should users be able to filter or slice by?
-
Data source: Live query, pasted data, CSV file, or sample data
- Gather the Data
If data warehouse is connected:
-
Query the necessary data
-
Embed the results as JSON within the HTML file
If data is pasted or uploaded:
-
Parse and clean the data
-
Embed as JSON in the dashboard
If working from a description without data:
-
Create a realistic sample dataset matching the described schema
-
Note in the dashboard that it uses sample data
-
Provide instructions for swapping in real data
- Design the Dashboard Layout
Follow a standard dashboard layout pattern:
┌──────────────────────────────────────────────────┐ │ Dashboard Title [Filters ▼] │ ├────────────┬────────────┬────────────┬───────────┤ │ KPI Card │ KPI Card │ KPI Card │ KPI Card │ ├────────────┴────────────┼────────────┴───────────┤ │ │ │ │ Primary Chart │ Secondary Chart │ │ (largest area) │ │ │ │ │ ├─────────────────────────┴────────────────────────┤ │ │ │ Detail Table (sortable, scrollable) │ │ │ └──────────────────────────────────────────────────┘
Adapt the layout to the content:
-
2-4 KPI cards at the top for headline numbers
-
1-3 charts in the middle section for trends and breakdowns
-
Optional detail table at the bottom for drill-down data
-
Filters in the header or sidebar depending on complexity
- Build the HTML Dashboard
Generate a single self-contained HTML file using the base template below. The file includes:
Structure (HTML):
-
Semantic HTML5 layout
-
Responsive grid using CSS Grid or Flexbox
-
Filter controls (dropdowns, date pickers, toggles)
-
KPI cards with values and labels
-
Chart containers
-
Data table with sortable headers
Styling (CSS):
-
Professional color scheme (clean whites, grays, with accent colors for data)
-
Card-based layout with subtle shadows
-
Consistent typography (system fonts for fast loading)
-
Responsive design that works on different screen sizes
-
Print-friendly styles
Interactivity (JavaScript):
-
Chart.js for interactive charts (included via CDN)
-
Filter dropdowns that update all charts and tables simultaneously
-
Sortable table columns
-
Hover tooltips on charts
-
Number formatting (commas, currency, percentages)
Data (embedded JSON):
-
All data embedded directly in the HTML as JavaScript variables
-
No external data fetches required
-
Dashboard works completely offline
- Implement Chart Types
Use Chart.js for all charts. Common dashboard chart patterns:
-
Line chart: Time series trends
-
Bar chart: Category comparisons
-
Doughnut chart: Composition (when <6 categories)
-
Stacked bar: Composition over time
-
Mixed (bar + line): Volume with rate overlay
Use the Chart.js integration patterns below for each chart type.
- Add Interactivity
Use the filter and interactivity implementation patterns below for dropdown filters, date range filters, combined filter logic, sortable tables, and chart updates.
- Save and Open
-
Save the dashboard as an HTML file with a descriptive name (e.g., sales_dashboard.html )
-
Open it in the user's default browser
-
Confirm it renders correctly
-
Provide instructions for updating data or customizing
Base Template
Every dashboard follows this structure:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Dashboard Title</title> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1" integrity="sha384-jb8JQMbMoBUzgWatfe6COACi2ljcDdZQ2OxczGA3bGNeWe+6DChMTBJemed7ZnvJ" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0" integrity="sha384-cVMg8E3QFwTvGCDuK+ET4PD341jF3W8nO1auiXfuZNQkzbUUiBGLsIQUE+b1mxws" crossorigin="anonymous"></script> <style> /* Dashboard styles go here */ </style> </head> <body> <div class="dashboard-container"> <header class="dashboard-header"> <h1>Dashboard Title</h1> <div class="filters"> <!-- Filter controls --> </div> </header>
<section class="kpi-row">
<!-- KPI cards -->
</section>
<section class="chart-row">
<!-- Chart containers -->
</section>
<section class="table-section">
<!-- Data table -->
</section>
<footer class="dashboard-footer">
<span>Data as of: <span id="data-date"></span></span>
</footer>
</div>
<script>
// Embedded data
const DATA = [];
// Dashboard logic
class Dashboard {
constructor(data) {
this.rawData = data;
this.filteredData = data;
this.charts = {};
this.init();
}
init() {
this.setupFilters();
this.renderKPIs();
this.renderCharts();
this.renderTable();
}
applyFilters() {
// Filter logic
this.filteredData = this.rawData.filter(row => {
// Apply each active filter
return true; // placeholder
});
this.renderKPIs();
this.updateCharts();
this.renderTable();
}
// ... methods for each section
}
const dashboard = new Dashboard(DATA);
</script>
</body> </html>
KPI Card Pattern
<div class="kpi-card"> <div class="kpi-label">Total Revenue</div> <div class="kpi-value" id="kpi-revenue">$0</div> <div class="kpi-change positive" id="kpi-revenue-change">+0%</div> </div>
function renderKPI(elementId, value, previousValue, format = 'number') { const el = document.getElementById(elementId); const changeEl = document.getElementById(elementId + '-change');
// Format the value
el.textContent = formatValue(value, format);
// Calculate and display change
if (previousValue && previousValue !== 0) {
const pctChange = ((value - previousValue) / previousValue) * 100;
const sign = pctChange >= 0 ? '+' : '';
changeEl.textContent = `${sign}${pctChange.toFixed(1)}% vs prior period`;
changeEl.className = `kpi-change ${pctChange >= 0 ? 'positive' : 'negative'}`;
}
}
function formatValue(value, format) {
switch (format) {
case 'currency':
if (value >= 1e6) return $${(value / 1e6).toFixed(1)}M;
if (value >= 1e3) return $${(value / 1e3).toFixed(1)}K;
return $${value.toFixed(0)};
case 'percent':
return ${value.toFixed(1)}%;
case 'number':
if (value >= 1e6) return ${(value / 1e6).toFixed(1)}M;
if (value >= 1e3) return ${(value / 1e3).toFixed(1)}K;
return value.toLocaleString();
default:
return value.toString();
}
}
Chart.js Integration
Chart Container Pattern
<div class="chart-container"> <h3 class="chart-title">Monthly Revenue Trend</h3> <canvas id="revenue-chart"></canvas> </div>
Line Chart
function createLineChart(canvasId, labels, datasets) {
const ctx = document.getElementById(canvasId).getContext('2d');
return new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: datasets.map((ds, i) => ({
label: ds.label,
data: ds.data,
borderColor: COLORS[i % COLORS.length],
backgroundColor: COLORS[i % COLORS.length] + '20',
borderWidth: 2,
fill: ds.fill || false,
tension: 0.3,
pointRadius: 3,
pointHoverRadius: 6,
}))
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false,
},
plugins: {
legend: {
position: 'top',
labels: { usePointStyle: true, padding: 20 }
},
tooltip: {
callbacks: {
label: function(context) {
return ${context.dataset.label}: ${formatValue(context.parsed.y, 'currency')};
}
}
}
},
scales: {
x: {
grid: { display: false }
},
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return formatValue(value, 'currency');
}
}
}
}
}
});
}
Bar Chart
function createBarChart(canvasId, labels, data, options = {}) { const ctx = document.getElementById(canvasId).getContext('2d'); const isHorizontal = options.horizontal || labels.length > 8;
return new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: options.label || 'Value',
data: data,
backgroundColor: options.colors || COLORS.map(c => c + 'CC'),
borderColor: options.colors || COLORS,
borderWidth: 1,
borderRadius: 4,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
indexAxis: isHorizontal ? 'y' : 'x',
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: function(context) {
return formatValue(context.parsed[isHorizontal ? 'x' : 'y'], options.format || 'number');
}
}
}
},
scales: {
x: {
beginAtZero: true,
grid: { display: isHorizontal },
ticks: isHorizontal ? {
callback: function(value) {
return formatValue(value, options.format || 'number');
}
} : {}
},
y: {
beginAtZero: !isHorizontal,
grid: { display: !isHorizontal },
ticks: !isHorizontal ? {
callback: function(value) {
return formatValue(value, options.format || 'number');
}
} : {}
}
}
}
});
}
Doughnut Chart
function createDoughnutChart(canvasId, labels, data) {
const ctx = document.getElementById(canvasId).getContext('2d');
return new Chart(ctx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: COLORS.map(c => c + 'CC'),
borderColor: '#ffffff',
borderWidth: 2,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '60%',
plugins: {
legend: {
position: 'right',
labels: { usePointStyle: true, padding: 15 }
},
tooltip: {
callbacks: {
label: function(context) {
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const pct = ((context.parsed / total) * 100).toFixed(1);
return ${context.label}: ${formatValue(context.parsed, 'number')} (${pct}%);
}
}
}
}
}
});
}
Updating Charts on Filter Change
function updateChart(chart, newLabels, newData) { chart.data.labels = newLabels;
if (Array.isArray(newData[0])) {
// Multiple datasets
newData.forEach((data, i) => {
chart.data.datasets[i].data = data;
});
} else {
chart.data.datasets[0].data = newData;
}
chart.update('none'); // 'none' disables animation for instant update
}
Filter and Interactivity Implementation
Dropdown Filter
<div class="filter-group"> <label for="filter-region">Region</label> <select id="filter-region" onchange="dashboard.applyFilters()"> <option value="all">All Regions</option> </select> </div>
function populateFilter(selectId, data, field) { const select = document.getElementById(selectId); const values = [...new Set(data.map(d => d[field]))].sort();
// Keep the "All" option, add unique values
values.forEach(val => {
const option = document.createElement('option');
option.value = val;
option.textContent = val;
select.appendChild(option);
});
}
function getFilterValue(selectId) { const val = document.getElementById(selectId).value; return val === 'all' ? null : val; }
Date Range Filter
<div class="filter-group"> <label>Date Range</label> <input type="date" id="filter-date-start" onchange="dashboard.applyFilters()"> <span>to</span> <input type="date" id="filter-date-end" onchange="dashboard.applyFilters()"> </div>
function filterByDateRange(data, dateField, startDate, endDate) { return data.filter(row => { const rowDate = new Date(row[dateField]); if (startDate && rowDate < new Date(startDate)) return false; if (endDate && rowDate > new Date(endDate)) return false; return true; }); }
Combined Filter Logic
applyFilters() { const region = getFilterValue('filter-region'); const category = getFilterValue('filter-category'); const startDate = document.getElementById('filter-date-start').value; const endDate = document.getElementById('filter-date-end').value;
this.filteredData = this.rawData.filter(row => {
if (region && row.region !== region) return false;
if (category && row.category !== category) return false;
if (startDate && row.date < startDate) return false;
if (endDate && row.date > endDate) return false;
return true;
});
this.renderKPIs();
this.updateCharts();
this.renderTable();
}
Sortable Table
function renderTable(containerId, data, columns) { const container = document.getElementById(containerId); let sortCol = null; let sortDir = 'desc';
function render(sortedData) {
let html = '<table class="data-table">';
// Header
html += '<thead><tr>';
columns.forEach(col => {
const arrow = sortCol === col.field
? (sortDir === 'asc' ? ' ▲' : ' ▼')
: '';
html += `<th onclick="sortTable('${col.field}')" style="cursor:pointer">${col.label}${arrow}</th>`;
});
html += '</tr></thead>';
// Body
html += '<tbody>';
sortedData.forEach(row => {
html += '<tr>';
columns.forEach(col => {
const value = col.format ? formatValue(row[col.field], col.format) : row[col.field];
html += `<td>${value}</td>`;
});
html += '</tr>';
});
html += '</tbody></table>';
container.innerHTML = html;
}
window.sortTable = function(field) {
if (sortCol === field) {
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
} else {
sortCol = field;
sortDir = 'desc';
}
const sorted = [...data].sort((a, b) => {
const aVal = a[field], bVal = b[field];
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
return sortDir === 'asc' ? cmp : -cmp;
});
render(sorted);
};
render(data);
}
CSS Styling for Dashboards
Color System
:root { /* Background layers */ --bg-primary: #f8f9fa; --bg-card: #ffffff; --bg-header: #1a1a2e;
/* Text */
--text-primary: #212529;
--text-secondary: #6c757d;
--text-on-dark: #ffffff;
/* Accent colors for data */
--color-1: #4C72B0;
--color-2: #DD8452;
--color-3: #55A868;
--color-4: #C44E52;
--color-5: #8172B3;
--color-6: #937860;
/* Status colors */
--positive: #28a745;
--negative: #dc3545;
--neutral: #6c757d;
/* Spacing */
--gap: 16px;
--radius: 8px;
}
Layout
- { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg-primary); color: var(--text-primary); line-height: 1.5; }
.dashboard-container { max-width: 1400px; margin: 0 auto; padding: var(--gap); }
.dashboard-header { background: var(--bg-header); color: var(--text-on-dark); padding: 20px 24px; border-radius: var(--radius); margin-bottom: var(--gap); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; }
.dashboard-header h1 { font-size: 20px; font-weight: 600; }
KPI Cards
.kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--gap); margin-bottom: var(--gap); }
.kpi-card { background: var(--bg-card); border-radius: var(--radius); padding: 20px 24px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); }
.kpi-label { font-size: 13px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
.kpi-value { font-size: 28px; font-weight: 700; color: var(--text-primary); margin-bottom: 4px; }
.kpi-change { font-size: 13px; font-weight: 500; }
.kpi-change.positive { color: var(--positive); } .kpi-change.negative { color: var(--negative); }
Chart Containers
.chart-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: var(--gap); margin-bottom: var(--gap); }
.chart-container { background: var(--bg-card); border-radius: var(--radius); padding: 20px 24px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); }
.chart-container h3 { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 16px; }
.chart-container canvas { max-height: 300px; }
Filters
.filters { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.filter-group { display: flex; align-items: center; gap: 6px; }
.filter-group label { font-size: 12px; color: rgba(255, 255, 255, 0.7); }
.filter-group select, .filter-group input[type="date"] { padding: 6px 10px; border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 4px; background: rgba(255, 255, 255, 0.1); color: var(--text-on-dark); font-size: 13px; }
.filter-group select option { background: var(--bg-header); color: var(--text-on-dark); }
Data Table
.table-section { background: var(--bg-card); border-radius: var(--radius); padding: 20px 24px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); overflow-x: auto; }
.data-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.data-table thead th { text-align: left; padding: 10px 12px; border-bottom: 2px solid #dee2e6; color: var(--text-secondary); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; user-select: none; }
.data-table thead th:hover { color: var(--text-primary); background: #f8f9fa; }
.data-table tbody td { padding: 10px 12px; border-bottom: 1px solid #f0f0f0; }
.data-table tbody tr:hover { background: #f8f9fa; }
.data-table tbody tr:last-child td { border-bottom: none; }
Responsive Design
@media (max-width: 768px) { .dashboard-header { flex-direction: column; align-items: flex-start; }
.kpi-row {
grid-template-columns: repeat(2, 1fr);
}
.chart-row {
grid-template-columns: 1fr;
}
.filters {
flex-direction: column;
align-items: flex-start;
}
}
@media print { body { background: white; } .dashboard-container { max-width: none; } .filters { display: none; } .chart-container { break-inside: avoid; } .kpi-card { border: 1px solid #dee2e6; box-shadow: none; } }
Performance Considerations for Large Datasets
Data Size Guidelines
Data Size Approach
<1,000 rows Embed directly in HTML. Full interactivity.
1,000 - 10,000 rows Embed in HTML. May need to pre-aggregate for charts.
10,000 - 100,000 rows Pre-aggregate server-side. Embed only aggregated data.
100,000 rows Not suitable for client-side dashboard. Use a BI tool or paginate.
Pre-Aggregation Pattern
Instead of embedding raw data and aggregating in the browser:
// DON'T: embed 50,000 raw rows const RAW_DATA = [/* 50,000 rows */];
// DO: pre-aggregate before embedding const CHART_DATA = { monthly_revenue: [ { month: '2024-01', revenue: 150000, orders: 1200 }, { month: '2024-02', revenue: 165000, orders: 1350 }, // ... 12 rows instead of 50,000 ], top_products: [ { product: 'Widget A', revenue: 45000 }, // ... 10 rows ], kpis: { total_revenue: 1980000, total_orders: 15600, avg_order_value: 127, } };
Chart Performance
-
Limit line charts to <500 data points per series (downsample if needed)
-
Limit bar charts to <50 categories
-
For scatter plots, cap at 1,000 points (use sampling for larger datasets)
-
Disable animations for dashboards with many charts: animation: false in Chart.js options
-
Use Chart.update('none') instead of Chart.update() for filter-triggered updates
DOM Performance
-
Limit data tables to 100-200 visible rows. Add pagination for more.
-
Use requestAnimationFrame for coordinated chart updates
-
Avoid rebuilding the entire DOM on filter change -- update only changed elements
// Efficient table pagination function renderTablePage(data, page, pageSize = 50) { const start = page * pageSize; const end = Math.min(start + pageSize, data.length); const pageData = data.slice(start, end); // Render only pageData // Show pagination controls: "Showing 1-50 of 2,340" }
Examples
/build-dashboard Monthly sales dashboard with revenue trend, top products, and regional breakdown. Data is in the orders table.
/build-dashboard Here's our support ticket data [pastes CSV]. Build a dashboard showing volume by priority, response time trends, and resolution rates.
/build-dashboard Create a template executive dashboard for a SaaS company showing MRR, churn, new customers, and NPS. Use sample data.
Tips
-
Dashboards are fully self-contained HTML files -- share them with anyone by sending the file
-
For real-time dashboards, consider connecting to a BI tool instead. These dashboards are point-in-time snapshots
-
Request "dark mode" or "presentation mode" for different styling
-
You can request a specific color scheme to match your brand