Ajout de couleurs en fonction du temps
Utilisation de graphiques radiaux pour la représentation de la direction du vent
|
Before Width: | Height: | Size: 317 KiB After Width: | Height: | Size: 529 KiB |
|
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 430 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 380 KiB After Width: | Height: | Size: 623 KiB |
|
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 414 KiB |
|
Before Width: | Height: | Size: 344 KiB After Width: | Height: | Size: 400 KiB |
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 383 KiB |
|
Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 395 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 303 KiB After Width: | Height: | Size: 497 KiB |
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 359 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 458 KiB |
|
Before Width: | Height: | Size: 286 KiB After Width: | Height: | Size: 454 KiB |
|
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 349 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 377 KiB After Width: | Height: | Size: 503 KiB |
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 374 KiB |
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 222 KiB |
139
meteo/plots.py
@ -2,8 +2,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
from matplotlib.colors import Normalize
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
@ -17,12 +19,19 @@ def plot_scatter_pair(
|
|||||||
output_path: str | Path,
|
output_path: str | Path,
|
||||||
*,
|
*,
|
||||||
sample_step: int = 10,
|
sample_step: int = 10,
|
||||||
|
color_by_time: bool = True,
|
||||||
|
cmap: str = "viridis",
|
||||||
) -> Path:
|
) -> Path:
|
||||||
"""
|
"""
|
||||||
Trace un nuage de points (scatter) pour une paire de variables.
|
Trace un nuage de points (scatter) pour une paire de variables.
|
||||||
|
|
||||||
- On sous-échantillonne les données avec `sample_step` (par exemple,
|
- On sous-échantillonne les données avec `sample_step` (par exemple,
|
||||||
1 point sur 10) pour éviter un graphique illisible.
|
1 point sur 10) pour éviter un graphique illisible.
|
||||||
|
- Si `color_by_time` vaut True et que l'index est temporel, les points
|
||||||
|
sont colorés du plus ancien (sombre) au plus récent (clair).
|
||||||
|
- Lorsque l'axe Y correspond à la direction du vent, on bascule sur
|
||||||
|
un graphique polaire plus adapté (0° = Nord, sens horaire) avec
|
||||||
|
un rayon normalisé : centre = valeur minimale, bord = maximale.
|
||||||
"""
|
"""
|
||||||
output_path = Path(output_path)
|
output_path = Path(output_path)
|
||||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
@ -33,14 +42,128 @@ def plot_scatter_pair(
|
|||||||
if sample_step > 1:
|
if sample_step > 1:
|
||||||
df_pair = df_pair.iloc[::sample_step, :]
|
df_pair = df_pair.iloc[::sample_step, :]
|
||||||
|
|
||||||
plt.figure()
|
use_polar = var_y.key == "wind_direction"
|
||||||
plt.scatter(df_pair[var_x.column], df_pair[var_y.column], s=5, alpha=0.5)
|
|
||||||
plt.xlabel(f"{var_x.label} ({var_x.unit})")
|
if use_polar:
|
||||||
plt.ylabel(f"{var_y.label} ({var_y.unit})")
|
fig, ax = plt.subplots(subplot_kw={"projection": "polar"})
|
||||||
plt.title(f"{var_y.label} en fonction de {var_x.label}")
|
else:
|
||||||
plt.tight_layout()
|
fig, ax = plt.subplots()
|
||||||
plt.savefig(output_path, dpi=150)
|
|
||||||
plt.close()
|
scatter_kwargs: dict = {"s": 5, "alpha": 0.5}
|
||||||
|
colorbar_meta: dict | None = None
|
||||||
|
|
||||||
|
if color_by_time and isinstance(df_pair.index, pd.DatetimeIndex):
|
||||||
|
idx = df_pair.index
|
||||||
|
timestamps = idx.view("int64")
|
||||||
|
time_span = np.ptp(timestamps)
|
||||||
|
norm = (
|
||||||
|
Normalize(vmin=timestamps.min(), vmax=timestamps.max())
|
||||||
|
if time_span > 0
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
scatter_kwargs |= {"c": timestamps, "cmap": cmap}
|
||||||
|
if norm is not None:
|
||||||
|
scatter_kwargs["norm"] = norm
|
||||||
|
colorbar_meta = {
|
||||||
|
"index": idx,
|
||||||
|
"timestamps": timestamps,
|
||||||
|
"time_span": time_span,
|
||||||
|
}
|
||||||
|
|
||||||
|
if use_polar:
|
||||||
|
theta = np.deg2rad(df_pair[var_y.column].to_numpy(dtype=float) % 360.0)
|
||||||
|
radius_raw = df_pair[var_x.column].to_numpy(dtype=float)
|
||||||
|
|
||||||
|
if radius_raw.size == 0:
|
||||||
|
radius = radius_raw
|
||||||
|
value_min = value_max = float("nan")
|
||||||
|
else:
|
||||||
|
value_min = float(np.min(radius_raw))
|
||||||
|
value_max = float(np.max(radius_raw))
|
||||||
|
if np.isclose(value_min, value_max):
|
||||||
|
radius = np.zeros_like(radius_raw)
|
||||||
|
else:
|
||||||
|
radius = (radius_raw - value_min) / (value_max - value_min)
|
||||||
|
|
||||||
|
scatter = ax.scatter(theta, radius, **scatter_kwargs)
|
||||||
|
|
||||||
|
cardinal_angles = np.deg2rad(np.arange(0, 360, 45))
|
||||||
|
cardinal_labels = ["N", "NE", "E", "SE", "S", "SO", "O", "NO"]
|
||||||
|
ax.set_theta_zero_location("N")
|
||||||
|
ax.set_theta_direction(-1)
|
||||||
|
ax.set_xticks(cardinal_angles)
|
||||||
|
ax.set_xticklabels(cardinal_labels)
|
||||||
|
|
||||||
|
if radius_raw.size > 0:
|
||||||
|
if np.isclose(value_min, value_max):
|
||||||
|
radial_positions = [0.0]
|
||||||
|
else:
|
||||||
|
radial_positions = np.linspace(0.0, 1.0, num=5).tolist()
|
||||||
|
if np.isclose(value_min, value_max):
|
||||||
|
actual_values = [value_min]
|
||||||
|
else:
|
||||||
|
actual_values = [
|
||||||
|
value_min + pos * (value_max - value_min)
|
||||||
|
for pos in radial_positions
|
||||||
|
]
|
||||||
|
ax.set_yticks(radial_positions)
|
||||||
|
ax.set_yticklabels([f"{val:.1f}" for val in actual_values])
|
||||||
|
ax.set_rlabel_position(225)
|
||||||
|
ax.set_ylim(0.0, 1.0)
|
||||||
|
|
||||||
|
unit_suffix = f" {var_x.unit}" if var_x.unit else ""
|
||||||
|
ax.text(
|
||||||
|
0.5,
|
||||||
|
-0.1,
|
||||||
|
f"Centre = {value_min:.1f}{unit_suffix}, bord = {value_max:.1f}{unit_suffix}",
|
||||||
|
transform=ax.transAxes,
|
||||||
|
ha="center",
|
||||||
|
va="top",
|
||||||
|
fontsize=8,
|
||||||
|
)
|
||||||
|
|
||||||
|
radial_label = f"{var_x.label} ({var_x.unit})" if var_x.unit else var_x.label
|
||||||
|
ax.set_ylabel(radial_label, labelpad=20)
|
||||||
|
else:
|
||||||
|
scatter = ax.scatter(
|
||||||
|
df_pair[var_x.column],
|
||||||
|
df_pair[var_y.column],
|
||||||
|
**scatter_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
if colorbar_meta is not None:
|
||||||
|
cbar = fig.colorbar(scatter, ax=ax)
|
||||||
|
idx = colorbar_meta["index"]
|
||||||
|
timestamps = colorbar_meta["timestamps"]
|
||||||
|
time_span = colorbar_meta["time_span"]
|
||||||
|
|
||||||
|
def _format_tick_label(ts: pd.Timestamp) -> str:
|
||||||
|
base = f"{ts.strftime('%Y-%m-%d')}\n{ts.strftime('%H:%M')}"
|
||||||
|
tz_name = ts.tzname()
|
||||||
|
return f"{base} ({tz_name})" if tz_name else base
|
||||||
|
|
||||||
|
if time_span > 0:
|
||||||
|
tick_datetimes = pd.date_range(start=idx.min(), end=idx.max(), periods=5)
|
||||||
|
tick_positions = tick_datetimes.view("int64")
|
||||||
|
tick_labels = [_format_tick_label(ts) for ts in tick_datetimes]
|
||||||
|
cbar.set_ticks(tick_positions)
|
||||||
|
cbar.set_ticklabels(tick_labels)
|
||||||
|
else:
|
||||||
|
cbar.set_ticks([timestamps[0]])
|
||||||
|
ts = idx[0]
|
||||||
|
cbar.set_ticklabels([_format_tick_label(ts)])
|
||||||
|
|
||||||
|
cbar.set_label("Temps (ancien → récent)")
|
||||||
|
|
||||||
|
if use_polar:
|
||||||
|
ax.set_title(f"{var_y.label} en fonction de {var_x.label}")
|
||||||
|
else:
|
||||||
|
ax.set_xlabel(f"{var_x.label} ({var_x.unit})")
|
||||||
|
ax.set_ylabel(f"{var_y.label} ({var_y.unit})")
|
||||||
|
ax.set_title(f"{var_y.label} en fonction de {var_x.label}")
|
||||||
|
fig.tight_layout()
|
||||||
|
fig.savefig(output_path, dpi=150)
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
return output_path.resolve()
|
return output_path.resolve()
|
||||||
|
|
||||||
|
|||||||