1// Wait for Chart.js to be available before defining functions 2(function() { 3 'use strict'; 4 5 // Function to wait for Chart.js to load 6 function waitForChart(callback) { 7 if (typeof Chart !== 'undefined') { 8 callback(); 9 } else { 10 setTimeout(function() { waitForChart(callback); }, 50); 11 } 12 } 13 14 // Initialize when Chart.js is ready 15 waitForChart(function() { 16 17 window.createFootfallChart = async function createFootfallChart(canvasId, options) { 18 const canvas = document.getElementById(canvasId); 19 if (!canvas) { 20 console.error(`Canvas element with ID '${canvasId}' not found`); 21 return; 22 } 23 24 const ctx = canvas.getContext('2d'); 25 26 try { 27 // Show loading state 28 canvas.style.opacity = '0.5'; 29 30 // Validate required options 31 if (!options.town) { 32 throw new Error('Town parameter is required'); 33 } 34 35 // Check if API configuration exists 36 if (!footfallAPI || !footfallAPI.baseURL || !footfallAPI.apiKey) { 37 throw new Error('API configuration is missing. Please check plugin settings.'); 38 } 39 40 const response = await fetch( 41 `${footfallAPI.baseURL}/footfall/trends/${encodeURIComponent(options.town)}?` + 42 `days=${options.days || 30}&granularity=${options.granularity || 'daily'}`, 43 { 44 headers: { 45 'X-API-Key': footfallAPI.apiKey, 46 'Content-Type': 'application/json' 47 } 48 } 49 ); 50 51 if (!response.ok) { 52 const errorText = await response.text(); 53 throw new Error(`API request failed (${response.status}): ${errorText || response.statusText}`); 54 } 55 56 const data = await response.json(); 57 58 // Validate API response structure 59 if (!data || !data.data || !Array.isArray(data.data)) { 60 throw new Error('Invalid API response format'); 61 } 62 63 if (data.data.length === 0) { 64 throw new Error('No data available for the specified parameters'); 65 } 66 67 const chartData = { 68 labels: data.data.map(item => { 69 if (!item.period_label) return 'Unknown'; 70 71 const date = new Date(item.period_label); 72 if (isNaN(date.getTime())) return item.period_label; // Fallback to raw label 73 74 if (options.granularity === 'hourly') { 75 return date.toLocaleString(); 76 } else if (options.granularity === 'weekly') { 77 return `Week ${window.getWeekNumber(date)}`; 78 } else { 79 return date.toLocaleDateString(); 80 } 81 }), 82 datasets: [{ 83 label: 'Average Footfall', 84 data: data.data.map(item => { 85 const footfall = parseFloat(item.avg_footfall); 86 return isNaN(footfall) ? 0 : Math.round(footfall); 87 }), 88 borderColor: '#3b82f6', 89 backgroundColor: 'rgba(59, 130, 246, 0.1)', 90 borderWidth: 2, 91 fill: true, 92 tension: 0.4, 93 pointRadius: 4, 94 pointHoverRadius: 6 95 }] 96 }; 97 98 // Add comparison data if enabled and available 99 if (options.show_comparison === 'true' && data.data.some(item => item.comparison_avg !== null && item.comparison_avg !== undefined)) { 100 chartData.datasets.push({ 101 label: 'Previous Period', 102 data: data.data.map(item => { 103 const comparison = parseFloat(item.comparison_avg); 104 return isNaN(comparison) ? 0 : Math.round(comparison); 105 }), 106 borderColor: '#ef4444', 107 backgroundColor: 'rgba(239, 68, 68, 0.1)', 108 borderWidth: 2, 109 borderDash: [5, 5], 110 fill: false, 111 tension: 0.4, 112 pointRadius: 4, 113 pointHoverRadius: 6 114 }); 115 } 116 117 const chartConfig = { 118 type: options.type || 'line', 119 data: chartData, 120 options: { 121 responsive: true, 122 maintainAspectRatio: false, 123 interaction: { 124 intersect: false, 125 mode: 'index' 126 }, 127 plugins: { 128 title: { 129 display: true, 130 text: `Footfall Trends - ${data.town || options.town}`, 131 font: { size: 16, weight: 'bold' }, 132 padding: 20 133 }, 134 legend: { 135 display: true, 136 position: 'top', 137 labels: { 138 usePointStyle: true, 139 padding: 20 140 } 141 }, 142 tooltip: { 143 backgroundColor: 'rgba(0, 0, 0, 0.8)', 144 titleColor: 'white', 145 bodyColor: 'white', 146 borderColor: '#3b82f6', 147 borderWidth: 1, 148 callbacks: { 149 afterBody: function(tooltipItems) { 150 const dataIndex = tooltipItems[0].dataIndex; 151 const item = data.data[dataIndex]; 152 if (item && item.percent_change !== null && item.percent_change !== undefined) { 153 const change = parseFloat(item.percent_change);
154 if (!isNaN(change)) { 155 const prefix = change > 0 ? '+' : ''; 156 return `Change: ${prefix}${change.toFixed(1)}%`; 157 } 158 } 159 return ''; 160 } 161 } 162 } 163 }, 164 scales: { 165 y: { 166 beginAtZero: true, 167 title: { 168 display: true, 169 text: 'Average Footfall Count', 170 font: { weight: 'bold' } 171 }, 172 grid: { 173 color: 'rgba(0, 0, 0, 0.1)' 174 } 175 }, 176 x: { 177 title: { 178 display: true, 179 text: 'Date', 180 font: { weight: 'bold' } 181 }, 182 grid: { 183 color: 'rgba(0, 0, 0, 0.1)' 184 } 185 } 186 }, 187 elements: { 188 point: { 189 hoverBackgroundColor: '#ffffff', 190 hoverBorderWidth: 3 191 } 192 } 193 } 194 }; 195 196 // Destroy existing chart if it exists 197 if (window.footfallCharts && window.footfallCharts[canvasId]) { 198 window.footfallCharts[canvasId].destroy(); 199 } 200 201 // Initialize charts storage 202 if (!window.footfallCharts) { 203 window.footfallCharts = {}; 204 } 205 206 // Create the chart 207 window.footfallCharts[canvasId] = new Chart(ctx, chartConfig); 208 209 // Remove loading state 210 canvas.style.opacity = '1'; 211 212 console.log(`Footfall chart created successfully for ${options.town}`); 213 214 } catch (error) { 215 console.error('Error creating footfall chart:', error); 216 canvas.style.opacity = '1'; 217 218 // Show user-friendly error message 219 const errorDiv = document.createElement('div'); 220 errorDiv.className = 'footfall-chart-error'; 221 errorDiv.innerHTML = ` 222 <div style=" 223 color: #dc3545; 224 text-align: center; 225 padding: 20px; 226 border: 1px solid #dc3545; 227 border-radius: 4px; 228 background-color: #f8d7da; 229 font-family: Arial, sans-serif; 230 "> 231 <strong>Chart Loading Error</strong><br> 232 <small>${error.message}</small> 233 </div> 234 `; 235 236 // Replace canvas with error message 237 if (canvas.parentNode) { 238 canvas.parentNode.replaceChild(errorDiv, canvas); 239 } 240 } 241 } 242 243 // Helper function to get week number with error handling 244 window.getWeekNumber = function getWeekNumber(date) { 245 try { 246 const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); 247 const dayNum = d.getUTCDay() || 7; 248 d.setUTCDate(d.getUTCDate() + 4 - dayNum); 249 const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); 250 return Math.ceil((((d - yearStart) / 86400000) + 1) / 7); 251 } catch (error) { 252 console.error('Error calculating week number:', error); 253 return 1; // Fallback to week 1 254 } 255 } 256 257 // Helper function to refresh a chart 258 window.refreshFootfallChart = function refreshFootfallChart(canvasId, options) { 259 if (window.footfallCharts && window.footfallCharts[canvasId]) { 260 window.createFootfallChart(canvasId, options); 261 } 262 } 263 264 // Helper function to destroy all charts (useful for cleanup) 265 window.destroyAllFootfallCharts = function destroyAllFootfallCharts() { 266 if (window.footfallCharts) {
267 Object.keys(window.footfallCharts).forEach(canvasId => { 268 if (window.footfallCharts[canvasId]) { 269 window.footfallCharts[canvasId].destroy(); 270 } 271 }); 272 window.footfallCharts = {}; 273 } 274 } 275 276 }); // End waitForChart callback 277 278})(); // End IIFE
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.