diff --git a/sohstationviewer/conf/config_processor.py b/sohstationviewer/conf/config_processor.py index f427fd5425f4d865ec2f2c601d764e1427064986..51b8f1a516098dd7c41afeefe900f89e43a91730 100644 --- a/sohstationviewer/conf/config_processor.py +++ b/sohstationviewer/conf/config_processor.py @@ -49,6 +49,7 @@ to_date = {QtCore.QDate.currentDate().toString("yyyy-MM-dd")} mp_color_mode = regular tps_color_mode = High date_mode = YYYY-MM-DD +base_plot_font_size = 6 add_mass_pos_to_soh = False ''' @@ -74,6 +75,7 @@ class ConfigProcessor: 'all_soh', 'from_date', 'to_date', 'mp_color_mode', 'tps_color_mode', 'date_mode', + 'base_plot_font_size', 'add_mass_pos_to_soh' # noqa: E131 } @@ -182,6 +184,19 @@ class ConfigProcessor: f'values: ' f'{", ".join(expected_date_modes)}.') + base_plot_font_size = self.config.get( + 'MiscOptions', 'base_plot_font_size') + expected_base_plot_font_sizes = [ + str(i) + for i in range(constants.MIN_FONTSIZE, constants.MAX_FONTSIZE + 1) + ] + + if base_plot_font_size not in expected_base_plot_font_sizes: + raise BadConfigError( + f'Font size can only have one of these ' + f'values: ' + f'{", ".join(expected_base_plot_font_sizes)}.') + def apply_config(self, window: MainWindow) -> None: """ Apply the loaded config to a window. @@ -263,6 +278,11 @@ class ConfigProcessor: elif date_mode == 'YYYY:DOY': window.yyyy_doy_action.trigger() + base_plot_font_size = window.config.get( + 'MiscOptions', 'base_plot_font_size') + window.base_plot_font_size_action_dict[ + int(base_plot_font_size)].trigger() + window.add_masspos_to_rt130_soh.setChecked( get_bool('MiscOptions', 'add_mass_pos_to_soh') ) diff --git a/sohstationviewer/conf/constants.py b/sohstationviewer/conf/constants.py index ffdec557d208d29ea87cdc4fb02a382dd2e2c4dd..a83be36509785de80f4a04f6ad6b28d6e20562ef 100644 --- a/sohstationviewer/conf/constants.py +++ b/sohstationviewer/conf/constants.py @@ -84,7 +84,7 @@ BASIC_HEIGHT_IN = 0.15 # 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 +DEFAULT_SIZE_FACTOR_TO_NORMALIZE = 0.005 # vertical margin TOP_SPACE_SIZE_FACTOR = 3 BOT_SPACE_SIZE_FACTOR = 6 # to avoid hidden time bar partially @@ -98,7 +98,7 @@ PLOT_NONE_SIZE_FACTOR = 0.01 PLOT_SEPARATOR_SIZE_FACTOR = 1 TPS_SEPARATOR_SIZE_FACTOR = 2 # Size factor of TPS legend height -TPS_LEGEND_SIZE_FACTOR = 21 +TPS_LEGEND_SIZE_FACTOR = 15 # ============================================================================= # ================================= NORMALIZE ================================= # Normalized distance from left edge to the start of channel plots @@ -121,8 +121,9 @@ 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 = 50 -# DEFAULT FONT SIZE -FONTSIZE = 6 +# Base font size range +MIN_FONTSIZE = 6 +MAX_FONTSIZE = 12 # 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/documentation/32 _ Options Menu.help.md b/sohstationviewer/documentation/32 _ Options Menu.help.md index 32988bd63789c2c4b51c1ea86c56086cd297c7da..5259e67096ee3f394c2361176a8f7ca1ece6f6c5 100644 --- a/sohstationviewer/documentation/32 _ Options Menu.help.md +++ b/sohstationviewer/documentation/32 _ Options Menu.help.md @@ -73,7 +73,7 @@ From, To dates. <br /> <br /> -<img alt="Date Format" src="images/options_menu/date_format.jpg" width="600" /> +<img alt="Date Format" src="images/options_menu/date_format.jpg" width="620" /> <br /> There are three options to select from: @@ -83,4 +83,15 @@ There are three options to select from: + YYYYMMMDD: Ex. 2023MAR01 + <br /> -<br /> \ No newline at end of file +<br /> + + +### Base Font Size + +Allow user to select the font size of texts displayed in the plotting. This +option has to be selected before reading data or replot data. + +<br /> +<br /> +<img alt="Base Plot Font Size" src="images/options_menu/base_plot_font_size.jpg" width="530" /> +<br /> diff --git a/sohstationviewer/documentation/images/options_menu/base_plot_font_size.jpg b/sohstationviewer/documentation/images/options_menu/base_plot_font_size.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7e67d3962dba333b5d503dd9e9da2e6acff3a434 Binary files /dev/null and b/sohstationviewer/documentation/images/options_menu/base_plot_font_size.jpg differ diff --git a/sohstationviewer/view/main_window.py b/sohstationviewer/view/main_window.py index 5fe5c6697272b65be8a186e7434dfc449009e666..d33f02ecd19a17872bb72acf32cce59c7a3c0456 100755 --- a/sohstationviewer/view/main_window.py +++ b/sohstationviewer/view/main_window.py @@ -67,6 +67,16 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): super().__init__(parent) self.setup_ui(self) """ + vertical_ratio: ratio based selected font and min_font to spread out + layout vertically according to the selected font + """ + self.vertical_ratio: float = 1. + """ + fig_height_in_ratio: ratio based selected font and min_font to resize + plot's main_widget, figure and canvas vertically + """ + self.fig_height_in_ratio: float = 1 + """ dir_names: list of absolute paths of data sets """ self.list_of_dir: List[Path] = [] @@ -940,6 +950,9 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): self.clear_plots() if self.has_problem: return + font_ratio = self.base_plot_font_size / constants.MAX_FONTSIZE + self.vertical_ratio = 0.45 + font_ratio + self.fig_height_in_ratio = 2 * font_ratio self.is_plotting_soh = True self.plotting_widget.set_colors(self.color_mode) self.waveform_dlg.plotting_widget.set_colors(self.color_mode) @@ -1221,6 +1234,18 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): raise Exception('Something is very wrong. No date mode is chosen.' 'Please contact the software group.') self.config.set('MiscOptions', 'date_mode', date_mode) + + base_plot_font_size = None + for i in range(constants.MIN_FONTSIZE, constants.MAX_FONTSIZE + 1): + if self.base_plot_font_size_action_dict[i].isChecked(): + base_plot_font_size = i + break + if base_plot_font_size is None: + raise Exception('Something is very wrong. No base font size is ' + 'chosen. Please contact the software group.') + self.config.set('MiscOptions', 'base_plot_font_size', + str(base_plot_font_size)) + self.config.set('MiscOptions', 'add_mass_pos_to_soh', str(self.add_masspos_to_rt130_soh.isChecked())) with open(CONFIG_PATH, 'w+') as file: 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 7b7ad46d599c829bb8ea8867dc28ffc3c441e958..2e37c46a578d38375651cc44da9c14b104464b76 100644 --- a/sohstationviewer/view/plotting/plotting_widget/multi_threaded_plotting_widget.py +++ b/sohstationviewer/view/plotting/plotting_widget/multi_threaded_plotting_widget.py @@ -106,7 +106,8 @@ class MultiThreadedPlottingWidget(PlottingWidget): 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 + const.DEFAULT_SIZE_FACTOR_TO_NORMALIZE * \ + self.main_window.vertical_ratio self.plotting_bot = const.BOTTOM self.axes = [] diff --git a/sohstationviewer/view/plotting/plotting_widget/plotting_axes.py b/sohstationviewer/view/plotting/plotting_widget/plotting_axes.py index 669d3af5678093123281ce45e078f59a796e7001..633c28dd8527e816e8be0579a883e80088689713 100644 --- a/sohstationviewer/view/plotting/plotting_widget/plotting_axes.py +++ b/sohstationviewer/view/plotting/plotting_widget/plotting_axes.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Dict +from typing import List, Optional, Dict, Tuple import numpy as np @@ -16,6 +16,28 @@ from sohstationviewer.view.util.color import clr from sohstationviewer.conf import constants as const +def split_label(label: str) -> Tuple[str, str]: + """ + Split label into 2 parts: + + If there is '-' between channel and detail, split at the hyphen + + Otherwise split into words + + :param label: label to be split + :return 2 parts of label + """ + if '-' in label: + labels = label.split('-') + if len(labels) == 2: + return labels[0], labels[1] + else: + return label[0], '-'.join(labels[1:]) + labels = label.split(' ') + half_of_total_word = int(len(labels) / 2) + label = ' '.join(labels[0:half_of_total_word]) + sub_label = ' '.join(labels[half_of_total_word:]) + return label, sub_label + + class PlottingAxes: """ Class that includes a figure to add axes for plotting and all methods @@ -77,15 +99,25 @@ class PlottingAxes: :param top: flag indicating this timestamp_bar is located on top or bottom to locate tick label. """ + height_normalizing_factor = self.height_normalizing_factor + if self.parent.name == 'TPS': + # For TPS, vertical_ratio only apply to title and timestamp bar + height_normalizing_factor *= self.main_window.vertical_ratio + # For bottom timestamp bar: space from the last plot to the bar + # For top timestamp bar: space from title to the bar self.parent.plotting_bot -= \ - const.TIME_BAR_SIZE_FACTOR * self.height_normalizing_factor + const.TIME_BAR_SIZE_FACTOR * 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.TOP_SPACE_SIZE_FACTOR * self.height_normalizing_factor - height = 0.0005 * self.height_normalizing_factor + # add space for title + margin = const.TOP_SPACE_SIZE_FACTOR * height_normalizing_factor + if self.parent.name != 'TPS': + # TPS has timestamp bar not shown, reduce the margin so + # the total space from edge to the plot is reasonable + margin /= 3 + self.parent.plotting_bot -= margin + + height = 0.0005 * height_normalizing_factor timestamp_bar = self.fig.add_axes( [const.PLOT_LEFT_NORMALIZE, self.parent.plotting_bot, @@ -107,9 +139,10 @@ class PlottingAxes: labelbottom = True # bottom timestamp bar: space from edge to the bar self.parent.plotting_bot -= \ - const.BOT_SPACE_SIZE_FACTOR * self.height_normalizing_factor + const.BOT_SPACE_SIZE_FACTOR * height_normalizing_factor timestamp_bar.tick_params(which='major', length=7, width=2, + pad=5 * self.main_window.vertical_ratio, direction='inout', colors=self.parent.display_color['basic'], labelbottom=labelbottom, @@ -119,7 +152,7 @@ class PlottingAxes: colors=self.parent.display_color['basic']) timestamp_bar.set_ylabel('HOURS', fontweight='bold', - fontsize=const.FONTSIZE, + fontsize=self.main_window.base_plot_font_size, rotation=0, labelpad=const.HOUR_TO_TMBAR_D, ha='left', @@ -142,8 +175,9 @@ class PlottingAxes: 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=const.FONTSIZE + 1) + timestamp_bar.set_xticklabels( + major_time_labels, + fontsize=self.main_window.base_plot_font_size + 1) timestamp_bar.set_xlim(self.parent.min_x, self.parent.max_x) @staticmethod @@ -192,7 +226,7 @@ class PlottingAxes: ax.tick_params(colors=self.parent.display_color['basic'], width=0, pad=-2, - labelsize=const.FONTSIZE) + labelsize=self.main_window.base_plot_font_size) # transparent background => self.fig will take care of background ax.patch.set_alpha(0) # prepare chan_plots list to be reference for the plotted lines/dots @@ -218,7 +252,7 @@ class PlottingAxes: rotation='horizontal', transform=ax.transAxes, color=color, - size=const.FONTSIZE + size=self.main_window.base_plot_font_size ) def set_axes_info(self, ax: Axes, @@ -249,8 +283,24 @@ class PlottingAxes: """ if label is None: label = chan_db_info['label'] + + label_color = self.parent.display_color['plot_label'] + if label.startswith("DEFAULT"): + label_color = self.parent.display_color["warning"] + + info_color = label_color + if info != '': + info_color = self.parent.display_color['sub_basic'] + + elif self.main_window.base_plot_font_size > 8: + # When there's no info and font size is big, + # separate label into 2 lines. + # The second line will be in the position of info. + label, info = split_label(label) + pos_y = 0.4 if info != '': + # set info/sub label on left side in the lower part ax.text( -0.11, 0.4, info, @@ -258,22 +308,20 @@ class PlottingAxes: verticalalignment='top', rotation='horizontal', transform=ax.transAxes, - color=self.parent.display_color['sub_basic'], - size=const.FONTSIZE + color=info_color, + size=self.main_window.base_plot_font_size ) pos_y = 0.6 - # set title on left side - color = self.parent.display_color['plot_label'] - if label.startswith("DEFAULT"): - color = self.parent.display_color["warning"] + # set main label on left side on the higher part + # or whole label on left side in the middle ax.text( -0.11, pos_y, label, horizontalalignment='left', rotation='horizontal', transform=ax.transAxes, - color=color, - size=const.FONTSIZE + 1 + color=label_color, + size=self.main_window.base_plot_font_size + 1 ) if not sample_no_pos: @@ -463,4 +511,4 @@ class PlottingAxes: horizontalalignment='left', transform=self.parent.timestamp_bar_top.transAxes, color=self.parent.display_color['basic'], - size=const.FONTSIZE + 2) + size=self.main_window.base_plot_font_size + 2) diff --git a/sohstationviewer/view/plotting/plotting_widget/plotting_widget.py b/sohstationviewer/view/plotting/plotting_widget/plotting_widget.py index 302a11922b3c26f56ed92cab08cc013dfe24a01c..0cd8f4185642d54d01277cb463522b9742650bab 100644 --- a/sohstationviewer/view/plotting/plotting_widget/plotting_widget.py +++ b/sohstationviewer/view/plotting/plotting_widget/plotting_widget.py @@ -305,9 +305,18 @@ class PlottingWidget(QtWidgets.QScrollArea): Set height of figure and main widget according to total of plotting channels' heights. + Vertical_ratio and fig_height_in_ratio are used to spread out plotting. + However, TPS channels won't need to be spread out. + Calculate height_normalizing_factor. """ self.set_size() + # Don't need to spead out channels for TPS + vertical_ratio = (self.main_window.vertical_ratio + if self.name != 'TPS' else 1) + if self.name != 'TPS': + self.fig_height_in *= self.main_window.fig_height_in_ratio + fig_height = math.ceil(self.fig_height_in * self.dpi_y) # adjusting main_widget to fit view port @@ -325,7 +334,7 @@ class PlottingWidget(QtWidgets.QScrollArea): # 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 + self.height_normalizing_factor = vertical_ratio / normalize_total if fig_height < max_height: # to avoid artifact in main widget's section that isn't covered by @@ -340,7 +349,7 @@ class PlottingWidget(QtWidgets.QScrollArea): self.plotting_axes.fig.set_figheight(self.fig_height_in) self.plotting_axes.height_normalizing_factor = \ - self.height_normalizing_factor + self.height_normalizing_factor * vertical_ratio # ======================================================================= # # EVENT # ======================================================================= # 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 c8834784b11af2ce0048e3fa4fdac8b0106ca0fb..579d0e8c42021485c2a3975ea50d6bafbf1780dd 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 @@ -104,7 +104,7 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget): def init_fig_height_in(self): return const.BASIC_HEIGHT_IN * ( - const.TOP_SPACE_SIZE_FACTOR + + const.TOP_SPACE_SIZE_FACTOR/3 + const.BOT_SPACE_SIZE_FACTOR + const.TPS_LEGEND_SIZE_FACTOR + const.TPS_SEPARATOR_SIZE_FACTOR) @@ -164,6 +164,8 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget): self.has_data = True self.title = get_title( data_set_id, self.min_x, self.max_x, self.date_mode) + # With TPS, not apply vertical ratio to channels, + # only for timestamp bar and title self.plotting_axes.height_normalizing_factor = \ const.DEFAULT_SIZE_FACTOR_TO_NORMALIZE self.plotting_bot = const.BOTTOM @@ -210,7 +212,7 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget): self.timestamp_bar_top = self.plotting_axes.add_timestamp_bar( show_bar=False, top=True ) - self.plotting_axes.set_title(self.title) + self.plotting_axes.set_title(self.title, y=1000) def create_plotting_channel_processors(self): for chan_id in sorted(self.plotting_data1.keys()): @@ -331,7 +333,7 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget): rotation='horizontal', transform=ax.transAxes, color=self.display_color['plot_label'], - size=const.FONTSIZE + 2 + size=self.main_window.base_plot_font_size + 2 ) zoom_marker1 = ax.plot( @@ -379,7 +381,8 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget): ax.set_ylim(-(c_data['tps_data'].shape[0] + 1), 1) # show separation year tick labels ax.set_yticks(start_year_indexes) - ax.set_yticklabels(start_year_labels, fontsize=const.FONTSIZE, + ax.set_yticklabels(start_year_labels, + fontsize=self.main_window.base_plot_font_size, color=self.display_color['basic']) return ax @@ -448,7 +451,8 @@ 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=const.FONTSIZE, + ax.set_xticklabels(major_time_labels, + fontsize=self.main_window.base_plot_font_size, color=self.display_color['basic']) if self.display_top_ticks: # Show time ticks on both top and bottom of the first ax. diff --git a/sohstationviewer/view/ui/main_ui.py b/sohstationviewer/view/ui/main_ui.py index a637aad5e50d7eb03ae947c918cd7c22255cfb7d..bf796eae77be982c8bf8213c6befabc042384ad7 100755 --- a/sohstationviewer/view/ui/main_ui.py +++ b/sohstationviewer/view/ui/main_ui.py @@ -1,6 +1,6 @@ # UI and connectSignals for main_window import configparser -from typing import Union, List, Optional +from typing import Union, List, Optional, Dict from PySide6 import QtCore, QtGui, QtWidgets from PySide6.QtWidgets import ( @@ -254,6 +254,10 @@ class UIMainWindow(object): self.yyyy_doy_action: Union[QAction, None] = None self.yyyy_mm_dd_action: Union[QAction, None] = None self.yyyymmmdd_action: Union[QAction, None] = None + """ + Dict of base_plot_font_size_action by size + """ + self.base_plot_font_size_action_dict: Dict[int, QAction] = {} # ======================== Database Menu ========================== """ @@ -732,6 +736,18 @@ class UIMainWindow(object): date_format_menu.addAction(self.yyyymmmdd_action) date_format_action_group.addAction(self.yyyymmmdd_action) + base_plot_font_size_menu = QMenu('Base Font Size:', main_window) + base_plot_font_size_action_group = QActionGroup(main_window) + menu.addMenu(base_plot_font_size_menu) + for i in range(constants.MIN_FONTSIZE, constants.MAX_FONTSIZE + 1): + self.base_plot_font_size_action_dict[i] = QAction( + f'{i}px', main_window) + self.base_plot_font_size_action_dict[i].setCheckable(True) + base_plot_font_size_menu.addAction( + self.base_plot_font_size_action_dict[i]) + base_plot_font_size_action_group.addAction( + self.base_plot_font_size_action_dict[i]) + def create_database_menu(self, main_window, menu): """ Create Database Menu's Actions which allow user to add/edit @@ -811,6 +827,9 @@ class UIMainWindow(object): self.yyyy_doy_action.triggered.connect( lambda: main_window.set_date_format('YYYY:DOY')) + for i in range(constants.MIN_FONTSIZE, constants.MAX_FONTSIZE + 1): + self.connect_base_plot_font_size_action(main_window, i) + # Database self.add_edit_data_type_action.triggered.connect( main_window.open_data_type) @@ -830,6 +849,23 @@ class UIMainWindow(object): self.calendar_action.triggered.connect(main_window.open_calendar) self.doc_action.triggered.connect(main_window.open_help_browser) + def connect_base_plot_font_size_action( + self, main_window: QMainWindow, size: int): + """ + Connect an action of base_plot_font_size_dict with task to assign + main_window.base_plot_font_size to the corresponding size. + + This have to be written separately to avoid problem of lambda + in loop, which only pass the value in the last loop to the function of + lambda. + + :param main_window: main control window + :param size: font size + """ + self.base_plot_font_size_action_dict[size].triggered.connect( + lambda: setattr( + main_window, 'base_plot_font_size', size)) + def connect_widget_signals(self, main_window): main_window.current_directory_changed.connect( self.curr_dir_line_edit.setText)