Module antimait.plotting

Plotting utilities for incoming data based on matplotlib.pyplot.

Expand source code
"""
Plotting utilities for incoming data based on matplotlib.pyplot.
"""

import matplotlib.pyplot as plt  # type: ignore
from threading import Lock
from typing import List, Dict, Union
from pathlib import Path
import datetime
import logging
import re

from . import DataReceiver, Comm

__all__ = ["format_filename", "Plotter"]


def format_filename(strin: str) -> str:
    """
    Args:
        strin: a string that has to be changed in a valid filename, generally a comport name

    Returns:
        str, a valid string for a filename
    """
    normalized = re.sub(r"[^\w]", "_", strin)
    if normalized.startswith("_"):
        normalized = normalized[1:]
    if normalized.endswith("_"):
        normalized = normalized[:-1]
    return normalized


class Plotter(DataReceiver):
    """
    Plotter class, this is a wrapper for matplotlib.pyplot and for a structure containing the
    sensor data.
    """
    _LOCK = Lock()
    _PLOTTING_PERIOD = 10  # every 10 new elements
    _MAX_CAPACITY = 100  # default capacity

    def __init__(self, session_name: str, frequency_mode: bool = None,
                 data: Union[List[float], Dict[str, int]] = None,
                 capacity: int = None, refresh_rate: int = None,
                 overwrite: bool = None, img_dir: str = None):
        """
        Args:
            session_name: the name of the session that will be used as the plot file name
            frequency_mode: if set to true, data is collected as a dict where data[elem] => frequency_elem
            data: data already available, used to initialize the Plotter obj instead of an empty collection
            capacity: max capacity of the underlying data container, this ensures that the plotted data does not explode
            refresh_rate: when a number of data is received equal to this parameter, a new plot is generated
            overwrite: if True the plot image will be just one and will always be owerwritten
            img_dir: if specified, images will be saved in this dir instead of Path.cwd()
        """

        self._session_name = session_name
        self._frequency_mode = frequency_mode or False
        self._session_data = data or [] if not self._frequency_mode else {}
        self._overwrite = True if overwrite is None else overwrite
        self._elem_counter: int = 0

        if capacity is not None and capacity <= 0:
            raise ValueError("The plotter capacity must be positive")
        self._capacity = capacity or Plotter._MAX_CAPACITY

        if refresh_rate is not None and refresh_rate <= 0:
            raise ValueError("The plotter refresh rate must be positive")
        self._refresh_rate = refresh_rate or Plotter._PLOTTING_PERIOD

        if img_dir:
            img_path = Path(img_dir)
            if not img_path.exists():
                raise ValueError("{} is not a correct path".format(img_dir))
            self._img_dir = img_path.absolute()
        else:
            self._img_dir = Path.cwd().absolute()

    def update(self, action: Comm, **update: str) -> None:
        if action == Comm.DATA:
            if "data" in update:
                data: Union[str, float] = update["data"]
                if not self._frequency_mode:
                    try:
                        data = float(data)
                    except ValueError:
                        logging.warning("Parsing error form float to str")
                        return
                self.add(data)
            else:
                logging.error("data keyword not passed!")
        elif action == Comm.CLOSING:
            self.plot()
            logging.info("Plotter {} closing!".format(self._session_name))

    def add(self, elem: Union[float, str]) -> None:
        """
        Adds an element or a list of elements to the internal data that is being plotted.
        Args:
            elem: the element to add, either a float or a list of floats

        Returns:
            None
        """

        if self._frequency_mode:
            if isinstance(self._session_data, dict) and isinstance(elem, str):
                if elem not in self._session_data:
                    self._session_data[elem] = 1
                else:
                    self._session_data[elem] += 1
        else:
            if isinstance(self._session_data, list) and isinstance(elem, float):
                if len(self._session_data) == self._capacity:
                    self._session_data.pop(0)
                self._session_data.append(elem)

        self._elem_counter += 1

        if self._elem_counter == self._refresh_rate:
            self.plot()
            self._elem_counter = 0

    def clear(self):
        """
        Just a wrapper around list.clear()

        Returns:
            None
        """

        self._session_data.clear()

    def plot(self) -> Path:
        """
        Plots the contents of session_data using matplotlib.pyplot.
        This method generates an image for the plotted data in the directory specified in __init__.
        Returns:
            pathlib.Path, the absolute Path of the generated plot
        """
        file_name: str
        with Plotter._LOCK:
            plt.title(self._session_name)
            if self._frequency_mode:
                if isinstance(self._session_data, dict):
                    plt.bar(self._session_data.keys(), self._session_data.values())
            else:
                plt.plot(self._session_data)

            if not self._overwrite:
                date_fmt = datetime.datetime.now().strftime("%d-%m-%y_%I-%M-%S")
                file_name = "{}_{}".format(format_filename(self._session_name), date_fmt)
            else:
                file_name = format_filename(self._session_name)

            plt.savefig(Path(self._img_dir, file_name))
            plt.clf()
        return Path(self._img_dir, file_name, ".png")

