Skip to content
Snippets Groups Projects
plotting_axes.py 17.6 KiB
Newer Older
from typing import List, Optional, Dict
import numpy as np
from matplotlib.axes import Axes
Lan Dam's avatar
Lan Dam committed
from matplotlib.patches import ConnectionPatch, Rectangle
from matplotlib.ticker import AutoMinorLocator
from matplotlib import pyplot as pl
from matplotlib.backends.backend_qt5agg import (
    FigureCanvasQTAgg as Canvas)

from sohstationviewer.controller.plotting_data import (
    get_time_ticks, get_unit_bitweight, get_disk_size_format)
Lan Dam's avatar
Lan Dam committed

from sohstationviewer.conf import constants
from sohstationviewer.view.util.color import clr
Lan Dam's avatar
Lan Dam committed


class PlottingAxes:
    """
    Class that includes a figure to add axes for plotting and all methods
        related to create axes, ruler, title.
    """
    def __init__(self, parent, main_window):
Lan Dam's avatar
Lan Dam committed
        """
        :param parent: PlottingWidget - widget to plot channels
        :param main_window: QApplication - Main Window to access user's
            setting parameters
Lan Dam's avatar
Lan Dam committed
        """
        self.main_window = main_window
Lan Dam's avatar
Lan Dam committed
        self.parent = parent
        # gaps: list of gaps which is a list of min and max of gaps
        self.gaps: List[List[float]] = []
Lan Dam's avatar
Lan Dam committed
        """
        fig: matplotlib.pyplot.Figure - figure associate with canvas to add
            axes for plotting
        Set fig's size 50in width, 100in height.
        This is the maximum size of plotting container.
        add_axes will draw proportion to this size.
        The actual view based on size of self.main_widget.
        """
        self.fig = pl.Figure(facecolor='white', figsize=(50, 100))
        self.fig.canvas.mpl_connect('button_press_event',
                                    parent.on_button_press_event)
        self.fig.canvas.mpl_connect('pick_event',
                                    parent.on_pick_event)
Lan Dam's avatar
Lan Dam committed

        """
        canvas: matplotlib.backends.backend_qt5agg.FigureCanvasQTAgg - the
            canvas inside main_widget associate with fig
        """
        self.canvas = Canvas(self.fig)

    def add_timestamp_bar(self, height, top=True):
        """
        Set the axes to display timestamp_bar including color, ticks' size,
            label.

        :param height: float - height of timestamp_bar
        :param top: bool - flag indicating this timestamp_bar is located on top
            or bottom to locate tick label.
        """
        self.parent.plotting_bot -= height
        timestamp_bar = self.fig.add_axes(
            [self.parent.plotting_l, self.parent.plotting_bot,
             self.parent.plotting_w, 0.00005],
        )
        # Some plotting widgets like TPS doesn't have timestamp_bar showed
        # but still need it to locate the title of the plot => set it to off

        timestamp_bar.axis('off')
        timestamp_bar.xaxis.set_minor_locator(AutoMinorLocator())
        timestamp_bar.spines['bottom'].set_color(
            self.parent.display_color['basic'])
        timestamp_bar.spines['top'].set_color(
            self.parent.display_color['basic'])

        if top:
            labelbottom = False
        else:
            labelbottom = True
Lan Dam's avatar
Lan Dam committed
            self.parent.plotting_bot -= 0.007       # space for ticks
Lan Dam's avatar
Lan Dam committed
        timestamp_bar.tick_params(which='major', length=7, width=2,
                                  direction='inout',
                                  colors=self.parent.display_color['basic'],
                                  labelbottom=labelbottom,
                                  labeltop=not labelbottom)
        timestamp_bar.tick_params(which='minor', length=4, width=1,
                                  direction='inout',
                                  colors=self.parent.display_color['basic'])
        timestamp_bar.set_ylabel('Hours',
                                 fontweight='bold',
                                 fontsize=self.parent.font_size,
                                 rotation=0,
Lan Dam's avatar
Lan Dam committed
                                 labelpad=constants.HOUR_TO_TMBAR_D *
                                 self.parent.ratio_w,
Lan Dam's avatar
Lan Dam committed
                                 ha='left',
                                 color=self.parent.display_color['basic'])
        # not show any y ticks
        timestamp_bar.set_yticks([])
        return timestamp_bar

    def update_timestamp_bar(self, timestamp_bar):
        """
        Update major, minor x ticks, tick labels on timestamp_bar based on
        curr_min_x, curr_max_x, date_mode, time_ticks_total, font_size.

        :param timestamp_bar: matplotlib.axes.Axes - axes for timestamp_bar
        """
        times, major_times, major_time_labels = get_time_ticks(
Lan Dam's avatar
Lan Dam committed
            self.parent.min_x, self.parent.max_x, self.parent.date_mode,
            self.parent.time_ticks_total
        )
        timestamp_bar.axis('on')
        timestamp_bar.set_xticks(times, minor=True)
        timestamp_bar.set_xticks(major_times)
        timestamp_bar.set_xticklabels(major_time_labels,
Lan Dam's avatar
Lan Dam committed
                                      fontsize=self.parent.font_size +
                                      2 * self.parent.ratio_w)
