| name | build-dashboard |
| description | Build an interactive HTML dashboard — a Data Schematic — with charts, filters, and tables. Use when creating a dashboard, an executive overview with KPI cards, turning query results into a shareable self-contained report, building a team monitoring snapshot, or needing multiple charts with filters in one browser-openable file. |
| argument-hint | <description> [data source] |
/build-dashboard - Build Interactive Data Schematics
Build a self-contained interactive HTML Data Schematic with charts, filters, tables, and professional styling. Opens directly in a browser -- no server or dependencies required.
Usage
/build-dashboard <description of Data Schematic> [data source]
Workflow
1. Understand the Data Schematic Requirements
Determine:
- Purpose: Executive overview, operational monitoring, deep-dive analysis, team reporting
- Audience: Who will use this Data Schematic?
- Key metrics: What numbers matter most?
- Dimensions: What should users be able to filter or slice by?
- Data source: CSV/TSV file, query results, pasted data, or sample data
2. Gather the Data
If data is in CSV/TSV files (common with qsv):
- Use qsv commands to explore and pre-aggregate the data:
qsv stats --cardinality --stats-jsonl to understand columns and distributions
qsv frequency to see value distributions for categorical columns
qsv sqlp to run SQL aggregations (GROUP BY, SUM, COUNT, etc.)
qsv slice to sample large files before embedding
- Embed the aggregated results as JSON within the HTML file
If data is pasted or uploaded:
- Parse and clean the data
- Embed as JSON in the Data Schematic
If working from a description without data:
- Create a realistic sample dataset matching the described schema
- Note in the Data Schematic that it uses sample data
- Provide instructions for swapping in real data
3. Design the Data Schematic Layout
Follow a standard Data Schematic layout pattern:
+--------------------------------------------------+
| Data Schematic Title [Filters v] |
+------------+------------+------------+-----------+
| 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
4. Build the HTML Data Schematic
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
- Data Schematic works completely offline
5. Implement Chart Types
Use Chart.js for all charts. Common Data Schematic 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.
6. Add Interactivity
Use the filter and interactivity implementation patterns below for dropdown filters, date range filters, combined filter logic, sortable tables, and chart updates.
7. Save and Open
- Save the Data Schematic 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
Data Preparation with qsv
Before embedding data, use qsv to pre-aggregate large datasets:
qsv stats data.csv --cardinality --stats-jsonl
qsv sqlp data.csv "SELECT month, SUM(revenue) as revenue, COUNT(*) as orders FROM data GROUP BY month ORDER BY month"
qsv frequency data.csv --select category --limit 10
qsv slice data.csv --len 200
For datasets over 100K rows, always pre-aggregate with qsv sqlp rather than embedding raw data.
Base Template
Every Data Schematic 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>Data Schematic 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>
</style>
</head>
<body>
<div class="data-schematic-container">
< =>
Data Schematic Title
Data as of:
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');
el.textContent = formatValue(value, format);
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 >= ) ;
;
:
;
:
(value >= ) ;
(value >= ) ;
value.();
:
value.();
}
}
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: {
: ,
: ,
},
: {
: {
: ,
: { : , : }
},
: {
: {
: () {
;
}
}
}
},
: {
: {
: { : }
},
: {
: ,
: {
: () {
(value, );
}
}
}
}
}
});
}
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: },
: {
: {
: () {
(context.[isHorizontal ? : ], options. || );
}
}
}
},
: {
: {
: ,
: { : isHorizontal },
: isHorizontal ? {
: () {
(value, options. || );
}
} : {}
},
: {
: !isHorizontal,
: { : !isHorizontal },
: !isHorizontal ? {
: () {
(value, options. || );
}
} : {}
}
}
}
});
}
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) {
total = context...( a + b, );
pct = ((context. / total) * ).();
;
}
}
}
}
}
});
}
Updating Charts on Filter Change
function updateChart(chart, newLabels, newData) {
chart.data.labels = newLabels;
if (Array.isArray(newData[0])) {
newData.forEach((data, i) => {
chart.data.datasets[i].data = data;
});
} else {
chart.data.datasets[0].data = newData;
}
chart.update('none');
}
Filter and Interactivity Implementation
Dropdown Filter
<div class="filter-group">
<label for="filter-region">Region</label>
<select id="filter-region" onchange="dataSchematic.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();
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="dataSchematic.applyFilters()">
<span>to</span>
<input type="date" id="filter-date-end" onchange="dataSchematic.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">';
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>';
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></td>`;
});
html += ;
});
html += ;
container. = html;
}
. = () {
(sortCol === field) {
sortDir = sortDir === ? : ;
} {
sortCol = field;
sortDir = ;
}
sorted = [...data].( {
aVal = a[field], bVal = b[field];
cmp = aVal < bVal ? - : aVal > bVal ? : ;
sortDir === ? cmp : -cmp;
});
(sorted);
};
(data);
}
CSS Styling for Dashboards
Color System
:root {
--bg-primary: #f8f9fa;
--bg-card: #ffffff;
--bg-header: #1a1a2e;
--text-primary: #212529;
--text-secondary: #6c757d;
--text-on-dark: #ffffff;
--color-1: #4C72B0;
--color-2: #DD8452;
--color-3: #55A868;
--color-4: #C44E52;
--color-5: #8172B3;
--color-6: #937860;
--positive: #28a745;
--negative: #dc3545;
--neutral: #6c757d;
--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;
}
.data-schematic-container {
max-width: 1400px;
margin: 0 auto;
padding: var(--gap);
}
.data-schematic-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;
}
.data-schematic-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;
}
{ : (--positive); }
{ : (--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: (--bg-header);
: (--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);
: ;
}
{
: ;
: solid ;
}
{
: ;
}
{
: none;
}
Responsive Design
@media (max-width: 768px) {
.data-schematic-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; }
.data-schematic-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 with qsv sqlp. Embed only aggregated data. |
| >100,000 rows | Pre-aggregate heavily with qsv sqlp. Consider qsv_to_parquet for archival. Use pagination for detail tables. |
Pre-Aggregation Pattern
Instead of embedding raw data and aggregating in the browser:
const RAW_DATA = [];
const CHART_DATA = {
monthly_revenue: [
{ month: '2024-01', revenue: 150000, orders: 1200 },
{ month: '2024-02', revenue: 165000, orders: 1350 },
],
top_products: [
{ product: 'Widget A', revenue: 45000 },
],
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
function renderTablePage(data, page, pageSize = 50) {
const start = page * pageSize;
const end = Math.min(start + pageSize, data.length);
const pageData = data.slice(start, end);
}
Examples
/build-dashboard Monthly sales Data Schematic with revenue trend, top products, and regional breakdown from sales.csv
/build-dashboard Here's our support ticket data [pastes CSV]. Build a Data Schematic showing volume by priority, response time trends, and resolution rates.
/build-dashboard Create a template executive Data Schematic 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
- Use
qsv stats and qsv frequency to understand your data before designing the Data Schematic layout
- Use
qsv sqlp for complex aggregations (GROUP BY, window functions, pivots) before embedding
- For real-time dashboards, consider 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