|
| 1 | +""" |
| 2 | +Quant/HFT infrastructure map — build script. |
| 3 | +
|
| 4 | +Reads data/sites_seed.csv, enriches it (Google Maps deep links, tier labels), |
| 5 | +and exports a standalone kepler.gl HTML map suitable for embedding in a blog post. |
| 6 | +
|
| 7 | +Usage: |
| 8 | + python build_map.py [--csv data/sites_seed.csv] [--out quant_dc_map.html] |
| 9 | +""" |
| 10 | + |
| 11 | +from __future__ import annotations |
| 12 | + |
| 13 | +import argparse |
| 14 | +import re |
| 15 | +from pathlib import Path |
| 16 | + |
| 17 | +import pandas as pd |
| 18 | +from keplergl import KeplerGl |
| 19 | + |
| 20 | +# keplergl's bundled JS ships kepler.gl's demo Mapbox token, which GitHub |
| 21 | +# push protection rejects. We don't need Mapbox at all: use CARTO's free |
| 22 | +# Dark Matter basemap (no access token) and strip the bundled token from |
| 23 | +# the exported HTML. |
| 24 | +CARTO_DARK = { |
| 25 | + "id": "carto_dark", |
| 26 | + "label": "CARTO Dark Matter (no token)", |
| 27 | + "url": "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json", |
| 28 | + "icon": "", |
| 29 | + "custom": True, |
| 30 | + "accessToken": None, |
| 31 | +} |
| 32 | +MAPBOX_TOKEN_RE = re.compile(r"pk\.eyJ[0-9A-Za-z_-]+\.[0-9A-Za-z_-]+") |
| 33 | + |
| 34 | +# kepler's Jupyter wrapper only measures its container on window resize |
| 35 | +# events (initial render falls back to 800x400), so pin the container to the |
| 36 | +# viewport and fire a resize once the page loads. |
| 37 | +FULLSCREEN_SNIPPET = """<style> |
| 38 | + html, body { width: 100%; height: 100%; } |
| 39 | + .keplergl-widget-container { width: 100vw !important; height: 100vh !important; } |
| 40 | +</style> |
| 41 | +<script> |
| 42 | + window.addEventListener('load', function () { |
| 43 | + setTimeout(function () { window.dispatchEvent(new Event('resize')); }, 100); |
| 44 | + }); |
| 45 | +</script> |
| 46 | +""" |
| 47 | + |
| 48 | +TIER_LABELS = { |
| 49 | + "execution": "Tier 1 — Execution (exchange colo)", |
| 50 | + "network": "Tier 2 — Network (microwave/shortwave)", |
| 51 | + "research": "Tier 3 — Research (ML compute)", |
| 52 | +} |
| 53 | + |
| 54 | +# Hex colors per tier, referenced in the kepler config below. |
| 55 | +TIER_COLORS = { |
| 56 | + "execution": [230, 80, 60], # red — the latency layer |
| 57 | + "network": [250, 190, 60], # amber — the towers |
| 58 | + "research": [70, 130, 240], # blue — the compute layer |
| 59 | +} |
| 60 | + |
| 61 | + |
| 62 | +def load_sites(csv_path: Path) -> pd.DataFrame: |
| 63 | + df = pd.read_csv(csv_path) |
| 64 | + |
| 65 | + required = {"site_id", "site_name", "tier", "lat", "lon", "confidence"} |
| 66 | + missing = required - set(df.columns) |
| 67 | + if missing: |
| 68 | + raise ValueError(f"CSV missing required columns: {missing}") |
| 69 | + |
| 70 | + # Google Maps deep link per pin — the "zoom in for detail" handoff. |
| 71 | + df["gmaps_url"] = ( |
| 72 | + "https://www.google.com/maps/search/?api=1&query=" |
| 73 | + + df["lat"].round(6).astype(str) |
| 74 | + + "," |
| 75 | + + df["lon"].round(6).astype(str) |
| 76 | + ) |
| 77 | + df["tier_label"] = df["tier"].map(TIER_LABELS).fillna(df["tier"]) |
| 78 | + |
| 79 | + # Fail loudly on bad coordinates rather than silently dropping pins. |
| 80 | + bad = df[(df["lat"].abs() > 90) | (df["lon"].abs() > 180) | df["lat"].isna()] |
| 81 | + if not bad.empty: |
| 82 | + raise ValueError(f"Bad coordinates for: {bad['site_id'].tolist()}") |
| 83 | + |
| 84 | + return df |
| 85 | + |
| 86 | + |
| 87 | +def kepler_config(df: pd.DataFrame) -> dict: |
| 88 | + """Minimal kepler config: one point layer per tier, tooltip with evidence fields.""" |
| 89 | + center_lat = float(df["lat"].mean()) |
| 90 | + center_lon = float(df["lon"].mean()) |
| 91 | + |
| 92 | + layers = [] |
| 93 | + for tier, color in TIER_COLORS.items(): |
| 94 | + layers.append( |
| 95 | + { |
| 96 | + "id": f"layer_{tier}", |
| 97 | + "type": "point", |
| 98 | + "config": { |
| 99 | + "dataId": tier, |
| 100 | + "label": TIER_LABELS[tier], |
| 101 | + "columns": {"lat": "lat", "lng": "lon"}, |
| 102 | + "isVisible": True, |
| 103 | + "color": color, |
| 104 | + "visConfig": { |
| 105 | + "radius": 14, |
| 106 | + "opacity": 0.85, |
| 107 | + "outline": True, |
| 108 | + "thickness": 2, |
| 109 | + }, |
| 110 | + }, |
| 111 | + } |
| 112 | + ) |
| 113 | + |
| 114 | + tooltip_fields = [ |
| 115 | + {"name": f} |
| 116 | + for f in [ |
| 117 | + "site_name", |
| 118 | + "operator_or_venue", |
| 119 | + "firms_linked", |
| 120 | + "capacity_note", |
| 121 | + "confidence", |
| 122 | + "evidence_note", |
| 123 | + "gmaps_url", |
| 124 | + ] |
| 125 | + ] |
| 126 | + |
| 127 | + return { |
| 128 | + "version": "v1", |
| 129 | + "config": { |
| 130 | + "visState": { |
| 131 | + "layers": layers, |
| 132 | + "interactionConfig": { |
| 133 | + "tooltip": { |
| 134 | + "fieldsToShow": {t: tooltip_fields for t in TIER_COLORS}, |
| 135 | + "enabled": True, |
| 136 | + } |
| 137 | + }, |
| 138 | + }, |
| 139 | + "mapState": { |
| 140 | + "latitude": center_lat, |
| 141 | + "longitude": center_lon, |
| 142 | + "zoom": 2.4, |
| 143 | + }, |
| 144 | + "mapStyle": { |
| 145 | + "styleType": CARTO_DARK["id"], |
| 146 | + "mapStyles": {CARTO_DARK["id"]: CARTO_DARK}, |
| 147 | + }, |
| 148 | + }, |
| 149 | + } |
| 150 | + |
| 151 | + |
| 152 | +def build(csv_path: Path, out_path: Path) -> None: |
| 153 | + df = load_sites(csv_path) |
| 154 | + |
| 155 | + m = KeplerGl(config=kepler_config(df)) |
| 156 | + # One dataset per tier so each gets its own layer + legend entry + toggle. |
| 157 | + for tier in TIER_COLORS: |
| 158 | + subset = df[df["tier"] == tier].reset_index(drop=True) |
| 159 | + if not subset.empty: |
| 160 | + m.add_data(data=subset, name=tier) |
| 161 | + |
| 162 | + m.save_to_html(file_name=str(out_path), read_only=False) |
| 163 | + |
| 164 | + html = out_path.read_text(encoding="utf-8") |
| 165 | + html, n_stripped = MAPBOX_TOKEN_RE.subn("", html) |
| 166 | + # rsplit: "</body>" also appears inside the bundled JS; only the last |
| 167 | + # occurrence is the real closing tag. |
| 168 | + head, _, tail = html.rpartition("</body>") |
| 169 | + html = head + FULLSCREEN_SNIPPET + "</body>" + tail |
| 170 | + out_path.write_text(html, encoding="utf-8") |
| 171 | + |
| 172 | + print(f"Wrote {out_path} — {len(df)} sites " |
| 173 | + f"({df['tier'].value_counts().to_dict()}); " |
| 174 | + f"stripped {n_stripped} bundled Mapbox token(s)") |
| 175 | + |
| 176 | + |
| 177 | +if __name__ == "__main__": |
| 178 | + parser = argparse.ArgumentParser() |
| 179 | + parser.add_argument("--csv", default="data/sites_seed.csv", type=Path) |
| 180 | + parser.add_argument("--out", default="quant_dc_map.html", type=Path) |
| 181 | + args = parser.parse_args() |
| 182 | + build(args.csv, args.out) |
0 commit comments