关于 build-dashboard
build-dashboard 是用于按数据或需求描述生成自包含交互式 HTML 仪表板的技能,适合制作分析和汇报页面。它将图表、筛选器、指标卡与表格整合在一个可离线打开的文件中。
主要功能
- 01生成自包含 HTML 仪表板
- 02创建图表与 KPI 指标卡
- 03添加筛选器和日期范围
- 04支持可排序数据表
- 05响应式布局与打印样式
- 06将数据嵌入页面供离线使用
适用场景
- 01从销售数据制作趋势与区域分析
- 02用工单数据查看优先级和处理时长
- 03创建使用样例数据的 SaaS 指标页面
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ anthropics/ knowledge-work-plugins --skill build-dashboard安装后在 Claude Code 中输入/build-dashboard确认已加载。
详细介绍
/build-dashboard - 构建交互式仪表板
如果您看到不熟悉的占位符,或需要检查已连接的工具,请参阅 CONNECTORS.md。
构建一个自包含的交互式 HTML 仪表板,包含图表、筛选器、表格和专业样式。可直接在浏览器中打开,无需服务器或任何依赖。
用法
/build-dashboard <description of dashboard> [data source]
工作流
1. 明确仪表板需求
确定以下内容:
- 用途:管理层概览、运营监控、深入分析、团队报告
- 受众:谁会使用此仪表板?
- 关键指标:哪些数字最重要?
- 维度:用户应能按哪些维度进行筛选或切片?
- 数据来源:实时查询、粘贴的数据、CSV 文件或示例数据
2. 收集数据
如果已连接数据仓库:
- 查询所需数据
- 将结果以 JSON 形式嵌入 HTML 文件
如果数据是粘贴或上传的:
- 解析并清洗数据
- 以 JSON 形式嵌入仪表板
如果仅有描述而没有数据:
- 创建符合所述结构的真实感示例数据集
- 在仪表板中注明其使用的是示例数据
- 提供替换为真实数据的说明
3. 设计仪表板布局
遵循标准的仪表板布局模式:
┌──────────────────────────────────────────────────┐
│ Dashboard Title [Filters ▼] │
├────────────┬────────────┬────────────┬───────────┤
│ KPI Card │ KPI Card │ KPI Card │ KPI Card │
├────────────┴────────────┼────────────┴───────────┤
│ │ │
│ Primary Chart │ Secondary Chart │
│ (largest area) │ │
│ │ │
├─────────────────────────┴────────────────────────┤
│ │
│ Detail Table (sortable, scrollable) │
│ │
└──────────────────────────────────────────────────┘
根据内容调整布局:
- 顶部放置 2-4 个 KPI 卡片,展示核心数字
- 中间区域放置 1-3 个图表,展示趋势和构成
- 底部可选放置详细表格,用于下钻数据
- 根据复杂程度,将筛选器放在页眉或侧边栏中
4. 构建 HTML 仪表板
使用下方的基础模板生成一个自包含的 HTML 文件。该文件包括:
结构(HTML):
- 语义化的 HTML5 布局
- 使用 CSS Grid 或 Flexbox 的响应式网格
- 筛选控件(下拉菜单、日期选择器、开关)
- 带数值和标签的 KPI 卡片
- 图表容器
- 带可排序表头的数据表格
样式(CSS):
- 专业的配色方案(干净的白色、灰色,并以强调色突出数据)
- 带有轻微阴影的卡片式布局
- 一致的排版(使用系统字体以加快加载)
- 适配不同屏幕尺寸的响应式设计
- 打印友好的样式
交互(JavaScript):
- 通过 CDN 引入 Chart.js 以实现交互式图表
- 筛选下拉菜单可同时更新所有图表和表格
- 可排序的表格列
- 图表上的悬停提示框
- 数字格式化(千位分隔符、货币、百分比)
数据(嵌入式 JSON):
- 所有数据以 JavaScript 变量的形式直接嵌入 HTML
- 无需任何外部数据请求
- 仪表板可完全离线工作
5. 实现图表类型
所有图表均使用 Chart.js。常见的仪表板图表模式:
- 折线图:时间序列趋势
- 柱状图:类别比较
- 环形图:构成占比(类别少于 6 个时使用)
- 堆叠柱状图:随时间变化的构成
- 组合图(柱状 + 折线):数量与比率叠加
请对每种图表类型使用下方的 Chart.js 集成模式。
6. 添加交互功能
使用下方的筛选和交互实现模式,涵盖下拉筛选、日期范围筛选、组合筛选逻辑、可排序表格以及图表更新。
7. 保存并打开
- 将仪表板保存为一个带有描述性名称的 HTML 文件(例如
sales_dashboard.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/[email protected]" integrity="sha384-jb8JQMbMoBUzgWatfe6COACi2ljcDdZQ2OxczGA3bGNeWe+6DChMTBJemed7ZnvJ" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]" 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">
</div>
</header>
<section class="kpi-row">
</section>
<section class="chart-row">
</section>
<section class="table-section">
</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 卡片模式
<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');
// 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 集成
图表容器模式
<div class="chart-container">
<h3 class="chart-title">Monthly Revenue Trend</h3>
<canvas id="revenue-chart"></canvas>
</div>
折线图
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');
}
}
}
}
}
});
}
柱状图
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');
}
} : {}
}
}
}
});
}
环形图
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}%)`;
}
}
}
}
}
});
}
筛选变化时更新图表
function 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
}
筛选和交互实现
下拉筛选
<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>
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 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;
}
日期范围筛选
<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>
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;
});
}
组合筛选逻辑
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();
}
可排序表格
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">';
// 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 样式
Color System
: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
* {
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
.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
.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: var(--bg-header);
color: var(--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);
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
@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; }
}
大数据集的性能注意事项
数据规模指南
| 数据规模 | 处理方式 |
|---|---|
| <1,000 行 | 直接嵌入 HTML。完整交互。 |
| 1,000 - 10,000 行 | 嵌入 HTML。图表可能需要预先聚合。 |
| 10,000 - 100,000 行 | 在服务端预先聚合。仅嵌入聚合后的数据。 |
| >100,000 行 | 不适合客户端仪表板。请使用 BI 工具或分页。 |
预聚合模式
与其嵌入原始数据并在浏览器中聚合:
// DON'T: embed 50,000 raw rows
const RAW_DATA = [/* 50,000 rows */];
// DO: pre-aggregate before embedding
const 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,
}
};
图表性能
- 每个系列的折线图数据点限制在 <500 个(如有需要请进行降采样)
- 柱状图分类限制在 <50 个
- 散点图上限为 1,000 个点(数据量更大时使用采样)
- 对于包含大量图表的仪表板,禁用动画:在 Chart.js 配置中设置
animation: false - 对于由筛选器触发的更新,使用
Chart.update('none')代替Chart.update()
DOM 性能
- 数据表格可见行数限制在 100-200 行,更多数据请添加分页。
- 使用
requestAnimationFrame协调图表更新 - 筛选条件变化时避免重建整个 DOM,只更新发生变化的元素
// Efficient table pagination
function 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"
}
示例
/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.
小技巧
- 仪表板是完全自包含的 HTML 文件,只需发送文件即可与任何人共享
- 对于实时仪表板,可以考虑改为连接 BI 工具。这些仪表板是某个时间点的快照
- 可以请求“深色模式”或“演示模式”以获得不同的样式
- 可以指定配色方案,使其与您的品牌保持一致
评论
支持 Markdown