diff --git a/sohstationviewer/conf/constants.py b/sohstationviewer/conf/constants.py index b1976032c537400fd04377c0dbac48f6395569e4..3833814b09f3762b894ed5bd86a8de24ca880a6a 100644 --- a/sohstationviewer/conf/constants.py +++ b/sohstationviewer/conf/constants.py @@ -23,9 +23,6 @@ RECAL_SIZE_LIMIT = 10**9 # rate of values to be cut of from means CUT_FROM_MEAN_FACTOR = 0.1 -# to split to time range (not use for now, but implemented in mseed) -FILE_PER_CHAN_LIMIT = 20 - # default start time DEFAULT_START_TIME = "1970-01-01" @@ -58,24 +55,70 @@ CONFIG_PATH = 'conf/read_settings.ini' # List of image formats. Have to put PNG at the beginning to go with # dpi in dialog + IMG_FORMAT = ['PNG', 'PDF', 'EPS', 'SVG'] # ================================================================= # # PLOTTING CONSTANT # ================================================================= # +# Maximum height of figure to assure that all channel data are plotted and all +# the height of plotting_widget is covered => Use the number that is the +# possible maximum height of screen +MAX_HEIGHT_IN = 25. + # Where the plotting start (0-1) -BOTTOM = 0.996 -# where the plotting start in pixel -BOTTOM_PX = 200 -# BASIC_HEIGHT of a plot (0-1) -BASIC_HEIGHT = 0.0012 +BOTTOM = 1 + +# ========= A BASIC_HEIGHT_IN IS THE HEIGHT OF ONE UNIT OF SIZE_FACTOR ====== +# Size in inch of 1 unit of SIZE_FACTOR +BASIC_HEIGHT_IN = 0.15 + +# DEFAULT_SIZE_FACTOR_TO_NORMALIZE is the default height_normalizing_factor +# which is the normalize factor corresponding to SIZE_FACTOR = 1 +# Basically this factor is calculated based on the total height of all plots. +# However, when there's no channel height_normalizing_factor can't be +# calculated, so DEFAULT_SIZE_FACTOR_TO_NORMALIZE will be used to plot +# timestamp. +DEFAULT_SIZE_FACTOR_TO_NORMALIZE = 0.02 +# vertical margin +VSPACE_SIZE_FACTOR = 3 +# space from previous bottom to time bar bottom +TIME_BAR_SIZE_FACTOR = 2 +# space from time bar bottom to the bottom of gap bar +GAP_SIZE_FACTOR = 2 +# size of empty plot bar at the end +PLOT_NONE_SIZE_FACTOR = 0.01 +# distance between plot +PLOT_SEPARATOR_SIZE_FACTOR = 1 +TPS_SEPARATOR_SIZE_FACTOR = 2 +# Size factor of TPS legend height +TPS_LEGEND_SIZE_FACTOR = 21 +# ============================================================================= +# ================================= NORMALIZE ================================= +# Normalized distance from left edge to the start of channel plots +PLOT_LEFT_NORMALIZE = 0.1 +TPS_LEFT_NORMALIZE = 0.15 + +# Normalized distance from right edge to the end of channel plots +PLOT_RIGHT_NORMALIZE = 0.05 +TPS_RIGHT_NORMALIZE = 0.05 + +# Normalized width of a plot +PLOT_WIDTH_NORMALIZE = 1 - (PLOT_LEFT_NORMALIZE + PLOT_RIGHT_NORMALIZE) +TPS_WIDTH_NORMALIZE = 1 - (TPS_LEFT_NORMALIZE + TPS_RIGHT_NORMALIZE) + +# Basic width in pixels to calculate ratio_w for items that use pixel unit +WIDTH_BASE_PX = 1546. + # Order to show plotting items on top of each other. The higher value, the # the higher priority Z_ORDER = {'AXIS_SPINES': 0, 'CENTER_LINE': 1, 'LINE': 2, 'GAP': 3, 'DOT': 3} # Distance from 'Hour' label to timestamp bar -HOUR_TO_TMBAR_D = 100 +HOUR_TO_TMBAR_D = 50 +# DEFAULT FONT SIZE +FONTSIZE = 6 # day total limit for all tps channels to stay in one tab DAY_LIMIT_FOR_TPS_IN_ONE_TAB = 180 # about half of a year # ================================================================= # diff --git a/sohstationviewer/view/main_window.py b/sohstationviewer/view/main_window.py index 6a4de80696f266603ca21a672184a394bb479064..971838508c337a2913852090c9747c74971db27e 100755 --- a/sohstationviewer/view/main_window.py +++ b/sohstationviewer/view/main_window.py @@ -62,6 +62,16 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): super().__init__(parent) self.setup_ui(self) """ + SCREEN INFO + actual_dpi: actual dpi of the screen where main_window is located. + actual_dpi = physical_dpi * device_pixel_ratio + dpi_y: vertical dpi of the screen + dpi_x: horizontal dpi of the screen + """ + self.actual_dpi: float = 100. + self.dpi_x: float = 25. + self.dpi_y: float = 30. + """ dir_names: list of absolute paths of data sets """ self.list_of_dir: List[Path] = [] @@ -1092,16 +1102,6 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): if t.strip() != ''] self.pref_soh_list_data_type = rows[0]['dataType'] - def resizeEvent(self, event): - """ - OVERRIDE Qt method. - When main_window is resized, its plotting_widget need to initialize - its size to fit the viewport. - - :param event: QResizeEvent - resize event - """ - self.plotting_widget.init_size() - def pull_current_directory_from_db(self): """ Set current directory with info saved in DB @@ -1112,40 +1112,6 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): if len(rows) > 0 and rows[0]['FieldValue']: self.set_current_directory(rows[0]['FieldValue']) - def closeEvent(self, event: QtGui.QCloseEvent) -> None: - """ - Cleans up when the user exits the program. - - :param event: parameter of method being overridden - """ - display_tracking_info(self.tracking_info_text_browser, - 'Cleaning up...', - LogType.INFO) - if self.data_loader.running: - self.data_loader.thread.requestInterruption() - self.data_loader.thread.quit() - self.data_loader.thread.wait() - - # If we don't explicitly clean up the running processing threads, - # there is a high chance that they will attempt to access temporary - # files that have already been cleaned up. While this should not be a - # problem, it is still a bad idea to touch the file system when you are - # not supposed to. - if self.is_plotting_waveform: - self.waveform_dlg.plotting_widget.request_stop() - self.waveform_dlg.plotting_widget.thread_pool.waitForDone() - - if self.is_plotting_tps: - for tps_widget in self.tps_dlg.tps_widget_dict.values(): - tps_widget.request_stop() - tps_widget.thread_pool.waitForDone() - - # close all remaining windows - for window in QtWidgets.QApplication.topLevelWidgets(): - window.close() - - self.write_config() - def write_config(self): """ Write the current state of the program to the config file. @@ -1346,3 +1312,63 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): found_files_list_item.setFlags(QtCore.Qt.ItemFlag.NoItemFlags) found_files_list_item.setForeground(QtCore.Qt.GlobalColor.black) self.open_files_list.insertItem(0, found_files_list_item) + + # ======================== EVENTS ========================== + def moveEvent(self, event): + """ + Get dpi, dpi_x, dpi_y whenever main window is moved + """ + screen = QtWidgets.QApplication.screenAt(event.pos()) + if screen is not None: + curr_actual_dpi = ( + screen.physicalDotsPerInch() * screen.devicePixelRatio()) + self.dpi_x = screen.physicalDotsPerInchX() + self.dpi_y = screen.physicalDotsPerInchY() + self.actual_dpi = curr_actual_dpi + + return super().moveEvent(event) + + def resizeEvent(self, event): + """ + OVERRIDE Qt method. + When main_window is resized, its plotting_widget need to initialize + its size to fit the viewport. + + :param event: QResizeEvent - resize event + """ + self.plotting_widget.init_size() + return super().resizeEvent(event) + + def closeEvent(self, event: QtGui.QCloseEvent) -> None: + """ + Cleans up when the user exits the program. + + :param event: parameter of method being overridden + """ + display_tracking_info(self.tracking_info_text_browser, + 'Cleaning up...', + LogType.INFO) + if self.data_loader.running: + self.data_loader.thread.requestInterruption() + self.data_loader.thread.quit() + self.data_loader.thread.wait() + + # If we don't explicitly clean up the running processing threads, + # there is a high chance that they will attempt to access temporary + # files that have already been cleaned up. While this should not be a + # problem, it is still a bad idea to touch the file system when you are + # not supposed to. + if self.is_plotting_waveform: + self.waveform_dlg.plotting_widget.request_stop() + self.waveform_dlg.plotting_widget.thread_pool.waitForDone() + + if self.is_plotting_tps: + for tps_widget in self.tps_dlg.tps_widget_dict.values(): + tps_widget.request_stop() + tps_widget.thread_pool.waitForDone() + + # close all remaining windows + for window in QtWidgets.QApplication.topLevelWidgets(): + window.close() + + self.write_config() diff --git a/sohstationviewer/view/plotting/plotting_widget/multi_threaded_plotting_widget.py b/sohstationviewer/view/plotting/plotting_widget/multi_threaded_plotting_widget.py index 63dad07cfa8a1fdd376e46eb822068308d6b49d8..486ae6f4337072dd85b56adeea2315f33db3043f 100644 --- a/sohstationviewer/view/plotting/plotting_widget/multi_threaded_plotting_widget.py +++ b/sohstationviewer/view/plotting/plotting_widget/multi_threaded_plotting_widget.py @@ -72,51 +72,52 @@ class MultiThreadedPlottingWidget(PlottingWidget): self.zoom_marker1_shown = False self.data_set_id = data_set_id self.processing_log = [] # [(message, type)] + self.gaps = d_obj.gaps[data_set_id] self.gap_bar = None self.date_mode = self.main_window.date_format.upper() + self.fig_height_in = self.init_fig_height_in() self.time_ticks_total = time_ticks_total self.min_x = max(data_time[0], start_tm) self.max_x = min(data_time[1], end_tm) self.plot_total = len(self.plotting_data1) + len(self.plotting_data2) - cond_total = len(self.plotting_data1) + number_channel_found = len(self.plotting_data1) name = self.name if not is_waveform: - cond_total += len(self.plotting_data2) + number_channel_found += len(self.plotting_data2) name += " DATA OR MASS POSITION" - if cond_total == 0: - title = f"NO {name} DATA TO DISPLAY." + if number_channel_found == 0: + self.title = f"NO {name} DATA TO DISPLAY." self.processing_log.append( (f"No {name} data to display.", LogType.INFO)) else: - title = get_title( + self.title = get_title( data_set_id, self.min_x, self.max_x, self.date_mode) + self.plotting_axes.height_normalizing_factor = \ + const.DEFAULT_SIZE_FACTOR_TO_NORMALIZE self.plotting_bot = const.BOTTOM - self.plotting_bot_pixel = const.BOTTOM_PX self.axes = [] - self.timestamp_bar_top = self.plotting_axes.add_timestamp_bar(0.003) - self.plotting_axes.set_title(title) - if cond_total == 0: + if number_channel_found == 0: + # set_size and plot timestamp_bar for the title's position + self.set_size() + self.timestamp_bar_top = self.plotting_axes.add_timestamp_bar( + False) + self.plotting_axes.set_title(self.title, y=5000) self.draw() return False else: - self.plotting_axes.add_gap_bar(d_obj.gaps[data_set_id]) return True - def create_plotting_channel_processors( + def get_plotting_info( self, plotting_data: Dict, - need_db_info: bool = False, is_plotting_data1: bool = False - ) -> None: + ) -> List[str]: """ - Create a data processor for each channel data in the order of - pref_order. If pref_order isn't given, process in order of - plotting_data. - + Get chan_db_info and channel order for plotting. :param plotting_data: dict of data by chan_id - :param need_db_info: flag to get db info :param is_plotting_data1: flag to tell if the plotting_data sent is plotting_data1 + :return chan_order: order to plot channels in plotting_data """ chan_order = self.pref_order if is_plotting_data1 and self.pref_order \ else sorted(list(plotting_data.keys())) @@ -127,30 +128,53 @@ class MultiThreadedPlottingWidget(PlottingWidget): not_plot_chans = [] for chan_id in chan_order: - if need_db_info: - chan_db_info = get_chan_plot_info(chan_id, - self.parent.data_type, - self.c_mode) - if (chan_db_info['height'] == 0 or - chan_db_info['plotType'] == ''): - # not draw - not_plot_chans.append(chan_id) - continue - if 'DEFAULT' in chan_db_info['channel']: - msg = (f"Channel {chan_id}'s " - f"definition can't be found in database. It will" - f"be displayed in DEFAULT style.") - # TODO: give user a way to add it to DB and leave - # instruction here - self.processing_log.append((msg, LogType.WARNING)) - - plotting_data[chan_id]['chan_db_info'] = chan_db_info + chan_db_info = get_chan_plot_info(chan_id, + self.parent.data_type, + self.c_mode) + if (chan_db_info['height'] == 0 or + chan_db_info['plotType'] == ''): + # not draw + not_plot_chans.append(chan_id) + continue + if 'DEFAULT' in chan_db_info['channel']: + msg = (f"Channel {chan_id}'s " + f"definition can't be found in database. It will" + f"be displayed in DEFAULT style.") + # TODO: give user a way to add it to DB and leave + # instruction here + self.processing_log.append((msg, LogType.WARNING)) + + plotting_data[chan_id]['chan_db_info'] = chan_db_info + chan_height_ratio = \ + chan_db_info['height'] + const.PLOT_SEPARATOR_SIZE_FACTOR + self.fig_height_in += chan_height_ratio * const.BASIC_HEIGHT_IN if not_plot_chans != []: msg = (f"The database settings 'plotType' or 'height' show not to " f"be plotted for the following channels: " - f"{', '.join( not_plot_chans)}") + f"{', '.join(not_plot_chans)}") self.processing_log.append((msg, LogType.WARNING)) + return chan_order + + def plotting_preset(self): + """ + Calling super's plotting_preset() will preset the width and height of + figure and main_widget. + Plot top timestamp bar, gap bar and set title. + """ + super().plotting_preset() + self.timestamp_bar_top = self.plotting_axes.add_timestamp_bar(top=True) + self.plotting_axes.set_title(self.title) + self.plotting_axes.add_gap_bar(self.gaps) + def create_plotting_channel_processors( + self, plotting_data: Dict, chan_order) -> None: + """ + Create a data processor for each channel data in the order of + pref_order. If pref_order isn't given, process in order of + plotting_data. + :param plotting_data: dict of data by chan_id + :param chan_order: order to plot channels + """ for chan_id in chan_order: if 'chan_db_info' not in plotting_data[chan_id]: continue @@ -167,8 +191,12 @@ class MultiThreadedPlottingWidget(PlottingWidget): self, d_obj, data_set_id, start_tm, end_tm, time_ticks_total, pref_order=[]): """ - Prepare to plot waveform/SOH/mass-position data by creating a data - processor for each channel, then, run the processors. + Prepare to plot waveform/SOH/mass-position data: + + get_plotting_info: get sizing info + + plotting_preset: preset figure, widget size according to sizing + info, plot top time bar, gaps bar + + creating a data processor for each channel, then, + run the processors. :param d_obj: object of data :param data_set_id: data set's id @@ -187,14 +215,18 @@ class MultiThreadedPlottingWidget(PlottingWidget): time_ticks_total) if not ret: self.draw() - self.clean_up() + self.clean_up(has_data=False) self.finished.emit() return + chan_order1 = self.get_plotting_info(self.plotting_data1, True) + chan_order2 = self.get_plotting_info(self.plotting_data2) + + self.plotting_preset() self.create_plotting_channel_processors( - self.plotting_data1, True, True) + self.plotting_data1, chan_order1) self.create_plotting_channel_processors( - self.plotting_data2, True) + self.plotting_data2, chan_order2) self.process_channel() @@ -250,15 +282,17 @@ class MultiThreadedPlottingWidget(PlottingWidget): """ pass - def clean_up(self): + def clean_up(self, has_data: bool = True): """ Clean up after all available channels have been plotted. The cleanup steps are as follows. Display a finish message Reset all internal flags + :param has_data: flag that shows if there is data or not """ - self.done() - finished_msg = f'{self.name} plot finished' + if has_data: + self.done() + finished_msg = f'{self.name} plot finished.' display_tracking_info(self.tracking_box, finished_msg, LogType.INFO) @@ -272,7 +306,7 @@ class MultiThreadedPlottingWidget(PlottingWidget): """ self.axes.append(self.plotting.plot_none()) self.timestamp_bar_bottom = self.plotting_axes.add_timestamp_bar( - 0.003, top=False) + top=False) super().set_lim(first_time=True) self.bottom = self.axes[-1].get_ybound()[0] self.ruler = self.plotting_axes.add_ruler( @@ -281,9 +315,6 @@ class MultiThreadedPlottingWidget(PlottingWidget): self.display_color['zoom_marker']) self.zoom_marker2 = self.plotting_axes.add_ruler( self.display_color['zoom_marker']) - # Set view size fit with the given data - if self.main_widget.geometry().height() < self.plotting_bot_pixel: - self.main_widget.setFixedHeight(self.plotting_bot_pixel) self.draw() self.finished.emit() diff --git a/sohstationviewer/view/plotting/plotting_widget/plotting.py b/sohstationviewer/view/plotting/plotting_widget/plotting.py index 0fd62fb28485794931ee7f6326144a98e0ae0da3..59d67efd6f31312375a2570ecf3715b00ef80bf0 100644 --- a/sohstationviewer/view/plotting/plotting_widget/plotting.py +++ b/sohstationviewer/view/plotting/plotting_widget/plotting.py @@ -39,11 +39,15 @@ class Plotting: Plot with nothing needed to show rulers. :return ax: matplotlib.axes.Axes - axes of the empty plot """ - plot_h = 0.00001 - bw_plots_distance = 0.0001 - self.parent.plotting_bot -= plot_h + bw_plots_distance + plot_h = (constants.PLOT_NONE_SIZE_FACTOR * + self.parent.height_normalizing_factor) + self.parent.plotting_bot -= self.parent.height_normalizing_factor * ( + constants.PLOT_NONE_SIZE_FACTOR + + constants.PLOT_SEPARATOR_SIZE_FACTOR) + ax = self.plotting_axes.create_axes( self.parent.plotting_bot, plot_h, has_min_max_lines=False) + ax.x = None ax.plot([0], [0], linestyle="") ax.chan_db_info = None diff --git a/sohstationviewer/view/plotting/plotting_widget/plotting_axes.py b/sohstationviewer/view/plotting/plotting_widget/plotting_axes.py index 4d6c35afb437043c9881a4a31bc3152d2b47f131..6b671ecf38c16a0cc06549cef6edc1bf5fe6143c 100644 --- a/sohstationviewer/view/plotting/plotting_widget/plotting_axes.py +++ b/sohstationviewer/view/plotting/plotting_widget/plotting_axes.py @@ -1,6 +1,7 @@ from typing import List, Optional, Dict import numpy as np + from matplotlib.axes import Axes from matplotlib.text import Text from matplotlib.patches import ConnectionPatch, Rectangle @@ -12,7 +13,7 @@ from matplotlib.backends.backend_qt5agg import ( from sohstationviewer.controller.plotting_data import ( get_time_ticks, get_unit_bitweight, get_disk_size_format) from sohstationviewer.view.util.color import clr -from sohstationviewer.conf import constants +from sohstationviewer.conf import constants as const class PlottingAxes: @@ -28,46 +29,69 @@ class PlottingAxes: """ self.main_window = main_window self.parent = parent - # gaps: list of gaps which is a list of min and max of gaps + + """ + height_normalizing_factor: Normalize factor corresponding to + size_factor=1 + Basically this factor is calculated based on the total height of all + plots. However, when there's no channel, height_normalizing_factor + can't be calculated so DEFAULT_SIZE_FACTOR_TO_NORMALIZE will be used + to plot timestamp. + """ + self.height_normalizing_factor: float = 0. + """ + 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. + fig: figure associate with canvas to add axes for plotting """ - self.fig = pl.Figure(facecolor='white', figsize=(50, 100)) + # falcecolor=none to make figure transparent, so the background + # color depends on canvas' + self.fig = pl.Figure(facecolor='none', figsize=(10, 10)) self.fig.canvas.mpl_connect('button_press_event', - parent.on_button_press_event) + self.parent.on_button_press_event) self.fig.canvas.mpl_connect('pick_event', - parent.on_pick_event) + self.parent.on_pick_event) """ - canvas: matplotlib.backends.backend_qt5agg.FigureCanvasQTAgg - the - canvas inside main_widget associate with fig + canvas: the canvas inside main_widget associates with fig """ self.canvas = Canvas(self.fig) - def add_timestamp_bar(self, height, top=True): + self.canvas.setParent(self.parent.main_widget) + # canvas background is transparent + self.canvas.setStyleSheet("background:transparent;") + + def add_timestamp_bar(self, show_bar: bool = True, top: bool = True): """ - Set the axes to display timestamp_bar including color, ticks' size, - label. + Set the axes to display timestamp_bar including color, tick size, label + + In case of no data or TPS, timestamp bar won't be displayed + but still be needed to display title - :param height: float - height of timestamp_bar - :param top: bool - flag indicating this timestamp_bar is located on top + :param show_bar: flag for the timestamp_bar to be displayed or not + :param top: flag indicating this timestamp_bar is located on top or bottom to locate tick label. """ - self.parent.plotting_bot -= height + # For bottom timestamp bar: space from the last plot to the bar + self.parent.plotting_bot -= \ + const.TIME_BAR_SIZE_FACTOR * self.height_normalizing_factor + if top: + # top timestamp bar: the total of this and the above value is the + # space from edge to the bar which includes space for title. + self.parent.plotting_bot -= \ + const.VSPACE_SIZE_FACTOR * self.height_normalizing_factor + height = 0.0005 * self.height_normalizing_factor + timestamp_bar = self.fig.add_axes( - [self.parent.plotting_l, self.parent.plotting_bot, - self.parent.plotting_w, 0.00005], + [const.PLOT_LEFT_NORMALIZE, self.parent.plotting_bot, + const.PLOT_WIDTH_NORMALIZE, height] ) - # 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') + if not show_bar: + timestamp_bar.set_axis_off() + return timestamp_bar timestamp_bar.xaxis.set_minor_locator(AutoMinorLocator()) timestamp_bar.spines['bottom'].set_color( self.parent.display_color['basic']) @@ -78,7 +102,10 @@ class PlottingAxes: labelbottom = False else: labelbottom = True - self.parent.plotting_bot -= 0.007 # space for ticks + # bottom timestamp bar: space from edge to the bar + self.parent.plotting_bot -= \ + const.VSPACE_SIZE_FACTOR * self.height_normalizing_factor + timestamp_bar.tick_params(which='major', length=7, width=2, direction='inout', colors=self.parent.display_color['basic'], @@ -87,12 +114,11 @@ class PlottingAxes: timestamp_bar.tick_params(which='minor', length=4, width=1, direction='inout', colors=self.parent.display_color['basic']) - timestamp_bar.set_ylabel('Hours', + timestamp_bar.set_ylabel('HOURS', fontweight='bold', - fontsize=self.parent.font_size, + fontsize=const.FONTSIZE, rotation=0, - labelpad=constants.HOUR_TO_TMBAR_D * - self.parent.ratio_w, + labelpad=const.HOUR_TO_TMBAR_D, ha='left', color=self.parent.display_color['basic']) # not show any y ticks @@ -114,8 +140,7 @@ class PlottingAxes: 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) + fontsize=const.FONTSIZE + 1) 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): @@ -129,15 +154,16 @@ class PlottingAxes: :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], + [const.PLOT_LEFT_NORMALIZE, plot_b, + const.PLOT_WIDTH_NORMALIZE, 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_zorder(const.Z_ORDER['AXIS_SPINES']) + ax.spines['bottom'].set_zorder(const.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']) @@ -149,7 +175,7 @@ class PlottingAxes: ax.tick_params(colors=self.parent.display_color['basic'], width=0, pad=-2, - labelsize=self.parent.font_size) + labelsize=const.FONTSIZE) # transparent background => self.fig will take care of background ax.patch.set_alpha(0) return ax @@ -173,7 +199,7 @@ class PlottingAxes: rotation='horizontal', transform=ax.transAxes, color=color, - size=self.parent.font_size + size=const.FONTSIZE ) def set_axes_info(self, ax: Axes, @@ -206,14 +232,14 @@ class PlottingAxes: pos_y = 0.4 if info != '': ax.text( - -0.15, 0.4, + -0.11, 0.4, info, horizontalalignment='left', verticalalignment='top', rotation='horizontal', transform=ax.transAxes, color=self.parent.display_color['sub_basic'], - size=self.parent.font_size + size=const.FONTSIZE ) pos_y = 0.6 # set title on left side @@ -221,13 +247,13 @@ class PlottingAxes: if label.startswith("DEFAULT"): color = self.parent.display_color["warning"] ax.text( - -0.15, pos_y, + -0.11, pos_y, label, horizontalalignment='left', rotation='horizontal', transform=ax.transAxes, color=color, - size=self.parent.font_size + 2 * self.parent.ratio_w + size=const.FONTSIZE + 1 ) # set samples' total on right side @@ -247,13 +273,14 @@ class PlottingAxes: [0, 0], color=self.parent.display_color['sub_basic'], linewidth=0.5, - zorder=constants.Z_ORDER['CENTER_LINE'] + zorder=const.Z_ORDER['CENTER_LINE'] ) ax.spines['top'].set_visible(False) ax.spines['bottom'].set_visible(False) else: if sample_no_list[0] == 0: return + if len(y_list[0]) == 0: min_y = 0 max_y = 0 @@ -325,15 +352,18 @@ class PlottingAxes: if self.main_window.gap_minimum is None: return self.gaps = gaps - self.parent.plotting_bot -= 0.003 + + gap_bar_height = self.height_normalizing_factor * const.GAP_SIZE_FACTOR + self.parent.plotting_bot -= gap_bar_height self.parent.gap_bar = self.create_axes(self.parent.plotting_bot, - 0.001, + gap_bar_height * 0.2, has_min_max_lines=False) gap_label = f"GAP({self.main_window.gap_minimum}sec)" h = 0.001 # height of rectangle represent gap gap_color = clr['W'] if self.main_window.color_mode == 'B'\ else clr['B'] + self.set_axes_info(self.parent.gap_bar, sample_no_list=[None, len(gaps), None], sample_no_colors=[None, gap_color, None], @@ -353,28 +383,24 @@ class PlottingAxes: self.parent.gap_bar.add_patch( Rectangle( (x, - h / 2), w, h, color=c, picker=True, lw=0., - zorder=constants.Z_ORDER['GAP'] + zorder=const.Z_ORDER['GAP'] ) ) - def get_height(self, ratio: float, bw_plots_distance: float = 0.0015, - pixel_height: float = 19) -> float: + def get_height(self, plot_height_ratio: float, + plot_separator_size_factor: float = + const.PLOT_SEPARATOR_SIZE_FACTOR) -> 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 + :param plot_height_ratio: ratio of the plot height + :param plot_separator_size_factor: ratio of distance between plots + :return normalize 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 + plot_h_and_space_ratio = plot_height_ratio + plot_separator_size_factor + self.parent.plotting_bot -= \ + plot_h_and_space_ratio * self.height_normalizing_factor + return plot_height_ratio * self.height_normalizing_factor def add_ruler(self, color): """ @@ -397,7 +423,7 @@ class PlottingAxes: self.parent.timestamp_bar_bottom.add_artist(ruler) return ruler - def set_title(self, title, x=-0.15, y=105, v_align='top'): + def set_title(self, title, x=-0.1, y=6000, v_align='bottom'): """ Display title of the data set's plotting based on @@ -414,4 +440,4 @@ class PlottingAxes: horizontalalignment='left', transform=self.parent.timestamp_bar_top.transAxes, color=self.parent.display_color['basic'], - size=self.parent.font_size + 2 * self.parent.ratio_w) + size=const.FONTSIZE + 2) diff --git a/sohstationviewer/view/plotting/plotting_widget/plotting_widget.py b/sohstationviewer/view/plotting/plotting_widget/plotting_widget.py old mode 100755 new mode 100644 index 58d61ad691b57bd01469196c3ed718d593e02f91..6412a1bc655f01d635e27d425c6c323a13713956 --- a/sohstationviewer/view/plotting/plotting_widget/plotting_widget.py +++ b/sohstationviewer/view/plotting/plotting_widget/plotting_widget.py @@ -2,16 +2,17 @@ Class of which object is used to plot data """ from typing import List, Optional, Union +import math import numpy as np import matplotlib.text from matplotlib import pyplot as pl from matplotlib.transforms import Bbox -from PySide6.QtCore import QTimer, QSize -from PySide6.QtGui import QResizeEvent + +from PySide6.QtCore import QTimer, Qt from PySide6 import QtCore, QtWidgets from PySide6.QtWidgets import QWidget, QApplication, QTextBrowser -from sohstationviewer.conf import constants +from sohstationviewer.conf import constants as const from sohstationviewer.view.util.color import set_color_mode from sohstationviewer.view.plotting.plotting_widget.plotting_widget_helper \ @@ -56,19 +57,6 @@ class PlottingWidget(QtWidgets.QScrollArea): """ self.processing_log = [] """ - width_base_px: float - width base in pixel which fit the plotting area - """ - self.width_base_px = 1546. - """ - width_base: float - basic width of plotting which can be enlarged to - the maximum of 1 which is 4x of width_base - """ - self.width_base = 0.25 - """ - plotting_l_base: float - The basic left of plotting area - """ - self.plotting_l_base = 0.04 - """ time_ticks_total: int - maximum of total major ticks label displayed """ self.time_ticks_total = 5 @@ -85,24 +73,10 @@ class PlottingWidget(QtWidgets.QScrollArea): """ self.ratio_w = 1. """ - plotting_w: float - plotting area's width which is set with ratio_w of - width_base - """ - self.plotting_w = self.width_base * self.ratio_w - """ - plotting_l: float - plotting area's left which is set with ratio_w of - plotting_l_base. - TODO: check to see if should keep this left - unchanged or changed with width of the scrolling area - """ - self.plotting_l = self.plotting_l_base * self.ratio_w - """ plotting_bot: float - bottom of a current plot, decrease by plot_h return from self.get_height() whenever a new plot is added - plotting_bot_pixel: float - bottom of current plot in pixel """ - self.plotting_bot = constants.BOTTOM - self.plotting_bot_pixel = constants.BOTTOM_PX + self.plotting_bot = const.BOTTOM """ display_color: dict - that defined colors to be used. """ @@ -112,11 +86,14 @@ class PlottingWidget(QtWidgets.QScrollArea): """ self.c_mode = 'B' """ - font_size: float - font size on plot. With some require bigger font, - +2 to the font_size + fig_height_in: height of figure in inch """ - self.base_font_size = 7 - self.font_size = 7 + self.fig_height_in = 0 + """ + height_normalizing_factor: factor to convert height size factor to + normalize in which total of all height ratios is 1 + """ + self.height_normalizing_factor = 0 """ bottom: float - y position of the bottom edge of all plots in self.axes This is to identify the bottom of rulers @@ -178,30 +155,28 @@ class PlottingWidget(QtWidgets.QScrollArea): """ self.zoom_marker1_shown = False """ - follower: int - connection id to help keeping zoom_marker2 following - mouse move - """ - self.follower = None - """ plotting_data1: dict that includes 'times', 'data', 'ax'- first set of data for plotting. It can be either - DataTypeModel.__init__.soh_data[data_set_id] or - DataTypeModel.__init__.waveform_data[data_set_id]['read_data'] + soh_data[data_set_id] or waveform_data[data_set_id] """ self.plotting_data1 = {} """ plotting_data2: dict that includes 'times' and 'data'- - second set of data for plotting. It is - DataTypeModel.__init__.mass_pos_data[data_set_id] + second set of data for plotting. It is mass_pos_data[data_set_id] """ self.plotting_data2 = {} - + """ + title: title of the plotting including data set index and time range + """ + self.title: str = '' # List of SOH message lines in RT130 to display in info box when # there're more than 2 lines for one data point clicked self.rt130_log_data: Optional[List[str]] = None # ---------------------------------------------------------------- QtWidgets.QScrollArea.__init__(self) + + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) """ main_widget: QWidget - widget in the scroll area to keep the drawing canvas @@ -211,7 +186,6 @@ class PlottingWidget(QtWidgets.QScrollArea): plotting_axes: object that helps creating axes for plotting """ self.plotting_axes = PlottingAxes(self, main_window) - self.plotting_axes.canvas.setParent(self.main_widget) self.setWidget(self.main_widget) @@ -238,39 +212,92 @@ class PlottingWidget(QtWidgets.QScrollArea): self.set_colors('B') - # ======================================================================= # - # EVENT - # ======================================================================= # - def resizeEvent(self, event: QResizeEvent): + def init_fig_height_in(self): """ - OVERRIDE Qt method. - When plottingWidget's viewport is resized along with parent window - (event opening MainWindow triggers resizeEvent too), call - set_size() to fit all components of the channel inside the width - of the new viewport. - - :param event: resize event + Start figure height in with the size of margin, time_bar, gap_bar + which aren't included in get_plotting_info() """ - self.set_size(self.maximumViewportSize()) - return super(PlottingWidget, self).resizeEvent(event) + vspace_in = const.BASIC_HEIGHT_IN * const.VSPACE_SIZE_FACTOR + time_bar_height_in = const.BASIC_HEIGHT_IN * const.TIME_BAR_SIZE_FACTOR + plot_none_in = const.BASIC_HEIGHT_IN * const.PLOT_NONE_SIZE_FACTOR + gap_bar_in_val = const.BASIC_HEIGHT_IN * const.GAP_SIZE_FACTOR + gap_bar_in = (0 if self.main_window.gap_minimum is None + else gap_bar_in_val) + return (gap_bar_in + plot_none_in + + 2 * vspace_in + + 2 * time_bar_height_in) - def set_size(self, view_port_size: QSize) -> None: + def set_size(self, view_port_size: Optional[float] = None) -> None: """ - Set the widget's width fit the width of geo so user don't have to - scroll the horizontal bar to view the channels in side the widget. - Recalculate ratio_w, plotting_w, self.plotting_l to plot channels and - their labels fit inside the widget width. - When there is no channels, height will be set to the height of - the viewport to cover the whole viewport. + Set figure's width and main widget's width to fit the width of the + view port of the scroll area. + + Recalculate ratio_w to adjust size of object that use pixel for sizing. + + When there is no channels, height will be set to the height of the + viewport to cover the whole viewport. + :param view_port_size: size of viewport """ + if view_port_size is None: + view_port_size = self.maximumViewportSize() + view_port_width = view_port_size.width() + self.plotting_axes.canvas.setFixedWidth(view_port_width) + fig_width_in = view_port_width/self.parent.dpi_x + + self.plotting_axes.fig.set_dpi(self.parent.actual_dpi) + self.plotting_axes.fig.set_figwidth(fig_width_in) + # set view size fit with the scroll's viewport size self.main_widget.setFixedWidth(view_port_size.width()) - self.ratio_w = view_port_size.width() / self.width_base_px - self.plotting_w = self.ratio_w * self.width_base - self.plotting_l = self.ratio_w * self.plotting_l_base + self.ratio_w = view_port_size.width() / const.WIDTH_BASE_PX if self.plot_total == 0: - self.main_widget.setFixedHeight(view_port_size.height()) + view_port_height = view_port_size.height() + self.main_widget.setFixedHeight(view_port_height) + self.plotting_axes.canvas.setFixedHeight(view_port_height) + fig_height_in = view_port_height/self.parent.dpi_y + self.plotting_axes.fig.set_figheight(fig_height_in) + + def plotting_preset(self): + """ + Call set size to apply current view port size to width of figure and + main widget. + + Set height of figure and main widget according to total of plotting + channels' heights. + + Calculate height_normalizing_factor. + """ + self.set_size() + fig_height = math.ceil(self.fig_height_in * self.parent.dpi_y) + + # adjusting main_widget to fit view port + max_height = max(self.maximumViewportSize().height(), fig_height) + self.main_widget.setFixedHeight(max_height) + self.plotting_axes.canvas.setFixedHeight(max_height) + # calculate height_normalizing_factor which is the ratio to multiply + # with height ratio of an item to fit in figure height start from + # 1 to 0. + normalize_total = math.ceil(self.fig_height_in / const.BASIC_HEIGHT_IN) + self.height_normalizing_factor = 1. / normalize_total + + if fig_height < max_height: + # to avoid artifact in main widget's section that isn't covered by + # figure: + # + Figure size need to be spread out to the whole view port by + # adjusting fig_height_in. + # + Plotting items need to be rescaled through + # height_normalizing_factor. + ratio = fig_height / max_height + self.height_normalizing_factor *= ratio + self.fig_height_in = math.ceil(self.fig_height_in / ratio) + + self.plotting_axes.fig.set_figheight(self.fig_height_in) + self.plotting_axes.height_normalizing_factor = \ + self.height_normalizing_factor + # ======================================================================= # + # EVENT + # ======================================================================= # def get_timestamp(self, event): """ @@ -465,7 +492,7 @@ class PlottingWidget(QtWidgets.QScrollArea): ruler_text_content = format_time(xdata, self.parent.date_format, 'HH:MM:SS') self.ruler_text = self.plotting_axes.fig.text( - xdata, 75, ruler_text_content, + xdata, 5000, ruler_text_content, verticalalignment='top', horizontalalignment='center', transform=self.timestamp_bar_top.transData, @@ -669,8 +696,8 @@ class PlottingWidget(QtWidgets.QScrollArea): """ self.c_mode = mode self.display_color = set_color_mode(mode) - self.plotting_axes.fig.patch.set_facecolor( - self.display_color['background']) + self.main_widget.setStyleSheet( + f"background-color:{self.display_color['background']}") def set_peer_plotting_widgets(self, widgets): """ @@ -682,8 +709,8 @@ class PlottingWidget(QtWidgets.QScrollArea): def save_plot(self, default_name='plot'): if self.c_mode != self.main_window.color_mode: - main_color = constants.ALL_COLOR_MODES[self.main_window.color_mode] - curr_color = constants.ALL_COLOR_MODES[self.c_mode] + main_color = const.ALL_COLOR_MODES[self.main_window.color_mode] + curr_color = const.ALL_COLOR_MODES[self.c_mode] msg = (f"Main window's color mode is {main_color}" f" but the mode haven't been applied to plotting.\n\n" f"Do you want to cancel to apply {main_color} mode " @@ -734,8 +761,14 @@ class PlottingWidget(QtWidgets.QScrollArea): display_tracking_info(self.tracking_box, msg) def clear(self): - self.plotting_axes.fig.clear() + try: + self.plotting_axes.fig.clear() + except AttributeError: + pass self.axes = [] self.plot_total = 0 self.init_size() - self.draw() + try: + self.draw() + except AttributeError: + pass diff --git a/sohstationviewer/view/plotting/time_power_square/time_power_squared_dialog.py b/sohstationviewer/view/plotting/time_power_square/time_power_squared_dialog.py index 116983fbad995762c770a29be369a1e472ba2247..fc23dc2853e079d8c36657778fcb15ccb4420b09 100755 --- a/sohstationviewer/view/plotting/time_power_square/time_power_squared_dialog.py +++ b/sohstationviewer/view/plotting/time_power_square/time_power_squared_dialog.py @@ -2,7 +2,7 @@ from typing import Union, Tuple, Dict, List from PySide6 import QtWidgets, QtCore -from PySide6.QtCore import QEventLoop, Qt +from PySide6.QtCore import QEventLoop, Qt, QSize from PySide6.QtGui import QCursor from PySide6.QtWidgets import QApplication, QTabWidget @@ -32,6 +32,18 @@ class TimePowerSquaredDialog(QtWidgets.QWidget): super().__init__() self.main_window = parent """ + SCREEN INFO + screen_size: size of screen where tps dialog is located + actual_dpi: actual dpi of the screen where tps dialog is located. + actual_dpi = physical_dpi * device_pixel_ratio + dpi_y: vertical dpi of the screen + dpi_x: horizontal dpi of the screen + """ + self.screen_size: QSize = None + self.actual_dpi: float = 100. + self.dpi_x: float = 25. + self.dpi_y: float = 30. + """ data_type: str - type of data being plotted """ self.data_type = None @@ -56,10 +68,15 @@ class TimePowerSquaredDialog(QtWidgets.QWidget): """ self.info_text_browser = QtWidgets.QTextBrowser(self) """ - plotting_widget: tab that contains widgets to draw time-power-square + plotting_tab: tab that contains widgets to draw time-power-square for each 5-minute of data """ self.plotting_tab = QTabWidget(self) + """ + processing_log_msg: processing message for different TPS channels that + separated by new line. + """ + self.processing_log_msg: str = '' main_layout.addWidget(self.plotting_tab, 2) bottom_layout = QtWidgets.QHBoxLayout() @@ -170,6 +187,20 @@ class TimePowerSquaredDialog(QtWidgets.QWidget): pass return super(TimePowerSquaredDialog, self).resizeEvent(event) + def moveEvent(self, event): + """ + Get actual dpi, dpi_x, dpi_y whenever tps dialog is moved + """ + screen = QtWidgets.QApplication.screenAt(event.pos()) + if screen is not None: + curr_actual_dpi = ( + screen.physicalDotsPerInch() * screen.devicePixelRatio()) + self.dpi_x = screen.physicalDotsPerInchX() + self.dpi_y = screen.physicalDotsPerInchY() + self.actual_dpi = curr_actual_dpi + + return super().moveEvent(event) + def connect_signals(self): """ Connect functions to widgets @@ -262,6 +293,7 @@ class TimePowerSquaredDialog(QtWidgets.QWidget): :param start_tm: requested start time to read :param end_tm: requested end time to read """ + self.processing_log_msg = "" min_x = max(d_obj.data_time[data_set_id][0], start_tm) max_x = min(d_obj.data_time[data_set_id][1], end_tm) self.start_5mins_of_diff_days = get_start_5mins_of_diff_days( @@ -275,17 +307,16 @@ class TimePowerSquaredDialog(QtWidgets.QWidget): if len(self.start_5mins_of_diff_days) <= DAY_LIMIT_FOR_TPS_IN_ONE_TAB: self.main_window.tps_tab_total = 1 self.create_tps_widget( - 0, data_set_id, 'TPS', d_obj.waveform_data[data_set_id]) + data_set_id, 'TPS', d_obj.waveform_data[data_set_id]) else: self.main_window.tps_tab_total = len( d_obj.waveform_data[data_set_id]) - for tab_idx, chan_id in enumerate( - d_obj.waveform_data[data_set_id]): + for chan_id in d_obj.waveform_data[data_set_id]: self.create_tps_widget( - tab_idx, data_set_id, chan_id, + data_set_id, chan_id, {chan_id: d_obj.waveform_data[data_set_id][chan_id]}) - def create_tps_widget(self, tab_idx, data_set_id, tab_name, data_dict): + def create_tps_widget(self, data_set_id, tab_name, data_dict): """ Create a tps widget and add to plotting_tab, then call plot Channels to plot all channels in data_dict. @@ -306,7 +337,5 @@ class TimePowerSquaredDialog(QtWidgets.QWidget): tps_widget.set_colors(self.main_window.color_mode) self.plotting_tab.addTab(tps_widget, tab_name) self.tps_widget_dict[tab_name] = tps_widget - if tab_idx > 0: - tps_widget.set_size(self.view_port_size) tps_widget.plot_channels( data_dict, data_set_id, self.start_5mins_of_diff_days) diff --git a/sohstationviewer/view/plotting/time_power_square/time_power_squared_processor.py b/sohstationviewer/view/plotting/time_power_square/time_power_squared_processor.py index 74cdb80de3ff3c1774263a9390a8fdcfe27817b6..5705d9ecc90458c18cd8e8e1e42f8c6452df8d98 100644 --- a/sohstationviewer/view/plotting/time_power_square/time_power_squared_processor.py +++ b/sohstationviewer/view/plotting/time_power_square/time_power_squared_processor.py @@ -96,3 +96,4 @@ class TimePowerSquaredProcessor(QtCore.QRunnable): self.stop_lock.lock() self.stop = True self.stop_lock.unlock() + self.signals.finished.emit('') diff --git a/sohstationviewer/view/plotting/time_power_square/time_power_squared_widget.py b/sohstationviewer/view/plotting/time_power_square/time_power_squared_widget.py index f26974b83915291acfe83082ff46f70788bc65fc..0ce2b9d4ed3e0f2e5e7f725f685ef7cb56175c21 100644 --- a/sohstationviewer/view/plotting/time_power_square/time_power_squared_widget.py +++ b/sohstationviewer/view/plotting/time_power_square/time_power_squared_widget.py @@ -1,5 +1,5 @@ from math import sqrt -from typing import List, Tuple, Union, Dict +from typing import List, Tuple, Union, Dict, Optional import numpy as np from PySide6 import QtCore @@ -32,6 +32,11 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget): Widget to display time power square data for waveform channels """ def __init__(self, data_set_id, tab_name, *args, **kwarg): + """ + :param data_set_id: the id of the data set + :param tab_name: name to show to identify the tab that content this + widget + """ self.data_set_id = data_set_id self.tab_name = tab_name """ @@ -51,7 +56,11 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget): mark the five-minute corresponding to the end of the zoom area """ self.zoom_marker2s: List[Line2D] = [] - + """ + start_5mins_of_diff_days: the list of starts of all five minutes + of days in which each day has 288 of 5 minutes. + """ + self.start_5mins_of_diff_days = [] """ tps_t: float - prompt's time on tps's chart to help rulers on other plotting widgets to identify their location @@ -85,6 +94,12 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget): return (self.tab_name if self.tab_name == 'TPS' else self.tab_name + " TPS") + def init_fig_height_in(self): + return const.BASIC_HEIGHT_IN * ( + 2 * const.VSPACE_SIZE_FACTOR + + const.TPS_LEGEND_SIZE_FACTOR + + const.TPS_SEPARATOR_SIZE_FACTOR) + def plot_channels(self, data_dict: Dict, data_set_id: Union[str, Tuple[str, str]], start_5mins_of_diff_days: List[List[float]]): @@ -99,37 +114,75 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget): self.is_working = True self.plotting_data1 = data_dict self.plot_total = len(self.plotting_data1) - + self.fig_height_in = self.init_fig_height_in() self.start_5mins_of_diff_days = start_5mins_of_diff_days self.plotting_bot = const.BOTTOM - self.plotting_bot_pixel = const.BOTTOM_PX self.processed_channels = [] self.channels = [] self.tps_processors = [] - start_msg = f'Plotting {self.get_plot_name()} ...' display_tracking_info(self.tracking_box, start_msg) self.processing_log = [] # [(message, type)] + self.parent.processing_log_message = '' self.gap_bar = None self.date_mode = self.main_window.date_format.upper() if self.plotting_data1 == {}: - title = "NO WAVEFORM DATA TO DISPLAY TPS." + self.title = "NO WAVEFORM DATA TO DISPLAY TPS." self.processing_log.append( ("No WAVEFORM data to display TPS.", LogType.INFO)) else: - title = get_title( + self.title = get_title( data_set_id, self.min_x, self.max_x, self.date_mode) - - self.timestamp_bar_top = self.plotting_axes.add_timestamp_bar(0.) - self.plotting_axes.set_title(title, y=5, v_align='bottom') - + self.plotting_axes.height_normalizing_factor = \ + const.DEFAULT_SIZE_FACTOR_TO_NORMALIZE + self.plotting_bot = const.BOTTOM + self.axes = [] if self.plotting_data1 == {}: + # set_size and plot timestamp_bar for the title's position + self.set_size() + self.timestamp_bar_top = self.plotting_axes.add_timestamp_bar( + False) + self.plotting_axes.set_title(self.title, y=0) self.is_working = False self.draw() - self.clean_up('NO DATA') + self.clean_up(None) return + self.get_plotting_info() + self.plotting_preset() + self.create_plotting_channel_processors() + + def get_plotting_info(self): + """ + Calculate height_ratio and add to c_data to be used to identify height + for TPS plotting. + Add value to fig_height_in corresponding to the tps channels plotted + """ + for chan_id in self.plotting_data1: + c_data = self.plotting_data1[chan_id] + if 'tps_data' not in c_data: + total_days = len(self.start_5mins_of_diff_days) + c_data['height_ratio'] = total_days/1.7 + chan_height_ratio = \ + c_data['height_ratio'] + const.TPS_SEPARATOR_SIZE_FACTOR + self.fig_height_in += chan_height_ratio * const.BASIC_HEIGHT_IN + + def plotting_preset(self): + """ + Set the current widget to be the active tab to have the correct view + port size before calling super's plotting_preset() will preset the + width and height of figure and main_widget. + Plot top timestamp bar but not show to anchor the title. + """ + # set self to be the current widget to have the correct view port size + self.parent.plotting_tab.setCurrentWidget(self) + super().plotting_preset() + self.timestamp_bar_top = self.plotting_axes.add_timestamp_bar( + show_bar=False, top=True + ) + self.plotting_axes.set_title(self.title) + def create_plotting_channel_processors(self): for chan_id in self.plotting_data1: c_data = self.plotting_data1[chan_id] self.channels.append(chan_id) @@ -175,33 +228,36 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget): self.clean_up(chan_id) self.finished_lock.unlock() - def clean_up(self, chan_id): + def clean_up(self, chan_id: Optional[str]) -> None: """ Clean up after all available waveform channels have been stopped or plotted. The cleanup steps are as follows. Display a finished message Add finishing touches to the plot Emit the stopped signal of the widget + + :param chan_id: channel name in str or None if no data, '' if stop """ - if chan_id == '': + if chan_id is None: + self.done() + msg = None + elif chan_id == '': msg = f'{self.get_plot_name()} stopped.' else: msg = f'{self.get_plot_name()} finished.' - if chan_id != 'NO DATA': - self.done() + if msg: + self.parent.processing_log_msg += msg + "\n" + display_tracking_info( + self.tracking_box, self.parent.processing_log_msg) - display_tracking_info(self.tracking_box, msg) - self.is_working = False self.stopped.emit() def done(self): """Add finishing touches to the plot and display it on the screen.""" self.set_legend() - # Set view size fit with the given data - if self.main_widget.geometry().height() < self.plotting_bot_pixel: - self.main_widget.setFixedHeight(self.plotting_bot_pixel) self.set_lim_markers() self.draw() + self.is_working = False def plot_channel(self, c_data: str, chan_id: str) -> Axes: """ @@ -224,9 +280,9 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget): :return ax: axes of the channel """ - total_days = c_data['tps_data'].shape[0] plot_h = self.plotting_axes.get_height( - total_days/1.5, bw_plots_distance=0.003, pixel_height=12.1) + plot_height_ratio=c_data['height_ratio'], + plot_separator_size_factor=const.TPS_SEPARATOR_SIZE_FACTOR) ax = self.create_axes(self.plotting_bot, plot_h) ax.spines[['right', 'left', 'top', 'bottom']].set_visible(False) ax.text( @@ -237,7 +293,7 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget): rotation='horizontal', transform=ax.transAxes, color=self.display_color['plot_label'], - size=self.font_size + 2 + size=const.FONTSIZE + 2 ) zoom_marker1 = ax.plot( @@ -276,15 +332,17 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget): """ Plot one dot for each color and assign label to it. The dots are plotted outside of xlim to not show up in plotting area. xlim is - set so that it has some extra space to show full hightlight square + set so that it has some extra space to show full highlight square of the ruler. ax.legend will create one label for each dot. """ # set height of legend and distance bw legend and upper ax plot_h = self.plotting_axes.get_height( - 21, bw_plots_distance=0.004, pixel_height=12) + plot_height_ratio=const.TPS_LEGEND_SIZE_FACTOR, + plot_separator_size_factor=const.TPS_SEPARATOR_SIZE_FACTOR) ax = self.plotting_axes.canvas.figure.add_axes( - [self.plotting_l, self.plotting_bot, self.plotting_w, plot_h], + [const.TPS_LEFT_NORMALIZE, self.plotting_bot, + const.TPS_WIDTH_NORMALIZE, plot_h], picker=True ) ax.axis('off') @@ -339,7 +397,8 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget): :return ax: matplotlib.axes.Axes - axes of tps of a waveform channel """ ax = self.plotting_axes.canvas.figure.add_axes( - [self.plotting_l, plot_b, self.plotting_w, plot_h], + [const.TPS_LEFT_NORMALIZE, plot_b, + const.TPS_WIDTH_NORMALIZE, plot_h], picker=True ) ax.spines['right'].set_visible(False) @@ -353,7 +412,7 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget): times, major_times, major_time_labels = get_day_ticks() ax.set_xticks(times, minor=True) ax.set_xticks(major_times) - ax.set_xticklabels(major_time_labels, fontsize=self.font_size, + ax.set_xticklabels(major_time_labels, fontsize=const.FONTSIZE, color=self.display_color['basic']) # extra to show highlight square ax.set_xlim(-2, const.NO_5M_DAY + 1) @@ -480,7 +539,8 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget): self.plotting_bot = const.BOTTOM title = get_title( self.data_set_id, self.min_x, self.max_x, self.date_mode) - self.timestamp_bar_top = self.plotting_axes.add_timestamp_bar(0.) + self.timestamp_bar_top = self.plotting_axes.add_timestamp_bar( + show_bar=False) self.plotting_axes.set_title(title, y=0, v_align='bottom') for chan_id in self.plotting_data1: c_data = self.plotting_data1[chan_id] diff --git a/sohstationviewer/view/plotting/waveform_dialog.py b/sohstationviewer/view/plotting/waveform_dialog.py index caf41df7ff3f3c1669567a9e972f835e458655b7..16b1763404dc3add874eee871dea6d62d6c49ade 100755 --- a/sohstationviewer/view/plotting/waveform_dialog.py +++ b/sohstationviewer/view/plotting/waveform_dialog.py @@ -74,6 +74,16 @@ class WaveformDialog(QtWidgets.QWidget): """ self.parent = parent """ + SCREEN INFO + actual_dpi: actual dpi of the screen where waveform dialog is located. + actual_dpi = physical_dpi * device_pixel_ratio + dpi_y: vertical dpi of the screen + dpi_x: horizontal dpi of the screen + """ + self.actual_dpi: float = 100. + self.dpi_x: float = 25. + self.dpi_y: float = 30. + """ data_type: str - type of data being plotted """ self.data_type = None @@ -134,6 +144,20 @@ class WaveformDialog(QtWidgets.QWidget): """ self.plotting_widget.init_size() + def moveEvent(self, event): + """ + Get dpi, dpi_x, dpi_y whenever main window is moved + """ + screen = QtWidgets.QApplication.screenAt(event.pos()) + if screen is not None: + curr_actual_dpi = ( + screen.physicalDotsPerInch() * screen.devicePixelRatio()) + self.dpi_x = screen.physicalDotsPerInchX() + self.dpi_y = screen.physicalDotsPerInchY() + self.actual_dpi = curr_actual_dpi + + return super().moveEvent(event) + @QtCore.Slot() def save_plot(self): """