# IMPORTS import os import streamlit as st import requests import datetime import pytz from dotenv import load_dotenv import pandas as pd import numpy as np import base64 from streamlit_searchbox import st_searchbox import plotly.graph_objects as go import plotly.express as px from plotly.subplots import make_subplots import streamlit.components.v1 as components # CHARGEMENT TXT PROVERBES ET SAINTS def charger_dictionnaire_depuis_fichier(chemin_fichier): dictionnaire = {} if os.path.exists(chemin_fichier): with open(chemin_fichier, "r", encoding="utf-8") as f: for ligne in f: ligne = ligne.strip() # On ignore les lignes vides ou les commentaires (ex: débutant par #) if not ligne or ligne.startswith("#"): continue if ":" in ligne: cle, valeur = ligne.split(":", 1) cle = cle.strip() if cle.isdigit(): dictionnaire[int(cle)] = valeur.strip() else: dictionnaire[cle] = valeur.strip() return dictionnaire # LECTURE LOGO def extraire_image_locale_html(chemin_image, hauteur=10, style_additionnel=""): try: if os.path.exists(chemin_image): with open(chemin_image, "rb") as image_file: encoded_string = base64.b64encode(image_file.read()).decode() ext = chemin_image.lower().split('.')[-1] type_mime = "image/jpeg" if ext in ["jpg", "jpeg"] else "image/png" return f'' else: return "" except Exception: return "" # LISTE DES VILLES def lister_villes_api_geo(search_term: str) -> list[tuple[str, str]]: if not search_term or len(search_term.strip()) < 2: return [ ("Aubigny-en-Artois (62)", "Aubigny-en-Artois"), ("Paris (75)", "Paris"), ("Lyon (69)", "Lyon"), ("Marseille (13)", "Marseille"), ("Lille (59)", "Lille") ] try: url = f"https://geo.api.gouv.fr/communes?nom={search_term.strip()}&limit=10&fields=nom,codesPostaux" reponse = requests.get(url, timeout=2) if reponse.status_code == 200: communes = reponse.json() suggestions = [] for c in communes: dep = c['codesPostaux'][0][:2] if c.get('codesPostaux') else "" label_affichage = f"{c['nom']} ({dep})" if dep else c['nom'] suggestions.append((label_affichage, c['nom'])) return suggestions except Exception: pass return [("Aubigny-en-Artois (62)", "Aubigny-en-Artois")] # ARRONDI def arrondir_a_5(valeur): if valeur == "--" or valeur is None: return "--" return int(5 * round(float(valeur) / 5)) def arrondir_a_05(valeur): if valeur == "--" or valeur is None: return "--" try: if isinstance(valeur, str): valeur = valeur.replace("°C", "").replace("", "").replace("", "").strip() num = float(valeur) arrondi = round(num * 2) / 2 if arrondi.is_integer(): return f"{int(arrondi)}" return f"{arrondi}" except Exception: return "--" # FORMATAGE DATE def formater_date_fr(date_str): date_obj = datetime.datetime.strptime(date_str, "%Y-%m-%d") j_en, m_en, num = date_obj.strftime("%a"), date_obj.strftime("%b"), date_obj.strftime("%d") return f"{JOURS_FR.get(j_en, j_en)}. {num} {MOIS_FR.get(m_en, m_en)}" # NETTOYAGE EXTRACTION def extraire_valeur_numerique(valeur): if valeur is None or valeur == "--": return None try: # Retire l'unité si elle est présente et convertit en float nettoye = str(valeur).replace("°C", "").replace("%", "").replace("km/h", "").strip() return float(nettoye) except ValueError: return None # CALCUL ICONE def est_la_nuit(heure_int): return heure_int >= 20 or heure_int < 7 def determiner_icone(description_ou_code, heure_int=12, source="text"): nuit = est_la_nuit(heure_int) text = str(description_ou_code).lower() if source == "mc_code": code = int(description_ou_code) if code == 0: return "🌙" if nuit else "☀️" if code in [1, 2, 3]: return "☁️" if nuit else "⛅" if code in [4, 5, 7]: return "☁️" if code in [10, 11, 12, 14, 15, 16, 40, 41, 42, 43, 44, 45, 46, 47, 48, 210, 211, 212]: return "🌧️" if code in [20, 21, 22, 30, 31, 32, 60, 61, 62, 63, 64, 65, 66, 67, 68, 70, 71, 72, 73, 74, 75, 76, 77, 78, 220, 221, 222, 230, 231, 232, 235]: return "❄️" if code in [100, 101, 102, 103, 120, 121, 122, 130, 131, 132, 140, 141, 142]: return "🌩️" return "⛅" if "clair" in text or "clear" in text or "soleil" in text: return "🌙" if nuit else "☀️" if "nuage" in text or "clouds" in text or "couvert" in text or "brume" in text or "fog" in text or "overcast" in text or "mist" in text: return "☁️" if nuit else "⛅" if "pluie" in text or "rain" in text or "bruine" in text or "drizzle" in text or "patchy rain" in text: return "🌧️" if "neige" in text or "snow" in text or "gargrele" in text: return "❄️" if "orage" in text or "thunderstorm" in text or "thundery" in text: return "🌩️" return "⛅" # DIRECTION VENT def direction_en_fleche(direction_str): return DIR_VERS_FLECHE.get(direction_str, "--") def degres_en_direction(deg): if deg is None or deg == "--": return "--" deg = float(deg) % 360 directions = ["N", "NE", "E", "SE", "S", "SO", "O", "NO"] return directions[int((deg + 22.5) / 45) % 8] # MOYENNE def calculer_moyenne_numerique(liste_valeurs, suffixe=""): valeurs_propres = [] for v in liste_valeurs: if v is None or v == "--": continue if isinstance(v, (int, float)): valeurs_propres.append(float(v)) elif isinstance(v, str): try: nettoie = v.replace("°C", "").replace("%", "").replace("km/h", "").replace("mm", "") nettoie = nettoie.replace("", "").replace("", "").strip() if nettoie: valeurs_propres.append(float(nettoie)) except ValueError: pass if not valeurs_propres: return "--" moyenne = sum(valeurs_propres) / len(valeurs_propres) if suffixe == "km/h": return f"{int(5 * round(moyenne / 5))}" elif suffixe == "%": return f"{round(moyenne)}%" elif suffixe == "°C": return arrondir_a_05(moyenne) elif suffixe == "mm": return f"{round(moyenne, 1)} mm" else: return f"{round(moyenne, 1)}" def calculer_moyenne_direction(liste_directions): angles = [DIR_VERS_DEG[d] for d in liste_directions if d in DIR_VERS_DEG] if not angles: return "--" rad = np.deg2rad(angles) return degres_en_direction(np.rad2deg(np.arctan2(np.mean(np.sin(rad)), np.mean(np.cos(rad)))) % 360) # RECHERCHE API @st.cache_data(ttl=3600) def geocoder_openmeteo(ville_nom): try: url = "https://geocoding-api.open-meteo.com/v1/search" res = requests.get(url, params={"name": ville_nom, "count": 1, "language": "fr"}, timeout=5) if res.status_code == 200 and "results" in res.json(): return res.json()["results"][0] except Exception: pass return None @st.cache_data(ttl=3600) def fetch_openweather(ville_nom, api_key): if not api_key: return None try: url = "https://api.openweathermap.org/data/2.5/forecast" params = {'q': ville_nom, 'appid': api_key, 'units': 'metric', 'lang': 'fr'} res = requests.get(url, params=params, timeout=5) return res.json() if res.status_code == 200 else None except Exception: return None @st.cache_data(ttl=3600) def fetch_openmeteo_global(lat, lon): try: url = "https://api.open-meteo.com/v1/forecast" params = { "latitude": lat, "longitude": lon, "hourly": ["temperature_2m", "wind_speed_10m", "wind_direction_10m", "weather_code", "precipitation_probability", "precipitation"], "daily": ["temperature_2m_min", "temperature_2m_max", "wind_speed_10m_max", "wind_gusts_10m_max", "precipitation_probability_max", "precipitation_sum"], "forecast_days": 16, "timezone": "auto" } res = requests.get(url, params=params, timeout=5) return res.json() if res.status_code == 200 else None except Exception: return None @st.cache_data(ttl=3600) def fetch_openmeteo_meteofrance(lat, lon): try: url = "https://api.open-meteo.com/v1/meteofrance" params = {"latitude": lat, "longitude": lon, "hourly": ["temperature_2m", "wind_speed_10m", "wind_direction_10m", "wind_gusts_10m", "weather_code", "precipitation_probability", "precipitation"], "timezone": "auto"} res = requests.get(url, params=params, timeout=5) return res.json() if res.status_code == 200 else None except Exception: return None @st.cache_data(ttl=3600) def fetch_openmeteo_gfs(lat, lon): try: url = "https://api.open-meteo.com/v1/gfs" params = { "latitude": lat, "longitude": lon, "hourly": ["temperature_2m", "wind_speed_10m", "wind_direction_10m", "weather_code", "precipitation_probability", "precipitation"], "daily": ["temperature_2m_min", "temperature_2m_max", "wind_speed_10m_max", "precipitation_sum"], "forecast_days": 16, "timezone": "auto" } res = requests.get(url, params=params, timeout=5) return res.json() if res.status_code == 200 else None except Exception: return None @st.cache_data(ttl=3600) def fetch_meteoconcept(lat, lon, token): if not token: return None try: url = f"https://api.meteo-concept.com/api/forecast/daily?token={token}&latlng={lat},{lon}" res = requests.get(url, timeout=5) if res.status_code == 200: return res.json() elif res.status_code == 429: st.sidebar.error("⚠️ Météo-Concept : Limite d'appels dépassée.") elif res.status_code == 403: st.sidebar.error("⚠️ Météo-Concept : Quota journalier épuisé.") except Exception: pass return None @st.cache_data(ttl=3600) def fetch_weatherapi(ville_nom, api_key): if not api_key: return None try: url = "https://api.weatherapi.com/v1/forecast.json" # Demande de 10 jours de prévisions (limite maximale gratuite classique de l'API) params = {'key': api_key, 'q': ville_nom, 'days': 10, 'aqi': 'no', 'alerts': 'no'} res = requests.get(url, params=params, timeout=5) return res.json() if res.status_code == 200 else None except Exception: return None # INIT CHEMIN_CONFIG = "config" JOURS_FR = {"Mon": "Lun", "Tue": "Mar", "Wed": "Mer", "Thu": "Jeu", "Fri": "Ven", "Sat": "Sam", "Sun": "Dim"} MOIS_FR = {"Jan": "janv.", "Feb": "févr.", "Mar": "mars", "Apr": "avril", "May": "mai", "Jun": "juin", "Jul": "juil.", "Aug": "août", "Sep": "sept.", "Oct": "oct.", "Nov": "nov.", "Dec": "déc."} DIR_VERS_FLECHE = {"N": "↓", "NE": "↙", "E": "←", "SE": "↖", "S": "↑", "SO": "↗", "O": "→", "NO": "↘", "--": "--"} DIR_VERS_DEG = {"N": 0, "NE": 45, "E": 90, "SE": 135, "S": 180, "SO": 225, "O": 270, "NO": 315} tz_paris = pytz.timezone('Europe/Paris') maintenant = datetime.datetime.now(tz_paris) SAINTS = charger_dictionnaire_depuis_fichier(os.path.join(CHEMIN_CONFIG, "saints.txt")) PROVERBES_JOURS = charger_dictionnaire_depuis_fichier(os.path.join(CHEMIN_CONFIG, "proverbes_jour.txt")) PROVERBES_MOIS = charger_dictionnaire_depuis_fichier(os.path.join(CHEMIN_CONFIG, "proverbes_mois.txt")) wmo_codes = {0: "Clair", 1: "Peu nuageux", 2: "Nuageux", 3: "Couvert", 45: "Brouillard", 51: "Bruine", 61: "Pluie", 71: "Neige", 80: "Averses", 95: "Orage"} # CHARGEMENT DES CLÉS API & TOKENS api_key_ow = None token_mc = None api_key_wa = None config_dir = "config" config_files = ["openweather.env", "meteoconcept.env", "weatherapi.env"] for file in config_files: file_path = os.path.join(config_dir, file) if os.path.exists(file_path): load_dotenv(dotenv_path=file_path, override=True) api_key_ow = os.environ.get("OPENWEATHER_API_KEY") token_mc = os.environ.get("METEOCONCEPT_API_KEY") api_key_wa = os.environ.get("WEATHERAPI_API_KEY") if "ville_memorisee" not in st.session_state: st.session_state["ville_memorisee"] = "Aubigny-en-Artois" # CONFIGURATION DE LA PAGE st.set_page_config(page_title="TOM", page_icon="🐸", layout="centered") # Injection CSS Global Harmonisé st.markdown( """ """, unsafe_allow_html=True ) # EN-TÊTE : LOGO TRANSPARENT + ALIGNEMENT DU TEXTE chemin_logo = os.path.join("images", "logo.png") # --- 1. VOS STYLES CSS GLOBAUX --- st.markdown( """ """, unsafe_allow_html=True ) # --- 2. LOGIQUE D'AFFICHAGE DU LOGO + TITRE --- titre_texte = "Temps & Observations Météo" # Cette ligne prend chaque mot, met la 1ère lettre entre et , et recolle le tout titre_formate = " ".join([ f"{mot[0]}{mot[1:]}" if mot != "&" else mot for mot in titre_texte.split() ]) # --- 2. AFFICHAGE DANS VOTRE BLOC HTML --- if os.path.exists(chemin_logo): with open(chemin_logo, "rb") as f: data_logo = base64.b64encode(f.read()).decode("utf-8") st.markdown( f"""

