|
7 | 7 | <link href="https://unpkg.com/maplibre-gl@^4/dist/maplibre-gl.css" rel="stylesheet" /> |
8 | 8 | <script src="https://unpkg.com/maplibre-gl@^4/dist/maplibre-gl.js"></script> |
9 | 9 | <script type="module"> |
10 | | - import { ScreenGridLayerGL, GlyphUtilities } from '../src/screengrid.js'; |
| 10 | + import { ScreenGridLayerGL, GlyphUtilities } from '../src/index.js'; |
| 11 | + |
| 12 | + // Global state for hovered year reference |
| 13 | + let hoveredYear = null; |
| 14 | + let gridLayer = null; |
11 | 15 |
|
12 | 16 | const map = new maplibregl.Map({ |
13 | 17 | container: 'map', |
|
26 | 30 | { id: 'dark', type: 'raster', source: 'dark', paint: { 'raster-opacity': 1.0 } } |
27 | 31 | ] |
28 | 32 | }, |
29 | | - // center: [-1.25, 51.75], |
30 | 33 | // center shows cambridge, uk |
31 | 34 | center: [0.1278, 52.2053], |
32 | 35 | zoom: 12 |
|
35 | 38 | /** |
36 | 39 | * Custom glyph function for time series visualization |
37 | 40 | * Groups data by year and aggregates ashp_carbonsaved values |
| 41 | + * Also draws reference line for hovered year if applicable |
38 | 42 | */ |
39 | 43 | function drawTimeSeriesGlyph(ctx, x, y, normVal, cellInfo) { |
40 | 44 | const { cellData, cellSize } = cellInfo; |
|
71 | 75 |
|
72 | 76 | if (timeSeriesData.length === 0) return; |
73 | 77 |
|
| 78 | + // Get year range for this cell |
| 79 | + const years = timeSeriesData.map(d => d.year); |
| 80 | + const minYear = Math.min(...years); |
| 81 | + const maxYear = Math.max(...years); |
| 82 | + const yearRange = maxYear - minYear || 1; |
| 83 | + |
74 | 84 | // Draw time series chart |
| 85 | + const padding = 0.15; |
75 | 86 | GlyphUtilities.drawTimeSeriesGlyph( |
76 | 87 | ctx, |
77 | 88 | x, |
|
86 | 97 | showPoints: true, |
87 | 98 | showArea: true, |
88 | 99 | areaColor: 'rgba(46, 204, 113, 0.15)', |
89 | | - padding: 0.15 |
| 100 | + padding: padding |
90 | 101 | } |
91 | 102 | ); |
92 | | - } |
93 | 103 |
|
94 | | - /** |
95 | | - * Alternative: Bar chart over time |
96 | | - */ |
97 | | - function drawTimeSeriesBarGlyph(ctx, x, y, normVal, cellInfo) { |
98 | | - const { cellData, cellSize } = cellInfo; |
99 | | - |
100 | | - if (!cellData || cellData.length === 0) return; |
101 | | - |
102 | | - // Group by year |
103 | | - const yearData = {}; |
104 | | - cellData.forEach(item => { |
105 | | - const year = item.data.year; |
106 | | - const carbonSaved = item.data.ashp_carbonsaved; |
| 104 | + // Draw reference line for hovered year if it exists in this cell's data |
| 105 | + if (hoveredYear !== null && hoveredYear >= minYear && hoveredYear <= maxYear) { |
| 106 | + const chartWidth = cellSize * (1 - 2 * padding); |
| 107 | + const chartHeight = cellSize * (1 - 2 * padding); |
| 108 | + const chartX = x - chartWidth / 2; |
| 109 | + const chartY = y - chartHeight / 2; |
107 | 110 |
|
108 | | - if (carbonSaved == null || isNaN(carbonSaved)) return; |
| 111 | + // Calculate x position for the hovered year |
| 112 | + const yearX = chartX + ((hoveredYear - minYear) / yearRange) * chartWidth; |
109 | 113 |
|
110 | | - if (!yearData[year]) { |
111 | | - yearData[year] = { total: 0, count: 0 }; |
| 114 | + // Draw vertical reference line |
| 115 | + ctx.strokeStyle = 'rgba(150, 150, 150, 0.7)'; |
| 116 | + ctx.lineWidth = 1.5; |
| 117 | + ctx.setLineDash([3, 3]); // Dashed line |
| 118 | + ctx.beginPath(); |
| 119 | + ctx.moveTo(yearX, chartY); |
| 120 | + ctx.lineTo(yearX, chartY + chartHeight); |
| 121 | + ctx.stroke(); |
| 122 | + ctx.setLineDash([]); // Reset dash |
| 123 | + |
| 124 | + // Draw a small circle at the intersection with the line (if data exists for this year) |
| 125 | + const yearValue = timeSeriesData.find(d => d.year === hoveredYear); |
| 126 | + if (yearValue) { |
| 127 | + const values = timeSeriesData.map(d => d.value); |
| 128 | + const minValue = Math.min(...values); |
| 129 | + const maxValue = Math.max(...values); |
| 130 | + const valueRange = maxValue - minValue || 1; |
| 131 | + |
| 132 | + const valueY = chartY + chartHeight - ((yearValue.value - minValue) / valueRange) * chartHeight; |
| 133 | + |
| 134 | + // Draw circle marker |
| 135 | + ctx.fillStyle = 'rgba(150, 150, 150, 0.9)'; |
| 136 | + ctx.beginPath(); |
| 137 | + ctx.arc(yearX, valueY, 3.5, 0, 2 * Math.PI); |
| 138 | + ctx.fill(); |
| 139 | + |
| 140 | + // Draw white outline for better visibility |
| 141 | + ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)'; |
| 142 | + ctx.lineWidth = 1; |
| 143 | + ctx.stroke(); |
112 | 144 | } |
113 | | - yearData[year].total += carbonSaved; |
114 | | - yearData[year].count += 1; |
115 | | - }); |
116 | | - |
117 | | - // Convert to sorted array |
118 | | - const sortedYears = Object.keys(yearData) |
119 | | - .map(y => parseInt(y)) |
120 | | - .sort((a, b) => a - b); |
121 | | - |
122 | | - const values = sortedYears.map(year => yearData[year].total); |
123 | | - const maxValue = Math.max(...values, 1); |
124 | | - |
125 | | - // Draw bar chart |
126 | | - GlyphUtilities.drawBarGlyph( |
127 | | - ctx, |
128 | | - x, |
129 | | - y, |
130 | | - values, |
131 | | - maxValue, |
132 | | - cellSize, |
133 | | - ['#3498db', '#2ecc71', '#e74c3c', '#f39c12', '#9b59b6'] |
134 | | - ); |
| 145 | + |
| 146 | + // Draw year label at the bottom of the chart |
| 147 | + ctx.fillStyle = 'rgba(150, 150, 150, 0.9)'; |
| 148 | + ctx.font = 'bold 9px Arial'; |
| 149 | + ctx.textAlign = 'center'; |
| 150 | + ctx.textBaseline = 'top'; |
| 151 | + ctx.fillText(hoveredYear.toString(), yearX, chartY + chartHeight + 2); |
| 152 | + } |
135 | 153 | } |
136 | 154 |
|
137 | 155 | map.on('load', async () => { |
|
151 | 169 |
|
152 | 170 | console.log(`Using ${validData.length} valid data points`); |
153 | 171 |
|
154 | | - let useTimeSeries = true; |
155 | | - |
156 | | - const gridLayer = new ScreenGridLayerGL({ |
| 172 | + gridLayer = new ScreenGridLayerGL({ |
157 | 173 | id: 'cambridge-timeseries', |
158 | 174 | data: validData, |
159 | 175 | getPosition: (d) => [d.lon, d.lat], |
|
162 | 178 | colorScale: (v) => [255 * v, 200 * (1 - v), 50, 180], |
163 | 179 | enableGlyphs: true, |
164 | 180 | glyphSize: 0.9, |
165 | | - onDrawCell: useTimeSeries ? drawTimeSeriesGlyph : drawTimeSeriesBarGlyph, |
| 181 | + onDrawCell: drawTimeSeriesGlyph, |
166 | 182 | onAggregate: (grid) => { |
167 | 183 | console.log('Grid aggregated:', { |
168 | 184 | cols: grid.cols, |
|
171 | 187 | maxValue: Math.max(...grid.grid) |
172 | 188 | }); |
173 | 189 | }, |
174 | | - onHover: ({ cell }) => { |
175 | | - if (cell.cellData && cell.cellData.length > 0) { |
176 | | - // Group by year for display |
177 | | - const yearGroups = {}; |
178 | | - cell.cellData.forEach(item => { |
179 | | - const year = item.data.year; |
180 | | - if (!yearGroups[year]) { |
181 | | - yearGroups[year] = { |
182 | | - total: 0, |
183 | | - count: 0 |
184 | | - }; |
185 | | - } |
186 | | - if (item.data.ashp_carbonsaved != null) { |
187 | | - yearGroups[year].total += item.data.ashp_carbonsaved; |
188 | | - yearGroups[year].count += 1; |
189 | | - } |
190 | | - }); |
| 190 | + onHover: ({ cell, event }) => { |
| 191 | + // Clear reference line if hovering over cell without data |
| 192 | + if (!cell || !cell.cellData || cell.cellData.length === 0) { |
| 193 | + if (hoveredYear !== null) { |
| 194 | + hoveredYear = null; |
| 195 | + gridLayer.render(); |
| 196 | + } |
| 197 | + return; |
| 198 | + } |
| 199 | + |
| 200 | + // Group by year for display |
| 201 | + const yearGroups = {}; |
| 202 | + cell.cellData.forEach(item => { |
| 203 | + const year = item.data.year; |
| 204 | + if (!yearGroups[year]) { |
| 205 | + yearGroups[year] = { |
| 206 | + total: 0, |
| 207 | + count: 0 |
| 208 | + }; |
| 209 | + } |
| 210 | + if (item.data.ashp_carbonsaved != null) { |
| 211 | + yearGroups[year].total += item.data.ashp_carbonsaved; |
| 212 | + yearGroups[year].count += 1; |
| 213 | + } |
| 214 | + }); |
| 215 | + |
| 216 | + // Calculate which year the mouse is over based on x position within cell |
| 217 | + const cellSize = gridLayer.config.cellSizePixels; |
| 218 | + const mouseX = event.point.x; |
| 219 | + |
| 220 | + // Calculate relative position within the cell (0 to 1) |
| 221 | + // cell.x is the left edge of the cell in canvas coordinates |
| 222 | + const relativeX = (mouseX - cell.x) / cellSize; |
| 223 | + |
| 224 | + // Get year range for this cell |
| 225 | + const years = Object.keys(yearGroups).map(y => parseInt(y)).sort((a, b) => a - b); |
| 226 | + if (years.length > 0) { |
| 227 | + const minYear = Math.min(...years); |
| 228 | + const maxYear = Math.max(...years); |
| 229 | + const yearRange = maxYear - minYear || 1; |
191 | 230 |
|
192 | | - const yearSummary = Object.keys(yearGroups) |
193 | | - .sort() |
194 | | - .map(year => `${year}: ${yearGroups[year].total.toFixed(1)} kg CO₂`) |
195 | | - .join('\n'); |
| 231 | + // Calculate year based on mouse x position within the chart area |
| 232 | + // Account for padding: chart starts at padding, ends at (1 - padding) |
| 233 | + const padding = 0.15; |
| 234 | + const chartStart = padding; |
| 235 | + const chartEnd = 1 - padding; |
| 236 | + const chartWidth = chartEnd - chartStart; |
196 | 237 |
|
197 | | - console.log(`Cell at [${cell.col}, ${cell.row}]:\n${yearSummary}`); |
| 238 | + // Normalize relativeX to chart area |
| 239 | + const normalizedX = (relativeX - chartStart) / chartWidth; |
| 240 | + const clampedX = Math.max(0, Math.min(1, normalizedX)); |
| 241 | + |
| 242 | + const calculatedYear = Math.round(minYear + clampedX * yearRange); |
| 243 | + |
| 244 | + // Only update if year is valid and in range |
| 245 | + if (calculatedYear >= minYear && calculatedYear <= maxYear) { |
| 246 | + hoveredYear = calculatedYear; |
| 247 | + // Trigger re-render to show reference lines |
| 248 | + gridLayer.render(); |
| 249 | + } |
| 250 | + } else { |
| 251 | + // No valid years found, clear reference line |
| 252 | + if (hoveredYear !== null) { |
| 253 | + hoveredYear = null; |
| 254 | + gridLayer.render(); |
| 255 | + } |
| 256 | + } |
| 257 | + |
| 258 | + const yearSummary = Object.keys(yearGroups) |
| 259 | + .sort() |
| 260 | + .map(year => `${year}: ${yearGroups[year].total.toFixed(1)} kg CO₂`) |
| 261 | + .join('\n'); |
| 262 | + |
| 263 | + // console.log(`Cell at [${cell.col}, ${cell.row}]:\n${yearSummary}`); |
| 264 | + if (hoveredYear !== null) { |
| 265 | + console.log(`Hovered year: ${hoveredYear}`); |
198 | 266 | } |
199 | 267 | }, |
200 | 268 | onClick: ({ cell }) => { |
|
223 | 291 |
|
224 | 292 | map.addLayer(gridLayer); |
225 | 293 |
|
226 | | - // Add controls |
227 | | - const controls = document.createElement('div'); |
228 | | - controls.style.cssText = ` |
229 | | - position: absolute; |
230 | | - top: 10px; |
231 | | - right: 10px; |
232 | | - background: white; |
233 | | - padding: 15px; |
234 | | - border-radius: 8px; |
235 | | - box-shadow: 0 2px 10px rgba(0,0,0,0.2); |
236 | | - z-index: 1000; |
237 | | - font-family: Arial, sans-serif; |
238 | | - font-size: 14px; |
239 | | - `; |
240 | | - |
241 | | - controls.innerHTML = ` |
242 | | - <h3 style="margin-top: 0; margin-bottom: 10px;">Visualization Options</h3> |
243 | | - <button id="toggleGlyph" style="padding: 8px 12px; margin-bottom: 10px; cursor: pointer;"> |
244 | | - Switch to Bar Chart |
245 | | - </button> |
246 | | - <div style="font-size: 12px; color: #666;"> |
247 | | - Showing: <span id="currentMode">Time Series Line</span> |
248 | | - </div> |
249 | | - `; |
250 | | - |
251 | | - document.body.appendChild(controls); |
252 | | - |
253 | | - const toggleBtn = document.getElementById('toggleGlyph'); |
254 | | - const modeSpan = document.getElementById('currentMode'); |
255 | | - |
256 | | - toggleBtn.onclick = () => { |
257 | | - useTimeSeries = !useTimeSeries; |
258 | | - gridLayer.setConfig({ |
259 | | - onDrawCell: useTimeSeries ? drawTimeSeriesGlyph : drawTimeSeriesBarGlyph |
260 | | - }); |
261 | | - toggleBtn.textContent = useTimeSeries ? 'Switch to Bar Chart' : 'Switch to Line Chart'; |
262 | | - modeSpan.textContent = useTimeSeries ? 'Time Series Line' : 'Time Series Bar'; |
263 | | - }; |
| 294 | + // Clear hovered year when mouse leaves the map |
| 295 | + map.on('mouseout', () => { |
| 296 | + hoveredYear = null; |
| 297 | + gridLayer.render(); |
| 298 | + }); |
264 | 299 | }); |
265 | 300 | </script> |
266 | 301 | <style> |
|
0 commit comments