1+ """
2+ Folium Multi-Language Choropleth Solution
3+
4+ This module provides multiple approaches to render folium choropleths
5+ with different number locales for legends, enabling creation of maps
6+ in multiple languages without changing system locale.
7+
8+ Requirements:
9+ - folium
10+ - pandas
11+ - geopandas (optional, for geojson data)
12+ - selenium (for image generation)
13+ - pillow (for image processing)
14+
15+ Install with: pip install folium pandas selenium pillow
16+ """
17+
18+ import folium
19+ import pandas as pd
20+ import json
21+ import re
22+ from typing import Dict , List , Optional , Union
23+ import tempfile
24+ import os
25+
26+
27+ class MultiLanguageChoropleth :
28+ """
29+ A class to create folium choropleths with customizable number formatting
30+ for different languages/locales without changing system settings.
31+ """
32+
33+ def __init__ (self ):
34+ self .number_formats = {
35+ 'en' : {
36+ 'decimal_separator' : '.' ,
37+ 'thousands_separator' : ',' ,
38+ 'currency_symbol' : '$' ,
39+ 'position' : 'before' # currency position
40+ },
41+ 'fr' : {
42+ 'decimal_separator' : ',' ,
43+ 'thousands_separator' : ' ' ,
44+ 'currency_symbol' : '€' ,
45+ 'position' : 'after'
46+ },
47+ 'de' : {
48+ 'decimal_separator' : ',' ,
49+ 'thousands_separator' : '.' ,
50+ 'currency_symbol' : '€' ,
51+ 'position' : 'after'
52+ },
53+ 'es' : {
54+ 'decimal_separator' : ',' ,
55+ 'thousands_separator' : '.' ,
56+ 'currency_symbol' : '€' ,
57+ 'position' : 'after'
58+ }
59+ }
60+
61+ def format_number (self , number : float , locale : str = 'en' ,
62+ decimals : int = 2 , include_currency : bool = False ) -> str :
63+ """
64+ Format a number according to specified locale conventions.
65+
66+ Args:
67+ number: The number to format
68+ locale: Language locale ('en', 'fr', 'de', 'es')
69+ decimals: Number of decimal places
70+ include_currency: Whether to include currency symbol
71+
72+ Returns:
73+ Formatted number string
74+ """
75+ if locale not in self .number_formats :
76+ locale = 'en' # fallback to English
77+
78+ fmt = self .number_formats [locale ]
79+
80+ # Round to specified decimals
81+ rounded = round (number , decimals )
82+
83+ # Split into integer and decimal parts
84+ integer_part = int (rounded )
85+ decimal_part = rounded - integer_part
86+
87+ # Format integer part with thousands separator
88+ integer_str = f"{ integer_part :,} " .replace (',' , '|TEMP|' )
89+ integer_str = integer_str .replace ('|TEMP|' , fmt ['thousands_separator' ])
90+
91+ # Format decimal part
92+ if decimals > 0 and decimal_part > 0 :
93+ decimal_str = f"{ decimal_part :.{decimals }f} " [2 :] # Remove "0."
94+ formatted = f"{ integer_str } { fmt ['decimal_separator' ]} { decimal_str } "
95+ else :
96+ formatted = integer_str
97+
98+ # Add currency if requested
99+ if include_currency :
100+ if fmt ['position' ] == 'before' :
101+ formatted = f"{ fmt ['currency_symbol' ]} { formatted } "
102+ else :
103+ formatted = f"{ formatted } { fmt ['currency_symbol' ]} "
104+
105+ return formatted
106+
107+ def create_custom_legend_html (self , values : List [float ], colors : List [str ],
108+ locale : str = 'en' , title : str = "Legend" ) -> str :
109+ """
110+ Create custom HTML legend with locale-specific number formatting.
111+
112+ Args:
113+ values: List of values for legend
114+ colors: List of corresponding colors
115+ locale: Language locale
116+ title: Legend title
117+
118+ Returns:
119+ HTML string for custom legend
120+ """
121+ legend_html = f'''
122+ <div style="position: fixed;
123+ bottom: 50px; right: 50px; width: 150px; height: auto;
124+ background-color: white; border:2px solid grey; z-index:9999;
125+ font-size:14px; padding: 10px">
126+ <h4 style="margin-top:0;">{ title } </h4>
127+ '''
128+
129+ for i , (value , color ) in enumerate (zip (values , colors )):
130+ formatted_value = self .format_number (value , locale )
131+ legend_html += f'''
132+ <p style="margin: 5px 0;">
133+ <span style="background-color:{ color } ; width: 20px; height: 20px;
134+ display: inline-block; margin-right: 5px;"></span>
135+ { formatted_value }
136+ </p>
137+ '''
138+
139+ legend_html += '</div>'
140+ return legend_html
141+
142+ def inject_locale_javascript (self , locale : str = 'en' ) -> str :
143+ """
144+ Generate JavaScript to modify number formatting in existing legend.
145+
146+ Args:
147+ locale: Target locale for number formatting
148+
149+ Returns:
150+ JavaScript code string
151+ """
152+ fmt = self .number_formats [locale ]
153+
154+ js_code = f'''
155+ <script>
156+ // Function to format numbers according to locale
157+ function formatNumberLocale(num, locale) {{
158+ const formats = { json .dumps (self .number_formats )} ;
159+ const fmt = formats[locale] || formats['en'];
160+
161+ // Convert number to string and parse
162+ let numStr = parseFloat(num).toFixed(2);
163+ let parts = numStr.split('.');
164+
165+ // Add thousands separator
166+ parts[0] = parts[0].replace(/\\ B(?=(\\ d{{3}})+(?!\\ d))/g, fmt.thousands_separator);
167+
168+ // Join with decimal separator
169+ if (parts[1] && parseFloat('0.' + parts[1]) > 0) {{
170+ return parts[0] + fmt.decimal_separator + parts[1];
171+ }}
172+ return parts[0];
173+ }}
174+
175+ // Wait for map to load, then modify legend
176+ setTimeout(function() {{
177+ // Find all text elements in the legend that contain numbers
178+ const legendElements = document.querySelectorAll('.legend text, .colorbar text');
179+
180+ legendElements.forEach(function(element) {{
181+ const text = element.textContent;
182+ const numberMatch = text.match(/\\ d+\\ .?\\ d*/);
183+
184+ if (numberMatch) {{
185+ const originalNumber = parseFloat(numberMatch[0]);
186+ const formattedNumber = formatNumberLocale(originalNumber, '{ locale } ');
187+ element.textContent = text.replace(numberMatch[0], formattedNumber);
188+ }}
189+ }});
190+ }}, 1000);
191+ </script>
192+ '''
193+
194+ return js_code
195+
196+ def create_choropleth_with_locale (self , map_obj : folium .Map ,
197+ geo_data : Union [str , dict ],
198+ data : pd .DataFrame ,
199+ columns : List [str ],
200+ key_on : str ,
201+ locale : str = 'en' ,
202+ ** choropleth_kwargs ) -> folium .Map :
203+ """
204+ Create a choropleth with custom locale formatting.
205+
206+ Args:
207+ map_obj: Folium map object
208+ geo_data: GeoJSON data
209+ data: DataFrame with data to map
210+ columns: Columns for choropleth [key_column, value_column]
211+ key_on: Key in GeoJSON to match with data
212+ locale: Target locale
213+ **choropleth_kwargs: Additional arguments for folium.Choropleth
214+
215+ Returns:
216+ Modified folium map
217+ """
218+ # Create the choropleth first
219+ choropleth = folium .Choropleth (
220+ geo_data = geo_data ,
221+ data = data ,
222+ columns = columns ,
223+ key_on = key_on ,
224+ ** choropleth_kwargs
225+ ).add_to (map_obj )
226+
227+ # Add JavaScript to modify number formatting
228+ js_code = self .inject_locale_javascript (locale )
229+ map_obj .get_root ().html .add_child (folium .Element (js_code ))
230+
231+ return map_obj
232+
233+
234+ def create_sample_data () -> tuple :
235+ """
236+ Create sample data for demonstration.
237+
238+ Returns:
239+ Tuple of (sample_data_df, sample_geojson)
240+ """
241+ # Sample data
242+ sample_data = pd .DataFrame ({
243+ 'country' : ['USA' , 'Canada' , 'Mexico' , 'Brazil' , 'Argentina' ],
244+ 'value' : [1234567.89 , 987654.32 , 456789.12 , 2345678.90 , 876543.21 ]
245+ })
246+
247+ # Simple sample GeoJSON (normally you'd load this from a file)
248+ sample_geojson = {
249+ "type" : "FeatureCollection" ,
250+ "features" : [
251+ {
252+ "type" : "Feature" ,
253+ "properties" : {"name" : "USA" },
254+ "geometry" : {"type" : "Polygon" , "coordinates" : [[[- 100 , 40 ], [- 90 , 40 ], [- 90 , 50 ], [- 100 , 50 ], [- 100 , 40 ]]]}
255+ },
256+ {
257+ "type" : "Feature" ,
258+ "properties" : {"name" : "Canada" },
259+ "geometry" : {"type" : "Polygon" , "coordinates" : [[[- 110 , 50 ], [- 90 , 50 ], [- 90 , 60 ], [- 110 , 60 ], [- 110 , 50 ]]]}
260+ }
261+ ]
262+ }
263+
264+ return sample_data , sample_geojson
265+
266+
267+ def demo_multilanguage_choropleth ():
268+ """
269+ Demonstrate creating choropleths in multiple languages.
270+ """
271+ # Initialize the multi-language choropleth handler
272+ ml_choropleth = MultiLanguageChoropleth ()
273+
274+ # Create sample data
275+ sample_data , sample_geojson = create_sample_data ()
276+
277+ # Create maps for different locales
278+ locales = ['en' , 'fr' , 'de' ]
279+ maps = {}
280+
281+ for locale in locales :
282+ # Create base map
283+ m = folium .Map (location = [45 , - 100 ], zoom_start = 3 )
284+
285+ # Add choropleth with custom locale
286+ m = ml_choropleth .create_choropleth_with_locale (
287+ map_obj = m ,
288+ geo_data = sample_geojson ,
289+ data = sample_data ,
290+ columns = ['country' , 'value' ],
291+ key_on = 'feature.properties.name' ,
292+ locale = locale ,
293+ fill_color = 'YlOrRd' ,
294+ fill_opacity = 0.7 ,
295+ line_opacity = 0.2 ,
296+ legend_name = f'Sample Values ({ locale .upper ()} )'
297+ )
298+
299+ maps [locale ] = m
300+
301+ # Save map
302+ filename = f'choropleth_{ locale } .html'
303+ m .save (filename )
304+ print (f"Saved map in { locale .upper ()} locale as { filename } " )
305+
306+ return maps
307+
308+
309+ def save_map_as_image (map_obj : folium .Map , filename : str ,
310+ width : int = 1200 , height : int = 800 ):
311+ """
312+ Save folium map as image using selenium.
313+
314+ Args:
315+ map_obj: Folium map object
316+ filename: Output filename
317+ width: Image width
318+ height: Image height
319+ """
320+ try :
321+ from selenium import webdriver
322+ from selenium .webdriver .chrome .options import Options
323+ import time
324+
325+ # Save map as temporary HTML
326+ temp_html = tempfile .NamedTemporaryFile (mode = 'w' , suffix = '.html' , delete = False )
327+ map_obj .save (temp_html .name )
328+
329+ # Setup headless browser
330+ chrome_options = Options ()
331+ chrome_options .add_argument ('--headless' )
332+ chrome_options .add_argument (f'--window-size={ width } ,{ height } ' )
333+
334+ driver = webdriver .Chrome (options = chrome_options )
335+ driver .get (f'file://{ temp_html .name } ' )
336+
337+ # Wait for map to load
338+ time .sleep (3 )
339+
340+ # Take screenshot
341+ driver .save_screenshot (filename )
342+ driver .quit ()
343+
344+ # Clean up
345+ os .unlink (temp_html .name )
346+
347+ print (f"Map saved as image: { filename } " )
348+
349+ except ImportError :
350+ print ("Selenium not available. Install with: pip install selenium" )
351+ print ("Also need to install ChromeDriver for your system" )
352+ except Exception as e :
353+ print (f"Error saving image: { e } " )
354+
355+
356+ if __name__ == "__main__" :
357+ # Run the demonstration
358+ print ("Creating multi-language choropleths..." )
359+ maps = demo_multilanguage_choropleth ()
360+
361+ # Optionally save as images (requires selenium)
362+ # for locale, map_obj in maps.items():
363+ # save_map_as_image(map_obj, f'choropleth_{locale}.png')
364+
365+ print ("\n Demonstration complete!" )
366+ print ("Check the generated HTML files to see the different number formats." )
367+
368+ # Example of manual number formatting
369+ ml = MultiLanguageChoropleth ()
370+ print ("\n Example number formatting:" )
371+ number = 1234567.89
372+ for locale in ['en' , 'fr' , 'de' ]:
373+ formatted = ml .format_number (number , locale )
374+ print (f"{ locale .upper ()} : { formatted } " )
0 commit comments