1import { Controller } from "@hotwired/stimulus" 2 3// Choropleth world map of recent storefront visitors by country, for the admin 4// Visitors page. Highcharts Maps + the world topology are loaded lazily from the 5// Highcharts CDN (we don't bundle ~300KB of mapping code) and shared across 6// Turbo navigations via module-level promises. 7// 8// data-traffic-map-data-value: [{ code: "AU", name: "Australia", value: 12 }, ...] 9 10const HIGHMAPS_SRC = "https://code.highcharts.com/maps/highmaps.js" 11const WORLD_TOPO_SRC = "https://code.highcharts.com/mapdata/custom/world.topo.json" 12 13export default class extends Controller { 14 static targets = ["chart"] 15 static values = { data: Array } 16 17 connect() { 18 Promise.all([loadHighmaps(), loadWorldTopology()]) 19 .then(([Highcharts, topology]) => this.draw(Highcharts, topology)) 20 .catch((error) => console.error("[traffic-map]", error)) 21 } 22 23 disconnect() { 24 this.chart?.destroy() 25 this.chart = null 26 } 27 28 draw(Highcharts, topology) { 29 if (this.chart || !this.hasChartTarget) return 30 31 const dark = document.documentElement.classList.contains("dark") 32 const series = this.dataValue.map((row) => ({ 33 "hc-key": String(row.code || "").toLowerCase(), 34 name: row.name, 35 value: row.value 36 })) 37 38 this.chart = Highcharts.mapChart(this.chartTarget, { 39 chart: { map: topology, backgroundColor: "transparent", spacing: [6, 0, 6, 0] }, 40 title: { text: null }, 41 credits: { enabled: false }, 42 legend: { enabled: false }, 43 mapNavigation: { 44 enabled: true, 45 enableMouseWheelZoom: false, 46 buttonOptions: { verticalAlign: "bottom" } 47 }, 48 colorAxis: { 49 min: 0, 50 minColor: dark ? "#312e81" : "#e0e7ff", 51 maxColor: "#4f46e5" 52 }, 53 tooltip: { 54 useHTML: true, 55 formatter() { 56 const value = this.point.value 57 if (value == null) return false 58 return `<b>${this.point.name}</b>: ${value} visitor${value === 1 ? "" : "s"}` 59 } 60 }, 61 series: [{ 62 data: series, 63 joinBy: "hc-key", 64 name: "Visitors", 65 nullColor: dark ? "#1f2937" : "#f1f5f9", 66 borderColor: dark ? "#374151" : "#cbd5e1", 67 borderWidth: 0.3, 68 states: { hover: { borderColor: "#4f46e5", borderWidth: 1 } } 69 }] 70 }) 71 } 72} 73 74let highmapsPromise 75function loadHighmaps() { 76 if (window.Highcharts?.mapChart) return Promise.resolve(window.Highcharts) 77 highmapsPromise ||= new Promise((resolve, reject) => { 78 const script = document.createElement("script") 79 script.src = HIGHMAPS_SRC 80 script.async = true 81 script.onload = () => resolve(window.Highcharts) 82 script.onerror = () => reject(new Error("Failed to load Highcharts Maps")) 83 document.head.appendChild(script) 84 }) 85 return highmapsPromise 86} 87 88let topologyPromise 89function loadWorldTopology() { 90 topologyPromise ||= fetch(WORLD_TOPO_SRC).then((response) => { 91 if (!response.ok) throw new Error(`Failed to load world map data (${response.status})`) 92 return response.json() 93 }) 94 return topologyPromise 95}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.