Install
Terminal · npx$
npx skills add https://github.com/obra/superpowers --skill brainstormingWorks 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 packSource file
SKILL.md924 linesExpandCollapse
---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 brandRelated skills
Accessibility Review
Install Accessibility Review skill for Claude Code from anthropics/knowledge-work-plugins.
Account Research
Install Account Research skill for Claude Code from anthropics/knowledge-work-plugins.
Algorithmic Art
When you want to create generative art that's actually algorithmic rather than just randomized shapes, this skill follows a two-step process that works surprisi