Driver Performance Dashboard
Driver Performance Overview
Monitor key performance indicators for your drivers.
Total Drivers
0
Total Miles Driven
0
Average Safety Score
N/A
Average Fuel Efficiency (MPG)
N/A
Individual Driver Performance
Safety Score Distribution
Miles Driven Per Driver
Configure Driver Data
Add, edit, or remove driver performance entries.
Add/Edit Driver Entry
| ID | Driver Name | Trips | Miles | Safety Score | Fuel Eff. | Actions |
|---|
Notes: ${d.notes || 'N/A'}
`; driverCardsContainer.appendChild(card); }); } /** * Renders a bar chart of safety scores per driver using D3.js. */ function renderSafetyScoreChart() { if (!safetyScoreChartContainer) { console.error('Error: safetyScoreChartContainer not found.'); return; } // Clear previous chart, but keep the title div const chartSvg = safetyScoreChartContainer.querySelector('svg'); if (chartSvg) { chartSvg.remove(); } if (drivers.length === 0) { safetyScoreChartContainer.innerHTML = 'Safety Score Distribution
No driver data available.
'; return; } // Sort data by safety score for the chart const chartData = [...drivers].sort((a, b) => a.safetyScore - b.safetyScore); const margin = { top: 20, right: 20, bottom: 40, left: 40 }; const width = safetyScoreChartContainer.clientWidth - margin.left - margin.right; const height = safetyScoreChartContainer.clientHeight - margin.top - margin.bottom - 30; // Account for title height const svg = d3.select(safetyScoreChartContainer) .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", `translate(${margin.left},${margin.top})`); // X scale const x = d3.scaleBand() .domain(chartData.map(d => d.driverName)) .range([0, width]) .padding(0.1); // Y scale const y = d3.scaleLinear() .domain([0, 100]) // Safety score is 0-100 .range([height, 0]); // Add X axis svg.append("g") .attr("class", "axis x-axis") .attr("transform", `translate(0,${height})`) .call(d3.axisBottom(x)) .selectAll("text") .style("text-anchor", "end") .attr("dx", "-.8em") .attr("dy", ".15em") .attr("transform", "rotate(-45)"); // Add Y axis svg.append("g") .attr("class", "axis y-axis") .call(d3.axisLeft(y).tickFormat(d3.format("d"))); // Integer ticks // Tooltip const tooltip = d3.select("body").append("div") .attr("class", "chart-tooltip") .style("opacity", 0); // Add bars svg.selectAll(".bar") .data(chartData) .enter().append("rect") .attr("class", "bar") .attr("x", d => x(d.driverName)) .attr("y", d => y(d.safetyScore)) .attr("width", x.bandwidth()) .attr("height", d => height - y(d.safetyScore)) .attr("fill", d => { if (d.safetyScore >= 90) return '#28a745'; // Green if (d.safetyScore >= 80) return '#ffc107'; // Amber return '#dc3545'; // Red }) .on("mouseover", function(event, d) { d3.select(this).attr("fill", d3.rgb(this.getAttribute('fill')).darker(0.5)); tooltip.transition() .duration(200) .style("opacity", .9); tooltip.html(`Driver: ${d.driverName}Safety Score: ${d.safetyScore}`) .style("left", (event.pageX + 10) + "px") .style("top", (event.pageY - 28) + "px"); }) .on("mouseout", function(d) { d3.select(this).attr("fill", d => { if (d.safetyScore >= 90) return '#28a745'; if (d.safetyScore >= 80) return '#ffc107'; return '#dc3545'; }); // Restore original color tooltip.transition() .duration(500) .style("opacity", 0); }); } /** * Renders a line chart of miles driven per driver using D3.js. */ function renderMilesPerDriverChart() { if (!milesPerDriverChartContainer) { console.error('Error: milesPerDriverChartContainer not found.'); return; } // Clear previous chart, but keep the title div const chartSvg = milesPerDriverChartContainer.querySelector('svg'); if (chartSvg) { chartSvg.remove(); } if (drivers.length === 0) { milesPerDriverChartContainer.innerHTML = '
Miles Driven Per Driver
No driver data available.
'; return; } // Sort data by miles driven for the chart const chartData = [...drivers].sort((a, b) => a.milesDriven - b.milesDriven); const margin = { top: 20, right: 30, bottom: 40, left: 60 }; const width = milesPerDriverChartContainer.clientWidth - margin.left - margin.right; const height = milesPerDriverChartContainer.clientHeight - margin.top - margin.bottom - 30; // Account for title height const svg = d3.select(milesPerDriverChartContainer) .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", `translate(${margin.left},${margin.top})`); // X scale (Band scale for driver names) const x = d3.scaleBand() .domain(chartData.map(d => d.driverName)) .range([0, width]) .padding(0.1); // Y scale (Linear scale for miles driven) const y = d3.scaleLinear() .domain([0, d3.max(chartData, d => d.milesDriven) * 1.1]) // 10% buffer .range([height, 0]); // Line generator const line = d3.line() .x(d => x(d.driverName) + x.bandwidth() / 2) // Center line on band .y(d => y(d.milesDriven)); // Add X axis svg.append("g") .attr("class", "axis x-axis") .attr("transform", `translate(0,${height})`) .call(d3.axisBottom(x)) .selectAll("text") .style("text-anchor", "end") .attr("dx", "-.8em") .attr("dy", ".15em") .attr("transform", "rotate(-45)"); // Add Y axis svg.append("g") .attr("class", "axis y-axis") .call(d3.axisLeft(y).tickFormat(d3.format("~s"))); // Format large numbers // Add the line path svg.append("path") .datum(chartData) .attr("class", "line") .attr("d", line); // Tooltip const tooltip = d3.select("body").append("div") .attr("class", "chart-tooltip") .style("opacity", 0); // Add dots for each data point svg.selectAll(".dot") .data(chartData) .enter().append("circle") .attr("class", "dot") .attr("cx", d => x(d.driverName) + x.bandwidth() / 2) .attr("cy", d => y(d.milesDriven)) .attr("r", 5) .on("mouseover", function(event, d) { d3.select(this).attr("r", 7).style("fill", "orange"); tooltip.transition() .duration(200) .style("opacity", .9); tooltip.html(`Driver: ${d.driverName}Miles: ${d.milesDriven.toLocaleString('en-US')}`) .style("left", (event.pageX + 10) + "px") .style("top", (event.pageY - 28) + "px"); }) .on("mouseout", function(d) { d3.select(this).attr("r", 5).style("fill", "steelblue"); tooltip.transition() .duration(500) .style("opacity", 0); }); } // --- PDF Download Function --- /** * Generates and downloads a PDF of the current dashboard content. */ window.downloadPdf = async function() { if (!dashboardTab) { console.error('Error: Dashboard tab content not found for PDF generation.'); return; } // Temporarily hide elements not needed in PDF const elementsToHide = document.querySelectorAll('.tab-nav, .nav-buttons, #downloadPdfButton, .chart-tooltip'); elementsToHide.forEach(el => el.style.display = 'none'); // Ensure the dashboard tab is active for capture dashboardTab.classList.add('active'); // Capture summary metrics const summaryCanvas = await html2canvas(document.querySelector('.summary-metrics'), { scale: 2, useCORS: true, logging: false }); const summaryImgData = summaryCanvas.toDataURL('image/png'); // Capture driver cards const cardsCanvas = await html2canvas(driver-cards-container, { scale: 2, useCORS: true, logging: false }); const cardsImgData = cardsCanvas.toDataURL('image/png'); // Capture safety score chart const safetyChartCanvas = await html2canvas(safetyScoreChartContainer, { scale: 2, useCORS: true, logging: false }); const safetyChartImgData = safetyChartCanvas.toDataURL('image/png'); // Capture miles per driver chart const milesChartCanvas = await html2canvas(milesPerDriverChartContainer, { scale: 2, useCORS: true, logging: false }); const milesChartImgData = milesChartCanvas.toDataURL('image/png'); // Re-show hidden elements immediately after capture elementsToHide.forEach(el => el.style.display = ''); const { jsPDF } = window.jspdf; const pdf = new jsPDF({ orientation: 'portrait', unit: 'px', format: 'a4' }); const pdfWidth = pdf.internal.pageSize.getWidth(); let yOffset = 40; // Add title to PDF pdf.setFontSize(22); pdf.text("Driver Performance Report", pdfWidth / 2, yOffset, { align: 'center' }); yOffset += 20; // Add current date/time to PDF pdf.setFontSize(10); pdf.text(`Generated on: ${new Date().toLocaleString()}`, pdfWidth / 2, yOffset, { align: 'center' }); yOffset += 40; // Add Summary Metrics Image pdf.setFontSize(18); pdf.text("Summary Metrics", pdfWidth / 2, yOffset, { align: 'center' }); yOffset += 10; const summaryImgHeight = (summaryCanvas.height * (pdfWidth - 40)) / summaryCanvas.width; pdf.addImage(summaryImgData, 'PNG', 20, yOffset, pdfWidth - 40, summaryImgHeight); yOffset += summaryImgHeight + 30; // Add Driver Cards Image pdf.setFontSize(18); pdf.text("Individual Driver Performance", pdfWidth / 2, yOffset, { align: 'center' }); yOffset += 10; const cardsImgHeight = (cardsCanvas.height * (pdfWidth - 40)) / cardsCanvas.width; // Check if image fits on current page, add new page if not if (yOffset + cardsImgHeight > pdf.internal.pageSize.getHeight() - 20) { pdf.addPage(); yOffset = 40; // Reset yOffset for new page } pdf.addImage(cardsImgData, 'PNG', 20, yOffset, pdfWidth - 40, cardsImgHeight); yOffset += cardsImgHeight + 30; // Add Safety Score Chart Image pdf.setFontSize(18); pdf.text("Safety Score Distribution Chart", pdfWidth / 2, yOffset, { align: 'center' }); yOffset += 10; const safetyChartImgHeight = (safetyChartCanvas.height * (pdfWidth - 40)) / safetyChartCanvas.width; // Check if image fits on current page, add new page if not if (yOffset + safetyChartImgHeight > pdf.internal.pageSize.getHeight() - 20) { pdf.addPage(); yOffset = 40; // Reset yOffset for new page } pdf.addImage(safetyChartImgData, 'PNG', 20, yOffset, pdfWidth - 40, safetyChartImgHeight); yOffset += safetyChartImgHeight + 30; // Add Miles Per Driver Chart Image pdf.setFontSize(18); pdf.text("Miles Driven Per Driver Chart", pdfWidth / 2, yOffset, { align: 'center' }); yOffset += 10; const milesChartImgHeight = (milesChartCanvas.height * (pdfWidth - 40)) / milesChartCanvas.width; // Check if image fits on current page, add new page if not if (yOffset + milesChartImgHeight > pdf.internal.pageSize.getHeight() - 20) { pdf.addPage(); yOffset = 40; // Reset yOffset for new page } pdf.addImage(milesChartImgData, 'PNG', 20, yOffset, pdfWidth - 40, milesChartImgHeight); yOffset += milesChartImgHeight + 30; // Add a section for detailed driver data (tabular format) pdf.addPage(); pdf.setFontSize(18); pdf.text("Detailed Driver Information", pdf.internal.pageSize.getWidth() / 2, 40, { align: 'center' }); const tableData = drivers.map(d => [ d.id, d.driverName, d.totalTrips.toLocaleString('en-US'), d.milesDriven.toLocaleString('en-US'), d.averageSpeed !== null ? d.averageSpeed.toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) : 'N/A', d.safetyScore.toLocaleString('en-US'), d.fuelEfficiency !== null ? d.fuelEfficiency.toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) : 'N/A', d.lastReviewDate || 'N/A', d.notes || 'N/A' ]); pdf.autoTable({ head: [['ID', 'Driver Name', 'Trips', 'Miles', 'Avg. Speed', 'Safety Score', 'Fuel Eff.', 'Last Review', 'Notes']], body: tableData, startY: 60, theme: 'grid', styles: { fontSize: 6, cellPadding: 2 }, // Smaller font for many columns headStyles: { fillColor: [242, 242, 242], textColor: [51, 51, 51], fontStyle: 'bold' }, alternateRowStyles: { fillColor: [251, 251, 251] }, margin: { top: 70, left: 5, right: 5 } // Adjust margins for more columns }); pdf.save('driver_performance_report.pdf'); }; // --- Event Listeners and Initial Render --- downloadPdfButton.addEventListener('click', downloadPdf); // Initial setup // Sort drivers by safety score (descending) initially drivers.sort((a, b) => b.safetyScore - a.safetyScore); updateConfigTable(); renderDashboardMetrics(); renderDriverCards(); renderSafetyScoreChart(); renderMilesPerDriverChart(); updateNavigationButtons(); // Set initial button states });
