diff --git a/web/src/padelnomics/content/templates/markets.html b/web/src/padelnomics/content/templates/markets.html index b62a09b..cf87f81 100644 --- a/web/src/padelnomics/content/templates/markets.html +++ b/web/src/padelnomics/content/templates/markets.html @@ -20,10 +20,6 @@
- - inner = Market Score -   ring = Opportunity Score - ≥80 ≥60 @@ -98,12 +94,6 @@ maxZoom: 18 }).addTo(map); - function dot(hex, filled) { - return filled - ? '' - : ''; - } - var data = {{ map_countries | tojson }}; if (data.length) { var maxV = Math.max.apply(null, data.map(function(d) { return d.total_venues; })); @@ -111,13 +101,13 @@ data.forEach(function(c) { if (!c.lat || !c.lon) return; var size = 12 + 44 * Math.sqrt(c.total_venues / maxV); - var coreHex = sc(c.avg_market_score); - var ringHex = sc(c.avg_opportunity_score || 0); + var score = c.avg_opportunity_score || 0; + var hex = sc(score); var tip = '' + c.country_name_en + '
' - + dot(coreHex, true) + 'Market Score: ' + c.avg_market_score + '/100
' - + dot(ringHex, false) + 'Opportunity Score: ' + (c.avg_opportunity_score || 0) + '/100
' + + '' + + 'Padelnomics Score: ' + score + '/100
' + '' + c.total_venues + ' venues · ' + c.city_count + ' cities'; - L.marker([c.lat, c.lon], { icon: PNMarkers.makeIcon({ size: size, coreColor: coreHex, ringColor: ringHex }) }) + L.marker([c.lat, c.lon], { icon: PNMarkers.makeIcon({ size: size, color: hex }) }) .bindTooltip(tip, { className: 'map-tooltip', direction: 'top', offset: [0, -Math.round(size / 2)] }) .on('click', function() { window.location = '/' + lang + '/markets/' + c.country_slug; }) .addTo(map); diff --git a/web/src/padelnomics/public/templates/opportunity_map.html b/web/src/padelnomics/public/templates/opportunity_map.html index 0fbe294..b31d0c1 100644 --- a/web/src/padelnomics/public/templates/opportunity_map.html +++ b/web/src/padelnomics/public/templates/opportunity_map.html @@ -39,7 +39,6 @@
- inner = Opportunity Score   ring = Market Score ≥80 ≥60 @@ -80,12 +79,6 @@ : (p || ''); } - function dot(hex, filled) { - return filled - ? '' - : ''; - } - function renderMap() { oppLayer.clearLayers(); refLayer.clearLayers(); @@ -109,20 +102,19 @@ oppData.forEach(function(loc) { if (!loc.lat || !loc.lon) return; var size = 8 + 40 * Math.sqrt((loc.population || 1) / maxPop); - var coreHex = sc(loc.opportunity_score); - var ringHex = sc(loc.market_score || 0); + var score = loc.opportunity_score; + var hex = sc(score); var dist = loc.nearest_padel_court_km != null ? loc.nearest_padel_court_km.toFixed(1) + ' km to nearest court' : 'No nearby courts'; var tip = '' + loc.location_name + '
' - + dot(coreHex, true) + 'Opportunity Score: ' + loc.opportunity_score + '/100
' - + dot(ringHex, false) + 'Market Score: ' + (loc.market_score || 0) + '/100
' + + '' + + 'Padelnomics Score: ' + score + '/100
' + '' + dist + ' · Pop. ' + fmtPop(loc.population) + ''; var icon = PNMarkers.makeIcon({ size: size, - coreColor: coreHex, - ringColor: ringHex, - pulse: loc.opportunity_score >= 75, + color: hex, + pulse: score >= 75, }); L.marker([loc.lat, loc.lon], { icon: icon }) .bindTooltip(tip, { className: 'map-tooltip', direction: 'top', offset: [0, -Math.round(size / 2)] }) diff --git a/web/src/padelnomics/static/css/input.css b/web/src/padelnomics/static/css/input.css index c09932d..d50c017 100644 --- a/web/src/padelnomics/static/css/input.css +++ b/web/src/padelnomics/static/css/input.css @@ -879,10 +879,9 @@ .leaflet-tooltip.map-tooltip::before { display: none; } .leaflet-tooltip.map-tooltip strong { color: white; } -/* ── Dual-ring map markers ── */ -/* Container: sets size, white border, shadow, hover */ +/* ── Single-color map markers ── */ +/* Circle: score-colored background, white border, shadow, hover */ .pn-marker { - position: relative; border-radius: 50%; border: 2.5px solid white; box-shadow: 0 2px 8px rgba(0,0,0,0.28); @@ -893,25 +892,6 @@ box-shadow: 0 3px 12px rgba(0,0,0,0.38); transform: scale(1.1); } -/* Outer ring: secondary score color */ -.pn-marker__ring { - position: absolute; - inset: 0; - border-radius: 50%; - opacity: 0.65; -} -/* Inner core: primary score color, inset 5px from container edges */ -.pn-marker__core { - position: absolute; - inset: 5px; - border-radius: 50%; - border: 1.5px solid rgba(255,255,255,0.5); -} -/* Compact fallback: marker < 18px, no ring — core fills container */ -.pn-marker--compact .pn-marker__core { - inset: 0; - border: none; -} /* User's city highlight — blue outer glow */ .pn-marker--highlight { @@ -919,29 +899,24 @@ box-shadow: 0 0 0 3px rgba(59,130,246,0.25), 0 2px 8px rgba(0,0,0,0.28); } -/* Non-article city markers: faded, dashed ring outline, no click */ +/* Non-article city markers: faded, no click */ .pn-marker--muted { opacity: 0.4; cursor: default; filter: saturate(0.6) brightness(1.1); } -.pn-marker--muted .pn-marker__ring { - background: transparent !important; - border: 1.5px dashed rgba(255,255,255,0.6); - opacity: 1; -} .pn-marker--muted:hover { transform: none; box-shadow: 0 2px 8px rgba(0,0,0,0.28); } -/* Pulse animation — opportunity map only, score >= 75 */ -.pn-marker--pulse .pn-marker__ring { +/* Pulse animation — high-score locations */ +.pn-marker--pulse { animation: marker-pulse 2.5s ease-in-out infinite; } @keyframes marker-pulse { - 0%, 100% { transform: scale(1); opacity: 0.65; } - 50% { transform: scale(1.35); opacity: 0.25; } + 0%, 100% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.2); opacity: 0.7; } } /* Small fixed venue dot */ diff --git a/web/src/padelnomics/static/js/article-maps.js b/web/src/padelnomics/static/js/article-maps.js index a767c70..01200c8 100644 --- a/web/src/padelnomics/static/js/article-maps.js +++ b/web/src/padelnomics/static/js/article-maps.js @@ -15,12 +15,12 @@ var TILES = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'; var TILES_ATTR = '© OSM © CARTO'; var sc = PNMarkers.scoreColor; + var T = window.__MAP_T || {}; - function tooltipDot(hex, filled) { - var style = filled - ? 'display:inline-block;width:8px;height:8px;border-radius:50%;background:' + hex + ';vertical-align:middle;margin-right:4px;' - : 'display:inline-block;width:8px;height:8px;border-radius:50%;border:2px solid ' + hex + ';vertical-align:middle;margin-right:4px;'; - return ''; + function fmtPop(p) { + return p >= 1000000 ? (p / 1000000).toFixed(1) + 'M' + : p >= 1000 ? Math.round(p / 1000) + 'K' + : (p || ''); } function initCountryMap(el) { @@ -38,26 +38,23 @@ if (!c.lat || !c.lon) return; var size = 10 + 36 * Math.sqrt((c.padel_venue_count || 1) / maxV); var hasArticle = c.has_article !== false; - var coreHex = sc(c.market_score); - var ringHex = sc(c.opportunity_score || 0); - var pop = c.population >= 1000000 - ? (c.population / 1000000).toFixed(1) + 'M' - : (c.population >= 1000 ? Math.round(c.population / 1000) + 'K' : (c.population || '')); + var score = c.opportunity_score || 0; + var hex = sc(score); + var pop = fmtPop(c.population); var tip = '' + c.city_name + '
' - + tooltipDot(coreHex, true) + 'Market Score: ' + Math.round(c.market_score) + '/100
' - + tooltipDot(ringHex, false) + 'Opportunity Score: ' + Math.round(c.opportunity_score || 0) + '/100
' + + '' + + '' + (T.score_label || 'Padelnomics Score') + ': ' + Math.round(score) + '/100
' + '' - + (c.padel_venue_count || 0) + ' venues' - + (pop ? ' · ' + pop : '') + ''; + + (c.padel_venue_count || 0) + ' ' + (T.venues || 'venues') + + (pop ? ' · ' + pop + ' ' + (T.pop || 'pop') : '') + '
'; if (hasArticle) { - tip += '
Click to explore →'; + tip += '
' + (T.click_explore || 'Click to explore →') + ''; } else { - tip += '
Coming soon'; + tip += '
' + (T.coming_soon || 'Coming soon') + ''; } var icon = PNMarkers.makeIcon({ size: size, - coreColor: coreHex, - ringColor: ringHex, + color: hex, muted: !hasArticle, }); var marker = L.marker([c.lat, c.lon], { icon: icon }) @@ -80,8 +77,7 @@ var hSize = 10 + 36 * Math.sqrt((match.padel_venue_count || 1) / maxV); var hIcon = PNMarkers.makeIcon({ size: hSize, - coreColor: sc(match.market_score), - ringColor: sc(match.opportunity_score || 0), + color: sc(match.opportunity_score || 0), highlight: true, }); L.marker([match.lat, match.lon], { icon: hIcon }).addTo(map); @@ -107,9 +103,9 @@ var outdoor = v.outdoor_court_count || 0; var total = v.court_count || (indoor + outdoor); var courtLine = total - ? total + ' court' + (total > 1 ? 's' : '') + ? total + ' ' + (T.courts || 'court' + (total > 1 ? 's' : '')) + (indoor || outdoor - ? ' (' + [indoor ? indoor + ' indoor' : '', outdoor ? outdoor + ' outdoor' : ''].filter(Boolean).join(', ') + ')' + ? ' (' + [indoor ? indoor + ' ' + (T.indoor || 'indoor') : '', outdoor ? outdoor + ' ' + (T.outdoor || 'outdoor') : ''].filter(Boolean).join(', ') + ')' : '') : ''; var tip = '' + v.name + '' + (courtLine ? '
' + courtLine : ''); diff --git a/web/src/padelnomics/static/js/map-markers.js b/web/src/padelnomics/static/js/map-markers.js index e0e3996..59ffbc8 100644 --- a/web/src/padelnomics/static/js/map-markers.js +++ b/web/src/padelnomics/static/js/map-markers.js @@ -1,18 +1,17 @@ /** - * Shared map marker utilities — dual-ring design with 5-tier color scale. + * Shared map marker utilities — single-color markers with 5-tier color scale. * * Exposes window.PNMarkers = { scoreColor, makeIcon } * * scoreColor(score) → hex color string (5 tiers, colorblind-safe luminance steps) - * makeIcon(opts) → L.divIcon with dual-ring HTML + * makeIcon(opts) → L.divIcon with single-color circle * * opts = { * size: number, // marker diameter in px - * coreColor: string, // hex for inner core (primary score) - * ringColor: string, // hex for outer ring (secondary score) - * muted: boolean, // dashed ring, no click affordance + * color: string, // hex color (from scoreColor) + * muted: boolean, // faded, no click affordance * highlight: boolean, // blue outer glow (user's geo city) - * pulse: boolean, // gentle ring pulse (high opportunity) + * pulse: boolean, // gentle pulse (high score) * } */ (function() { @@ -27,30 +26,15 @@ return '#DC2626'; // red — poor } - var COMPACT_THRESHOLD_PX = 18; - function makeIcon(opts) { var s = Math.round(opts.size); - var compact = s < COMPACT_THRESHOLD_PX; var cls = 'pn-marker'; - if (compact) cls += ' pn-marker--compact'; if (opts.muted) cls += ' pn-marker--muted'; if (opts.highlight) cls += ' pn-marker--highlight'; if (opts.pulse && !opts.muted) cls += ' pn-marker--pulse'; - var html; - if (compact || !opts.ringColor) { - // Single-layer fallback: core fills entire marker - html = '
' - + '
' - + '
'; - } else { - html = '
' - + '
' - + '
' - + '
'; - } + var html = '
'; return L.divIcon({ className: '',