Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| import h3 | |
| import pydeck as pdk | |
| import os | |
| # --- CONFIG GÉNÉRALE --- | |
| st.set_page_config( | |
| page_title="Visualisation H3 3D optimisée", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # --- CSS GLOBAL BEAUTÉ --- | |
| style = """ | |
| <style> | |
| /* --- TITRE CENTRÉ --- */ | |
| h1 { | |
| font-family: 'Inter', sans-serif; | |
| font-weight: 700; | |
| letter-spacing: -1px; | |
| } | |
| /* --- FOND PAGE --- */ | |
| body { | |
| background: #f4f6fa; | |
| } | |
| /* --- SIDEBAR --- */ | |
| section[data-testid="stSidebar"] { | |
| background: linear-gradient(180deg, #101826 0%, #1d2b45 100%); | |
| color: white; | |
| } | |
| section[data-testid="stSidebar"] * { | |
| color: #e6e8ef !important; | |
| font-family: 'Inter', sans-serif; | |
| } | |
| /* Titres Sidebar */ | |
| section[data-testid="stSidebar"] h1, | |
| section[data-testid="stSidebar"] h2, | |
| section[data-testid="stSidebar"] h3 { | |
| color: #fafafa !important; | |
| } | |
| /* --- SELECTBOX / INPUT --- */ | |
| .stSelectbox, .stNumberInput, .stSlider { | |
| padding-top: 8px; | |
| } | |
| .css-1in5nid, .css-16huue1 { | |
| color: white !important; | |
| } | |
| /* --- TEXTE À L’INTÉRIEUR DES SELECTBOX = #444 --- */ | |
| section[data-testid="stSidebar"] div[data-baseweb="select"] *, | |
| section[data-testid="stSidebar"] .stSelectbox input { | |
| color: #444 !important; | |
| } | |
| /* --- ENCART DE LA CARTE --- */ | |
| .map-container { | |
| background: white; | |
| padding: 0; | |
| border-radius: 16px; | |
| box-shadow: 0 6px 22px rgba(0,0,0,0.15); | |
| margin-top: 10px; | |
| } | |
| /* --- TOOLTIP PYDECK --- */ | |
| .deck-tooltip { | |
| backdrop-filter: blur(6px); | |
| } | |
| section[data-testid="stSidebar"] .stNumberInput input { | |
| color: #444 !important; | |
| } | |
| </style> | |
| """ | |
| st.markdown(style, unsafe_allow_html=True) | |
| # TITRE | |
| titre_centre = """ | |
| <h1 style='text-align: center; color: #1f77b4; margin-bottom:10px;'> | |
| Analyse territoriale avancée | |
| </h1> | |
| """ | |
| st.html(titre_centre) | |
| # --- SIDEBAR --- | |
| with st.sidebar: | |
| st.header("⚙️ Paramètres") | |
| COLONNE_VALEUR = st.selectbox( | |
| 'Caractéristique', | |
| ['population', 'altitude_moyenne', 'altitude_minimale', 'altitude_maximale', 'superficie_km2', 'densite'], | |
| index=0 | |
| ) | |
| RESOLUTION_H3 = st.slider('Résolution H3', 6, 9, 7) | |
| default_seuil = { | |
| 'population': 2000, 'altitude_moyenne': 500, 'altitude_minimale': 500, | |
| 'altitude_maximale': 500, 'superficie_km2': 100, 'densite': 500 | |
| }.get(COLONNE_VALEUR, 100) | |
| SEUIL_MIN = st.number_input(f'Seuil minimum ({COLONNE_VALEUR})', value=default_seuil, step=100) | |
| style_name = st.selectbox( | |
| 'Style de carte', | |
| ['Dark Matter', 'Positron (Clair)', 'Voyager (Coloré)'], | |
| index=0 | |
| ) | |
| MAP_STYLES = { | |
| 'Dark Matter': 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json', | |
| 'Positron (Clair)': 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json', | |
| 'Voyager (Coloré)': 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json' | |
| } | |
| MAP_STYLE = MAP_STYLES[style_name] | |
| # --- CHARGEMENT --- | |
| def load_data(): | |
| if not os.path.exists("Communes_all_fr.parquet"): | |
| st.error("Fichier manquant !") | |
| st.stop() | |
| df = pd.read_parquet("Communes_all_fr.parquet") | |
| df = df[df['superficie_km2'] < 800].copy() | |
| df = df.dropna(subset=['latitude', 'longitude', 'population', 'nom_standard']) | |
| df = df[df['population'] > 0] | |
| if 'reg_code' in df.columns: | |
| df['reg_code'] = df['reg_code'].astype('int8', errors='ignore') | |
| if 'dep_code' in df.columns: | |
| df['dep_code'] = df['dep_code'].astype('float16', errors='ignore') | |
| if 'canton_code' in df.columns: | |
| df['canton_code'] = df['canton_code'].astype('float16', errors='ignore') | |
| df['population'] = df['population'].astype('int32', errors='ignore') | |
| for col in ['superficie_hectare','superficie_km2','densite','altitude_moyenne','altitude_minimale','altitude_maximale']: | |
| if col in df.columns: | |
| df[col] = df[col].astype('float32', errors='ignore') | |
| df['latitude'] = df['latitude'].astype(np.float32) | |
| df['longitude'] = df['longitude'].astype(np.float32) | |
| return df | |
| df = load_data() | |
| # --- TRAITEMENT H3 --- | |
| def prepare_h3(_df, resolution, colonne, seuil): | |
| df = _df.copy() | |
| df['h3'] = df.apply(lambda r: h3.latlng_to_cell(r.latitude, r.longitude, resolution), axis=1) | |
| agg = 'sum' if colonne == 'population' else 'mean' | |
| grouped = df.groupby('h3').agg( | |
| valeur=(colonne, agg), | |
| population=('population', 'sum') | |
| ).reset_index() | |
| grouped = grouped[grouped.valeur >= seuil] | |
| if grouped.empty: return [] | |
| top_city = df.loc[df.groupby('h3')['population'].idxmax(), ['h3', 'nom_standard']] | |
| grouped = grouped.merge(top_city, on='h3', how='left') | |
| grouped['ville'] = grouped['nom_standard'].fillna("Inconnue") | |
| vmax, vmin = grouped.valeur.max(), grouped.valeur.min() | |
| grouped['norm'] = 1.0 if vmax == vmin else (grouped.valeur - vmin) / (vmax - vmin) | |
| data = [] | |
| for _, row in grouped.iterrows(): | |
| boundary = h3.cell_to_boundary(row.h3) | |
| polygon = [[lon, lat] for lat, lon in boundary] | |
| ratio = row['norm'] | |
| if ratio < 0.5: | |
| r = int(100 + 310 * ratio) | |
| g = int(150 + 210 * ratio) | |
| b = int(255 * (1 - ratio * 2)) | |
| else: | |
| r = 255 | |
| g = int(255 * (2 - ratio * 2)) | |
| b = 0 | |
| color = [r, g, b, 220] | |
| data.append({ | |
| "polygon": polygon, | |
| "ville": str(row.ville), | |
| "valeur": round(float(row.valeur), 2), | |
| "population": int(row.population), | |
| "color": color, | |
| "elevation": float(row.valeur) * (10 if colonne == "superficie_km2" else 1) | |
| }) | |
| return data | |
| data = prepare_h3(df, RESOLUTION_H3, COLONNE_VALEUR, SEUIL_MIN) | |
| if not data: | |
| st.warning("Aucun hexagone – baisser le seuil !") | |
| st.stop() | |
| LABEL_MAP = { | |
| 'population': 'Population', | |
| 'altitude_moyenne': 'Altitude moyenne (m)', | |
| 'altitude_minimale': 'Altitude min (m)', | |
| 'altitude_maximale': 'Altitude max (m)', | |
| 'superficie_km2': 'Superficie (km²)', | |
| 'densite': 'Densité (hab/km²)' | |
| } | |
| label = LABEL_MAP.get(COLONNE_VALEUR, COLONNE_VALEUR.replace('_', ' ').title()) | |
| tooltip_html = f"<b>{{ville}}</b><br/>{label} : {{valeur}}" | |
| layer = pdk.Layer( | |
| "PolygonLayer", | |
| data, | |
| get_polygon="polygon", | |
| get_fill_color="color", | |
| get_elevation="elevation", | |
| elevation_scale=0.12 if COLONNE_VALEUR == "population" else 1.2, | |
| extruded=True, | |
| wireframe=True, | |
| pickable=True, | |
| auto_highlight=True, | |
| line_width_min_pixels=1 | |
| ) | |
| deck = pdk.Deck( | |
| layers=[layer], | |
| initial_view_state=pdk.ViewState( | |
| latitude=46.5, longitude=2.5, zoom=5.5, pitch=50 | |
| ), | |
| map_style=MAP_STYLE, | |
| tooltip={ | |
| "html": tooltip_html, | |
| "style": { | |
| "backgroundColor": "rgba(25,40,70,0.85)", | |
| "color": "white", | |
| "fontSize": "14px", | |
| "padding": "12px", | |
| "borderRadius": "10px", | |
| "boxShadow": "0 4px 20px rgba(0,0,0,0.5)", | |
| } | |
| } | |
| ) | |
| # --- CARTE DANS UN BEL ENCART --- | |
| st.markdown("<div class='map-container'>", unsafe_allow_html=True) | |
| st.pydeck_chart(deck, width='stretch', on_select="ignore") | |
| st.markdown("</div>", unsafe_allow_html=True) | |