diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/hazards.css b/app/hazards.css new file mode 100644 index 0000000..33251cf --- /dev/null +++ b/app/hazards.css @@ -0,0 +1,77 @@ +/* +Remove the margin from the body so the ProtoApp fills the page +*/ +body { + margin: 0px 0px 0px 0px; +} + +/* +Window buttons +These must be configured individually due to Panel widgets +creating them as different classes +button.bk.bk-btn.bk-btn-default is the default button widget +a.bk-btn.bk-btn-default is the default download button widget +*/ +button.bk.bk-btn.bk-btn-default { + color: #FFFFFF; + background-color: #AD9C70; + border: 0px; +} +a.bk-btn.bk-btn-default { + color: #FFFFFF; + background-color: #AD9C70; + border: 0px; +} +button.bk.bk-btn.bk-btn-default:hover { + color: #FFFFFF; + background-color: #9b8d69; +} +a.bk-btn.bk-btn-default:hover { + color: #FFFFFF; + background-color: #9b8d69; +} + +/* +Metadata input dropdown +*/ +.bk.card { + border-color: #FFFFFF; +} +.bk.card-header { + background-color: #AD9C70; +} +.bk.card-header:hover { + background-color: #9b8d69; +} +.bk.card-button { + color: #FFFFFF; +} +.bk.card-header-row { + color: #FFFFFF; +} +.bk.bk-clearfix { + font-size: 12px; + font-weight: 400; +} +textarea { + resize: none; +} + +/* +Custom CSS classes for multiple widgets +They must all start with .bk. followed by the CSS class name +.bk.custom_css_class_name +*/ +.bk.custom_header { + text-align: center; +} +.bk.bgs_blue_background { + color: #FFFFFF; + background-color: #002E40; +} +.bk.left_sidebar_divider { + background-color: #AD9C70; +} +.bk.right_sidebar { + background-color: #f8f8f8; +} \ No newline at end of file diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..7f23a57 --- /dev/null +++ b/app/main.py @@ -0,0 +1,231 @@ +import datetime +import itertools +import json +import logging +import param + +import pandas as pd +import panel as pn + +from copy import deepcopy +from io import StringIO, BytesIO + +import bokeh.plotting.figure +from bokeh.palettes import Category20 +from bokeh.models import HoverTool, Range1d +from bokeh.models.widgets.tables import BooleanFormatter + +# Set up the CSS +pn.extension( + sizing_mode="stretch_width", + css_files=["app/hazard.css"], + notifications=True, +) + +logger = logging.getLogger('hazards') +logger.setLevel(logging.DEBUG) +logging.basicConfig(level=logging.DEBUG, + format="%(asctime)s %(name)s %(message)s", + force=True) + +# Suppress debug messages from other packages +for package in ('bokeh', 'matplotlib', 'asyncio', 'MARKDOWN'): + logging.getLogger(package).setLevel(logging.INFO) + + +class tomrapApp(param.Parameterized): + """ + The tomrap Application is created using a class using parameterization. + """ + # Parameterized values allow for callbacks to be made on input fields + pressures_file_input = param.Parameter( + doc=""" + The file input widget for the pressures CSV file. + """, + ) + calibration_file_input = param.Parameter( + doc=""" + The file input widget for the calibration CSV file. + """, + ) + json_filename = param.String( + default="experiment_metadata.json", + doc=""" + The filename which is given to the JSON file upon . + """, + ) + figure_range_slider = param.Parameter( + doc=""" + The slider which is used for changing the range of days which are included in the Bokeh figure. + """, + ) + + def __init__(self, **params) -> None: + logger.info("Initialising tomrap app") + super().__init__(**params) + + # Define starting values for the program + self.loading = True + self.left_sidebar = pn.Column() + self.bokeh_pane_row = pn.Row() + self.centre_view = pn.Column() + self.right_sidebar = pn.Column() + self.date_fields = ["experiment_start_date", "experiment_end_date"] + + # Define default values for the program + # Date fields use today's date as their default value + date_now = datetime.datetime.now() + + # Define default element sizes for the program + self.default_sizes = { + "figure_range_slider": { + "width": 500, + }, + "bokeh_pane_row": { + "height": 500, + }, + "centre_view": { + "width": 900, + }, + } + + # Values in the dictionary can be reset to their defaults when required + self.default_values = { + "logger_loaded": "Loaded data from %s", + "df_calibrated_pressures_original": pd.DataFrame(), + "df_calibrated_pressures": pd.DataFrame(), + "pressure_cols": [], + "df_selected_pressure_cols": pd.DataFrame(), + "df_average_pressures": pd.DataFrame(), + "toggle_page_size_button": { + "reduce": "Reduce Page Size", + "increase": "Increase Page Size", + }, + "metadata_status": { + "active": "Metadata file settings have been applied!", + "inactive": "Metadata file settings are not currently applied!", + }, + "metadata_date": datetime.date( + year=date_now.year, + month=date_now.month, + day=date_now.day, + ), + "json_filename": "experiment_metadata.json", + "bokeh_figure": None, + "figure_range_slider": { + "start": 0, + "end": 100, + }, + "sensor_names_table": pd.DataFrame(), + "last_points_number": 50, + "averages_table": pd.DataFrame(), + "csv_filename": "average_calibrated_pressures.csv", + } + + self.assign_default_data() + self.create_ui_elements() + self.loading = False + + + def assign_default_data(self) -> None: + """ + Function to assign default data values from the default dictionary to class variables. + """ + self.df_calibrated_pressures_original = self.default_values["df_calibrated_pressures_original"] + self.df_calibrated_pressures = self.default_values["df_calibrated_pressures"] + self.pressure_cols = self.default_values["pressure_cols"] + self.df_selected_pressure_cols = self.default_values["df_selected_pressure_cols"] + self.df_average_pressures = self.default_values["df_average_pressures"] + + + def create_ui_elements(self) -> None: + """ + Function to create the UI elements of the application + and apply any require formatting/settings. + """ + # Create default/shared UI values + self.page_size_large = True + self.sidebar_title_margin = (5, 5, 5, 10) + + self.left_sidebar_button = pn.widgets.Button( + name="=", + width=25, + margin=10, + ) + self.left_sidebar_button.on_click(self.toggle_left_sidebar) + self.toggle_page_size_button = pn.widgets.Button( + name=self.default_values["toggle_page_size_button"]["reduce"], + width=100, + margin=10, + ) + self.toggle_page_size_button.on_click(self.toggle_page_size) + # Left sidebar elements + self.pressures_file_input = pn.widgets.FileInput( + accept=".csv", + ) + self.calibration_file_input = pn.widgets.FileInput( + accept=".csv", + ) + self.update_data_button = pn.widgets.Button( + name="Update Data", + ) + + + # Centre view elements + # Bokeh figure + self.bokeh_pane = pn.pane.Bokeh( + sizing_mode="stretch_both", + ) + self.figure_range_slider = pn.widgets.EditableRangeSlider( + name="Start/End", + start=self.default_values["figure_range_slider"]["start"], + end=self.default_values["figure_range_slider"]["end"], + value=tuple(self.default_values["figure_range_slider"].values()), + step=1, + # Set the width of the slider here rather than in the view + # because otherwise the slider shrinks too small + width=self.default_sizes["figure_range_slider"]["width"], + ) + + +def start_tomrap_app(local: bool = False): # -> pn.Column | pn.Row: + """ + Function to run the application. + local determines if the app should start a local server + or return a servable object for deployment. + """ + # Create an instance of the application + tomrap_app_create = tomrapApp() + # Create the view of the application + tomrap_app = tomrap_app_create.view() + title = "tomrap Analysis" + + if local: + server = pn.serve( + # Pass the application to a Panel serve function to create a server instance + tomrap_app, + title=title, + # web_socket_max_message_size allows us to change the default max file size + # when getting a file using the file picker + # The default with Panel is 20MB + websocket_max_message_size=int(1e9), + # Specify the port the application is on + port=8080, + # Solve issues when deploying + allow_websocket_origin=["*"], + ) + # Run the server until the process is ended + # This means the user does not have to run a Panel command to start the application + # They can just run the Python script which will automatically open a browser + # tab with the application in the user's default browser + server.run_until_shutdown() + else: + tomrap_app.servable(title=title) + + return tomrap_app + + +if __name__ == "__main__": + tomrap_app = start_tomrap_app(local=True) +else: + tomrap_app = start_tomrap_app(local=False)