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..897f80eae262f8e35247c969efd0b6fa66279a63 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,10 +121,15 @@ 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 + +# PLOTTING SIZE MAP +SIZE_PIXEL_MAP = {'smaller': .5, 'normal': 2, 'bigger': 3} +SIZE_UNICODE_MAP = {'smaller': '▼', 'normal': '', 'bigger': '▲'} # ================================================================= # # TYPING CONSTANT # ================================================================= # 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/about_dialog.py b/sohstationviewer/view/about_dialog.py index 2823d734a5b20d9c3e69acea6139607ffed98f24..5f400d64550245243491b52ce87b5d646a7b5b08 100644 --- a/sohstationviewer/view/about_dialog.py +++ b/sohstationviewer/view/about_dialog.py @@ -1,8 +1,12 @@ import sys -from typing import Union +from typing import Union, Dict +import os +from pathlib import Path +import re from PySide6 import QtWidgets, QtCore -from PySide6.QtWidgets import QApplication, QWidget, QDialog, QLabel, QFrame +from PySide6.QtWidgets import QApplication, QWidget, QDialog, QLabel, QFrame, \ + QListWidget, QPlainTextEdit from sohstationviewer.conf import constants @@ -18,6 +22,17 @@ def add_separation_line(layout): layout.addWidget(label) +def remove_empty_lines(text: str) -> str: + """ + Remove lines with no text from text + :param text: the text with empty lines that need to be removed + :return: text with no empty lines + """ + lines = text.split('\n') + no_empty_lines = [line for line in lines if line.strip()] + return '\n'.join(no_empty_lines) + + class AboutDialog(QDialog): """ Dialog to show information of the software. @@ -25,7 +40,7 @@ class AboutDialog(QDialog): About Dialog is always opened and will be raised up when the about menu action is triggered. """ - def __init__(self, parent: Union[QWidget, QApplication]): + def __init__(self, parent: Union[QWidget, QApplication, None]): """ :param parent: the parent widget """ @@ -41,18 +56,31 @@ class AboutDialog(QDialog): "by different types of data loggers.") self.description_label = QLabel(description) + self.history_label = QLabel('History') + # Dictionary of history by version + self.history_dict: Dict[str, str] = self.get_history_dict() + + self.version_list_widget = QListWidget(self) + self.version_list_widget.addItems(reversed(self.history_dict.keys())) + + self.changelog_edit = QPlainTextEdit() + self.changelog_edit.setReadOnly(True) + version = f"Version {constants.SOFTWARE_VERSION}" self.version_label = QLabel(version) built_time = f"Built on {constants.BUILD_TIME}" self.built_time_label = QLabel(built_time) - copyright = u"Copyright \u00A9 2024 EarthScope Consortium" - self.copyright_label = QLabel(copyright) + version_year = constants.SOFTWARE_VERSION.split('.')[0] + copy_right = (u"Copyright \u00A9 " + f"{version_year} EarthScope Consortium") + self.copyright_label = QLabel(copy_right) self.ok_button = QtWidgets.QPushButton('OK', self) self.setup_ui() self.connect_signals() + self.version_list_widget.setCurrentRow(0) def setup_ui(self): self.setWindowTitle("About SOHViewer") @@ -63,6 +91,13 @@ class AboutDialog(QDialog): main_layout.addWidget(self.description_label) add_separation_line(main_layout) + main_layout.addWidget(self.history_label) + history_layout = QtWidgets.QHBoxLayout() + main_layout.addLayout(history_layout) + self.version_list_widget.setFixedWidth(100) + history_layout.addWidget(self.version_list_widget) + history_layout.addWidget(self.changelog_edit) + main_layout.addWidget(self.version_label) main_layout.addWidget(self.built_time_label) main_layout.addWidget(self.copyright_label) @@ -73,8 +108,45 @@ class AboutDialog(QDialog): button_layout.addWidget(self.ok_button) def connect_signals(self) -> None: + self.version_list_widget.currentTextChanged.connect( + self.on_version_changed) self.ok_button.clicked.connect(self.close) + @staticmethod + def get_history_dict(): + """ + Get dictionary of history by version from file HISTORY.rst + :return: dictionary of history by version + """ + current_file_path = os.path.abspath(__file__) + root = Path(current_file_path).parent.parent.parent + history_path = root.joinpath('HISTORY.rst') + version_re = re.compile('^[1-3][0-9]{3}.[1-9].[0-9].[0-9]$') + history_dict = {} + lines = [] + version = None + with open(history_path) as file: + lines = file.readlines() + for line in lines: + line = line.strip() + if version_re.match(line): + version = line + history_dict[version] = "" + elif version is not None and '------' not in line: + history_dict[version] += line + '\n' + return history_dict + + @QtCore.Slot() + def on_version_changed(self, version): + """ + When version is changed, place the corresponded changelog in changelog + edit. + :param version: the selected version + """ + changelog = remove_empty_lines(self.history_dict[version]) + + self.changelog_edit.setPlainText(changelog) + if __name__ == '__main__': app = QtWidgets.QApplication(sys.argv) diff --git a/sohstationviewer/view/db_config/value_color_helper/edit_value_color_dialog/multi_color_dot_dialog.py b/sohstationviewer/view/db_config/value_color_helper/edit_value_color_dialog/multi_color_dot_dialog.py index c8117e1c9fb6fd13fff53dbef23ab18b82720f16..aa47c2d8746beac3d0dd19304f18ca39999f2a05 100644 --- a/sohstationviewer/view/db_config/value_color_helper/edit_value_color_dialog/multi_color_dot_dialog.py +++ b/sohstationviewer/view/db_config/value_color_helper/edit_value_color_dialog/multi_color_dot_dialog.py @@ -10,6 +10,8 @@ from sohstationviewer.view.db_config.value_color_helper.\ edit_value_color_dialog.edit_value_color_dialog import \ EditValueColorDialog, display_color +from sohstationviewer.conf.constants import SIZE_PIXEL_MAP + ROW_TOTAL = 7 @@ -49,7 +51,8 @@ class MultiColorDotDialog(EditValueColorDialog): or lower_bound """ self.upper_equal = upper_equal - + # list of combo boxes to display sizes of points + self.size_comboboxes: List[QtWidgets.QComboBox] = [] # list of widgets to display lower bound which is read only of the # value range of rows self.lower_bound_lnedits: List[QtWidgets.QLineEdit] = [] @@ -99,13 +102,14 @@ class MultiColorDotDialog(EditValueColorDialog): if i == ROW_TOTAL - 1: self.main_layout.addWidget( self.include_greater_than_chkbox, ROW_TOTAL - 1, 0, 1, 1) - (lower_bound_lnedit, higher_bound_lnedit, + (size_combobox, lower_bound_lnedit, higher_bound_lnedit, select_color_btn, color_label) = self.add_row(i) if i == 0: # We provide a default value for the first higher bound editor # because it is the only one that is included in the saved # value colors string by default. higher_bound_lnedit.setText('0') + self.size_comboboxes.append(size_combobox) self.lower_bound_lnedits.append(lower_bound_lnedit) self.higher_bound_lnedits.append(higher_bound_lnedit) self.select_color_btns.append(select_color_btn) @@ -142,6 +146,10 @@ class MultiColorDotDialog(EditValueColorDialog): higher bound line edit, select color button, display color label :param row_id: id of current row """ + size_combobox = QtWidgets.QComboBox() + size_combobox.addItems(SIZE_PIXEL_MAP.keys()) + size_combobox.setCurrentText('normal') + lower_bound_lnedit = QtWidgets.QLineEdit() lower_bound_lnedit.setEnabled(False) @@ -168,13 +176,15 @@ class MultiColorDotDialog(EditValueColorDialog): color_label.setAutoFillBackground(True) # layout - self.main_layout.addWidget(lower_bound_lnedit, row_id, 1, 1, 1) - self.main_layout.addWidget(comparing_label, row_id, 2, 1, 1) - self.main_layout.addWidget(higher_bound_lnedit, row_id, 3, 1, 1) - self.main_layout.addWidget(select_color_btn, row_id, 4, 1, 1) - self.main_layout.addWidget(color_label, row_id, 5, 1, 1) - - return (lower_bound_lnedit, higher_bound_lnedit, + self.main_layout.addWidget(QtWidgets.QLabel('Size'), row_id, 1, 1, 1) + self.main_layout.addWidget(size_combobox, row_id, 2, 1, 1) + self.main_layout.addWidget(lower_bound_lnedit, row_id, 3, 1, 1) + self.main_layout.addWidget(comparing_label, row_id, 4, 1, 1) + self.main_layout.addWidget(higher_bound_lnedit, row_id, 5, 1, 1) + self.main_layout.addWidget(select_color_btn, row_id, 6, 1, 1) + self.main_layout.addWidget(color_label, row_id, 7, 1, 1) + + return (size_combobox, lower_bound_lnedit, higher_bound_lnedit, select_color_btn, color_label) def handle_clear_higher_bound(self, row_id): @@ -360,7 +370,7 @@ class MultiColorDotDialog(EditValueColorDialog): """ self.set_color_enabled(row_id, chkbox.isChecked()) - def set_row(self, vc_idx: int, value: float, color: str): + def set_row(self, vc_idx: int, value: float, color: str, size_desc: str): """ Add values to widgets in a row + row 0: consider uncheck include checkbox if color='not plot' and @@ -374,6 +384,9 @@ class MultiColorDotDialog(EditValueColorDialog): self.include_less_than_chkbox.setChecked(False) else: self.include_less_than_chkbox.setChecked(True) + + self.size_comboboxes[vc_idx].setCurrentText(size_desc) + if vc_idx < ROW_TOTAL - 1: self.higher_bound_lnedits[vc_idx].setText(str(value)) self.lower_bound_lnedits[vc_idx + 1].setText(str(value)) @@ -414,16 +427,22 @@ class MultiColorDotDialog(EditValueColorDialog): if i == 0 and not self.include_less_than_chkbox.isChecked(): color = 'not plot' operator = '<=' if self.upper_equal else '<' - value_color_list.append(f"{operator}{value}:{color}") - if self.include_greater_than_chkbox.isChecked(): + size_desc = self.size_comboboxes[i].currentText() + size = f':{size_desc}' if size_desc != 'normal' else '' + + value_color_list.append(f"{operator}{value}:{color}{size}") + if self.include_greater_than_chkbox.isChecked(): color = self.color_labels[ROW_TOTAL - 1].palette().window( ).color().name().upper() val = f"{self.lower_bound_lnedits[ROW_TOTAL - 1].text()}" + size_desc = self.size_comboboxes[ROW_TOTAL - 1].currentText() + size = f':{size_desc}' if size_desc != 'normal' else '' + if self.upper_equal: - value_color_list.append(f"{val}<:{color}") + value_color_list.append(f"{val}<:{color}{size}") else: - value_color_list.append(f"={val}:{color}") + value_color_list.append(f"={val}:{color}{size}") self.value_color_str = '|'.join(value_color_list) self.accept() @@ -460,17 +479,20 @@ class MultiColorDotLowerEqualDialog(MultiColorDotDialog): vc_parts = self.value_color_str.split('|') count = 0 for vc_str in vc_parts: - value, color = vc_str.split(':') + vcs = vc_str.split(':') + value = vcs[0] + color = vcs[1] + size_desc = vcs[2] if len(vcs) > 2 else 'normal' if value.startswith('<'): # Ex: <1:#00FFFF # Ex: <0:not plot (can be on first value only) value = value.replace('<', '') - self.set_row(count, float(value), color) + self.set_row(count, float(value), color, size_desc) count += 1 else: # Ex: =1:#FF00FF value = value.replace('=', '') - self.set_row(ROW_TOTAL - 1, float(value), color) + self.set_row(ROW_TOTAL - 1, float(value), color, size_desc) class MultiColorDotUpperEqualDialog(MultiColorDotDialog): @@ -504,17 +526,20 @@ class MultiColorDotUpperEqualDialog(MultiColorDotDialog): vc_parts = self.value_color_str.split('|') count = 0 for vc_str in vc_parts: - value, color = vc_str.split(':') + vcs = vc_str.split(':') + value = vcs[0] + color = vcs[1] + size_desc = vcs[2] if len(vcs) > 2 else 'normal' if value.startswith('<='): # Ex: <=1:#00FFFF # Ex: <=0:not plot (can be on first value only) value = value.replace('<=', '') - self.set_row(count, float(value), color) + self.set_row(count, float(value), color, size_desc) count += 1 else: # Ex: 1<:#FF00FF value = value.replace('<', '') - self.set_row(ROW_TOTAL - 1, float(value), color) + self.set_row(ROW_TOTAL - 1, float(value), color, size_desc) if __name__ == '__main__': diff --git a/sohstationviewer/view/db_config/value_color_helper/functions.py b/sohstationviewer/view/db_config/value_color_helper/functions.py index 42e78ba68302685f644b749f4a5cfdbbfa936b5d..5d9684cdca04962f417614becb43f40b8354b834 100644 --- a/sohstationviewer/view/db_config/value_color_helper/functions.py +++ b/sohstationviewer/view/db_config/value_color_helper/functions.py @@ -1,43 +1,7 @@ import re -from sohstationviewer.view.util.color import clr - from sohstationviewer.view.util.plot_type_info import plot_types - - -def convert_value_color_str( - plot_type: str, old_value_color_str: str) -> str: - """ - Convert value_color str to new format. This will be removed after - value_colors in database changed - linesDots: L:G|D:G => Line:#00FF00|Dot:#00FF00 - upDownDots: 0:R|1:G => Down:#FF0000|Up:#00FF00 - multiColorDotsEqualOnUpperBound: - 0:_|1:Y|2:G|+2:M => <=0:not plot|<=1:#FFFF00|<=2:#00FF00|2<:#FF00FF - multiColorDotsEqualOnLowerBound: - 3:R|3.3:Y|=3.3:G => <3:#FF0000|<3.3:#FFFF00|=3.3:#00FF00 - triColorLines: - -1:M|0:R|1:G => -1:#FF00FF|0:#FF0000|1:#00FF00 - :param plot_type: type of channel's plot - :param old_value_color_str: value_color_str in old format - :return: value_color_str in new format - """ - if old_value_color_str == '': - return '' - value_color_list = [] - if old_value_color_str == '' and plot_type == 'linesDots': - old_value_color_str = "L:G" - value_color_parts = old_value_color_str.split('|') - for c_str in value_color_parts: - val, color = c_str.split(':') - val = convert_value(plot_type, val) - if color == '_': - color = "not plot" - else: - if color in clr.keys(): - color = clr[color] - value_color_list.append(f"{val}:{color}") - return '|'.join(value_color_list) +from sohstationviewer.conf.constants import SIZE_UNICODE_MAP def convert_value(plot_type: str, old_value: str): @@ -107,15 +71,16 @@ def prepare_value_color_html(value_colors: str) -> str: html_color_parts = [] color_parts = value_colors.split('|') for c_str in color_parts: - value, color = c_str.split(':') - value = value.replace('<=', '≤').replace('<', '<') - if color == 'not plot': + vcs = c_str.split(':') + value = vcs[0].replace('<=', '≤').replace('<', '<') + size = f':{SIZE_UNICODE_MAP[vcs[2]]}' if len(vcs) > 2 else '' + if vcs[1] == 'not plot': c_html = f"{value}:not plot" else: c_html = ( f"{value}:" - f"<span style='color: {color}; font-size:25px;'>∎" - f"</span>") + f"<span style='color: {vcs[1]}; font-size:25px;'>∎" + f"</span>{size}") html_color_parts.append(c_html) value_color_html = f"<p>{'|'.join(html_color_parts)}</p>" diff --git a/sohstationviewer/view/main_window.py b/sohstationviewer/view/main_window.py index 95d7834744631c1413fc0adb783cf37b8663925e..e6469eac1c3fa04b3526eded6024535b434de230 100755 --- a/sohstationviewer/view/main_window.py +++ b/sohstationviewer/view/main_window.py @@ -68,6 +68,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] = [] @@ -938,6 +948,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) @@ -1219,6 +1232,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.py b/sohstationviewer/view/plotting/plotting_widget/plotting.py index d71957dcf9d5227f3bbdbe14c2ec91a0ed6804f7..d98ac4f8ff8fe8c0c7d216135b4a6c7708775589 100644 --- a/sohstationviewer/view/plotting/plotting_widget/plotting.py +++ b/sohstationviewer/view/plotting/plotting_widget/plotting.py @@ -57,8 +57,10 @@ class Plotting: ax: Optional[Axes] = None, equal_upper: bool = True): """ plot dots in center with colors defined by valueColors in database: - Color codes are defined in colorSettings and limitted in 'valColRE' - in dbSettings.py + + x position of each dot defined in value in points_list. + Color of each dot defined in hex in colors list. + Size of each dot defined in pixel in sizes list. :param c_data: data of the channel which includes down-sampled (if needed) data in keys 'times' and 'data'. @@ -70,19 +72,21 @@ class Plotting: :return: ax in which the channel is plotted """ if equal_upper: - points_list, colors = \ + points_list, colors, sizes = \ get_categorized_data_from_value_color_equal_on_upper_bound( c_data, chan_db_info) else: - points_list, colors = \ + points_list, colors, sizes = \ get_categorized_data_from_value_color_equal_on_lower_bound( c_data, chan_db_info) # flatten point_list to be x x = [item for row in points_list for item in row] - for points, c in zip(points_list, colors): + for points, c, s in zip(points_list, colors, sizes): + # use bar marker instead of dot to avoid overlap when selecting + # bigger size for dots. chan_plot, = ax.plot( points, len(points) * [0], linestyle="", - marker='s', markersize=2, + marker='|', markersize=s, markeredgewidth=.75, zorder=constants.Z_ORDER['DOT'], color=c, picker=True, pickradius=3) ax.chan_plots.append(chan_plot) 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_helper.py b/sohstationviewer/view/plotting/plotting_widget/plotting_helper.py index f6310b48b0ae3c4aeb88f9d20ab677d702ed54d2..deb52e0342bc3ad4b0efa1dd48b273cf6ab380b8 100644 --- a/sohstationviewer/view/plotting/plotting_widget/plotting_helper.py +++ b/sohstationviewer/view/plotting/plotting_widget/plotting_helper.py @@ -70,14 +70,14 @@ def get_masspos_value_colors( def get_categorized_data_from_value_color_equal_on_upper_bound( c_data: Dict, chan_db_info: Dict -) -> Tuple[List[List[float]], List[str]]: +) -> Tuple[List[List[float]], List[str], List[float]]: """ Separate data points and color using valueColors in which the condition will check if value equal to or less than upper bound and greater than lower bound as following example - <=-1:not plot|<=0:#FF0000|0<:#FF00FF means: + <=-1:not plot|<=0:#FF0000:bigger|0<:#FF00FF means: value <= -1 => not plot - -1 < value <= 0 => plot with #FF0000 color + -1 < value <= 0 => plot with #FF0000 color in the bigger size 0 < value => plot with #FF00FF color :param c_data: dict of data of the channel which includes down-sampled data in keys 'times' and 'data'. @@ -85,13 +85,19 @@ def get_categorized_data_from_value_color_equal_on_upper_bound( :return x: list of x to be plotted to get total samples :return colors: list of color to be plotted to decide color of total sample text. + :return sizes: list of sizes in pixel to plot for each points """ prev_val = -constants.HIGHEST_INT value_colors = chan_db_info['valueColors'].split('|') colors = [] points_list = [] + sizes = [] for vc in value_colors: - v, c = vc.split(':') + vcs = vc.split(':') + v = vcs[0] + c = vcs[1] + s_desc = vcs[2] if len(vcs) > 2 else 'normal' + sizes.append(constants.SIZE_PIXEL_MAP[s_desc]) if v == '*': val = v # to have some value for pre_val = val else: @@ -118,20 +124,20 @@ def get_categorized_data_from_value_color_equal_on_upper_bound( if prev_val < data[i] <= val] points_list.append(points) prev_val = val - return points_list, colors + return points_list, colors, sizes def get_categorized_data_from_value_color_equal_on_lower_bound( c_data: Dict, chan_db_info: Dict -) -> Tuple[List[List[float]], List[str]]: +) -> Tuple[List[List[float]], List[str], List[float]]: """ Separate data points and color using valueColors in which the condition will check if value equal to or greater than lower bound and less than upper bound as the following example: - <-1:not plot|<0:#FF0000|=0:#FF00FF means: + <-1:not plot|<0:#FF0000|=0:#FF00FF:smaller means: value < -1 => not plot -1 =< value < 0 => plot with #FF0000 color - value >= 0 => plot with #FF00FF color + value >= 0 => plot with #FF00FF color in a smaller :param c_data: dict of data of the channel which includes down-sampled data in keys 'times' and 'data'. :param chan_db_info: dict of info of channel from DB @@ -143,8 +149,13 @@ def get_categorized_data_from_value_color_equal_on_lower_bound( value_colors = chan_db_info['valueColors'].split('|') colors = [] points_list = [] + sizes = [] for vc in value_colors: - v, c = vc.split(':') + vcs = vc.split(':') + v = vcs[0] + c = vcs[1] + s_desc = vcs[2] if len(vcs) > 2 else 'normal' + sizes.append(constants.SIZE_PIXEL_MAP[s_desc]) colors.append(c) val = get_val(v) times, data = c_data['times'][0], c_data['data'][0] @@ -159,7 +170,7 @@ def get_categorized_data_from_value_color_equal_on_lower_bound( if prev_val <= data[i] < val] points_list.append(points) prev_val = val - return points_list, colors + return points_list, colors, sizes def get_colors_sizes_for_abs_y_from_value_colors( 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) diff --git a/sohstationviewer/view/util/plot_type_info.py b/sohstationviewer/view/util/plot_type_info.py index c90cd9473e76e005267afbafb683bd152f478fef..3a3420ace613bee1854bfbcf411f28747b9e3712 100644 --- a/sohstationviewer/view/util/plot_type_info.py +++ b/sohstationviewer/view/util/plot_type_info.py @@ -1,6 +1,12 @@ import re -color_regex = '#[0-9A-F]{6}' +hexcolor_re = '#[0-9A-F]{6}' # Ex: #0F3A4C +value_re = r'-?[0-9]\.?[0-9]?' # Ex: 0.1, -2.2, 3 +le_value_re = rf'<={value_re}' # Ex: <=0.1 +gt_value_re = rf'{value_re}<' # Ex: 0.1< +le_gt_value_re = rf'({le_value_re}|{gt_value_re})' # Ex: <=0.1|0.1< +l_e_value_re = rf'[=<]{value_re}' # Ex: <0.1, =-0.1 +size_re = '(:((smaller)|(normal)|(bigger)))?' # Ex: :smaller plot_types = { 'linesDots': { @@ -20,7 +26,7 @@ plot_types = { ), "plot_function": "plot_lines_dots", "value_pattern": re.compile('^(L|D|Z|Line|Dot|Zero)$'), - "pattern": re.compile(f'^$|^(?:Line|Dot|Zero):{color_regex}$'), + "pattern": re.compile(f'^$|^(?:Line|Dot|Zero):{hexcolor_re}$'), "default_value_color": "Line:#00CC00" }, 'linesSRate': { @@ -41,7 +47,7 @@ plot_types = { "value = 1 => plot on line y=1 with #0000FF color"), "plot_function": "plot_tri_colors", "value_pattern": re.compile('^-?[10]?$'), - "pattern": re.compile(f'^-?[10]:{color_regex}$'), + "pattern": re.compile(f'^-?[10]:{hexcolor_re}$'), "default_value_color": "-1:#FF0000|0:#00CC00|1:#0099DD", "total_value_color_required": 3 }, @@ -52,7 +58,7 @@ plot_types = { "Ex: Color:#00FF00"), "plot_function": "plot_dot_for_time", "value_pattern": re.compile('^C|(Color)$'), - "pattern": re.compile(f'^$|^Color:{color_regex}$'), + "pattern": re.compile(f'^$|^Color:{hexcolor_re}$'), "default_value_color": "Color:#00CC00", "total_value_color_required": 1 }, @@ -64,11 +70,15 @@ plot_types = { " value <= -1 => not plot\n" " -1 < value <= 0 => plot with #FF0000 color\n" " 0 < value => plot with #FF00FF color\n" + "If the valueColor has arrow up (or the word 'bigger')," + " the point size will be bigger\n" + "If the valueColor has arrow down (or the word 'smaller')," + " the point size will be smaller\n" ), "plot_function": "plot_multi_color_dots_equal_on_upper_bound", - "value_pattern": re.compile(r'^(\+|<=)?[0-9]+\.?[0-9]?<?$'), + "value_pattern": re.compile(rf'^{le_gt_value_re}$'), "pattern": re.compile( - fr'^(\+|<=)?[0-9]+\.?[0-9]?<?:(?:{color_regex}|not plot)$' + fr'^{le_gt_value_re}:({hexcolor_re}|not plot){size_re}$' ), "default_value_color": "<=0:#FF0000|0<:#FF00FF" }, @@ -80,11 +90,15 @@ plot_types = { " value < -1 => not plot\n" " -1 =< value < 0 => plot with #FF0000 color\n" " value >= 0 => plot with #FF00FF color\n" + "If the valueColor has arrow up (or the word 'bigger')," + " the point size will be bigger\n" + "If the valueColor has arrow down (or the word 'smaller')," + " the point size will be smaller\n" ), "plot_function": "plot_multi_color_dots_equal_on_lower_bound", - "value_pattern": re.compile(r'^[=<]?[0-9]\.?[0-9]?$'), + "value_pattern": re.compile(rf'^{l_e_value_re}$'), "pattern": re.compile( - fr'^[=<]?[0-9]\.?[0-9]?:(?:{color_regex}|not plot)' + fr'^{l_e_value_re}:({hexcolor_re}|not plot){size_re}$' ), "default_value_color": "<0:#FF0000|=0:#00CC00" }, @@ -97,7 +111,7 @@ plot_types = { " value == 0 => plot under center line with #FF0000 color"), "plot_function": 'plot_up_down_dots', "value_pattern": re.compile("^(0|1|Up|Down)$"), - "pattern": re.compile(f"^(?:Up|Down):{color_regex}$"), + "pattern": re.compile(f"^(?:Up|Down):{hexcolor_re}$"), "default_value_color": "Down:#FF0000|Up:#00FFFF", "total_value_color_required": 2 } diff --git a/tests/view/db_config/test_param_helper.py b/tests/view/db_config/test_param_helper.py index 00f6e63feb62ffa8996c097185da027a2f7b2be5..8c180cdbc9afd9af7053cebd23b29f3a007e313c 100644 --- a/tests/view/db_config/test_param_helper.py +++ b/tests/view/db_config/test_param_helper.py @@ -142,19 +142,19 @@ class TestValidateValueColorStr(BaseTestCase): def test_multi_color_dots_equal_on_upper_bound(self): with self.subTest("Valid value color string"): result = validate_value_color_str( - '<=0:not plot|<=1:#FFFF00|<=2:#00FF00|2<:#FF00FF', + '<=0:not plot|<=1:#FFFF00|<=2:#00FF00|2<:#FF00FF:smaller', 'multiColorDotsEqualOnUpperBound') self.assertEqual(result, (True, '')) with self.subTest("Repeated value"): result = validate_value_color_str( - '<=0:not plot|<=2:#FFFF00|<=2:#00FF00|2<:#FF00FF', + '<=0:not plot|<=2:#FFFF00|<=2:#00FF00:smaller|2<:#FF00FF', 'multiColorDotsEqualOnUpperBound') self.assertEqual( result, (False, "Duplicated value '<=2' " "in ValueColors string " - "'<=0:not plot|<=2:#FFFF00|<=2:#00FF00|2<:#FF00FF' " + "'<=0:not plot|<=2:#FFFF00|<=2:#00FF00:smaller|2<:#FF00FF' " "isn't allowed.")) with self.subTest("Disordered value"): result = validate_value_color_str( diff --git a/tests/view/db_config/value_color_helper/test_functions.py b/tests/view/db_config/value_color_helper/test_functions.py index c55a0de0173bde2c102962d88c99fed47cb7d772..91ae92c148c16192893ba515e1b2ef1d4c233532 100644 --- a/tests/view/db_config/value_color_helper/test_functions.py +++ b/tests/view/db_config/value_color_helper/test_functions.py @@ -1,121 +1,10 @@ from sohstationviewer.view.db_config.value_color_helper.functions import ( - convert_value_color_str, prepare_value_color_html + prepare_value_color_html ) from tests.base_test_case import BaseTestCase -class TestConvertValueColorStr(BaseTestCase): - def test_lines_dots(self): - with self.subTest("Old format of both line and dot value"): - expected_value_colors = "Line:#00FF00|Dot:#00FF00" - result = convert_value_color_str('linesDots', 'L:G|D:G') - self.assertEqual(result, expected_value_colors) - with self.subTest("Old format of line value"): - expected_value_colors = "Line:#00FF00" - result = convert_value_color_str('linesDots', 'L:G') - self.assertEqual(result, expected_value_colors) - with self.subTest("Old format of default value which is empty string"): - expected_value_colors = "" - result = convert_value_color_str('linesDots', '') - self.assertEqual(result, expected_value_colors) - with self.subTest("New format of both line and dot value"): - expected_value_colors = "Line:#00FF00|Dot:#00FF00" - result = convert_value_color_str('linesDots', - "Line:#00FF00|Dot:#00FF00") - self.assertEqual(result, expected_value_colors) - with self.subTest("New format of line value"): - expected_value_colors = "Line:#00FF00" - result = convert_value_color_str('linesDots', "Line:#00FF00") - self.assertEqual(result, expected_value_colors) - - def test_up_down_dots(self): - with self.subTest("Old format"): - expected_value_colors = "Down:#FF0000|Up:#00FF00" - result = convert_value_color_str('upDownDots', '0:R|1:G') - self.assertEqual(result, expected_value_colors) - with self.subTest("New format"): - expected_value_colors = "Down:#FF0000|Up:#00FF00" - result = convert_value_color_str('upDownDots', - "Down:#FF0000|Up:#00FF00") - self.assertEqual(result, expected_value_colors) - - def test_multi_color_dots_equal_on_upper_bound(self): - with self.subTest("Old format"): - expected_value_colors = ('<=0:not plot|<=1:#FFFF00|<=2:#00FF00' - '|2<:#FF00FF') - result = convert_value_color_str( - 'multiColorDotsEqualOnUpperBound', - '0:_|1:Y|2:G|+2:M') - self.assertEqual(result, expected_value_colors) - with self.subTest("New format"): - expected_value_colors = ('<=0:not plot|<=1:#FFFF00|<=2:#00FF00' - '|2<:#FF00FF') - result = convert_value_color_str( - 'multiColorDotsEqualOnUpperBound', - '<=0:not plot|<=1:#FFFF00|<=2:#00FF00|2<:#FF00FF') - self.assertEqual(result, expected_value_colors) - - def test_multi_color_dots_equal_on_lower_bound(self): - with self.subTest("Old format"): - expected_value_colors = '<3:#FF0000|<3.3:#FFFF00|=3.3:#00FF00' - result = convert_value_color_str( - 'multiColorDotsEqualOnLowerBound', - '3:R|3.3:Y|=3.3:G') - self.assertEqual(result, expected_value_colors) - with self.subTest("New format"): - expected_value_colors = '<3:#FF0000|<3.3:#FFFF00|=3.3:#00FF00' - result = convert_value_color_str( - 'multiColorDotsEqualOnLowerBound', - '<3:#FF0000|<3.3:#FFFF00|=3.3:#00FF00') - self.assertEqual(result, expected_value_colors) - - def test_tri_color_lines(self): - with self.subTest("Old format"): - expected_value_colors = '-1:#FF00FF|0:#FF0000|1:#00FF00' - result = convert_value_color_str( - 'triColorLines', - '-1:M|0:R|1:G') - self.assertEqual(result, expected_value_colors) - with self.subTest("New format"): - expected_value_colors = '-1:#FF00FF|0:#FF0000|1:#00FF00' - result = convert_value_color_str( - 'triColorLines', - '-1:#FF00FF|0:#FF0000|1:#00FF00') - self.assertEqual(result, expected_value_colors) - - def test_incorrect_format(self): - with self.subTest("triColorLines"): - with self.assertRaises(ValueError): - convert_value_color_str( - 'triColorLines', - '=1:M|*0:R|1.1:G') - with self.subTest("upDownDots"): - with self.assertRaises(ValueError): - convert_value_color_str( - 'upDownDots', - '2:M|Line:R|1.1:G|L:Y') - with self.subTest("linesDots"): - with self.assertRaises(ValueError): - convert_value_color_str( - 'linesDots', - '1:M|Up:R|1.1:G|L:Y') - with self.subTest("multiColorDotsEqualOnUpperBound"): - with self.assertRaises(ValueError): - convert_value_color_str( - 'multiColorDotsEqualOnUpperBound', - '*3:#FF0000|<3.3:#FFFF00|=3.3:#00FF00') - with self.subTest("multiColorDotsEqualOnLowerBound"): - with self.assertRaises(ValueError): - convert_value_color_str( - 'multiColorDotsEqualOnLowerBound', - '+0:R|-1:Y|<=2:G|2<:M') - - def test_old_value_color_str_is_empty(self): - result = convert_value_color_str('linesSRate', '') - self.assertEqual(result, '') - - class TestPrepareValueColorHTML(BaseTestCase): def test_lines_dots(self): with self.subTest("Line and dot values"): @@ -149,13 +38,13 @@ class TestPrepareValueColorHTML(BaseTestCase): "<p>≤0:not plot" "|≤1:" "<span style='color: #FFFF00; font-size:25px;'>∎</span>" - "|≤2:" + ":▲|≤2:" "<span style='color: #00FF00; font-size:25px;'>∎</span>" "|2<:" "<span style='color: #FF00FF; font-size:25px;'>∎</span>" "</p>") result = prepare_value_color_html( - '<=0:not plot|<=1:#FFFF00|<=2:#00FF00|2<:#FF00FF') + '<=0:not plot|<=1:#FFFF00:bigger|<=2:#00FF00|2<:#FF00FF') self.assertEqual(result, expected_value_colors) def test_multi_color_dots_equal_on_lower_bound(self): @@ -165,9 +54,9 @@ class TestPrepareValueColorHTML(BaseTestCase): "<span style='color: #FFFF00; font-size:25px;'>∎</span>" "|=3.3:" "<span style='color: #00FF00; font-size:25px;'>∎</span>" - "</p>") + ":▼</p>") result = prepare_value_color_html( - '<3:not plot|<3.3:#FFFF00|=3.3:#00FF00') + '<3:not plot|<3.3:#FFFF00|=3.3:#00FF00:smaller') self.assertEqual(result, expected_value_colors) def test_tri_color_lines(self): diff --git a/tests/view/plotting/plotting_widget/test_plotting_helper.py b/tests/view/plotting/plotting_widget/test_plotting_helper.py index 762016eb87988fbdecfc382e5e8e02e6343a2ddc..943a80c62bf6f0b44ee02647039f696942079384 100644 --- a/tests/view/plotting/plotting_widget/test_plotting_helper.py +++ b/tests/view/plotting/plotting_widget/test_plotting_helper.py @@ -10,6 +10,7 @@ from sohstationviewer.view.plotting.plotting_widget.plotting_helper import ( apply_convert_factor ) from sohstationviewer.view.util.color import clr +from sohstationviewer.conf import constants from tests.base_test_case import BaseTestCase @@ -113,36 +114,48 @@ class TestGetCategorizedDataFromValueColorEqualOnUpperBound(BaseTestCase): def test_equal_on_upper_bound(self): chan_db_info = { - 'valueColors': '<=3:#FF0000|<=3.3:#00FFFF|3.3<:#00FF00' + 'valueColors': '<=3:#FF0000:bigger|<=3.3:#00FFFF|3.3<:#00FF00' } - points_list, colors = \ + points_list, colors, sizes = \ get_categorized_data_from_value_color_equal_on_upper_bound( self.c_data, chan_db_info) self.assertEqual(points_list, [[0, 1], [2, 3], [4]]) self.assertEqual(colors, ['#FF0000', '#00FFFF', '#00FF00']) + self.assertEqual(sizes, + [constants.SIZE_PIXEL_MAP['bigger'], + constants.SIZE_PIXEL_MAP['normal'], + constants.SIZE_PIXEL_MAP['normal']]) def test_not_plot(self): chan_db_info = { 'valueColors': '<=3:not plot|<=3.3:#00FFFF|3.3<:#00FF00' } - points_list, colors = \ + points_list, colors, sizes = \ get_categorized_data_from_value_color_equal_on_upper_bound( self.c_data, chan_db_info) self.assertEqual(points_list, [[2, 3], [4]]) self.assertEqual(colors, ['#00FFFF', '#00FF00']) + self.assertEqual(sizes, + [constants.SIZE_PIXEL_MAP['normal'], + constants.SIZE_PIXEL_MAP['normal'], + constants.SIZE_PIXEL_MAP['normal']]) class TestGetCategorizedDataFromValueColorEqualOnLowerBound(BaseTestCase): def test_equal_on_lower_bound(self): c_data = {'times': [[0, 1, 2, 3, 4]], 'data': [[1, 3, 3.2, 3.3, 3.4]]} - chan_db_info = {'valueColors': '3:R|3.3:Y|=3.3:G'} - points_list, colors = \ + chan_db_info = {'valueColors': '3:R|3.3:Y|=3.3:G:smaller'} + points_list, colors, sizes = \ get_categorized_data_from_value_color_equal_on_lower_bound( c_data, chan_db_info) self.assertEqual(points_list, [[0], [1, 2], [3, 4]]) self.assertEqual(colors, ['R', 'Y', 'G']) + self.assertEqual(sizes, + [constants.SIZE_PIXEL_MAP['normal'], + constants.SIZE_PIXEL_MAP['normal'], + constants.SIZE_PIXEL_MAP['smaller']]) class TestGetColorsSizesForAbsYFromValueColors(BaseTestCase):