Functions

def format_filename(strin)

Args

strin
a string that has to be changed in a valid filename, generally a comport name

Returns

str, a valid string for a filename
 
Expand source code
def format_filename(strin: str) -> str:
    """
    Args:
        strin: a string that has to be changed in a valid filename, generally a comport name

    Returns:
        str, a valid string for a filename
    """
    normalized = re.sub(r"[^\w]", "_", strin)
    if normalized.startswith("_"):
        normalized = normalized[1:]
    if normalized.endswith("_"):
        normalized = normalized[:-1]
    return normalized

Classes

class Plotter (session_name, frequency_mode=None, data=None, capacity=None, refresh_rate=None, overwrite=None, img_dir=None)

Plotter class, this is a wrapper for matplotlib.pyplot and for a structure containing the sensor data.

Args

session_name
the name of the session that will be used as the plot file name
frequency_mode
if set to true, data is collected as a dict where data[elem] => frequency_elem
data
data already available, used to initialize the Plotter obj instead of an empty collection
capacity
max capacity of the underlying data container, this ensures that the plotted data does not explode
refresh_rate
when a number of data is received equal to this parameter, a new plot is generated
overwrite
if True the plot image will be just one and will always be owerwritten
img_dir
if specified, images will be saved in this dir instead of Path.cwd()
Expand source code
class Plotter(DataReceiver):
    """
    Plotter class, this is a wrapper for matplotlib.pyplot and for a structure containing the
    sensor data.
    """
    _LOCK = Lock()
    _PLOTTING_PERIOD = 10  # every 10 new elements
    _MAX_CAPACITY = 100  # default capacity

    def __init__(self, session_name: str, frequency_mode: bool = None,
                 data: Union[List[float], Dict[str, int]] = None,
                 capacity: int = None, refresh_rate: int = None,
                 overwrite: bool = None, img_dir: str = None):
        """
        Args:
            session_name: the name of the session that will be used as the plot file name
            frequency_mode: if set to true, data is collected as a dict where data[elem] => frequency_elem
            data: data already available, used to initialize the Plotter obj instead of an empty collection
            capacity: max capacity of the underlying data container, this ensures that the plotted data does not explode
            refresh_rate: when a number of data is received equal to this parameter, a new plot is generated
            overwrite: if True the plot image will be just one and will always be owerwritten
            img_dir: if specified, images will be saved in this dir instead of Path.cwd()
        """

        self._session_name = session_name
        self._frequency_mode = frequency_mode or False
        self._session_data = data or [] if not self._frequency_mode else {}
        self._overwrite = True if overwrite is None else overwrite
        self._elem_counter: int = 0

        if capacity is not None and capacity <= 0:
            raise ValueError("The plotter capacity must be positive")
        self._capacity = capacity or Plotter._MAX_CAPACITY

        if refresh_rate is not None and refresh_rate <= 0:
            raise ValueError("The plotter refresh rate must be positive")
        self._refresh_rate = refresh_rate or Plotter._PLOTTING_PERIOD

        if img_dir:
            img_path = Path(img_dir)
            if not img_path.exists():
                raise ValueError("{} is not a correct path".format(img_dir))
            self._img_dir = img_path.absolute()
        else:
            self._img_dir = Path.cwd().absolute()

    def update(self, action: Comm, **update: str) -> None:
        if action == Comm.DATA:
            if "data" in update:
                data: Union[str, float] = update["data"]
                if not self._frequency_mode:
                    try:
                        data = float(data)
                    except ValueError:
                        logging.warning("Parsing error form float to str")
                        return
                self.add(data)
            else:
                logging.error("data keyword not passed!")
        elif action == Comm.CLOSING:
            self.plot()
            logging.info("Plotter {} closing!".format(self._session_name))

    def add(self, elem: Union[float, str]) -> None:
        """
        Adds an element or a list of elements to the internal data that is being plotted.
        Args:
            elem: the element to add, either a float or a list of floats

        Returns:
            None
        """

        if self._frequency_mode:
            if isinstance(self._session_data, dict) and isinstance(elem, str):
                if elem not in self._session_data:
                    self._session_data[elem] = 1
                else:
                    self._session_data[elem] += 1
        else:
            if isinstance(self._session_data, list) and isinstance(elem, float):
                if len(self._session_data) == self._capacity:
                    self._session_data.pop(0)
                self._session_data.append(elem)

        self._elem_counter += 1

        if self._elem_counter == self._refresh_rate:
            self.plot()
            self._elem_counter = 0

    def clear(self):
        """
        Just a wrapper around list.clear()

        Returns:
            None
        """

        self._session_data.clear()

    def plot(self) -> Path:
        """
        Plots the contents of session_data using matplotlib.pyplot.
        This method generates an image for the plotted data in the directory specified in __init__.
        Returns:
            pathlib.Path, the absolute Path of the generated plot
        """
        file_name: str
        with Plotter._LOCK:
            plt.title(self._session_name)
            if self._frequency_mode:
                if isinstance(self._session_data, dict):
                    plt.bar(self._session_data.keys(), self._session_data.values())
            else:
                plt.plot(self._session_data)

            if not self._overwrite:
                date_fmt = datetime.datetime.now().strftime("%d-%m-%y_%I-%M-%S")
                file_name = "{}_{}".format(format_filename(self._session_name), date_fmt)
            else:
                file_name = format_filename(self._session_name)

            plt.savefig(Path(self._img_dir, file_name))
            plt.clf()
        return Path(self._img_dir, file_name, ".png")

