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):
         """