{titre_formate}

""", unsafe_allow_html=True ) else: st.markdown( f"""
🐸

{titre_formate}

""", unsafe_allow_html=True ) #prev, detail, visu, precip, temperature, vent, serie = st.tabs(["🔮 Prévisions", "🔍 Détails", "📈 Visualisation", "☔ Précipitations", "🌡️ Températures", "💨 Vent", "📅 Série"], ) prev, detail, visu, precip, temperature, vent, serie = st.tabs(["🔮 ", "🔍 ", "📈 ", "☔ ", "🌡️ ", "💨 ", "📅 "], ) with prev: query_params = st.query_params if "day" in query_params: st.session_state.jour_selectionne = query_params["day"] if "ville" in query_params: st.session_state["ville_selectionnee"] = query_params["ville"] elif "ville_selectionnee" not in st.session_state: st.session_state["ville_selectionnee"] = "Aubigny-en-Artois" # RECHERCHE VILLE ville_actuelle = st.session_state.get("ville_selectionnee", "Aubigny-en-Artois") # 2. Affichage de la searchbox ville_saisie = st_searchbox( search_function=lister_villes_api_geo, placeholder="Tapez le nom d'une ville (ex: Mont, Aubi, Pari...)", default=ville_actuelle, key="recherche_ville_searchbox" ) if ville_saisie and ville_saisie != ville_actuelle: st.session_state["ville_selectionnee"] = ville_saisie # On l'injecte immédiatement dans l'URL du navigateur st.query_params["ville"] = ville_saisie st.rerun() ville = st.session_state["ville_selectionnee"] if not ville: ville = "Aubigny-en-Artois" nom_ville_officiel = ville geo_data = geocoder_openmeteo(nom_ville_officiel) if geo_data: lat = geo_data['latitude'] lon = geo_data['longitude'] nom_ville_officiel = geo_data['name'] else: st.sidebar.warning("⚠️ Impossible de géolocaliser précisément via Open-Meteo. Coordonnées par défaut appliquées.") # Recup json API data_om = fetch_openmeteo_global(lat, lon) data_mf = fetch_openmeteo_meteofrance(lat, lon) data_gfs = fetch_openmeteo_gfs(lat, lon) data_ow = fetch_openweather(ville, api_key_ow) toutes_previsions_mc = fetch_meteoconcept(lat, lon, token_mc) data_wa = fetch_weatherapi(ville, api_key_wa) donnees_meteo_harmonisees = {} # Météo-France tgros_mf, tmin_mf, tmax_mf, vent_mf, raf_mf, dir_mf, proba_pluie_mf, qte_pluie_mf, icone_mf = "--", "--", "--", "--", "--", "--", "--", "--", "--" # Open-Meteo tgros_om, tmin_om, tmax_om, vent_om, raf_om, dir_om, proba_pluie_om, qte_pluie_om, icone_om = "--", "--", "--", "--", "--", "--", "--", "--", "--" # OpenWeatherMap tgros_ow, tmin_ow, tmax_ow, vent_ow, raf_ow, dir_ow, proba_pluie_ow, qte_pluie_ow, icone_ow = "--", "--", "--", "--", "--", "--" , "--", "--", "--" # GFS tgros_gfs, tmin_gfs, tmax_gfs, vent_gfs, raf_gfs, dir_gfs, proba_pluie_gfs, qte_pluie_gfs, icone_gfs = "--", "--", "--", "--", "--", "--", "--", "--", "--" # MeteoConcept (mc) tgros_mc, tmin_mc, tmax_mc, vent_mc, raf_mc, dir_mc, proba_pluie_mc, qte_pluie_mc, icone_mc = "--", "--", "--", "--", "--", "--", "--", "--", "--" # WeatherAPI (wa) tgros_wa, tmin_wa, tmax_wa, vent_wa, raf_wa, dir_wa, proba_pluie_wa, qte_pluie_wa,icone_wa = "--", "--", "--", "--", "--", "--", "--", "--", "--" dates_possibles = [] # Depuis Open-Meteo (data_om) if data_om and "hourly" in data_om and "time" in data_om["hourly"]: dates_possibles.extend([t.split("T")[0] for t in data_om['hourly']['time']]) # Depuis Météo-France (data_mf) si présent if data_mf and "hourly" in data_mf and "time" in data_mf["hourly"]: dates_possibles.extend([t.split("T")[0] for t in data_mf['hourly']['time']]) # Depuis OpenWeather (data_ow) si présent et si Open-Meteo avait échoué if data_ow and "list" in data_ow: dates_possibles.extend([item['dt_txt'].split(" ")[0] for item in data_ow['list']]) # Depuis WeatherAPI (data_wa) si présent if data_wa and "forecast" in data_wa and "forecastday" in data_wa["forecast"]: dates_possibles.extend([day_item['date'] for day_item in data_wa['forecast']['forecastday']]) # 2. Création de la liste unique et triée (avec sécurité si TOUTES les APIs échouent) if dates_possibles: dates_uniques = sorted(list(set(dates_possibles))) else: # Fallback ultime : si aucune API ne répond, on met au moins la date du jour dates_uniques = [datetime.datetime.now().strftime("%Y-%m-%d")] heure_actuelle_int = int(datetime.datetime.now().strftime("%H")) date_aujourdhui_paris = maintenant.strftime("%Y-%m-%d") if "jour_selectionne" not in st.session_state: # Si la date du jour est dans les dates fournies par l'API, on la prend, sinon on prend la 1ère dispo if date_aujourdhui_paris in dates_uniques: st.session_state.jour_selectionne = date_aujourdhui_paris else: st.session_state.jour_selectionne = dates_uniques[0] if dates_uniques else date_aujourdhui_paris elif st.session_state.jour_selectionne not in dates_uniques: if date_aujourdhui_paris in dates_uniques: st.session_state.jour_selectionne = date_aujourdhui_paris else: st.session_state.jour_selectionne = dates_uniques[0] if "valeur_afficher_details" not in st.session_state: st.session_state.valeur_afficher_details = False journee = st.session_state.jour_selectionne i = dates_uniques.index(journee) if journee in dates_uniques else 0 est_aujourdhui = (i == 0) if est_aujourdhui: heure_cible_int = heure_actuelle_int else: heure_cible_int = 12 if est_aujourdhui: heure_cible_int = heure_actuelle_int libelle_colonne_temp = f"🌡️
T° ({heure_actuelle_int}h)" texte_moment = f"{formater_date_fr(journee)} | {heure_actuelle_int}h" else: heure_cible_int = 12 libelle_colonne_temp = "🌡️
T° à 12h" texte_moment = f"{formater_date_fr(journee)} | 12h" jours_distincts = [] for i in range(14): futur_dt = maintenant + pd.Timedelta(days=i) jours_distincts.append(futur_dt.strftime("%Y-%m-%d")) donnees_meteo_harmonisees = {} for idx_j, jour_j in enumerate(jours_distincts): i = dates_uniques.index(jour_j) if jour_j in dates_uniques else 0 est_aujourdhui = (i == 0) if est_aujourdhui: heure_cible_int = heure_actuelle_int else: heure_cible_int = 12 #st.write (f"recherche jour {idx_j}-{jour_j}-{heure_cible_int}") # --- EXTRACTION MÉTÉO-FRANCE --- tgros_mf, tmin_mf, tmax_mf, vent_mf, raf_mf, dir_mf, icone_mf, proba_pluie_mf, qte_pluie_mf = "--", "--", "--", "--", "--", "--", "--", "--", "--" indices_mf = [] if data_mf and "hourly" in data_mf: try: heures_mf = data_mf['hourly']['time'] indices_mf = [idx for idx, t in enumerate(heures_mf) if t.split("T")[0] == jour_j] if indices_mf: idx_proche_mf = min(indices_mf, key=lambda idx: abs(int(heures_mf[idx].split("T")[1][:2]) - heure_cible_int)) if data_mf['hourly']['temperature_2m'][idx_proche_mf] is not None: tgros_mf = arrondir_a_05(data_mf['hourly']['temperature_2m'][idx_proche_mf]) dir_mf = degres_en_direction(data_mf['hourly']['wind_direction_10m'][idx_proche_mf]) icone_mf = determiner_icone(wmo_codes.get(data_mf['hourly']['weather_code'][idx_proche_mf], "nuage"), heure_cible_int) temps_mf_jour = [data_mf['hourly']['temperature_2m'][idx] for idx in indices_mf if data_mf['hourly']['temperature_2m'][idx] is not None] vents_mf_jour = [data_mf['hourly']['wind_speed_10m'][idx] for idx in indices_mf if data_mf['hourly']['wind_speed_10m'][idx] is not None] raf_mf_jour = [data_mf['hourly']['wind_gusts_10m'][idx] for idx in indices_mf if 'wind_gusts_10m' in data_mf['hourly'] and data_mf['hourly']['wind_gusts_10m'][idx] is not None] pluies_mf_jour = [] p_valides = [] for idx in indices_mf: hr_bloc = int(heures_mf[idx].split("T")[1][:2]) if est_aujourdhui and hr_bloc < heure_actuelle_int: continue if 'precipitation' in data_mf['hourly'] and data_mf['hourly']['precipitation'][idx] is not None: pluies_mf_jour.append(data_mf['hourly']['precipitation'][idx]) if "precipitation_probability" in data_mf['hourly'] and data_mf['hourly']['precipitation_probability'][idx] is not None: p_valides.append(data_mf['hourly']['precipitation_probability'][idx]) if temps_mf_jour: tmin_mf = arrondir_a_05(min(temps_mf_jour)) tmax_mf = arrondir_a_05(max(temps_mf_jour)) if vents_mf_jour: vent_mf = f"{arrondir_a_5(sum(vents_mf_jour) / len(vents_mf_jour))}" if raf_mf_jour: raf_mf = f"{arrondir_a_5(max(raf_mf_jour))}" if pluies_mf_jour: qte_pluie_mf = f"{round(sum(pluies_mf_jour), 1)} mm" if p_valides: proba_pluie_mf = f"{max(p_valides)}%" if qte_pluie_mf == "0.0 mm": proba_pluie_mf = "0%" except Exception: pass tgros_om, tmin_om, tmax_om, vent_om, raf_om, dir_om, icone_om, proba_pluie_om, qte_pluie_om = "--", "--", "--", "--", "--", "--", "--", "--", "--" indices_om = [] if data_om and "hourly" in data_om: try: heures_om = data_om['hourly']['time'] indices_om = [idx for idx, t in enumerate(heures_om) if t.split("T")[0] == jour_j] if indices_om: idx_proche_om = min(indices_om, key=lambda idx: abs(int(heures_om[idx].split("T")[1][:2]) - heure_cible_int)) if data_om['hourly']['temperature_2m'][idx_proche_om] is not None: tgros_om = arrondir_a_05(data_om['hourly']['temperature_2m'][idx_proche_om]) dir_om = degres_en_direction(data_om['hourly']['wind_direction_10m'][idx_proche_om]) icone_om = determiner_icone(wmo_codes.get(data_om['hourly']['weather_code'][idx_proche_om], "nuage"), heure_cible_int) temps_om_jour = [data_om['hourly']['temperature_2m'][idx] for idx in indices_om if data_om['hourly']['temperature_2m'][idx] is not None] vents_om_jour = [data_om['hourly']['wind_speed_10m'][idx] for idx in indices_om if data_om['hourly']['wind_speed_10m'][idx] is not None] if temps_om_jour: tmin_om = arrondir_a_05(min(temps_om_jour)) tmax_om = arrondir_a_05(max(temps_om_jour)) if vents_om_jour: vent_om = f"{arrondir_a_5(sum(vents_om_jour) / len(vents_om_jour))}" pluies_om_jour = [] probas_om = [] for idx in indices_om: hr_bloc = int(heures_om[idx].split("T")[1][:2]) if est_aujourdhui and hr_bloc < heure_actuelle_int: continue if 'precipitation' in data_om['hourly'] and data_om['hourly']['precipitation'][idx] is not None: pluies_om_jour.append(data_om['hourly']['precipitation'][idx]) if 'precipitation_probability' in data_om['hourly'] and data_om['hourly']['precipitation_probability'][idx] is not None: probas_om.append(data_om['hourly']['precipitation_probability'][idx]) if pluies_om_jour: qte_pluie_om = f"{round(sum(pluies_om_jour), 1)} mm" if probas_om: proba_pluie_om = f"{max(probas_om)}%" if qte_pluie_om == "0.0 mm": proba_pluie_om = "0%" except Exception: pass # --- EXTRACTION OPENWEATHER --- ow_par_jour = {} if data_ow and 'list' in data_ow: for bloc in data_ow['list']: j_b = bloc['dt_txt'].split(" ")[1][:2] dt_b = bloc['dt_txt'].split(" ")[0] if dt_b not in ow_par_jour: ow_par_jour[dt_b] = [] ow_par_jour[dt_b].append(bloc) tgros_ow, tmin_ow, tmax_ow, vent_ow, raf_ow, dir_ow, icone_ow, proba_pluie_ow, qte_pluie_ow = "--", "--", "--", "--", "--", "--", "--", "--", "--" if jour_j in ow_par_jour: try: temps_blocs = [b['main']['temp'] for b in ow_par_jour[jour_j] if b['main'].get('temp') is not None] vents_blocs = [b['wind']['speed'] * 3.6 for b in ow_par_jour[jour_j] if b['wind'].get('speed') is not None] raf_blocs = [b['wind']['gust'] * 3.6 for b in ow_par_jour[jour_j] if b['wind'].get('gust') is not None] proba_blocs = [] pluies_ow_jour = [] for b in ow_par_jour[jour_j]: hr_bloc = int(b['dt_txt'].split(" ")[1][:2]) if est_aujourdhui and hr_bloc < heure_actuelle_int: continue proba_blocs.append(b.get('pop', 0) * 100) if 'rain' in b and '3h' in b['rain']: pluies_ow_jour.append(b['rain']['3h']) if temps_blocs: tmin_ow = arrondir_a_05(min(temps_blocs)) tmax_ow = arrondir_a_05(max(temps_blocs)) if vents_blocs: vent_ow = f"{arrondir_a_5(sum(vents_blocs) / len(vents_blocs))}" if raf_blocs: raf_ow = f"{arrondir_a_5(max(raf_blocs))}" if proba_blocs: proba_pluie_ow = f"{round(max(proba_blocs))}%" if pluies_ow_jour: qte_pluie_ow = f"{round(sum(pluies_ow_jour), 1)} mm" if qte_pluie_ow == "0.0 mm": proba_pluie_ow = "0%" proche_ow = min(ow_par_jour[jour_j], key=lambda x: abs(int(x['dt_txt'].split(" ")[1][:2]) - heure_cible_int)) if proche_ow['main'].get('temp') is not None: tgros_ow = arrondir_a_05(proche_ow['main']['temp']) dir_ow = degres_en_direction(proche_ow['wind'].get('deg')) icone_ow = determiner_icone(proche_ow['weather'][0]['description'], heure_cible_int) except Exception: pass # --- EXTRACTION GFS --- tgros_gfs, tmin_gfs, tmax_gfs, vent_gfs, raf_gfs, dir_gfs, icone_gfs, proba_pluie_gfs, qte_pluie_gfs = "--", "--", "--", "--", "--", "--", "--", "--", "--" indices_gfs = [] if data_gfs and "hourly" in data_gfs: try: heures_gfs = data_gfs['hourly']['time'] indices_gfs = [idx for idx, t in enumerate(heures_gfs) if t.split("T")[0] == jour_j] if indices_gfs: idx_proche_gfs = min(indices_gfs, key=lambda idx: abs(int(heures_gfs[idx].split("T")[1][:2]) - heure_cible_int)) if data_gfs['hourly']['temperature_2m'][idx_proche_gfs] is not None: tgros_gfs = arrondir_a_05(data_gfs['hourly']['temperature_2m'][idx_proche_gfs]) dir_gfs = degres_en_direction(data_gfs['hourly']['wind_direction_10m'][idx_proche_gfs]) icone_gfs = determiner_icone(wmo_codes.get(data_gfs['hourly']['weather_code'][idx_proche_gfs], "nuage"), heure_cible_int) temps_gfs_jour = [data_gfs['hourly']['temperature_2m'][idx] for idx in indices_gfs if data_gfs['hourly']['temperature_2m'][idx] is not None] vents_gfs_jour = [data_gfs['hourly']['wind_speed_10m'][idx] for idx in indices_gfs if data_gfs['hourly']['wind_speed_10m'][idx] is not None] if temps_gfs_jour: tmin_gfs = arrondir_a_05(min(temps_gfs_jour)) tmax_gfs = arrondir_a_05(max(temps_gfs_jour)) if vents_gfs_jour: vent_gfs = f"{arrondir_a_5(sum(vents_gfs_jour) / len(vents_gfs_jour))}" pluies_gfs_jour = [] probas_gfs = [] for idx in indices_gfs: hr_bloc = int(heures_gfs[idx].split("T")[1][:2]) if est_aujourdhui and hr_bloc < heure_actuelle_int: continue if 'precipitation' in data_gfs['hourly'] and data_gfs['hourly']['precipitation'][idx] is not None: pluies_gfs_jour.append(data_gfs['hourly']['precipitation'][idx]) if 'precipitation_probability' in data_gfs['hourly'] and data_gfs['hourly']['precipitation_probability'][idx] is not None: probas_gfs.append(data_gfs['hourly']['precipitation_probability'][idx]) if pluies_gfs_jour: qte_pluie_gfs = f"{round(sum(pluies_gfs_jour), 1)} mm" if probas_gfs: proba_pluie_gfs = f"{max(probas_gfs)}%" if qte_pluie_gfs == "0.0 mm": proba_pluie_gfs = "0%" except Exception: pass # --- EXTRACTION MÉTÉO-CONCEPT --- tgros_mc, tmin_mc, tmax_mc, vent_mc, raf_mc, dir_mc, icone_mc, proba_pluie_mc, qte_pluie_mc = "--", "--", "--", "--", "--", "--", "--", "--", "--" donnees_mc = None if toutes_previsions_mc and "forecast" in toutes_previsions_mc: try: for f in toutes_previsions_mc['forecast']: if f['datetime'].split("T")[0] == jour_j: donnees_mc = f break if donnees_mc: if donnees_mc.get('tmin') is not None: tmin_mc = arrondir_a_05(donnees_mc['tmin']) if donnees_mc.get('tmax') is not None: tmax_mc = arrondir_a_05(donnees_mc['tmax']) if donnees_mc.get('wind10m') is not None: vent_mc = f"{arrondir_a_5(donnees_mc['wind10m'])}" if donnees_mc.get('gust10m') is not None: raf_mc = f"{arrondir_a_5(donnees_mc['gust10m'])}" dir_mc = degres_en_direction(donnees_mc.get('dirwind10m')) icone_mc = determiner_icone(donnees_mc.get('weather', 0), heure_cible_int, source="mc_code") if est_aujourdhui and heure_actuelle_int >= 18: proba_pluie_mc, qte_pluie_mc = "--", "--" else: if donnees_mc.get('probarain') is not None: proba_pluie_mc = f"{donnees_mc['probarain']}%" if donnees_mc.get('rr10') is not None: qte_pluie_mc = f"{round(donnees_mc['rr10'], 1)} mm" if qte_pluie_mc == "0.0 mm": proba_pluie_mc = "0%" except Exception: pass # --- EXTRACTION WEATHERAPI --- wa_par_jour = {} if data_wa and 'forecast' in data_wa and 'forecastday' in data_wa['forecast']: for f_day in data_wa['forecast']['forecastday']: wa_par_jour[f_day['date']] = f_day tgros_wa, tmin_wa, tmax_wa, vent_wa, raf_wa, dir_wa, icone_wa, proba_pluie_wa, qte_pluie_wa = "--", "--", "--", "--", "--", "--", "--", "--", "--" if jour_j in wa_par_jour: try: donnees_wa_jour = wa_par_jour[jour_j] tmin_wa = arrondir_a_05(donnees_wa_jour['day']['mintemp_c']) tmax_wa = arrondir_a_05(donnees_wa_jour['day']['maxtemp_c']) qte_pluie_wa = f"{round(donnees_wa_jour['day']['totalprecip_mm'], 1)} mm" proba_pluie_wa = f"{donnees_wa_jour['day']['daily_chance_of_rain']}%" vent_wa = f"{arrondir_a_5(donnees_wa_jour['day']['maxwind_kph'])}" proche_wa = min(donnees_wa_jour['hour'], key=lambda x: abs(int(x['time'].split(" ")[1][:2]) - heure_cible_int)) tgros_wa = arrondir_a_05(proche_wa['temp_c']) vent_wa = f"{arrondir_a_5(proche_wa['wind_kph'])}" raf_wa = f"{arrondir_a_5(proche_wa['gust_kph'])}" dir_wa = degres_en_direction(proche_wa['wind_degree']) icone_wa = determiner_icone(proche_wa['condition']['text'], heure_cible_int) if qte_pluie_wa == "0.0 mm": proba_pluie_wa = "0%" except Exception: pass #st.write (f"Recup MF : {tmin_mf}° {tmax_mf}° {vent_mf} {raf_mf}km/h {qte_pluie_mf}{proba_pluie_mf}") #st.write (f"Recup OM : {tmin_om}° {tmax_om}° {vent_om} {raf_om}km/h {qte_pluie_om}{proba_pluie_om}") #st.write (f"Recup OW : {tmin_ow}° {tmax_ow}° {vent_ow} {raf_ow}km/h {qte_pluie_ow}{proba_pluie_ow}") #st.write (f"Recup GFS : {tmin_gfs}° {tmax_gfs}° {vent_gfs} {raf_gfs}km/h {qte_pluie_gfs}{proba_pluie_gfs}") #st.write (f"Recup MC : {tmin_mc}° {tmax_mc}° {vent_mc} {raf_mc}km/h {qte_pluie_mc}{proba_pluie_mc}") #st.write (f"Recup WA : {tmin_wa}° {tmax_wa}° {vent_wa} {raf_wa}km/h {qte_pluie_wa}{proba_pluie_wa} {icone_wa}") #st.write(f"Min MF {tmin_mf}° OM {tmin_om}° OW {tmin_ow}° GFS {tmin_gfs}° MC {tmin_mc}° WA {tmin_wa}°") #st.write(f"Max MF {tmax_mf}° OM {tmax_om}° OW {tmax_ow}° GFS {tmax_gfs}° MC {tmax_mc}° WA {tmax_wa}°") #st.write(f"Ico MF {icone_mf} OM {icone_om} OW {icone_ow} GFS {icone_gfs} MC {icone_mc} WA {icone_wa}") top_tgros = calculer_moyenne_numerique([tgros_mf, tgros_om, tgros_ow, tgros_gfs, tgros_mc, tgros_wa], suffixe="°C") top_tmin = calculer_moyenne_numerique([tmin_mf, tmin_om, tmin_ow, tmin_gfs, tmin_mc, tmin_wa], suffixe="°C") top_tmax = calculer_moyenne_numerique([tmax_mf, tmax_om, tmax_ow, tmax_gfs, tmax_mc, tmax_wa], suffixe="°C") top_vent = calculer_moyenne_numerique([vent_mf, vent_om, vent_ow, vent_gfs, vent_mc, vent_wa], suffixe="km/h") top_raf = calculer_moyenne_numerique([raf_mf, raf_om, raf_ow, raf_gfs, raf_mc, raf_wa], suffixe="km/h") top_dir_texte = calculer_moyenne_direction([dir_mf, dir_om, dir_ow, dir_gfs, dir_mc, dir_wa]) top_dir_fleche = direction_en_fleche(top_dir_texte) top_proba_pluie = calculer_moyenne_numerique([proba_pluie_mf, proba_pluie_om, proba_pluie_ow, proba_pluie_gfs, proba_pluie_mc, proba_pluie_wa], suffixe="%") top_qte_pluie = Platform_Rain = calculer_moyenne_numerique([qte_pluie_mf, qte_pluie_om, qte_pluie_ow, qte_pluie_gfs, qte_pluie_mc, qte_pluie_wa], suffixe="mm") #st.write(f"pluie {top_qte_pluie}") icones_api = [] for icone in [icone_mf, icone_om, icone_ow, icone_gfs, icone_mc, icone_wa]: if icone and icone != "--" and "--" not in str(icone): icones_api.append(icone) if icones_api: top_tendance = max(set(icones_api), key=icones_api.count) else: top_tendance = "☀️" donnees_meteo_harmonisees[jour_j] = { "top_tgros": top_tgros, "top_tmin": top_tmin, "top_tmax": top_tmax, "top_vent": top_vent, "top_raf": top_raf, "top_dir_texte": top_dir_texte, "top_dir_fleche": top_dir_fleche, "top_proba_pluie": top_proba_pluie, "top_qte_pluie": top_qte_pluie, "top_tendance": top_tendance, "tgros_mf": tgros_mf, "tmin_mf": tmin_mf, "tmax_mf": tmax_mf, "vent_mf": vent_mf, "raf_mf": raf_mf, "dir_mf": dir_mf, "dir_fleche_mf": direction_en_fleche(dir_mf), "proba_pluie_mf": proba_pluie_mf, "qte_pluie_mf": qte_pluie_mf, "icone_mf": icone_mf, "tgros_om": tgros_om, "tmin_om": tmin_om, "tmax_om": tmax_om, "vent_om": vent_om, "raf_om": raf_om, "dir_om": dir_om, "dir_fleche_om": direction_en_fleche(dir_om), "proba_pluie_om": proba_pluie_om, "qte_pluie_om": qte_pluie_om, "icone_om": icone_om, "tgros_ow": tgros_ow, "tmin_ow": tmin_ow, "tmax_ow": tmax_ow, "vent_ow": vent_ow, "raf_ow": raf_ow, "dir_ow": dir_ow, "dir_fleche_ow": direction_en_fleche(dir_ow), "proba_pluie_ow": proba_pluie_ow, "qte_pluie_ow": qte_pluie_ow, "icone_ow": icone_ow, "tgros_gfs": tgros_gfs, "tmin_gfs": tmin_gfs, "tmax_gfs": tmax_gfs, "vent_gfs": vent_gfs, "raf_gfs": raf_gfs, "dir_gfs": dir_gfs, "dir_fleche_gfs": direction_en_fleche(dir_gfs), "proba_pluie_gfs": proba_pluie_gfs, "qte_pluie_gfs": qte_pluie_gfs, "icone_gfs": icone_gfs, "tgros_mc": tgros_mc, "tmin_mc": tmin_mc, "tmax_mc": tmax_mc, "vent_mc": vent_mc, "raf_mc": raf_mc, "dir_mc": dir_mc, "dir_fleche_mc": direction_en_fleche(dir_mc), "proba_pluie_mc": proba_pluie_mc, "qte_pluie_mc": qte_pluie_mc, "icone_mc": icone_mc, "tgros_wa": tgros_wa, "tmin_wa": tmin_wa, "tmax_wa": tmax_wa, "vent_wa": vent_wa, "raf_wa": raf_wa, "dir_wa": dir_wa, "dir_fleche_wa": direction_en_fleche(dir_wa), "proba_pluie_wa": proba_pluie_wa, "qte_pluie_wa": qte_pluie_wa, "icone_wa": icone_wa, } st.markdown(f"#### {nom_ville_officiel} - {texte_moment} {top_tendance}") try: date_selectionnee_obj = datetime.datetime.strptime(journee, "%Y-%m-%d") cle_mm_dd = date_selectionnee_obj.strftime("%m-%d") num_mois = date_selectionnee_obj.month saint_du_jour = SAINTS.get(cle_mm_dd, "le Saint du jour") proverbe_du_mois = PROVERBES_MOIS.get(num_mois, " ") dicton_du_jour = PROVERBES_JOURS.get(cle_mm_dd, "") st.markdown( f"""
😇 {saint_du_jour}
\"{dicton_du_jour}\"
\"{proverbe_du_mois}\"
""", unsafe_allow_html=True ) except Exception as e: st.error(f"❌ Le code a planté dans le bloc try !") st.warning(f"**Type de l'erreur :** {type(e).__name__}") st.code(f"Message technique : {e}") pass # MINI-CALENDRIER HORIZONTAL DÉFILANT (14 jours, 5 visibles) st.markdown("#### 📅 Calendrier des prévisions") html_calendrier = '
' tmin_elements = [] tmax_elements = [] icones_dispo_j = [] tmin_elements = [ float(meteo_jour["top_tmin"]) for meteo_jour in donnees_meteo_harmonisees.values() if meteo_jour.get("top_tmin") and meteo_jour["top_tmin"].strip() ] tmax_elements = [ float(meteo_jour["top_tmax"]) for meteo_jour in donnees_meteo_harmonisees.values() if meteo_jour.get("top_tmax") and meteo_jour["top_tmax"].strip() ] icones_dispo_j = [ (meteo_jour["top_tendance"]) for meteo_jour in donnees_meteo_harmonisees.values() if meteo_jour.get("top_tendance") and meteo_jour["top_tendance"].strip() ] #st.write(tmin_elements) #st.write(tmax_elements) #st.write(icones_dispo_j) for idx_j, jour_j in enumerate(jours_distincts): tendance_j = icones_dispo_j[idx_j] if icones_dispo_j[idx_j] else "⛅" valeur_cal_tmin = tmin_elements[idx_j] valeur_cal_tmax = tmax_elements[idx_j] date_obj = datetime.datetime.strptime(jour_j, "%Y-%m-%d") jours_semaine = ["dim.", "lun.", "mar.", "mer.", "jeu.", "ven.", "sam."] nom_jour = jours_semaine[int(date_obj.strftime("%w"))] date_courte = f"{nom_jour} {date_obj.day}" est_selectionne = (st.session_state.jour_selectionne == jour_j) classe_carte = "day-card-active" if est_selectionne else "day-card" url_cible = f"?day={jour_j}&ville={ville}" # Version ultra-compacte pour cellules plus petites bloc_html = f"""
{date_courte}
{tendance_j}
{valeur_cal_tmin}° {valeur_cal_tmax}°
""" html_calendrier += bloc_html html_calendrier += "
" st.html(html_calendrier) alertes_trouvees = False # 1. On vérifie si WeatherAPI signale des alertes activement if data_wa and "alerts" in data_wa and data_wa["alerts"].get("alert"): alertes_trouvees = True for alerte in data_wa["alerts"]["alert"]: # Affichage d'un bandeau rouge/orange pour chaque alerte trouvée st.error(f"🚨 **{alerte.get('event', 'Alerte météo')}** : {alerte.get('headline')}") # 2. Si après vérification aucune alerte n'a été détectée if not alertes_trouvees: st.caption("🟢 Aucune alerte météo signalée pour les 14 prochains jours.") # Préparation des clés (on garde les clés d'origine pour le dictionnaire) cles_origines = list(donnees_meteo_harmonisees.keys()) # Transformation des clés en texte propre YYYY-MM-DD pour le tri et la période dates_graph = [str(d)[:10] for d in cles_origines] dates_triees = sorted(dates_graph) # Calcul de la période pour les titres if dates_triees: date_debut = datetime.datetime.strptime(dates_triees[0], "%Y-%m-%d").strftime("%d/%m") date_fin = datetime.datetime.strptime(dates_triees[-1], "%Y-%m-%d").strftime("%d/%m") texte_periode = f"Période du {date_debut} au {date_fin} : {ville}" else: texte_periode = "" series_actuelles = { "pluie_toute": 0, "pluie_nulle": 0, "pluie_5mm": 0, "pluie_10mm": 0, "pluie_20mm": 0, "tmax_30": 0, "tmax_25": 0, "tmax_20": 0, "tmin_10": 0, "tmin_5": 0, "tmin_0": 0, "sec": 0, "vent_nul": 0, "vent_leger": 0, "vent_modere": 0, "vent_fort": 0, "vent_violent": 0 } records_series = { "pluie_toute": 0, "pluie_nulle": 0, "pluie_5mm": 0, "pluie_10mm": 0, "pluie_20mm": 0, "tmax_30": 0, "tmax_25": 0, "tmax_20": 0, "tmin_10": 0, "tmin_5": 0, "tmin_0": 0, "sec": 0, "vent_nul": 0, "vent_leger": 0, "vent_modere": 0, "vent_fort": 0, "vent_violent": 0 } criteres = ["pluie_toute", "pluie_nulle", "pluie_5mm", "pluie_10mm", "pluie_20mm", "tmax_30", "tmax_25", "tmax_20", "tmin_10", "tmin_5", "tmin_0", "sec", "vent_nul", "vent_leger", "vent_modere", "vent_fort", "vent_violent"] # Dictionnaires de suivi series_actuelles = {c: 0 for c in criteres} debut_actuel = {c: None for c in criteres} records_series = {c: 0 for c in criteres} periodes_records = {c: " " for c in criteres} for d in cles_origines: meteo_jour = donnees_meteo_harmonisees[d] # Nettoyage des données du jour qte_pluie = float(str(meteo_jour.get('top_qte_pluie', '0.0')).replace(' mm', '').replace('--', '0').strip()) tmax = float(str(meteo_jour.get('top_tmax', '0')).replace('--', '0').strip()) tmin = float(str(meteo_jour.get('top_tmin', '0')).replace('--', '0').strip()) rafale = float(str(meteo_jour.get('top_raf', '0')).replace(' km/h', '').replace('--', '0').strip()) date_courante_fr = datetime.datetime.strptime(str(d)[:10], "%Y-%m-%d").strftime("%d/%m") conditions = { "pluie_toute": (0.0 < qte_pluie), "pluie_nulle": (0.0 < qte_pluie < 5.0), "pluie_5mm": (5.0 <= qte_pluie < 10.0), "pluie_10mm": (10.0 <= qte_pluie < 20.0), "pluie_20mm": (20 <= qte_pluie), "tmax_30": (tmax >= 30.0), "tmax_25": (30 > tmax >= 25.0), "tmax_20": (25 > tmax >= 20.0), "tmin_10": (5 < tmin <= 10.0), "tmin_5": (0 80.0), } # --- ÉVALUATION DES ANCIENS CRITÈRES --- for c in criteres: if conditions[c]: # Si la série commence juste, on enregistre sa date de départ if series_actuelles[c] == 0: debut_actuel[c] = date_courante_fr series_actuelles[c] += 1 # Si le record actuel est battu (ou égalé) if series_actuelles[c] > records_series[c]: records_series[c] = series_actuelles[c] # On sauvegarde dynamiquement la période du record if debut_actuel[c] == date_courante_fr: periodes_records[c] = f"le {date_courante_fr}" else: periodes_records[c] = f"du {debut_actuel[c]} au {date_courante_fr}" else: # Fin de la série en cours, on réinitialise le compteur series_actuelles[c] = 0 debut_actuel[c] = None with detail: st.markdown(f"#### {nom_ville_officiel} - {texte_moment} {top_tendance}") afficher_details_api = st.checkbox("Afficher le détail", value=True) jour_sel = donnees_meteo_harmonisees.get(journee) # --- LIGNE DE SYNTHÈSE (MOYENNE) --- row_synth = " " logo_html_mf = extraire_image_locale_html("images/logo_meteofrance.png", hauteur=25, style_additionnel="background: #004b93; padding: 3px; border-radius: 3px;") row_mf = f"{logo_html_mf} Météo-France" logo_html_om = extraire_image_locale_html("images/logo_open_meteo.jpg", hauteur=20) row_om = f"{logo_html_om} Open-Meteo" logo_html_ow = extraire_image_locale_html("images/logo_openweather.png", hauteur=25, style_additionnel="background: #004b93; padding: 3px; border-radius: 3px;") row_ow = f"{logo_html_ow} OpenWeather" logo_html_gfs = extraire_image_locale_html("images/logo_gfs.png", hauteur=20) row_gfs = f"{logo_html_gfs} Modèle GFS" logo_html_mc = extraire_image_locale_html("images/logo_meteo_concept.jpg", hauteur=20) row_mc = f"{logo_html_mc} Météo-Concept" logo_html_wa = extraire_image_locale_html("images/logo_weatherAPI.png", hauteur=20) # Ajout du logo local potentiel row_wa = f"{logo_html_wa} WeatherAPI" if jour_sel is not None: donnees_tableau = { "Fournisseurs / Métriques": [row_synth, row_mf, row_om, row_ow, row_gfs, row_mc, row_wa], " ": [jour_sel["top_tendance"], jour_sel["icone_mf"], jour_sel["icone_om"], jour_sel["icone_ow"], jour_sel["icone_gfs"], jour_sel["icone_mc"], jour_sel["icone_wa"]], libelle_colonne_temp: [jour_sel["top_tgros"], jour_sel["tgros_mf"], jour_sel["tgros_om"], jour_sel["tgros_ow"], jour_sel["tgros_gfs"], jour_sel["tgros_mc"], jour_sel["tgros_wa"]], "❄️
Min": [jour_sel["top_tmin"], jour_sel["tmin_mf"], jour_sel["tmin_om"], jour_sel["tmin_ow"], jour_sel["tmin_gfs"], jour_sel["tmin_mc"], jour_sel["tmin_wa"]], "🔥
Max": [jour_sel["top_tmax"], jour_sel["tmax_mf"], jour_sel["tmax_om"], jour_sel["tmax_ow"], jour_sel["tmax_gfs"], jour_sel["tmax_mc"], jour_sel["tmax_wa"]], "🌧️
Proba": [f"{jour_sel['top_proba_pluie']}", jour_sel["proba_pluie_mf"], jour_sel["proba_pluie_om"], jour_sel["proba_pluie_ow"], jour_sel["proba_pluie_gfs"], jour_sel["proba_pluie_mc"], jour_sel["proba_pluie_wa"]], "💧
Total": [f"{jour_sel['top_qte_pluie']}", jour_sel["qte_pluie_mf"], jour_sel["qte_pluie_om"], jour_sel["qte_pluie_ow"], jour_sel["qte_pluie_gfs"], jour_sel["qte_pluie_mc"], jour_sel["qte_pluie_wa"]], "💨
Vent": [jour_sel["top_vent"], jour_sel["vent_mf"], jour_sel["vent_om"], jour_sel["vent_ow"], jour_sel["vent_gfs"], jour_sel["vent_mc"], jour_sel["vent_wa"]], "🌪️
Rafale": [jour_sel["top_raf"], jour_sel["raf_mf"], jour_sel["raf_om"], jour_sel["raf_ow"], jour_sel["raf_gfs"], jour_sel["raf_mc"], jour_sel["raf_wa"]], "🧭
Dir.": [jour_sel["top_dir_fleche"], direction_en_fleche(jour_sel["dir_mf"]), direction_en_fleche(jour_sel["dir_om"]), direction_en_fleche(jour_sel["dir_ow"]), direction_en_fleche(jour_sel["dir_gfs"]), direction_en_fleche(jour_sel["dir_mc"]), direction_en_fleche(jour_sel["dir_wa"])] } df = pd.DataFrame(donnees_tableau).set_index("Fournisseurs / Métriques") df.index.name = None if not afficher_details_api: df = df.loc[[row_synth]] else: lignes_a_garder = [row_synth] for api_row in [row_mf, row_om, row_ow, row_gfs, row_mc, row_wa]: if api_row in df.index: contient_donnees = not df.loc[api_row].astype(str).str.contains('--').all() if contient_donnees: lignes_a_garder.append(api_row) if not lignes_a_garder and row_synth in df.index: lignes_a_garder = [row_synth] df = df.loc[lignes_a_garder] st.markdown(f'
{df.to_html(escape=False)}
', unsafe_allow_html=True) st.markdown("#### Évolution dans la journée") heures_standards = ["00h00", "03h00", "06h00", "09h00", "12h00", "15h00", "18h00", "21h00"] data_heures = {h: {"MF": ["--", "--", "--", "--", None, None], "OM": ["--", "--", "--", "--", None, None], "OW": ["--", "--", "--", "--", None, None], "GFS": ["--", "--", "--", "--", None, None], "WA": ["--", "--", "--", "--", None, None]} for h in heures_standards} #heures_standards = ["06h00", "09h00", "12h00", "15h00", "18h00", "21h00"] #data_heures = {h: {"MF": ["--", "--", "--", "--", "--", "0"], "OM": ["--", "--", "--", "--", "--", "0"], "OW": ["--", "--", "--", "--", "--", "0"], "GFS": ["--", "--", "--", "--", "--", "0"], "WA": ["--", "--", "--", "--", "--", "0"]} for h in heures_standards} if data_mf and "hourly" in data_mf and 'indices_mf' in locals() and indices_mf: for idx in indices_mf: hr_str = heures_mf[idx].split("T")[1][:2] + "h00" if hr_str in data_heures and data_mf['hourly']['temperature_2m'][idx] is not None: p_pluie = f"{data_mf['hourly']['precipitation_probability'][idx]}%" if "precipitation_probability" in data_mf['hourly'] and data_mf['hourly']['precipitation_probability'][idx] is not None else None q_pluie = f"{round(data_mf['hourly']['precipitation'][idx], 1)}" if "precipitation" in data_mf['hourly'] and data_mf['hourly']['precipitation'][idx] is not None else "0" temp_arrondie = arrondir_a_05(data_mf['hourly']['temperature_2m'][idx]) data_heures[hr_str]["MF"] = [determiner_icone(wmo_codes.get(data_mf['hourly']['weather_code'][idx], "nuage"), int(hr_str[:2])), temp_arrondie, f"{arrondir_a_5(data_mf['hourly']['wind_speed_10m'][idx])}", direction_en_fleche(degres_en_direction(data_mf['hourly']['wind_direction_10m'][idx])), p_pluie, q_pluie] if data_om and "hourly" in data_om and 'indices_om' in locals() and indices_om: for idx in indices_om: hr_str = heures_om[idx].split("T")[1][:2] + "h00" if hr_str in data_heures and data_om['hourly']['temperature_2m'][idx] is not None: p_pluie = f"{data_om['hourly']['precipitation_probability'][idx]}%" if "precipitation_probability" in data_om['hourly'] and data_om['hourly']['precipitation_probability'][idx] is not None else None q_pluie = f"{round(data_om['hourly']['precipitation'][idx], 1)}" if "precipitation" in data_om['hourly'] and data_om['hourly']['precipitation'][idx] is not None else "0" temp_arrondie = arrondir_a_05(data_om['hourly']['temperature_2m'][idx]) data_heures[hr_str]["OM"] = [determiner_icone(wmo_codes.get(data_om['hourly']['weather_code'][idx], "nuage"), int(hr_str[:2])), temp_arrondie, f"{arrondir_a_5(data_om['hourly']['wind_speed_10m'][idx])}", direction_en_fleche(degres_en_direction(data_om['hourly']['wind_direction_10m'][idx])), p_pluie, q_pluie] if journee in ow_par_jour: for p in ow_par_jour[journee]: hr_str = p['dt_txt'].split(" ")[1][:2] + "h00" if hr_str in data_heures and p['main'].get('temp') is not None: p_pluie = f"{round(p.get('pop', 0) * 100)}%" q_pluie = f"{round(p.get('rain', {}).get('3h', 0), 1)}" temp_arrondie = arrondir_a_05(p['main']['temp']) data_heures[hr_str]["OW"] = [determiner_icone(p['weather'][0]['description'], int(hr_str[:2])), temp_arrondie, f"{arrondir_a_5(p['wind'].get('speed', 0) * 3.6)}", direction_en_fleche(degres_en_direction(p['wind'].get('deg'))), p_pluie, q_pluie] if data_gfs and "hourly" in data_gfs and 'indices_gfs' in locals() and indices_gfs: for idx in indices_gfs: hr_str = heures_gfs[idx].split("T")[1][:2] + "h00" if hr_str in data_heures and data_gfs['hourly']['temperature_2m'][idx] is not None: p_pluie = f"{data_gfs['hourly']['precipitation_probability'][idx]}%" if "precipitation_probability" in data_gfs['hourly'] and data_gfs['hourly']['precipitation_probability'][idx] is not None else None q_pluie = f"{round(data_gfs['hourly']['precipitation'][idx], 1)}" if "precipitation" in data_gfs['hourly'] and data_gfs['hourly']['precipitation'][idx] is not None else "0" temp_arrondie = arrondir_a_05(data_gfs['hourly']['temperature_2m'][idx]) data_heures[hr_str]["GFS"] = [determiner_icone(wmo_codes.get(data_gfs['hourly']['weather_code'][idx], "nuage"), int(hr_str[:2])), temp_arrondie, f"{arrondir_a_5(data_gfs['hourly']['wind_speed_10m'][idx])}", direction_en_fleche(degres_en_direction(data_gfs['hourly']['wind_direction_10m'][idx])), p_pluie, q_pluie] if journee in wa_par_jour: for hr_bloc in wa_par_jour[journee]['hour']: hr_str = hr_bloc['time'].split(" ")[1][:2] + "h00" if hr_str in data_heures: p_pluie = f"{hr_bloc['chance_of_rain']}%" q_pluie = f"{round(hr_bloc['precip_mm'], 1)}" temp_arrondie = arrondir_a_05(hr_bloc['temp_c']) data_heures[hr_str]["WA"] = [determiner_icone(hr_bloc['condition']['text'], int(hr_str[:2])), temp_arrondie, f"{arrondir_a_5(hr_bloc['wind_kph'])}", direction_en_fleche(degres_en_direction(hr_bloc['wind_degree'])), p_pluie, q_pluie] logo_h_mf = extraire_image_locale_html("images/logo_meteofrance.png", hauteur=22, style_additionnel="background: #004b93; padding: 2px; border-radius: 3px; display: block; margin: 0 auto 4px auto;") h_row_mf = f"{logo_h_mf}Météo-France" if logo_h_mf else "Météo-France" logo_h_om = extraire_image_locale_html("images/logo_open_meteo.jpg", hauteur=18, style_additionnel="display: block; margin: 0 auto 4px auto;") h_row_om = f"{logo_h_om}Open-Meteo" if logo_h_om else "Open-Meteo" logo_h_ow = extraire_image_locale_html("images/logo_openweather.png", hauteur=22, style_additionnel="background: #004b93; padding: 2px; border-radius: 3px; display: block; margin: 0 auto 4px auto;") h_row_ow = f"{logo_h_ow}OpenWeather" if logo_h_ow else "OpenWeather" logo_h_gfs = extraire_image_locale_html("images/logo_gfs.png", hauteur=18, style_additionnel="display: block; margin: 0 auto 4px auto;") h_row_gfs = f"{logo_h_gfs}Modèle GFS" if logo_h_gfs else "Modèle GFS" logo_h_wa = extraire_image_locale_html("images/logo_weatherAPI.png", hauteur=18, style_additionnel="display: block; margin: 0 auto 4px auto;") h_row_wa = f"{logo_h_wa}WeatherAPI" if logo_h_wa else "WeatherAPI" h_row_synth = row_synth donnees_par_api = {h_row_synth: [], h_row_mf: [], h_row_om: [], h_row_ow: [], h_row_gfs: [], h_row_wa: []} for h in heures_standards: v_mf = data_heures[h]["MF"] v_om = data_heures[h]["OM"] v_ow = data_heures[h]["OW"] v_gfs = data_heures[h]["GFS"] v_wa = data_heures[h]["WA"] icones_h = [v[0] for v in [v_mf, v_om, v_ow, v_gfs, v_wa] if v[0] != "--"] top_icone_h = max(set(icones_h), key=icones_h.count) if icones_h else "☀️" # Calculs des moyennes (On change le suffixe du vent à "" pour enlever le km/h) top_t_h = calculer_moyenne_numerique([v[1] for v in [v_mf, v_om, v_ow, v_gfs, v_wa]], suffixe="°C") top_v_h = calculer_moyenne_numerique([v[2] for v in [v_mf, v_om, v_ow, v_gfs, v_wa]], suffixe="km/h") # top_dir_h = calculer_moyenne_direction([degres_en_direction(DIR_VERS_DEG[k]) for k in [v_mf[3], v_om[3], v_ow[3], v_gfs[3], v_wa[3]] if k in DIR_VERS_DEG]) # top_dir_texte = degres_en_direction(top_dir_h) # top_dir_fleche_h = direction_en_fleche(top_dir_texte) fleches_h = [v[3] for v in [v_mf, v_om, v_ow, v_gfs, v_wa] if v[3] != "--"] top_dir_fleche_h = max(set(fleches_h), key=fleches_h.count) if fleches_h else "→" top_p_h = calculer_moyenne_numerique([v[4] for v in [v_mf, v_om, v_ow, v_gfs, v_wa]], suffixe="%") top_q_h = calculer_moyenne_numerique([v[5] for v in [v_mf, v_om, v_ow, v_gfs, v_wa]], suffixe="mm") # 1. REMPLISSAGE DES LIGNES DE DÉTAIL PAR API for row_key, v_api in [(h_row_mf, v_mf), (h_row_om, v_om), (h_row_ow, v_ow), (h_row_gfs, v_gfs), (h_row_wa, v_wa)]: if v_api[1] == "--": # Pour garder la même hauteur de cellule (4 lignes) même quand c'est vide donnees_par_api[row_key].append( '
' '
--
--
--
--
' ) else: # Nettoyage de la probabilité pour enlever les % ou symboles superflus dans l'affichage proba_clean = str(v_api[4]).replace("%", "").strip() if v_api[4] and v_api[4] != "--" else "--" proba_format = f"{proba_clean}%" if proba_clean != "--" else "--" # Nettoyage de la quantité de pluie (mm) qte_clean = str(v_api[5]).replace("mm", "").strip() if v_api[5] and v_api[5] != "--" else "0" aff_prob = ( f"
 
") if proba_format != "--": aff_prob = "(" + proba_format + ")" if qte_clean in ["0", "0.0", "--"]: ligne_pluie_api = ( f"
 
" #   est un espace invisible pour garder la hauteur de la cellule f"
 
" #   est un espace invisible pour garder la hauteur de la cellule ) else: ligne_pluie_api = ( f"
💧 {qte_clean} mm " f"{aff_prob}
" ) bloc_html_api = ( f"
" f"
{v_api[0]}
" f"
{v_api[1]}°C
" f"
{v_api[2]} {v_api[3]}
" f"{ligne_pluie_api}" f"
" ) donnees_par_api[row_key].append(bloc_html_api) # 2. REMPLISSAGE DE LA LIGNE D'ENTÊTE (SYNTHÈSE) # Uniformisation du format pour la probabilité et la quantité de la ligne globale top_p_clean = str(top_p_h).replace("%", "").strip() top_p_format = f"{top_p_clean}%" if top_p_clean != "--" else "--" top_q_clean = str(top_q_h).replace("mm", "").strip() if top_q_h != "--" else "0" aff_prob = ( f"
 
") if top_p_format != "--": aff_prob = "(" + top_p_format + ")" if top_q_clean in ["0", "0.0", "--"]: ligne_pluie_synth = ( f"
 
" #   est un espace invisible pour garder la hauteur de la cellule f"
 
" #   est un espace invisible pour garder la hauteur de la cellule ) else: ligne_pluie_synth = ( f"
💧 {qte_clean} mm
" f"
{aff_prob}
" ) bloc_html_synth = ( f"
" f"
{top_icone_h}
" f"
{top_t_h}
" f"
{top_v_h} {top_dir_fleche_h}
" # f"
💧 {top_q_clean} ({top_p_format})
" f"{ligne_pluie_synth}" f"
" ) donnees_par_api[h_row_synth].append(bloc_html_synth) ordre_lignes = [h_row_synth, h_row_mf, h_row_om, h_row_ow, h_row_gfs, h_row_wa] df_horaire = pd.DataFrame(donnees_par_api, index=heures_standards).T df_horaire = df_horaire.reindex(ordre_lignes) df_horaire.index.name = None if not afficher_details_api: df_horaire = df_horaire.loc[[h_row_synth]] else: # 1. On commence par garder d'office la ligne de synthèse lignes_horaires_a_garder = [h_row_synth] # 2. On examine chaque fournisseur d'API for h_row in [h_row_mf, h_row_om, h_row_ow, h_row_gfs, h_row_wa]: if h_row in df_horaire.index: contient_donnees = False # On vérifie chaque cellule horaire de la ligne for cellule in df_horaire.loc[h_row]: # Convertit en texte pour nettoyer les balises HTML éventuelles cellule_txt = str(cellule) # SI la cellule n'est pas vide ET ne contient pas de tirets (bruts ou HTML) if (cellule_txt.strip() != "" and "--" not in cellule_txt and "--" not in cellule_txt): contient_donnees = True break # Inutile de tester le reste de la ligne, elle contient une vraie donnée ! # Si le modèle renvoie au moins une information, on conserve sa ligne if contient_donnees: lignes_horaires_a_garder.append(h_row) # 3. On applique le filtre sur le DataFrame df_horaire = df_horaire.loc[lignes_horaires_a_garder] html_table_2 = f'' for col in df_horaire.columns: html_table_2 += f'' html_table_2 += '' for idx, r in df_horaire.iterrows(): html_table_2 += f'' for col in df_horaire.columns: html_table_2 += f'' html_table_2 += '' html_table_2 += '
Heures{col}
{idx}{r[col]}
' #st.markdown(html_table_2, unsafe_allow_html=True) #st.markdown(f'
{df_horaire.to_html(escape=False)}
', unsafe_allow_html=True) #html_propre = df_horaire.to_html(escape=False).replace("\n", "") st.markdown(f'
{df_horaire.to_html(escape=False)}
', unsafe_allow_html=True) else: # Si jour_sel est None (le temps que le téléphone charge), on affiche un message propre au lieu de planter st.info("⌛ Récupération des prévisions météo en cours...") with visu: tmin_graph = [float(str(donnees_meteo_harmonisees[d]['top_tmin']).replace('--', '0')) for d in cles_origines] tmax_graph = [float(str(donnees_meteo_harmonisees[d]['top_tmax']).replace('--', '0')) for d in cles_origines] tgros_graph = [float(str(donnees_meteo_harmonisees[d]['top_tgros']).replace('--', '0')) for d in cles_origines] st.markdown(f"#### 📈 {texte_periode}") # --- 2. CALCUL DES PRÉCIPITATIONS & RÉCUPÉRATION DES TENDANCES --- # Conversion sécurisée en float en nettoyant le "mm" et les "--" precip_graph = [ float(str(donnees_meteo_harmonisees[d].get('top_qte_pluie', '0.0')) .replace(' mm', '') # 🟢 Supprime l'unité " mm" .replace('--', '0') # Gère les valeurs vides .strip()) # Nettoie les espaces restants for d in dates_graph ] libelles_axe_x = [] for d in dates_graph: # 1. Formatage en jj/mm uniquement (ex: 09/06) date_fr = " " + datetime.datetime.strptime(d, "%Y-%m-%d").strftime("%d/%m") libelles_axe_x.append(date_fr) # --- 3. CRÉATION DU GRAPHIQUE UNIQUE (DOUBLE AXE Y) --- fig_combine = make_subplots(specs=[[{"secondary_y": True}]]) max_pluie = max(precip_graph) if precip_graph else 0 plafond_axe_pluie = max(max_pluie * 3.5, 10.0) # 🟢 MODIFICATION 1 : Pour que les étiquettes personnalisées s'alignent parfaitement, # on passe directement 'libelles_axe_x' dans le paramètre 'x' de chaque trace au lieu de 'dates_graph'. # A. Ajout des Précipitations (Barres - Axe Y de droite) fig_combine.add_trace( go.Bar( x=libelles_axe_x, # 🟢 Changement ici y=precip_graph, name="Précipitations (mm)", marker=dict(color='rgba(52, 152, 219, 0.4)', line=dict(color='#3498db', width=1)), hovertemplate='Pluie : %{y}' ), secondary_y=True, ) # B. Ajout de la Temp. Maximale (Ligne - Axe Y de gauche) fig_combine.add_trace( go.Scatter( x=libelles_axe_x, # 🟢 Changement ici y=tmax_graph, name="Temp. Maximale (°C)", mode='lines+markers', line=dict(color='#e74c3c', width=3, shape='spline'), marker=dict(size=6), hovertemplate='Max : %{y}°C' ), secondary_y=False, ) # C. Ajout de la Temp. Minimale (Ligne - Axe Y de gauche) fig_combine.add_trace( go.Scatter( x=libelles_axe_x, # 🟢 Changement ici y=tmin_graph, name="Temp. Minimale (°C)", mode='lines+markers', line=dict(color="#1428dd", width=3, shape='spline'), marker=dict(size=6), hovertemplate='Min : %{y}°C' ), secondary_y=False, ) fig_combine.add_trace( go.Scatter( x=libelles_axe_x, y=tgros_graph, name="Temp. Midi (°C)", mode='lines+markers', line=dict(color='#2ecc71', width=3, shape='spline'), # Vert émeraude marker=dict(size=6), hovertemplate='Midi : %{y}°C' ), secondary_y=False, # Relié à l'axe des températures à gauche ) # D. Configuration du Layout global fig_combine.update_layout( showlegend=False, paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', hovermode="x unified", dragmode=False, margin=dict(l=50, r=45, t=40, b=70), hoverlabel=dict( bgcolor="rgba(214, 234, 248, 0.9)", # 🟢 Bleu très léger (semi-opaque pour masquer le fond) bordercolor="#3498db", # 🟢 Bordure bleue pour finie la boîte font=dict( size=12, color="#2c3e50" # 🟢 Texte foncé pour une lisibilité maximale ) ), # 🟢 MODIFICATION 2 : Configuration simplifiée et robuste de l'axe X xaxis=dict( type='category', # 🟢 Repasser en 'category' pour valider les chaînes de texte showgrid=False, tickfont=dict( style='normal', size=11, color="#2c3e50" ), tickangle =90, fixedrange=True, automargin=True # Permet à Plotly de gérer dynamiquement l'espace requis ), # Configuration de l'axe Y gauche (Températures) yaxis=dict( title=dict( text="Température (°C)", font=dict(size=12, color="#e74c3c", weight="bold") ), tickfont=dict(size=11, color="#2c3e50"), gridcolor='rgba(128,128,128,0.15)', fixedrange=True, automargin=True ), # Configuration de l'axe Y droite (Précipitations) yaxis2=dict( title=dict( text="Précipitations (mm)", font=dict(size=12, color="#2980b9", weight="bold") ), tickfont=dict(size=11, color="#2c3e50"), gridcolor='rgba(128,128,128,0.05)', overlaying='y', side='right', fixedrange=True, automargin=True, range=[0, plafond_axe_pluie] ), ) # --- Reste de votre code pour l'Iframe (Inchangé et correct) --- config_options = {"scrollZoom": False, "displayModeBar": False} html_graph = fig_combine.to_html( config=config_options, include_plotlyjs='cdn', full_html=False ) code_html_final = f"""
{html_graph}
""" components.html(code_html_final, height=510, scrolling=False) # --- 4. CALCULS DES INDICATEURS EN DESSOUS DU GRAPHIQUE --- # Récupération de l'heure actuelle maintenant = datetime.datetime.now() with precip: st.markdown(f"#### ☔ {texte_periode}") prochaine_pluie_heures = None prochain_sans_pluie = None prochain_avec_pluie = None prochain_plus_5mm = None prochain_plus_10mm = None prochain_plus_20mm = None # On trie les dates chronologiquement pour être sûr de l'ordre temporel dates_triees = sorted(dates_graph) for d in dates_triees: meteo_jour = donnees_meteo_harmonisees[d] # Nettoyage sécurisé de la quantité de pluie du jour qte_pluie = float(str(meteo_jour.get('top_qte_pluie', '0.0')).replace(' mm', '').replace('--', '0').strip()) proba_pluie = float(str(meteo_jour.get('top_proba_pluie', '0')).replace('%', '').replace('--', '0').strip()) date_obj = datetime.datetime.strptime(d, "%Y-%m-%d") date_fr = date_obj.strftime("%d/%m/%Y") # A. Prochain jour SANS pluie (0 mm) if qte_pluie == 0.0 and prochain_sans_pluie is None: prochain_sans_pluie = date_fr # B. Prochain jour AVEC pluie (> 0 mm) if qte_pluie > 0.0 and prochain_avec_pluie is None: prochain_avec_pluie = date_fr # C. Prochain jour avec au moins 5mm de pluie if qte_pluie >= 5.0 and prochain_plus_5mm is None: prochain_plus_5mm = date_fr # D. Prochain jour avec au moins 10mm de pluie if qte_pluie >= 10.0 and prochain_plus_10mm is None: prochain_plus_10mm = date_fr # D. Prochain jour avec au moins 10mm de pluie if qte_pluie >= 20.0 and prochain_plus_20mm is None: prochain_plus_20mm = date_fr # E. Calcul : Dans combien de temps il va pleuvoir (en heures) # On regarde si le jour même ou un jour futur a de la pluie prévue if qte_pluie > 0.0 and prochaine_pluie_heures is None: # Si c'est aujourd'hui, on peut estimer par rapport à l'heure actuelle, # sinon on calcule l'écart avec le début de la journée pluvieuse if date_obj.date() == maintenant.date(): # Si c'est aujourd'hui, on suppose (faute de précision horaire) que ça commence bientôt # ou on met à 0 si la journée est en cours. On va calculer par rapport au début du jour + 12h par exemple : debut_pluie = datetime.datetime.combine(date_obj.date(), datetime.time(12, 0)) else: debut_pluie = datetime.datetime.combine(date_obj.date(), datetime.time(0, 0)) delai = debut_pluie - maintenant heures_restantes = max(0, int(delai.total_seconds() / 3600)) prochaine_pluie_heures = f"Dans environ {heures_restantes}h" # Sécurités si aucune date ne correspond prochaine_pluie_heures = prochaine_pluie_heures if prochaine_pluie_heures else "Pas de pluie prévue" prochain_sans_pluie = prochain_sans_pluie if prochain_sans_pluie else "Aucun" prochain_avec_pluie = prochain_avec_pluie if prochain_avec_pluie else "Pas de pluie prévue" prochain_plus_5mm = prochain_plus_5mm if prochain_plus_5mm else "Aucun" prochain_plus_10mm = prochain_plus_10mm if prochain_plus_10mm else "Aucun" prochain_plus_20mm = prochain_plus_20mm if prochain_plus_20mm else "Aucun" toutes_pluies = [ float(str(donnees_meteo_harmonisees[d].get('top_qte_pluie', '0.0')).replace(' mm', '').replace('--', '0').strip()) for d in dates_graph ] # 2. Initialisation des compteurs par tranche stats_pluie = { "🌞 Sec (0 mm)": 0, "🌦️ Pluie faible (< 5 mm)": 0, "🌧️ Pluie modérée (< 10 mm)": 0, "☔ Pluie forte (< 20 mm)": 0, "🛟 Déluge (≥ 20 mm)": 0 } # 3. Classement de chaque jour dans sa catégorie exclusive for qte in toutes_pluies: if qte == 0.0: stats_pluie["🌞 Sec (0 mm)"] += 1 elif qte < 5.0: stats_pluie["🌦️ Pluie faible (< 5 mm)"] += 1 elif qte < 10.0: stats_pluie["🌧️ Pluie modérée (< 10 mm)"] += 1 elif qte < 20.0: stats_pluie["☔ Pluie forte (< 20 mm)"] += 1 else: stats_pluie["🛟 Déluge (≥ 20 mm)"] += 1 # 4. Conversion en DataFrame pour Plotly df_pie = pd.DataFrame(list(stats_pluie.items()), columns=["Catégorie", "Nombre de jours"]) # On filtre pour ne pas afficher les catégories vides (optionnel, mais plus propre) df_pie = df_pie[df_pie["Nombre de jours"] > 0] # ========================================== # 2. AFFICHAGE EN VALEURS SIMPLES (CÔTE À CÔTE) # ========================================== # Création des deux colonnes principales (Pluie à gauche, Vent à droite) col_tableau, col_graphique = st.columns([1.1, 1],gap ="small") # --- COLONNE GAUCHE : TES INFOS DE PRÉCIPITATIONS --- with col_tableau: st.markdown(f""" 🔹 🌞 Prochain jour sec : {prochain_sans_pluie}
🔹 🌦️ Prochaine pluie : {prochaine_pluie_heures}
🔹 🌧️ Prochain jour ≥ 5mm : {prochain_plus_5mm}
🔹 💧 Prochain jour ≥ 10mm : {prochain_plus_10mm}
🔹 🛟 Prochain jour ≥ 20mm : {prochain_plus_20mm} """, unsafe_allow_html=True) with col_graphique: if not df_pie.empty: fig_pie = px.pie( df_pie, values="Nombre de jours", names="Catégorie", hole=0, color="Catégorie", color_discrete_map={ "🌞 Sec (0 mm)": "#f1c40f", "🌦️ Pluie faible (< 5 mm)": "#74b9ff", "🌧️ Pluie modérée (< 10 mm)": "#0984e3", "☔ Pluie forte (< 20 mm)": "#5f27cd", "🛟 Déluge (≥ 20 mm)": "#2c3e50" } ) # Ajustements du texte à l'intérieur fig_pie.update_traces( textinfo="value+percent", textposition="inside", insidetextfont=dict(size=11), # On ajuste légèrement le domaine pour laisser de la place à droite domain=dict(x=[0.0, 0.7], y=[0.1, 0.9]) ) # CONFIGURATION DE LA LÉGENDE À DROITE fig_pie.update_layout( height=250, # <-- Force le composant à être très compact en hauteur (en pixels) showlegend=True, margin=dict(t=10, b=10, l=10, r=10), # Réduit les marges intérieures au minimum legend=dict( font=dict(size=13), orientation="v", # <-- "v" pour Verticale yanchor="middle", # Centre la légende verticalement y=0.5, # Position au milieu (0.5 = 50% de la hauteur) xanchor="left", # Aligne le bord gauche de la légende x=0.75 # Place la légende à droite du camembert (qui s'arrête à 0.7) ) ) # Affichage st.plotly_chart(fig_pie, use_container_width=True) else: st.info("Pas de statistiques de répartition disponibles.") # ========================================================= # 1. CALCUL DU CUMUL PROGRESSIF # ========================================================= cumul_progressif = [] total_accumule = 0.0 for d in dates_triees: meteo_jour = donnees_meteo_harmonisees[d] # Nettoyage de la pluie du jour qte_pluie = float(str(meteo_jour.get('top_qte_pluie', '0.0')).replace(' mm', '').replace('--', '0').strip()) # On ajoute la pluie du jour au total précédent total_accumule += qte_pluie cumul_progressif.append(round(total_accumule, 1)) col_opt1, col_opt2 = st.columns(2) with col_opt1: # True par défaut pour que le graphique soit visible à l'ouverture afficher_graph = st.checkbox("📈 Graphique de cumul", value=False) with col_opt2: # False par défaut pour garder l'interface épurée afficher_tableau = st.checkbox("📋Détail", value=False) # ========================================================= # 2. GRAPHIQUE DU CUMUL (PLOTLY) # ========================================================= if afficher_graph: # 1. PRÉPARATION DES DATES AU FORMAT DD/MM dates_jour_mois = [ datetime.datetime.strptime(d, "%Y-%m-%d").strftime("%d/%m") for d in dates_triees ] fig_cumul = go.Figure() # Ajout de la ligne de cumul fig_cumul.add_trace(go.Scatter( x=dates_jour_mois, y=cumul_progressif, mode='lines+markers', name='Cumul (mm)', line=dict(color='#2980b9', width=3, shape='spline'), fill='tozeroy', fillcolor='rgba(41, 128, 185, 0.1)', hovertemplate='Date : %{x}
Cumul total : %{y} mm' )) fig_cumul.update_layout( title="📈 Évolution du cumul des précipitations (mm)", height=280, # 🟢 Ta hauteur d'origine est conservée ici margin=dict(l=10, r=10, t=40, b=50), paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', hovermode="x unified", dragmode=False, # 🟢 VERROUILLAGE : Bloque l'outil de sélection/zoom xaxis=dict( type='category', showgrid=False, tickangle=-90, tickfont=dict(style='normal', size=11), fixedrange=True # 🟢 VERROUILLAGE : Bloque le zoom X ), yaxis=dict( title="Eau cumulée (mm)", gridcolor='rgba(128,128,128,0.15)', fixedrange=True # 🟢 VERROUILLAGE : Bloque le zoom Y ) ) # 2. CONFIGURATION DES OPTIONS DE SÉCURITÉ config_options = { "scrollZoom": False, # Bloque le zoom à la molette "displayModeBar": False, # Masque la barre d'outils Plotly "doubleClick": False # Désactive le double-clic de réinitialisation } # 3. INJECTION DU CSS POUR BLOQUER LA SÉLECTION DE TEXTE BLEUE # Cela évite que l'utilisateur "sélectionne" le graphique par erreur sur mobile st.markdown( """ """, unsafe_allow_html=True ) # Affichage du graphique d'origine (100% de la largeur) avec les restrictions de comportement st.plotly_chart(fig_cumul, use_container_width=True, config=config_options) # ========================================================= # 3. TABLEAU RÉCAPITULATIF DES DONNÉES # ========================================================= if afficher_tableau: # Préparation des lignes du tableau donnees_tableau = { "Date": [datetime.datetime.strptime(d, "%Y-%m-%d").strftime("%d/%m/%Y") for d in dates_triees], "Pluie du jour": [f"{float(str(donnees_meteo_harmonisees[d].get('top_qte_pluie', '0.0')).replace(' mm', '').replace('--', '0').strip())} mm" for d in dates_triees], "Cumul global": [f"{v} mm" for v in cumul_progressif] } # Création du DataFrame Pandas df_cumul = pd.DataFrame(donnees_tableau) # Affichage du tableau propre dans Streamlit (sans les index) st.markdown("###### 📋 Détail des volumes par jour") st.dataframe(df_cumul, use_container_width=True, hide_index=True) with temperature: st.markdown(f"#### 🌡️ {texte_periode}") # ========================================== # 1. CALCULS DES SEUILS DE TEMPÉRATURE & CATÉGORIES # ========================================== prochain_plus_30 = None prochain_plus_25 = None prochain_plus_20 = None prochain_moins_10 = None prochain_moins_5 = None prochain_moins_0 = None # Initialisation des compteurs pour le camembert stats_temp = { "🥵 Canicule (≥ 30°C)": 0, "orange_chaud": 0, # Clé temporaire propre pour l'émoji "🟡 Doux (20°C à 25°C)": 0, "🟢 Frais (10°C à 20°C)": 0, "🥶 Froid (< 10°C)": 0 } # Astuce pour l'émoji orange qui pose parfois problème en clé brute dans les scripts stats_temp["🟠 Chaud (25°C à 30°C)"] = stats_temp.pop("orange_chaud") for d in dates_triees: meteo_jour = donnees_meteo_harmonisees[d] tmax = float(str(meteo_jour.get('top_tmax', '0')).replace('--', '0').strip()) tmin = float(str(meteo_jour.get('top_tmin', '0')).replace('--', '0').strip()) date_fr = datetime.datetime.strptime(d, "%Y-%m-%d").strftime("%d/%m/%Y") # --- Échéances (Ta logique existante) --- if tmax >= 30.0 and prochain_plus_30 is None: prochain_plus_30 = date_fr if tmax >= 25.0 and prochain_plus_25 is None: prochain_plus_25 = date_fr if tmax >= 20.0 and prochain_plus_20 is None: prochain_plus_20 = date_fr if tmin <= 10.0 and prochain_moins_10 is None: prochain_moins_10 = date_fr if tmin <= 5.0 and prochain_moins_5 is None: prochain_moins_5 = date_fr if tmin <= 0.0 and prochain_moins_0 is None: prochain_moins_0 = date_fr # --- Classement exclusif pour le Camembert (Basé sur Tmax) --- if tmax >= 30.0: stats_temp["🥵 Canicule (≥ 30°C)"] += 1 elif tmax >= 25.0: stats_temp["🟠 Chaud (25°C à 30°C)"] += 1 elif tmax >= 20.0: stats_temp["🟡 Doux (20°C à 25°C)"] += 1 elif tmax >= 10.0: stats_temp["🟢 Frais (10°C à 20°C)"] += 1 else: stats_temp["🥶 Froid (< 10°C)"] += 1 # Sécurités d'affichage prochain_plus_30 = prochain_plus_30 or "Aucun" prochain_plus_25 = prochain_plus_25 or "Aucun" prochain_plus_20 = prochain_plus_20 or "Aucun" prochain_moins_10 = prochain_moins_10 or "Aucun" prochain_moins_5 = prochain_moins_5 or "Aucun" prochain_moins_0 = prochain_moins_0 or "Aucun" # Préparation du DataFrame pour Plotly df_temp_pie = pd.DataFrame(list(stats_temp.items()), columns=["Catégorie", "Nombre de jours"]) df_temp_pie_filtre = df_temp_pie[df_temp_pie["Nombre de jours"] > 0] # ========================================== # 2. AFFICHAGE EN COLONNES (Infos à gauche, Graphique à droite) # ========================================== st.markdown(" ") col_gauche_infos, col_droite_graph = st.columns([1.1, 1], gap="small") # --- COLONNE GAUCHE : TES BLOCS CHAUD / FROID EXTENSIBLES --- with col_gauche_infos: # Sous-bloc Chaud st.markdown("🥵 Chaleur (Max)", unsafe_allow_html=True) st.markdown(f""" 🔴 Prochain jour ≥ 30°C : {prochain_plus_30}
🟠 Prochain jour ≥ 25°C : {prochain_plus_25}
🟡 Prochain jour ≥ 20°C : {prochain_plus_20} """, unsafe_allow_html=True) st.markdown(" ") # Petit espace # Sous-bloc Froid st.markdown("🥶 Fraîcheur (Min)", unsafe_allow_html=True) st.markdown(f""" 🟢 Prochain jour ≤ 10°C : {prochain_moins_10}
🔵 Prochain jour ≤ 5°C : {prochain_moins_5}
❄️ Prochain jour ≤ 0°C : {prochain_moins_0} """, unsafe_allow_html=True) # --- COLONNE DROITE : LE CAMEMBERT PETIT FORMAT --- with col_droite_graph: if not df_temp_pie_filtre.empty: fig_temp_pie = px.pie( df_temp_pie_filtre, values="Nombre de jours", names="Catégorie", hole=0, # Camembert classique (plein) color="Catégorie", # Mappage de couleurs cohérent avec la météo color_discrete_map={ "🥵 Canicule (≥ 30°C)": "#d63031", # Rouge vif "🟠 Chaud (25°C à 30°C)": "#e67e22", # Orange "🟡 Doux (20°C à 25°C)": "#f1c40f", # Jaune "🟢 Frais (10°C à 20°C)": "#2ecc71", # Vert "🥶 Froid (< 10°C)": "#74b9ff" # Bleu clair } ) # Ajustement du texte interne fig_temp_pie.update_traces( textinfo="value+percent", textposition="inside", insidetextfont=dict(size=11), domain=dict(x=[0.0, 0.65], y=[0.0, 1.0]) # Diminue le disque pour la légende ) # Configuration de la taille et de la légende verticale à droite fig_temp_pie.update_layout( height=230, # Hauteur restreinte pour éviter l'espace vide showlegend=True, margin=dict(t=10, b=10, l=10, r=10), legend=dict( font=dict(size=12), orientation="v", yanchor="middle", y=0.5, xanchor="left", x=0.7 ) ) st.plotly_chart(fig_temp_pie, use_container_width=True) else: st.info("Aucune donnée pour générer le graphique thermique.") with vent: st.markdown(f"#### 💨 {texte_periode}") # ========================================== # 1. CALCULS DES ÉCHÉANCES & INITIALISATION # ========================================== prochain_violent = None # > 80 prochain_fort = None # <= 80 (donc > 50) prochain_modere = None # <= 50 (donc > 30) prochain_leger = None # <= 30 (donc > 15) # Initialisation des compteurs pour le camembert stats_vent = { "🍃 Nul (< 15 km/h)": 0, "🌬️ Léger (≤ 30 km/h)": 0, "💨 Modéré (≤ 50 km/h)": 0, "🪁 Fort (≤ 80 km/h)": 0, "🌪️ Violent (> 80 km/h)": 0 } for d in dates_triees: meteo_jour = donnees_meteo_harmonisees[d] # Extraction et nettoyage de la valeur de rafale numérique rafale = float(str(meteo_jour.get('top_raf', '0')).replace(' km/h', '').replace('--', '0').strip()) date_fr = datetime.datetime.strptime(d, "%Y-%m-%d").strftime("%d/%m/%Y") # --- Tri dans les tranches & capture de la première échéance --- if rafale < 15.0: stats_vent["🍃 Nul (< 15 km/h)"] += 1 elif rafale <= 30.0: stats_vent["🌬️ Léger (≤ 30 km/h)"] += 1 if prochain_leger is None: prochain_leger = date_fr elif rafale <= 50.0: stats_vent["💨 Modéré (≤ 50 km/h)"] += 1 if prochain_modere is None: prochain_modere = date_fr elif rafale <= 80.0: stats_vent["🪁 Fort (≤ 80 km/h)"] += 1 if prochain_fort is None: prochain_fort = date_fr else: stats_vent["🌪️ Violent (> 80 km/h)"] += 1 if prochain_violent is None: prochain_violent = date_fr # Sécurités d'affichage si une catégorie n'apparaît jamais prochain_violent = prochain_violent or "Aucun" prochain_fort = prochain_fort or "Aucun" prochain_modere = prochain_modere or "Aucun" prochain_leger = prochain_leger or "Aucun" # ========================================== # 2. PRÉPARATION DES DONNÉES DU GRAPH # ========================================== df_vent_tableau = pd.DataFrame(list(stats_vent.items()), columns=["Tranche de vent", "Nombre de jours"]) df_vent_pie_filtre = df_vent_tableau[df_vent_tableau["Nombre de jours"] > 0] # ========================================== # 3. AFFICHAGE EN COLONNES (Côte à côte) # ========================================== st.markdown(" ") col_gauche_vent, col_droite_vent = st.columns([1.1, 1], gap="small") # --- COLONNE GAUCHE : UNIQUEMENT LES ÉCHÉANCES --- with col_gauche_vent: st.markdown(f""" 🌪️ Prochain vent violent (> 80 km/h) : {prochain_violent}
🪁 Prochain vent fort (≤ 80 km/h) : {prochain_fort}
💨 Prochain vent modéré (≤ 50 km/h) : {prochain_modere}
🌬️ Prochain vent léger (≤ 30 km/h) : {prochain_leger} """, unsafe_allow_html=True) # --- COLONNE DROITE : LE CAMEMBERT COMPACT --- with col_droite_vent: if not df_vent_pie_filtre.empty: fig_vent_pie = px.pie( df_vent_pie_filtre, values="Nombre de jours", names="Tranche de vent", hole=0, color="Tranche de vent", color_discrete_map={ "🍃 Nul (< 15 km/h)": "#b2bec3", "🌬️ Léger (≤ 30 km/h)": "#2ecc71", "💨 Modéré (≤ 50 km/h)": "#f1c40f", "🪁 Fort (≤ 80 km/h)": "#e67e22", "🌪️ Violent (> 80 km/h)": "#d63031" } ) fig_vent_pie.update_traces( textinfo="value+percent", textposition="inside", insidetextfont=dict(size=11), domain=dict(x=[0.0, 0.65], y=[0.0, 1.0]) ) fig_vent_pie.update_layout( height=200, # Hauteur réduite pour s'aligner pile sur les 4 lignes de texte de gauche showlegend=True, margin=dict(t=0, b=0, l=10, r=10), # Marges haut/bas à 0 pour coller au titre legend=dict( font=dict(size=11), orientation="v", yanchor="middle", y=0.5, xanchor="left", x=0.7 ) ) st.plotly_chart(fig_vent_pie, use_container_width=True) else: st.info("Aucune donnée disponible pour générer le graphique du vent.") with serie: st.markdown(f"#### 📅 {texte_periode}") # Fonction rapide pour accorder le mot "jour" (à définir une seule fois) def formater_jours(valeur): if valeur == 0: return " " elif valeur == 1: return "1 jour" else: return f"{valeur} jours" col_opt_pluie, col_opt_temp, col_opt_vent = st.columns(3) with col_opt_pluie: voir_pluie = st.checkbox("🌧️ Précipitations", value=False) with col_opt_temp: voir_temp = st.checkbox("🌡️ Températures", value=False) with col_opt_vent: voir_vent = st.checkbox("💨 Vent", value=False) st.write("") # Petit espace après la ligne de filtres if voir_pluie: # ========================================== # 🌧️ SECTION 1 : PRÉCIPITATIONS # ========================================== st.markdown("##### 🌧️ Précipitations d'affilée") df_pluie = pd.DataFrame({ " ": [ "🌞 Jours secs", "🌦️ Jours de pluie", "🌧️ Pluie légère (< 5 mm)", "☔ Pluie moyenne (≥ 5 mm)", "💧 Pluie soutenue (≥ 10 mm)", "🛟 Déluge (≥ 20 mm)" ], "Durée maximale": [ formater_jours(records_series['sec']), formater_jours(records_series['pluie_toute']), formater_jours(records_series['pluie_nulle']), formater_jours(records_series['pluie_5mm']), formater_jours(records_series['pluie_10mm']), formater_jours(records_series['pluie_20mm']) ], "Période concernée": [ periodes_records['sec'], periodes_records['pluie_toute'], periodes_records['pluie_nulle'], periodes_records['pluie_5mm'], periodes_records['pluie_10mm'], periodes_records['pluie_20mm'] ] }) st.dataframe(df_pluie, use_container_width=True, hide_index=True) st.write("") # Petit espace vertical de séparation if voir_temp: # ========================================== # 🌡️ SECTION 2 : TEMPÉRATURES # ========================================== st.markdown("##### 🌡️ Températures d'affilée") df_temp = pd.DataFrame({ " ": [ "🥵 Canicule (≥ 30°C)", "☀️ Chaleur (≥ 25°C)", "🌤️ Douceur (≥ 20°C)", "🔵 Fraîcheur (≤ 10°C)", "❄️ Froid (≤ 5°C)", "🥶 Gel (≤ 0°C)" ], "Durée maximale": [ formater_jours(records_series['tmax_30']), formater_jours(records_series['tmax_25']), formater_jours(records_series['tmax_20']), formater_jours(records_series['tmin_10']), formater_jours(records_series['tmin_5']), formater_jours(records_series['tmin_0']) ], "Période concernée": [ periodes_records['tmax_30'], periodes_records['tmax_25'], periodes_records['tmax_20'], periodes_records['tmin_10'], periodes_records['tmin_5'], periodes_records['tmin_0'] ] }) st.dataframe(df_temp, use_container_width=True, hide_index=True) st.write("") # Petit espace vertical de séparation if voir_vent: # ========================================== # 💨 SECTION 3 : VENT # ========================================== st.markdown("##### 💨 Vent d'affilée ") df_vent = pd.DataFrame({ " ": [ "💨 Nul (< 15 km/h)", "💨 Léger (≤ 30 km/h)", "💨 Modéré (≤ 50 km/h)", "💨 Fort (≤ 80 km/h)", "💨 Violent (> 80 km/h)", ], "Durée maximale": [ formater_jours(records_series['vent_nul']), formater_jours(records_series['vent_leger']), formater_jours(records_series['vent_modere']), formater_jours(records_series['vent_fort']), formater_jours(records_series['vent_violent']), ], "Période concernée": [ periodes_records['vent_nul'], periodes_records['vent_leger'], periodes_records['vent_modere'], periodes_records['vent_fort'], periodes_records['vent_violent'], ] }) st.dataframe(df_vent, use_container_width=True, hide_index=True)