Eric2mangel commited on
Commit
4da4a24
·
verified ·
1 Parent(s): b32f240

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +182 -48
app.py CHANGED
@@ -1,28 +1,119 @@
1
  import streamlit as st
2
  import pandas as pd
 
3
  import h3
4
  import pydeck as pdk
5
  import os
6
 
7
- st.set_page_config(page_title="France H3 3D", layout="wide", initial_sidebar_state="expanded")
8
- st.title("France 3D avec H3 – PyDeck + Streamlit")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  # --- SIDEBAR ---
11
  with st.sidebar:
12
- st.header("Paramètres")
13
-
14
  COLONNE_VALEUR = st.selectbox(
15
- 'Colonne (hauteur + couleur)',
16
- ['population', 'altitude_moyenne', 'superficie_km2', 'densite'],
17
  index=0
18
  )
19
 
20
  RESOLUTION_H3 = st.slider('Résolution H3', 6, 9, 7)
21
-
22
- default_seuil = {'population': 1000, 'altitude_moyenne': 300, 'superficie_km2': 50, 'densite': 300}.get(COLONNE_VALEUR, 100)
 
 
 
 
23
  SEUIL_MIN = st.number_input(f'Seuil minimum ({COLONNE_VALEUR})', value=default_seuil, step=100)
24
 
25
- style_name = st.selectbox('Style', ['Dark Matter', 'Positron (Clair)', 'Voyager (Coloré)'], index=0)
 
 
 
 
 
26
  MAP_STYLES = {
27
  'Dark Matter': 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
28
  'Positron (Clair)': 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
@@ -30,91 +121,134 @@ with st.sidebar:
30
  }
31
  MAP_STYLE = MAP_STYLES[style_name]
32
 
33
- # --- CHARGEMENT + CACHE ---
34
  @st.cache_data
35
  def load_data():
36
  if not os.path.exists("Communes_all_fr.parquet"):
37
- st.error("Fichier Communes_all_fr.parquet manquant !")
38
  st.stop()
39
  df = pd.read_parquet("Communes_all_fr.parquet")
40
  df = df[df['superficie_km2'] < 800].copy()
