|
|
|
|
|
|
|
|
|
|
|
import os |
|
|
import numpy as np |
|
|
import pandas as pd |
|
|
import matplotlib.pyplot as plt |
|
|
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas |
|
|
from PIL import Image |
|
|
from typing import Optional |
|
|
from api_call import get_kit_measurements_df |
|
|
|
|
|
|
|
|
def weather_data_visualisation( |
|
|
kit: int = 1001, |
|
|
save_to_disk: bool = False, |
|
|
df: Optional[pd.DataFrame] = None, |
|
|
) -> Image.Image: |
|
|
"""Generates a 'Monsoon Mandala' visualization from weather data. |
|
|
|
|
|
Args: |
|
|
kit: The kit ID to fetch data for, if df is not provided. |
|
|
save_to_disk: Whether to save the output image to disk. |
|
|
df: An optional DataFrame with pre-loaded weather data. If None, data will be fetched. |
|
|
|
|
|
Returns: |
|
|
A PIL.Image object of the generated visualization. |
|
|
""" |
|
|
|
|
|
if df is None: |
|
|
df = get_kit_measurements_df(kit) |
|
|
|
|
|
|
|
|
if df is None or df.empty: |
|
|
raise ValueError(f"No data available for kit {kit}") |
|
|
|
|
|
|
|
|
|
|
|
df.drop(columns=['kit_id', "unit"], inplace=True, errors='ignore') |
|
|
df.dropna(inplace=True) |
|
|
|
|
|
|
|
|
if not isinstance(df.index, pd.DatetimeIndex): |
|
|
df['timestamp'] = pd.to_datetime(df['timestamp']) |
|
|
df.set_index('timestamp', inplace=True) |
|
|
|
|
|
df = df.pivot(columns='sensor', values='value') |
|
|
|
|
|
|
|
|
for col in ['ftTemp', 'gbHum', 'NH3', 'C3H8', 'CO']: |
|
|
if col not in df.columns: |
|
|
df[col] = 0.0 |
|
|
|
|
|
|
|
|
|
|
|
theta = np.linspace(0, 2 * np.pi, len(df), endpoint=False) |
|
|
|
|
|
|
|
|
def norm(x): |
|
|
x = np.asarray(x) |
|
|
if np.nanmax(x) - np.nanmin(x) == 0: |
|
|
return np.zeros_like(x) |
|
|
return (x - np.nanmin(x)) / (np.nanmax(x) - np.nanmin(x)) |
|
|
|
|
|
T = norm(df['ftTemp'].values) |
|
|
H = norm(df['gbHum'].values) |
|
|
R = norm(df['NH3'].values) |
|
|
W = norm(df['C3H8'].values) |
|
|
L = norm(df['CO'].values) |
|
|
|
|
|
|
|
|
radius = 0.45 + 0.35*(0.5*T + 0.3*H + 0.2*L) |
|
|
|
|
|
|
|
|
stroke = 0.3 + 3.2*W |
|
|
dots = 5 + 60*R |
|
|
|
|
|
|
|
|
def smooth(x, k=21): |
|
|
if k < 3: |
|
|
return x |
|
|
w = np.ones(k)/k |
|
|
return np.convolve(x, w, mode="same") |
|
|
|
|
|
radius_smooth = smooth(radius, k=31) |
|
|
|
|
|
|
|
|
fig = plt.figure(figsize=(8, 8)) |
|
|
ax = plt.subplot(111, projection="polar") |
|
|
ax.set_theta_direction(-1) |
|
|
ax.set_theta_offset(np.pi/2.0) |
|
|
ax.set_axis_off() |
|
|
|
|
|
|
|
|
ax.plot(theta, radius_smooth, linewidth=2.0) |
|
|
|
|
|
|
|
|
for k in [3, 7, 13]: |
|
|
ax.plot(theta, smooth(radius * (0.85 + 0.05*np.sin(k*theta)), k=15), linewidth=0.8) |
|
|
|
|
|
|
|
|
ax.scatter(theta[::3], (radius_smooth*0.92)[::3], s=dots[::3], alpha=0.6) |
|
|
|
|
|
|
|
|
for th, rr, sw in zip(theta[::12], radius_smooth[::12], stroke[::12]): |
|
|
ax.plot([th, th], [rr*0.75, rr*0.98], linewidth=sw*0.12, alpha=0.8) |
|
|
|
|
|
plt.tight_layout() |
|
|
|
|
|
|
|
|
canvas = FigureCanvas(fig) |
|
|
canvas.draw() |
|
|
buf = np.asarray(canvas.buffer_rgba()) |
|
|
pil_img = Image.fromarray(buf, mode="RGBA").convert("RGB") |
|
|
|
|
|
|
|
|
if save_to_disk: |
|
|
png_path = "output/monsoon_mandala_example.png" |
|
|
svg_path = "output/monsoon_mandala_example.svg" |
|
|
try: |
|
|
os.makedirs(os.path.dirname(png_path), exist_ok=True) |
|
|
fig.savefig(png_path, dpi=300, bbox_inches="tight", pad_inches=0.05) |
|
|
fig.savefig(svg_path, bbox_inches="tight", pad_inches=0.05) |
|
|
except Exception: |
|
|
|
|
|
pass |
|
|
|
|
|
plt.close(fig) |
|
|
return pil_img |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
|
|
img = weather_data_visualisation(kit=1001, save_to_disk=True) |
|
|
if img: |
|
|
print("Visualization generated and saved to 'output/'.") |
|
|
img.show() |