Lan Dam's avatar
Lan Dam committed
        timestamp_bar.set_xlim(self.parent.min_x, self.parent.max_x)

    def create_axes(self, plot_b, plot_h, has_min_max_lines=True):
        """
        Create axes to plot a channel.

        :param plot_b: float - bottom of the plot
        :param plot_h: float - height of the plot
        :param has_min_max_lines: bool - flag showing if the plot need min/max
            lines
        :return ax: matplotlib.axes.Axes - axes of a channel
        """
        ax = self.fig.add_axes(
            [self.parent.plotting_l, plot_b, self.parent.plotting_w, plot_h],
            picker=True
        )

        ax.spines['right'].set_visible(False)
        ax.spines['left'].set_visible(False)
        if has_min_max_lines:
            ax.spines['top'].set_zorder(constants.Z_ORDER['AXIS_SPINES'])
            ax.spines['bottom'].set_zorder(constants.Z_ORDER['AXIS_SPINES'])
            ax.spines['top'].set_color(self.parent.display_color['sub_basic'])
            ax.spines['bottom'].set_color(
                self.parent.display_color['sub_basic'])

        # no stick (use timestamp bar as stick)
        ax.set_yticks([])
        ax.set_xticks([])
        # set tick_params for left ticklabel
        ax.tick_params(colors=self.parent.display_color['basic'],
                       width=0,
                       pad=-2,
                       labelsize=self.parent.font_size)
        # transparent background => self.fig will take care of background
        ax.patch.set_alpha(0)
        return ax

    def set_axes_info(self, ax: Axes,
                      sample_no_list: List[int],
                      sample_no_colors: List[str] = [clr['W'], clr['W']],
                      sample_no_pos: List[float] = [0.05, 0.95],
                      label: Optional[str] = None,
                      info: str = '',
                      y_list: Optional[np.ndarray] = None,
                      chan_db_info: Optional[Dict] = None,
                      linked_ax: Optional[Axes] = None):
Lan Dam's avatar
Lan Dam committed
        """
        Draw plot's title, sub title, sample total label, center line, y labels
        for a channel.

        :param ax:  axes of a channel
        :param sample_no_list: list of totals of different sample groups
        :param sample_no_colors: list of color to display sample numbers
        :param sample_no_pos: list of position to display sample numbers
            top/bottom
        :param label: title of the plot. If None, show chan_db_info['label']
        :param info: additional info to show in sub title which is
Lan Dam's avatar
Lan Dam committed
            smaller and under title on the left side
        :param y: y values of the channel for min/max labels, min/max lines
        :param chan_db_info: info of channel from database
        :param linked_ax:
Lan Dam's avatar
Lan Dam committed
            if linked_ax is None, this is a main channel, label of channel will
                be displayed with title's format, on top right of plot.
            if linked_ax is not None, this is a channel using main channel's
                axes, label of channel will be displayed with sub title's
                format - under main title.
        """
        if label is None:
            label = chan_db_info['label']