41
- df = df.dropna(subset=['latitude', 'longitude', 'population', COLONNE_VALEUR])
42
  df = df[df['population'] > 0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  return df
44
 
45
  df = load_data()
46
 
47
  # --- TRAITEMENT H3 ---
48
  @st.cache_data
49
- def prepare_h3(df, res, col, seuil):
50
- df = df.copy()
51
- df['h3'] = df.apply(lambda r: h3.latlng_to_cell(r.latitude, r.longitude, res), axis=1)
52
 
53
- agg = 'sum' if col == 'population' else 'mean'
54
  grouped = df.groupby('h3').agg(
55
- valeur=(col, agg),
56
- population=('population', 'sum'),
57
- ville=('nom_standard', 'first')
58
  ).reset_index()
59
-
60
  grouped = grouped[grouped.valeur >= seuil]
61
- if grouped.empty:
62
- return []
 
 
 
63
 
64
- vmax = grouped.valeur.max()
65
- vmin = grouped.valeur.min()
66
  grouped['norm'] = 1.0 if vmax == vmin else (grouped.valeur - vmin) / (vmax - vmin)
67
 
68
  data = []
69
  for _, row in grouped.iterrows():
70
  boundary = h3.cell_to_boundary(row.h3)
71
  polygon = [[lon, lat] for lat, lon in boundary]
 
72
  ratio = row['norm']
73
- color = [255, int(255*(1-ratio)), 0, 230] if ratio > 0.5 else [0, int(255*ratio*2), 255, 230]
 
 
 
 
 
 
 
 
 
74
  data.append({
75
  "polygon": polygon,
76
- "valeur": round(row.valeur, 2),
77
- "ville": str(row.ville) if pd.notna(row.ville) else "Inconnue",
78
  "population": int(row.population),
79
  "color": color,
80
- "elevation": row.valeur * (10 if col == "superficie_km2" else 1)
81
  })
82
- return data, vmax
83
-
84
- data, vmax = prepare_h3(df, RESOLUTION_H3, COLONNE_VALEUR, SEUIL_MIN)
85
 
 
86
  if not data:
87
- st.warning("Aucun hexagone avec ce seuil baisse-le !")
88
  st.stop()
89
 
90
- # --- CARTE PYDECK SANS BOUCLE INFINIE ---
 
 
 
 
 
 
 
 
 
 
 
91
  layer = pdk.Layer(
92
  "PolygonLayer",
93
  data,
94
  get_polygon="polygon",
95
  get_fill_color="color",
96
  get_elevation="elevation",
97
- elevation_scale=0.06 if COLONNE_VALEUR == "population" else 1.2,
98
  extruded=True,
99
  wireframe=True,
100
- pickable=False, # ← DÉSACTIVE LES INTERACTIONS
101
- auto_highlight=False, # ← ÉVITE LES RERUNS
102
  line_width_min_pixels=1
103
  )
104
 
105
  deck = pdk.Deck(
106
  layers=[layer],
107
- initial_view_state=pdk.ViewState(latitude=46.5, longitude=2.5, zoom=5.5, pitch=50),
108
- map_style=MAP_STYLE
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  )
110
 
111
- # LE PARAMÈTRE MAGIQUE QUI ÉVITE LA BOUCLE
112
- st.pydeck_chart(deck, use_container_width=True, on_select="ignore")
113
-
114
- # --- MÉTRIQUES ---
115
- col1, col2, col3 = st.columns(3)
116
- col1.metric("Hexagones", len(data))
117
- col2.metric("Valeur max", f"{vmax:,.0f}")
118
- col3.metric("Résolution H3", RESOLUTION_H3)
119
-
120
- st.success("Carte affichée sans boucle infinie – tout fonctionne !")
 
1
  import streamlit as st
2
  import pandas as pd
3
+ import numpy as np
4
  import h3
5
  import pydeck as pdk
6
  import os
7
 
8
+ # --- CONFIG GÉNÉRALE ---
9
+ st.set_page_config(
10
+ page_title="Visualisation H3 3D optimisée",
11
+ layout="wide",
12
+ initial_sidebar_state="expanded"
13
+ )
14
+
15
+ # --- CSS GLOBAL BEAUTÉ ---
16
+ style = """
17
+ <style>
18
+
19
+ /* --- TITRE CENTRÉ --- */
20
+ h1 {
21
+ font-family: 'Inter', sans-serif;
22
+ font-weight: 700;
23
+ letter-spacing: -1px;
24
+ }
25
+
26
+ /* --- FOND PAGE --- */
27
+ body {
28
+ background: #f4f6fa;
29
+ }
30
+
31
+ /* --- SIDEBAR --- */
32
+ section[data-testid="stSidebar"] {
33
+ background: linear-gradient(180deg, #101826 0%, #1d2b45 100%);
34
+ color: white;
35
+ }
36
+ section[data-testid="stSidebar"] * {
37
+ color: #e6e8ef !important;
38
+ font-family: 'Inter', sans-serif;
39
+ }
40
+
41
+ /* Titres Sidebar */
42
+ section[data-testid="stSidebar"] h1,
43
+ section[data-testid="stSidebar"] h2,
44
+ section[data-testid="stSidebar"] h3 {
45
+ color: #fafafa !important;
46
+ }
47
+
48
+ /* --- SELECTBOX / INPUT --- */
49
+ .stSelectbox, .stNumberInput, .stSlider {
50
+ padding-top: 8px;
51
+ }
52
+ .css-1in5nid, .css-16huue1 {
53
+ color: white !important;
54
+ }
55
+
56
+ /* --- TEXTE À L’INTÉRIEUR DES SELECTBOX = #444 --- */
57
+ section[data-testid="stSidebar"] div[data-baseweb="select"] *,
58
+ section[data-testid="stSidebar"] .stSelectbox input {
59
+ color: #444 !important;
60
+ }
61
+
62
+ /* --- ENCART DE LA CARTE --- */
63
+ .map-container {
64
+ background: white;
65
+ padding: 0;
66
+ border-radius: 16px;
67
+ box-shadow: 0 6px 22px rgba(0,0,0,0.15);
68
+ margin-top: 10px;
69
+ }
70
+
71
+ /* --- TOOLTIP PYDECK --- */
72
+ .deck-tooltip {
73
+ backdrop-filter: blur(6px);
74
+ }
75
+
76
+ section[data-testid="stSidebar"] .stNumberInput input {
77
+ color: #444 !important;
78
+ }
79
+
80
+ </style>
81
+ """
82
+ st.markdown(style, unsafe_allow_html=True)
83
+
84
+ # TITRE
85
+ titre_centre = """
86
+ <h1 style='text-align: center; color: #1f77b4; margin-bottom:10px;'>
87
+ Analyse territoriale avancée
88
+ </h1>
89
+ """
90
+ st.html(titre_centre)
91
 
92
  # --- SIDEBAR ---
93
  with st.sidebar:
94
+ st.header("⚙️ Paramètres")
95
+
96
  COLONNE_VALEUR = st.selectbox(
97
+ 'Caractéristique',
98
+ ['population', 'altitude_moyenne', 'altitude_minimale', 'altitude_maximale', 'superficie_km2', 'densite'],
99
  index=0
100
  )
101
 
102
  RESOLUTION_H3 = st.slider('Résolution H3', 6, 9, 7)
103
+
104
+ default_seuil = {
105
+ 'population': 2000, 'altitude_moyenne': 500, 'altitude_minimale': 500,
106
+ 'altitude_maximale': 500, 'superficie_km2': 100, 'densite': 500
107
+ }.get(COLONNE_VALEUR, 100)
108
+
109
  SEUIL_MIN = st.number_input(f'Seuil minimum ({COLONNE_VALEUR})', value=default_seuil, step=100)
110
 
111
+ style_name = st.selectbox(
112
+ 'Style de carte',
113
+ ['Dark Matter', 'Positron (Clair)', 'Voyager (Coloré)'],
114
+ index=0
115
+ )
116
+
117
  MAP_STYLES = {
118
  'Dark Matter': 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
119
  'Positron (Clair)': 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
 
121
  }
122
  MAP_STYLE = MAP_STYLES[style_name]
123
 
124
+ # --- CHARGEMENT ---
125
  @st.cache_data
126
  def load_data():
127
  if not os.path.exists("Communes_all_fr.parquet"):
128
+ st.error("Fichier manquant !")
129
  st.stop()
130
  df = pd.read_parquet("Communes_all_fr.parquet")
131
  df = df[df['superficie_km2'] < 800].copy()
132
+ df = df.dropna(subset=['latitude', 'longitude', 'population', 'nom_standard'])
133
  df = df[df['population'] > 0]
134
+
135
+ if 'reg_code' in df.columns:
136
+ df['reg_code'] = df['reg_code'].astype('int8', errors='ignore')
137
+ if 'dep_code' in df.columns:
138
+ df['dep_code'] = df['dep_code'].astype('float16', errors='ignore')
139
+ if 'canton_code' in df.columns:
140
+ df['canton_code'] = df['canton_code'].astype('float16', errors='ignore')
141
+
142
+ df['population'] = df['population'].astype('int32', errors='ignore')
143
+
144
+ for col in ['superficie_hectare','superficie_km2','densite','altitude_moyenne','altitude_minimale','altitude_maximale']:
145
+ if col in df.columns:
146
+ df[col] = df[col].astype('float32', errors='ignore')
147
+
148
+ df['latitude'] = df['latitude'].astype(np.float32)
149
+ df['longitude'] = df['longitude'].astype(np.float32)
150
  return df
151
 
152
  df = load_data()
153
 
154
  # --- TRAITEMENT H3 ---
155
  @st.cache_data
156
+ def prepare_h3(_df, resolution, colonne, seuil):
157
+ df = _df.copy()
158
+ df['h3'] = df.apply(lambda r: h3.latlng_to_cell(r.latitude, r.longitude, resolution), axis=1)
159
 
160
+ agg = 'sum' if colonne == 'population' else 'mean'
161
  grouped = df.groupby('h3').agg(
162
+ valeur=(colonne, agg),
163
+ population=('population', 'sum')
 
164
  ).reset_index()
 
165
  grouped = grouped[grouped.valeur >= seuil]
166
+ if grouped.empty: return []
167
+
168
+ top_city = df.loc[df.groupby('h3')['population'].idxmax(), ['h3', 'nom_standard']]
169
+ grouped = grouped.merge(top_city, on='h3', how='left')
170
+ grouped['ville'] = grouped['nom_standard'].fillna("Inconnue")
171
 
172
+ vmax, vmin = grouped.valeur.max(), grouped.valeur.min()
 
173
  grouped['norm'] = 1.0 if vmax == vmin else (grouped.valeur - vmin) / (vmax - vmin)
174
 
175
  data = []
176
  for _, row in grouped.iterrows():
177
  boundary = h3.cell_to_boundary(row.h3)
178
  polygon = [[lon, lat] for lat, lon in boundary]
179
+
180
  ratio = row['norm']
181
+ if ratio < 0.5:
182
+ r = int(100 + 310 * ratio)
183
+ g = int(150 + 210 * ratio)
184
+ b = int(255 * (1 - ratio * 2))
185
+ else:
186
+ r = 255
187
+ g = int(255 * (2 - ratio * 2))
188
+ b = 0
189
+ color = [r, g, b, 220]
190
+
191
  data.append({
192
  "polygon": polygon,
193
+ "ville": str(row.ville),
194
+ "valeur": round(float(row.valeur), 2),
195
  "population": int(row.population),
196
  "color": color,
197
+ "elevation": float(row.valeur) * (10 if colonne == "superficie_km2" else 1)
198
  })
199
+ return data
 
 
200
 
201
+ data = prepare_h3(df, RESOLUTION_H3, COLONNE_VALEUR, SEUIL_MIN)
202
  if not data:
203
+ st.warning("Aucun hexagone – baisser le seuil !")
204
  st.stop()
205
 
206
+ LABEL_MAP = {
207
+ 'population': 'Population',
208
+ 'altitude_moyenne': 'Altitude moyenne (m)',
209
+ 'altitude_minimale': 'Altitude min (m)',
210
+ 'altitude_maximale': 'Altitude max (m)',
211
+ 'superficie_km2': 'Superficie (km²)',
212
+ 'densite': 'Densité (hab/km²)'
213
+ }
214
+ label = LABEL_MAP.get(COLONNE_VALEUR, COLONNE_VALEUR.replace('_', ' ').title())
215
+
216
+ tooltip_html = f"<b>{{ville}}</b><br/>{label} : {{valeur}}"
217
+
218
  layer = pdk.Layer(
219
  "PolygonLayer",
220
  data,
221
  get_polygon="polygon",
222
  get_fill_color="color",
223
  get_elevation="elevation",
224
+ elevation_scale=0.12 if COLONNE_VALEUR == "population" else 1.2,
225
  extruded=True,
226
  wireframe=True,
227
+ pickable=True,
228
+ auto_highlight=True,
229
  line_width_min_pixels=1
230
  )
231
 
232
  deck = pdk.Deck(
233
  layers=[layer],
234
+ initial_view_state=pdk.ViewState(
235
+ latitude=46.5, longitude=2.5, zoom=5.5, pitch=50
236
+ ),
237
+ map_style=MAP_STYLE,
238
+ tooltip={
239
+ "html": tooltip_html,
240
+ "style": {
241
+ "backgroundColor": "rgba(25,40,70,0.85)",
242
+ "color": "white",
243
+ "fontSize": "14px",
244
+ "padding": "12px",
245
+ "borderRadius": "10px",
246
+ "boxShadow": "0 4px 20px rgba(0,0,0,0.5)",
247
+ }
248
+ }
249
  )
250
 
251
+ # --- CARTE DANS UN BEL ENCART ---
252
+ st.markdown("<div class='map-container'>", unsafe_allow_html=True)
253
+ st.pydeck_chart(deck, width='stretch', on_select="ignore")
254
+ st.markdown("</div>", unsafe_allow_html=True)