-
◉ 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: '',