Generates self-contained interactive HTML dashboards with KPI cards, charts, filters, and tables from queries, CSVs, or samples for reports and monitoring.
From datanpx claudepluginhub cy-wali/knowledge --plugin dataThis skill uses the workspace's default tool permissions.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Designs, audits, and improves analytics tracking systems using Signal Quality Index for reliable, decision-ready data in marketing, product, and growth.
Enforces A/B test setup with gates for hypothesis locking, metrics definition, sample size calculation, assumptions checks, and execution readiness before implementation.
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.
/build-dashboard <description of dashboard> [data source]
Determine:
If data warehouse is connected:
If data is pasted or uploaded:
If working from a description without data:
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:
Generate a single self-contained HTML file using the base template below. The file includes:
Structure (HTML):
Styling (CSS):
Interactivity (JavaScript):
Data (embedded JSON):
Use Chart.js for all charts. Common dashboard chart patterns:
Use the Chart.js integration patterns below for each chart type.
Use the filter and interactivity implementation patterns below for dropdown filters, date range filters, combined filter logic, sortable tables, and chart updates.
sales_dashboard.html)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>
<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();
}
}
<div class="chart-container">
<h3 class="chart-title">Monthly Revenue Trend</h3>
<canvas id="revenue-chart"></canvas>
</div>
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');
}
}
}
}
}
});
}
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');
}
} : {}
}
}
}
});
}
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}%)`;
}
}
}
}
}
});
}
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
}
<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;
}
<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;
});
}
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();
}
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);
}
: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;
}
* {
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-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-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 {
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);
}
.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;
}
@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; }
}
| 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. |
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,
}
};
animation: false in Chart.js optionsChart.update('none') instead of Chart.update() for filter-triggered updatesrequestAnimationFrame for coordinated chart updates// 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"
}
/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.