11"""
22Quant/HFT infrastructure map — build script.
33
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.
4+ Reads data/sites_seed.csv (point pins) and data/paths.csv (Tier-2 arcs,
5+ joined to site coordinates via origin/dest site_ids), enriches them
6+ (Google Maps deep links, tier labels), and exports a standalone kepler.gl
7+ HTML map suitable for embedding in a blog post.
68
79Usage:
8- python build_map.py [--csv data/sites_seed.csv] [--out quant_dc_map.html]
10+ python build_map.py [--csv data/sites_seed.csv] [--paths data/paths.csv]
11+ [--out quant_dc_map.html]
912"""
1013
1114from __future__ import annotations
5861 "research" : [70 , 130 , 240 ], # blue — the compute layer
5962}
6063
64+ PATHS_LABEL = "Tier 2 — Paths (arcs)"
65+ PATHS_COLOR = [250 , 190 , 60 ] # amber, matching the network tier
66+
6167
6268def load_sites (csv_path : Path ) -> pd .DataFrame :
6369 df = pd .read_csv (csv_path )
@@ -84,8 +90,33 @@ def load_sites(csv_path: Path) -> pd.DataFrame:
8490 return df
8591
8692
87- def kepler_config (df : pd .DataFrame ) -> dict :
88- """Minimal kepler config: one point layer per tier, tooltip with evidence fields."""
93+ def load_paths (paths_csv : Path , sites : pd .DataFrame ) -> pd .DataFrame | None :
94+ """Load Tier-2 arcs and join origin/dest coordinates from the sites table.
95+
96+ Returns None when the paths file is absent or has no rows."""
97+ if not paths_csv .exists ():
98+ return None
99+ paths = pd .read_csv (paths_csv )
100+ if paths .empty :
101+ return None
102+
103+ coords = sites .set_index ("site_id" )[["lat" , "lon" , "site_name" ]]
104+ unknown = (set (paths ["origin_site_id" ]) | set (paths ["dest_site_id" ])) - set (coords .index )
105+ if unknown :
106+ raise ValueError (f"paths.csv references unknown site_ids: { sorted (unknown )} " )
107+
108+ for end , col in (("origin" , "origin_site_id" ), ("dest" , "dest_site_id" )):
109+ joined = coords .loc [paths [col ]].reset_index (drop = True )
110+ paths [f"{ end } _lat" ] = joined ["lat" ]
111+ paths [f"{ end } _lon" ] = joined ["lon" ]
112+ paths [f"{ end } _name" ] = joined ["site_name" ]
113+ paths ["route" ] = paths ["origin_name" ] + " → " + paths ["dest_name" ]
114+ return paths
115+
116+
117+ def kepler_config (df : pd .DataFrame , with_paths : bool = False ) -> dict :
118+ """Minimal kepler config: one point layer per tier (+ an arc layer for
119+ Tier-2 paths), tooltip with evidence fields."""
89120 center_lat = float (df ["lat" ].mean ())
90121 center_lon = float (df ["lon" ].mean ())
91122
@@ -110,6 +141,26 @@ def kepler_config(df: pd.DataFrame) -> dict:
110141 },
111142 }
112143 )
144+ if with_paths :
145+ layers .append (
146+ {
147+ "id" : "layer_paths" ,
148+ "type" : "arc" ,
149+ "config" : {
150+ "dataId" : "paths" ,
151+ "label" : PATHS_LABEL ,
152+ "columns" : {
153+ "lat0" : "origin_lat" ,
154+ "lng0" : "origin_lon" ,
155+ "lat1" : "dest_lat" ,
156+ "lng1" : "dest_lon" ,
157+ },
158+ "isVisible" : True ,
159+ "color" : PATHS_COLOR ,
160+ "visConfig" : {"opacity" : 0.5 , "thickness" : 1.5 },
161+ },
162+ }
163+ )
113164
114165 tooltip_fields = [
115166 {"name" : f }
@@ -118,11 +169,22 @@ def kepler_config(df: pd.DataFrame) -> dict:
118169 "operator_or_venue" ,
119170 "firms_linked" ,
120171 "capacity_note" ,
172+ "power_mw" ,
173+ "status" ,
121174 "confidence" ,
122175 "evidence_note" ,
176+ "evidence_url" ,
123177 "gmaps_url" ,
124178 ]
125179 ]
180+ paths_tooltip = [
181+ {"name" : f }
182+ for f in ["route" , "operator" , "medium" , "status" , "confidence" ,
183+ "evidence_note" , "evidence_url" ]
184+ ]
185+ fields_to_show = {t : tooltip_fields for t in TIER_COLORS }
186+ if with_paths :
187+ fields_to_show ["paths" ] = paths_tooltip
126188
127189 return {
128190 "version" : "v1" ,
@@ -131,15 +193,15 @@ def kepler_config(df: pd.DataFrame) -> dict:
131193 "layers" : layers ,
132194 "interactionConfig" : {
133195 "tooltip" : {
134- "fieldsToShow" : { t : tooltip_fields for t in TIER_COLORS } ,
196+ "fieldsToShow" : fields_to_show ,
135197 "enabled" : True ,
136198 }
137199 },
138200 },
139201 "mapState" : {
140202 "latitude" : center_lat ,
141203 "longitude" : center_lon ,
142- "zoom" : 2.4 ,
204+ "zoom" : 1.7 ,
143205 },
144206 "mapStyle" : {
145207 "styleType" : CARTO_DARK ["id" ],
@@ -149,15 +211,18 @@ def kepler_config(df: pd.DataFrame) -> dict:
149211 }
150212
151213
152- def build (csv_path : Path , out_path : Path ) -> None :
214+ def build (csv_path : Path , out_path : Path , paths_csv : Path | None = None ) -> None :
153215 df = load_sites (csv_path )
216+ paths = load_paths (paths_csv , df ) if paths_csv else None
154217
155- m = KeplerGl (config = kepler_config (df ))
218+ m = KeplerGl (config = kepler_config (df , with_paths = paths is not None ))
156219 # One dataset per tier so each gets its own layer + legend entry + toggle.
157220 for tier in TIER_COLORS :
158221 subset = df [df ["tier" ] == tier ].reset_index (drop = True )
159222 if not subset .empty :
160223 m .add_data (data = subset , name = tier )
224+ if paths is not None :
225+ m .add_data (data = paths , name = "paths" )
161226
162227 m .save_to_html (file_name = str (out_path ), read_only = False )
163228
@@ -169,14 +234,16 @@ def build(csv_path: Path, out_path: Path) -> None:
169234 html = head + FULLSCREEN_SNIPPET + "</body>" + tail
170235 out_path .write_text (html , encoding = "utf-8" )
171236
237+ n_paths = 0 if paths is None else len (paths )
172238 print (f"Wrote { out_path } — { len (df )} sites "
173- f"({ df ['tier' ].value_counts ().to_dict ()} ); "
239+ f"({ df ['tier' ].value_counts ().to_dict ()} ), { n_paths } paths ; "
174240 f"stripped { n_stripped } bundled Mapbox token(s)" )
175241
176242
177243if __name__ == "__main__" :
178244 parser = argparse .ArgumentParser ()
179245 parser .add_argument ("--csv" , default = "data/sites_seed.csv" , type = Path )
246+ parser .add_argument ("--paths" , default = "data/paths.csv" , type = Path )
180247 parser .add_argument ("--out" , default = "quant_dc_map.html" , type = Path )
181248 args = parser .parse_args ()
182- build (args .csv , args .out )
249+ build (args .csv , args .out , paths_csv = args . paths )
0 commit comments