| | <!DOCTYPE html> |
| | <html> |
| | <head> |
| | <meta charset="utf-8"> |
| | <title>DVF - Prix Immobiliers en France</title> |
| | <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no"> |
| | <link href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css" rel="stylesheet"> |
| | <script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script> |
| | <script src="https://unpkg.com/pmtiles@2.11.0/dist/index.js"></script> |
| | <style> |
| | body { margin: 0; padding: 0; } |
| | #map { position: absolute; top: 0; bottom: 0; width: 100%; } |
| | |
| | .map-overlay { |
| | position: absolute; |
| | background: rgba(255, 255, 255, 0.95); |
| | border-radius: 8px; |
| | padding: 15px; |
| | box-shadow: 0 2px 10px rgba(0,0,0,0.15); |
| | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
| | font-size: 13px; |
| | } |
| | |
| | #info { |
| | top: 10px; |
| | left: 10px; |
| | min-width: 200px; |
| | } |
| | |
| | #info h3 { |
| | margin: 0 0 10px 0; |
| | font-size: 16px; |
| | color: #333; |
| | } |
| | |
| | #info .stat { |
| | margin: 5px 0; |
| | color: #666; |
| | } |
| | |
| | #info .stat strong { |
| | color: #333; |
| | } |
| | |
| | #legend { |
| | bottom: 30px; |
| | left: 10px; |
| | padding: 10px 15px; |
| | } |
| | |
| | #legend h4 { |
| | margin: 0 0 10px 0; |
| | font-size: 14px; |
| | } |
| | |
| | .legend-scale { |
| | display: flex; |
| | height: 15px; |
| | border-radius: 3px; |
| | overflow: hidden; |
| | } |
| | |
| | .legend-scale div { |
| | flex: 1; |
| | } |
| | |
| | .legend-labels { |
| | display: flex; |
| | justify-content: space-between; |
| | margin-top: 5px; |
| | font-size: 11px; |
| | color: #666; |
| | } |
| | |
| | #controls { |
| | top: 10px; |
| | right: 10px; |
| | } |
| | |
| | #controls label { |
| | display: block; |
| | margin: 5px 0; |
| | cursor: pointer; |
| | } |
| | |
| | #controls select { |
| | width: 100%; |
| | padding: 5px; |
| | margin-top: 10px; |
| | border-radius: 4px; |
| | border: 1px solid #ccc; |
| | } |
| | |
| | #zoom-level { |
| | position: absolute; |
| | bottom: 30px; |
| | right: 10px; |
| | background: rgba(255,255,255,0.9); |
| | padding: 5px 10px; |
| | border-radius: 4px; |
| | font-family: monospace; |
| | font-size: 12px; |
| | } |
| | |
| | #top-cities { |
| | top: 80px; |
| | right: 10px; |
| | max-width: 220px; |
| | display: none; |
| | } |
| | |
| | #top-cities h4 { |
| | margin: 0 0 10px 0; |
| | font-size: 14px; |
| | } |
| | |
| | #top-cities ol { |
| | margin: 0; |
| | padding-left: 20px; |
| | } |
| | |
| | #top-cities li { |
| | margin: 4px 0; |
| | font-size: 12px; |
| | } |
| | |
| | #top-cities .city-name { |
| | font-weight: 500; |
| | } |
| | |
| | #top-cities .city-stats { |
| | color: #666; |
| | font-size: 11px; |
| | } |
| | </style> |
| | </head> |
| | <body> |
| | <div id="map"></div> |
| | |
| | <div class="map-overlay" id="info"> |
| | <h3>Prix Immobiliers</h3> |
| | <div id="hover-info"> |
| | <p style="color: #999;">Survolez une zone pour voir les prix</p> |
| | </div> |
| | </div> |
| | |
| | <div class="map-overlay" id="legend"> |
| | <h4>Prix médian (€/m²)</h4> |
| | <div class="legend-scale" id="legend-scale"></div> |
| | <div class="legend-labels" id="legend-labels"> |
| | <span>1 500</span> |
| | <span>2 500</span> |
| | <span>3 500</span> |
| | <span>5 000</span> |
| | <span>8 000+</span> |
| | </div> |
| | <div style="margin-top: 8px; display: flex; align-items: center; font-size: 11px; color: #666;"> |
| | <span style="display: inline-block; width: 16px; height: 12px; background: #999999; border-radius: 2px; margin-right: 6px;"></span> |
| | <span>Aucune transaction</span> |
| | </div> |
| | <div style="margin-top: 4px; display: flex; align-items: center; font-size: 11px; color: #666;"> |
| | <span style="display: inline-block; width: 16px; height: 12px; background: #cccccc; border-radius: 2px; margin-right: 6px;"></span> |
| | <span>< 5 transactions</span> |
| | </div> |
| | <div id="current-level" style="margin-top: 8px; font-size: 11px; color: #999;">Niveau: Pays</div> |
| | </div> |
| | |
| | <div class="map-overlay" id="controls"> |
| | <strong>Type de bien</strong> |
| | <select id="property-type"> |
| | <option value="all">Tous types</option> |
| | <option value="maison">Maisons</option> |
| | <option value="appartement">Appartements</option> |
| | </select> |
| | </div> |
| | |
| | <div id="zoom-level">Zoom: <span id="zoom-value">5</span></div> |
| | |
| | <div class="map-overlay" id="top-cities"> |
| | <h4>🏆 Top 10 Villes</h4> |
| | <div id="top-cities-list"></div> |
| | </div> |
| |
|
| | <script> |
| | console.log('Script starting...'); |
| | console.log('pmtiles defined?', typeof pmtiles !== 'undefined'); |
| | console.log('maplibregl defined?', typeof maplibregl !== 'undefined'); |
| | |
| | |
| | let pmtilesAvailable = false; |
| | try { |
| | if (typeof pmtiles !== 'undefined' && typeof maplibregl !== 'undefined') { |
| | const protocol = new pmtiles.Protocol(); |
| | maplibregl.addProtocol("pmtiles", protocol.tile); |
| | pmtilesAvailable = true; |
| | console.log('PMTiles protocol registered successfully'); |
| | } else { |
| | console.log('PMTiles or MapLibre library not loaded'); |
| | } |
| | } catch (e) { |
| | console.error('PMTiles registration error:', e); |
| | } |
| | |
| | |
| | const colors = ['#1a9850', '#a6d96a', '#ffdd00', '#f46d43', '#d73027']; |
| | |
| | |
| | const priceScales = { |
| | country: null, |
| | regions: null, |
| | departments: null, |
| | iris: null, |
| | communes: null, |
| | parcels: null |
| | }; |
| | |
| | |
| | const geoData = {}; |
| | |
| | |
| | function computePriceScale(features, priceField, level = 'default') { |
| | |
| | return [1500, 3125, 4750, 6375, 8000]; |
| | } |
| | |
| | |
| | const legendScale = document.getElementById('legend-scale'); |
| | colors.forEach(color => { |
| | const div = document.createElement('div'); |
| | div.style.backgroundColor = color; |
| | legendScale.appendChild(div); |
| | }); |
| | |
| | |
| | const map = new maplibregl.Map({ |
| | container: 'map', |
| | style: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json', |
| | center: [2.3, 46.5], |
| | zoom: 4, |
| | minZoom: 3, |
| | maxZoom: 18 |
| | }); |
| | |
| | |
| | let currentPropertyType = 'all'; |
| | let currentLevel = 'country'; |
| | let topCitiesData = []; |
| | |
| | function updateTopCitiesPanel() { |
| | const countField = getCountField(); |
| | const priceField = getPriceField(); |
| | |
| | |
| | const aggregated = {}; |
| | const bigCities = { |
| | 'Paris': { pattern: /^Paris \d+e?r? Arrondissement$/i, code: '75' }, |
| | 'Marseille': { pattern: /^Marseille \d+e?r? Arrondissement$/i, code: '13' }, |
| | 'Lyon': { pattern: /^Lyon \d+e?r? Arrondissement$/i, code: '69' } |
| | }; |
| | |
| | topCitiesData.forEach(f => { |
| | const name = f.properties.nom_commune || f.properties.nom_commune_geo || ''; |
| | const count = f.properties[countField] || 0; |
| | const price = f.properties[priceField]; |
| | |
| | if (count === 0) return; |
| | |
| | |
| | let aggregateName = null; |
| | for (const [city, info] of Object.entries(bigCities)) { |
| | if (info.pattern.test(name)) { |
| | aggregateName = city; |
| | break; |
| | } |
| | } |
| | |
| | if (aggregateName) { |
| | |
| | if (!aggregated[aggregateName]) { |
| | aggregated[aggregateName] = { name: aggregateName, count: 0, priceSum: 0, priceCount: 0 }; |
| | } |
| | aggregated[aggregateName].count += count; |
| | if (price) { |
| | aggregated[aggregateName].priceSum += price * count; |
| | aggregated[aggregateName].priceCount += count; |
| | } |
| | } else { |
| | |
| | aggregated[name] = { name, count, priceSum: price ? price * count : 0, priceCount: price ? count : 0 }; |
| | } |
| | }); |
| | |
| | |
| | const sorted = Object.values(aggregated) |
| | .map(c => ({ |
| | name: c.name, |
| | count: c.count, |
| | price: c.priceCount > 0 ? c.priceSum / c.priceCount : null |
| | })) |
| | .sort((a, b) => b.count - a.count) |
| | .slice(0, 10); |
| | |
| | const listHtml = sorted.map((c, i) => { |
| | const priceStr = c.price ? Math.round(c.price).toLocaleString('fr-FR') + ' €/m²' : 'N/A'; |
| | return `<div style="margin: 6px 0;"><span class="city-name">${i+1}. ${c.name}</span><br><span class="city-stats">${c.count.toLocaleString('fr-FR')} transactions • ${priceStr}</span></div>`; |
| | }).join(''); |
| | |
| | document.getElementById('top-cities-list').innerHTML = listHtml; |
| | } |
| | |
| | function showTopCities(show) { |
| | document.getElementById('top-cities').style.display = show ? 'block' : 'none'; |
| | } |
| | |
| | function getPriceField() { |
| | switch(currentPropertyType) { |
| | case 'maison': return 'prix_m2_maison_median'; |
| | case 'appartement': return 'prix_m2_appart_median'; |
| | default: return 'prix_m2_median'; |
| | } |
| | } |
| | |
| | function getCountField() { |
| | switch(currentPropertyType) { |
| | case 'maison': return 'nb_maisons'; |
| | case 'appartement': return 'nb_appartements'; |
| | default: return 'nb_transactions'; |
| | } |
| | } |
| | |
| | |
| | function formatPrice(price) { |
| | if (price === null || price === undefined) return 'N/A'; |
| | return Math.round(price).toLocaleString('fr-FR') + ' €/m²'; |
| | } |
| | |
| | function formatCount(count) { |
| | if (count === null || count === undefined) return 'N/A'; |
| | return count.toLocaleString('fr-FR'); |
| | } |
| | |
| | |
| | function updateInfo(properties, level) { |
| | const priceField = getPriceField(); |
| | const countField = getCountField(); |
| | |
| | let name = ''; |
| | if (level === 'country') { |
| | name = properties.name || 'France'; |
| | } else if (level === 'regions') { |
| | name = properties.nom_region || properties.nom_region_geo || 'Région'; |
| | } else if (level === 'departments') { |
| | name = properties.nom_departement || 'Département'; |
| | } else if (level === 'iris') { |
| | const irisName = properties.nom_iris || ''; |
| | const communeName = properties.nom_commune_iris || ''; |
| | const irisCode = properties.code_iris || ''; |
| | |
| | if (irisName === communeName && irisCode) { |
| | name = `IRIS ${irisCode} (${communeName})`; |
| | } else if (irisName) { |
| | name = communeName && irisName !== communeName ? `${irisName} (${communeName})` : irisName; |
| | } else { |
| | name = irisCode ? `IRIS ${irisCode}` : 'IRIS'; |
| | } |
| | } else if (level === 'communes') { |
| | name = properties.nom_commune || properties.nom_commune_geo || 'Commune'; |
| | } else if (level === 'parcels') { |
| | const parcelId = properties.id_parcelle_unique || properties.id || ''; |
| | const commune = properties.nom_commune || ''; |
| | name = commune ? `Parcelle ${parcelId} (${commune})` : `Parcelle ${parcelId}`; |
| | } |
| | |
| | const price = properties[priceField]; |
| | const priceMean = properties[priceField.replace('median', 'mean')]; |
| | const count = properties[countField]; |
| | const priceQ25 = properties[priceField.replace('median', 'q25')]; |
| | const priceQ75 = properties[priceField.replace('median', 'q75')]; |
| | |
| | const noData = (count || 0) === 0; |
| | const insufficientData = (count || 0) > 0 && (count || 0) < 5; |
| | |
| | let dataNote = ''; |
| | if (noData) { |
| | dataNote = '<div class="stat" style="color: #888; font-style: italic;">⛔ Aucune transaction</div>'; |
| | } else if (insufficientData) { |
| | dataNote = '<div class="stat" style="color: #999; font-style: italic;">⚠️ Données insuffisantes (<5 transactions)</div>'; |
| | } |
| | |
| | document.getElementById('hover-info').innerHTML = ` |
| | <div class="stat"><strong>${name}</strong></div> |
| | ${dataNote} |
| | <div class="stat">Prix médian: <strong>${noData ? 'N/A' : formatPrice(price)}</strong></div> |
| | <div class="stat">Prix moyen: ${noData ? 'N/A' : formatPrice(priceMean)}</div> |
| | <div class="stat">Fourchette: ${noData ? 'N/A' : formatPrice(priceQ25) + ' - ' + formatPrice(priceQ75)}</div> |
| | <div class="stat">Transactions: <strong>${formatCount(count)}</strong></div> |
| | `; |
| | } |
| | |
| | function resetInfo() { |
| | document.getElementById('hover-info').innerHTML = |
| | '<p style="color: #999;">Survolez une zone pour voir les prix</p>'; |
| | } |
| | |
| | |
| | |
| | function buildFillColor(level) { |
| | const priceField = getPriceField(); |
| | const countField = getCountField(); |
| | |
| | const defaultScale = [1500, 3125, 4750, 6375, 8000]; |
| | const scale = priceScales[level] || priceScales['communes'] || defaultScale; |
| | |
| | |
| | const stops = []; |
| | for (let i = 0; i < scale.length; i++) { |
| | stops.push(scale[i], colors[i]); |
| | } |
| | |
| | |
| | if (level === 'parcels') { |
| | return [ |
| | 'interpolate', |
| | ['linear'], |
| | ['coalesce', ['get', priceField], 0], |
| | ...stops |
| | ]; |
| | } |
| | |
| | |
| | return [ |
| | 'case', |
| | ['==', ['coalesce', ['get', countField], 0], 0], |
| | '#999999', |
| | ['<', ['coalesce', ['get', countField], 0], 5], |
| | '#cccccc', |
| | [ |
| | 'interpolate', |
| | ['linear'], |
| | ['coalesce', ['get', priceField], 0], |
| | ...stops |
| | ] |
| | ]; |
| | } |
| | |
| | |
| | function updateLegend(level) { |
| | const scale = priceScales[level] || [1500, 3125, 4750, 6375, 8000]; |
| | const labels = document.getElementById('legend-labels'); |
| | const levelNames = { |
| | country: 'Pays', |
| | regions: 'Régions', |
| | departments: 'Départements', |
| | communes: 'Communes', |
| | iris: 'IRIS (Quartiers)', |
| | parcels: 'Parcelles' |
| | }; |
| | |
| | |
| | const fmt = (n) => n >= 1000 ? `${(n/1000).toFixed(1)}k` : n.toString(); |
| | |
| | |
| | labels.innerHTML = ` |
| | <span>${fmt(scale[0])}</span> |
| | <span>${fmt(scale[1])}</span> |
| | <span>${fmt(scale[2])}</span> |
| | <span>${fmt(scale[3])}</span> |
| | <span>${fmt(scale[4])}</span> |
| | `; |
| | |
| | document.getElementById('current-level').textContent = `Niveau: ${levelNames[level]}`; |
| | } |
| | |
| | |
| | function updateScalesAndColors() { |
| | const priceField = getPriceField(); |
| | |
| | |
| | ['country', 'regions', 'departments', 'iris', 'communes'].forEach(level => { |
| | if (geoData[level]) { |
| | priceScales[level] = computePriceScale(geoData[level].features, priceField, level); |
| | } |
| | }); |
| | |
| | priceScales['parcels'] = [1500, 3125, 4750, 6375, 8000]; |
| | |
| | |
| | ['country', 'regions', 'departments', 'iris', 'communes'].forEach(level => { |
| | const layerId = `${level}-fill`; |
| | if (map.getLayer(layerId)) { |
| | map.setPaintProperty(layerId, 'fill-color', buildFillColor(level)); |
| | } |
| | }); |
| | |
| | |
| | if (map.getLayer('parcels-fill')) { |
| | map.setPaintProperty('parcels-fill', 'fill-color', buildFillColor('parcels')); |
| | } |
| | |
| | |
| | updateLegend(currentLevel); |
| | } |
| | |
| | |
| | function updateLayerColors() { |
| | updateScalesAndColors(); |
| | } |
| | |
| | |
| | function getLevelFromZoom(zoom) { |
| | if (zoom < 5) return 'country'; |
| | if (zoom < 7) return 'regions'; |
| | if (zoom < 9) return 'departments'; |
| | if (zoom < 11) return 'communes'; |
| | if (zoom < 13) return 'iris'; |
| | return 'parcels'; |
| | } |
| | |
| | map.on('load', async () => { |
| | |
| | const levels = ['country', 'regions', 'departments', 'iris', 'communes']; |
| | const priceField = getPriceField(); |
| | |
| | const fetchPromises = levels.map(level => |
| | fetch(`data/${level}.geojson`).then(r => r.json()) |
| | ); |
| | const results = await Promise.all(fetchPromises); |
| | |
| | levels.forEach((level, i) => { |
| | geoData[level] = results[i]; |
| | priceScales[level] = computePriceScale(geoData[level].features, priceField, level); |
| | console.log(`${level} scale:`, priceScales[level]); |
| | }); |
| | |
| | |
| | topCitiesData = geoData.communes.features; |
| | updateTopCitiesPanel(); |
| | showTopCities(true); |
| | |
| | |
| | map.addSource('country', { |
| | type: 'geojson', |
| | data: geoData.country |
| | }); |
| | |
| | map.addLayer({ |
| | id: 'country-fill', |
| | type: 'fill', |
| | source: 'country', |
| | paint: { |
| | 'fill-color': buildFillColor('country'), |
| | 'fill-opacity': 0.7 |
| | }, |
| | maxzoom: 5 |
| | }); |
| | |
| | map.addLayer({ |
| | id: 'country-line', |
| | type: 'line', |
| | source: 'country', |
| | paint: { |
| | 'line-color': '#333', |
| | 'line-width': 2 |
| | }, |
| | maxzoom: 5 |
| | }); |
| | |
| | |
| | map.addSource('regions', { |
| | type: 'geojson', |
| | data: geoData.regions |
| | }); |
| | |
| | map.addLayer({ |
| | id: 'regions-fill', |
| | type: 'fill', |
| | source: 'regions', |
| | paint: { |
| | 'fill-color': buildFillColor('regions'), |
| | 'fill-opacity': 0.7 |
| | }, |
| | minzoom: 5, |
| | maxzoom: 7 |
| | }); |
| | |
| | map.addLayer({ |
| | id: 'regions-line', |
| | type: 'line', |
| | source: 'regions', |
| | paint: { |
| | 'line-color': '#333', |
| | 'line-width': 1.5 |
| | }, |
| | minzoom: 5, |
| | maxzoom: 7 |
| | }); |
| | |
| | |
| | map.addSource('departments', { |
| | type: 'geojson', |
| | data: geoData.departments |
| | }); |
| | |
| | map.addLayer({ |
| | id: 'departments-fill', |
| | type: 'fill', |
| | source: 'departments', |
| | paint: { |
| | 'fill-color': buildFillColor('departments'), |
| | 'fill-opacity': 0.7 |
| | }, |
| | minzoom: 7, |
| | maxzoom: 9 |
| | }); |
| | |
| | map.addLayer({ |
| | id: 'departments-line', |
| | type: 'line', |
| | source: 'departments', |
| | paint: { |
| | 'line-color': '#333', |
| | 'line-width': 1 |
| | }, |
| | minzoom: 7, |
| | maxzoom: 9 |
| | }); |
| | |
| | |
| | map.addSource('iris', { |
| | type: 'geojson', |
| | data: geoData.iris |
| | }); |
| | |
| | map.addLayer({ |
| | id: 'iris-fill', |
| | type: 'fill', |
| | source: 'iris', |
| | paint: { |
| | 'fill-color': buildFillColor('iris'), |
| | 'fill-opacity': 0.7 |
| | }, |
| | minzoom: 11, |
| | maxzoom: 13 |
| | }); |
| | |
| | map.addLayer({ |
| | id: 'iris-line', |
| | type: 'line', |
| | source: 'iris', |
| | paint: { |
| | 'line-color': '#555', |
| | 'line-width': 0.5 |
| | }, |
| | minzoom: 11, |
| | maxzoom: 13 |
| | }); |
| | |
| | |
| | |
| | if (pmtilesAvailable) { |
| | try { |
| | map.addSource('parcels', { |
| | type: 'vector', |
| | url: 'pmtiles://data/parcels.pmtiles' |
| | }); |
| | |
| | map.addLayer({ |
| | id: 'parcels-fill', |
| | type: 'fill', |
| | source: 'parcels', |
| | 'source-layer': 'parcels', |
| | paint: { |
| | 'fill-color': buildFillColor('parcels'), |
| | 'fill-opacity': 0.8 |
| | }, |
| | minzoom: 13 |
| | }); |
| | |
| | map.addLayer({ |
| | id: 'parcels-line', |
| | type: 'line', |
| | source: 'parcels', |
| | 'source-layer': 'parcels', |
| | paint: { |
| | 'line-color': '#333', |
| | 'line-width': 0.3 |
| | }, |
| | minzoom: 13 |
| | }); |
| | |
| | console.log('Parcels PMTiles layer added'); |
| | } catch (e) { |
| | console.log('Parcels PMTiles not available:', e.message); |
| | } |
| | } else { |
| | console.log('PMTiles not available, skipping parcels layer'); |
| | } |
| | |
| | |
| | map.addSource('communes', { |
| | type: 'geojson', |
| | data: geoData.communes |
| | }); |
| | |
| | map.addLayer({ |
| | id: 'communes-fill', |
| | type: 'fill', |
| | source: 'communes', |
| | paint: { |
| | 'fill-color': buildFillColor('communes'), |
| | 'fill-opacity': 0.7 |
| | }, |
| | minzoom: 9, |
| | maxzoom: 11 |
| | }); |
| | |
| | map.addLayer({ |
| | id: 'communes-line', |
| | type: 'line', |
| | source: 'communes', |
| | paint: { |
| | 'line-color': '#666', |
| | 'line-width': 0.5 |
| | }, |
| | minzoom: 9, |
| | maxzoom: 11 |
| | }); |
| | |
| | |
| | ['country', 'regions', 'departments', 'iris', 'communes'].forEach(layer => { |
| | map.on('mousemove', `${layer}-fill`, (e) => { |
| | map.getCanvas().style.cursor = 'pointer'; |
| | if (e.features.length > 0) { |
| | updateInfo(e.features[0].properties, layer); |
| | } |
| | }); |
| | |
| | map.on('mouseleave', `${layer}-fill`, () => { |
| | map.getCanvas().style.cursor = ''; |
| | resetInfo(); |
| | }); |
| | }); |
| | |
| | |
| | if (map.getSource('parcels')) { |
| | map.on('mousemove', 'parcels-fill', (e) => { |
| | map.getCanvas().style.cursor = 'pointer'; |
| | if (e.features.length > 0) { |
| | updateInfo(e.features[0].properties, 'parcels'); |
| | } |
| | }); |
| | |
| | map.on('mouseleave', 'parcels-fill', () => { |
| | map.getCanvas().style.cursor = ''; |
| | resetInfo(); |
| | }); |
| | } |
| | |
| | |
| | updateLegend(getLevelFromZoom(map.getZoom())); |
| | }); |
| | |
| | |
| | map.on('zoom', () => { |
| | const zoom = map.getZoom(); |
| | document.getElementById('zoom-value').textContent = zoom.toFixed(1); |
| | |
| | const newLevel = getLevelFromZoom(zoom); |
| | if (newLevel !== currentLevel) { |
| | currentLevel = newLevel; |
| | updateLegend(currentLevel); |
| | } |
| | }); |
| | |
| | |
| | document.getElementById('property-type').addEventListener('change', (e) => { |
| | currentPropertyType = e.target.value; |
| | updateLayerColors(); |
| | updateTopCitiesPanel(); |
| | }); |
| | |
| | |
| | map.addControl(new maplibregl.NavigationControl()); |
| | </script> |
| | </body> |
| | </html> |
| |
|