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