# -*- coding: utf-8 -*- import pandas as pd import numpy as np import matplotlib.pyplot as plt from pathlib import Path # ========================== # CONFIG # ========================== CHEM_PATH = "data_valider_Pierre_toutes_dates_hydro.xlsx" CHEM_SHEET = "tri_points" TYPE_COL = "type" # 'rivière' / 'source' HYDRO_COL = "hydrodynamique" # 'hautes-eaux' / 'moyennes-eaux' / 'étiage' KM_COL = "km" CAMP_COL = " nb campagne" # (il y a un espace initial dans le fichier) # Dossier d’export EXPORT_DIR = Path("out") EXPORT_DIR.mkdir(parents=True, exist_ok=True) # Limites de l’axe X (km) pour tous les subplots KM_XLIM = (0, 50) # mets à None pour auto; sinon tuple (min,max) # === COULEURS PAR ÉTAT HYDRODYNAMIQUE === hautes_eaux_colors = ["#EA66FF", "#E01EFF", "#C942FF", "#B300F9", "#8000B2", "#591D71", "#2C0E38"] moyennes_eaux_colors = ["#D8E6F2", "#B1CEE6", "#8BB5D9", "#649DCD", "#3E85C0", "#316A9A", "#254F73", "#5671F7", "#0000FF", "#18354D"] etiage_colors = ["#FFE4B5", "#FFA500", "#FF0000", "#8B0000"] # Valeurs EXACTES présentes dans le fichier HYDRO_STATES = [ ("hautes-eaux", hautes_eaux_colors), ("moyennes-eaux", moyennes_eaux_colors), ("étiage", etiage_colors) ] # ========================== # PARAMÈTRES À TRACER # (y_riv / y_src facultatifs — auto si omis) # ========================== parametres = [ {"variable": "Fe2+", "ylabel": "Fe2+ (mg/L)"}, {"variable": "Alcalinité", "ylabel": "HCO3- (mg/L)"}, {"variable": "Na+", "ylabel": "Na+ (mg/L)"}, {"variable": "Mg2+", "ylabel": "Mg2+ (mg/L)"}, {"variable": "T°C", "ylabel": "Température (°C)"}, {"variable": "pH", "ylabel": "pH"}, {"variable": "Cl-", "ylabel": "Cl- (mg/L)"}, {"variable": "c25°C ", "ylabel": "Conductivité (μS/cm)"}, {"variable": "DOC", "ylabel": "DOC (mg/L)"}, {"variable": "SO42-", "ylabel": "SO42- (mg/L)"}, {"variable": "SiO2", "ylabel": "SiO2 (mg/L)"}, {"variable": "PO43-", "ylabel": "PO43- (mg/L)"}, {"variable": "NO3-", "ylabel": "NO3- (mg/L)"}, {"variable": "Ca recalculé","ylabel": "Ca2+ (mg/L)"}, {"variable": "O2 mg/L", "ylabel": "O2 (mg/L)"}, {"variable": "Fluorure", "ylabel": "Fl- (mg/L)"}, {"variable": "NO2-", "ylabel": "NO2- (mg/L)"}, {"variable": "NH4+", "ylabel": "NH4+ (mg/L)"}, {"variable": "δ13C VPDB", "ylabel": "δ13C"}, ] # ========================== # UTILITAIRES # ========================== def sanitize_filename(s: str) -> str: s = str(s).strip().replace("\n", " ") for b in ['<','>',':','"','/','\\','|','?','*']: s = s.replace(b, '_') s = '_'.join(s.split()) # compresser espaces return s def nice_camp_label(c): try: cf = float(c) if cf.is_integer(): return f"camp. {int(cf)}" except Exception: pass return f"camp. {c}" def get_ylim_for(param, df_sources, df_riviere, var, qlo=0.05, qhi=0.95, pad=0.05): """ Calcule (y_src, y_riv). - Si 'y_src'/'y_riv' sont fournis dans param -> on les respecte. - Sinon: auto depuis les données (quantiles qlo–qhi) + marge 'pad'. y_riv est calculé sur TOUTES les données 'rivière' (tous états) pour que les 3 subplots partagent la même échelle. """ # --- rivière if "y_riv" in param: y_riv = tuple(param["y_riv"]) else: dr = df_riviere[[var]].dropna() if dr.empty: y_riv = (0, 1) else: lo, hi = dr[var].quantile([qlo, qhi]).values if not np.isfinite(lo) or not np.isfinite(hi) or lo == hi: lo, hi = float(dr[var].min()), float(dr[var].max()) span = max(hi - lo, 1e-12) y_riv = (lo - pad*span, hi + pad*span) # --- sources if "y_src" in param: y_src = tuple(param["y_src"]) else: ds = df_sources[[var]].dropna() if ds.empty: y_src = y_riv else: lo, hi = ds[var].quantile([qlo, qhi]).values if not np.isfinite(lo) or not np.isfinite(hi) or lo == hi: lo, hi = float(ds[var].min()), float(ds[var].max()) span = max(hi - lo, 1e-12) y_src = (lo - pad*span, hi + pad*span) return y_src, y_riv def build_campaign_color_map(df_riv, hydro_col, camp_col): """Map {state: {camp_id: color}} avec palettes par état (boucle si nécessaire).""" mapping = {} for state, palette in HYDRO_STATES: sub = df_riv.loc[df_riv[hydro_col] == state] camps = pd.unique(sub[camp_col].dropna()) camps_sorted = np.sort(camps) if len(camps_sorted) == 0: mapping[state] = {} continue colors = (palette * ((len(camps_sorted) // len(palette)) + 1))[:len(camps_sorted)] mapping[state] = {c: col for c, col in zip(camps_sorted, colors)} return mapping # ========================== # LECTURE & PRÉPA # ========================== chem = pd.read_excel(CHEM_PATH, sheet_name=CHEM_SHEET) # Harmoniser 'type' chem[TYPE_COL] = chem[TYPE_COL].astype(str).str.strip().str.lower() # Numériques utiles chem[KM_COL] = pd.to_numeric(chem[KM_COL], errors="coerce") chem[CAMP_COL] = pd.to_numeric(chem[CAMP_COL], errors="coerce") # Tri logique (km puis campagne) chem = chem.sort_values([KM_COL, CAMP_COL]) # Sous-ensembles df_sources = chem.loc[chem[TYPE_COL] == "source"].copy() df_riviere = chem.loc[chem[TYPE_COL] == "rivière"].copy() # Colonnes indispensables côté rivière for c in [HYDRO_COL, KM_COL, CAMP_COL]: if c not in df_riviere.columns: raise KeyError(f"Colonne manquante pour 'rivière': {c}") # Couleurs par campagne×état camp_color_map = build_campaign_color_map(df_riviere, HYDRO_COL, CAMP_COL) # ========================== # TRAÇAGE # ========================== for p in parametres: var, ylabel = p["variable"], p["ylabel"] if var not in chem.columns: print(f"[AVERTISSEMENT] '{var}' absent → figure ignorée.") continue y_src_limits, y_riv_limits = get_ylim_for(p, df_sources, df_riviere, var, qlo=0, qhi=1) fig, axes = plt.subplots(nrows=4, ncols=1, sharex=False, figsize=(12, 9)) # Réserve de l'espace à droite pour la légende fig.subplots_adjust(hspace=0.18, right=0.80) fig.suptitle(f"{var} — X = km (campagnes en courbes pour la rivière)", fontsize=12, x=0.01, ha="left") # 1) SOURCES — X = km (nuage gris) ax_src = axes[0] if not df_sources.empty: ds = df_sources[[KM_COL, var]].dropna() ax_src.scatter(ds[KM_COL], ds[var], s=24, alpha=0.7, edgecolors="white", linewidths=0.4, color="#666666") ax_src.set_ylabel(ylabel + "\n(sources)") ax_src.set_xlabel("km") if KM_XLIM: ax_src.set_xlim(*KM_XLIM) ax_src.set_ylim(*y_src_limits) ax_src.grid(True, linestyle=":", alpha=0.35) # 2–4) RIVIÈRE par ÉTAT — X = km ; une courbe par 'nb campagne' legend_handles, legend_labels = [], [] for i, (state, palette) in enumerate(HYDRO_STATES, start=1): ax = axes[i] sub = df_riviere.loc[df_riviere[HYDRO_COL] == state, [KM_COL, CAMP_COL, var]] sub = sub.dropna(subset=[KM_COL, var]) if not sub.empty: for camp, grp in sub.groupby(CAMP_COL): grp = grp.sort_values(KM_COL) color = camp_color_map.get(state, {}).get(camp, (palette[0] if palette else "C0")) h = ax.plot(grp[KM_COL], grp[var], marker="o", ms=4, lw=1.6, color=color)[0] lab = nice_camp_label(camp) if lab not in legend_labels: legend_handles.append(h) legend_labels.append(lab) ax.set_ylabel(f"{ylabel}\n({state})") ax.set_xlabel("km") if KM_XLIM: ax.set_xlim(*KM_XLIM) ax.set_ylim(*y_riv_limits) ax.grid(True, linestyle=":", alpha=0.35) # Légende globale (campagnes) à droite, hors du tracé if legend_handles: axes[1].legend( legend_handles, legend_labels, loc="center left", bbox_to_anchor=(1.02, 0.5), frameon=False, ncol=1, title="Campagnes", borderaxespad=0.0 ) # Export fname = sanitize_filename(f"{var}_rivieres_sources.png") outpath = EXPORT_DIR / fname fig.savefig(outpath, dpi=200, bbox_inches="tight") plt.close(fig) print(f"[OK] Export: {outpath.resolve()}")