Source code for zyra.visualization.timeseries_manager

"""Render time series from CSV or NetCDF as simple line charts."""

from __future__ import annotations

from io import BytesIO
from typing import Any, Optional

from .base import Renderer
from .styles import FIGURE_DPI, apply_matplotlib_style


[docs] class TimeSeriesManager(Renderer): """Render a time series chart from CSV or NetCDF inputs. Parameters ---------- title : str, optional Figure title. xlabel, ylabel : str, optional Axis labels. style : str, default="line" One of {"line", "marker", "line_marker"}. """ def __init__( self, *, title: Optional[str] = None, xlabel: Optional[str] = None, ylabel: Optional[str] = None, style: str = "line", ) -> None: self.title = title self.xlabel = xlabel self.ylabel = ylabel self.style = style self._fig = None # Renderer API
[docs] def configure(self, **kwargs: Any) -> None: self.title = kwargs.get("title", self.title) self.xlabel = kwargs.get("xlabel", self.xlabel) self.ylabel = kwargs.get("ylabel", self.ylabel) self.style = kwargs.get("style", self.style)
def _resolve_series( self, *, input_path: str, x: Optional[str] = None, y: Optional[str] = None, var: Optional[str] = None, ): if input_path.lower().endswith(".csv"): import pandas as pd df = pd.read_csv(input_path) if not x or not y: raise ValueError("CSV inputs require --x and --y column names") xs = df[x] ys = df[y] return xs, ys elif input_path.lower().endswith((".nc", ".nc4")): import xarray as xr if not var: raise ValueError("NetCDF inputs require --var for the data variable") ds = xr.open_dataset(input_path) try: da = ds[var] # Use 'time' coord when available, else the first coordinate if "time" in da.coords: xs = da["time"].values else: first_dim = list(da.coords)[0] if da.coords else da.dims[0] xs = ( da[first_dim].values if first_dim in da.coords else range(da.shape[0]) ) ys = da.values finally: ds.close() return xs, ys else: raise ValueError("Unsupported input file; use .csv or .nc for timeseries")
[docs] def render(self, data: Any = None, **kwargs: Any): # data unused for now width = int(kwargs.get("width", 1024)) height = int(kwargs.get("height", 512)) dpi = int(kwargs.get("dpi", FIGURE_DPI)) input_path = kwargs.get("input_path") x = kwargs.get("x") y = kwargs.get("y") var = kwargs.get("var") style = kwargs.get("style", self.style) if not input_path: raise ValueError("input_path is required") xs, ys = self._resolve_series(input_path=input_path, x=x, y=y, var=var) apply_matplotlib_style() import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(width / dpi, height / dpi), dpi=dpi) if style == "marker": ax.plot(xs, ys, linestyle="", marker="o", markersize=3) elif style == "line_marker": ax.plot(xs, ys, linestyle="-", marker="o", markersize=3) else: ax.plot(xs, ys, linestyle="-") if self.title: ax.set_title(self.title) if self.xlabel: ax.set_xlabel(self.xlabel) if self.ylabel: ax.set_ylabel(self.ylabel) ax.grid(True, linestyle=":", alpha=0.5) fig.tight_layout() self._fig = fig return fig
[docs] def save(self, output_path: Optional[str] = None, *, as_buffer: bool = False): if self._fig is None: return None if as_buffer: bio = BytesIO() self._fig.savefig(bio, format="png", bbox_inches="tight", pad_inches=0) bio.seek(0) return bio if output_path is None: output_path = "timeseries.png" self._fig.savefig(output_path, bbox_inches="tight", pad_inches=0) return output_path