TOM / app.py
davidformation's picture
Upload app.py
7e7eeb9 verified
Raw
History Blame Contribute Delete
109 kB
# 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
@st.cache_data(ttl=3600)
def geocoder_openmeteo(ville_nom):
try:
url = "https://geocoding-api.open-meteo.com/v1/search"
res = requests.get(url, params={"name": ville_nom, "count": 1, "language": "fr"}, timeout=5)
if res.status_code == 200 and "results" in res.json():
return res.json()["results"][0]
except Exception:
pass
return None
@st.cache_data(ttl=3600)
def fetch_openweather(ville_nom, api_key):
if not api_key: return None
try:
url = "https://api.openweathermap.org/data/2.5/forecast"
params = {'q': ville_nom, 'appid': api_key, 'units': 'metric', 'lang': 'fr'}
res = requests.get(url, params=params, timeout=5)
return res.json() if res.status_code == 200 else None
except Exception:
return None
@st.cache_data(ttl=3600)
def fetch_openmeteo_global(lat, lon):
try:
url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": lat, "longitude": lon,
"hourly": ["temperature_2m", "wind_speed_10m", "wind_direction_10m", "weather_code", "precipitation_probability", "precipitation"],
"daily": ["temperature_2m_min", "temperature_2m_max", "wind_speed_10m_max", "wind_gusts_10m_max", "precipitation_probability_max", "precipitation_sum"],
"forecast_days": 16,
"timezone": "auto"
}
res = requests.get(url, params=params, timeout=5)
return res.json() if res.status_code == 200 else None
except Exception:
return None
@st.cache_data(ttl=3600)
def fetch_openmeteo_meteofrance(lat, lon):
try:
url = "https://api.open-meteo.com/v1/meteofrance"
params = {"latitude": lat, "longitude": lon, "hourly": ["temperature_2m", "wind_speed_10m", "wind_direction_10m", "wind_gusts_10m", "weather_code", "precipitation_probability", "precipitation"], "timezone": "auto"}
res = requests.get(url, params=params, timeout=5)
return res.json() if res.status_code == 200 else None
except Exception:
return None
@st.cache_data(ttl=3600)
def fetch_openmeteo_gfs(lat, lon):
try:
url = "https://api.open-meteo.com/v1/gfs"
params = {
"latitude": lat, "longitude": lon,
"hourly": ["temperature_2m", "wind_speed_10m", "wind_direction_10m", "weather_code", "precipitation_probability", "precipitation"],
"daily": ["temperature_2m_min", "temperature_2m_max", "wind_speed_10m_max", "precipitation_sum"],
"forecast_days": 16, "timezone": "auto"
}
res = requests.get(url, params=params, timeout=5)
return res.json() if res.status_code == 200 else None
except Exception:
return None
@st.cache_data(ttl=3600)
def fetch_meteoconcept(lat, lon, token):
if not token: return None
try:
url = f"https://api.meteo-concept.com/api/forecast/daily?token={token}&latlng={lat},{lon}"
res = requests.get(url, timeout=5)
if res.status_code == 200:
return res.json()
elif res.status_code == 429:
st.sidebar.error("⚠️ Météo-Concept : Limite d'appels dépassée.")
elif res.status_code == 403:
st.sidebar.error("⚠️ Météo-Concept : Quota journalier épuisé.")
except Exception:
pass
return None
@st.cache_data(ttl=3600)
def fetch_weatherapi(ville_nom, api_key):
if not api_key: return None
try:
url = "https://api.weatherapi.com/v1/forecast.json"
# Demande de 10 jours de prévisions (limite maximale gratuite classique de l'API)
params = {'key': api_key, 'q': ville_nom, 'days': 10, 'aqi': 'no', 'alerts': 'no'}
res = requests.get(url, params=params, timeout=5)
return res.json() if res.status_code == 200 else None
except Exception:
return None
# INIT
CHEMIN_CONFIG = "config"
JOURS_FR = {"Mon": "Lun", "Tue": "Mar", "Wed": "Mer", "Thu": "Jeu", "Fri": "Ven", "Sat": "Sam", "Sun": "Dim"}
MOIS_FR = {"Jan": "janv.", "Feb": "févr.", "Mar": "mars", "Apr": "avril", "May": "mai", "Jun": "juin", "Jul": "juil.", "Aug": "août", "Sep": "sept.", "Oct": "oct.", "Nov": "nov.", "Dec": "déc."}
DIR_VERS_FLECHE = {"N": "↓", "NE": "↙", "E": "←", "SE": "↖", "S": "↑", "SO": "↗", "O": "→", "NO": "↘", "--": "--"}
DIR_VERS_DEG = {"N": 0, "NE": 45, "E": 90, "SE": 135, "S": 180, "SO": 225, "O": 270, "NO": 315}
tz_paris = pytz.timezone('Europe/Paris')
maintenant = datetime.datetime.now(tz_paris)
SAINTS = charger_dictionnaire_depuis_fichier(os.path.join(CHEMIN_CONFIG, "saints.txt"))
PROVERBES_JOURS = charger_dictionnaire_depuis_fichier(os.path.join(CHEMIN_CONFIG, "proverbes_jour.txt"))
PROVERBES_MOIS = charger_dictionnaire_depuis_fichier(os.path.join(CHEMIN_CONFIG, "proverbes_mois.txt"))
wmo_codes = {0: "Clair", 1: "Peu nuageux", 2: "Nuageux", 3: "Couvert", 45: "Brouillard", 51: "Bruine", 61: "Pluie", 71: "Neige", 80: "Averses", 95: "Orage"}
# CHARGEMENT DES CLÉS API & TOKENS
api_key_ow = None
token_mc = None
api_key_wa = None
config_dir = "config"
config_files = ["openweather.env", "meteoconcept.env", "weatherapi.env"]
for file in config_files:
file_path = os.path.join(config_dir, file)
if os.path.exists(file_path):
load_dotenv(dotenv_path=file_path, override=True)
api_key_ow = os.environ.get("OPENWEATHER_API_KEY")
token_mc = os.environ.get("METEOCONCEPT_API_KEY")
api_key_wa = os.environ.get("WEATHERAPI_API_KEY")
if "ville_memorisee" not in st.session_state:
st.session_state["ville_memorisee"] = "Aubigny-en-Artois"
# CONFIGURATION DE LA PAGE
st.set_page_config(page_title="TOM", page_icon="🐸", layout="centered")
# Injection CSS Global Harmonisé
st.markdown(
"""
<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>&nbsp;</div>")
if proba_format != "--":
aff_prob = "(" + proba_format + ")"
if qte_clean in ["0", "0.0", "--"]:
ligne_pluie_api = (
f"<div>&nbsp;</div>" # &nbsp; est un espace invisible pour garder la hauteur de la cellule
f"<div>&nbsp;</div>" # &nbsp; 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>&nbsp;</div>")
if top_p_format != "--":
aff_prob = "(" + top_p_format + ")"
if top_q_clean in ["0", "0.0", "--"]:
ligne_pluie_synth = (
f"<div>&nbsp;</div>" # &nbsp; est un espace invisible pour garder la hauteur de la cellule
f"<div>&nbsp;</div>" # &nbsp; 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)