Skip to content

Commit 7ae6304

Browse files
committed
✨ Add choropleth map overlay to Defense Systems tab - shows all countries with consistent baseline z-values
1 parent 5d9e6f8 commit 7ae6304

File tree

4 files changed

+904
-3893
lines changed

4 files changed

+904
-3893
lines changed

docs/chart3-defense-systems.html

Lines changed: 1 addition & 3879 deletions
Large diffs are not rendered by default.
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
"""
2+
Export charts from Dash app with improved hover templates and interactive filters
3+
"""
4+
5+
import os
6+
import sys
7+
import json
8+
import re
9+
10+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
11+
12+
from geopolitical_app import create_threat_density_map
13+
from pages.chart1 import create_3d_surface_figure
14+
from pages.chart3 import create_3d_scatter_figure
15+
from pages.chart4 import create_radar_figure
16+
from pages.chart5 import create_heatmap_figure
17+
from pages.chart7 import create_connection_map_figure
18+
19+
def generate_chart_variations(chart_func, chart_name, filter_configs):
20+
"""Generate multiple chart variations for different filter options"""
21+
variations = {}
22+
23+
for filter_key, filter_params in filter_configs.items():
24+
try:
25+
fig = chart_func(**filter_params)
26+
variations[filter_key] = fig.to_json()
27+
except Exception as e:
28+
print(f" ⚠️ Error generating variation {filter_key}: {e}")
29+
30+
return variations
31+
32+
def create_interactive_html(chart_name, chart_id, variations, filters_config):
33+
"""Create HTML with embedded chart variations and JavaScript interactivity"""
34+
35+
# Get the first variation to use as default
36+
default_variation = list(variations.values())[0] if variations else "{}"
37+
38+
# Create filter HTML
39+
filter_html = ""
40+
filter_js = ""
41+
42+
for filter_name, filter_options in filters_config.items():
43+
filter_id = f"{chart_id}_{filter_name}"
44+
filter_html += f'''
45+
<div class="filter-group">
46+
<label for="{filter_id}">{filter_name}:</label>
47+
<select id="{filter_id}" class="filter-select" onchange="updateChart()">
48+
'''
49+
for option_value, option_label in filter_options:
50+
filter_html += f' <option value="{option_value}">{option_label}</option>\n'
51+
filter_html += ' </select>\n </div>\n'
52+
53+
# Create JavaScript to handle filter changes
54+
filter_js = f'''
55+
function updateChart() {{
56+
const filters = {{}};
57+
'''
58+
for filter_name in filters_config.keys():
59+
filter_id = f"{chart_id}_{filter_name}"
60+
filter_js += f''' filters['{filter_name}'] = document.getElementById('{filter_id}').value;
61+
'''
62+
63+
filter_js += f''' const key = Object.values(filters).join('_');
64+
const chartData = window.chartVariations[key];
65+
if (chartData) {{
66+
Plotly.react('chart-container', chartData.data, chartData.layout, chartData.config);
67+
}}
68+
}}
69+
'''
70+
71+
html = f'''<!DOCTYPE html>
72+
<html lang="en">
73+
<head>
74+
<meta charset="UTF-8">
75+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
76+
<title>{chart_name} | Geopolitical Dashboard</title>
77+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
78+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
79+
<style>
80+
* {{
81+
margin: 0;
82+
padding: 0;
83+
box-sizing: border-box;
84+
}}
85+
86+
body {{
87+
background: linear-gradient(135deg, #0d1b2a 0%, #1a1a2e 100%);
88+
color: #fff;
89+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
90+
}}
91+
92+
.navbar {{
93+
background-color: #0d1b2a;
94+
border-bottom: 2px solid #1e90ff;
95+
padding: 15px 20px;
96+
box-shadow: 0 2px 10px rgba(30, 144, 255, 0.2);
97+
}}
98+
99+
.navbar-container {{
100+
max-width: 100%;
101+
margin: 0 auto;
102+
display: flex;
103+
justify-content: space-between;
104+
align-items: center;
105+
flex-wrap: wrap;
106+
gap: 20px;
107+
}}
108+
109+
.navbar-brand {{
110+
font-size: 1.5rem;
111+
font-weight: bold;
112+
color: #1e90ff;
113+
text-decoration: none;
114+
white-space: nowrap;
115+
}}
116+
117+
.navbar-brand:hover {{
118+
color: #00d4ff;
119+
}}
120+
121+
.nav-tabs {{
122+
display: flex;
123+
gap: 10px;
124+
flex-wrap: wrap;
125+
justify-content: flex-end;
126+
}}
127+
128+
.nav-tab {{
129+
padding: 8px 15px;
130+
background-color: #1a1a2e;
131+
border: 1px solid #1e90ff;
132+
border-radius: 4px;
133+
color: #aaa;
134+
text-decoration: none;
135+
font-size: 0.85rem;
136+
transition: all 0.3s;
137+
white-space: nowrap;
138+
}}
139+
140+
.nav-tab:hover {{
141+
background-color: #1e90ff;
142+
color: #fff;
143+
border-color: #00d4ff;
144+
}}
145+
146+
.nav-tab.active {{
147+
background-color: #1e90ff;
148+
color: #fff;
149+
border-color: #00d4ff;
150+
}}
151+
152+
.content-container {{
153+
padding: 20px;
154+
max-width: 100%;
155+
}}
156+
157+
.filters-section {{
158+
background-color: #1a1a2e;
159+
border: 1px solid #1e90ff;
160+
border-radius: 8px;
161+
padding: 20px;
162+
margin-bottom: 20px;
163+
display: flex;
164+
gap: 20px;
165+
flex-wrap: wrap;
166+
align-items: flex-end;
167+
}}
168+
169+
.filter-group {{
170+
display: flex;
171+
flex-direction: column;
172+
gap: 8px;
173+
}}
174+
175+
.filter-group label {{
176+
font-weight: bold;
177+
color: #1e90ff;
178+
font-size: 0.9rem;
179+
}}
180+
181+
.filter-select {{
182+
padding: 8px 12px;
183+
background-color: #0d1b2a;
184+
border: 1px solid #1e90ff;
185+
border-radius: 4px;
186+
color: #fff;
187+
font-size: 0.9rem;
188+
cursor: pointer;
189+
transition: all 0.3s;
190+
}}
191+
192+
.filter-select:hover {{
193+
border-color: #00d4ff;
194+
box-shadow: 0 0 10px rgba(30, 144, 255, 0.3);
195+
}}
196+
197+
.filter-select:focus {{
198+
outline: none;
199+
border-color: #00d4ff;
200+
box-shadow: 0 0 10px rgba(30, 144, 255, 0.5);
201+
}}
202+
203+
.filter-select option {{
204+
background-color: #0d1b2a;
205+
color: #fff;
206+
}}
207+
208+
#chart-container {{
209+
background-color: #1a1a2e;
210+
border: 1px solid #1e90ff;
211+
border-radius: 8px;
212+
padding: 20px;
213+
margin-bottom: 20px;
214+
}}
215+
216+
.footer {{
217+
background-color: #0d1b2a;
218+
border-top: 2px solid #1e90ff;
219+
padding: 20px;
220+
text-align: center;
221+
color: #aaa;
222+
margin-top: 40px;
223+
}}
224+
225+
@media (max-width: 768px) {{
226+
.navbar-container {{
227+
flex-direction: column;
228+
align-items: flex-start;
229+
}}
230+
231+
.nav-tabs {{
232+
justify-content: flex-start;
233+
width: 100%;
234+
}}
235+
236+
.nav-tab {{
237+
flex: 1;
238+
text-align: center;
239+
min-width: 100px;
240+
}}
241+
242+
.filters-section {{
243+
flex-direction: column;
244+
align-items: stretch;
245+
}}
246+
247+
.filter-group {{
248+
width: 100%;
249+
}}
250+
}}
251+
</style>
252+
</head>
253+
<body>
254+
<nav class="navbar">
255+
<div class="navbar-container">
256+
<a class="navbar-brand" href="index.html">🌍 Geopolitical Dashboard</a>
257+
<div class="nav-tabs">
258+
<a href="threat-perception-analysis.html" class="nav-tab">Threat Perception</a>
259+
<a href="chart1-supplier-influence.html" class="nav-tab active">Supplier Influence</a>
260+
<a href="chart3-defense-systems.html" class="nav-tab">Defense Systems</a>
261+
<a href="chart4-multi-country-radar.html" class="nav-tab">Multi-Country Radar</a>
262+
<a href="chart5-priorities-heatmap.html" class="nav-tab">Priorities Heatmap</a>
263+
<a href="chart7-supplier-connections.html" class="nav-tab">Supplier Connections</a>
264+
</div>
265+
</div>
266+
</nav>
267+
268+
<div class="content-container">
269+
<h1 style="margin-bottom: 20px;">{chart_name}</h1>
270+
271+
<div class="filters-section">
272+
{filter_html}
273+
</div>
274+
275+
<div id="chart-container"></div>
276+
</div>
277+
278+
<div class="footer">
279+
<p>© 2025 Geopolitical Analysis Dashboard</p>
280+
</div>
281+
282+
<script>
283+
// Store all chart variations
284+
window.chartVariations = {json.dumps(variations)};
285+
286+
// Initialize with default chart
287+
function initChart() {{
288+
const defaultKey = Object.keys(window.chartVariations)[0];
289+
const chartData = window.chartVariations[defaultKey];
290+
if (chartData) {{
291+
Plotly.react('chart-container', chartData.data, chartData.layout, chartData.config);
292+
}}
293+
}}
294+
295+
{filter_js}
296+
297+
// Initialize on page load
298+
window.addEventListener('load', initChart);
299+
</script>
300+
</body>
301+
</html>'''
302+
303+
return html
304+
305+
def main():
306+
print("=" * 70)
307+
print("EXPORTING CHARTS WITH IMPROVED HOVER TEMPLATES AND FILTERS")
308+
print("=" * 70)
309+
print()
310+
311+
# Chart 1: Supplier Influence with filters
312+
print("📊 Chart 1: Supplier Influence Surface")
313+
chart1_variations = generate_chart_variations(
314+
create_3d_surface_figure,
315+
"Supplier Influence",
316+
{
317+
"Influence_AllCountries": {"influence_type": "Influence", "show_all_countries": True},
318+
"Influence_TopCountries": {"influence_type": "Influence", "show_all_countries": False},
319+
"Matrix_AllCountries": {"influence_type": "Matrix", "show_all_countries": True},
320+
"Matrix_TopCountries": {"influence_type": "Matrix", "show_all_countries": False},
321+
}
322+
)
323+
324+
if chart1_variations:
325+
chart1_html = create_interactive_html(
326+
"Supplier Influence Surface",
327+
"chart1",
328+
chart1_variations,
329+
{
330+
"Influence Type": [("Influence_AllCountries", "Influence - All Countries"), ("Influence_TopCountries", "Influence - Top 3"), ("Matrix_AllCountries", "Matrix - All Countries"), ("Matrix_TopCountries", "Matrix - Top 3")]
331+
}
332+
)
333+
with open("chart1-supplier-influence.html", "w") as f:
334+
f.write(chart1_html)
335+
print(" ✅ Generated with interactive filters")
336+
337+
print()
338+
339+
if __name__ == '__main__':
340+
main()

0 commit comments

Comments
 (0)