PageSourceSearch

https://haiphen.io/components/geo/geo.js?v=1

js haiphen.io collected 2026-10-01 12:02:20 UTC 10,420 bytes, 275 lines download raw bytes

1/**
2 * Geographic Infrastructure Concentration component.
3 *
4 * Injects HHI badges into the Featured signal-feed rows and provides
5 * an expandable Leaflet mini-map showing infrastructure distribution.
6 * Leaflet is lazy-loaded from CDN only when the map is first opened.
7 */
8(function () {
9  "use strict";
10
11  var API_URL = "https://api.haiphen.io/v1/prospect/geo/concentration";
12  var LEAFLET_JS  = "https://unpkg.com/[email protected]/dist/leaflet.js";
13  var LEAFLET_CSS = "https://unpkg.com/[email protected]/dist/leaflet.css";
14  var TILE_URL    = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
15  var TILE_ATTR   = '&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a>';
16
17  var COLORS = { high: "#dc2626", medium: "#f59e0b", low: "#10b981" };
18
19  var _data = null;       // API response companies array
20  var _leafletReady = false;
21  var _mapInstance = null;
22  var _panelEl = null;
23  var _rootEl = null;
24
25  // ── Helpers ──────────────────────────────────────────────
26
27  function esc(s) { var d = document.createElement("div"); d.textContent = s; return d.innerHTML; }
28
29  function fetchGeo(cb) {
30    var ctrl = new AbortController();
31    var timer = setTimeout(function () { ctrl.abort(); }, 10000);
32    fetch(API_URL, { credentials: "include", signal: ctrl.signal })
33      .then(function (r) { clearTimeout(timer); return r.ok ? r.json() : Promise.reject(r.status); })
34      .then(function (d) { cb(null, d); })
35      .catch(function (e) { clearTimeout(timer); cb(e); });
36  }
37
38  function loadLeaflet(cb) {
39    if (_leafletReady) return cb();
40    // CSS
41    var link = document.createElement("link");
42    link.rel = "stylesheet"; link.href = LEAFLET_CSS;
43    document.head.appendChild(link);
44    // JS
45    var script = document.createElement("script");
46    script.src = LEAFLET_JS;
47    script.onload = function () { _leafletReady = true; cb(); };
48    script.onerror = function () { cb(new Error("Leaflet load failed")); };
49    document.head.appendChild(script);
50  }
51
52  // ── Badge injection ─────────────────────────────────────
53
54  function injectBadges(rootEl) {
55    if (!_data || !_data.length) return;
56
57    // Build lookup: lowercase company name → company data
58    var lookup = {};
59    for (var i = 0; i < _data.length; i++) {
60      lookup[_data[i].group_key.toLowerCase()] = _data[i];
61    }
62
63    var cells = rootEl.querySelectorAll(".hp-feed__cell--company");
64    for (var c = 0; c < cells.length; c++) {
65      var cell = cells[c];
66      // Skip if badge already injected
67      if (cell.querySelector(".hp-geo-badge")) continue;
68
69      // Extract company name from cell text (first line before <br>)
70      var name = (cell.childNodes[0] && cell.childNodes[0].textContent || "").trim().toLowerCase();
71      var match = lookup[name];
72      if (!match) continue;
73
74      var badge = document.createElement("span");
75      badge.className = "hp-geo-badge hp-geo-badge--" + match.concentration;
76      badge.textContent = "HHI " + match.hhi.toFixed(2);
77      badge.title = "Infrastructure concentration: " + match.concentration
78        + " (" + match.total_infrastructure + " endpoints in "
79        + match.regions.length + " regions)";
80      badge.setAttribute("data-geo-company", match.group_key);
81      badge.addEventListener("click", onBadgeClick);
82      cell.appendChild(badge);
83    }
84  }
85
86  // ── Map panel ───────────────────────────────────────────
87
88  function ensurePanel(rootEl) {
89    if (_panelEl) return _panelEl;
90    // Fetch template
91    fetch("components/geo/geo.html", { cache: "no-store" })
92      .then(function (r) { return r.text(); })
93      .then(function (html) {
94        var wrapper = document.createElement("div");
95        wrapper.innerHTML = html;
96        _panelEl = wrapper.firstElementChild;
97
98        // Insert after the signal feed table
99        var feed = rootEl.querySelector("#hp-signal-feed");
100        if (feed && feed.parentNode) {
101          feed.parentNode.insertBefore(_panelEl, feed.nextSibling);
102        } else {
103          rootEl.appendChild(_panelEl);
104        }
105
106        // Close button
107        var closeBtn = _panelEl.querySelector("[data-geo-close]");
108        if (closeBtn) {
109          closeBtn.addEventListener("click", function () {
110            _panelEl.classList.remove("is-open");
111          });
112        }
113      });
114    return null; // async — caller should retry
115  }
116
117  function onBadgeClick(e) {
118    e.stopPropagation();
119    var companyName = e.currentTarget.getAttribute("data-geo-company");
120    if (!companyName) return;
121
122    var company = null;
123    for (var i = 0; i < _data.length; i++) {
124      if (_data[i].group_key === companyName) { company = _data[i]; break; }
125    }
126    if (!company || !company.regions.length) return;
127
128    showMap(company);
129  }
130
131  function showMap(company) {
132    if (!_panelEl) {
133      // Panel not loaded yet — wait and retry
134      setTimeout(function () { showMap(company); }, 200);
135      return;
136    }
137
138    _panelEl.classList.add("is-open");
139
140    // Update title
141    var title = _panelEl.querySelector(".hp-geo__title");
142    if (title) title.textContent = company.group_key + " — Infrastru
142cture Concentration";
143
144    // Update legend
145    var legend = _panelEl.querySelector("[data-geo-legend]");
146    if (legend) {
147      legend.textContent = "HHI: " + company.hhi.toFixed(3) + " (" + company.concentration
148        + ") — " + company.total_infrastructure + " endpoints across "
149        + company.regions.length + " regions";
150    }
151
152    // Load Leaflet and render
153    loadLeaflet(function (err) {
154      if (err) { console.warn("[Geo] Leaflet load error:", err); return; }
155      renderMap(company);
156    });
157  }
158
159  function renderMap(company) {
160    var mapEl = _panelEl.querySelector("[data-geo-map]");
161    if (!mapEl) return;
162
163    // Destroy previous instance
164    if (_mapInstance) { _mapInstance.remove(); _mapInstance = null; }
165
166    _mapInstance = L.map(mapEl, { scrollWheelZoom: false }).setView([20, 0], 2);
167    L.tileLayer(TILE_URL, { attribution: TILE_ATTR, maxZoom: 18 }).addTo(_mapInstance);
168
169    var bounds = [];
170    for (var i = 0; i < company.regions.length; i++) {
171      var r = company.regions[i];
172      if (r.lat == null || r.lng == null) continue;
173
174      var radius = Math.max(6, Math.min(30, Math.sqrt(r.count) * 8));
175      var color = COLORS[company.concentration] || COLORS.low;
176
177      L.circleMarker([r.lat, r.lng], {
178        radius: radius,
179        fillColor: color,
180        color: "#fff",
181        weight: 1,
182        fillOpacity: 0.7,
183      }).addTo(_mapInstance)
184        .bindPopup(
185          "<strong>" + esc(r.city || "Unknown") + ", " + esc(r.country || "??") + "</strong><br>"
186          + r.count + " endpoints (" + (r.share * 100).toFixed(1) + "%)"
187        );
188
189      bounds.push([r.lat, r.lng]);
190    }
191
192    if (bounds.length > 1) {
193      _mapInstance.fitBounds(bounds, { padding: [30, 30] });
194    } else if (bounds.length === 1) {
195      _mapInstance.setView(bounds[0], 6);
196    }
197
198    // Force Leaflet to recalculate after panel opens
199    setTimeout(function () { _mapInstance.invalidateSize(); }, 100);
200  }
201
202  // ── Summary bar ──────────────────────────────────────────
203
204  function injectSummaryBar(rootEl) {
205    if (!_data || !_data.length) return;
206    // Don't inject twice
207    if (rootEl.querySelector(".hp-geo-summary")) return;
208
209    var bar = document.createElement("div");
210    bar.className = "hp-geo-summary";
211    bar.innerHTML = '<span class="hp-geo-summary__icon" title="Geographic infrastructure concentration">&#x1f30d;</span> '
212      + '<span class="hp-geo-summary__text">
212Infrastructure concentration assessed for <strong>'
213      + _data.length + '</strong> compan' + (_data.length !== 1 ? 'ies' : 'y')
214      + ' &mdash; click <span class="hp-geo-badge hp-geo-badge--medium" style="font-size:10px;cursor:default">HHI</span> badges to view maps</span>';
215    bar.style.cssText = "background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;padding:8px 14px;font-size:12px;color:#166534;margin-bottom:12px;display:flex;align-items:center;gap:8px";
216
217    // Insert before the feed table
218    var feed = rootEl.querySelector("#hp-signal-feed");
219    if (feed) {
220      feed.parentNode.insertBefore(bar, feed);
221    }
222  }
223
224  function addColumnHint(rootEl) {
225    var th = rootEl.querySelector(".hp-feed__table th.hp-feed__cell--company, .hp-feed__table th:nth-child(2)");
226    if (!th || th.querySelector(".hp-geo-hint")) return;
227    var hint = document.createElement("span");
228    hint.className = "hp-geo-hint";
229    hint.textContent = " \u24D8";
230    hint.title = "Some companies have HHI infrastructure concentration badges — click them to view geographic distribution maps";
231    hint.style.cssText = "color:#5A9BD4;cursor:help;font-size:11px";
232    th.appendChild(hint);
233  }
234
235  // ── Init ────────────────────────────────────────────────
236
237  function init(rootEl) {
238    _rootEl = rootEl;
239
240    fetchGeo(function (err, data) {
241      if (err || !data || !data.companies || !data.companies.length) {
242        if (err) console.warn("[Geo] API error:", err);
243        return;
244      }
245      _data = data.companies;
246
247      // Pre-load the panel template
248      ensurePanel(rootEl);
249
250      // Show summary bar and column hint
251      injectSummaryBar(rootEl);
252      addColumnHint(rootEl);
253
254      // Inject badges into existing rows
255      injectBadges(rootEl);
256
257      // Re-inject after feed filter/sort changes (MutationObserver on tbody)
258      var tbody = rootEl.querySelector(".hp-feed__table tbody");
259      if (tbody) {
260        new MutationObserver(function () { injectBadges(rootEl); })
261          .observe(tbody, { childList: true });
262      }
263    });
264  }
265
266  function getCompanyNames() {
267    if (!_data) return [];
268    return _data.map(function (c) { return c.group_key; });
269  }
270
271  // ── Export ──────────────────────────────────────────────
272
273  window.HAIPHEN = window.HAIPHEN || {};
274  window.HAIPHEN.Geo = { init: init, getCompanyNames: getCompanyNames };
275})();

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.