Source code for broh5.lib.rendering

"""
Module for constructing GUI components:
    -   Main window.
    -   Dialog for selecting a file.
    -   Dialog for saving a file.
"""

import os
import string
import platform
from pathlib import Path
from typing import Optional, List
from nicegui import ui
from nicegui.events import GenericEventArguments
import broh5.lib.utilities as util


# ============================================================================
#                   Global parameters for the GUI
# ============================================================================

MARKER_LIST = [",", ".", "o", "x"]
CMAP_LIST = ["gray", "inferno", "afmhot", "viridis", "magma"]
AXIS_LIST = [0, 1, 2]
FONT_STYLE = "font-size: 105%; font-weight: bold"
DISPLAY_TYPE = ["plot", "table"]
UPDATE_RATE = 0.2  # second
RATIO = 0.65  # Ratio for adjusting size between image/plot and screen
MAX_FIG_SIZE = [12.0, 9.0]
MAX_PLOT_SIZE = [9.0, 7.0]
INPUT_EXT = ["hdf", "nxs", "h5", "hdf5"]
PLOT_COLOR = "blue"
HEADER_COLOR = "#3874c8"
HEADER_TITLE = "BROWSER-BASED HDF VIEWER"
LEFT_DRAWER_COLOR = "#d7e3f4"
TREE_BGR_COLOR = "#f8f8ff"
BOX_LINE_COLOR = "lime"
BOX_LINE_WIDTH = 3


