Claude Agent Skill · by Anthropics

Build Dashboard

Install Build Dashboard skill for Claude Code from anthropics/knowledge-work-plugins.

Install
Terminal · npx
$npx skills add https://github.com/obra/superpowers --skill brainstorming
Works with Paperclip

How Build Dashboard fits into a Paperclip company.

Build Dashboard drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md924 lines
Expand
---name: build-dashboarddescription: Build an interactive HTML dashboard with charts, filters, and tables. Use when creating 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 Dashboards > If you see unfamiliar placeholders or need to check which tools are connected, see [CONNECTORS.md](../../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 ### 1. 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 ### 2. Gather the Data **If data warehouse is connected:**1. Query the necessary data2. Embed the results as JSON within the HTML file **If data is pasted or uploaded:**1. Parse and clean the data2. Embed as JSON in the dashboard **If working from a description without data:**1. Create a realistic sample dataset matching the described schema2. Note in the dashboard that it uses sample data3. Provide instructions for swapping in real data ### 3. 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 ### 4. 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 ### 5. 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. ### 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 1. Save the dashboard as an HTML file with a descriptive name (e.g., `sales_dashboard.html`)2. Open it in the user's default browser3. Confirm it renders correctly4. Provide instructions for updating data or customizing --- ## Base Template Every dashboard follows this structure: ```html<!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 ```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>``` ```javascriptfunction 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 ```html<div class="chart-container">    <h3 class="chart-title">Monthly Revenue Trend</h3>    <canvas id="revenue-chart"></canvas></div>``` ### Line Chart ```javascriptfunction 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 ```javascriptfunction 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 ```javascriptfunction 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 ```javascriptfunction 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 ```html<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>``` ```javascriptfunction 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 ```html<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>``` ```javascriptfunction 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 ```javascriptapplyFilters() {    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 ```javascriptfunction 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 ```css: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 ```css* {    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 ```css.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 ```css.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 ```css.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 ```css.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 ```css@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: ```javascript// DON'T: embed 50,000 raw rowsconst RAW_DATA = [/* 50,000 rows */]; // DO: pre-aggregate before embeddingconst 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 ```javascript// Efficient table paginationfunction 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