Newer
Older
from typing import List, Optional, Dict

Lan Dam
committed
import numpy as np
from matplotlib.axes import Axes
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)
from sohstationviewer.view.util.color import clr
class PlottingAxes:
"""
Class that includes a figure to add axes for plotting and all methods
related to create axes, ruler, title.
"""
:param main_window: QApplication - Main Window to access user's
setting parameters

Lan Dam
committed
# gaps: list of gaps which is a list of min and max of gaps
self.gaps: List[List[float]] = []
"""
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)
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
"""
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
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,
labelpad=constants.HOUR_TO_TMBAR_D *
self.parent.ratio_w,
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(
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,
fontsize=self.parent.font_size +
2 * self.parent.ratio_w)
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
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):
"""
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
: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:
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']
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
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,
)
# 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(
1.005, 0.5,
sample_no_list[0],
horizontalalignment='left',
verticalalignment='center',
rotation='horizontal',
transform=ax.transAxes,
color=sample_no_colors[0],
# 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

Lan Dam
committed
# 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],
sample_no_list[0],
horizontalalignment='left',
verticalalignment='center',
rotation='horizontal',
transform=ax.transAxes,
color=sample_no_colors[0],
1.005, sample_no_pos[1],
sample_no_list[1],
horizontalalignment='left',
verticalalignment='center',
rotation='horizontal',
transform=ax.transAxes,
color=sample_no_colors[1],
if linked_ax is not None:
ax.set_yticks([])
return

Lan Dam
committed
if y_list is None:
# 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:

Lan Dam
committed
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])
ax.spines['top'].set_visible(True)
ax.spines['bottom'].set_visible(True)
chan_db_info, self.main_window.bit_weight_opt
self.set_axes_ylim(ax, min_y, max_y, chan_db_info)

Lan Dam
committed
def set_axes_ylim(self, ax: Axes, org_min_y: float, org_max_y: float,
chan_db_info: dict):
"""
Limit y range in min_y, max_y.
Set y tick labels at min_y, max_y

Lan Dam
committed
: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
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

Lan Dam
committed
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
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
"""
self.parent.plotting_bot -= 0.003
self.parent.gap_bar = self.create_axes(self.parent.plotting_bot,
0.001,
has_min_max_lines=False)
gap_label = f"GAP({self.main_window.gap_minimum}sec)"
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
committed
if w > 0:
# gap
c = 'red'
else:
# overlap
c = 'orange'

Lan Dam
committed
(x, - h / 2), w, h, color=c, picker=True, lw=0.,
def get_height(self, ratio: float, bw_plots_distance: float = 0.0015,
pixel_height: float = 19) -> float:
"""
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
"""
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)
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'):
"""
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'],