Spaces:
Sleeping
Sleeping
| # 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'<img src="data:{type_mime};base64,{encoded_string}" style="height: {hauteur}px; vertical-align: middle; margin-right: 5px; {style_additionnel}"/>' | |
| 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("<b>", "").replace("</b>", "").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("<b>", "").replace("</b>", "").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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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( | |
| """ | |
| <style> | |
| /* Conteneur horizontal scrollable */ | |
| /* Conteneur transformé en Grille Fixe 4x4 */ | |
| .scroll-calendar { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); /* Crée 4 colonnes égales */ | |
| gap: 6px; /* Espace réduit entre les cellules */ | |
| padding: 5px 0px; | |
| overflow: hidden; | |
| } | |
| /* Astuce pour centrer les 2 dernières cellules sur la 4ème ligne */ | |
| /* Le 13ème élément commencera à la colonne 2 */ | |
| .scroll-calendar > a:nth-child(13) { | |
| grid-column: 2; | |
| } | |
| /* Style d'une cellule jour (Taille réduite) */ | |
| .day-card { | |
| background-color: rgba(128, 128, 128, 0.08); | |
| border: 1px solid rgba(128, 128, 128, 0.2); | |
| border-radius: 8px; /* Arrondi légèrement réduit */ | |
| padding: 6px 4px; /* Espacement intérieur très compact */ | |
| text-align: center; | |
| cursor: pointer; | |
| transition: transform 0.2s, background-color 0.2s; | |
| text-decoration: none !important; | |
| color: inherit !important; | |
| } | |
| .day-card:hover { | |
| background-color: rgba(128, 128, 128, 0.15); | |
| transform: translateY(-1px); | |
| } | |
| /* Style de la cellule sélectionnée (Taille réduite) */ | |
| .day-card-active { | |
| background-color: #3498db !important; | |
| color: white !important; | |
| border: 1px solid #2980b9; | |
| border-radius: 8px; | |
| padding: 6px 4px; /* Aligné sur la taille réduite */ | |
| text-align: center; | |
| text-decoration: none !important; | |
| box-shadow: 0 2px 8px rgba(52, 152, 219, 0.3); | |
| } | |
| .custom-weather-table table { width: 100%; border-collapse: collapse; margin-bottom: 20px; } | |
| .custom-weather-table th, .custom-weather-table td { text-align: center !important; vertical-align: middle !important; padding: 10px 6px !important; line-height: 1.4 !important; } | |
| .custom-weather-table th { background-color: rgba(128, 128, 128, 0.15); font-weight: bold; } | |
| .custom-weather-table tbody tr:first-child { | |
| background-color: rgba(52, 152, 219, 0.15) !important; | |
| font-weight: bold !important; | |
| font-size: 1.12rem !important; | |
| border-bottom: 2px solid rgba(52, 152, 219, 0.4); | |
| } | |
| /* --- AJOUTS POUR LES BOUTONS DU CALENDRIER --- */ | |
| div[data-testid="stButton"] button div[data-testid="stMarkdownContainer"] p { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| white-space: pre-line; | |
| } | |
| .cal-icon { | |
| font-size: 2.8rem !important; /* Augmenté (anciennement 2.2rem) */ | |
| margin: 8px 0 !important; | |
| display: inline-block; | |
| } | |
| </style> | |
| """, | |
| 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( | |
| """ | |
| <style> | |
| /* Supprime la marge blanche tout en haut */ | |
| .block-container { | |
| padding-top: 1rem !important; | |
| padding-bottom: 1rem !important; | |
| } | |
| /* Cache le header Streamlit */ | |
| header { | |
| visibility: hidden; | |
| height: 0px; | |
| } | |
| </style> | |
| """, | |
| 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 <b> et </b>, et recolle le tout | |
| titre_formate = " ".join([ | |
| f"<b>{mot[0]}</b>{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""" | |
| <div style="display: flex; align-items: center; gap: 15px; margin-top: -15px; flex-wrap: nowrap;"> | |
| <img src="data:image/png;base64,{data_logo}" style="width: 55px; height: 55px; object-fit: contain; flex-shrink: 0; background: transparent;"> | |
| <h1 style="margin:0; padding:0; font-size: clamp(1.1rem, 3.8vw, 1.4rem); font-weight: normal; line-height: 1.2;"> | |
| {titre_formate} | |
| </h1> | |
| </div> | |
| """, | |
| unsafe_allow_html=True | |
| ) | |
| else: | |
| st.markdown( | |
| f""" | |
| <div style="display: flex; align-items: center; gap: 15px; margin-top: -15px; flex-wrap: nowrap;"> | |
| <span style="font-size: 2.8rem; line-height: 1; flex-shrink: 0;">🐸</span> | |
| <h1 style="margin:0; padding:0; font-size: clamp(1.1rem, 3.8vw, 1.4rem); font-weight: normal; line-height: 1.2;"> | |
| {titre_formate} | |
| </h1> | |
| </div> | |
| """, | |
| 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"🌡️<br>T° ({heure_actuelle_int}h)" | |
| texte_moment = f"{formater_date_fr(journee)} | {heure_actuelle_int}h" | |
| else: | |
| heure_cible_int = 12 | |
| libelle_colonne_temp = "🌡️<br>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 "<b>--</b>" 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""" | |
| <div style="background-color: rgba(128, 128, 128, 0.05); | |
| border-left: 4px solid #3498db; | |
| padding: 10px 14px; | |
| border-radius: 4px; | |
| margin-bottom: 15px; | |
| font-size: 0.92rem; | |
| line-height: 1.4;"> | |
| 😇 {saint_du_jour} | |
| <br> | |
| <i>\"{dicton_du_jour}\"</i> | |
| <br> | |
| <i>\"{proverbe_du_mois}\"</i> | |
| </div> | |
| """, | |
| 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 = '<div class="scroll-calendar">' | |
| 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""" | |
| <a href="{url_cible}" target="_self" class="{classe_carte}"> | |
| <div style="font-size: 0.82rem; font-weight: bold; margin-bottom: 1px; opacity: 0.9;">{date_courte}</div> | |
| <div style="font-size: 1.5rem; margin: 1px 0; line-height: 1.1;">{tendance_j}</div> | |
| <div style="font-size: 0.8rem; opacity: 0.95; font-weight: bold;"> | |
| <span style="color: {'#fff' if est_selectionne else '#3498db'};">{valeur_cal_tmin}°</span> | |
| <span style="color: {'#fff' if est_selectionne else '#e74c3c'}; margin-left: 3px;">{valeur_cal_tmax}°</span> | |
| </div> | |
| </a> | |
| """ | |
| html_calendrier += bloc_html | |
| html_calendrier += "</div>" | |
| 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 <tmin <= 5), | |
| "tmin_0": (tmin <= 0.0), | |
| "sec": (qte_pluie == 0.0), | |
| "vent_nul": (rafale < 15.0), | |
| "vent_leger": (15.0 <= rafale <= 30.0), | |
| "vent_modere": (31.0 <= rafale <= 50.0), | |
| "vent_fort": (51.0 <= rafale <= 80.0), | |
| "vent_violent": (rafale > 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"]], | |
| "❄️<br>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"]], | |
| "🔥<br>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"]], | |
| "🌧️<br>Proba": [f"<span style='color:#3498db;'>{jour_sel['top_proba_pluie']}</span>", 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"]], | |
| "💧<br>Total": [f"<span style='color:#3498db;'>{jour_sel['top_qte_pluie']}</span>", 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"]], | |
| "💨<br>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"]], | |
| "🌪️<br>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"]], | |
| "🧭<br>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'<div class="custom-weather-table">{df.to_html(escape=False)}</div>', 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}<span style='font-size:0.85em;'><b>Météo-France</b></span>" if logo_h_mf else "<b>Météo-France</b>" | |
| 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}<span style='font-size:0.85em;'><b>Open-Meteo</b></span>" if logo_h_om else "<b>Open-Meteo</b>" | |
| 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}<span style='font-size:0.85em;'><b>OpenWeather</b></span>" if logo_h_ow else "<b>OpenWeather</b>" | |
| 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}<span style='font-size:0.85em;'><b>Modèle GFS</b></span>" if logo_h_gfs else "<b>Modèle GFS</b>" | |
| 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}<span style='font-size:0.85em;'><b>WeatherAPI</b></span>" if logo_h_wa else "<b>WeatherAPI</b>" | |
| 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( | |
| '<div style="text-align: center; color:gray; font-size:0.92rem; line-height: 1.3;">' | |
| '<div>--</div><div>--</div><div>--</div><div>--</div></div>' | |
| ) | |
| 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"<div> </div>") | |
| if proba_format != "--": | |
| aff_prob = "(" + proba_format + ")" | |
| if qte_clean in ["0", "0.0", "--"]: | |
| ligne_pluie_api = ( | |
| f"<div> </div>" # est un espace invisible pour garder la hauteur de la cellule | |
| f"<div> </div>" # est un espace invisible pour garder la hauteur de la cellule | |
| ) | |
| else: | |
| ligne_pluie_api = ( | |
| f"<div style='font-size:0.85em; color:#3498db;'>💧 {qte_clean} mm " | |
| f"{aff_prob}</div>" | |
| ) | |
| bloc_html_api = ( | |
| f"<div style='text-align: center; font-size:0.92rem; line-height: 1.3;'>" | |
| f"<div style='font-size: 1.1em;'>{v_api[0]}</div>" | |
| f"<div><b>{v_api[1]}°C</b></div>" | |
| f"<div><b>{v_api[2]}</b> {v_api[3]}</div>" | |
| f"{ligne_pluie_api}" | |
| f"</div>" | |
| ) | |
| 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"<div> </div>") | |
| if top_p_format != "--": | |
| aff_prob = "(" + top_p_format + ")" | |
| if top_q_clean in ["0", "0.0", "--"]: | |
| ligne_pluie_synth = ( | |
| f"<div> </div>" # est un espace invisible pour garder la hauteur de la cellule | |
| f"<div> </div>" # est un espace invisible pour garder la hauteur de la cellule | |
| ) | |
| else: | |
| ligne_pluie_synth = ( | |
| f"<div style='font-size:0.88em; color:#3498db;'>💧 {qte_clean} mm </div>" | |
| f"<div style='font-size:0.88em; color:#3498db;'>{aff_prob}</div>" | |
| ) | |
| bloc_html_synth = ( | |
| f"<div style='text-align: center; font-size:1.02rem; line-height: 1.3; font-weight: bold;'>" | |
| f"<div style='font-size: 1.1em;'>{top_icone_h}</div>" | |
| f"<div>{top_t_h}</div>" | |
| f"<div>{top_v_h} {top_dir_fleche_h}</div>" | |
| # f"<div style='font-size:0.88em; color:#3498db;'>💧 {top_q_clean} ({top_p_format})</div>" | |
| f"{ligne_pluie_synth}" | |
| f"</div>" | |
| ) | |
| 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 | |
| "<b>--</b>" 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'<table class="custom-weather-table"><thead><tr><th>Heures</th>' | |
| for col in df_horaire.columns: | |
| html_table_2 += f'<th>{col}</th>' | |
| html_table_2 += '</tr></thead><tbody>' | |
| for idx, r in df_horaire.iterrows(): | |
| html_table_2 += f'<tr><td>{idx}</td>' | |
| for col in df_horaire.columns: | |
| html_table_2 += f'<td>{r[col]}</td>' | |
| html_table_2 += '</tr>' | |
| html_table_2 += '</tbody></table>' | |
| #st.markdown(html_table_2, unsafe_allow_html=True) | |
| #st.markdown(f'<div class="custom-weather-table">{df_horaire.to_html(escape=False)}</div>', unsafe_allow_html=True) | |
| #html_propre = df_horaire.to_html(escape=False).replace("\n", "") | |
| st.markdown(f'<div class="custom-weather-table">{df_horaire.to_html(escape=False)}</div>', 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}<extra></extra>' | |
| ), | |
| 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<extra></extra>' | |
| ), | |
| 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<extra></extra>' | |
| ), | |
| 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<extra></extra>' | |
| ), | |
| 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""" | |
| <div style="width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; padding-bottom: 10px;"> | |
| <div style="width: 850px; height: 480px;"> | |
| {html_graph} | |
| </div> | |
| </div> | |
| """ | |
| 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""" | |
| 🔹 <b>🌞 Prochain jour sec :</b> {prochain_sans_pluie}<br> | |
| 🔹 <b>🌦️ Prochaine pluie :</b> {prochaine_pluie_heures}<br> | |
| 🔹 <b>🌧️ Prochain jour ≥ 5mm :</b> {prochain_plus_5mm}<br> | |
| 🔹 <b>💧 Prochain jour ≥ 10mm :</b> {prochain_plus_10mm}<br> | |
| 🔹 <b>🛟 Prochain jour ≥ 20mm :</b> {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='<b>Date :</b> %{x}<br><b>Cumul total :</b> %{y} mm<extra></extra>' | |
| )) | |
| 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( | |
| """ | |
| <style> | |
| .stPlotlyChart { | |
| user-select: none; | |
| -webkit-user-select: none; | |
| } | |
| </style> | |
| """, | |
| 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("<b>🥵 Chaleur (Max)</b>", unsafe_allow_html=True) | |
| st.markdown(f""" | |
| 🔴 <b>Prochain jour ≥ 30°C :</b> {prochain_plus_30}<br> | |
| 🟠 <b>Prochain jour ≥ 25°C :</b> {prochain_plus_25}<br> | |
| 🟡 <b>Prochain jour ≥ 20°C :</b> {prochain_plus_20} | |
| """, unsafe_allow_html=True) | |
| st.markdown(" ") # Petit espace | |
| # Sous-bloc Froid | |
| st.markdown("<b>🥶 Fraîcheur (Min)</b>", unsafe_allow_html=True) | |
| st.markdown(f""" | |
| 🟢 <b>Prochain jour ≤ 10°C :</b> {prochain_moins_10}<br> | |
| 🔵 <b>Prochain jour ≤ 5°C :</b> {prochain_moins_5}<br> | |
| ❄️ <b>Prochain jour ≤ 0°C :</b> {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""" | |
| 🌪️ <b>Prochain vent violent (> 80 km/h) :</b> {prochain_violent}<br> | |
| 🪁 <b>Prochain vent fort (≤ 80 km/h) :</b> {prochain_fort}<br> | |
| 💨 <b>Prochain vent modéré (≤ 50 km/h) :</b> {prochain_modere}<br> | |
| 🌬️ <b>Prochain vent léger (≤ 30 km/h) :</b> {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) |