PageSourceSearch

https://caerphilly.observer/wp-content/plugins/footfall-charts/footfall-charts.js?ver=1.1

js caerphilly.observer collected 2026-10-02 04:31:26 UTC 10,196 bytes, 278 lines download raw bytes

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.