PageSourceSearch

https://littlebirdelectronics.com.au/assets/controllers/traffic_map_controller-8286d8d6.js

js littlebirdelectronics.com.au collected 2026-09-25 14:11:57 UTC 3,096 bytes, 95 lines download raw bytes

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.