diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index dbc6f8e..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code-runner.runInTerminal": true, - "code-runner.saveFileBeforeRun": true, - "code-runner.clearPreviousOutput": true, -} \ No newline at end of file diff --git a/app.py b/app.py index 29de324..43e4af5 100644 --- a/app.py +++ b/app.py @@ -1,12 +1,15 @@ +"""Main Dash App for Rainfall Analysis""" + +from itertools import product +from pathlib import Path +from dash import dcc, html, Input, Output, State +import pandas as pd import dash import dash_bootstrap_components as dbc -import pandas as pd import plotly.io as pio -import pyfigure, pyfunc, pylayout, pylayoutfunc -from dash import dcc, html, Input, Output, State -from pathlib import Path from pyconfig import appConfig from pytemplate import hktemplate +import pyfigure, pyfunc, pylayout, pylayoutfunc # pylint: disable=multiple-imports pio.templates.default = hktemplate @@ -17,14 +20,14 @@ # BOOTSRAP THEME THEME = appConfig.DASH_THEME.THEME -dbc_css = ( - "https://cdn.jsdelivr.net/gh/AnnMarieW/dash-bootstrap-templates@V1.0.4/dbc.min.css" +DBC_CSS = ( + "https://cdn.jsdelivr.net/gh/AnnMarieW/dash-bootstrap-templates@V1.1.2/dbc.min.css" ) # APP app = dash.Dash( APP_TITLE, - external_stylesheets=[getattr(dbc.themes, THEME), dbc_css], + external_stylesheets=[getattr(dbc.themes, THEME), DBC_CSS], title=APP_TITLE, update_title=UPDATE_TITLE, meta_tags=[ @@ -39,6 +42,7 @@ pylayout.HTML_TITLE, pylayout.HTML_ALERT_README, pylayout.HTML_ROW_BUTTON_UPLOAD, + pylayout.HTML_ROW_BUTTON_EXAMPLE, pylayout.HTML_ROW_TABLE, pylayout.HTML_ROW_BUTTON_VIZ, pylayout.HTML_ROW_OPTIONS_GRAPH_RAINFALL, @@ -49,11 +53,11 @@ pylayout.HTML_ROW_GRAPH_ANALYSIS, pylayout.HTML_ROW_GRAPH_CUMSUM, pylayout.HTML_ROW_GRAPH_CONSISTENCY, - # pylayout.HTML_MADEBY, + html.Hr(), pylayout.HTML_SUBTITLE, pylayout.HTML_FOOTER, ], - fluid=True, + fluid=False, className="dbc", ) @@ -69,19 +73,32 @@ Input("dcc-upload", "contents"), State("dcc-upload", "filename"), State("dcc-upload", "last_modified"), - Input("button-skip", "n_clicks"), + Input("button-example-1", "n_clicks"), + Input("button-example-2", "n_clicks"), + Input("button-example-3", "n_clicks"), + Input("button-example-4", "n_clicks"), prevent_initial_call=True, ) -def callback_upload(content, filename, filedate, _): +def callback_upload(content, filename, filedate, _b1, _b2, _b3, _b4): + """Callback for uploading data and displaying the table.""" + ctx = dash.callback_context if content is not None: children, dataframe = pyfunc.parse_upload_data(content, filename, filedate) - if ctx.triggered[0]["prop_id"] == "button-skip.n_clicks": - dataframe = pd.read_csv( - Path(r"./example_7Y5S.csv"), index_col=0, parse_dates=True - ) + example_data = { + "button-example-1.n_clicks": r"./example_7Y5S.csv", + "button-example-2.n_clicks": r"./example_2Y4S_named.csv", + "button-example-3.n_clicks": r"./example_9Y1S_named.csv", + "button-example-4.n_clicks": r"./example_1Y7S_named.csv", + } + + context_trigger_prop_id = ctx.triggered[0]["prop_id"] + + if context_trigger_prop_id in example_data: + example_file = example_data[context_trigger_prop_id] + dataframe = pd.read_csv(Path(example_file), index_col=0, parse_dates=True) filename = None filedate = None @@ -130,6 +147,8 @@ def callback_upload(content, filename, filedate, _): prevent_initial_call=True, ) def callback_visualize(_, table_data, table_columns, graphbar_opt): + """Callback for visualizing the rainfall data.""" + dataframe = pyfunc.transform_to_dataframe(table_data, table_columns) row_download_table_style = {"visibility": "visible"} @@ -139,13 +158,13 @@ def callback_visualize(_, table_data, table_columns, graphbar_opt): button_analyze_outline = False if dataframe.size > (366 * 8): - fig = pyfigure.figure_scatter(dataframe) + fig = pyfigure.generate_scatter_figure(dataframe) else: row_graphbar_options_style = {"visibility": "visible"} if graphbar_opt in ["group", "stack"]: - fig = pyfigure.figure_bar(dataframe, graphbar_opt) + fig = pyfigure.generate_bar_figure(dataframe, graphbar_opt) else: - fig = pyfigure.figure_scatter(dataframe) + fig = pyfigure.generate_scatter_figure(dataframe) return [ fig, @@ -165,6 +184,7 @@ def callback_visualize(_, table_data, table_columns, graphbar_opt): prevent_initial_call=True, ) def callback_download_table(_, table_data, table_columns): + """Callback for downloading the table data.""" dataframe = pyfunc.transform_to_dataframe(table_data, table_columns) return dcc.send_data_frame(dataframe.to_csv, "derived_table.csv") @@ -182,6 +202,7 @@ def callback_download_table(_, table_data, table_columns): prevent_initial_call=True, ) def callback_analyze(_, table_data, table_columns): + """Callback for analyzing the rainfall data.""" button_viz_analysis_disabled = True button_viz_analysis_outline = True @@ -200,7 +221,7 @@ def callback_analyze(_, table_data, table_columns): ] # CUMUMLATIVE SUM - cumsum = pyfunc.calc_cumsum(dataframe) + cumsum = pyfunc.calculate_cumulative_sum(dataframe) _, table_cumsum = pylayoutfunc.create_table_layout( cumsum, "table-cumsum", deletable=False @@ -219,8 +240,12 @@ def callback_analyze(_, table_data, table_columns): button_viz_analysis_disabled = False button_viz_analysis_outline = False row_button_download_analysis_style = {"visibility": "visible"} - except Exception as e: - children = html.Div(f"SOMETHING ERROR {e}") + except (TypeError, ValueError) as e: + children = html.Div( + f"Input data or columns are not in the expected format: {e}" + ) + except KeyError as e: + children = html.Div(f"Dataframe does not have the expected columns: {e}") return [ children, @@ -254,6 +279,7 @@ def callback_download_results( cumsum_data, cumsum_columns, ): + """Callback for downloading the analysis results.""" biweekly = (biweekly_data, biweekly_columns) monthly = (monthly_data, monthly_columns) @@ -310,7 +336,7 @@ def callback_graph_analysis( cumsum_data, cumsum_columns, ): - from itertools import product + """Callback for generating the analysis graphs.""" label_periods = ["Biweekly", "Monthly", "Yearly"] label_maxsum = ["Max + Sum"] @@ -334,7 +360,7 @@ def callback_graph_analysis( summary_all.append(dataframe) graphs_maxsum = [ - pyfigure.figure_summary_maxsum( + pyfigure.generate_summary_maximum_sum( summary, title=f"{period}: {title}", period=period, @@ -343,12 +369,12 @@ def callback_graph_analysis( for summary, title, period in zip(summary_all, label_maxsum * 3, label_periods) ] graphs_raindry = [ - pyfigure.figure_summary_raindry( + pyfigure.generate_summary_rain_dry( summary, title=f"{period}: {title}", period=period ) for summary, title, period in zip(summary_all, label_raindry * 3, label_periods) ] - graph_maxdate = [pyfigure.figure_summary_maxdate(summary_all)] + graph_maxdate = [pyfigure.generate_summary_maximum_date(summary_all)] all_graphs = graphs_maxsum + graphs_raindry + graph_maxdate labels = [": ".join(i) for i in product(label_ufunc, label_periods)] @@ -363,7 +389,8 @@ def callback_graph_analysis( cumsum = pyfunc.transform_to_dataframe(cumsum_data, cumsum_columns) graph_cumsum = [ - pyfigure.figure_cumsum_single(cumsum, col=station) for station in cumsum.columns + pyfigure.generate_cumulative_sum(cumsum, data_column=station) + for station in cumsum.columns ] children_cumsum = pylayoutfunc.create_tabcard_graph_layout( @@ -375,13 +402,15 @@ def callback_graph_analysis( if cumsum.columns.size == 1: children_consistency = ( dcc.Graph( - figure=pyfigure.figure_empty(text="Not Available for Single Station"), + figure=pyfigure.generate_empty_figure( + text="Not Available for Single Station" + ), config={"staticPlot": True}, ), ) else: graph_consistency = [ - pyfigure.figure_consistency(cumsum, col=station) + pyfigure.generate_scatter_with_trendline(cumsum, data_column=station) for station in cumsum.columns ] @@ -392,14 +421,5 @@ def callback_graph_analysis( return children_analysis, children_cumsum, children_consistency -@app.callback( - Output("row-troubleshoot", "children"), - Input("button-troubleshoot", "n_clicks"), - prevent_initial_call=True, -) -def _callback_troubleshoot(_): - return html.Div("troubleshoot") - - if __name__ == "__main__": app.run_server(debug=DEBUG) diff --git a/app_config.yml b/app_config.yml index a1cc01a..386170c 100644 --- a/app_config.yml +++ b/app_config.yml @@ -1,10 +1,10 @@ DASH_APP: APP_TITLE: Rainfall Data Explorer UPDATE_TITLE: Updating... - DEBUG: False + DEBUG: FALSE DASH_THEME: - THEME: COSMO + THEME: ZEPHYR TEMPLATE: LOGO_SOURCE: @@ -12,6 +12,6 @@ TEMPLATE: SHOW_LEGEND_INSIDE: False SHOW_RANGESELECTOR: False -VERSION: v1.3.0 -GITHUB_LINK: https://github.com/fiakoenjiniring/rainfall -GITHUB_REPO: fiakoenjiniring/rainfall +VERSION: v1.4.0 +GITHUB_LINK: https://github.com/taruma/rainfall +GITHUB_REPO: taruma/rainfall diff --git a/pyconfig.py b/pyconfig.py index cd879e7..849eb22 100644 --- a/pyconfig.py +++ b/pyconfig.py @@ -1,3 +1,5 @@ +"""This module is used to load the configuration file and make it available to the application.""" + from box import Box _CONFIG_PATH = "app_config.yml" diff --git a/pyfigure.py b/pyfigure.py index f27f961..1029cbc 100644 --- a/pyfigure.py +++ b/pyfigure.py @@ -1,47 +1,76 @@ -from dash import dcc -from plotly.subplots import make_subplots -from pyconfig import appConfig +""" +This module contains functions for generating different types of figures + related to rainfall data analysis. +""" + +from collections import defaultdict, OrderedDict +from itertools import cycle, islice +import re import numpy as np +import pandas as pd import plotly.express as px import plotly.graph_objects as go +from dash import dcc +from plotly.subplots import make_subplots +from pyconfig import appConfig import pytemplate -import pandas as pd -from collections import defaultdict, OrderedDict -from itertools import cycle, islice THRESHOLD_SUMMARY = (367 * 8) // 2 THRESHOLD_GRAPH_RAINFALL = 365 * 8 THRESHOLD_XAXES = 12 * 2 * 5 THRESHOLD_STATIONS = 8 +LABEL_GRAPH_RAINFALL = { + "title": "Rainfall Each Station", + "yaxis": {"title": "Rainfall (mm)"}, + "xaxis": {"title": "Date"}, + "legend": {"title": "Stations"}, +} -def _generate_dict_watermark(n: int = 1, source=appConfig.TEMPLATE.WATERMARK_SOURCE): - n = "" if n == 1 else n - return dict( - source=source, - xref=f"x{n} domain", - yref=f"y{n} domain", - x=0.5, - y=0.5, - sizex=0.5, - sizey=0.5, - xanchor="center", - yanchor="middle", - name="watermark-hidrokit", - layer="below", - opacity=0.1, - ) +current_font_color = pytemplate.FONT_COLOR_RGB_ALPHA -LABEL_GRAPH_RAINFALL = dict( - title="Rainfall Each Station", - yaxis={"title": "Rainfall (mm)"}, - xaxis={"title": "Date"}, - legend={"title": "Stations"}, -) +def generate_watermark( + subplot_number: int = 1, watermark_source=appConfig.TEMPLATE.WATERMARK_SOURCE +): + """Generate a watermark for a subplot. + + Args: + subplot_number (int, optional): The number of the subplot. + Defaults to 1. + watermark_source (str, optional): The source of the watermark. + Defaults to appConfig.TEMPLATE.WATERMARK_SOURCE. + + Returns: + dict: A dictionary containing the watermark properties. + """ + subplot_number = "" if subplot_number == 1 else subplot_number + return { + "source": watermark_source, + "xref": f"x{subplot_number} domain", + "yref": f"y{subplot_number} domain", + "x": 0.5, + "y": 0.5, + "sizex": 0.5, + "sizey": 0.5, + "xanchor": "center", + "yanchor": "middle", + "name": "watermark-hidrokit", + "layer": "below", + "opacity": 0.1, + } -def figure_scatter(dataframe): +def generate_scatter_figure(dataframe): + """ + Generate a scatter plot figure based on the provided dataframe. + + Parameters: + dataframe (pandas.DataFrame): The dataframe containing the data to be plotted. + + Returns: + plotly.graph_objs._figure.Figure: The scatter plot figure. + """ data = [ go.Scatter(x=dataframe.index, y=dataframe[col], mode="lines", name=col) @@ -54,7 +83,21 @@ def figure_scatter(dataframe): return fig -def figure_bar(dataframe, barmode="stack"): +def generate_bar_figure(dataframe, barmode="stack"): + """ + Generate a bar figure based on the given dataframe. + + Parameters: + - dataframe: pandas DataFrame + The input dataframe containing the data for the bar figure. + - barmode: str, optional + The mode for displaying the bars. Default is "stack". + + Returns: + - fig: plotly Figure + The generated bar figure. + + """ if barmode == "stack": col_df = dataframe.columns[::-1] @@ -80,7 +123,17 @@ def figure_bar(dataframe, barmode="stack"): return fig -def figure_empty(text: str = "", size: int = 40): +def generate_empty_figure(text: str = "", size: int = 40): + """ + Generates an empty figure with optional text annotation. + + Args: + text (str, optional): Text to be displayed as an annotation. Defaults to "". + size (int, optional): Font size of the annotation. Defaults to 40. + + Returns: + go.Figure: An empty figure with the specified text annotation. + """ data = [{"x": [], "y": []}] layout = go.Layout( title={"text": "", "x": 0.5}, @@ -96,19 +149,19 @@ def figure_empty(text: str = "", size: int = 40): "showticklabels": False, "zeroline": False, }, - margin=dict(t=55, l=55, r=55, b=55), + margin={"t": 55, "l": 55, "r": 55, "b": 55}, annotations=[ - dict( - name="text", - text=f"{text}", - opacity=0.3, - font_size=size, - xref="x domain", - yref="y domain", - x=0.5, - y=0.05, - showarrow=False, - ) + { + "name": "text", + "text": f"{text}", + "opacity": 0.3, + "font_size": size, + "xref": "x domain", + "yref": "y domain", + "x": 0.5, + "y": 0.05, + "showarrow": False, + } ], height=450, ) @@ -116,7 +169,7 @@ def figure_empty(text: str = "", size: int = 40): return go.Figure(data, layout) -def figure_summary_maxsum( +def generate_summary_maximum_sum( summary, ufunc_cols: list[str] = None, rows: int = 2, @@ -125,6 +178,25 @@ def figure_summary_maxsum( title: str = "Summary Rainfall", period: str = None, ) -> dcc.Graph: + """ + Generates a summary graph of maximum and sum values for rainfall data. + + Args: + summary: The summary data containing rainfall information. + ufunc_cols (optional): A list of column names to include in the graph. + Defaults to ["max", "sum"]. + rows (optional): The number of rows in the subplot grid. Defaults to 2. + cols (optional): The number of columns in the subplot grid. Defaults to 1. + subplot_titles (optional): A list of titles for each subplot. + Defaults to the values in ufunc_cols. + title (optional): The title of the graph. Defaults to "Summary Rainfall". + period (optional): The period of the data. Can be "monthly", "yearly", or None. + Defaults to None. + + Returns: + A dcc.Graph object representing the summary graph. + + """ ufunc_cols = ["max", "sum"] if ufunc_cols is None else ufunc_cols subplot_titles = ufunc_cols if subplot_titles is None else subplot_titles @@ -133,7 +205,8 @@ def figure_summary_maxsum( (summary.size > THRESHOLD_SUMMARY) or (summary.index.size > THRESHOLD_XAXES) ) and (period.lower() != "yearly"): return dcc.Graph( - figure=figure_empty("dataset above threshold"), config={"staticPlot": True} + figure=generate_empty_figure("dataset above threshold"), + config={"staticPlot": True}, ) fig = make_subplots( @@ -144,11 +217,12 @@ def figure_summary_maxsum( subplot_titles=subplot_titles, ) - fig.layout.images = [_generate_dict_watermark(n) for n in range(2, rows + 1)] + fig.layout.images = [generate_watermark(n) for n in range(2, rows + 1)] data_dict = defaultdict(list) stations = [station_name for station_name, _ in summary.columns.to_list()] stations = list(OrderedDict.fromkeys(stations)) + last_series = None for station in stations: for ufcol, series in summary[station].items(): if ufcol in ufunc_cols: @@ -160,6 +234,7 @@ def figure_summary_maxsum( legendgrouptitle_text=station, ) data_dict[ufcol].append(_bar) + last_series = series for counter, (ufcol, data) in enumerate(data_dict.items(), 1): fig.add_traces(data, rows=counter, cols=cols) @@ -175,30 +250,30 @@ def figure_summary_maxsum( legend={"title": "Stations"}, ) - ticktext = series.index.strftime("%d %b %Y") + ticktext = last_series.index.strftime("%d %b %Y") if period.lower() in ["monthly", "yearly"]: if period.lower() == "monthly": - ticktext = series.index.strftime("%B %Y") + ticktext = last_series.index.strftime("%B %Y") if period.lower() == "yearly": - ticktext = series.index.strftime("%Y") + ticktext = last_series.index.strftime("%Y") - if series.index.size <= THRESHOLD_XAXES: + if last_series.index.size <= THRESHOLD_XAXES: xticktext = ticktext - xtickvals = np.arange(series.index.size) + xtickvals = np.arange(last_series.index.size) else: xticktext = ticktext[::2] - xtickvals = np.arange(series.index.size)[::2] + xtickvals = np.arange(last_series.index.size)[::2] - UPDATE_XAXES = { + update_x_axes = { "ticktext": xticktext, "tickvals": xtickvals, - "gridcolor": pytemplate._FONT_COLOR_RGB_ALPHA.replace("0.4", "0.2"), + "gridcolor": current_font_color.replace("0.4", "0.2"), "gridwidth": 2, } - UPDATE_YAXES = { - "gridcolor": pytemplate._FONT_COLOR_RGB_ALPHA.replace("0.4", "0.2"), + update_y_axes = { + "gridcolor": current_font_color.replace("0.4", "0.2"), "gridwidth": 2, "fixedrange": True, "title": "Rainfall (mm)", @@ -209,7 +284,7 @@ def update_axis(fig, update, n, axis: str = "x"): fig.update(layout={f"{axis}axis{n}": update}) for n_row in range(1, rows + 1): - for axis, update in zip(["x", "y"], [UPDATE_XAXES, UPDATE_YAXES]): + for axis, update in zip(["x", "y"], [update_x_axes, update_y_axes]): update_axis(fig, update, n_row, axis) # ref: https://stackoverflow.com/questions/39863250 @@ -229,7 +304,7 @@ def update_axis(fig, update, n, axis: str = "x"): return dcc.Graph(figure=fig) -def figure_summary_raindry( +def generate_summary_rain_dry( summary: pd.DataFrame, ufunc_cols: list[str] = None, rows: int = None, @@ -238,7 +313,23 @@ def figure_summary_raindry( title: str = "Summary Rainfall", period: str = None, ) -> dcc.Graph: - + """ + Generates a summary graph of rainfall and dry days. + + Args: + summary (pd.DataFrame): The summary data containing rainfall and dry day information. + ufunc_cols (list[str], optional): The columns to include in the graph. + Defaults to ["n_rain", "n_dry"]. + rows (int, optional): The number of rows in the graph. Defaults to None. + cols (int, optional): The number of columns in the graph. Defaults to 1. + subplot_titles (list[str], optional): The titles for each subplot. Defaults to None. + title (str, optional): The title of the graph. Defaults to "Summary Rainfall". + period (str, optional): The period of the data (e.g., "monthly", "yearly"). + Defaults to None. + + Returns: + dcc.Graph: The generated graph. + """ rows = summary.columns.levels[0].size if rows is None else rows ufunc_cols = ["n_rain", "n_dry"] if ufunc_cols is None else ufunc_cols @@ -250,7 +341,8 @@ def figure_summary_raindry( (summary.size > THRESHOLD_SUMMARY) or (summary.index.size > THRESHOLD_XAXES) ) and (period.lower() != "yearly"): return dcc.Graph( - figure=figure_empty("dataset above threshold"), config={"staticPlot": True} + figure=generate_empty_figure("dataset above threshold"), + config={"staticPlot": True}, ) vertical_spacing = 0.2 / rows @@ -263,7 +355,7 @@ def figure_summary_raindry( subplot_titles=subplot_titles, ) - fig.layout.images = [_generate_dict_watermark(n) for n in range(2, rows + 1)] + fig.layout.images = [generate_watermark(n) for n in range(2, rows + 1)] for station in summary.columns.levels[0]: summary[(station, "n_left")] = ( @@ -275,6 +367,7 @@ def figure_summary_raindry( data_dict = defaultdict(list) stations = [station_name for station_name, _ in summary.columns.to_list()] stations = list(OrderedDict.fromkeys(stations)) + last_series = None for station in stations: for ufcol, series in summary[station].items(): if ufcol in ufunc_cols + ["n_left"]: @@ -304,6 +397,7 @@ def figure_summary_raindry( legendrank=500, ) data_dict[station].append(_bar) + last_series = series for counter, (ufcol, data) in enumerate(data_dict.items(), 1): fig.add_traces(data, rows=counter, cols=cols) @@ -318,32 +412,32 @@ def figure_summary_raindry( legend={"title": "Stations"}, ) - ticktext = series.index.strftime("%d %b %Y") + ticktext = last_series.index.strftime("%d %b %Y") if period.lower() in ["monthly", "yearly"]: if period.lower() == "monthly": - ticktext = series.index.strftime("%B %Y") + ticktext = last_series.index.strftime("%B %Y") if period.lower() == "yearly": - ticktext = series.index.strftime("%Y") + ticktext = last_series.index.strftime("%Y") - if series.index.size <= THRESHOLD_XAXES: + if last_series.index.size <= THRESHOLD_XAXES: xticktext = ticktext - xtickvals = np.arange(series.index.size) + xtickvals = np.arange(last_series.index.size) else: xticktext = ticktext[::2] - xtickvals = np.arange(series.index.size)[::2] + xtickvals = np.arange(last_series.index.size)[::2] - UPDATE_XAXES = { + update_x_axes = { "ticktext": xticktext, "tickvals": xtickvals, - "gridcolor": pytemplate._FONT_COLOR_RGB_ALPHA.replace("0.4", "0.1"), + "gridcolor": current_font_color.replace("0.4", "0.1"), "gridwidth": 2, # "nticks": 2, "ticklabelstep": 2, } - UPDATE_YAXES = { - "gridcolor": pytemplate._FONT_COLOR_RGB_ALPHA.replace("0.4", "0.1"), + update_y_axes = { + "gridcolor": current_font_color.replace("0.4", "0.1"), "gridwidth": 2, "fixedrange": True, "title": "Days", @@ -357,7 +451,7 @@ def update_axis(fig, update, n, axis: str = "x"): fig.update(layout={f"xaxis{rows}": {"title": "Date"}}) for n_row in range(1, rows + 1): - for axis, update in zip(["x", "y"], [UPDATE_XAXES, UPDATE_YAXES]): + for axis, update in zip(["x", "y"], [update_x_axes, update_y_axes]): update_axis(fig, update, n_row, axis) color_list = list(pytemplate.hktemplate.layout.colorway[:2]) + ["DarkGray"] @@ -368,7 +462,7 @@ def update_axis(fig, update, n, axis: str = "x"): return dcc.Graph(figure=fig) -def figure_summary_maxdate( +def generate_summary_maximum_date( summary_all: pd.DataFrame, ufunc_col: list[str] = None, rows: int = 3, @@ -378,7 +472,25 @@ def figure_summary_maxdate( periods: list[str] = None, bubble_sizes: list[int] = None, ): - + """ + Generates a summary graph of maximum rainfall events. + + Args: + summary_all (pd.DataFrame): The summary data containing rainfall information. + ufunc_col (list[str], optional): The columns to use for calculations. + Defaults to None. + rows (int, optional): The number of rows in the subplot grid. Defaults to 3. + cols (int, optional): The number of columns in the subplot grid. Defaults to 1. + subplot_titles (list[str], optional): The titles for each subplot. Defaults to None. + title (str, optional): The title of the graph. Defaults to "Maximum Rainfall Events". + periods (list[str], optional): The periods to consider for the analysis. + Defaults to None. + bubble_sizes (list[int], optional): The sizes of the bubbles in the graph. + Defaults to None. + + Returns: + dcc.Graph: The generated graph. + """ ufunc_col = ["max_date"] if ufunc_col is None else ufunc_col subplot_titles = ( ["Biweekly", "Monthly", "Yearly"] if subplot_titles is None else subplot_titles @@ -393,7 +505,7 @@ def figure_summary_maxdate( subplot_titles=subplot_titles, ) - fig.layout.images = [_generate_dict_watermark(n) for n in range(2, rows + 1)] + fig.layout.images = [generate_watermark(n) for n in range(2, rows + 1)] # Create new DF @@ -461,23 +573,23 @@ def update_axis(fig, update, n, axis: str = "x"): fig.update(layout={f"xaxis{rows}": {"title": "Date"}}) # GENERAL UPDATE - UPDATE_XAXES = { - "gridcolor": pytemplate._FONT_COLOR_RGB_ALPHA.replace("0.4", "0.1"), + update_x_axes = { + "gridcolor": current_font_color.replace("0.4", "0.1"), "gridwidth": 2, "showspikes": True, "spikesnap": "cursor", "spikemode": "across", "spikethickness": 1, } - UPDATE_YAXES = { - "gridcolor": pytemplate._FONT_COLOR_RGB_ALPHA.replace("0.4", "0.1"), + update_y_axes = { + "gridcolor": current_font_color.replace("0.4", "0.1"), "gridwidth": 2, "fixedrange": True, "title": "Station", } for n_row in range(1, rows + 1): - for axis, update in zip(["x", "y"], [UPDATE_XAXES, UPDATE_YAXES]): + for axis, update in zip(["x", "y"], [update_x_axes, update_y_axes]): update_axis(fig, update, n_row, axis) n_data = len(fig.data) @@ -495,17 +607,30 @@ def update_axis(fig, update, n, axis: str = "x"): return dcc.Graph(figure=fig) -def figure_cumsum_single(cumsum: pd.DataFrame, col: str = None) -> go.Figure: - import re +def generate_cumulative_sum( + cumulative_sum_df: pd.DataFrame, data_column: str = None +) -> go.Figure: + """ + Generates a cumulative sum plot using the provided DataFrame. + + Args: + cumulative_sum_df (pd.DataFrame): The DataFrame containing the cumulative sum data. + data_column (str, optional): The column name to use for the y-axis data. + If not provided, the first column of the DataFrame will be used. + + Returns: + go.Figure: The generated cumulative sum plot as a Plotly Figure. - col = cumsum.columns[0] if col is None else col + """ - new_dataframe = cumsum.copy() + data_column = cumulative_sum_df.columns[0] if data_column is None else data_column + + new_dataframe = cumulative_sum_df.copy() new_dataframe["number"] = np.arange(1, len(new_dataframe) + 1) fig = px.scatter( x=new_dataframe.number, - y=new_dataframe[col], + y=new_dataframe[data_column], trendline="ols", trendline_color_override=pytemplate.hktemplate.layout.colorway[1], ) @@ -518,9 +643,9 @@ def figure_cumsum_single(cumsum: pd.DataFrame, col: str = None) -> go.Figure: _scatter.line.width = 1 _scatter.marker.size = 12 _scatter.marker.symbol = "circle" - _scatter.name = col + _scatter.name = data_column _scatter.hovertemplate = ( - f"{col}
%{{y}} mm
%{{x}}" + f"{data_column}
%{{y}} mm
%{{x}}" ) # MODIFIED TRENDLINE @@ -556,15 +681,26 @@ def figure_cumsum_single(cumsum: pd.DataFrame, col: str = None) -> go.Figure: return dcc.Graph(figure=fig) -def figure_consistency(cumsum: pd.DataFrame, col: str) -> go.Figure: - import re +def generate_scatter_with_trendline( + cumulative_sum_df: pd.DataFrame, data_column: str +) -> go.Figure: + """ + Generate a scatter plot with a trendline. + + Args: + cumulative_sum_df (pd.DataFrame): The cumulative sum dataframe. + data_column (str): The column name for the data. + + Returns: + go.Figure: The scatter plot figure with a trendline. + """ - cumsum = cumsum.copy() + cumulative_sum_df = cumulative_sum_df.copy() # Create Mean Cumulative Other Stations - cumsum_x = cumsum[col] - other_stations = cumsum.columns.drop(col) - cumsum_y = cumsum[other_stations].mean(axis=1) + cumsum_x = cumulative_sum_df[data_column] + other_stations = cumulative_sum_df.columns.drop(data_column) + cumsum_y = cumulative_sum_df[other_stations].mean(axis=1) fig = px.scatter( x=cumsum_x, @@ -581,9 +717,9 @@ def figure_consistency(cumsum: pd.DataFrame, col: str) -> go.Figure: _scatter.line.width = 1 _scatter.marker.size = 12 _scatter.marker.symbol = "circle" - _scatter.name = col + _scatter.name = data_column _scatter.hovertemplate = ( - f"{col}
y: %{{y}} mm
x: %{{x}} mm
" + f"{data_column}
y: %{{y}} mm
x: %{{x}} mm
" ) # MODIFIED TRENDLINE @@ -608,7 +744,7 @@ def figure_consistency(cumsum: pd.DataFrame, col: str) -> go.Figure: _trendline.name = "trendline" fig.update_layout( - xaxis_title=f"Cumulative Annual {col} (mm)", + xaxis_title=f"Cumulative Annual {data_column} (mm)", yaxis_title="Cumulative Average Annual References (mm)", margin=dict(l=0, t=35, b=0, r=0), yaxis_tickformat=".0f", diff --git a/pyfunc.py b/pyfunc.py index f22ac49..b5796fd 100644 --- a/pyfunc.py +++ b/pyfunc.py @@ -1,12 +1,37 @@ +""" +This module contains functions for parsing and processing uploaded data, + generating summary statistics for rainfall data, + transforming table data into a pandas DataFrame, + and calculating the cumulative sum of a DataFrame. +""" + import base64 import io import pandas as pd from dash import html import numpy as np -from hidrokit.contrib.taruma import hk98 +from hidrokit.contrib.taruma import statistic_summary def parse_upload_data(content, filename, filedate): + """ + Parse and process uploaded data. + + Args: + content (str): The content of the uploaded file. + filename (str): The name of the uploaded file. + filedate (str): The date of the uploaded file. + + Returns: + tuple: A tuple containing the processed data and an HTML element. + The processed data is a pandas DataFrame if the file is in CSV format. + If the file is in XLSX or XLS format, an HTML element with a warning message + is returned. + If the file is in any other format, an HTML element with an error message + is returned. + """ + + _ = filedate # unused variable _, content_string = content.split(",") decoded = base64.b64decode(content_string) @@ -34,18 +59,37 @@ def parse_upload_data(content, filename, filedate): ), None, ) - except Exception as e: + except UnicodeDecodeError as e: + print(e) + return html.Div([f"File is not valid UTF-8. {e}"]), None + except pd.errors.ParserError as e: + print(e) + return html.Div([f"CSV file is not well-formed. {e}"]), None + except ValueError as e: print(e) - return html.Div([f"There was an error processing this file. {e}"]), None + return html.Div([f"Content string is not valid base64. {e}"]), None return html.Div(["File Diterima"]), dataframe def generate_summary_single(dataframe, n_days="1MS"): + """ + Generate a summary of rainfall data for a single location. + + Args: + dataframe (pandas.DataFrame): The input dataframe containing rainfall data. + n_days (str, optional): The number of days to consider for the summary. + Defaults to "1MS". + + Returns: + pandas.DataFrame: The summary dataframe containing various statistics of + the rainfall data. + """ + def days(vector): return len(vector) - def sum(vector): + def vector_sum(vector): return vector.sum().round(3) def n_rain(vector): @@ -57,16 +101,15 @@ def n_dry(vector): def max_date(vector): if vector.any(): return vector.idxmax().date() - else: - return pd.NaT + return pd.NaT - def max(vector): + def vector_max(vector): return vector.max() - ufunc = [days, max, sum, n_rain, n_dry, max_date] + ufunc = [days, vector_max, vector_sum, n_rain, n_dry, max_date] ufunc_col = ["days", "max", "sum", "n_rain", "n_dry", "max_date"] - summary = hk98.summary_all( + summary = statistic_summary.summary_all( dataframe, ufunc=ufunc, ufunc_col=ufunc_col, n_days=n_days ) @@ -74,6 +117,19 @@ def max(vector): def generate_summary_all(dataframe, n_days: list = None): + """ + Generate summary statistics for multiple time periods. + + Args: + dataframe (pandas.DataFrame): The input dataframe containing the data. + n_days (list, optional): A list of time periods to calculate + the summary statistics for. + If not provided, the default time periods ["16D", "1MS", "1YS"] will be used. + + Returns: + list: A list of summary statistics for each time period. + + """ n_days = ["16D", "1MS", "1YS"] if n_days is None else n_days summary_all = [] @@ -90,6 +146,22 @@ def transform_to_dataframe( apply_numeric: bool = True, parse_dates: list = None, ): + """ + Transform table data into a pandas DataFrame. + + Args: + table_data (list): The data to be transformed into a DataFrame. + table_columns (list): The column names of the table data. + multiindex (bool, optional): Whether to create a multi-index DataFrame. + Defaults to False. + apply_numeric (bool, optional): Whether to apply numeric conversion to the DataFrame. + Defaults to True. + parse_dates (list, optional): The column names to parse as dates. + Defaults to None. + + Returns: + pandas.DataFrame: The transformed DataFrame. + """ if multiindex is True: dataframe = pd.DataFrame(table_data) @@ -133,8 +205,16 @@ def transform_to_dataframe( return dataframe -def calc_cumsum(dataframe): +def calculate_cumulative_sum(dataframe): + """ + Calculate the cumulative sum of a DataFrame by resampling it on a yearly basis. + + Parameters: + dataframe (pandas.DataFrame): The input DataFrame containing the data. + Returns: + pandas.DataFrame: The DataFrame with the cumulative sum rounded to the nearest integer. + """ consistency = dataframe.resample("YS").sum().cumsum() return consistency.round() diff --git a/pylayout.py b/pylayout.py index a067e8a..957a841 100644 --- a/pylayout.py +++ b/pylayout.py @@ -1,7 +1,11 @@ +""" +This module defines the layout components for a Dash application used for rainfall analysis. +""" + from dash import html, dcc import dash_bootstrap_components as dbc -from pyconfig import appConfig import plotly.io as pio +from pyconfig import appConfig from pytemplate import hktemplate import pyfigure import pylayoutfunc @@ -15,7 +19,11 @@ className="float fw-bold text-center mt-3 fs-1 fw-bold", ), html.Span( - [appConfig.GITHUB_REPO, "@", appConfig.VERSION], + html.A( + [appConfig.GITHUB_REPO, "@", appConfig.VERSION], + href="https://github.com/taruma/rainfall", + target="_blank", + ), className="text-muted", ), ], @@ -32,60 +40,19 @@ className="text-center fs-5", ) -HTML_SPONSORED = html.Div( - [ - "sponsored by ", - html.A("FIAKO Engineering", href="https://fiako.engineering"), - ], - className="text-center fs-5 mb-3", -) - -ALERT_CONTRIBUTION = dbc.Alert( - [ - "Tertarik untuk berkontribusi? Ingin terlibat proyek hidrokit seperti ini? hubungi saya di ", - html.A("hi@taruma.info", href="mailto:hi@taruma.info", className="text-bold"), - ". Langsung buat isu di ", - html.A("Github", href=appConfig.GITHUB_LINK), - " jika memiliki pertanyaan/komentar/kritik/saran atau menemui kesalahan di proyek ini.", - ] -) - -HTML_ALERT_CONTRIBUTION = pylayoutfunc.create_HTML_alert(ALERT_CONTRIBUTION) - ALERT_README = dbc.Alert( [ - "Informasi aplikasi ini dapat dilihat di ", - html.A( - "GitHub README", - href="https://github.com/fiakoenjiniring/rainfall#readme", - ), + "Rainfall Data Explorer (hidrokit-rainfall) is a web application for " + "analyzing daily rainfall data, providing maximum rainfall, total rainfall, " + "rainy days, dry days, and maximum rainfall events visualization, " + "with added features like annual cumulative graph and consistency (mass curve)", ".", ], color="warning", className="m-4", ) -HTML_ALERT_README = pylayoutfunc.create_HTML_alert(ALERT_README, className=None) - -ALERT_SPONSOR = dbc.Alert( - [ - "Terima kasih untuk ", - html.A( - "FIAKO Engineering", - href="https://fiako.engineering", - ), - " yang telah mensponsori versi v1.1.0. Untuk catatan pembaruan bisa dilihat melalui ", - html.A( - "halaman rilis di github", - href="https://github.com/fiakoenjiniring/rainfall/releases/tag/v1.1.0", - ), - ".", - ], - color="info", -) - -HTML_ALERT_SPONSOR = pylayoutfunc.create_HTML_alert(ALERT_SPONSOR, className=None) - +HTML_ALERT_README = pylayoutfunc.create_html_alert(ALERT_README, class_name=None) DCC_UPLOAD = html.Div( dcc.Upload( @@ -93,11 +60,12 @@ children=html.Div( [ dbc.Button( - "Drag and Drop or Select Files", + "Upload File (.csv)", color="primary", outline=False, class_name="fs-4 text-center", id="button-upload", + size="lg", ) ] ), @@ -115,24 +83,78 @@ [DCC_UPLOAD], width="auto", ), + ], + justify="center", + ), + ], + fluid=True, + ), +) + +HTML_ROW_BUTTON_EXAMPLE = html.Div( + dbc.Container( + [ + dbc.Row( + [ + dbc.Col( + [ + dbc.Button( + "Example 1 (5 Stations, 7 Years)", + color="success", + id="button-example-1", + class_name="text-center", + size="sm", + ), + ], + class_name="text-center", + width="auto", + ), + dbc.Col( + [ + dbc.Button( + "Example 2 (4 Stations, 2 Years)", + color="success", + id="button-example-2", + class_name="text-center", + size="sm", + ), + ], + class_name="text-center", + width="auto", + ), + dbc.Col( + [ + dbc.Button( + "Example 3 (1 Station, 9 Years)", + color="success", + id="button-example-3", + class_name="text-center", + size="sm", + ), + ], + class_name="text-center", + width="auto", + ), dbc.Col( [ dbc.Button( - "Use Example Data", - color="info", - id="button-skip", - class_name="fs-4 text-center", + "Example 4 (7 Station, 1 Years)", + color="success", + id="button-example-4", + class_name="text-center", + size="sm", ), ], - class_name="fs-4 text-center", + class_name="text-center", width="auto", ), ], justify="center", + class_name="my-3", ), ], fluid=True, - ), + ) ) HTML_ROW_TABLE = html.Div( @@ -142,7 +164,7 @@ dbc.CardBody( id="row-table-uploaded", children=dcc.Graph( - figure=pyfigure.figure_empty(), + figure=pyfigure.generate_empty_figure(), config={"staticPlot": True}, ), ), @@ -228,7 +250,7 @@ dcc.Loading( dcc.Graph( id="graph-rainfall", - figure=pyfigure.figure_empty(), + figure=pyfigure.generate_empty_figure(), config={"staticPlot": True}, ) ) @@ -271,7 +293,7 @@ dbc.Container( dcc.Loading( children=dcc.Graph( - figure=pyfigure.figure_empty(), + figure=pyfigure.generate_empty_figure(), config={"staticPlot": True}, ), id="tab-analysis", @@ -327,7 +349,7 @@ dbc.Container( dcc.Loading( children=dcc.Graph( - figure=pyfigure.figure_empty(), + figure=pyfigure.generate_empty_figure(), config={"staticPlot": True}, ), id="tab-graph-analysis", @@ -345,7 +367,7 @@ dbc.Col( dcc.Loading( children=dcc.Graph( - figure=pyfigure.figure_empty(), + figure=pyfigure.generate_empty_figure(), config={"staticPlot": True}, ), id="tab-graph-cumsum", @@ -367,7 +389,7 @@ dbc.Col( dcc.Loading( children=dcc.Graph( - figure=pyfigure.figure_empty(), + figure=pyfigure.generate_empty_figure(), config={"staticPlot": True}, ), id="tab-graph-consistency", @@ -381,53 +403,15 @@ className="my-3", ) -_HTML_TROUBLESHOOT = html.Div( - dbc.Container( - [ - dbc.Row([html.Div("HEELLOOOO")]), - dbc.Button("Hello", id="button-troubleshoot"), - html.Div(id="row-troubleshoot"), - ], - fluid=True, - ) -) - -HTML_OTHER_PROJECTS = html.Div( - [ - html.Span("other dashboard:"), - html.A( - [ - html.Del("BMKG", style={"text-decoration-style": "double"}), - " 🛖 Explorer", - ], - href="https://github.com/taruma/dash-data-explorer", - style={"text-decoration": "none"}, - ), - ], - className="d-flex gap-2 justify-content-center my-2", -) - -HTML_MADEBY = html.Div( - dcc.Markdown( - "Made with [Dash+Plotly](https://plotly.com).", - className="fs-4 text-center mt-2", - ), -) - HTML_FOOTER = html.Div( html.Footer( [ html.Span("\u00A9"), - " 2022 ", - # html.A( - # "Taruma Sakti Megariansyah", - # href="https://github.com/taruma", - # ), - # ", ", + " 2022-2024 ", html.A( - "PT. FIAKO ENJINIRING INDONESIA", - href="https://fiako.engineering", - target="_blank" + "Taruma Sakti Megariansyah", + href="https://dev.taruma.info", + target="_blank", ), ". MIT License. Visit on ", html.A( diff --git a/pylayoutfunc.py b/pylayoutfunc.py index 78a220e..96f79d8 100644 --- a/pylayoutfunc.py +++ b/pylayoutfunc.py @@ -1,9 +1,13 @@ -from __future__ import annotations +""" +This module contains functions for creating table and graph layouts + using Dash and Bootstrap components. +""" + +from collections.abc import Iterable +from datetime import datetime from dash import html, dash_table, dcc import dash_bootstrap_components as dbc from pytemplate import hktemplate -from datetime import datetime -from pyconfig import appConfig def create_table_layout( @@ -15,7 +19,23 @@ def create_table_layout( deletable=True, renamable=False, ): - from collections.abc import Iterable + """ + Create a table layout using the given dataframe. + + Args: + dataframe (pandas.DataFrame): The input dataframe. + idtable (str): The ID of the DataTable component. + filename (str, optional): The name of the file. Defaults to None. + filedate (int, optional): The timestamp of the file. Defaults to None. + editable (list or bool, optional): A list of booleans indicating + the editability of each column, + or a single boolean value to be applied to all columns. Defaults to False. + deletable (bool, optional): Whether the columns are deletable. Defaults to True. + renamable (bool, optional): Whether the columns are renamable. Defaults to False. + + Returns: + tuple: A tuple containing the title element and the DataTable component. + """ new_dataframe = dataframe.rename_axis("DATE").reset_index() new_dataframe.DATE = new_dataframe.DATE.dt.date @@ -52,7 +72,7 @@ def create_table_layout( if (filename is not None) and (filedate is not None) else "" ) - title_table = f"DATA TABLE" + add_title + title_table = "DATA TABLE" + add_title return html.H2(title_table, className="text-center"), table @@ -63,6 +83,23 @@ def create_table_summary( deletable=True, renamable=False, ): + """ + Creates a table summary using the provided summary data. + + Args: + summary (pandas.DataFrame): The summary data to be displayed in the table. + idtable (str): The ID of the DataTable component. + editable (bool, optional): Specifies whether the table cells are editable. + Defaults to False. + deletable (bool, optional): Specifies whether the table columns are deletable. + Defaults to True. + renamable (bool, optional): Specifies whether the table columns are renamable. + Defaults to False. + + Returns: + dash_table.DataTable: The created DataTable component. + """ + new_summary = summary.rename_axis("DATE").reset_index() new_summary.DATE = new_summary.DATE.dt.date @@ -96,6 +133,20 @@ def create_tabcard_table_layout( disabled: list = None, active_tab: str = None, ): + """ + Create a tabbed card layout with tables. + + Args: + tables (list): A list of tables to be displayed in each tab. + tab_names (list, optional): A list of tab names. + Defaults to ["Biweekly", "Monthly", "Yearly"]. + disabled (list, optional): A list of booleans indicating whether each tab is disabled. + Defaults to None. + active_tab (str, optional): The active tab. Defaults to None. + + Returns: + dbc.Tabs: A tabbed card layout with the specified tables and settings. + """ disabled = [False] * len(tables) if disabled is None else disabled tab_names = ["Biweekly", "Monthly", "Yearly"] if tab_names is None else tab_names @@ -121,6 +172,20 @@ def create_tabcard_graph_layout( disabled: list = None, active_tab: str = None, ): + """ + Create a layout with tab cards containing graphs. + + Args: + graphs (list[dcc.Graph]): A list of Dash Core Component Graph objects. + tab_names (list, optional): A list of tab names. + Defaults to ["Biweekly", "Monthly", "Yearly"]. + disabled (list, optional): A list of boolean values indicating whether + each tab is disabled. Defaults to None. + active_tab (str, optional): The ID of the active tab. Defaults to None. + + Returns: + dbc.Tabs: A Dash Bootstrap Components Tabs object containing tab cards with graphs. + """ disabled = [False] * len(graphs) if disabled is None else disabled tab_names = ["Biweekly", "Monthly", "Yearly"] if tab_names is None else tab_names @@ -140,11 +205,23 @@ def create_tabcard_graph_layout( return dbc.Tabs(tab, active_tab=active_tab) -def create_HTML_alert(alert: dbc.Alert, className: str = "my-2"): +def create_html_alert(alert: dbc.Alert, class_name: str = "my-2"): + """ + Creates an HTML alert container with the specified alert component and class name. + + Parameters: + alert (dbc.Alert): The alert component to be displayed. + className (str, optional): The class name to be applied to the alert container. + Defaults to "my-2". + + Returns: + html.Div: The HTML alert container. + + """ return html.Div( dbc.Container( dbc.Row([dbc.Col(alert, width="auto")], justify="center"), fluid=True, ), - className=className, + className=class_name, ) diff --git a/pytemplate.py b/pytemplate.py index 541902b..ca51820 100644 --- a/pytemplate.py +++ b/pytemplate.py @@ -1,8 +1,9 @@ """TEMPLATE PLOTLY BASED ON THEME""" + import plotly.io as pio from dash_bootstrap_templates import load_figure_template -from pyconfig import appConfig from plotly import colors +from pyconfig import appConfig load_figure_template(appConfig.DASH_THEME.THEME.lower()) hktemplate = pio.templates[pio.templates.default] @@ -10,27 +11,27 @@ # VARS _TEMPLATE = appConfig.TEMPLATE _FONT_FAMILY = hktemplate.layout.font.family -_FONT_COLOR_TUPLE = colors.hex_to_rgb(hktemplate.layout.font.color) -_FONT_COLOR_RGB_ALPHA = "rgba({},{},{},0.4)".format(*_FONT_COLOR_TUPLE) +_RED, _GREEN, _BLUE = colors.hex_to_rgb(hktemplate.layout.font.color) +FONT_COLOR_RGB_ALPHA = f"rgba({_RED},{_GREEN},{_BLUE},0.4)" ## LAYOUT # WATERMARK _SOURCE_WATERMARK = _TEMPLATE.WATERMARK_SOURCE hktemplate.layout.images = [ - dict( - source=_SOURCE_WATERMARK, - xref="x domain", - yref="y domain", - x=0.5, - y=0.5, - sizex=0.5, - sizey=0.5, - xanchor="center", - yanchor="middle", - name="watermark-hidrokit", - layer="below", - opacity=0.1, - ), + { + "source": _SOURCE_WATERMARK, + "xref": "x domain", + "yref": "y domain", + "x": 0.5, + "y": 0.5, + "sizex": 0.5, + "sizey": 0.5, + "xanchor": "center", + "yanchor": "middle", + "name": "watermark-hidrokit", + "layer": "below", + "opacity": 0.1, + }, ] ## GENERAL @@ -51,7 +52,7 @@ # hktemplate.layout.legend.title.text = "placeholder" -def apply_legend_inside(): +def _apply_legend_inside(): hktemplate.layout.legend.xanchor = "left" hktemplate.layout.legend.yanchor = "top" hktemplate.layout.legend.x = 0.005 @@ -63,7 +64,7 @@ def apply_legend_inside(): if _TEMPLATE.SHOW_LEGEND_INSIDE: - apply_legend_inside() + _apply_legend_inside() # MODEBAR hktemplate.layout.modebar.activecolor = "blue" @@ -101,53 +102,57 @@ def apply_legend_inside(): hktemplate.layout.xaxis.linewidth = _XAXIS_LINEWIDTH hktemplate.layout.xaxis.linecolor = _XAXIS_GRIDCOLOR hktemplate.layout.xaxis.spikecolor = _XAXIS_GRIDCOLOR -hktemplate.layout.xaxis.gridcolor = _FONT_COLOR_RGB_ALPHA +hktemplate.layout.xaxis.gridcolor = FONT_COLOR_RGB_ALPHA hktemplate.layout.xaxis.gridwidth = _XAXIS_LINEWIDTH # hktemplate.layout.xaxis.title.text = "PLACEHOLDER XAXIS" hktemplate.layout.xaxis.title.font.size = _XAXIS_TITLE_FONT_SIZE hktemplate.layout.xaxis.title.standoff = _XAXIS_TITLE_STANDOFF + # RANGESELECTOR XAXIS -def apply_rangeselector(): +def _apply_rangeselector(): hktemplate.layout.xaxis.rangeselector.buttons = [ - dict( - count=1, - label="1m", - step="month", - stepmode="backward", - visible=True, - name="button1", - ), - dict( - count=6, - label="6m", - step="month", - stepmode="backward", - visible=True, - name="button2", - ), - dict( - count=1, - label="YTD", - step="year", - stepmode="todate", - visible=True, - name="button3", - ), - dict( - count=1, - label="1y", - step="year", - stepmode="backward", - visible=True, - name="button4", - ), - dict(step="all", name="button5"), + { + "count": 1, + "label": "1m", + "step": "month", + "stepmode": "backward", + "visible": True, + "name": "button1", + }, + { + "count": 6, + "label": "6m", + "step": "month", + "stepmode": "backward", + "visible": True, + "name": "button2", + }, + { + "count": 1, + "label": "YTD", + "step": "year", + "stepmode": "todate", + "visible": True, + "name": "button3", + }, + { + "count": 1, + "label": "1y", + "step": "year", + "stepmode": "backward", + "visible": True, + "name": "button4", + }, + { + "step": "all", + "name": "button5", + }, ] if _TEMPLATE.SHOW_RANGESELECTOR: - apply_rangeselector() + _apply_rangeselector() # YAXIS _YAXIS_GRIDCOLOR = "black" # hktemplate.layout.yaxis.gridcolor @@ -160,7 +165,7 @@ def apply_rangeselector(): hktemplate.layout.yaxis.linecolor = _YAXIS_GRIDCOLOR hktemplate.layout.yaxis.spikecolor = _YAXIS_GRIDCOLOR hktemplate.layout.yaxis.rangemode = "tozero" -hktemplate.layout.yaxis.gridcolor = _FONT_COLOR_RGB_ALPHA +hktemplate.layout.yaxis.gridcolor = FONT_COLOR_RGB_ALPHA hktemplate.layout.yaxis.gridwidth = _YAXIS_LINEWIDTH # hktemplate.layout.yaxis.title.text = "PLACEHOLDER XAXIS" hktemplate.layout.yaxis.title.font.size = _YAXIS_TITLE_FONT_SIZE