Skill Tracer
Skills › Knowledge Work Plugins › build-dashboard

build-dashboard

skill · 924 lines · Apache-2.0 · data/skills/build-dashboard/SKILL.md

The facts

Its 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.

The description the agent reads to decide when to use it.

From its settingsline 3

When it starts

The agent picks this skill when someone asks for a browser-openable dashboard — an executive overview with KPI cards, a shareable report built from query results or pasted data, or a monitoring snapshot combining several charts, filters, and a table in one self-contained HTML file.

Things a person might say (the model's guesses)

  • “Build me a monthly sales dashboard with a revenue trend, top products, and a regional breakdown — the data is in the orders table.”
  • “Here's our support ticket CSV. Make a dashboard showing volume by priority, response time trends, and resolution rates.”
  • “Create a template executive dashboard for a SaaS company with MRR, churn, new customers, and NPS, using sample data.”

A model's summary of when it is used.

A model's reading

What it takes when started

<description> [data source]

What to type after its name when you start it.

From its settingsline 4

Licence

Apache-2.0, from data/LICENSE

The terms its author shares it under, which let it be shown here.

From the repositorylicence text

The text

Open on GitHub
1---
2name: build-dashboard
3description: 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.
4argument-hint: "<description> [data source]"
5---
6
7# /build-dashboard - Build Interactive Dashboards
8
9> If you see unfamiliar placeholders or need to check which tools are connected, see [CONNECTORS.md](../../CONNECTORS.md).
11Build a self-contained interactive HTML dashboard with charts, filters, tables, and professional styling. Opens directly in a browser -- no server or dependencies required.

/build-dashboard - Build Interactive Dashboards

If you see unfamiliar placeholders or need to check which tools are connected, see 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.

Raw lines 1 to 12
13## Usage
15```
16/build-dashboard <description of dashboard> [data source]
17```

Usage

/build-dashboard <description of dashboard> [data source]
Raw lines 13 to 18
19## Workflow
21### 1. Understand the Dashboard Requirements
23Determine:
25- **Purpose**: Executive overview, operational monitoring, deep-dive analysis, team reporting
26- **Audience**: Who will use this dashboard?
27- **Key metrics**: What numbers matter most?
28- **Dimensions**: What should users be able to filter or slice by?
29- **Data source**: Live query, pasted data, CSV file, or sample data

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
Raw lines 21 to 30
31### 2. Gather the Data
33**If data warehouse is connected:**
341. Query the necessary data
352. Embed the results as JSON within the HTML file
37**If data is pasted or uploaded:**
381. Parse and clean the data
392. Embed as JSON in the dashboard
41**If working from a description without data:**
421. Create a realistic sample dataset matching the described schema
432. Note in the dashboard that it uses sample data
443. Provide instructions for swapping in real data

2. Gather the Data

If data warehouse is connected:

  1. Query the necessary data
  2. Embed the results as JSON within the HTML file

If data is pasted or uploaded:

  1. Parse and clean the data
  2. Embed as JSON in the dashboard

If working from a description without data:

  1. Create a realistic sample dataset matching the described schema
  2. Note in the dashboard that it uses sample data
  3. Provide instructions for swapping in real data
Raw lines 31 to 45
46### 3. Design the Dashboard Layout
48Follow a standard dashboard layout pattern:
50```
51┌──────────────────────────────────────────────────┐
52│ Dashboard Title [Filters ▼] │
53├────────────┬────────────┬────────────┬───────────┤
54│ KPI Card │ KPI Card │ KPI Card │ KPI Card │
55├────────────┴────────────┼────────────┴───────────┤
56│ │ │
57│ Primary Chart │ Secondary Chart │
58│ (largest area) │ │
59│ │ │
60├─────────────────────────┴────────────────────────┤
61│ │
62│ Detail Table (sortable, scrollable) │
63│ │
64└──────────────────────────────────────────────────┘
65```
67**Adapt the layout to the content:**
68- 2-4 KPI cards at the top for headline numbers
69- 1-3 charts in the middle section for trends and breakdowns
70- Optional detail table at the bottom for drill-down data
71- Filters in the header or sidebar depending on complexity

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
Raw lines 46 to 72
73### 4. Build the HTML Dashboard
75Generate a single self-contained HTML file using the base template below. The file includes:
77**Structure (HTML):**
78- Semantic HTML5 layout
79- Responsive grid using CSS Grid or Flexbox
80- Filter controls (dropdowns, date pickers, toggles)
81- KPI cards with values and labels
82- Chart containers
83- Data table with sortable headers
85**Styling (CSS):**
86- Professional color scheme (clean whites, grays, with accent colors for data)
87- Card-based layout with subtle shadows
88- Consistent typography (system fonts for fast loading)
89- Responsive design that works on different screen sizes
90- Print-friendly styles
92**Interactivity (JavaScript):**
93- Chart.js for interactive charts (included via CDN)
94- Filter dropdowns that update all charts and tables simultaneously
95- Sortable table columns
96- Hover tooltips on charts
97- Number formatting (commas, currency, percentages)
99**Data (embedded JSON):**
100- All data embedded directly in the HTML as JavaScript variables
101- No external data fetches required
102- Dashboard works completely offline

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
Raw lines 73 to 103
104### 5. Implement Chart Types
106Use Chart.js for all charts. Common dashboard chart patterns:
108- **Line chart**: Time series trends
109- **Bar chart**: Category comparisons
110- **Doughnut chart**: Composition (when <6 categories)
111- **Stacked bar**: Composition over time
112- **Mixed (bar + line)**: Volume with rate overlay
114Use the Chart.js integration patterns below for each chart type.

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.

Raw lines 104 to 115
116### 6. Add Interactivity
118Use the filter and interactivity implementation patterns below for dropdown filters, date range filters, combined filter logic, sortable tables, and chart updates.

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.

Raw lines 116 to 119
120### 7. Save and Open
1221. Save the dashboard as an HTML file with a descriptive name (e.g., `sales_dashboard.html`)
1232. Open it in the user's default browser
1243. Confirm it renders correctly
1254. Provide instructions for updating data or customizing
127---

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 browser
  3. Confirm it renders correctly
  4. Provide instructions for updating data or customizing

Raw lines 120 to 128
129## Base Template
131Every dashboard follows this structure:
133```html
134<!DOCTYPE html>
135<html lang="en">
136<head>
137 <meta charset="UTF-8">
138 <meta name="viewport" content="width=device-width, initial-scale=1.0">
139 <title>Dashboard Title</title>
140 <script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1" integrity="sha384-jb8JQMbMoBUzgWatfe6COACi2ljcDdZQ2OxczGA3bGNeWe+6DChMTBJemed7ZnvJ" crossorigin="anonymous"></script>
141 <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0" integrity="sha384-cVMg8E3QFwTvGCDuK+ET4PD341jF3W8nO1auiXfuZNQkzbUUiBGLsIQUE+b1mxws" crossorigin="anonymous"></script>
142 <style>
143 /* Dashboard styles go here */
144 </style>
145</head>
146<body>
147 <div class="dashboard-container">
148 <header class="dashboard-header">
149 <h1>Dashboard Title</h1>
150 <div class="filters">
151 <!-- Filter controls -->
152 </div>
153 </header>
155 <section class="kpi-row">
156 <!-- KPI cards -->
157 </section>
159 <section class="chart-row">
160 <!-- Chart containers -->
161 </section>
163 <section class="table-section">
164 <!-- Data table -->
165 </section>
167 <footer class="dashboard-footer">
168 <span>Data as of: <span id="data-date"></span></span>
169 </footer>
170 </div>
172 <script>
173 // Embedded data
174 const DATA = [];
176 // Dashboard logic
177 class Dashboard {
178 constructor(data) {
179 this.rawData = data;
180 this.filteredData = data;
181 this.charts = {};
182 this.init();
183 }
185 init() {
186 this.setupFilters();
187 this.renderKPIs();
188 this.renderCharts();
189 this.renderTable();
190 }
192 applyFilters() {
193 // Filter logic
194 this.filteredData = this.rawData.filter(row => {
195 // Apply each active filter
196 return true; // placeholder
197 });
198 this.renderKPIs();
199 this.updateCharts();
200 this.renderTable();
201 }
203 // ... methods for each section
204 }
206 const dashboard = new Dashboard(DATA);
207 </script>
208</body>
209</html>
210```

Base Template

Every dashboard follows this structure:

<!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>
Raw lines 129 to 211
212## KPI Card Pattern
214```html
215<div class="kpi-card">
216 <div class="kpi-label">Total Revenue</div>
217 <div class="kpi-value" id="kpi-revenue">$0</div>
218 <div class="kpi-change positive" id="kpi-revenue-change">+0%</div>
219</div>
220```
222```javascript
223function renderKPI(elementId, value, previousValue, format = 'number') {
224 const el = document.getElementById(elementId);
225 const changeEl = document.getElementById(elementId + '-change');
227 // Format the value
228 el.textContent = formatValue(value, format);
230 // Calculate and display change
231 if (previousValue && previousValue !== 0) {
232 const pctChange = ((value - previousValue) / previousValue) * 100;
233 const sign = pctChange >= 0 ? '+' : '';
234 changeEl.textContent = `${sign}${pctChange.toFixed(1)}% vs prior period`;
235 changeEl.className = `kpi-change ${pctChange >= 0 ? 'positive' : 'negative'}`;
236 }
239function formatValue(value, format) {
240 switch (format) {
241 case 'currency':
242 if (value >= 1e6) return `$${(value / 1e6).toFixed(1)}M`;
243 if (value >= 1e3) return `$${(value / 1e3).toFixed(1)}K`;
244 return `$${value.toFixed(0)}`;
245 case 'percent':
246 return `${value.toFixed(1)}%`;
247 case 'number':
248 if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
249 if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
250 return value.toLocaleString();
251 default:
252 return value.toString();
253 }
255```

KPI Card Pattern

<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();
    }
}
Raw lines 212 to 256
257## Chart.js Integration

