Source code for finam_plot.time_series

"""Time series visualization."""
from datetime import datetime, timedelta

import finam as fm
import matplotlib.dates as mdates
import matplotlib.pyplot as plt

from .plot import PlotBase
from .tools import create_figure


[docs] class TimeSeriesPlot(PlotBase): """Line plot for multiple time series, push-based. Expects all inputs to be scalar values. Uses :func:`matplotlib.pyplot.plot`. .. code-block:: text +----------------+ --> [custom] | | --> [custom] | TimeSeriesPlot | --> [......] | | +----------------+ Note: This component is push-based without an internal time step. Examples -------- .. testcode:: constructor import finam_plot as fmp plot = fmp.TimeSeriesPlot( inputs=["Value1", "Value2"], colors=["red", "#ff00ee"], marker="o", lw=2.0, # plot kwargs ) .. testcode:: constructor :hide: plot.initialize() Parameters ---------- inputs : list of str or dict of str, str List of input names (plot series) that will become available for coupling. Can also be a dictionary with units as values. title : str, optional Title for plot and window. colors : list of str, optional List of colors for the inputs. Uses matplotlib default colors by default. pos : tuple(number, number), optional Figure position. ``int`` is interpreted as pixels, ``float`` is interpreted as fraction of screen size. size : tuple(number, number), optional Figure size. ``int`` is interpreted as pixels, ``float`` is interpreted as fraction of screen size. update_interval : int, optional Redraw interval (independent of data retrieval). **plot_kwargs Keyword arguments passed to plot function. See :func:`matplotlib.pyplot.plot`. """ def __init__( self, inputs, title=None, colors=None, pos=None, size=None, update_interval=1, **plot_kwargs, ): super().__init__(title, pos, size, update_interval, **plot_kwargs) self._time = None self._caller = None self._data = [[] for _ in inputs] self._x = [[] for _ in inputs] self._lines = None self._input_units = ( inputs if isinstance(inputs, dict) else {n: None for n in inputs} ) self._colors = colors or [e["color"] for e in plt.rcParams["axes.prop_cycle"]] def _initialize(self): """Initialize the component. After the method call, the component's inputs and outputs must be available, and the component should have status INITIALIZED. """ for inp, units in self._input_units.items(): self.inputs.add( fm.CallbackInput( self._data_changed, name=inp, time=None, grid=fm.NoGrid(), units=units, ) ) self.create_connector(pull_data=self._input_units.keys()) def _connect(self, start_time): """Push initial values to outputs. After the method call, the component should have status CONNECTED. """ if self.figure is None: with plt.style.context("fast"): self.create_figure() date_format = mdates.AutoDateFormatter(self.axes.xaxis) self.axes.xaxis.set_major_formatter(date_format) self.axes.tick_params(axis="x", labelrotation=20) self.figure.tight_layout() self.try_connect(start_time) def _validate(self): """Validate the correctness of the component's settings and coupling. After the method call, the component should have status VALIDATED. """ self._caller = None self._update_plot() self.figure.show() def _data_changed(self, caller, time): """Update for changed data. Parameters ---------- caller Caller. time : datetime simulation time to get the data for. """ if self._time != time: if self.should_repaint(): self.repaint(relim=True) self._caller = caller self._time = time if self.status in (fm.ComponentStatus.UPDATED, fm.ComponentStatus.VALIDATED): self._update_plot() def _update_plot(self): with plt.style.context("fast"): if self._lines is None: self._lines = [] for i, n in enumerate(self._input_units): units = self.inputs[n].info.meta.get("units") units = f" [{units}]" if units else "" self._lines.append( self.axes.plot( [], [], label=n + units, c=self._colors[i % len(self._colors)], **self.plot_kwargs, )[0] ) self.axes.legend(loc=1) for i, inp in enumerate(self._input_units): if self._caller is None or self.inputs[inp] == self._caller: value = fm.data.get_magnitude( self.inputs[inp].pull_data(self._time) ) self._x[i].append(self._time) self._data[i].append(value.item()) self._lines[i].set_xdata(self._x[i]) self._lines[i].set_ydata(self._data[i]) def _finalize(self): """Finalize and clean up the component. After the method call, the component should have status FINALIZED. """ self.repaint(relim=True)
[docs] class StepTimeSeriesPlot(fm.TimeComponent): """Line plot for multiple time series, with internal time step. Expects all inputs to be scalar values. This component has an internal time step. For a push-based line series plot, see :class:`.TimeSeriesPlot`. Uses :func:`matplotlib.pyplot.plot`. .. code-block:: text +----------------+ --> [custom] | | --> [custom] | TimeSeriesPlot | --> [......] | | +----------------+ Examples -------- .. testcode:: constructor import datetime as dt import finam_plot as fmp plot = fmp.StepTimeSeriesPlot( inputs=["Value1", "Value2"], colors=["red", "#ff00ee"], start=dt.datetime(2000, 1, 1), step=dt.timedelta(days=1), marker="o", lw=2.0, # plot kwargs ) .. testcode:: constructor :hide: plot.initialize() Parameters ---------- inputs : list of str or dict of str, str List of input names (plot series) that will become available for coupling. Can also be a dictionary with units as values. start : datetime Starting time. step : timedelta Time step. title : str, optional Title for plot and window. colors : list of str, optional List of colors for the inputs. Uses matplotlib default colors by default. intervals : list of int or None, optional List of interval values to interleave data retrieval of certain inputs. Values are numbers of updates, i.e. whole-numbered factors for ``step``. update_interval : int, optional Redraw interval (independent of data retrieval). pos : tuple(number, number), optional Figure position. ``int`` is interpreted as pixels, ``float`` is interpreted as fraction of screen size. size : tuple(number, number), optional Figure size. ``int`` is interpreted as pixels, ``float`` is interpreted as fraction of screen size. **plot_kwargs Keyword arguments passed to plot function. See :func:`matplotlib.pyplot.plot`. """ def __init__( self, inputs, start, step, title=None, colors=None, intervals=None, update_interval=1, pos=None, size=None, **plot_kwargs, ): super().__init__() with fm.tools.ErrorLogger(self.logger): if not isinstance(start, datetime): raise ValueError("Start must be of type datetime") if not isinstance(step, timedelta): raise ValueError("Step must be of type timedelta") self._step = step self._update_interval = update_interval self._intervals = intervals if intervals else [1 for _ in inputs] self._time = start self._updates = 0 self._figure = None self._axes = None self._data = [[] for _ in inputs] self._x = [[] for _ in inputs] self._lines = None self._input_units = ( inputs if isinstance(inputs, dict) else {n: None for n in inputs} ) self._title = title self._bounds = (pos, size) self._plot_kwargs = plot_kwargs self._colors = colors or [e["color"] for e in plt.rcParams["axes.prop_cycle"]] @property def next_time(self): """The component's predicted simulation time of the next pulls.""" return self.time + self._step def _initialize(self): """Initialize the component. After the method call, the component's inputs and outputs must be available, and the component should have status INITIALIZED. """ for inp, units in self._input_units.items(): self.inputs.add(name=inp, time=self.time, grid=fm.NoGrid(), units=units) self.create_connector() def _connect(self, start_time): """Push initial values to outputs. After the method call, the component should have status CONNECTED. """ self.try_connect(start_time) def _validate(self): """Validate the correctness of the component's settings and coupling. After the method call, the component should have status VALIDATED. """ with plt.style.context("fast"): self._figure, self._axes = create_figure(self._bounds) self._figure.canvas.manager.set_window_title(self._title or "FINAM") self._axes.set_title(self._title) date_format = mdates.AutoDateFormatter(self._axes.xaxis) self._axes.xaxis.set_major_formatter(date_format) self._axes.tick_params(axis="x", labelrotation=20) self._figure.show() def _update(self): """Update the component by one time step. Push new values to outputs. After the method call, the component should have status UPDATED or FINISHED. """ self._time += self._step with plt.style.context("fast"): if self._lines is None: self._lines = [] for i, n in enumerate(self._input_units): units = self.inputs[n].info.meta.get("units") units = f" [{units}]" if units else "" self._lines.append( self._axes.plot( [], [], label=n + units, c=self._colors[i % len(self._colors)], **self._plot_kwargs, )[0] ) self._axes.legend(loc=1) for i, inp in enumerate(self._input_units): if self._updates % self._intervals[i] == 0: value = fm.data.get_magnitude(self.inputs[inp].pull_data(self.time)) self._x[i].append(self.time) self._data[i].append(value.item()) if self._updates % self._update_interval == 0: for i, line in enumerate(self._lines): line.set_xdata(self._x[i]) line.set_ydata(self._data[i]) self._repaint() self._updates += 1 def _repaint(self): self._axes.relim() self._axes.autoscale_view(True, True, True) self._figure.canvas.draw_idle() self._figure.canvas.flush_events() def _finalize(self): """Finalize and clean up the component. After the method call, the component should have status FINALIZED. """ self._repaint()