[docs] class GuiRendering: """ A class to build the graphical user interface for an HDF viewer. This class creates various UI elements like headers, buttons, sliders, tables, and plots to facilitate user interaction with HDF data. Attributes ---------- fig_size : tuple Dimensions for the figure in the UI. plot_size : tuple Dimensions for the histogram plot in the UI. tree_container : UI column Container for the HDF tree structure. select_file_button : UI button Button to trigger file selection. file_path_display : UI label Label to display selected file path. hdf_key_display : UI label Label to display HDF key. hdf_value_display : UI label Label to display HDF value. axis_list : UI select Dropdown to select axis for slicing. cmap_list : UI select Dropdown to select color map for plots. enable_zoom : UI checkbox Check-box to enable/disable image zoom. zoom_list : UI select Dropdown to select zooming ratio. enable_profile : UI checkbox Check-box to enable/disable the intensity-profile plot profile_list : UI select Dropdown to select the direction of the intensity profile. save_image_button : UI button Button to save current image. display_type : UI select Dropdown to choose the display type (plot or table). marker_list : UI select Dropdown to select marker type for plots. save_data_button : UI button Button to save current data. main_slider : UI slider Slider to navigate through slices of 3D data. main_table : UI table Table to display data in tabular form. main_plot : UI matplotlib Matplotlib element to display plots. zoom_profile_plot : UI pyplot Pyplot element to display zooming of an image or intensity-profile. min_slider : UI slider Slider to adjust the minimum value for image contrast. max_slider : UI slider Slider to adjust the maximum value for image contrast. reset_button : UI button Button to reset adjustments. histogram_plot : UI pyplot Pyplot element to display histogram of an image. image_info_table : UI table Table to display statistical information of an image. Methods ------- init_gui() Initializes and constructs the GUI elements. """ def __init__(self): super().__init__() # Initial parameters (sc_height, sc_width, dpi) = util.get_height_width_screen() hei_size = RATIO * sc_width / dpi wid_size = RATIO * sc_height / dpi self.dpi = dpi self.fig_size = (min(hei_size, MAX_FIG_SIZE[0]), min(wid_size, MAX_FIG_SIZE[1])) self.plot_size = (min(hei_size, MAX_PLOT_SIZE[0]), min(wid_size, MAX_PLOT_SIZE[1])) self.tree_container = None self.select_file_button = None self.file_path_display = None self.hdf_key_display = None self.hdf_value_display = None self.axis_list = None self.cmap_list = None self.enable_zoom = None self.zoom_list = None self.enable_profile = None self.profile_list = None self.save_image_button = None self.display_type = None self.marker_list = None self.save_data_button = None self.main_slider = None self.main_table = None self.main_plot = None self.zoom_profile_plot = None self.min_slider = None self.max_slider = None self.reset_button = None self.histogram_plot = None self.image_info_table = None self.tab_one = None self.tab_two = None self.panel_tabs = None self.init_gui()
[docs] def init_gui(self): """ Initializes and constructs the various elements of the GUI. This method sets up headers, drawers, rows, labels, buttons, sliders, tables, and plots to create an interactive user interface. """ # For the header with ui.header().style("background-color: " + HEADER_COLOR).classes( "items-center justify-between"): ui.label(HEADER_TITLE).style(FONT_STYLE) # For the left drawer, used to display a hdf tree. with ui.left_drawer(fixed=True, bottom_corner=True).style( "background-color: " + LEFT_DRAWER_COLOR): with ui.row(): self.tree_container = ui.column() with self.tree_container: self.select_file_button = ui.button( "Select file").props("icon=folder") # Layout for the main page. with ui.column().classes("w-full no-wrap gap-1"): # For displaying file-path, key, and value of a hdf/nxs/h5 file with ui.row().classes("w-full no-wrap"): with ui.row().classes("w-1/3 items-center"): ui.label("File path: ").style(FONT_STYLE) self.file_path_display = ui.label("") with ui.row().classes("w-1/3 items-center"): ui.label("Key: ").style(FONT_STYLE) self.hdf_key_display = ui.label("") with ui.row().classes("w-1/3 items-center"): ui.label("Value: ").style(FONT_STYLE) self.hdf_value_display = ui.label("") ui.separator() # For ui-components used to interact with data. with ui.row().classes("w-full justify-between items-center"): # For ui-components used to interact with 3d data. with ui.row().classes("items-center"): ui.label("Axis: ").style(FONT_STYLE) self.axis_list = ui.select(AXIS_LIST, value=AXIS_LIST[0]) with ui.row().classes("items-center"): ui.label("Color map: ").style(FONT_STYLE) self.cmap_list = ui.select(CMAP_LIST, value=CMAP_LIST[0]) with ui.row().classes("items-center"): self.enable_zoom = ui.checkbox('Zoom') self.zoom_list = ui.select(['2x', '4x', '8x'], value="2x") with ui.row().classes("items-center"): self.enable_profile = ui.checkbox('Profile') self.profile_list = ui.select(['vertical', 'horizontal'], value='horizontal') self.save_image_button = ui.button("Save image") # For ui-components used to interact with 1d/2d data. with ui.row().classes("items-center"): ui.label("Display: ").style(FONT_STYLE) self.display_type = ui.select(DISPLAY_TYPE, value=DISPLAY_TYPE[0]) with ui.row().classes("items-center"): ui.label("Marker: ").style(FONT_STYLE) self.marker_list = ui.select(MARKER_LIST, value=MARKER_LIST[0]) self.save_data_button = ui.button("Save data") # Slider for slicing 3d dataset with ui.row().classes("w-full items-center no-wrap"): ui.label("Slice: ").style(FONT_STYLE) self.main_slider = ui.slider(min=0, max=100, value=0).props( "label-always").on("update:model-value", throttle=UPDATE_RATE, leading_events=False) # Tabs for data visualization and displaying image information tabs = ui.tabs().classes('w-full') with tabs: self.tab_one = ui.tab('Data visualization').style( "background-color: " + TREE_BGR_COLOR) self.tab_two = ui.tab('Image information').style( "background-color: " + TREE_BGR_COLOR) self.panel_tabs = ui.tab_panels(tabs, value=self.tab_one).classes( 'w-full') with self.panel_tabs: # Tab 1 for data visualization with ui.tab_panel(self.tab_one): # For display data as an image, table, or plot self.main_table = ui.table(columns=[], rows=[], row_key="Index") with ui.row().classes("w-full justify-left items-center"): self.main_plot = ui.matplotlib(figsize=self.fig_size, dpi=self.dpi) self.zoom_profile_plot = ui.matplotlib( figsize=self.fig_size, dpi=self.dpi) # Sliders for adjust the contrast of an image. with ui.row().classes( "w-full justify-between no-wrap items-center"): ui.label("Min: ").style(FONT_STYLE) self.min_slider = ui.slider(min=0, max=254, value=0).props( "label-always").on("update:model-value", throttle=UPDATE_RATE, leading_events=False) ui.label("Max: ").style(FONT_STYLE) self.max_slider = ui.slider(min=1, max=255, value=255).props( "label-always").on("update:model-value", throttle=UPDATE_RATE, leading_events=False) self.reset_button = ui.button("Reset") # Tab 2 for showing image information with ui.tab_panel(self.tab_two): with ui.row().classes( "w-full justify-between no-wrap items-center"): self.histogram_plot = ui.pyplot(figsize=self.plot_size, close=False, ).classes("w-full") self.image_info_table = ui.table(columns=[], rows=[], row_key="name")
[docs] class FilePicker(ui.dialog): """ A dialog class for file picking in the GUI. Codes are adapted from an example of NiceGUI: https://github.com/zauberzeug/nicegui/tree/main/examples/local_file_picker Allows users to browse and select files from the local filesystem where the application is running. Parameters ---------- directory : str The starting directory for the file picker. upper_limit : str, optional The upper directory limit for browsing. None by default. show_hidden_files : bool, optional Flag to show hidden files. False by default. allowed_extensions : List of str, optional List of allowed file extensions for filtering. None by default. Methods ------- check_extension(filename: str) Check if the given filename has an allowed extension. add_drives_toggle() Add a toggle for drive selection on Windows systems. update_grid() Update the file grid based on the current directory and filters. handle_double_click(GenericEventArguments) Handle double click events on the file grid. handle_ok() Handle the OK button, click to submit the selected file path. """ def __init__(self, directory: str, *, upper_limit: Optional[str] = None, show_hidden_files: bool = False, allowed_extensions: Optional[List[str]] = None) -> None: super().__init__() self.show_hidden_files = show_hidden_files self.allowed_extensions = allowed_extensions self.drives_toggle = None self.path = Path(directory).expanduser() if upper_limit is None: self.upper_limit = None else: self.upper_limit = Path( directory if upper_limit is ... else upper_limit).expanduser() with self, ui.card(): self.add_drives_toggle() self.grid = ui.aggrid( {'columnDefs': [{'field': 'name', 'headerName': 'File'}], 'rowSelection': 'single'}, html_columns=[0]).classes( 'w-96').on('cellDoubleClicked', self.handle_double_click) with ui.row().classes('w-full justify-end'): ui.button('Cancel', on_click=self.close).props('outline') ui.button('Ok', on_click=self.handle_ok) self.update_grid()
[docs] def check_extension(self, filename: str) -> bool: """Check if the filename has an allowed extension.""" if self.allowed_extensions is None: return True else: return filename.split('.')[-1].lower() in self.allowed_extensions
[docs] def add_drives_toggle(self): """Give a list of available drivers in a WinOS computer""" if platform.system() == 'Windows': drives = ['%s:\\' % d for d in string.ascii_uppercase if os.path.exists('%s:' % d)] if self.path != "" or self.path != ".": select_drive = os.path.splitdrive(self.path)[0] + "\\" else: self.path = Path(drives[0]).expanduser() select_drive = drives[0] self.drives_toggle = ui.toggle(drives, value=select_drive, on_change=self.__update_drive)
def __update_drive(self): if self.drives_toggle: self.path = Path(self.drives_toggle.value).expanduser() self.update_grid()
[docs] def update_grid(self) -> None: paths = list(self.path.glob('*')) if not self.show_hidden_files: paths = [p for p in paths if not p.name.startswith('.')] if self.allowed_extensions: paths = [p for p in paths if p.is_dir() or self.check_extension(p.name)] paths.sort(key=lambda p: p.name.lower()) paths.sort(key=lambda p: not p.is_dir()) self.grid.options['rowData'] = [ {'name': f'📁 <strong>{p.name}</strong>' if p.is_dir() else p.name, 'path': str(p), } for p in paths] if (self.upper_limit is None and self.path != self.path.parent or self.upper_limit is not None and self.path != self.upper_limit): self.grid.options['rowData'].insert(0, { 'name': '📁 <strong>..</strong>', 'path': str(self.path.parent), }) self.grid.update()
[docs] def handle_double_click(self, e: GenericEventArguments) -> None: self.path = Path(e.args['data']['path']) if self.path.is_dir(): self.update_grid() else: if self.path: self.submit(str(self.path)) else: return
[docs] async def handle_ok(self): try: self.update_grid() rows = await self.grid.get_selected_rows() if rows: fpath = [r['path'] for r in rows] if fpath: selected_path = Path(fpath[0]) if selected_path.suffix in {'.h5', '.hdf', '.nxs'}: self.submit(str(selected_path)) else: ui.notify("Please select a file with the extension " ".h5, .hdf, or .nxs!") return else: ui.notify("No file path found in the selected rows") return else: ui.notify("No rows selected. Try double-clicking instead!") return except Exception as e: ui.notify(f"An error occurred: {e}") return
[docs] class FileSaver(ui.dialog): """ A dialog class for saving files in the GUI. Allows users to specify a file name and directory for saving files. Parameters ---------- directory : str Starting directory for the file saver. upper_limit : str, optional Upper directory limit for browsing. None by default. show_hidden_files : bool, optional Flag to show hidden files. False by default. title : str, optional Title for the file-name input-field. 'File name' by default. Methods ------- add_drives_toggle() Add a toggle for drive selection on Windows systems. update_grid() -> None Update the file grid based on the current directory. handle_double_click(e: GenericEventArguments) -> None Handle double-click events on the file grid. handle_save() Handle the Save button, click to submit the specified file path. create_folder_dialog() Open a dialog for creating a new folder. create_folder(folder_name: str, dialog: ui.dialog) Create a new folder with the specified name. """ def __init__(self, directory: str, *, upper_limit: Optional[str] = None, show_hidden_files: bool = False, title: Optional[str] = 'File name') -> None: super().__init__() self.show_hidden_files = show_hidden_files self.drives_toggle = None self.path = Path(directory).expanduser() self.title = title if upper_limit is None: self.upper_limit = None else: self.upper_limit = Path( directory if upper_limit is ... else upper_limit).expanduser() with self, ui.card(): self.add_drives_toggle() self.grid = ui.aggrid( {'columnDefs': [{'field': 'name', 'headerName': 'File'}], 'rowSelection': 'single'}, html_columns=[0]).classes( 'w-96').on('cellDoubleClicked', self.handle_double_click) # Input field for filename self.filename_input = ui.input(self.title).classes( 'w-full justify-between').on('keydown.enter', self.handle_save) with ui.row().classes('w-full justify-between'): ui.button('Create Folder', on_click=self.create_folder_dialog).props('outline') ui.button('Cancel', on_click=self.close).props('outline') ui.button('Save', on_click=self.handle_save) self.update_grid()
[docs] def add_drives_toggle(self): """Give a list of available drivers in a WinOS computer""" if platform.system() == 'Windows': drives = ['%s:\\' % d for d in string.ascii_uppercase if os.path.exists('%s:' % d)] if self.path != "" or self.path != ".": select_drive = os.path.splitdrive(self.path)[0] + "\\" else: self.path = Path(drives[0]).expanduser() select_drive = drives[0] self.drives_toggle = ui.toggle(drives, value=select_drive, on_change=self.__update_drive)
def __update_drive(self): if self.drives_toggle: self.path = Path(self.drives_toggle.value).expanduser() self.update_grid()
[docs] def update_grid(self) -> None: paths = list(self.path.glob('*')) if not self.show_hidden_files: paths = [p for p in paths if not p.name.startswith('.')] paths.sort(key=lambda p: p.name.lower()) paths.sort(key=lambda p: not p.is_dir()) self.grid.options['rowData'] = [ {'name': f'📁 <strong>{p.name}</strong>' if p.is_dir() else p.name, 'path': str(p)} for p in paths] if (self.upper_limit is None and self.path != self.path.parent or self.upper_limit is not None and self.path != self.upper_limit): self.grid.options['rowData'].insert(0, { 'name': '📁 <strong>..</strong>', 'path': str(self.path.parent)}) self.grid.update()
[docs] def handle_double_click(self, e: GenericEventArguments) -> None: self.path = Path(e.args['data']['path']) if self.path.is_dir(): self.update_grid() else: self.filename_input.value = self.path.name self.path = self.path.parent
[docs] def handle_save(self): filename = self.filename_input.value if not filename: ui.notify('File name cannot be empty!') return save_path = self.path / filename save_path_str = str(save_path).replace('\\', '/') self.submit(save_path_str)
[docs] async def create_folder_dialog(self): """Open a dialog to get the name of the new folder and create it.""" with ui.dialog().classes('w-100 h-100') as dialog, ui.card(): with ui.column(): folder_name_input = ui.input('Folder Name').classes( 'w-full justify-between') with ui.row(): ui.button('Cancel', on_click=dialog.close).props('outline') ui.button('Create', on_click=lambda: self.create_folder( folder_name_input.value, dialog)) await dialog
[docs] def create_folder(self, folder_name: str, dialog: ui.dialog): if not folder_name: ui.notify('Folder name cannot be empty!') return new_folder_path = self.path / folder_name if new_folder_path.exists(): ui.notify(f"A folder named '{folder_name}' already exists!") return new_folder_path.mkdir(parents=True, exist_ok=True) self.update_grid() dialog.close()