Chart.js Integration

Raw lines 257 to 258
259### Chart Container Pattern
261```html
262<div class="chart-container">
263 <h3 class="chart-title">Monthly Revenue Trend</h3>
264 <canvas id="revenue-chart"></canvas>
265</div>
266```

Chart Container Pattern

<div class="chart-container">
    <h3 class="chart-title">Monthly Revenue Trend</h3>
    <canvas id="revenue-chart"></canvas>
</div>
Raw lines 259 to 267
268### Line Chart
270```javascript
271function createLineChart(canvasId, labels, datasets) {
272 const ctx = document.getElementById(canvasId).getContext('2d');
273 return new Chart(ctx, {
274 type: 'line',
275 data: {
276 labels: labels,
277 datasets: datasets.map((ds, i) => ({
278 label: ds.label,
279 data: ds.data,
280 borderColor: COLORS[i % COLORS.length],
281 backgroundColor: COLORS[i % COLORS.length] + '20',
282 borderWidth: 2,
283 fill: ds.fill || false,
284 tension: 0.3,
285 pointRadius: 3,
286 pointHoverRadius: 6,
287 }))
288 },
289 options: {
290 responsive: true,
291 maintainAspectRatio: false,
292 interaction: {
293 mode: 'index',
294 intersect: false,
295 },
296 plugins: {
297 legend: {
298 position: 'top',
299 labels: { usePointStyle: true, padding: 20 }
300 },
301 tooltip: {
302 callbacks: {
303 label: function(context) {
304 return `${context.dataset.label}: ${formatValue(context.parsed.y, 'currency')}`;
305 }
306 }
307 }
308 },
309 scales: {
310 x: {
311 grid: { display: false }
312 },
313 y: {
314 beginAtZero: true,
315 ticks: {
316 callback: function(value) {
317 return formatValue(value, 'currency');
318 }
319 }
320 }
321 }
322 }
323 });
325```