Ancestors

Methods

def add(self, elem)

Adds an element or a list of elements to the internal data that is being plotted.

Args

elem
the element to add, either a float or a list of floats

Returns

None
 
Expand source code
def add(self, elem: Union[float, str]) -> None:
    """
    Adds an element or a list of elements to the internal data that is being plotted.
    Args:
        elem: the element to add, either a float or a list of floats

    Returns:
        None
    """

    if self._frequency_mode:
        if isinstance(self._session_data, dict) and isinstance(elem, str):
            if elem not in self._session_data:
                self._session_data[elem] = 1
            else:
                self._session_data[elem] += 1
    else:
        if isinstance(self._session_data, list) and isinstance(elem, float):
            if len(self._session_data) == self._capacity:
                self._session_data.pop(0)
            self._session_data.append(elem)

    self._elem_counter += 1

    if self._elem_counter == self._refresh_rate:
        self.plot()
        self._elem_counter = 0
def clear(self)

Just a wrapper around list.clear()

Returns

None
 
Expand source code
def clear(self):
    """
    Just a wrapper around list.clear()

    Returns:
        None
    """

    self._session_data.clear()
def plot(self)

Plots the contents of session_data using matplotlib.pyplot. This method generates an image for the plotted data in the directory specified in init.

Returns

pathlib.Path, the absolute Path of the generated plot
 
Expand source code
def plot(self) -> Path:
    """
    Plots the contents of session_data using matplotlib.pyplot.
    This method generates an image for the plotted data in the directory specified in __init__.
    Returns:
        pathlib.Path, the absolute Path of the generated plot
    """
    file_name: str
    with Plotter._LOCK:
        plt.title(self._session_name)
        if self._frequency_mode:
            if isinstance(self._session_data, dict):
                plt.bar(self._session_data.keys(), self._session_data.values())
        else:
            plt.plot(self._session_data)

        if not self._overwrite:
            date_fmt = datetime.datetime.now().strftime("%d-%m-%y_%I-%M-%S")
            file_name = "{}_{}".format(format_filename(self._session_name), date_fmt)
        else:
            file_name = format_filename(self._session_name)

        plt.savefig(Path(self._img_dir, file_name))
        plt.clf()
    return Path(self._img_dir, file_name, ".png")

Inherited members