feat(maps): Phase 2 — markets hub country bubble map
Add Leaflet map to /markets with country-level bubbles sized by total_venues and colored by avg_market_score. Click navigates to country overview page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
<meta name="description" content="{{ t.markets_page_description }}">
|
<meta name="description" content="{{ t.markets_page_description }}">
|
||||||
<meta property="og:title" content="{{ t.markets_page_og_title }} - {{ config.APP_NAME }}">
|
<meta property="og:title" content="{{ t.markets_page_og_title }} - {{ config.APP_NAME }}">
|
||||||
<meta property="og:description" content="{{ t.markets_page_og_description }}">
|
<meta property="og:description" content="{{ t.markets_page_og_description }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/leaflet/leaflet.min.css') }}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -15,6 +16,8 @@
|
|||||||
<p class="text-slate">{{ t.mkt_subheading }}</p>
|
<p class="text-slate">{{ t.mkt_subheading }}</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<div id="markets-map" style="height:420px; border-radius:12px;" class="mb-6"></div>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<div class="card mb-8">
|
<div class="card mb-8">
|
||||||
<div style="display: grid; grid-template-columns: 1fr auto auto; gap: 1rem; align-items: end;">
|
<div style="display: grid; grid-template-columns: 1fr auto auto; gap: 1rem; align-items: end;">
|
||||||
@@ -62,3 +65,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='vendor/leaflet/leaflet.min.js') }}"></script>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var map = L.map('markets-map', {scrollWheelZoom: false}).setView([48.5, 10], 4);
|
||||||
|
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
||||||
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> © <a href="https://carto.com/">CARTO</a>',
|
||||||
|
maxZoom: 18
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
function scoreColor(score) {
|
||||||
|
if (score >= 60) return '#16A34A';
|
||||||
|
if (score >= 30) return '#D97706';
|
||||||
|
return '#DC2626';
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/api/markets/countries.json')
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (!data.length) return;
|
||||||
|
var maxV = Math.max.apply(null, data.map(function(d) { return d.total_venues; }));
|
||||||
|
var lang = document.documentElement.lang || 'en';
|
||||||
|
data.forEach(function(c) {
|
||||||
|
if (!c.lat || !c.lon) return;
|
||||||
|
var radius = 6 + 22 * Math.sqrt(c.total_venues / maxV);
|
||||||
|
var color = scoreColor(c.avg_market_score);
|
||||||
|
L.circleMarker([c.lat, c.lon], {
|
||||||
|
radius: radius,
|
||||||
|
fillColor: color, color: color,
|
||||||
|
fillOpacity: 0.6, opacity: 0.9, weight: 1
|
||||||
|
})
|
||||||
|
.bindTooltip(c.country_name_en + '<br>' + c.total_venues + ' venues in ' + c.city_count + ' cities', {className: 'map-tooltip'})
|
||||||
|
.on('click', function() { window.location = '/' + lang + '/markets/' + c.country_slug; })
|
||||||
|
.addTo(map);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user