diff --git a/requirements.txt b/requirements.txt index f2f5b413..3aeef1f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,6 +20,7 @@ PyYAML==6.0.1 uritemplate==4.1.1 crispy-bootstrap4==2023.1 django-plotly-dash==2.2.2 +dash-ag-grid==2.4.0 ## Legacy dependency versions # asgiref==3.3.4 diff --git a/static/styles/dashstyle.css b/static/styles/dashstyle.css new file mode 100644 index 00000000..704fab2a --- /dev/null +++ b/static/styles/dashstyle.css @@ -0,0 +1,15 @@ +body { + font-family: Open Sans, Raleway, Dosis, Ubuntu, sans-serif; +} + +.DateInput_input, .DateInput_input_1 { + font-size: inherit; + Height: 30px; +} + +.Select-input{ + height: 30px; +} +.Select-control{ + height: 30px; +} diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index cbf01890..721762db 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -1,39 +1,612 @@ from datetime import datetime from decimal import Decimal -import plotly.express as px -from dash import dcc, html +import dash +import plotly.graph_objects as go +from dash import Input, Output, State, dcc, html +from dash_ag_grid import AgGrid from django_plotly_dash import DjangoDash from station.models import Station -from validated.functions import daily_validation +from validated.functions import ( + daily_validation, + detail_list, + get_conditions, + reset_daily_validated, + reset_detail_validated, + save_detail_to_validated, + save_to_validated, +) +from validated.plots import create_validation_plot +from validated.tables import create_columns_daily, create_columns_detail from variable.models import Variable -# Create a Dash app -app = DjangoDash("DailyValidation") - -# Filters (in final app this will get data from forms) -station: Station = Station.objects.order_by("station_code")[7] -variable: Variable = Variable.objects.order_by("variable_code")[0] -start_time: datetime = datetime.strptime("2023-03-01", "%Y-%m-%d") -end_time: datetime = datetime.strptime("2023-03-31", "%Y-%m-%d") -minimum: Decimal = Decimal(-5) -maximum: Decimal = Decimal(28) - -# Load data -data: dict = daily_validation( - station=station, - variable=variable, - start_time=start_time, - end_time=end_time, - minimum=minimum, - maximum=maximum, -) - -# Create plot -x = data["series"]["measurement"]["time"] -y = data["series"]["measurement"]["average"] -plot = px.line(x=x, y=y) - -# Create layout -app.layout = html.Div([dcc.Graph(figure=plot)]) +app = DjangoDash( + "DailyValidation", external_stylesheets=["/static/styles/dashstyle.css"] +) + +# Initial filters +STATION: Station = Station.objects.order_by("station_code")[7] +VARIABLE: Variable = Variable.objects.order_by("variable_code")[0] +START_DATE: datetime = datetime.strptime("2023-03-01", "%Y-%m-%d") +END_DATE: datetime = datetime.strptime("2023-03-31", "%Y-%m-%d") +MINIMUM: Decimal = Decimal(-5) +MAXIMUM: Decimal = Decimal(28) +SELECTED_DAY: datetime = datetime.strptime("2023-03-14", "%Y-%m-%d") +PLOT_TYPE = "average" + +# Daily data +DATA_DAILY = daily_validation( + station=STATION, + variable=VARIABLE, + start_time=START_DATE, + end_time=END_DATE, + minimum=MINIMUM, + maximum=MAXIMUM, +) + +# Detail data +DATA_DETAIL = detail_list( + station=STATION, + variable=VARIABLE, + date_of_interest=SELECTED_DAY, + minimum=MINIMUM, + maximum=MAXIMUM, +) + +# Filters +filters = html.Div( + children=[ + html.Div( + [ + html.Label("Station:", style={"display": "block"}), + dcc.Dropdown( + id="station_drop", + options=[ + {"label": item.station_code, "value": item.station_code} + for item in Station.objects.order_by("station_code") + ], + value=STATION.station_code, + ), + ], + style={"margin-right": "10px", "width": "250px"}, + ), + html.Div( + [ + html.Label("Variable:", style={"display": "block"}), + dcc.Dropdown( + id="variable_drop", + options=[ + {"label": item.name, "value": item.variable_code} + for item in Variable.objects.order_by("variable_code") + ], + value=VARIABLE.variable_code, + ), + ], + style={"margin-right": "10px", "width": "250px"}, + ), + html.Div( + [ + html.Label("Date Range:", style={"display": "block"}), + dcc.DatePickerRange( + id="date_range_picker", + display_format="YYYY-MM-DD", + start_date=START_DATE.strftime("%Y-%m-%d"), + end_date=END_DATE.strftime("%Y-%m-%d"), + ), + ], + style={"margin-right": "10px", "width": "300px"}, + ), + html.Div( + [ + html.Label("Minimum:", style={"display": "block"}), + dcc.Input(id="minimum_input", type="number", value=MINIMUM), + ], + style={"margin-right": "10px", "width": "200px"}, + ), + html.Div( + [ + html.Label("Maximum:", style={"display": "block"}), + dcc.Input(id="maximum_input", type="number", value=MAXIMUM), + ], + style={"margin-right": "10px", "width": "200px"}, + ), + ], + style={ + "display": "flex", + "justify-content": "flex-start", + "font-size": "14px", + }, +) + +# Tables +table_daily = AgGrid( + id="table_daily", + rowData=DATA_DAILY["data"], + columnDefs=create_columns_daily(value_columns=DATA_DAILY["value_columns"]), + columnSize="sizeToFit", + defaultColDef={ + "resizable": True, + "sortable": True, + "checkboxSelection": { + "function": "params.column == params.columnApi.getAllDisplayedColumns()[0]" + }, + "headerCheckboxSelection": { + "function": "params.column == params.columnApi.getAllDisplayedColumns()[0]" + }, + "headerCheckboxSelectionFilteredOnly": True, + }, + dashGridOptions={ + "rowSelection": "multiple", + "suppressRowClickSelection": True, + }, + selectAll=True, + getRowId="params.data.id", +) + +table_detail = AgGrid( + id="table_detail", + rowData=DATA_DETAIL["series"], + columnDefs=create_columns_detail(value_columns=DATA_DETAIL["value_columns"]), + columnSize="sizeToFit", + defaultColDef={ + "resizable": True, + "sortable": True, + "checkboxSelection": { + "function": "params.column == params.columnApi.getAllDisplayedColumns()[0]" + }, + "headerCheckboxSelection": { + "function": "params.column == params.columnApi.getAllDisplayedColumns()[0]" + }, + "headerCheckboxSelectionFilteredOnly": True, + }, + dashGridOptions={ + "rowSelection": "multiple", + "suppressRowClickSelection": True, + }, + selectAll=True, + getRowId="params.data.id", +) + + +# Date picker +detail_date_picker = html.Div( + children=[ + html.Div( + children=["Open detailed view"], + style={ + "display": "inline-block", + "padding-right": "5px", + "font-size": "14px", + }, + ), + dcc.DatePickerSingle( + id="detail-date-picker", + display_format="YYYY-MM-DD", + min_date_allowed=DATA_DAILY["data"][0]["date"], + max_date_allowed=DATA_DAILY["data"][-1]["date"], + ), + ], + style={ + "display": "inline-block", + "width": "50%", + "text-align": "right", + }, +) + +# Table menu +menu = html.Div( + children=[ + html.Div( + children=[ + html.Button("Save to Validated", id="save-button"), + html.Button("Reset Validated", id="reset-button"), + html.Button( + "Add row", + id="add-button", + disabled=True, + style={"margin-left": "10px"}, + ), + ], + style={"display": "inline-block", "width": "50%"}, + ), + detail_date_picker, + ], + style={ + "background-color": "#f0f0f0", + "width": "100%", + }, +) + +# Status message +status_message = html.Div( + id="status-message", + children=[""], + style={ + "font-size": "14px", + "min-height": "20px", + "padding-top": "5px", + "padding-bottom": "10px", + }, +) + + +# Plot +plot = create_validation_plot(data=DATA_DAILY, plot_type=PLOT_TYPE) + +# Plot radio +plot_radio = dcc.RadioItems( + id="plot_radio", + options=[ + {"value": c, "label": c.capitalize()} for c in DATA_DAILY["value_columns"] + ], + value=PLOT_TYPE, + inline=True, + style={"font-size": "14px"}, +) + +# Layout +app.layout = html.Div( + children=[ + filters, + html.Button("Submit", id="submit-button", style={"margin-top": "10px"}), + dcc.Loading( + type="dot", + children=html.Div(id="loading_top"), + ), + html.Hr(), + dcc.Tabs( + id="tabs", + value="tab-daily", + style={"width": "100%"}, + children=[ + dcc.Tab( + label="Daily Report", + id="tab-daily", + value="tab-daily", + children=[ + table_daily, + ], + ), + dcc.Tab( + label="Detail of Selected Day", + id="tab-detail", + value="tab-detail", + disabled=True, + children=[ + table_detail, + ], + ), + ], + ), + menu, + status_message, + dcc.Loading( + type="dot", + children=html.Div(id="loading"), + ), + html.Hr(), + plot_radio, + dcc.Graph(id="plot", figure=plot, style={"width": "100%"}), + ] +) + + +@app.callback( + [ + Output("loading_top", "children"), + Output("loading", "children"), + Output("status-message", "children"), + Output("plot", "figure"), + Output("table_daily", "rowData"), + Output("table_detail", "rowData"), + Output("table_detail", "rowTransaction"), + Output("table_detail", "scrollTo"), + Output("table_daily", "selectedRows"), + Output("table_detail", "selectedRows"), + Output("tab-detail", "disabled"), + Output("tab-detail", "label"), + Output("tabs", "value"), + Output("add-button", "disabled"), + ], + [ + Input("submit-button", "n_clicks"), + Input("save-button", "n_clicks"), + Input("reset-button", "n_clicks"), + Input("add-button", "n_clicks"), + Input("detail-date-picker", "date"), + Input("plot_radio", "value"), + Input("tabs", "value"), + ], + [ + State("station_drop", "value"), + State("variable_drop", "value"), + State("date_range_picker", "start_date"), + State("date_range_picker", "end_date"), + State("minimum_input", "value"), + State("maximum_input", "value"), + State("table_daily", "selectedRows"), + State("table_daily", "rowData"), + State("table_detail", "selectedRows"), + State("table_detail", "rowData"), + ], + prevent_initial_call=True, +) +def callbacks( + in_submit_clicks: int, + in_save_clicks: int, + in_reset_clicks: int, + in_add_clicks: int, + in_detail_date: datetime.date, + in_plot_radio_value: str, + in_tabs_value: str, + in_station: str, + in_variable: str, + in_start_date: str, + in_end_date: str, + in_minimum: float, + in_maximum: float, + in_daily_selected_rows: list[dict], + in_daily_row_data: list[dict], + in_detail_selected_rows: list[dict], + in_detail_row_data: list[dict], +) -> tuple[ + dash.no_update, + dash.no_update, + str, + go.Figure, + list[dict], + list[dict], + dict, + dict, + list[dict], + list[dict], + bool, + str, + str, + bool, +]: + """Callbacks for daily validation app + + Args: + in_submit_clicks (int): Number of times submit-button was clicked + in_save_clicks (int): Number of times save-button was clicked + in_reset_clicks (int): Number of times reset-button was clicked + in_add_clicks (int): Number of times add-button was clicked + in_detail_date (datetime.date): Date for detail view + in_plot_radio_value (str): Value of plot radio button + in_tabs_value (str): Value of tabs + in_station (str): Station from filters + in_variable (str): Variable from filters + in_start_date (str): Start date from filters + in_end_date (str): End date from filters + in_minimum (float): Minimum from filters + in_maximum (float): Maximum from filters + in_daily_selected_rows (list[dict]): Selected rows in table_daily + in_daily_row_data (list[dict]): Full row data for table_daily + in_detail_selected_rows (list[dict]): Selected rows in table_detail + in_detail_row_data (list[dict]): Full row data for table_detail + + Returns: + tuple[ dash.no_update, dash.no_update, str, go.Figure, list[dict], list[dict], dict, dict, list[dict], list[dict], bool, str, str, bool, ]: Outputs + """ + global DATA_DAILY, DATA_DETAIL, STATION, VARIABLE, START_DATE, END_DATE, MINIMUM, MAXIMUM, SELECTED_DAY, PLOT_TYPE + + ctx = dash.callback_context + input_id = ctx.triggered[0]["prop_id"].split(".")[0] + + out_loading_top = dash.no_update + out_loading = dash.no_update + out_status = dash.no_update + out_plot = dash.no_update + out_daily_row_data = dash.no_update + out_detail_row_data = dash.no_update + out_detail_row_transaction = dash.no_update + out_detail_scroll = dash.no_update + out_daily_selected_rows = dash.no_update + out_detail_selected_rows = dash.no_update + out_tab_detail_disabled = dash.no_update + out_tab_detail_label = dash.no_update + out_tabs_value = dash.no_update + out_add_button_disabled = dash.no_update + + daily_data_refresh_required = False + detail_data_refresh_required = False + daily_table_refresh_required = False + detail_table_refresh_required = False + daily_table_reset_selection = False + detail_table_reset_selection = False + plot_refresh_required = False + + # Button: Submit + if input_id == "submit-button": + STATION = Station.objects.get(station_code=in_station) + VARIABLE = Variable.objects.get(variable_code=in_variable) + START_DATE = datetime.strptime(in_start_date, "%Y-%m-%d") + END_DATE = datetime.strptime(in_end_date, "%Y-%m-%d") + MINIMUM = Decimal(in_minimum) if in_minimum is not None else None + MAXIMUM = Decimal(in_maximum) if in_maximum is not None else None + out_status = "" + daily_data_refresh_required = True + detail_data_refresh_required = True + daily_table_refresh_required = True + detail_table_refresh_required = True + daily_table_reset_selection = True + detail_table_reset_selection = True + plot_refresh_required = True + + # Button: Save (daily) + if input_id == "save-button" and in_tabs_value == "tab-daily": + selected_ids = {row["id"] for row in in_daily_selected_rows} + for row in in_daily_row_data: + row["state"] = row["id"] in selected_ids + conditions = get_conditions(in_daily_row_data) + save_to_validated( + variable=VARIABLE, + station=STATION, + to_delete=conditions, + start_date=START_DATE, + end_date=END_DATE, + minimum=MINIMUM, + maximum=MAXIMUM, + ) + out_status = f"{len(in_daily_selected_rows)} days saved to Validated" + daily_data_refresh_required = True + daily_table_refresh_required = True + plot_refresh_required = True + + # Button: Save (detail) + elif input_id == "save-button" and in_tabs_value == "tab-detail": + selected_ids = {row["id"] for row in in_detail_selected_rows} + for row in in_detail_row_data: + row["is_selected"] = row["id"] in selected_ids + save_detail_to_validated( + data_list=in_detail_row_data, + variable=VARIABLE, + station=STATION, + ) + out_status = f"{len(in_detail_selected_rows)} entries saved to Validated" + daily_data_refresh_required = True + daily_table_refresh_required = True + detail_data_refresh_required = True + detail_table_refresh_required = True + plot_refresh_required = True + + # Button: Reset (daily) + elif input_id == "reset-button" and in_tabs_value == "tab-daily": + reset_daily_validated( + variable=VARIABLE, + station=STATION, + start_date=START_DATE, + end_date=END_DATE, + ) + out_status = "Validation reset" + daily_data_refresh_required = True + daily_table_refresh_required = True + daily_table_reset_selection = True + plot_refresh_required = True + + # Button: Reset (detail) + elif input_id == "reset-button" and in_tabs_value == "tab-detail": + reset_detail_validated( + data_list=in_detail_row_data, + variable=VARIABLE, + station=STATION, + ) + out_status = "Validation reset" + daily_data_refresh_required = True + daily_table_refresh_required = True + detail_data_refresh_required = True + detail_table_refresh_required = True + detail_table_reset_selection = True + plot_refresh_required = True + + # Button: New row (detail) + elif input_id == "add-button" and in_tabs_value == "tab-detail": + last_id = in_detail_row_data[-1]["id"] + last_time = in_detail_row_data[-1]["time"] + new_row = { + "id": last_id + 1, + "time": last_time, + **{key: None for key in DATA_DAILY["value_columns"]}, + "outlier": False, + "value_difference": None, + "is_selected": False, + } + out_detail_row_transaction = {"add": [new_row]} + out_detail_scroll = {"data": new_row} + + # Date picker + elif input_id == "detail-date-picker": + new_selected_day = next( + ( + d["date"] + for d in DATA_DAILY["data"] + if d["date"].strftime("%Y-%m-%d") == in_detail_date + ), + None, + ) + if new_selected_day is not None: + SELECTED_DAY = new_selected_day + detail_data_refresh_required = True + detail_table_refresh_required = True + detail_table_reset_selection = True + out_tab_detail_disabled = False + out_tab_detail_label = ( + f"Detail of Selected Day ({SELECTED_DAY.strftime('%Y-%m-%d')})" + ) + out_tabs_value = "tab-detail" + out_add_button_disabled = False + out_status = "" + else: + out_status = "Invalid ID" + + # Plot radio + elif input_id == "plot_radio": + PLOT_TYPE = in_plot_radio_value + plot_refresh_required = True + + # Switching tabs + elif input_id == "tabs": + if in_tabs_value == "tab-detail": + out_add_button_disabled = False + else: + out_add_button_disabled = True + + # Reload daily data + if daily_data_refresh_required: + DATA_DAILY = daily_validation( + station=STATION, + variable=VARIABLE, + start_time=START_DATE, + end_time=END_DATE, + minimum=MINIMUM, + maximum=MAXIMUM, + ) + + # Reload detail data + if detail_data_refresh_required: + DATA_DETAIL = detail_list( + station=STATION, + variable=VARIABLE, + date_of_interest=SELECTED_DAY, + minimum=MINIMUM, + maximum=MAXIMUM, + ) + + # Refresh plot + if plot_refresh_required: + out_plot = create_validation_plot(data=DATA_DAILY, plot_type=PLOT_TYPE) + + # Refresh daily table + if daily_table_refresh_required: + out_daily_row_data = DATA_DAILY["data"] + + # Reset daily table selection + if daily_table_reset_selection: + out_daily_selected_rows = out_daily_row_data + + # Refresh detail table + if detail_table_refresh_required: + out_detail_row_data = DATA_DETAIL["series"] + + # Reset detail table selection + if detail_table_reset_selection: + out_detail_selected_rows = out_detail_row_data + + return ( + out_loading_top, + out_loading, + out_status, + out_plot, + out_daily_row_data, + out_detail_row_data, + out_detail_row_transaction, + out_detail_scroll, + out_daily_selected_rows, + out_detail_selected_rows, + out_tab_detail_disabled, + out_tab_detail_label, + out_tabs_value, + out_add_button_disabled, + ) diff --git a/validated/functions.py b/validated/functions.py index 552cb756..9889159d 100755 --- a/validated/functions.py +++ b/validated/functions.py @@ -905,6 +905,30 @@ def save_to_validated( return True +def reset_daily_validated( + variable: Variable, + station: Station, + start_date: datetime, + end_date: datetime, +): + """Removes selected daily data from the Validated table. + + Args: + station: Station of interest. + variable: Variable of interest. + start_date: Start date. + end_date: End date. + """ + validated = apps.get_model(app_label="validated", model_name=variable.variable_code) + tz = zoneinfo.ZoneInfo(station.timezone) + start_date, end_date = set_time_limits(start_date, end_date, tz) + + validated.timescale.filter( + time__range=[start_date, end_date], + station_id=station.station_id, + ).delete() + + def save_detail_to_validated( data_list: List[Dict[str, Any]], variable: Variable, station: Station ) -> bool: @@ -952,6 +976,24 @@ def save_detail_to_validated( return len(insert_result) == len(model_instances) +def reset_detail_validated(data_list, variable: Variable, station: Station): + """Removes detail data in selected time range from the validated table. + + Args: + data_list: List of data covering the time range to remove. + variable: The variable to update. + station: The station this records relate to. + """ + start_time = data_list[0]["time"] + end_time = data_list[-1]["time"] + + validated = apps.get_model(app_label="validated", model_name=variable.variable_code) + validated.timescale.filter( + time__range=[start_time, end_time], + station_id=station.station_id, + ).delete() + + def data_report( temporality: str, station: Station, diff --git a/validated/plots.py b/validated/plots.py new file mode 100644 index 00000000..bc4698ab --- /dev/null +++ b/validated/plots.py @@ -0,0 +1,56 @@ +import plotly.graph_objects as go + + +def create_validation_plot(data: dict, plot_type: str) -> go.Figure: + """Creates plot for Validation app + + Args: + data (dict): Daily data + plot_type (str): Type of plot + + Returns: + go.Figure: Plot + """ + variable: str = data["variable"]["name"] + data_series: dict = data["series"] + is_cumulative: bool = data["variable"]["is_cumulative"] + mode = "lines" if is_cumulative else "markers" + + fig = go.Figure() + + datasets = [ + {"key": "measurement", "name": "Measurement", "color": "black"}, + {"key": "selected", "name": "Selected", "color": "#636EFA"}, + {"key": "validated", "name": "Validated", "color": "#00CC96"}, + ] + + for dataset in datasets: + fig.add_trace( + go.Scatter( + x=data_series[dataset["key"]]["time"], + y=data_series[dataset["key"]][plot_type], + name=dataset["name"], + line=dict(color=dataset["color"]), + mode=mode, + marker_size=3, + ) + ) + + fig.update_yaxes(title_text=f"{variable} ({plot_type.capitalize()})") + fig.update_layout( + legend=dict( + x=1, + y=1, + xanchor="auto", + yanchor="auto", + ), + autosize=True, + margin=dict( + l=50, + r=0, + b=0, + t=20, + ), + ) + + return fig diff --git a/validated/tables.py b/validated/tables.py new file mode 100644 index 00000000..241eb53e --- /dev/null +++ b/validated/tables.py @@ -0,0 +1,257 @@ +""" +Functions defining columns and style conditions for tables in the validation app + +""" + + +def create_columns_daily(value_columns: list) -> list: + """Creates columns for Daily Report table + + Args: + value_columns (list): List of value columns + + Returns: + list: List of columns + """ + styles = create_style_conditions_daily() + + columns = [ + { + "field": "id", + "headerName": "Id", + "filter": "agNumberColumnFilter", + "maxWidth": 150, + }, + { + "valueGetter": {"function": "d3.timeParse('%Y-%m-%d')(params.data.date)"}, + "headerName": "Date", + "filter": "agDateColumnFilter", + "valueFormatter": {"function": "params.data.date"}, + **styles["date"], + }, + { + "field": "percentage", + "headerName": "Percnt.", + "filter": "agNumberColumnFilter", + **styles["percentage"], + }, + ] + + additional_columns = [ + { + "field": "sum", + "headerName": "Sum", + "filter": "agNumberColumnFilter", + **styles["sum"], + }, + { + "field": "average", + "headerName": "Average", + "filter": "agNumberColumnFilter", + **styles["average"], + }, + { + "field": "maximum", + "headerName": "Max. of Maxs.", + "filter": "agNumberColumnFilter", + **styles["maximum"], + }, + { + "field": "minimum", + "headerName": "Min. of Mins.", + "filter": "agNumberColumnFilter", + **styles["minimum"], + }, + ] + + columns += [d for d in additional_columns if d["field"] in value_columns] + + columns += [ + { + "field": "value_difference_error_count", + "headerName": "Diff. Err", + "filter": "agNumberColumnFilter", + **styles["value_difference_error_count"], + }, + ] + return columns + + +def create_columns_detail(value_columns: list) -> list: + """Creates columns for Detail table + + Args: + value_columns (list): List of value columns + + Returns: + list: List of columns + """ + styles = create_style_conditions_detail(value_columns) + + columns = [ + { + "field": "id", + "headerName": "Id", + "filter": "agNumberColumnFilter", + "maxWidth": 150, + }, + { + "field": "time", + "valueFormatter": {"function": "params.value.split('T')[1].split('+')[0]"}, + "headerName": "Time", + "editable": True, + **styles["time"], + }, + ] + columns += [ + { + "field": c, + "headerName": c.capitalize(), + "filter": "agNumberColumnFilter", + "editable": True, + **styles[c], + } + for c in value_columns + ] + columns += [ + { + "field": "stddev_error", + "headerName": "Outliers", + "valueFormatter": {"function": "params.value ? 'X' : '-'"}, + }, + {"field": "value_difference", "headerName": "Value diff."}, + ] + return columns + + +def create_style_condition( + condition: str, style_true: dict, style_false: dict +) -> list[dict]: + """Create a cell style condition + + Args: + condition (str): Javascript code to evaluate + style_true (dict): Style to apply when condition is true + style_false (dict): Style to apply when condition is false + + Returns: + list[dict]: Style condition + """ + + return [ + { + "condition": condition, + "style": style_true, + }, + { + "condition": f"!({condition})", + "style": style_false, + }, + ] + + +def create_style_conditions_daily() -> dict: + """Creates style conditions for Daily Report table + + Returns: + dict: Style conditions + """ + style_error = {"backgroundColor": "#E45756"} + style_normal = {"backgroundColor": "transparent"} + style_validated = {"backgroundColor": "#00CC96"} + + styles = {} + + styles["id"] = { + "cellStyle": { + "styleConditions": create_style_condition( + condition="params.data['all_validated']", + style_true=style_validated, + style_false=style_normal, + ) + }, + } + + styles["date"] = { + "cellStyle": { + "styleConditions": create_style_condition( + condition="params.data['date_error'] > 0", + style_true=style_error, + style_false=style_normal, + ) + }, + } + + styles["percentage"] = { + "cellStyle": { + "styleConditions": create_style_condition( + condition="params.data['percentage_error']", + style_true=style_error, + style_false=style_normal, + ) + }, + } + + styles["value_difference_error_count"] = { + "cellStyle": { + "styleConditions": create_style_condition( + condition="params.data['value_difference_error_count'] > 0", + style_true=style_error, + style_false=style_normal, + ) + }, + } + + for field in ["sum", "average", "maximum", "minimum"]: + styles[field] = { + "cellStyle": { + "styleConditions": create_style_condition( + condition=f"params.data['suspicious_{field}s_count'] > 0", + style_true=style_error, + style_false=style_normal, + ) + }, + } + + return styles + + +def create_style_conditions_detail(value_columns: list) -> dict: + """Creates style conditions for Detail table + + Args: + value_columns (list): List of value columns + + Returns: + dict: Style conditions + """ + styles = {} + + style_error = {"backgroundColor": "#E45756"} + style_warning = {"backgroundColor": "#FFA15A"} + style_normal = {"backgroundColor": "transparent"} + + styles["time"] = { + "cellStyle": { + "styleConditions": [ + { + "condition": f"params.data['time_lapse_status'] == {val}", + "style": s, + } + for val, s in zip([0, 1, 2], [style_error, style_normal, style_warning]) + ] + }, + } + + for field in value_columns + ["stdev", "value_difference"]: + styles[field] = { + "cellStyle": { + "styleConditions": create_style_condition( + condition=f"params.data['{field}_error']", + style_true=style_error, + style_false=style_normal, + ) + }, + } + + return styles