Line Chart

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');
                        }
                    }
                }
            }
        }
    });
}
Raw lines 268 to 326
327### Bar Chart
329```javascript
330function createBarChart(canvasId, labels, data, options = {}) {
331 const ctx = document.getElementById(canvasId).getContext('2d');
332 const isHorizontal = options.horizontal || labels.length > 8;
334 return new Chart(ctx, {
335 type: 'bar',
336 data: {
337 labels: labels,
338 datasets: [{
339 label: options.label || 'Value',
340 data: data,
341 backgroundColor: options.colors || COLORS.map(c => c + 'CC'),
342 borderColor: options.colors || COLORS,
343 borderWidth: 1,
344 borderRadius: 4,
345 }]
346 },
347 options: {
348 responsive: true,
349 maintainAspectRatio: false,
350 indexAxis: isHorizontal ? 'y' : 'x',
351 plugins: {
352 legend: { display: false },
353 tooltip: {
354 callbacks: {
355 label: function(context) {
356 return formatValue(context.parsed[isHorizontal ? 'x' : 'y'], options.format || 'number');
357 }
358 }
359 }
360 },
361 scales: {
362 x: {
363 beginAtZero: true,
364 grid: { display: isHorizontal },
365 ticks: isHorizontal ? {
366 callback: function(value) {
367 return formatValue(value, options.format || 'number');
368 }
369 } : {}
370 },
371 y: {
372 beginAtZero: !isHorizontal,
373 grid: { display: !isHorizontal },
374 ticks: !isHorizontal ? {
375 callback: function(value) {
376 return formatValue(value, options.format || 'number');
377 }
378 } : {}
379 }
380 }
381 }
382 });
384```

