interactive-dashboard-builder

安装量: 588
排名: #1903

安装

npx skills add https://github.com/anthropics/knowledge-work-plugins --skill interactive-dashboard-builder

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: <! 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 "

</ 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 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 ( ) ; } } Chart Container Pattern < div class = " chart-container "

< h3 class = " chart-title "

Monthly Revenue Trend </ h3

< canvas id = " revenue-chart "

</ canvas

</ div

Chart.js Integration 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' ) ; } } } } } } ) ; } 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' ) ; } } : { } } } } } ) ; } 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 } %) ; } } } } } } ) ; } 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 } Filter and Interactivity Implementation 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 ; } 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 ; } ) ; } 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 ( ) ; } Sortable Table function renderTable ( containerId , data , columns ) { const container = document . getElementById ( containerId ) ; let sortCol = null ; let sortDir = 'desc' ; function render ( sortedData ) { let html = '

' ; // Header html += '' ; columns . forEach ( col => { const arrow = sortCol === col . field ? ( sortDir === 'asc' ? ' ▲' : ' ▼' ) : '' ; html += `

; } ) ; 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 +=

` ; } ) ; html += '

' ; } ) ; html += '
${ col . label } ${ arrow } ${ value }
' ; 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 :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 : 16 px ; --radius : 8 px ; } 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 : 1400 px ; margin : 0 auto ; padding : var ( --gap ) ; } .dashboard-header { background : var ( --bg-header ) ; color : var ( --text-on-dark ) ; padding : 20 px 24 px ; border-radius : var ( --radius ) ; margin-bottom : var ( --gap ) ; display : flex ; justify-content : space-between ; align-items : center ; flex-wrap : wrap ; gap : 12 px ; } .dashboard-header h1 { font-size : 20 px ; font-weight : 600 ; } KPI Cards .kpi-row { display : grid ; grid-template-columns : repeat ( auto-fit , minmax ( 200 px , 1 fr ) ) ; gap : var ( --gap ) ; margin-bottom : var ( --gap ) ; } .kpi-card { background : var ( --bg-card ) ; border-radius : var ( --radius ) ; padding : 20 px 24 px ; box-shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , 0.08 ) ; } .kpi-label { font-size : 13 px ; color : var ( --text-secondary ) ; text-transform : uppercase ; letter-spacing : 0.5 px ; margin-bottom : 4 px ; } .kpi-value { font-size : 28 px ; font-weight : 700 ; color : var ( --text-primary ) ; margin-bottom : 4 px ; } .kpi-change { font-size : 13 px ; 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 ( 400 px , 1 fr ) ) ; gap : var ( --gap ) ; margin-bottom : var ( --gap ) ; } .chart-container { background : var ( --bg-card ) ; border-radius : var ( --radius ) ; padding : 20 px 24 px ; box-shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , 0.08 ) ; } .chart-container h3 { font-size : 14 px ; font-weight : 600 ; color : var ( --text-primary ) ; margin-bottom : 16 px ; } .chart-container canvas { max-height : 300 px ; } Filters .filters { display : flex ; gap : 12 px ; align-items : center ; flex-wrap : wrap ; } .filter-group { display : flex ; align-items : center ; gap : 6 px ; } .filter-group label { font-size : 12 px ; color : rgba ( 255 , 255 , 255 , 0.7 ) ; } .filter-group select , .filter-group input [ type = "date" ] { padding : 6 px 10 px ; border : 1 px solid rgba ( 255 , 255 , 255 , 0.2 ) ; border-radius : 4 px ; background : rgba ( 255 , 255 , 255 , 0.1 ) ; color : var ( --text-on-dark ) ; font-size : 13 px ; } .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 : 20 px 24 px ; box-shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , 0.08 ) ; overflow-x : auto ; } .data-table { width : 100 % ; border-collapse : collapse ; font-size : 13 px ; } .data-table thead th { text-align : left ; padding : 10 px 12 px ; border-bottom : 2 px solid

dee2e6

; color : var ( --text-secondary ) ; font-weight : 600 ; font-size : 12 px ; text-transform : uppercase ; letter-spacing : 0.5 px ; white-space : nowrap ; user-select : none ; } .data-table thead th :hover { color : var ( --text-primary ) ; background :

f8f9fa

; } .data-table tbody td { padding : 10 px 12 px ; border-bottom : 1 px solid

f0f0f0

; } .data-table tbody tr :hover { background :

f8f9fa

; } .data-table tbody tr :last-child td { border-bottom : none ; } Responsive Design @media ( max-width : 768 px ) { .dashboard-header { flex-direction : column ; align-items : flex-start ; } .kpi-row { grid-template-columns : repeat ( 2 , 1 fr ) ; } .chart-row { grid-template-columns : 1 fr ; } .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 : 1 px 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: // 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 , } } ; 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 // 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" }

返回排行榜