<< All versions
Skill v1.0.0
currentAutomated scan100/100minicoohei/ai-agent-camp/interactive-dashboard-builder
──Details
PublishedApril 28, 2026 at 05:31 AM
Content Hashsha256:73c29f31b0f32f0f...
Git SHA9cd83f76f12f
──Files
Files (1 file, 20.4 KB)
SKILL.md20.4 KBactive
SKILL.md · 796 lines · 20.4 KB
version: "1.0.0" name: interactive-dashboard-builder description: "Chart.js を使ったインタラクティブなHTMLダッシュボードを構築するスキル。 「ダッシュボードを作って」「インタラクティブなレポート」「HTMLチャート作成」等のリクエストで発動。 フィルター・グラフ・プロフェッショナルなスタイリング付きのスタンドアロンHTMLを生成。" source: github.com/anthropics/knowledge-work-plugins@main triggers:
- interactive-dashboard-builder
- ダッシュボード作成
- インタラクティブレポート
- HTMLダッシュボード
- Chart.js
- グラフ付きレポート
Interactive Dashboard Builder Skill
Patterns and techniques for building self-contained HTML/JS dashboards with Chart.js, filters, interactivity, and professional styling.
HTML/JS Dashboard Patterns
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 dataconst DATA = [];// Dashboard logicclass Dashboard {constructor(data) {this.rawData = data;this.filteredData = data;this.charts = {};this.init();}init() {this.setupFilters();this.renderKPIs();this.renderCharts();this.renderTable();}applyFilters() {// Filter logicthis.filteredData = this.rawData.filter(row => {// Apply each active filterreturn 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>
javascript
function renderKPI(elementId, value, previousValue, format = 'number') {const el = document.getElementById(elementId);const changeEl = document.getElementById(elementId + '-change');// Format the valueel.textContent = formatValue(value, format);// Calculate and display changeif (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 Container Pattern
html
<div class="chart-container"><h3 class="chart-title">Monthly Revenue Trend</h3><canvas id="revenue-chart"></canvas></div>
Chart.js Integration
Line Chart
javascript
function createLineChart(canvasId, labels, datasets) {const ctx = document.getElementById(canvasId).getContext('2d');return new Chart(ctx, {type: 'line',data: {labels: labels,datasets: datasets.map((ds, i) => ({label: ds.label,data: ds.data,borderColor: COLORS[i % COLORS.length],backgroundColor: COLORS[i % COLORS.length] + '20',borderWidth: 2,fill: ds.fill || false,tension: 0.3,pointRadius: 3,pointHoverRadius: 6,}))},options: {responsive: true,maintainAspectRatio: false,interaction: {mode: 'index',intersect: false,},plugins: {legend: {position: 'top',labels: { usePointStyle: true, padding: 20 }},tooltip: {callbacks: {label: function(context) {return `${context.dataset.label}: ${formatValue(context.parsed.y, 'currency')}`;}}}},scales: {x: {grid: { display: false }},y: {beginAtZero: true,ticks: {callback: function(value) {return formatValue(value, 'currency');}}}}}});}
Bar Chart
javascript
function createBarChart(canvasId, labels, data, options = {}) {const ctx = document.getElementById(canvasId).getContext('2d');const isHorizontal = options.horizontal || labels.length > 8;return new Chart(ctx, {type: 'bar',data: {labels: labels,datasets: [{label: options.label || 'Value',data: data,backgroundColor: options.colors || COLORS.map(c => c + 'CC'),borderColor: options.colors || COLORS,borderWidth: 1,borderRadius: 4,}]},options: {responsive: true,maintainAspectRatio: false,indexAxis: isHorizontal ? 'y' : 'x',plugins: {legend: { display: false },tooltip: {callbacks: {label: function(context) {return formatValue(context.parsed[isHorizontal ? 'x' : 'y'], options.format || 'number');}}}},scales: {x: {beginAtZero: true,grid: { display: isHorizontal },ticks: isHorizontal ? {callback: function(value) {return formatValue(value, options.format || 'number');}} : {}},y: {beginAtZero: !isHorizontal,grid: { display: !isHorizontal },ticks: !isHorizontal ? {callback: function(value) {return formatValue(value, options.format || 'number');}} : {}}}}});}
Doughnut Chart
javascript
function createDoughnutChart(canvasId, labels, data) {const ctx = document.getElementById(canvasId).getContext('2d');return new Chart(ctx, {type: 'doughnut',data: {labels: labels,datasets: [{data: data,backgroundColor: COLORS.map(c => c + 'CC'),borderColor: '#ffffff',borderWidth: 2,}]},options: {responsive: true,maintainAspectRatio: false,cutout: '60%',plugins: {legend: {position: 'right',labels: { usePointStyle: true, padding: 15 }},tooltip: {callbacks: {label: function(context) {const total = context.dataset.data.reduce((a, b) => a + b, 0);const pct = ((context.parsed / total) * 100).toFixed(1);return `${context.label}: ${formatValue(context.parsed, 'number')} (${pct}%)`;}}}}}});}
Updating Charts on Filter Change
javascript
function updateChart(chart, newLabels, newData) {chart.data.labels = newLabels;if (Array.isArray(newData[0])) {// Multiple datasetsnewData.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>
javascript
function populateFilter(selectId, data, field) {const select = document.getElementById(selectId);const values = [...new Set(data.map(d => d[field]))].sort();// Keep the "All" option, add unique valuesvalues.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>
javascript
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
javascript
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
javascript
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">';// Headerhtml += '<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>';// Bodyhtml += '<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: falsein Chart.js options - Use
Chart.update('none')instead ofChart.update()for filter-triggered updates
DOM Performance
- Limit data tables to 100-200 visible rows. Add pagination for more.
- Use
requestAnimationFramefor 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"}