1+ from pathlib import Path
2+
3+ import pandas as pd
4+ import geopandas as gpd
5+ import folium
6+
7+
8+ # =========================
9+ # Paths
10+ # =========================
11+
12+ ROOT = Path (__file__ ).resolve ().parent # ffrm_python/
13+ DOCS = ROOT / "docs"
14+
15+ # Your CSV in docs/data/
16+ DATA_CSV = DOCS / "data" / "EMDE_fossil-fuel_regimes___repopulated_inclusive_mapping.csv"
17+
18+ # Output HTML map
19+ OUT_HTML = DOCS / "_static" / "emde_regime_map.html"
20+
21+
22+ # =========================
23+ # Load and prepare data
24+ # =========================
25+
26+ print (f"Reading CSV from: { DATA_CSV } " )
27+ df = pd .read_csv (DATA_CSV )
28+
29+ print ("Columns in CSV:" , list (df .columns ))
30+
31+ # Use PrimaryBucket for colouring; fallback to 'unknown'
32+ df ["DisplayBucket" ] = df ["PrimaryBucket" ].fillna ("unknown" )
33+
34+ # Flag mixed systems: more than one of Has_Market / Has_PPA / Has_Regulated = True
35+ def classify_mixed (row ):
36+ flags = [
37+ bool (row .get ("Has_Market" , False )),
38+ bool (row .get ("Has_PPA" , False )),
39+ bool (row .get ("Has_Regulated" , False )),
40+ ]
41+ return "Yes" if sum (flags ) > 1 else "No"
42+
43+ df ["MixedSystem" ] = df .apply (classify_mixed , axis = 1 )
44+
45+ print ("Unique PrimaryBucket values:" , df ["PrimaryBucket" ].dropna ().unique ())
46+ print ("Mixed system counts:" , df ["MixedSystem" ].value_counts ())
47+
48+
49+ # =========================
50+ # Load world geometry & join
51+ # =========================
52+
53+ print ("Loading Natural Earth world geometries..." )
54+ world = gpd .read_file (gpd .datasets .get_path ("naturalearth_lowres" ))
55+
56+ # Join on name (Natural Earth) vs Country (CSV)
57+ gdf = world .merge (df , left_on = "name" , right_on = "Country" , how = "inner" )
58+
59+ print (f"Joined rows: { len (gdf )} (out of { len (df )} in CSV)" )
60+ missing = set (df ["Country" ]) - set (gdf ["Country" ])
61+ if missing :
62+ print ("Countries in CSV not matched in Natural Earth:" , missing )
63+
64+ gdf = gpd .GeoDataFrame (gdf , crs = world .crs )
65+
66+
67+ # =========================
68+ # Build Folium map
69+ # =========================
70+
71+ m = folium .Map (location = [10 , 10 ], zoom_start = 2 , tiles = "cartodbpositron" )
72+
73+ colors = {
74+ "market" : "#1f77b4" ,
75+ "ppa" : "#ff7f0e" ,
76+ "regulated" : "#2ca02c" ,
77+ "unknown" : "#b0b0b0" , # unclassified
78+ "_default" : "#e0e0e0" ,
79+ }
80+
81+ def style_function (feature ):
82+ bucket = feature ["properties" ].get ("DisplayBucket" , "_default" )
83+ color = colors .get (bucket , colors ["_default" ])
84+ return {
85+ "fillColor" : color ,
86+ "color" : "white" ,
87+ "weight" : 0.5 ,
88+ "fillOpacity" : 0.8 ,
89+ }
90+
91+ def highlight_function (_feature ):
92+ return {
93+ "weight" : 2 ,
94+ "color" : "#000000" ,
95+ "fillOpacity" : 0.9 ,
96+ }
97+
98+ # Hover: simple
99+ tooltip = folium .GeoJsonTooltip (
100+ fields = ["Country" , "PrimaryBucket" ],
101+ aliases = ["Country:" , "Electricity market regime:" ],
102+ localize = True ,
103+ sticky = True ,
104+ )
105+
106+ # Click popup: show mixed system + flags
107+ popup = folium .GeoJsonPopup (
108+ fields = [
109+ "Country" ,
110+ "PrimaryBucket" ,
111+ "MixedSystem" ,
112+ "Has_Market" ,
113+ "Has_PPA" ,
114+ "Has_Regulated" ,
115+ "Notes" ,
116+ ],
117+ aliases = [
118+ "Country:" ,
119+ "Primary regime:" ,
120+ "Mixed system (multiple mechanisms):" ,
121+ "Has market?" ,
122+ "Has PPA?" ,
123+ "Has regulated tariffs?" ,
124+ "Notes:" ,
125+ ],
126+ localize = True ,
127+ labels = True ,
128+ parse_html = True ,
129+ )
130+
131+ geojson = folium .GeoJson (
132+ gdf ,
133+ style_function = style_function ,
134+ highlight_function = highlight_function ,
135+ tooltip = tooltip ,
136+ popup = popup ,
137+ )
138+
139+ geojson .add_to (m )
140+
141+ # Legend (no title, just colour → label)
142+ legend_html = """
143+ <div style="
144+ position: fixed;
145+ bottom: 30px;
146+ left: 30px;
147+ z-index: 9999;
148+ background: white;
149+ padding: 10px;
150+ border: 1px solid #ccc;
151+ font-size: 12px;
152+ ">
153+ <span style="background:#1f77b4;width:12px;height:12px;display:inline-block;margin-right:4px;"></span> Market-based<br>
154+ <span style="background:#ff7f0e;width:12px;height:12px;display:inline-block;margin-right:4px;"></span> PPA-based<br>
155+ <span style="background:#2ca02c;width:12px;height:12px;display:inline-block;margin-right:4px;"></span> Regulated<br>
156+ <span style="background:#b0b0b0;width:12px;height:12px;display:inline-block;margin-right:4px;"></span> Unclassified<br>
157+ </div>
158+ """
159+ m .get_root ().html .add_child (folium .Element (legend_html ))
160+
161+
162+ # =========================
163+ # Save map
164+ # =========================
165+
166+ OUT_HTML .parent .mkdir (parents = True , exist_ok = True )
167+ m .save (str (OUT_HTML ))
168+ print (f"Saved map to: { OUT_HTML } " )
169+
0 commit comments