Lan Dam's avatar
Lan Dam committed
        title_ver_alignment = 'center'
        # set info in subtitle under title
        if linked_ax is not None:
            info = label
        if info != '':
            ax.text(
                -0.15, 0.2,
                info,
                horizontalalignment='left',
                verticalalignment='top',
                rotation='horizontal',
                transform=ax.transAxes,
                color=self.parent.display_color['sub_basic'],
                size=self.parent.font_size
            )
            title_ver_alignment = 'top'

        if linked_ax is None:
            # set title on left side
            color = self.parent.display_color['plot_label']
            if label.startswith("DEFAULT"):
                color = self.parent.display_color["warning"]
            ax.text(
                -0.15, 0.6,
                label,
                horizontalalignment='left',
                verticalalignment=title_ver_alignment,
                rotation='horizontal',
                transform=ax.transAxes,
                color=color,
Lan Dam's avatar
Lan Dam committed
                size=self.parent.font_size + 2 * self.parent.ratio_w
Lan Dam's avatar
Lan Dam committed
            )

        # set samples' total on right side
        if len(sample_no_list) == 1:
            # center_total_point_lbl: The label to display total number of data
            # points for plots whose ax has attribute x_list.
            # The plotTypes that use this label are linesDot, linesSRate,
            # linesMassPos, dotForTime, multiColorDot
            ax.center_total_point_lbl = ax.text(
Lan Dam's avatar
Lan Dam committed
                1.005, 0.5,
                sample_no_list[0],
                horizontalalignment='left',
                verticalalignment='center',
                rotation='horizontal',
                transform=ax.transAxes,
                color=sample_no_colors[0],
Lan Dam's avatar
Lan Dam committed
                size=self.parent.font_size
            )
        else:
            # bottom_total_point_lbl, top_total_point_lbl are label to diplay
            # total number of data points which are splitted into top
            # and bottom. The ax needs to include attributes x_bottom and x_top
            # The plotTypes that use these labels are upDownDots and linesDot
            # with channel='GPS Lk/Unlk'
            ax.bottom_total_point_lbl = ax.text(
                1.005, sample_no_pos[0],
Lan Dam's avatar
Lan Dam committed
                sample_no_list[0],
                horizontalalignment='left',
                verticalalignment='center',
                rotation='horizontal',
                transform=ax.transAxes,
                color=sample_no_colors[0],
Lan Dam's avatar
Lan Dam committed
                size=self.parent.font_size
            )
            # top
            ax.top_total_point_lbl = ax.text(
                1.005, sample_no_pos[1],
Lan Dam's avatar
Lan Dam committed
                sample_no_list[1],
                horizontalalignment='left',
                verticalalignment='center',
                rotation='horizontal',
                transform=ax.transAxes,
                color=sample_no_colors[1],
Lan Dam's avatar
Lan Dam committed
                size=self.parent.font_size
            )
        if linked_ax is not None:
            ax.set_yticks([])
            return
Lan Dam's avatar
Lan Dam committed
            # draw center line
            ax.plot([self.parent.min_x, self.parent.max_x],
                    [0, 0],
                    color=self.parent.display_color['sub_basic'],
                    linewidth=0.5,
                    zorder=constants.Z_ORDER['CENTER_LINE']
                    )
            ax.spines['top'].set_visible(False)
            ax.spines['bottom'].set_visible(False)
        else:
            if sample_no_list[0] == 0:
                return
            min_y = min([min(y) for y in y_list])
            max_y = max([max(y) for y in y_list])

Lan Dam's avatar
Lan Dam committed
            ax.spines['top'].set_visible(True)
            ax.spines['bottom'].set_visible(True)
            ax.unit_bw = get_unit_bitweight(
                chan_db_info, self.main_window.bit_weight_opt
Lan Dam's avatar
Lan Dam committed
            )
            self.set_axes_ylim(ax, min_y, max_y, chan_db_info)
Lan Dam's avatar
Lan Dam committed

    def set_axes_ylim(self, ax: Axes, org_min_y: float, org_max_y: float,
                      chan_db_info: dict):
Lan Dam's avatar
Lan Dam committed
        """
        Limit y range in min_y, max_y.
        Set y tick labels at min_y, max_y
        :param ax: axes of a channel
        :param min_y: minimum of y values
        :param max_y: maximum of y values
        :param chan_db_info: info of channel from database
Lan Dam's avatar
Lan Dam committed
        """
        if chan_db_info['channel'].startswith('Disk Usage'):
            ax.set_yticks([org_min_y, org_max_y])
            min_y_label = get_disk_size_format(org_min_y)
            max_y_label = get_disk_size_format(org_max_y)
            ax.set_yticklabels([min_y_label, max_y_label])
            return

        if chan_db_info['channel'] == 'GPS Lk/Unlk':
            # to avoid case that the channel doesn't have Lk or Unlk
            # preset min, max value so that GPS Clock Power is always in the
            # middle
            org_min_y = -1
            org_max_y = 1
        min_y = round(org_min_y, 7)
        max_y = round(org_max_y, 7)
        if chan_db_info['fixPoint'] == 0 and org_max_y > org_min_y:
            # if fixPoint=0, the format uses the save value created
            # => try to round to to the point that user can see the differences
            for dec in range(2, 8, 1):
                min_y = round(org_min_y, dec)
                max_y = round(org_max_y, dec)
                if max_y > min_y:
                    break
