Gov Contracting Dashboard

Loading data…

Federal Contract Award Analytics

Interactive dashboard analyzing USASpending contract award data

Search Filters

Total Contract Value
$0
Total Contracts
0
Small Business Value
$0
Small Business Vendors
0

Contract Spending Over Time

Showing quarterly spending for the past 5 years

Spending Distribution

Showing distribution by business size
+ val.toLocaleString(); } } } }, tension: 0.4, elements: { point: { radius: 3, hoverRadius: 6 }, line: { borderWidth: 3 } } } }); // Enhanced donut chart configuration donutChart = new Chart(donutCtx, { type: ‘doughnut’, data: { labels: [], datasets: [] }, options: { responsive: true, maintainAspectRatio: false, onClick: function(e, elements) { console.log(‘Donut chart clicked’, e, elements); handleDonutChartClick(e); }, cutout: ‘60%’, plugins: { legend: { position: ‘bottom’, labels: { boxWidth: 12, padding: 15, usePointStyle: true, pointStyle: ‘circle’ } }, tooltip: { callbacks: { label: (context) => { const val = context.parsed || 0; const label = context.label || ”; const total = context.dataset.data.reduce((s, v) => s + v, 0); const pct = total > 0 ? ((val / total) * 100).toFixed(1) : 0; return `${label}: ${val.toLocaleString()} (${pct}%)`; } } } }, animation: { animateRotate: true, animateScale: true } } }); console.log(‘Charts initialized successfully’); } catch (error) { console.error(‘Error initializing charts:’, error); showError(‘There was an error initializing the charts. Please refresh the page and try again.’); showLoading(false); } } // Load data with default filters async function loadInitialData() { try { showLoading(true); // Build default time period – last 5 fiscal years const timePeriod = getTimePeriod(5); currentFilters = { time_period: [timePeriod], award_type_codes: [“A”, “B”, “C”, “D”] // Contract award types }; await fetchDataAndRender(); } catch (error) { console.error(‘Error loading initial data:’, error); showError(‘There was an error loading the dashboard data. Please try again later.’); } finally { showLoading(false); } } // Calculate the time period based on fiscal years function getTimePeriod(yearRange) { const currentDate = new Date(); let fiscalYear = currentDate.getFullYear(); const currentMonth = currentDate.getMonth(); // 0-indexed // If before October, we’re in the previous fiscal year if (currentMonth ({…r, type: ‘small’})), …otherBizData.results.map(r => ({…r, type: ‘other’})) ].sort((a, b) => { if (a.time_period.fiscal_year !== b.time_period.fiscal_year) { return a.time_period.fiscal_year – b.time_period.fiscal_year; } return a.time_period.quarter – b.time_period.quarter; }); // Get unique sorted quarters const uniqueQuarters = […new Set(combinedResults.map(r => `${r.time_period.fiscal_year}-Q${r.time_period.quarter}` ))].sort(); // Create data points for each quarter uniqueQuarters.forEach(quarter => { quarterLabels.push(quarter); const smallResult = smallBizData.results.find(r => `${r.time_period.fiscal_year}-Q${r.time_period.quarter}` === quarter ); const otherResult = otherBizData.results.find(r => `${r.time_period.fiscal_year}-Q${r.time_period.quarter}` === quarter ); smallAmounts.push(smallResult ? Math.round(smallResult.aggregated_amount) : 0); otherAmounts.push(otherResult ? Math.round(otherResult.aggregated_amount) : 0); }); // Store current line chart data for reset functionality currentLineData = { labels: quarterLabels, datasets: [ { label: ‘Small Business’, data: smallAmounts, borderColor: chartColors.small, backgroundColor: `${chartColors.small}20`, pointBackgroundColor: chartColors.small, borderWidth: 3, fill: true }, { label: ‘Other-than-Small’, data: otherAmounts, borderColor: chartColors.other, backgroundColor: `${chartColors.other}20`, pointBackgroundColor: chartColors.other, borderWidth: 3, fill: true } ] }; // Update line chart lineChart.data = currentLineData; lineChart.update(); // 2. Donut chart: small vs other const totalSmall = smallAmounts.reduce((s,v)=>s+v,0); const totalOther = otherAmounts.reduce((s,v)=>s+v,0); // Store current donut chart data for reset functionality currentDonutData = { labels: [‘Small Business’,’Other-than-Small’], datasets: [{ data: [totalSmall, totalOther], backgroundColor: [chartColors.small, chartColors.other], borderColor: [‘#ffffff’, ‘#ffffff’], borderWidth: 2, hoverOffset: 15 }] }; // Update donut chart donutChart.data = currentDonutData; donutChart.update(); // 3. Update chart subtitles updateChartSubtitles(); // 4. Get contract counts and vendor data await fetchContractCounts(totalSmall, totalOther); } catch (error) { console.error(‘Error fetching data:’, error); showError(‘Error retrieving data from USASpending. Please try again or adjust your search criteria.’); // Initialize charts with empty data initializeEmptyCharts(); } } // Fetch spending over time data from USASpending API async function fetchSpendingOverTime(filters) { const requestBody = { group: “quarter”, filters: filters, subawards: false }; try { const response = await fetch(SPENDING_OVER_TIME_ENDPOINT, { method: ‘POST’, headers: { ‘Content-Type’: ‘application/json’ }, body: JSON.stringify(requestBody) }); if (!response.ok) { throw new Error(`API request failed with status ${response.status}`); } const data = await response.json(); return data; } catch (error) { console.error(‘Error fetching spending over time:’, error); throw error; } } // Fetch contract counts by category (award counts, vendor counts) async function fetchContractCounts(totalSmall, totalOther) { try { // Get contract counts – small business vs other const contractCountRequest = { filters: {…currentFilters}, category: “award_count” }; const smallVendorRequest = { filters: {…currentFilters, business_size: “S”}, category: “recipient_duns” }; // Use spending by category endpoint to get counts const [countResponse, vendorResponse] = await Promise.all([ fetch(SPENDING_BY_CATEGORY_ENDPOINT, { method: ‘POST’, headers: { ‘Content-Type’: ‘application/json’ }, body: JSON.stringify(contractCountRequest) }), fetch(SPENDING_BY_CATEGORY_ENDPOINT, { method: ‘POST’, headers: { ‘Content-Type’: ‘application/json’ }, body: JSON.stringify(smallVendorRequest) }) ]); if (!countResponse.ok || !vendorResponse.ok) { throw new Error(‘Failed to fetch count data’); } const countData = await countResponse.json(); const vendorData = await vendorResponse.json(); // Total contract count const totalContracts = countData?.results?.length > 0 ? countData.results[0].count : 0; // Small vendor count (unique recipients) const smallVendors = vendorData?.results?.length || 0; // Update KPIs updateKPIs({ totalValue: totalSmall + totalOther, contractCount: totalContracts, smallBizValue: totalSmall, smallVendorCount: smallVendors }); // Calculate and display previous period comparison if (chartState.previousData) { calculatePeriodComparison(totalSmall, totalOther, totalContracts, smallVendors); } } catch (error) { console.error(‘Error fetching contract counts:’, error); // Use available data for KPIs even if count request fails updateKPIs({ totalValue: totalSmall + totalOther, contractCount: 0, smallBizValue: totalSmall, smallVendorCount: 0 }); } } // Calculate period-over-period comparison for KPIs async function calculatePeriodComparison(totalSmall, totalOther, totalContracts, smallVendors) { try { // Only attempt if we have previous period data if (!chartState.previousData?.time_period) return; // Create filters for previous period with same length const prevStartDate = new Date(chartState.previousData.time_period.start_date); const prevEndDate = new Date(chartState.previousData.time_period.end_date); // Calculate previous period (same length, one period earlier) const periodLength = (prevEndDate – prevStartDate) / (1000 * 60 * 60 * 24); const newPrevStart = new Date(prevStartDate); newPrevStart.setDate(newPrevStart.getDate() – periodLength); const prevFilters = { …currentFilters, time_period: [{ start_date: newPrevStart.toISOString().split(‘T’)[0], end_date: prevStartDate.toISOString().split(‘T’)[0] }] }; // Get previous period data const [prevSmallData, prevOtherData] = await Promise.all([ fetchSpendingOverTime({ …prevFilters, business_size: ‘S’ }), fetchSpendingOverTime({ …prevFilters, business_size: ‘O’ }) ]); // Calculate previous totals const prevSmallTotal = prevSmallData?.results?.reduce((total, item) => total + (item.aggregated_amount || 0), 0) || 0; const prevOtherTotal = prevOtherData?.results?.reduce((total, item) => total + (item.aggregated_amount || 0), 0) || 0; // Calculate percent changes const totalChange = calculatePercentChange(prevSmallTotal + prevOtherTotal, totalSmall + totalOther); const smallBizChange = calculatePercentChange(prevSmallTotal, totalSmall); // Update KPI change indicators document.getElementById(‘kpiTotalChange’).textContent = formatChange(totalChange); document.getElementById(‘kpiTotalChange’).className = `kpi-change ${totalChange >= 0 ? ‘positive’ : ‘negative’}`; document.getElementById(‘kpiSmallBizChange’).textContent = formatChange(smallBizChange); document.getElementById(‘kpiSmallBizChange’).className = `kpi-change ${smallBizChange >= 0 ? ‘positive’ : ‘negative’}`; // For contract and vendor counts, show placeholder change values document.getElementById(‘kpiContractsChange’).textContent = ‘from previous period’; document.getElementById(‘kpiContractsChange’).className = ‘kpi-change’; document.getElementById(‘kpiSmallVendorsChange’).textContent = ‘from previous period’; document.getElementById(‘kpiSmallVendorsChange’).className = ‘kpi-change’; } catch (error) { console.error(‘Error calculating period comparison:’, error); // Set default change values setDefaultKpiChanges(); } } // Calculate percent change between two values function calculatePercentChange(oldValue, newValue) { if (oldValue === 0) return newValue > 0 ? 100 : 0; return ((newValue – oldValue) / oldValue) * 100; } // Format percent change for display function formatChange(percentChange) { const sign = percentChange >= 0 ? ‘+’ : ”; return `${sign}${percentChange.toFixed(1)}% from previous period`; } // Set default percent changes for KPIs function setDefaultKpiChanges() { document.getElementById(‘kpiTotalChange’).textContent = ‘from previous period’; document.getElementById(‘kpiTotalChange’).className = ‘kpi-change’; document.getElementById(‘kpiContractsChange’).textContent = ‘from previous period’; document.getElementById(‘kpiContractsChange’).className = ‘kpi-change’; document.getElementById(‘kpiSmallBizChange’).textContent = ‘from previous period’; document.getElementById(‘kpiSmallBizChange’).className = ‘kpi-change’; document.getElementById(‘kpiSmallVendorsChange’).textContent = ‘from previous period’; document.getElementById(‘kpiSmallVendorsChange’).className = ‘kpi-change’; } // Initialize charts with empty data (used when API fails) function initializeEmptyCharts() { // Reset line chart lineChart.data = { labels: [], datasets: [ { label: ‘Small Business’, data: [], borderColor: chartColors.small, backgroundColor: `${chartColors.small}20`, }, { label: ‘Other-than-Small’, data: [], borderColor: chartColors.other, backgroundColor: `${chartColors.other}20`, } ] }; lineChart.update(); // Reset donut chart donutChart.data = { labels: [‘Small Business’, ‘Other-than-Small’], datasets: [{ data: [0, 0], backgroundColor: [chartColors.small, chartColors.other], borderColor: [‘#ffffff’, ‘#ffffff’], borderWidth: 2 }] }; donutChart.update(); // Reset KPIs updateKPIs({ totalValue: 0, contractCount: 0, smallBizValue: 0, smallVendorCount: 0 }); setDefaultKpiChanges(); } // Handle line chart click events function handleLineChartClick(evt) { const points = lineChart.getElementsAtEventForMode(evt, ‘nearest’, { intersect: true }, true); if (!points.length) return; const idx = points[0].index; const datasetIdx = points[0].datasetIndex; const quarterLabel = lineChart.data.labels[idx]; // If clicking the same quarter again, reset the view if (chartState.selectedQuarter === quarterLabel) { resetDonutChart(); return; } chartState.selectedQuarter = quarterLabel; document.getElementById(‘lineChartSubtitle’).textContent = `Focusing on ${quarterLabel}`; // Extract fiscal year and quarter const [fyStr, qStr] = quarterLabel.split(‘-Q’); // If we’re already in set-aside drill-down mode if (chartState.drilledIntoSetAside) { // Update the set-aside breakdown for this specific quarter showSetAsideBreakdown(parseInt(fyStr, 10), parseInt(qStr, 10)); } else { // Regular quarter drill-down drillDonutToQuarter(parseInt(fyStr, 10), parseInt(qStr, 10)); } } // Handle donut chart click events function handleDonutChartClick(evt) { const activePoints = donutChart.getElementsAtEventForMode(evt, ‘nearest’, { intersect: true }, true); if (!activePoints.length) return; const idx = activePoints[0].index; const label = donutChart.data.labels[idx]; // If not yet drilled into set-asides and clicked on Small Business slice if (!chartState.drilledIntoSetAside && label === ‘Small Business’) { chartState.drilledIntoSetAside = true; showSetAsideBreakdown(); document.getElementById(‘donutChartSubtitle’).textContent = ‘Small Business Set-Aside Programs’; } // If already in set-aside view and clicked on a specific set-aside else if (chartState.drilledIntoSetAside) { const setAsideCode = Object.keys(setAsideMap).find(code => setAsideMap[code].label === label ); if (setAsideCode) { drillLineChartToSetAside(setAsideCode, label); } } // If not drilled in and clicked other-than-small else if (!chartState.drilledIntoSetAside && label === ‘Other-than-Small’) { alert(‘No further breakdown for Other-than-Small Business contracts is available.’); } } // Show donut breakdown for small business set-asides async function showSetAsideBreakdown(fiscalYear = null, quarter = null) { try { showLoading(true); // Create base filters – either for specific quarter or whole period let setAsideFilters = {…currentFilters}; if (fiscalYear !== null && quarter !== null) { // For specific quarter setAsideFilters = { …currentFilters, time_period: [{ start_date: `${fiscalYear}-${quarter * 3 – 2}-01`, end_date: `${fiscalYear}-${quarter * 3}-${quarter === 4 ? ’30’ : ’31’}` }] }; } // Set business size to small setAsideFilters.business_size = “S”; // Use spending by category endpoint to get set-aside breakdown const requestBody = { filters: setAsideFilters, category: “type_of_set_aside”, subawards: false }; const response = await fetch(SPENDING_BY_CATEGORY_ENDPOINT, { method: ‘POST’, headers: { ‘Content-Type’: ‘application/json’ }, body: JSON.stringify(requestBody) }); if (!response.ok) { throw new Error(`API request failed with status ${response.status}`); } const data = await response.json(); // Transform API results to our format let results = []; if (data.results && data.results.length > 0) { data.results.forEach(item => { // Map API set-aside codes to our set-aside map const code = item.code; const setAside = setAsideMap[code] || { code: code, label: item.name || code, description: “Small business set-aside program” }; results.push({ code: code, label: setAside.label, description: setAside.description, amount: Math.round(item.amount) }); }); } // Add “Other Small Business” category if we have any small business spending const lineChartData = lineChart.data.datasets[0].data; const totalSmall = lineChartData.reduce((sum, val) => sum + val, 0); const totalSetAside = results.reduce((sum, item) => sum + item.amount, 0); if (totalSmall > totalSetAside) { results.push({ code: “OTHER_SMALL”, label: “Other Small Business”, description: “Small businesses without set-aside designation”, amount: Math.round(totalSmall – totalSetAside) }); } // Sort by amount descending results.sort((a, b) => b.amount – a.amount); // Limit to top 6 categories to fit our color scheme if (results.length > 6) { const otherAmount = results.slice(5).reduce((sum, item) => sum + item.amount, 0); results = results.slice(0, 5); results.push({ code: “OTHER”, label: “Other Set-Asides”, description: “Other small business set-aside programs”, amount: otherAmount }); } // Update donut chart with set-aside data donutChart.data.labels = results.map(r => r.label); donutChart.data.datasets[0].data = results.map(r => r.amount); donutChart.data.datasets[0].backgroundColor = chartColors.setAsides.slice(0, results.length); donutChart.update(); // Update legend with descriptions updateDonutLegend(results); } catch (error) { console.error(‘Error showing set-aside breakdown:’, error); showError(‘Unable to fetch set-aside program data. Please try again.’); // Reset to previous view resetDonutChart(); } finally { showLoading(false); } } // Update donut chart legend with program descriptions function updateDonutLegend(results) { const legendEl = document.getElementById(‘donutLegend’); legendEl.innerHTML = ”; if (results.length > 0 && chartState.drilledIntoSetAside) { results.forEach((result, idx) => { const description = result.description || “Small business set-aside program”; const color = chartColors.setAsides[idx % chartColors.setAsides.length]; const itemEl = document.createElement(‘div’); itemEl.className = ‘state-legend-item’; itemEl.innerHTML = `
${result.label}: ${description}
`; legendEl.appendChild(itemEl); }); } else { legendEl.innerHTML = ”; } } // Filter the line chart for a specific set-aside type async function drillLineChartToSetAside(code, label) { try { showLoading(true); // Create filter set for this set-aside type const setAsideFilters = { …currentFilters, business_size: “S” }; // Add set-aside filter if it’s a specific set-aside code (not OTHER_SMALL or OTHER) if (code !== “OTHER_SMALL” && code !== “OTHER”) { setAsideFilters.set_aside_type = [code]; } // Filter further if a specific quarter is selected if (chartState.selectedQuarter) { const [fyStr, qStr] = chartState.selectedQuarter.split(‘-Q’); const fy = parseInt(fyStr); const q = parseInt(qStr); // Set time period to just this quarter setAsideFilters.time_period = [{ start_date: `${fy}-${q * 3 – 2}-01`, end_date: `${fy}-${q * 3}-${q === 4 ? ’30’ : ’31’}` }]; // Fetch data for just this quarter const response = await fetchSpendingOverTime(setAsideFilters); if (response.results && response.results.length > 0) { const quarterData = response.results[0]; const quarterLabel = `${fy}-Q${q}`; lineChart.data.labels = [quarterLabel]; lineChart.data.datasets = [ { label: `${label} Contracts`, data: [Math.round(quarterData.aggregated_amount)], borderColor: chartColors.setAsides[0], backgroundColor: `${chartColors.setAsides[0]}20`, pointBackgroundColor: chartColors.setAsides[0], borderWidth: 3, fill: true } ]; lineChart.update(); // Update line chart subtitle document.getElementById(‘lineChartSubtitle’).textContent = `Showing ${label} contracts for ${quarterLabel}`; showLoading(false); return; } } // Fetch spending over time for this set-aside type const response = await fetchSpendingOverTime(setAsideFilters); if (!response.results || response.results.length === 0) { throw new Error(‘No data found for the selected set-aside type’); } // Sort by fiscal year and quarter response.results.sort((a, b) => { if (a.time_period.fiscal_year !== b.time_period.fiscal_year) { return a.time_period.fiscal_year – b.time_period.fiscal_year; } return a.time_period.quarter – b.time_period.quarter; }); // Build chart data const labels = response.results.map(r => `${r.time_period.fiscal_year}-Q${r.time_period.quarter}` ); const amounts = response.results.map(r => Math.round(r.aggregated_amount) ); // Update line chart lineChart.data.labels = labels; lineChart.data.datasets = [ { label: `${label} Contracts`, data: amounts, borderColor: chartColors.setAsides[0], backgroundColor: `${chartColors.setAsides[0]}20`, pointBackgroundColor: chartColors.setAsides[0], borderWidth: 3, fill: true } ]; lineChart.update(); // Update line chart subtitle document.getElementById(‘lineChartSubtitle’).textContent = `Showing quarterly spending for ${label} contracts`; } catch (error) { console.error(‘Error drilling down to set-aside:’, error); showError(‘Unable to fetch set-aside time series data. Please try again.’); resetLineChart(); } finally { showLoading(false); } } // Re-fetch donut data for a specific quarter async function drillDonutToQuarter(fy, quarter) { try { showLoading(true); const quarterStr = `${fy}-Q${quarter}`; // Create quarter-specific filters const quarterFilters = { …currentFilters, time_period: [{ start_date: `${fy}-${quarter * 3 – 2}-01`, end_date: `${fy}-${quarter * 3}-${quarter === 4 ? ’30’ : ’31’}` }] }; // Get data for the specific quarter – small business const smallBizFilter = { …quarterFilters, business_size: “S” }; // Other-than-small business const otherBizFilter = { …quarterFilters, business_size: “O” }; // Fetch both datasets const [smallBizData, otherBizData] = await Promise.all([ fetchSpendingOverTime(smallBizFilter), fetchSpendingOverTime(otherBizFilter) ]); // Calculate totals for the quarter const smallBizAmount = smallBizData.results && smallBizData.results.length > 0 ? Math.round(smallBizData.results[0].aggregated_amount) : 0; const otherAmount = otherBizData.results && otherBizData.results.length > 0 ? Math.round(otherBizData.results[0].aggregated_amount) : 0; // Update donut chart donutChart.data.labels = [‘Small Business’,’Other-than-Small’]; donutChart.data.datasets[0].data = [smallBizAmount, otherAmount]; donutChart.data.datasets[0].backgroundColor = [chartColors.small, chartColors.other]; donutChart.update(); // Clear any previous legend document.getElementById(‘donutLegend’).innerHTML = ”; // Update donut subtitle document.getElementById(‘donutChartSubtitle’).textContent = `Spending distribution for ${fy}-Q${quarter}`; // Get contract counts for this quarter const contractCountRequest = { filters: quarterFilters, category: “award_count” }; const smallVendorRequest = { filters: smallBizFilter, category: “recipient_duns” }; // Try to get contract and vendor counts try { const [countResponse, vendorResponse] = await Promise.all([ fetch(SPENDING_BY_CATEGORY_ENDPOINT, { method: ‘POST’, headers: { ‘Content-Type’: ‘application/json’ }, body: JSON.stringify(contractCountRequest) }), fetch(SPENDING_BY_CATEGORY_ENDPOINT, { method: ‘POST’, headers: { ‘Content-Type’: ‘application/json’ }, body: JSON.stringify(smallVendorRequest) }) ]); if (countResponse.ok && vendorResponse.ok) { const countData = await countResponse.json(); const vendorData = await vendorResponse.json(); const contractCount = countData?.results?.length > 0 ? countData.results[0].count : 0; const smallVendorCount = vendorData?.results?.length || 0; // Update KPIs updateKPIs({ totalValue: smallBizAmount + otherAmount, contractCount: contractCount, smallBizValue: smallBizAmount, smallVendorCount: smallVendorCount }); } else { // Use available data for KPIs updateKPIs({ totalValue: smallBizAmount + otherAmount, contractCount: 0, smallBizValue: smallBizAmount, smallVendorCount: 0 }); } } catch (error) { console.error(‘Error fetching contract counts for quarter:’, error); // Update KPIs with available data updateKPIs({ totalValue: smallBizAmount + otherAmount, contractCount: 0, smallBizValue: smallBizAmount, smallVendorCount: 0 }); } } catch (error) { console.error(‘Error drilling down to quarter:’, error); showError(‘Unable to fetch quarterly data. Please try again.’); resetDonutChart(); } finally { showLoading(false); } } // Reset line chart to original view function resetLineChart() { lineChart.data = currentLineData; lineChart.update(); // Reset subtitle document.getElementById(‘lineChartSubtitle’).textContent = ‘Showing quarterly spending for the selected time period’; // Reset chart state if focused on line chart if (chartState.selectedQuarter) { chartState.selectedQuarter = null; // If we’re in set-aside mode, reload the set-aside donut if (chartState.drilledIntoSetAside) { showSetAsideBreakdown(); } else { // Otherwise reset the donut chart too resetDonutChart(); } } } // Reset donut chart to original view function resetDonutChart() { donutChart.data = currentDonutData; donutChart.update(); // Reset subtitle document.getElementById(‘donutChartSubtitle’).textContent = ‘Showing distribution by business size’; // Clear legend document.getElementById(‘donutLegend’).innerHTML = ”; // Reset drill-down state chartState.drilledIntoSetAside = false; // Refresh KPIs refreshKPIs(); } // Refresh KPIs to match current filter set async function refreshKPIs() { try { showLoading(true); // Calculate total values from current line chart data const smallAmounts = lineChart.data.datasets[0].data; const otherAmounts = lineChart.data.datasets.length > 1 ? lineChart.data.datasets[1].data : []; const totalSmall = smallAmounts.reduce((s,v)=>s+v,0); const totalOther = otherAmounts.reduce((s,v)=>s+v,0); // Update KPIs with totals updateKPIs({ totalValue: totalSmall + totalOther, contractCount: 0, // Will be updated by fetchContractCounts smallBizValue: totalSmall, smallVendorCount: 0 // Will be updated by fetchContractCounts }); // Get contract and vendor counts await fetchContractCounts(totalSmall, totalOther); } catch (error) { console.error(‘Error refreshing KPIs:’, error); } finally { showLoading(false); } } // Update KPI values function updateKPIs(data) { document.getElementById(‘kpiTotal’).textContent = ‘ + formatMoney(data.totalValue); document.getElementById(‘kpiContracts’).textContent = formatNumber(data.contractCount); document.getElementById(‘kpiSmallBiz’).textContent = ‘ + formatMoney(data.smallBizValue); document.getElementById(‘kpiSmallVendors’).textContent = formatNumber(data.smallVendorCount); } // Update chart subtitles function updateChartSubtitles() { // Extract year range from filters const startDate = new Date(currentFilters.time_period[0].start_date); const endDate = new Date(currentFilters.time_period[0].end_date); const startYear = startDate.getFullYear(); const endYear = endDate.getFullYear(); const yearRange = endYear – startYear + 1; const periodText = yearRange === 1 ? ‘year’ : `${yearRange} years`; document.getElementById(‘lineChartSubtitle’).textContent = `Showing quarterly spending for the past ${periodText}`; document.getElementById(‘donutChartSubtitle’).textContent = ‘Showing distribution by business size’; } // Show/hide loading overlay function showLoading(show) { document.getElementById(‘loadingOverlay’).style.display = show ? ‘flex’ : ‘none’; } // Show/hide error message function showError(message) { const errorEl = document.getElementById(‘errorMessage’); errorEl.textContent = message; errorEl.style.display = ‘block’; // Auto-hide after 8 seconds setTimeout(() => { errorEl.style.display = ‘none’; }, 8000); } // Hide error message function hideError() { document.getElementById(‘errorMessage’).style.display = ‘none’; } // Number formatting helpers function formatMoney(val) { if (val >= 1e9) return (val/1e9).toFixed(1) + ‘B’; if (val >= 1e6) return (val/1e6).toFixed(1) + ‘M’; if (val >= 1e3) return (val/1e3).toFixed(0) + ‘K’; return val.toLocaleString(); } function formatNumber(val) { if (val >= 1e6) return (val/1e6).toFixed(1) + ‘M’; if (val >= 1e3) return (val/1e3).toFixed(1) + ‘K’; return val.toLocaleString(); }