Bar Chart

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');
                        }
                    } : {}
                }
            }
        }
    });
}
Raw lines 327 to 385
386### Doughnut Chart
388```javascript
389function createDoughnutChart(canvasId, labels, data) {
390 const ctx = document.getElementById(canvasId).getContext('2d');
391 return new Chart(ctx, {
392 type: 'doughnut',
393 data: {
394 labels: labels,
395 datasets: [{
396 data: data,
397 backgroundColor: COLORS.map(c => c + 'CC'),
398 borderColor: '#ffffff',
399 borderWidth: 2,
400 }]
401 },
402 options: {
403 responsive: true,
404 maintainAspectRatio: false,
405 cutout: '60%',
406 plugins: {
407 legend: {
408 position: 'right',
409 labels: { usePointStyle: true, padding: 15 }
410 },
411 tooltip: {
412 callbacks: {
413 label: function(context) {
414 const total = context.dataset.data.reduce((a, b) => a + b, 0);
415 const pct = ((context.parsed / total) * 100).toFixed(1);
416 return `${context.label}: ${formatValue(context.parsed, 'number')} (${pct}%)`;
417 }
418 }
419 }
420 }
421 }
422 });
424```

Doughnut Chart

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}%)`;
                        }
                    }
                }
            }
        }
    });
}
Raw lines 386 to 425
426### Updating Charts on Filter Change
428```javascript
429function updateChart(chart, newLabels, newData) {
430 chart.data.labels = newLabels;
432 if (Array.isArray(newData[0])) {
433 // Multiple datasets
434 newData.forEach((data, i) => {
435 chart.data.datasets[i].data = data;
436 });
437 } else {
438 chart.data.datasets[0].data = newData;
439 }
441 chart.update('none'); // 'none' disables animation for instant update
443```

Updating Charts on Filter Change

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
}
Raw lines 426 to 444
445## Filter and Interactivity Implementation

Filter and Interactivity Implementation

Raw lines 445 to 446
447### Dropdown Filter
449```html
450<div class="filter-group">
451 <label for="filter-region">Region</label>
452 <select id="filter-region" onchange="dashboard.applyFilters()">
453 <option value="all">All Regions</option>
454 </select>
455</div>
456```
458```javascript
459function populateFilter(selectId, data, field) {
460 const select = document.getElementById(selectId);
461 const values = [...new Set(data.map(d => d[field]))].sort();
463 // Keep the "All" option, add unique values
464 values.forEach(val => {
465 const option = document.createElement('option');
466 option.value = val;
467 option.textContent = val;
468 select.appendChild(option);
469 });
472function getFilterValue(selectId) {
473 const val = document.getElementById(selectId).value;
474 return val === 'all' ? null : val;
476```

Dropdown Filter

<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;
}
Raw lines 447 to 477
478### Date Range Filter
480```html
481<div class="filter-group">
482 <label>Date Range</label>
483 <input type="date" id="filter-date-start" onchange="dashboard.applyFilters()">
484 <span>to</span>
485 <input type="date" id="filter-date-end" onchange="dashboard.applyFilters()">
486</div>
487```
489```javascript
490function filterByDateRange(data, dateField, startDate, endDate) {
491 return data.filter(row => {
492 const rowDate = new Date(row[dateField]);
493 if (startDate && rowDate < new Date(startDate)) return false;
494 if (endDate && rowDate > new Date(endDate)) return false;
495 return true;
496 });
498```

Date Range Filter

<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;
    });
}
Raw lines 478 to 499
500### Combined Filter Logic
502```javascript
503applyFilters() {
504 const region = getFilterValue('filter-region');
505 const category = getFilterValue('filter-category');
506 const startDate = document.getElementById('filter-date-start').value;
507 const endDate = document.getElementById('filter-date-end').value;
509 this.filteredData = this.rawData.filter(row => {
510 if (region && row.region !== region) return false;
511 if (category && row.category !== category) return false;
512 if (startDate && row.date < startDate) return false;
513 if (endDate && row.date > endDate) return false;
514 return true;
515 });
517 this.renderKPIs();
518 this.updateCharts();
519 this.renderTable();
521```

Combined Filter Logic

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();
}
Raw lines 500 to 522
523### Sortable Table
525```javascript
526function renderTable(containerId, data, columns) {
527 const container = document.getElementById(containerId);
528 let sortCol = null;
529 let sortDir = 'desc';
531 function render(sortedData) {
532 let html = '<table class="data-table">';
534 // Header
535 html += '<thead><tr>';
536 columns.forEach(col => {
537 const arrow = sortCol === col.field
538 ? (sortDir === 'asc' ? ' ▲' : ' ▼')
539 : '';
540 html += `<th onclick="sortTable('${col.field}')" style="cursor:pointer">${col.label}${arrow}</th>`;
541 });
542 html += '</tr></thead>';
544 // Body
545 html += '<tbody>';
546 sortedData.forEach(row => {
547 html += '<tr>';
548 columns.forEach(col => {
549 const value = col.format ? formatValue(row[col.field], col.format) : row[col.field];
550 html += `<td>${value}</td>`;
551 });
552 html += '</tr>';
553 });
554 html += '</tbody></table>';
556 container.innerHTML = html;
557 }
559 window.sortTable = function(field) {
560 if (sortCol === field) {
561 sortDir = sortDir === 'asc' ? 'desc' : 'asc';
562 } else {
563 sortCol = field;
564 sortDir = 'desc';
565 }
566 const sorted = [...data].sort((a, b) => {
567 const aVal = a[field], bVal = b[field];
568 const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
569 return sortDir === 'asc' ? cmp : -cmp;
570 });
571 render(sorted);
572 };
574 render(data);
576```

Sortable Table

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);
}
Raw lines 523 to 577
578## CSS Styling for Dashboards

CSS Styling for Dashboards

Raw lines 578 to 579
580### Color System
582```css
583:root {
584 /* Background layers */
585 --bg-primary: #f8f9fa;
586 --bg-card: #ffffff;
587 --bg-header: #1a1a2e;
589 /* Text */
590 --text-primary: #212529;
591 --text-secondary: #6c757d;
592 --text-on-dark: #ffffff;
594 /* Accent colors for data */
595 --color-1: #4C72B0;
596 --color-2: #DD8452;
597 --color-3: #55A868;
598 --color-4: #C44E52;
599 --color-5: #8172B3;
600 --color-6: #937860;
602 /* Status colors */
603 --positive: #28a745;
604 --negative: #dc3545;
605 --neutral: #6c757d;
607 /* Spacing */
608 --gap: 16px;
609 --radius: 8px;
611```

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;
}
Raw lines 580 to 612
613### Layout
615```css
616* {
617 margin: 0;
618 padding: 0;
619 box-sizing: border-box;
622body {
623 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
624 background: var(--bg-primary);
625 color: var(--text-primary);
626 line-height: 1.5;
629.dashboard-container {
630 max-width: 1400px;
631 margin: 0 auto;
632 padding: var(--gap);
635.dashboard-header {
636 background: var(--bg-header);
637 color: var(--text-on-dark);
638 padding: 20px 24px;
639 border-radius: var(--radius);
640 margin-bottom: var(--gap);
641 display: flex;
642 justify-content: space-between;
643 align-items: center;
644 flex-wrap: wrap;
645 gap: 12px;
648.dashboard-header h1 {
649 font-size: 20px;
650 font-weight: 600;
652```

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;
}
Raw lines 613 to 653
654### KPI Cards
656```css
657.kpi-row {
658 display: grid;
659 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
660 gap: var(--gap);
661 margin-bottom: var(--gap);
664.kpi-card {
665 background: var(--bg-card);
666 border-radius: var(--radius);
667 padding: 20px 24px;
668 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
671.kpi-label {
672 font-size: 13px;
673 color: var(--text-secondary);
674 text-transform: uppercase;
675 letter-spacing: 0.5px;
676 margin-bottom: 4px;
679.kpi-value {
680 font-size: 28px;
681 font-weight: 700;
682 color: var(--text-primary);
683 margin-bottom: 4px;
686.kpi-change {
687 font-size: 13px;
688 font-weight: 500;
691.kpi-change.positive { color: var(--positive); }
692.kpi-change.negative { color: var(--negative); }
693```

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); }
Raw lines 654 to 694
695### Chart Containers
697```css
698.chart-row {
699 display: grid;
700 grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
701 gap: var(--gap);
702 margin-bottom: var(--gap);
705.chart-container {
706 background: var(--bg-card);
707 border-radius: var(--radius);
708 padding: 20px 24px;
709 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
712.chart-container h3 {
713 font-size: 14px;
714 font-weight: 600;
715 color: var(--text-primary);
716 margin-bottom: 16px;
719.chart-container canvas {
720 max-height: 300px;
722```

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;
}
Raw lines 695 to 723
724### Filters
726```css
727.filters {
728 display: flex;
729 gap: 12px;
730 align-items: center;
731 flex-wrap: wrap;
734.filter-group {
735 display: flex;
736 align-items: center;
737 gap: 6px;
740.filter-group label {
741 font-size: 12px;
742 color: rgba(255, 255, 255, 0.7);
745.filter-group select,
746.filter-group input[type="date"] {
747 padding: 6px 10px;
748 border: 1px solid rgba(255, 255, 255, 0.2);
749 border-radius: 4px;
750 background: rgba(255, 255, 255, 0.1);
751 color: var(--text-on-dark);
752 font-size: 13px;
755.filter-group select option {
756 background: var(--bg-header);
757 color: var(--text-on-dark);
759```

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);
}
Raw lines 724 to 760
761### Data Table
763```css
764.table-section {
765 background: var(--bg-card);
766 border-radius: var(--radius);
767 padding: 20px 24px;
768 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
769 overflow-x: auto;
772.data-table {
773 width: 100%;
774 border-collapse: collapse;
775 font-size: 13px;
778.data-table thead th {
779 text-align: left;
780 padding: 10px 12px;
781 border-bottom: 2px solid #dee2e6;
782 color: var(--text-secondary);
783 font-weight: 600;
784 font-size: 12px;
785 text-transform: uppercase;
786 letter-spacing: 0.5px;
787 white-space: nowrap;
788 user-select: none;
791.data-table thead th:hover {
792 color: var(--text-primary);
793 background: #f8f9fa;
796.data-table tbody td {
797 padding: 10px 12px;
798 border-bottom: 1px solid #f0f0f0;
801.data-table tbody tr:hover {
802 background: #f8f9fa;
805.data-table tbody tr:last-child td {
806 border-bottom: none;
808```

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;
}
Raw lines 761 to 809
810### Responsive Design
812```css
813@media (max-width: 768px) {
814 .dashboard-header {
815 flex-direction: column;
816 align-items: flex-start;
817 }
819 .kpi-row {
820 grid-template-columns: repeat(2, 1fr);
821 }
823 .chart-row {
824 grid-template-columns: 1fr;
825 }
827 .filters {
828 flex-direction: column;
829 align-items: flex-start;
830 }
833@media print {
834 body { background: white; }
835 .dashboard-container { max-width: none; }
836 .filters { display: none; }
837 .chart-container { break-inside: avoid; }
838 .kpi-card { border: 1px solid #dee2e6; box-shadow: none; }
840```

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; }
}
Raw lines 810 to 841
842## Performance Considerations for Large Datasets

Performance Considerations for Large Datasets

Raw lines 842 to 843
844### Data Size Guidelines
846| Data Size | Approach |
847|---|---|
848| <1,000 rows | Embed directly in HTML. Full interactivity. |
849| 1,000 - 10,000 rows | Embed in HTML. May need to pre-aggregate for charts. |
850| 10,000 - 100,000 rows | Pre-aggregate server-side. Embed only aggregated data. |
851| >100,000 rows | Not suitable for client-side dashboard. Use a BI tool or paginate. |

Data Size Guidelines

Data SizeApproach
<1,000 rowsEmbed directly in HTML. Full interactivity.
1,000 - 10,000 rowsEmbed in HTML. May need to pre-aggregate for charts.
10,000 - 100,000 rowsPre-aggregate server-side. Embed only aggregated data.
>100,000 rowsNot suitable for client-side dashboard. Use a BI tool or paginate.
Raw lines 844 to 852
853### Pre-Aggregation Pattern
855Instead of embedding raw data and aggregating in the browser:
857```javascript
858// DON'T: embed 50,000 raw rows
859const RAW_DATA = [/* 50,000 rows */];
861// DO: pre-aggregate before embedding
862const CHART_DATA = {
863 monthly_revenue: [
864 { month: '2024-01', revenue: 150000, orders: 1200 },
865 { month: '2024-02', revenue: 165000, orders: 1350 },
866 // ... 12 rows instead of 50,000
867 ],
868 top_products: [
869 { product: 'Widget A', revenue: 45000 },
870 // ... 10 rows
871 ],
872 kpis: {
873 total_revenue: 1980000,
874 total_orders: 15600,
875 avg_order_value: 127,
876 }
877};
878```

Pre-Aggregation Pattern

Instead of embedding raw data and aggregating in the browser:

// 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,
    }
};
Raw lines 853 to 879
880### Chart Performance
882- Limit line charts to <500 data points per series (downsample if needed)
883- Limit bar charts to <50 categories
884- For scatter plots, cap at 1,000 points (use sampling for larger datasets)
885- Disable animations for dashboards with many charts: `animation: false` in Chart.js options
886- Use `Chart.update('none')` instead of `Chart.update()` for filter-triggered updates

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
Raw lines 880 to 887
888### DOM Performance
890- Limit data tables to 100-200 visible rows. Add pagination for more.
891- Use `requestAnimationFrame` for coordinated chart updates
892- Avoid rebuilding the entire DOM on filter change -- update only changed elements
894```javascript
895// Efficient table pagination
896function renderTablePage(data, page, pageSize = 50) {
897 const start = page * pageSize;
898 const end = Math.min(start + pageSize, data.length);
899 const pageData = data.slice(start, end);
900 // Render only pageData
901 // Show pagination controls: "Showing 1-50 of 2,340"
903```

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
// 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"
}
Raw lines 888 to 904
905## Examples
907```
908/build-dashboard Monthly sales dashboard with revenue trend, top products, and regional breakdown. Data is in the orders table.
909```
911```
912/build-dashboard Here's our support ticket data [pastes CSV]. Build a dashboard showing volume by priority, response time trends, and resolution rates.
913```
915```
916/build-dashboard Create a template executive dashboard for a SaaS company showing MRR, churn, new customers, and NPS. Use sample data.
917```

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.
Raw lines 905 to 918
919## Tips
921- Dashboards are fully self-contained HTML files -- share them with anyone by sending the file
922- For real-time dashboards, consider connecting to a BI tool instead. These dashboards are point-in-time snapshots
923- Request "dark mode" or "presentation mode" for different styling
924- You can request a specific color scheme to match your brand

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
Raw lines 919 to 924

From anthropics/knowledge-work-plugins at 2574e29, 2026-10-27. Skills read by DeepSeek V4.1 Flash on 2026-11-04. Summary and techniques by Claude Opus 5.5 on 2026-11-06.