Lan Dam's avatar
Lan Dam committed
        if max_y > min_y:
            # There are different values for y => show yticks for min, max
            # separately
            ax.set_yticks([min_y, max_y])
            ax.set_yticklabels(
                [ax.unit_bw.format(min_y), ax.unit_bw.format(max_y)])
        if min_y == max_y:
            # All values for y are the same => show only one yticks
            max_y += 1
            ax.set_yticks([min_y])
            ax.set_yticklabels([ax.unit_bw.format(min_y)])
        ax.set_ylim(min_y, max_y)

    def add_gap_bar(self, gaps):
        """
        Draw the axes and labels to display gap_bar right under top timestamp
        bar.
        Update plotting_bot to draw next plot.

        :param gaps: [[float, float], ] - list of [min, max] of gaps
        """
Lan Dam's avatar
Lan Dam committed
        if self.main_window.gap_minimum is None:
Lan Dam's avatar
Lan Dam committed
            return
Lan Dam's avatar
Lan Dam committed
        self.gaps = gaps
Lan Dam's avatar
Lan Dam committed
        self.parent.plotting_bot -= 0.003
        self.parent.gap_bar = self.create_axes(self.parent.plotting_bot,
                                               0.001,
                                               has_min_max_lines=False)

Lan Dam's avatar
Lan Dam committed
        gap_label = f"GAP({self.main_window.gap_minimum}sec)"
Lan Dam's avatar
Lan Dam committed
        h = 0.001  # height of rectangle represent gap
        self.set_axes_info(self.parent.gap_bar, [len(gaps)],
                           label=gap_label)
        # draw gaps
        for i in range(len(gaps)):
            x = gaps[i][0]
            # width of rectangle represent gap
            w = gaps[i][1] - gaps[i][0]
Lan Dam's avatar
Lan Dam committed
            self.parent.gap_bar.add_patch(
                Rectangle(
                    (x, - h / 2), w, h, color=c, picker=True, lw=0.,
Lan Dam's avatar
Lan Dam committed
                    zorder=constants.Z_ORDER['GAP']
                )
            )

    def get_height(self, ratio: float, bw_plots_distance: float = 0.0015,
                   pixel_height: float = 19) -> float:
Lan Dam's avatar
Lan Dam committed
        """
        Calculate new plot's bottom position and return plot's height.

        :param ratio: ratio of the plot height on the BASIC_HEIGHT
        :param bw_plots_distance: distance between plots
        :param pixel_height: height of plot in pixel (
            for TPS/TPS legend, height of each day row)

        :return plot_h: height of the plot
Lan Dam's avatar
Lan Dam committed
        """
        plot_h = constants.BASIC_HEIGHT * ratio  # ratio with figure height
        self.parent.plotting_bot -= plot_h + bw_plots_distance
        bw_plots_distance_pixel = 3000 * bw_plots_distance
        self.parent.plotting_bot_pixel += (pixel_height * ratio +
                                           bw_plots_distance_pixel)
Lan Dam's avatar
Lan Dam committed
        return plot_h

    def add_ruler(self, color):
        """
        Create a line across all channels' plots from top timestamp
            bar to bottom timestamp bar. There are 3 different rulers defined
            in __init__(): self.ruler, self.zoom_marker1, self.zoom_marker2.

        :param color: color of the ruler
        """
        ruler = ConnectionPatch(
            xyA=(0, 0),
            xyB=(0, self.parent.bottom),
            coordsA="data",
            coordsB="data",
            axesA=self.parent.timestamp_bar_top,
            axesB=self.parent.timestamp_bar_bottom,
            color=color,
        )
        ruler.set_visible(False)
        self.parent.timestamp_bar_bottom.add_artist(ruler)
        return ruler

    def set_title(self, title, x=-0.15, y=105, v_align='top'):
Lan Dam's avatar
Lan Dam committed
        """
        Display title of the data set's plotting based on

        :param title: str - title of the data set's plotting
        :param x: float - horizontal position to the right from the left edge
            of the self.parent.timestamp_bar_top
        :param y: float - vertical position upward from the top edge of
            the self.parent.timestamp_bar_top
        :param v_align: str - vertical alignment of title to the
            self.parent.timestamp_bar_top
        """
        self.fig.text(x, y, title,
                      verticalalignment=v_align,
                      horizontalalignment='left',
                      transform=self.parent.timestamp_bar_top.transAxes,
                      color=self.parent.display_color['basic'],
Lan Dam's avatar
Lan Dam committed
                      size=self.parent.font_size + 2 * self.parent.ratio_w)