From 0b917059e1baafcac4499a89af150d1f359a6cd1 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 30 Jan 2024 11:57:01 +0000 Subject: [PATCH 01/42] made a start on tables --- requirements.txt | 1 + .../finished_apps/daily_validation.py | 117 +++++++++++++++++- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f2f5b413..117f5607 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 ## Legacy dependency versions # asgiref==3.3.4 diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index cbf01890..a25e7150 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -3,6 +3,8 @@ import plotly.express as px from dash import dcc, html +from dash.dash_table import DataTable +from dash_ag_grid import AgGrid from django_plotly_dash import DjangoDash from station.models import Station @@ -35,5 +37,118 @@ y = data["series"]["measurement"]["average"] plot = px.line(x=x, y=y) +""" +DAILY TABLE + +""" + + +def get_columns_daily(value_columns): + # Essential columns + always_present_columns = [ + ("id", "Id"), + ("date", "Date"), + ("percentage", "Percnt."), + ("value_difference_error_count", "Diff. Err"), + ] + columns = [{"id": id, "name": name} for id, name in always_present_columns] + + # Optional columns + optional_columns = { + "sum": "Sum", + "average": "Average", + "maximum": "Max. of Maxs.", + "minimum": "Min. of Mins.", + } + columns += [ + {"id": id, "name": name} + for id, name in optional_columns.items() + if id in value_columns + ] + + # Action column + columns.append({"id": "action", "name": "Action"}) + return columns + + +def get_style_data_conditional(): + style_date = { + "if": {"column_id": "date", "filter_query": "{date_error} > 0"}, + "backgroundColor": "red", + } + + # This isn't working + style_percentage = { + "if": {"column_id": "percentage", "filter_query": "{percentage_error} eq true"}, + "backgroundColor": "red", + } + + style_value_diff = { + "if": { + "column_id": "value_difference_error_count", + "filter_query": "{value_difference_error_count} > 0", + }, + "backgroundColor": "red", + } + + style_value = [ + { + "if": { + "column_id": "value", + "filter_query": "{{suspicious_{field}s_count}} > 0".format(field=field), + }, + "backgroundColor": "red", + } + for field in ["sum", "average", "maximum", "minimum"] + ] + + style_data_conditional = [ + style_date, + style_percentage, + style_value_diff, + ] + style_value + return style_data_conditional + + +def create_daily_table_ag(data): + table = AgGrid( + id="table", + data=data["data"], + columns=get_columns_daily(value_columns=data["value_columns"]), + rowMultiSelect=True, + gridOptions={ + "defaultColDef": { + "cellStyle": get_style_data_conditional(), + }, + }, + ) + return table + + +def create_daily_table(data): + table = DataTable( + id="table", + columns=get_columns_daily(value_columns=data["value_columns"]), + data=data["data"], + row_selectable="multi", + style_header={"fontWeight": "bold"}, + style_data_conditional=get_style_data_conditional(), + ) + return table + + +table = create_daily_table(data) +# table = create_daily_table_ag(data) + # Create layout -app.layout = html.Div([dcc.Graph(figure=plot)]) +# app.layout = html.Div([dcc.Graph(figure=plot)]) +app.layout = html.Div([table]) + + +""" +To do: +- Add action buttons +- Fix percentage column formatting +- Add error counts + +""" From 1783c3bffe4c6df276fe013d472c68cc9afbdccc Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 30 Jan 2024 14:09:28 +0000 Subject: [PATCH 02/42] Ag grid, improved layout --- .../finished_apps/daily_validation.py | 78 ++++++++++++++----- validated/templates/daily_validation_dev.html | 4 +- 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index a25e7150..b19850a4 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -110,21 +110,6 @@ def get_style_data_conditional(): return style_data_conditional -def create_daily_table_ag(data): - table = AgGrid( - id="table", - data=data["data"], - columns=get_columns_daily(value_columns=data["value_columns"]), - rowMultiSelect=True, - gridOptions={ - "defaultColDef": { - "cellStyle": get_style_data_conditional(), - }, - }, - ) - return table - - def create_daily_table(data): table = DataTable( id="table", @@ -137,12 +122,67 @@ def create_daily_table(data): return table -table = create_daily_table(data) -# table = create_daily_table_ag(data) +""" +AG GRID + +""" + + +def get_columns_daily_ag(value_columns): + # Essential columns + always_present_columns = [ + ("id", "Id"), + ("date", "Date"), + ("percentage", "Percnt."), + ("value_difference_error_count", "Diff. Err"), + ] + columns = [{"field": id, "headerName": name} for id, name in always_present_columns] + + # Optional columns + optional_columns = { + "sum": "Sum", + "average": "Average", + "maximum": "Max. of Maxs.", + "minimum": "Min. of Mins.", + } + columns += [ + {"field": id, "headerName": name} + for id, name in optional_columns.items() + if id in value_columns + ] + + # Action column + columns.append({"field": "action", "headerName": "Action"}) + return columns + + +def create_daily_table_ag(data): + table = AgGrid( + id="table", + rowData=data["data"], + columnDefs=get_columns_daily_ag(value_columns=data["value_columns"]), + columnSize="sizeToFit", + defaultColDef={ + "resizable": False, + "sortable": True, + "filter": False, + "checkboxSelection": { + "function": "params.column == params.columnApi.getAllDisplayedColumns()[0]" + }, + "headerCheckboxSelection": { + "function": "params.column == params.columnApi.getAllDisplayedColumns()[0]" + }, + }, + dashGridOptions={"rowSelection": "multiple", "suppressRowClickSelection": True}, + ) + return table + + +# table = create_daily_table(data) +table = create_daily_table_ag(data) # Create layout -# app.layout = html.Div([dcc.Graph(figure=plot)]) -app.layout = html.Div([table]) +app.layout = html.Div([table, dcc.Graph(figure=plot)]) """ diff --git a/validated/templates/daily_validation_dev.html b/validated/templates/daily_validation_dev.html index b5258d46..7e574f33 100644 --- a/validated/templates/daily_validation_dev.html +++ b/validated/templates/daily_validation_dev.html @@ -1,3 +1,5 @@ {% load plotly_dash %} -{% plotly_app name="DailyValidation" %} \ No newline at end of file +
+{% plotly_app name="DailyValidation" ratio=1 %} +
\ No newline at end of file From c4b1da236c0a00036e04d4af49b9bd08ed7e3fa0 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 30 Jan 2024 16:38:40 +0000 Subject: [PATCH 03/42] Using Ag grid --- .../finished_apps/daily_validation.py | 197 +++++++++--------- 1 file changed, 102 insertions(+), 95 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index b19850a4..d3213383 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -3,17 +3,21 @@ import plotly.express as px from dash import dcc, html -from dash.dash_table import DataTable 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 from variable.models import Variable # Create a Dash app app = DjangoDash("DailyValidation") +""" +DAILY DATA + +""" + # 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] @@ -32,7 +36,26 @@ maximum=maximum, ) -# Create plot +""" +DETAIL DATA + +""" + +date: datetime = datetime.strptime("2023-03-14", "%Y-%m-%d") + +data_detail = detail_list( + station=station, + variable=variable, + date_of_interest=date, + minimum=minimum, + maximum=maximum, +) + +""" +PLOT + +""" + x = data["series"]["measurement"]["time"] y = data["series"]["measurement"]["average"] plot = px.line(x=x, y=y) @@ -44,123 +67,96 @@ def get_columns_daily(value_columns): + styles = get_style_data_conditional() + # Essential columns - always_present_columns = [ - ("id", "Id"), - ("date", "Date"), - ("percentage", "Percnt."), - ("value_difference_error_count", "Diff. Err"), + essental_columns = [ + {"field": "id", "headerName": "Id"}, + {"field": "date", "headerName": "Date", **styles["date"]}, + {"field": "percentage", "headerName": "Percnt.", **styles["percentage"]}, + { + "field": "value_difference_error_count", + "headerName": "Diff. Err", + **styles["value_difference_error_count"], + }, ] - columns = [{"id": id, "name": name} for id, name in always_present_columns] # Optional columns - optional_columns = { - "sum": "Sum", - "average": "Average", - "maximum": "Max. of Maxs.", - "minimum": "Min. of Mins.", - } - columns += [ - {"id": id, "name": name} - for id, name in optional_columns.items() - if id in value_columns + optional_columns = [ + {"field": "sum", "headerName": "Sum"}, + {"field": "average", "headerName": "Average", **styles["average"]}, + {"field": "maximum", "headerName": "Max. of Maxs.", **styles["maximum"]}, + {"field": "minimum", "headerName": "Min. of Mins.", **styles["minimum"]}, ] + columns = essental_columns + columns += [d for d in optional_columns if d["field"] in value_columns] + # Action column - columns.append({"id": "action", "name": "Action"}) + columns.append({"field": "action", "headerName": "Action"}) return columns def get_style_data_conditional(): - style_date = { - "if": {"column_id": "date", "filter_query": "{date_error} > 0"}, - "backgroundColor": "red", + styles = {} + + styles["date"] = { + "cellStyle": { + "styleConditions": [ + { + "condition": "params.data['date_error'] > 0", + "style": {"backgroundColor": "sandybrown"}, + }, + ] + }, } - # This isn't working - style_percentage = { - "if": {"column_id": "percentage", "filter_query": "{percentage_error} eq true"}, - "backgroundColor": "red", + styles["percentage"] = { + "cellStyle": { + "styleConditions": [ + { + "condition": "params.data['percentage_error']", + "style": {"backgroundColor": "sandybrown"}, + }, + ] + }, } - style_value_diff = { - "if": { - "column_id": "value_difference_error_count", - "filter_query": "{value_difference_error_count} > 0", + styles["value_difference_error_count"] = { + "cellStyle": { + "styleConditions": [ + { + "condition": "params.data['value_difference_error_count'] > 0", + "style": {"backgroundColor": "sandybrown"}, + }, + ] }, - "backgroundColor": "red", } - style_value = [ - { - "if": { - "column_id": "value", - "filter_query": "{{suspicious_{field}s_count}} > 0".format(field=field), + for field in ["sum", "average", "maximum", "minimum"]: + styles[field] = { + "cellStyle": { + "styleConditions": [ + { + "condition": f"params.data['suspicious_{field}s_count'] > 0", + "style": {"backgroundColor": "sandybrown"}, + }, + ] }, - "backgroundColor": "red", } - for field in ["sum", "average", "maximum", "minimum"] - ] - style_data_conditional = [ - style_date, - style_percentage, - style_value_diff, - ] + style_value - return style_data_conditional + return styles def create_daily_table(data): - table = DataTable( - id="table", - columns=get_columns_daily(value_columns=data["value_columns"]), - data=data["data"], - row_selectable="multi", - style_header={"fontWeight": "bold"}, - style_data_conditional=get_style_data_conditional(), - ) - return table - - -""" -AG GRID - -""" - - -def get_columns_daily_ag(value_columns): - # Essential columns - always_present_columns = [ - ("id", "Id"), - ("date", "Date"), - ("percentage", "Percnt."), - ("value_difference_error_count", "Diff. Err"), - ] - columns = [{"field": id, "headerName": name} for id, name in always_present_columns] - - # Optional columns - optional_columns = { - "sum": "Sum", - "average": "Average", - "maximum": "Max. of Maxs.", - "minimum": "Min. of Mins.", - } - columns += [ - {"field": id, "headerName": name} - for id, name in optional_columns.items() - if id in value_columns - ] + table_data = data["data"].copy() + for d in table_data: + d["action"] = "Action" - # Action column - columns.append({"field": "action", "headerName": "Action"}) - return columns - - -def create_daily_table_ag(data): table = AgGrid( id="table", - rowData=data["data"], - columnDefs=get_columns_daily_ag(value_columns=data["value_columns"]), + rowData=table_data, + columnDefs=get_columns_daily(value_columns=data["value_columns"]), columnSize="sizeToFit", defaultColDef={ "resizable": False, @@ -178,8 +174,19 @@ def create_daily_table_ag(data): return table +""" +DETAIL TABLE + +""" + + +""" +LAYOUT + +""" + # table = create_daily_table(data) -table = create_daily_table_ag(data) +table = create_daily_table(data) # Create layout app.layout = html.Div([table, dcc.Graph(figure=plot)]) @@ -188,7 +195,7 @@ def create_daily_table_ag(data): """ To do: - Add action buttons -- Fix percentage column formatting - Add error counts +- Have chackboxes selected by default """ From 2613fd753c2f4575662909530d27acd10b005991 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 30 Jan 2024 21:50:47 +0000 Subject: [PATCH 04/42] Detail table --- .../finished_apps/daily_validation.py | 78 +++++++++++++++++-- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index d3213383..ecd2bc66 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -67,7 +67,7 @@ def get_columns_daily(value_columns): - styles = get_style_data_conditional() + styles = get_daily_style_data_conditional() # Essential columns essental_columns = [ @@ -97,7 +97,7 @@ def get_columns_daily(value_columns): return columns -def get_style_data_conditional(): +def get_daily_style_data_conditional(): styles = {} styles["date"] = { @@ -180,16 +180,82 @@ def create_daily_table(data): """ +def get_columns_detail(value_columns): + styles = get_detail_style_data_conditional(value_columns) + + columns = [ + {"field": "id", "headerName": "Id"}, + {"field": "time", "headerName": "Time", **styles["time"]}, + ] + columns += [ + {"field": c, "headerName": c[0].upper() + c[1:], **styles[c]} + for c in value_columns + ] + return columns + + +def get_detail_style_data_conditional(value_columns): + styles = {} + + styles["time"] = { + "cellStyle": { + "styleConditions": [ + { + "condition": f"params.data['time_lapse_status'] == {val}", + "style": {"backgroundColor": f"{col}"}, + } + for val, col in zip([0, 2], ["sandybrown", "yellow"]) + ] + }, + } + + for field in value_columns: + styles[field] = { + "cellStyle": { + "styleConditions": [ + { + "condition": f"params.data['{field}_error']", + "style": {"backgroundColor": "sandybrown"}, + }, + ] + }, + } + + return styles + + +def create_detail_table(data_detail): + table = AgGrid( + id="table_detail", + rowData=data_detail["series"], + columnDefs=get_columns_detail(value_columns=data_detail["value_columns"]), + columnSize="sizeToFit", + defaultColDef={ + "resizable": False, + "sortable": True, + "filter": True, + "checkboxSelection": { + "function": "params.column == params.columnApi.getAllDisplayedColumns()[0]" + }, + "headerCheckboxSelection": { + "function": "params.column == params.columnApi.getAllDisplayedColumns()[0]" + }, + }, + dashGridOptions={"rowSelection": "multiple", "suppressRowClickSelection": True}, + ) + return table + + """ LAYOUT """ -# table = create_daily_table(data) -table = create_daily_table(data) +table_daily = create_daily_table(data) +table_detail = create_detail_table(data_detail) # Create layout -app.layout = html.Div([table, dcc.Graph(figure=plot)]) +app.layout = html.Div([table_daily, table_detail, dcc.Graph(figure=plot)]) """ @@ -197,5 +263,7 @@ def create_daily_table(data): - Add action buttons - Add error counts - Have chackboxes selected by default +- Reformat time column +- Better filters """ From 9fa5a4380300d95d74091af32d6b6fcd01350944 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 31 Jan 2024 13:54:54 +0000 Subject: [PATCH 05/42] Move tables code, more progress --- requirements.txt | 2 +- .../finished_apps/daily_validation.py | 247 +++--------------- validated/tables.py | 216 +++++++++++++++ 3 files changed, 250 insertions(+), 215 deletions(-) create mode 100644 validated/tables.py diff --git a/requirements.txt b/requirements.txt index 117f5607..3aeef1f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ PyYAML==6.0.1 uritemplate==4.1.1 crispy-bootstrap4==2023.1 django-plotly-dash==2.2.2 -dash-ag-grid +dash-ag-grid==2.4.0 ## Legacy dependency versions # asgiref==3.3.4 diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index ecd2bc66..00547bd3 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -2,22 +2,18 @@ from decimal import Decimal import plotly.express as px +import plotly.graph_objects as go from dash import 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, detail_list +from validated.tables import create_daily_table, create_detail_table from variable.models import Variable # Create a Dash app app = DjangoDash("DailyValidation") -""" -DAILY DATA - -""" - # 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] @@ -26,7 +22,7 @@ minimum: Decimal = Decimal(-5) maximum: Decimal = Decimal(28) -# Load data +# Daily data data: dict = daily_validation( station=station, variable=variable, @@ -36,13 +32,8 @@ maximum=maximum, ) -""" -DETAIL DATA - -""" - +# Detail data date: datetime = datetime.strptime("2023-03-14", "%Y-%m-%d") - data_detail = detail_list( station=station, variable=variable, @@ -51,219 +42,47 @@ maximum=maximum, ) -""" -PLOT - -""" - -x = data["series"]["measurement"]["time"] -y = data["series"]["measurement"]["average"] -plot = px.line(x=x, y=y) - -""" -DAILY TABLE - -""" - - -def get_columns_daily(value_columns): - styles = get_daily_style_data_conditional() - - # Essential columns - essental_columns = [ - {"field": "id", "headerName": "Id"}, - {"field": "date", "headerName": "Date", **styles["date"]}, - {"field": "percentage", "headerName": "Percnt.", **styles["percentage"]}, - { - "field": "value_difference_error_count", - "headerName": "Diff. Err", - **styles["value_difference_error_count"], - }, - ] - - # Optional columns - optional_columns = [ - {"field": "sum", "headerName": "Sum"}, - {"field": "average", "headerName": "Average", **styles["average"]}, - {"field": "maximum", "headerName": "Max. of Maxs.", **styles["maximum"]}, - {"field": "minimum", "headerName": "Min. of Mins.", **styles["minimum"]}, - ] - - columns = essental_columns - columns += [d for d in optional_columns if d["field"] in value_columns] - - # Action column - columns.append({"field": "action", "headerName": "Action"}) - return columns - - -def get_daily_style_data_conditional(): - styles = {} - - styles["date"] = { - "cellStyle": { - "styleConditions": [ - { - "condition": "params.data['date_error'] > 0", - "style": {"backgroundColor": "sandybrown"}, - }, - ] - }, - } - - styles["percentage"] = { - "cellStyle": { - "styleConditions": [ - { - "condition": "params.data['percentage_error']", - "style": {"backgroundColor": "sandybrown"}, - }, - ] - }, - } - - styles["value_difference_error_count"] = { - "cellStyle": { - "styleConditions": [ - { - "condition": "params.data['value_difference_error_count'] > 0", - "style": {"backgroundColor": "sandybrown"}, - }, - ] - }, - } - - for field in ["sum", "average", "maximum", "minimum"]: - styles[field] = { - "cellStyle": { - "styleConditions": [ - { - "condition": f"params.data['suspicious_{field}s_count'] > 0", - "style": {"backgroundColor": "sandybrown"}, - }, - ] - }, - } - return styles - - -def create_daily_table(data): - table_data = data["data"].copy() - for d in table_data: - d["action"] = "Action" - - table = AgGrid( - id="table", - rowData=table_data, - columnDefs=get_columns_daily(value_columns=data["value_columns"]), - columnSize="sizeToFit", - defaultColDef={ - "resizable": False, - "sortable": True, - "filter": False, - "checkboxSelection": { - "function": "params.column == params.columnApi.getAllDisplayedColumns()[0]" - }, - "headerCheckboxSelection": { - "function": "params.column == params.columnApi.getAllDisplayedColumns()[0]" - }, - }, - dashGridOptions={"rowSelection": "multiple", "suppressRowClickSelection": True}, +# Plot +plot = go.Figure() +plot.add_trace( + go.Scatter( + x=data["series"]["measurement"]["time"], + y=data["series"]["measurement"]["average"], + mode="lines", + name="Measurement", ) - return table - - -""" -DETAIL TABLE - -""" - - -def get_columns_detail(value_columns): - styles = get_detail_style_data_conditional(value_columns) - - columns = [ - {"field": "id", "headerName": "Id"}, - {"field": "time", "headerName": "Time", **styles["time"]}, - ] - columns += [ - {"field": c, "headerName": c[0].upper() + c[1:], **styles[c]} - for c in value_columns - ] - return columns - - -def get_detail_style_data_conditional(value_columns): - styles = {} - - styles["time"] = { - "cellStyle": { - "styleConditions": [ - { - "condition": f"params.data['time_lapse_status'] == {val}", - "style": {"backgroundColor": f"{col}"}, - } - for val, col in zip([0, 2], ["sandybrown", "yellow"]) - ] - }, - } - - for field in value_columns: - styles[field] = { - "cellStyle": { - "styleConditions": [ - { - "condition": f"params.data['{field}_error']", - "style": {"backgroundColor": "sandybrown"}, - }, - ] - }, - } - - return styles - - -def create_detail_table(data_detail): - table = AgGrid( - id="table_detail", - rowData=data_detail["series"], - columnDefs=get_columns_detail(value_columns=data_detail["value_columns"]), - columnSize="sizeToFit", - defaultColDef={ - "resizable": False, - "sortable": True, - "filter": True, - "checkboxSelection": { - "function": "params.column == params.columnApi.getAllDisplayedColumns()[0]" - }, - "headerCheckboxSelection": { - "function": "params.column == params.columnApi.getAllDisplayedColumns()[0]" - }, - }, - dashGridOptions={"rowSelection": "multiple", "suppressRowClickSelection": True}, +) +plot.add_trace( + go.Scatter( + x=data["series"]["validated"]["time"], + y=data["series"]["validated"]["average"], + mode="lines", + name="Validated", ) - return table - - -""" -LAYOUT - -""" +) +plot.add_trace( + go.Scatter( + x=data["series"]["selected"]["time"], + y=data["series"]["selected"]["average"], + mode="lines", + name="Selected", + ) +) +# Tables table_daily = create_daily_table(data) table_detail = create_detail_table(data_detail) -# Create layout +# Layout app.layout = html.Div([table_daily, table_detail, dcc.Graph(figure=plot)]) +# Callback: check boxes + """ To do: -- Add action buttons -- Add error counts -- Have chackboxes selected by default - Reformat time column -- Better filters +- Add outliers and value diffs to detail table """ diff --git a/validated/tables.py b/validated/tables.py new file mode 100644 index 00000000..ad2462c4 --- /dev/null +++ b/validated/tables.py @@ -0,0 +1,216 @@ +from dash_ag_grid import AgGrid + + +def get_columns_daily(value_columns): + styles = get_daily_style_data_conditional() + + columns = [ + { + "field": "id", + "headerName": "Id", + "filter": "agNumberColumnFilter", + }, + { + "field": "date", + "headerName": "Date", + "filter": "agDateColumnFilter", + **styles["date"], + }, + { + "field": "percentage", + "headerName": "Percnt.", + "filter": "agNumberColumnFilter", + **styles["percentage"], + }, + { + "field": "value_difference_error_count", + "headerName": "Diff. Err", + "filter": "agNumberColumnFilter", + **styles["value_difference_error_count"], + }, + ] + + optional_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 optional_columns if d["field"] in value_columns] + return columns + + +def get_columns_detail(value_columns): + styles = get_detail_style_data_conditional(value_columns) + + columns = [ + {"field": "id", "headerName": "Id", "filter": "agNumberColumnFilter"}, + { + "field": "time", + "headerName": "Time", + **styles["time"], + }, + ] + columns += [ + { + "field": c, + "headerName": c[0].upper() + c[1:], + "filter": "agNumberColumnFilter", + "editable": True, + **styles[c], + } + for c in value_columns + ] + columns += [ + {"field": "stdev_error", "headerName": "Outliers"}, + {"field": "value_difference", "headerName": "Value diff."}, + ] + return columns + + +def get_daily_style_data_conditional(): + styles = {} + + styles["date"] = { + "cellStyle": { + "styleConditions": [ + { + "condition": "params.data['date_error'] > 0", + "style": {"backgroundColor": "sandybrown"}, + }, + ] + }, + } + + styles["percentage"] = { + "cellStyle": { + "styleConditions": [ + { + "condition": "params.data['percentage_error']", + "style": {"backgroundColor": "sandybrown"}, + }, + ] + }, + } + + styles["value_difference_error_count"] = { + "cellStyle": { + "styleConditions": [ + { + "condition": "params.data['value_difference_error_count'] > 0", + "style": {"backgroundColor": "sandybrown"}, + }, + ] + }, + } + + for field in ["sum", "average", "maximum", "minimum"]: + styles[field] = { + "cellStyle": { + "styleConditions": [ + { + "condition": f"params.data['suspicious_{field}s_count'] > 0", + "style": {"backgroundColor": "sandybrown"}, + }, + ] + }, + } + + return styles + + +def get_detail_style_data_conditional(value_columns): + styles = {} + + styles["time"] = { + "cellStyle": { + "styleConditions": [ + { + "condition": f"params.data['time_lapse_status'] == {val}", + "style": {"backgroundColor": f"{col}"}, + } + for val, col in zip([0, 2], ["sandybrown", "yellow"]) + ] + }, + } + + for field in value_columns + ["stdev", "value_difference"]: + styles[field] = { + "cellStyle": { + "styleConditions": [ + { + "condition": f"params.data['{field}_error']", + "style": {"backgroundColor": "sandybrown"}, + }, + ] + }, + } + + return styles + + +def create_daily_table(data): + table = AgGrid( + id="table", + rowData=data["data"], + columnDefs=get_columns_daily(value_columns=data["value_columns"]), + columnSize="sizeToFit", + defaultColDef={ + "resizable": False, + "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, + ) + return table + + +def create_detail_table(data_detail): + table = AgGrid( + id="table_detail", + rowData=data_detail["series"], + columnDefs=get_columns_detail(value_columns=data_detail["value_columns"]), + columnSize="sizeToFit", + defaultColDef={ + "resizable": False, + "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, + ) + return table From 5999390cef8294e2c74c40e2e5c51e30d0279037 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 31 Jan 2024 14:28:03 +0000 Subject: [PATCH 06/42] Minor formatting changes --- .../finished_apps/daily_validation.py | 2 -- validated/tables.py | 25 ++++++++++++------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 00547bd3..ab103c57 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -1,7 +1,6 @@ from datetime import datetime from decimal import Decimal -import plotly.express as px import plotly.graph_objects as go from dash import dcc, html from django_plotly_dash import DjangoDash @@ -83,6 +82,5 @@ """ To do: - Reformat time column -- Add outliers and value diffs to detail table """ diff --git a/validated/tables.py b/validated/tables.py index ad2462c4..430f913b 100644 --- a/validated/tables.py +++ b/validated/tables.py @@ -22,15 +22,9 @@ def get_columns_daily(value_columns): "filter": "agNumberColumnFilter", **styles["percentage"], }, - { - "field": "value_difference_error_count", - "headerName": "Diff. Err", - "filter": "agNumberColumnFilter", - **styles["value_difference_error_count"], - }, ] - optional_columns = [ + additional_columns = [ { "field": "sum", "headerName": "Sum", @@ -57,7 +51,16 @@ def get_columns_daily(value_columns): }, ] - columns += [d for d in optional_columns if d["field"] in value_columns] + 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 @@ -83,7 +86,11 @@ def get_columns_detail(value_columns): for c in value_columns ] columns += [ - {"field": "stdev_error", "headerName": "Outliers"}, + { + "field": "stdev_error", + "headerName": "Outliers", + "valueFormatter": {"function": "params.value ? 'X' : '-'"}, + }, {"field": "value_difference", "headerName": "Value diff."}, ] return columns From 27654d82a058871a33ab6e6b91f6d6ac43c35a5e Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 31 Jan 2024 15:46:52 +0000 Subject: [PATCH 07/42] Docstrings, cosmetic changes --- .../finished_apps/daily_validation.py | 47 +++-- validated/tables.py | 177 ++++++++++++------ 2 files changed, 150 insertions(+), 74 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index ab103c57..24c668f3 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -10,7 +10,8 @@ from validated.tables import create_daily_table, create_detail_table from variable.models import Variable -# Create a Dash app +DEFAULT_FONT = "Open Sans, Raleway, Dosis, Ubuntu, sans-serif" + app = DjangoDash("DailyValidation") # Filters (in final app this will get data from forms) @@ -50,22 +51,25 @@ y=data["series"]["measurement"]["average"], mode="lines", name="Measurement", + line=dict(color="black"), ) ) plot.add_trace( go.Scatter( - x=data["series"]["validated"]["time"], - y=data["series"]["validated"]["average"], + x=data["series"]["selected"]["time"], + y=data["series"]["selected"]["average"], mode="lines", - name="Validated", + name="Selected", + line=dict(color="#636EFA"), ) ) plot.add_trace( go.Scatter( - x=data["series"]["selected"]["time"], - y=data["series"]["selected"]["average"], + x=data["series"]["validated"]["time"], + y=data["series"]["validated"]["average"], mode="lines", - name="Selected", + name="Validated", + line=dict(color="#00CC96"), ) ) @@ -74,13 +78,22 @@ table_detail = create_detail_table(data_detail) # Layout -app.layout = html.Div([table_daily, table_detail, dcc.Graph(figure=plot)]) - -# Callback: check boxes - - -""" -To do: -- Reformat time column - -""" +app.layout = html.Div( + children=[ + html.H1( + children="Daily Report", + style={"font-family": DEFAULT_FONT}, + ), + table_daily, + html.H1( + children="Detail of Selected Day", + style={"font-family": DEFAULT_FONT}, + ), + table_detail, + html.H1( + children="Plot", + style={"font-family": DEFAULT_FONT}, + ), + dcc.Graph(figure=plot), + ] +) diff --git a/validated/tables.py b/validated/tables.py index 430f913b..9f08bda7 100644 --- a/validated/tables.py +++ b/validated/tables.py @@ -1,7 +1,77 @@ from dash_ag_grid import AgGrid -def get_columns_daily(value_columns): +def create_daily_table(data: dict) -> AgGrid: + """Creates Daily Report table + + Args: + data (dict): Daily report data (from functions.daily_validation) + + Returns: + AgGrid: Daily report table + """ + table = AgGrid( + id="table", + rowData=data["data"], + columnDefs=get_columns_daily(value_columns=data["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, + ) + return table + + +def create_detail_table(data: dict) -> AgGrid: + """Creates Detail table for a specific date + + Args: + data (dict): Detail data (from functions.detail_list) + + Returns: + AgGrid: Detail table + """ + table = AgGrid( + id="table_detail", + rowData=data["series"], + columnDefs=get_columns_detail(value_columns=data["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, + ) + return table + + +def get_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 = get_daily_style_data_conditional() columns = [ @@ -9,6 +79,7 @@ def get_columns_daily(value_columns): "field": "id", "headerName": "Id", "filter": "agNumberColumnFilter", + "maxWidth": 150, }, { "field": "date", @@ -64,13 +135,27 @@ def get_columns_daily(value_columns): return columns -def get_columns_detail(value_columns): +def get_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 = get_detail_style_data_conditional(value_columns) columns = [ - {"field": "id", "headerName": "Id", "filter": "agNumberColumnFilter"}, + { + "field": "id", + "headerName": "Id", + "filter": "agNumberColumnFilter", + "maxWidth": 150, + }, { "field": "time", + "valueFormatter": {"function": "params.value.split('T')[1].split('+')[0]"}, "headerName": "Time", **styles["time"], }, @@ -96,15 +181,31 @@ def get_columns_detail(value_columns): return columns -def get_daily_style_data_conditional(): +def get_daily_style_data_conditional() -> dict: + """Creates style conditions for Daily Report table + + Returns: + dict: Style conditions + """ styles = {} + styles["id"] = { + "cellStyle": { + "styleConditions": [ + { + "condition": "params.data['all_validated']", + "style": {"backgroundColor": "#00CC96"}, + }, + ] + }, + } + styles["date"] = { "cellStyle": { "styleConditions": [ { "condition": "params.data['date_error'] > 0", - "style": {"backgroundColor": "sandybrown"}, + "style": {"backgroundColor": "#E45756"}, }, ] }, @@ -115,7 +216,7 @@ def get_daily_style_data_conditional(): "styleConditions": [ { "condition": "params.data['percentage_error']", - "style": {"backgroundColor": "sandybrown"}, + "style": {"backgroundColor": "#E45756"}, }, ] }, @@ -126,7 +227,7 @@ def get_daily_style_data_conditional(): "styleConditions": [ { "condition": "params.data['value_difference_error_count'] > 0", - "style": {"backgroundColor": "sandybrown"}, + "style": {"backgroundColor": "#E45756"}, }, ] }, @@ -138,7 +239,7 @@ def get_daily_style_data_conditional(): "styleConditions": [ { "condition": f"params.data['suspicious_{field}s_count'] > 0", - "style": {"backgroundColor": "sandybrown"}, + "style": {"backgroundColor": "#E45756"}, }, ] }, @@ -147,7 +248,15 @@ def get_daily_style_data_conditional(): return styles -def get_detail_style_data_conditional(value_columns): +def get_detail_style_data_conditional(value_columns: list) -> dict: + """Creates style conditions for Detail table + + Args: + value_columns (list): List of value columns + + Returns: + dict: Style conditions + """ styles = {} styles["time"] = { @@ -157,7 +266,7 @@ def get_detail_style_data_conditional(value_columns): "condition": f"params.data['time_lapse_status'] == {val}", "style": {"backgroundColor": f"{col}"}, } - for val, col in zip([0, 2], ["sandybrown", "yellow"]) + for val, col in zip([0, 2], ["#E45756", "#FFA15A"]) ] }, } @@ -168,56 +277,10 @@ def get_detail_style_data_conditional(value_columns): "styleConditions": [ { "condition": f"params.data['{field}_error']", - "style": {"backgroundColor": "sandybrown"}, + "style": {"backgroundColor": "#E45756"}, }, ] }, } return styles - - -def create_daily_table(data): - table = AgGrid( - id="table", - rowData=data["data"], - columnDefs=get_columns_daily(value_columns=data["value_columns"]), - columnSize="sizeToFit", - defaultColDef={ - "resizable": False, - "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, - ) - return table - - -def create_detail_table(data_detail): - table = AgGrid( - id="table_detail", - rowData=data_detail["series"], - columnDefs=get_columns_detail(value_columns=data_detail["value_columns"]), - columnSize="sizeToFit", - defaultColDef={ - "resizable": False, - "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, - ) - return table From d8dbf49999d7b4e8566bb6273798941ccebcb0aa Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 31 Jan 2024 15:58:34 +0000 Subject: [PATCH 08/42] Rename functions --- .../finished_apps/daily_validation.py | 7 +++---- validated/tables.py | 18 +++++++++--------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 24c668f3..e3aae53c 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -42,6 +42,9 @@ maximum=maximum, ) +# Tables +table_daily = create_daily_table(data) +table_detail = create_detail_table(data_detail) # Plot plot = go.Figure() @@ -73,10 +76,6 @@ ) ) -# Tables -table_daily = create_daily_table(data) -table_detail = create_detail_table(data_detail) - # Layout app.layout = html.Div( children=[ diff --git a/validated/tables.py b/validated/tables.py index 9f08bda7..c25ddd40 100644 --- a/validated/tables.py +++ b/validated/tables.py @@ -11,9 +11,9 @@ def create_daily_table(data: dict) -> AgGrid: AgGrid: Daily report table """ table = AgGrid( - id="table", + id="table_daily", rowData=data["data"], - columnDefs=get_columns_daily(value_columns=data["value_columns"]), + columnDefs=create_columns_daily(value_columns=data["value_columns"]), columnSize="sizeToFit", defaultColDef={ "resizable": True, @@ -44,7 +44,7 @@ def create_detail_table(data: dict) -> AgGrid: table = AgGrid( id="table_detail", rowData=data["series"], - columnDefs=get_columns_detail(value_columns=data["value_columns"]), + columnDefs=create_columns_detail(value_columns=data["value_columns"]), columnSize="sizeToFit", defaultColDef={ "resizable": True, @@ -63,7 +63,7 @@ def create_detail_table(data: dict) -> AgGrid: return table -def get_columns_daily(value_columns: list) -> list: +def create_columns_daily(value_columns: list) -> list: """Creates columns for Daily Report table Args: @@ -72,7 +72,7 @@ def get_columns_daily(value_columns: list) -> list: Returns: list: List of columns """ - styles = get_daily_style_data_conditional() + styles = create_style_conditions_daily() columns = [ { @@ -135,7 +135,7 @@ def get_columns_daily(value_columns: list) -> list: return columns -def get_columns_detail(value_columns: list) -> list: +def create_columns_detail(value_columns: list) -> list: """Creates columns for Detail table Args: @@ -144,7 +144,7 @@ def get_columns_detail(value_columns: list) -> list: Returns: list: List of columns """ - styles = get_detail_style_data_conditional(value_columns) + styles = create_style_conditions_detail(value_columns) columns = [ { @@ -181,7 +181,7 @@ def get_columns_detail(value_columns: list) -> list: return columns -def get_daily_style_data_conditional() -> dict: +def create_style_conditions_daily() -> dict: """Creates style conditions for Daily Report table Returns: @@ -248,7 +248,7 @@ def get_daily_style_data_conditional() -> dict: return styles -def get_detail_style_data_conditional(value_columns: list) -> dict: +def create_style_conditions_detail(value_columns: list) -> dict: """Creates style conditions for Detail table Args: From 694a22c078a1f9551916b4435bbd8ab2aa43695d Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 31 Jan 2024 16:58:24 +0000 Subject: [PATCH 09/42] Fix date filter --- validated/tables.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/validated/tables.py b/validated/tables.py index c25ddd40..2f3edbfb 100644 --- a/validated/tables.py +++ b/validated/tables.py @@ -82,9 +82,10 @@ def create_columns_daily(value_columns: list) -> list: "maxWidth": 150, }, { - "field": "date", + "valueGetter": {"function": "d3.timeParse('%Y-%m-%d')(params.data.date)"}, "headerName": "Date", "filter": "agDateColumnFilter", + "valueFormatter": {"function": "params.data.date"}, **styles["date"], }, { From 72c4dadaee4936555f567fd9490df9d2bba99011 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 1 Feb 2024 17:05:48 +0000 Subject: [PATCH 10/42] Calbacks for saving to validated, resetting --- .../finished_apps/daily_validation.py | 215 +++++++++++++++--- validated/functions.py | 22 ++ 2 files changed, 206 insertions(+), 31 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index e3aae53c..dd746ece 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -1,13 +1,22 @@ from datetime import datetime from decimal import Decimal +import dash import plotly.graph_objects as go -from dash import dcc, html +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, detail_list -from validated.tables import create_daily_table, create_detail_table +from validated.functions import ( + daily_validation, + detail_list, + get_conditions, + reset_detail_validated, + save_detail_to_validated, + save_to_validated, +) +from validated.tables import create_columns_daily, create_columns_detail from variable.models import Variable DEFAULT_FONT = "Open Sans, Raleway, Dosis, Ubuntu, sans-serif" @@ -43,38 +52,81 @@ ) # Tables -table_daily = create_daily_table(data) -table_detail = create_detail_table(data_detail) +table_daily = AgGrid( + id="table_daily", + rowData=data["data"], + columnDefs=create_columns_daily(value_columns=data["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, +) + +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, +) + # Plot -plot = go.Figure() -plot.add_trace( - go.Scatter( - x=data["series"]["measurement"]["time"], - y=data["series"]["measurement"]["average"], - mode="lines", - name="Measurement", - line=dict(color="black"), +def create_plot(data): + plot = go.Figure() + plot.add_trace( + go.Scatter( + x=data["series"]["measurement"]["time"], + y=data["series"]["measurement"]["average"], + mode="lines", + name="Measurement", + line=dict(color="black"), + ) ) -) -plot.add_trace( - go.Scatter( - x=data["series"]["selected"]["time"], - y=data["series"]["selected"]["average"], - mode="lines", - name="Selected", - line=dict(color="#636EFA"), + plot.add_trace( + go.Scatter( + x=data["series"]["selected"]["time"], + y=data["series"]["selected"]["average"], + mode="lines", + name="Selected", + line=dict(color="#636EFA"), + ) ) -) -plot.add_trace( - go.Scatter( - x=data["series"]["validated"]["time"], - y=data["series"]["validated"]["average"], - mode="lines", - name="Validated", - line=dict(color="#00CC96"), + plot.add_trace( + go.Scatter( + x=data["series"]["validated"]["time"], + y=data["series"]["validated"]["average"], + mode="lines", + name="Validated", + line=dict(color="#00CC96"), + ) ) -) + return plot + + +line_graph = create_plot(data=data) # Layout app.layout = html.Div( @@ -84,15 +136,116 @@ style={"font-family": DEFAULT_FONT}, ), table_daily, + html.Button("Save to Validated", id="daily-save-button"), + html.Div( + id="daily-status-message", + children=[""], + style={"font-family": DEFAULT_FONT}, + ), html.H1( children="Detail of Selected Day", style={"font-family": DEFAULT_FONT}, ), table_detail, + html.Button("Save to Validated", id="detail-save-button"), + html.Button("Reset Validated", id="detail-reset-button"), + html.Div( + id="detail-status-message", + children=[""], + style={"font-family": DEFAULT_FONT}, + ), html.H1( children="Plot", style={"font-family": DEFAULT_FONT}, ), - dcc.Graph(figure=plot), + dcc.Graph(id="plot", figure=line_graph), ] ) + + +@app.callback( + [ + Output("daily-status-message", "children"), + Output("detail-status-message", "children"), + Output("plot", "figure"), + ], + [ + Input("daily-save-button", "n_clicks"), + Input("detail-save-button", "n_clicks"), + Input("detail-reset-button", "n_clicks"), + ], + [ + State("table_daily", "selectedRows"), + State("table_detail", "selectedRows"), + State("table_detail", "rowData"), + ], + prevent_initial_call=True, +) +def combined_callback( + daily_save_clicks, + detail_save_clicks, + detail_reset_clicks, + daily_selected_rows, + detail_selected_rows, + detail_row_data, +): + global data_detail + + ctx = dash.callback_context + if not ctx.triggered: + return dash.no_update + else: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + + # Daily save + if button_id == "daily-save-button" and daily_selected_rows is not None: + conditions = get_conditions(daily_selected_rows) + save_to_validated( + variable=variable, + station=station, + to_delete=conditions, + start_date=start_time, + end_date=end_time, + minimum=minimum, + maximum=maximum, + ) + daily_status = f"{len(daily_selected_rows)} entries saved to Validated" + detail_status = dash.no_update + + # Detail save + elif button_id == "detail-save-button" and detail_selected_rows is not None: + save_detail_to_validated( + data_list=detail_selected_rows, + variable=variable, + station=station, + ) + + daily_status = dash.no_update + detail_status = f"{len(detail_selected_rows)} entries saved to Validated" + + # Detail reset + elif button_id == "detail-reset-button": + reset_detail_validated( + data_list=detail_row_data, + variable=variable, + station=station, + ) + + daily_status = dash.no_update + detail_status = "Validation reset" + + else: + daily_status = dash.no_update + detail_status = dash.no_update + + # Reload data and redraw plot + data = daily_validation( + station=station, + variable=variable, + start_time=start_time, + end_time=end_time, + minimum=minimum, + maximum=maximum, + ) + figure = create_plot(data) + return daily_status, detail_status, figure diff --git a/validated/functions.py b/validated/functions.py index 552cb756..7321133e 100755 --- a/validated/functions.py +++ b/validated/functions.py @@ -952,6 +952,28 @@ def save_detail_to_validated( return len(insert_result) == len(model_instances) +def reset_detail_validated(data_list, variable: Variable, station: Station): + """TODO. + + Args: + start_time: TODO. + end_time: TODO. + variable: The variable to update. + station: The station this records relate to. + + Returns: + True if the selected data is inserted successfully in the database. + """ + 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, From 43bd82fbbf61d1bc123f3340f0c72a547820da96 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 2 Feb 2024 12:05:42 +0000 Subject: [PATCH 11/42] Callbacks for resetting, and minor fixes --- .../finished_apps/daily_validation.py | 107 ++++++++++-------- validated/functions.py | 32 +++++- validated/tables.py | 67 +---------- 3 files changed, 89 insertions(+), 117 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index dd746ece..fab3d37d 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -12,6 +12,7 @@ daily_validation, detail_list, get_conditions, + reset_daily_validated, reset_detail_validated, save_detail_to_validated, save_to_validated, @@ -26,27 +27,27 @@ # 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") +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") # Daily data -data: dict = daily_validation( +data_daily = daily_validation( station=station, variable=variable, - start_time=start_time, - end_time=end_time, + start_time=start_date, + end_time=end_date, minimum=minimum, maximum=maximum, ) # Detail data -date: datetime = datetime.strptime("2023-03-14", "%Y-%m-%d") data_detail = detail_list( station=station, variable=variable, - date_of_interest=date, + date_of_interest=selected_day, minimum=minimum, maximum=maximum, ) @@ -54,8 +55,8 @@ # Tables table_daily = AgGrid( id="table_daily", - rowData=data["data"], - columnDefs=create_columns_daily(value_columns=data["value_columns"]), + rowData=data_daily["data"], + columnDefs=create_columns_daily(value_columns=data_daily["value_columns"]), columnSize="sizeToFit", defaultColDef={ "resizable": True, @@ -94,39 +95,37 @@ # Plot -def create_plot(data): +def create_validation_plot(data: dict) -> go.Figure: + """Creates plot for Validation app + + Args: + data (dict): Daily data + + Returns: + go.Figure: Plot + """ plot = go.Figure() - plot.add_trace( - go.Scatter( - x=data["series"]["measurement"]["time"], - y=data["series"]["measurement"]["average"], - mode="lines", - name="Measurement", - line=dict(color="black"), - ) - ) - plot.add_trace( - go.Scatter( - x=data["series"]["selected"]["time"], - y=data["series"]["selected"]["average"], - mode="lines", - name="Selected", - line=dict(color="#636EFA"), - ) - ) - plot.add_trace( - go.Scatter( - x=data["series"]["validated"]["time"], - y=data["series"]["validated"]["average"], - mode="lines", - name="Validated", - line=dict(color="#00CC96"), + + datasets = [ + {"key": "measurement", "name": "Measurement", "color": "black"}, + {"key": "selected", "name": "Selected", "color": "#636EFA"}, + {"key": "validated", "name": "Validated", "color": "#00CC96"}, + ] + + for dataset in datasets: + plot.add_trace( + go.Scatter( + x=data["series"][dataset["key"]]["time"], + y=data["series"][dataset["key"]]["average"], + name=dataset["name"], + line=dict(color=dataset["color"]), + ) ) - ) + return plot -line_graph = create_plot(data=data) +plot = create_validation_plot(data=data_daily) # Layout app.layout = html.Div( @@ -137,6 +136,7 @@ def create_plot(data): ), table_daily, html.Button("Save to Validated", id="daily-save-button"), + html.Button("Reset Validated", id="daily-reset-button"), html.Div( id="daily-status-message", children=[""], @@ -158,7 +158,7 @@ def create_plot(data): children="Plot", style={"font-family": DEFAULT_FONT}, ), - dcc.Graph(id="plot", figure=line_graph), + dcc.Graph(id="plot", figure=plot), ] ) @@ -171,6 +171,7 @@ def create_plot(data): ], [ Input("daily-save-button", "n_clicks"), + Input("daily-reset-button", "n_clicks"), Input("detail-save-button", "n_clicks"), Input("detail-reset-button", "n_clicks"), ], @@ -183,6 +184,7 @@ def create_plot(data): ) def combined_callback( daily_save_clicks, + daily_reset_clicks, detail_save_clicks, detail_reset_clicks, daily_selected_rows, @@ -190,6 +192,7 @@ def combined_callback( detail_row_data, ): global data_detail + global data_daily ctx = dash.callback_context if not ctx.triggered: @@ -204,18 +207,32 @@ def combined_callback( variable=variable, station=station, to_delete=conditions, - start_date=start_time, - end_date=end_time, + start_date=start_date, + end_date=end_date, minimum=minimum, maximum=maximum, ) daily_status = f"{len(daily_selected_rows)} entries saved to Validated" detail_status = dash.no_update + # Daily reset + elif button_id == "daily-reset-button": + reset_daily_validated( + variable=variable, + station=station, + start_date=start_date, + end_date=end_date, + ) + daily_status = "Validation reset" + detail_status = dash.no_update + # Detail save elif button_id == "detail-save-button" and detail_selected_rows is not None: + selected_ids = {row["id"] for row in detail_selected_rows} + for row in detail_row_data: + row["is_selected"] = row["id"] in selected_ids save_detail_to_validated( - data_list=detail_selected_rows, + data_list=detail_row_data, variable=variable, station=station, ) @@ -239,13 +256,13 @@ def combined_callback( detail_status = dash.no_update # Reload data and redraw plot - data = daily_validation( + data_daily = daily_validation( station=station, variable=variable, - start_time=start_time, - end_time=end_time, + start_time=start_date, + end_time=end_date, minimum=minimum, maximum=maximum, ) - figure = create_plot(data) + figure = create_validation_plot(data_daily) return daily_status, detail_status, figure diff --git a/validated/functions.py b/validated/functions.py index 7321133e..206252ff 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: @@ -953,16 +977,12 @@ def save_detail_to_validated( def reset_detail_validated(data_list, variable: Variable, station: Station): - """TODO. + """Removes selected detail data from the validated table. Args: - start_time: TODO. - end_time: TODO. + data_list: List of data covering the time range to remove. variable: The variable to update. station: The station this records relate to. - - Returns: - True if the selected data is inserted successfully in the database. """ start_time = data_list[0]["time"] end_time = data_list[-1]["time"] diff --git a/validated/tables.py b/validated/tables.py index 2f3edbfb..44baaae8 100644 --- a/validated/tables.py +++ b/validated/tables.py @@ -1,68 +1,3 @@ -from dash_ag_grid import AgGrid - - -def create_daily_table(data: dict) -> AgGrid: - """Creates Daily Report table - - Args: - data (dict): Daily report data (from functions.daily_validation) - - Returns: - AgGrid: Daily report table - """ - table = AgGrid( - id="table_daily", - rowData=data["data"], - columnDefs=create_columns_daily(value_columns=data["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, - ) - return table - - -def create_detail_table(data: dict) -> AgGrid: - """Creates Detail table for a specific date - - Args: - data (dict): Detail data (from functions.detail_list) - - Returns: - AgGrid: Detail table - """ - table = AgGrid( - id="table_detail", - rowData=data["series"], - columnDefs=create_columns_detail(value_columns=data["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, - ) - return table - - def create_columns_daily(value_columns: list) -> list: """Creates columns for Daily Report table @@ -173,7 +108,7 @@ def create_columns_detail(value_columns: list) -> list: ] columns += [ { - "field": "stdev_error", + "field": "stddev_error", "headerName": "Outliers", "valueFormatter": {"function": "params.value ? 'X' : '-'"}, }, From f01411a3a5081e6a6a24a918cbdbc12f8690c1d8 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 2 Feb 2024 15:19:50 +0000 Subject: [PATCH 12/42] Update tables with callback --- .../finished_apps/daily_validation.py | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index fab3d37d..42a6e79c 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -69,7 +69,11 @@ }, "headerCheckboxSelectionFilteredOnly": True, }, - dashGridOptions={"rowSelection": "multiple", "suppressRowClickSelection": True}, + dashGridOptions={ + "rowSelection": "multiple", + "suppressRowClickSelection": True, + "suppressScrollOnNewData": True, + }, selectAll=True, ) @@ -89,7 +93,11 @@ }, "headerCheckboxSelectionFilteredOnly": True, }, - dashGridOptions={"rowSelection": "multiple", "suppressRowClickSelection": True}, + dashGridOptions={ + "rowSelection": "multiple", + "suppressRowClickSelection": True, + "suppressScrollOnNewData": True, + }, selectAll=True, ) @@ -119,6 +127,8 @@ def create_validation_plot(data: dict) -> go.Figure: y=data["series"][dataset["key"]]["average"], name=dataset["name"], line=dict(color=dataset["color"]), + mode="markers", + marker_size=3, ) ) @@ -168,6 +178,10 @@ def create_validation_plot(data: dict) -> go.Figure: Output("daily-status-message", "children"), Output("detail-status-message", "children"), Output("plot", "figure"), + Output("table_daily", "rowData"), + Output("table_detail", "rowData"), + Output("table_daily", "selectedRows"), + Output("table_detail", "selectedRows"), ], [ Input("daily-save-button", "n_clicks"), @@ -255,7 +269,7 @@ def combined_callback( daily_status = dash.no_update detail_status = dash.no_update - # Reload data and redraw plot + # Reload data_daily = daily_validation( station=station, variable=variable, @@ -264,5 +278,32 @@ def combined_callback( minimum=minimum, maximum=maximum, ) + data_detail = detail_list( + station=station, + variable=variable, + date_of_interest=selected_day, + minimum=minimum, + maximum=maximum, + ) + + # Reset tables + daily_selected_ids = {row["id"] for row in daily_selected_rows} + daily_selected_rows = [ + row for row in data_daily["data"] if row["id"] in daily_selected_ids + ] + detail_selected_ids = {row["id"] for row in detail_selected_rows} + detail_selected_rows = [ + row for row in data_detail["series"] if row["id"] in detail_selected_ids + ] + + # Redraw plot figure = create_validation_plot(data_daily) - return daily_status, detail_status, figure + return ( + daily_status, + detail_status, + figure, + data_daily["data"], + data_detail["series"], + daily_selected_rows, + detail_selected_rows, + ) From 0192a7ffc3b936c6151c4f32a85a3cb4089c8685 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 2 Feb 2024 16:16:01 +0000 Subject: [PATCH 13/42] Cleaner callback function --- .../finished_apps/daily_validation.py | 130 ++++++++++-------- 1 file changed, 73 insertions(+), 57 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 42a6e79c..46e458bd 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -196,17 +196,30 @@ def create_validation_plot(data: dict) -> go.Figure: ], prevent_initial_call=True, ) -def combined_callback( - daily_save_clicks, - daily_reset_clicks, - detail_save_clicks, - detail_reset_clicks, - daily_selected_rows, - detail_selected_rows, - detail_row_data, -): - global data_detail - global data_daily +def buttons_callback( + daily_save_clicks: int, + daily_reset_clicks: int, + detail_save_clicks: int, + detail_reset_clicks: int, + in_daily_selected_rows: list[dict], + in_detail_selected_rows: list[dict], + in_detail_row_data: list[dict], +) -> tuple[str, str, go.Figure, list[dict], list[dict], list[dict], list[dict]]: + """Callback for buttons adding and resetting Validated data + + Args: + daily_save_clicks (int): Number of times daily-save-button was clicked + daily_reset_clicks (int): Number of times daily-reset-button was clicked + detail_save_clicks (int): Number of times detail-save-button was clicked + detail_reset_clicks (int): Number of times detail-reset-button was clicked + in_daily_selected_rows (list[dict]): Selected rows in 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[str, str, go.Figure, list[dict], list[dict], list[dict], list[dict]]: + Callback outputs + """ ctx = dash.callback_context if not ctx.triggered: @@ -214,9 +227,17 @@ def combined_callback( else: button_id = ctx.triggered[0]["prop_id"].split(".")[0] + out_daily_status = dash.no_update + out_detail_status = dash.no_update + out_plot = dash.no_update + out_daily_row_data = dash.no_update + out_detail_row_data = dash.no_update + out_daily_selected_rows = dash.no_update + out_detail_selected_rows = dash.no_update + # Daily save - if button_id == "daily-save-button" and daily_selected_rows is not None: - conditions = get_conditions(daily_selected_rows) + if button_id == "daily-save-button" and in_daily_selected_rows is not None: + conditions = get_conditions(in_daily_selected_rows) save_to_validated( variable=variable, station=station, @@ -226,8 +247,7 @@ def combined_callback( minimum=minimum, maximum=maximum, ) - daily_status = f"{len(daily_selected_rows)} entries saved to Validated" - detail_status = dash.no_update + out_daily_status = f"{len(in_daily_selected_rows)} days saved to Validated" # Daily reset elif button_id == "daily-reset-button": @@ -237,39 +257,30 @@ def combined_callback( start_date=start_date, end_date=end_date, ) - daily_status = "Validation reset" - detail_status = dash.no_update + out_daily_status = "Validation reset" # Detail save - elif button_id == "detail-save-button" and detail_selected_rows is not None: - selected_ids = {row["id"] for row in detail_selected_rows} - for row in detail_row_data: + elif button_id == "detail-save-button" and in_detail_selected_rows is not None: + 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=detail_row_data, + data_list=in_detail_row_data, variable=variable, station=station, ) - - daily_status = dash.no_update - detail_status = f"{len(detail_selected_rows)} entries saved to Validated" + out_detail_status = f"{len(in_detail_selected_rows)} entries saved to Validated" # Detail reset elif button_id == "detail-reset-button": reset_detail_validated( - data_list=detail_row_data, + data_list=in_detail_row_data, variable=variable, station=station, ) + out_detail_status = "Validation reset" - daily_status = dash.no_update - detail_status = "Validation reset" - - else: - daily_status = dash.no_update - detail_status = dash.no_update - - # Reload + # Reload daily data data_daily = daily_validation( station=station, variable=variable, @@ -278,32 +289,37 @@ def combined_callback( minimum=minimum, maximum=maximum, ) - data_detail = detail_list( - station=station, - variable=variable, - date_of_interest=selected_day, - minimum=minimum, - maximum=maximum, - ) + + # Redraw plot + out_plot = create_validation_plot(data_daily) # Reset tables - daily_selected_ids = {row["id"] for row in daily_selected_rows} - daily_selected_rows = [ - row for row in data_daily["data"] if row["id"] in daily_selected_ids - ] - detail_selected_ids = {row["id"] for row in detail_selected_rows} - detail_selected_rows = [ - row for row in data_detail["series"] if row["id"] in detail_selected_ids - ] + if out_daily_status != dash.no_update: + out_daily_row_data = data_daily["data"] + daily_selected_ids = {row["id"] for row in in_daily_selected_rows} + out_daily_selected_rows = [ + row for row in out_daily_row_data if row["id"] in daily_selected_ids + ] + elif out_detail_status != dash.no_update: + data_detail = detail_list( + station=station, + variable=variable, + date_of_interest=selected_day, + minimum=minimum, + maximum=maximum, + ) + out_detail_row_data = data_detail["series"] + detail_selected_ids = {row["id"] for row in in_detail_selected_rows} + out_detail_selected_rows = [ + row for row in out_detail_row_data if row["id"] in detail_selected_ids + ] - # Redraw plot - figure = create_validation_plot(data_daily) return ( - daily_status, - detail_status, - figure, - data_daily["data"], - data_detail["series"], - daily_selected_rows, - detail_selected_rows, + out_daily_status, + out_detail_status, + out_plot, + out_daily_row_data, + out_detail_row_data, + out_daily_selected_rows, + out_detail_selected_rows, ) From bcb28c78fae69c209d5c027896e9a2cf2cc2aa96 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 2 Feb 2024 16:19:40 +0000 Subject: [PATCH 14/42] Docstring --- validated/tables.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/validated/tables.py b/validated/tables.py index 44baaae8..bdc17dba 100644 --- a/validated/tables.py +++ b/validated/tables.py @@ -1,3 +1,9 @@ +""" +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 From 9b7555aa4d3cc86932c517f8788d966bb6f821b7 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 5 Feb 2024 14:24:58 +0000 Subject: [PATCH 15/42] Started callback --- .../finished_apps/daily_validation.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 46e458bd..f1a718dc 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -157,6 +157,7 @@ def create_validation_plot(data: dict) -> go.Figure: style={"font-family": DEFAULT_FONT}, ), table_detail, + html.Button("Add row", id="detail-add-button"), html.Button("Save to Validated", id="detail-save-button"), html.Button("Reset Validated", id="detail-reset-button"), html.Div( @@ -323,3 +324,34 @@ def buttons_callback( out_daily_selected_rows, out_detail_selected_rows, ) + + +@app.callback( + [Output("table_detail", "rowTransaction"), Output("table_detail", "scrollTo")], + [Input("detail-add-button", "n_clicks")], + [State("table_detail", "rowData")], + prevent_initial_call=True, +) +def add_detail_row(detail_add_clicks: int, detail_row_data: list[dict]): + # TODO: restore selection status + # TODO: scroll to bottom + last_id = detail_row_data[-1]["id"] + last_time = 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": True, + } + return ({"add": [new_row]}, {"rowPosition": "bottom"}) + + +@app.callback( + [Output("table_detail", "scrollTo")], + [Input("table_detail", "rowTransaction")], + prevent_initial_call=True, +) +def scroll_to_bottom(row_transaction: dict): + return ({"rowPosition": "bottom"},) From b4e6398ce2c8d69a9945c2b03f09e7d751135e23 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 5 Feb 2024 14:27:43 +0000 Subject: [PATCH 16/42] Make time column editable --- validated/tables.py | 1 + 1 file changed, 1 insertion(+) diff --git a/validated/tables.py b/validated/tables.py index bdc17dba..cf97bd5c 100644 --- a/validated/tables.py +++ b/validated/tables.py @@ -99,6 +99,7 @@ def create_columns_detail(value_columns: list) -> list: "field": "time", "valueFormatter": {"function": "params.value.split('T')[1].split('+')[0]"}, "headerName": "Time", + "editable": True, **styles["time"], }, ] From 966567ad714381403fdd02b537c88f0a546da5ff Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 5 Feb 2024 15:48:39 +0000 Subject: [PATCH 17/42] Fixed scrolling and selection --- .../finished_apps/daily_validation.py | 144 +++++++++--------- 1 file changed, 68 insertions(+), 76 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index f1a718dc..6d0df171 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -34,7 +34,7 @@ selected_day: datetime = datetime.strptime("2023-03-14", "%Y-%m-%d") # Daily data -data_daily = daily_validation( +DATA_DAILY = daily_validation( station=station, variable=variable, start_time=start_date, @@ -44,7 +44,7 @@ ) # Detail data -data_detail = detail_list( +DATA_DETAIL = detail_list( station=station, variable=variable, date_of_interest=selected_day, @@ -55,8 +55,8 @@ # Tables table_daily = AgGrid( id="table_daily", - rowData=data_daily["data"], - columnDefs=create_columns_daily(value_columns=data_daily["value_columns"]), + rowData=DATA_DAILY["data"], + columnDefs=create_columns_daily(value_columns=DATA_DAILY["value_columns"]), columnSize="sizeToFit", defaultColDef={ "resizable": True, @@ -72,15 +72,15 @@ dashGridOptions={ "rowSelection": "multiple", "suppressRowClickSelection": True, - "suppressScrollOnNewData": 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"]), + rowData=DATA_DETAIL["series"], + columnDefs=create_columns_detail(value_columns=DATA_DETAIL["value_columns"]), columnSize="sizeToFit", defaultColDef={ "resizable": True, @@ -96,9 +96,9 @@ dashGridOptions={ "rowSelection": "multiple", "suppressRowClickSelection": True, - "suppressScrollOnNewData": True, }, selectAll=True, + getRowId="params.data.id", ) @@ -135,7 +135,7 @@ def create_validation_plot(data: dict) -> go.Figure: return plot -plot = create_validation_plot(data=data_daily) +plot = create_validation_plot(data=DATA_DAILY) # Layout app.layout = html.Div( @@ -181,14 +181,15 @@ def create_validation_plot(data: dict) -> go.Figure: Output("plot", "figure"), Output("table_daily", "rowData"), Output("table_detail", "rowData"), - Output("table_daily", "selectedRows"), - Output("table_detail", "selectedRows"), + Output("table_detail", "rowTransaction"), + Output("table_detail", "scrollTo"), ], [ Input("daily-save-button", "n_clicks"), Input("daily-reset-button", "n_clicks"), Input("detail-save-button", "n_clicks"), Input("detail-reset-button", "n_clicks"), + Input("detail-add-button", "n_clicks"), ], [ State("table_daily", "selectedRows"), @@ -202,6 +203,7 @@ def buttons_callback( daily_reset_clicks: int, detail_save_clicks: int, detail_reset_clicks: int, + detail_add_clicks: int, in_daily_selected_rows: list[dict], in_detail_selected_rows: list[dict], in_detail_row_data: list[dict], @@ -221,6 +223,7 @@ def buttons_callback( tuple[str, str, go.Figure, list[dict], list[dict], list[dict], list[dict]]: Callback outputs """ + global DATA_DAILY, DATA_DETAIL ctx = dash.callback_context if not ctx.triggered: @@ -233,10 +236,14 @@ def buttons_callback( out_plot = dash.no_update out_daily_row_data = dash.no_update out_detail_row_data = dash.no_update - out_daily_selected_rows = dash.no_update - out_detail_selected_rows = dash.no_update + out_detail_row_transaction = dash.no_update + out_detail_scroll = dash.no_update - # Daily save + daily_refresh_required = False + detail_refresh_required = False + plot_refresh_required = False + + # Button: Daily save if button_id == "daily-save-button" and in_daily_selected_rows is not None: conditions = get_conditions(in_daily_selected_rows) save_to_validated( @@ -249,8 +256,10 @@ def buttons_callback( maximum=maximum, ) out_daily_status = f"{len(in_daily_selected_rows)} days saved to Validated" + plot_refresh_required = True + daily_refresh_required = True - # Daily reset + # Button: Daily reset elif button_id == "daily-reset-button": reset_daily_validated( variable=variable, @@ -259,8 +268,10 @@ def buttons_callback( end_date=end_date, ) out_daily_status = "Validation reset" + plot_refresh_required = True + daily_refresh_required = True - # Detail save + # Button: Detail save elif button_id == "detail-save-button" and in_detail_selected_rows is not None: selected_ids = {row["id"] for row in in_detail_selected_rows} for row in in_detail_row_data: @@ -271,8 +282,10 @@ def buttons_callback( station=station, ) out_detail_status = f"{len(in_detail_selected_rows)} entries saved to Validated" + plot_refresh_required = True + detail_refresh_required = True - # Detail reset + # Button: Detail reset elif button_id == "detail-reset-button": reset_detail_validated( data_list=in_detail_row_data, @@ -280,40 +293,50 @@ def buttons_callback( station=station, ) out_detail_status = "Validation reset" + plot_refresh_required = True + detail_refresh_required = True + + # Button: Detail new row + elif button_id == "detail-add-button": + 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": True, + } + out_detail_row_transaction = {"add": [new_row]} + out_detail_scroll = {"data": new_row} + + # Refresh plot + if plot_refresh_required: + DATA_DAILY = daily_validation( + station=station, + variable=variable, + start_time=start_date, + end_time=end_date, + minimum=minimum, + maximum=maximum, + ) + out_plot = create_validation_plot(DATA_DAILY) - # Reload daily data - data_daily = daily_validation( - station=station, - variable=variable, - start_time=start_date, - end_time=end_date, - minimum=minimum, - maximum=maximum, - ) + # Refresh daily table + if daily_refresh_required: + out_daily_row_data = DATA_DAILY["data"] - # Redraw plot - out_plot = create_validation_plot(data_daily) - - # Reset tables - if out_daily_status != dash.no_update: - out_daily_row_data = data_daily["data"] - daily_selected_ids = {row["id"] for row in in_daily_selected_rows} - out_daily_selected_rows = [ - row for row in out_daily_row_data if row["id"] in daily_selected_ids - ] - elif out_detail_status != dash.no_update: - data_detail = detail_list( + # Refresh detail table + if detail_refresh_required: + DATA_DETAIL = detail_list( station=station, variable=variable, date_of_interest=selected_day, minimum=minimum, maximum=maximum, ) - out_detail_row_data = data_detail["series"] - detail_selected_ids = {row["id"] for row in in_detail_selected_rows} - out_detail_selected_rows = [ - row for row in out_detail_row_data if row["id"] in detail_selected_ids - ] + out_detail_row_data = DATA_DETAIL["series"] return ( out_daily_status, @@ -321,37 +344,6 @@ def buttons_callback( out_plot, out_daily_row_data, out_detail_row_data, - out_daily_selected_rows, - out_detail_selected_rows, + out_detail_row_transaction, + out_detail_scroll, ) - - -@app.callback( - [Output("table_detail", "rowTransaction"), Output("table_detail", "scrollTo")], - [Input("detail-add-button", "n_clicks")], - [State("table_detail", "rowData")], - prevent_initial_call=True, -) -def add_detail_row(detail_add_clicks: int, detail_row_data: list[dict]): - # TODO: restore selection status - # TODO: scroll to bottom - last_id = detail_row_data[-1]["id"] - last_time = 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": True, - } - return ({"add": [new_row]}, {"rowPosition": "bottom"}) - - -@app.callback( - [Output("table_detail", "scrollTo")], - [Input("table_detail", "rowTransaction")], - prevent_initial_call=True, -) -def scroll_to_bottom(row_transaction: dict): - return ({"rowPosition": "bottom"},) From 7a513905a10464ba69aca8cb92c0b834f34e1a8f Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 5 Feb 2024 15:55:30 +0000 Subject: [PATCH 18/42] Minor fixes --- validated/dash_apps/finished_apps/daily_validation.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 6d0df171..56b9e721 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -227,9 +227,8 @@ def buttons_callback( ctx = dash.callback_context if not ctx.triggered: - return dash.no_update - else: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] + return (dash.no_update,) * 7 + button_id = ctx.triggered[0]["prop_id"].split(".")[0] out_daily_status = dash.no_update out_detail_status = dash.no_update @@ -306,7 +305,7 @@ def buttons_callback( **{key: None for key in DATA_DAILY["value_columns"]}, "outlier": False, "value_difference": None, - "is_selected": True, + "is_selected": False, } out_detail_row_transaction = {"add": [new_row]} out_detail_scroll = {"data": new_row} From 3be1ce2c110628c6ca9ce776f58795daaff448d3 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 5 Feb 2024 16:29:48 +0000 Subject: [PATCH 19/42] Text field for opening selected day --- .../finished_apps/daily_validation.py | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 56b9e721..67c5873e 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -31,7 +31,7 @@ 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") +SELECTED_DAY: datetime = datetime.strptime("2023-03-14", "%Y-%m-%d") # Daily data DATA_DAILY = daily_validation( @@ -47,7 +47,7 @@ DATA_DETAIL = detail_list( station=station, variable=variable, - date_of_interest=selected_day, + date_of_interest=SELECTED_DAY, minimum=minimum, maximum=maximum, ) @@ -147,6 +147,16 @@ def create_validation_plot(data: dict) -> go.Figure: table_daily, html.Button("Save to Validated", id="daily-save-button"), html.Button("Reset Validated", id="daily-reset-button"), + html.Div( + children=["Open detailed view:"], + style={"font-family": DEFAULT_FONT}, + ), + dcc.Input( + id="input-daily-id", + type="text", + debounce=True, + placeholder="YYYY-MM-DD", + ), html.Div( id="daily-status-message", children=[""], @@ -190,6 +200,7 @@ def create_validation_plot(data: dict) -> go.Figure: Input("detail-save-button", "n_clicks"), Input("detail-reset-button", "n_clicks"), Input("detail-add-button", "n_clicks"), + Input("input-daily-id", "value"), ], [ State("table_daily", "selectedRows"), @@ -204,6 +215,7 @@ def buttons_callback( detail_save_clicks: int, detail_reset_clicks: int, detail_add_clicks: int, + value: str, in_daily_selected_rows: list[dict], in_detail_selected_rows: list[dict], in_detail_row_data: list[dict], @@ -223,7 +235,7 @@ def buttons_callback( tuple[str, str, go.Figure, list[dict], list[dict], list[dict], list[dict]]: Callback outputs """ - global DATA_DAILY, DATA_DETAIL + global DATA_DAILY, DATA_DETAIL, SELECTED_DAY ctx = dash.callback_context if not ctx.triggered: @@ -310,6 +322,11 @@ def buttons_callback( out_detail_row_transaction = {"add": [new_row]} out_detail_scroll = {"data": new_row} + # Input: Daily date + elif button_id == "input-daily-id": + SELECTED_DAY = datetime.strptime(value, "%Y-%m-%d") + detail_refresh_required = True + # Refresh plot if plot_refresh_required: DATA_DAILY = daily_validation( @@ -331,7 +348,7 @@ def buttons_callback( DATA_DETAIL = detail_list( station=station, variable=variable, - date_of_interest=selected_day, + date_of_interest=SELECTED_DAY, minimum=minimum, maximum=maximum, ) From 3f1082fcfc7af802e9d888994a15bd3275615b85 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 5 Feb 2024 16:44:42 +0000 Subject: [PATCH 20/42] Switch input to ID --- .../finished_apps/daily_validation.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 67c5873e..7ecd2ed3 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -153,9 +153,9 @@ def create_validation_plot(data: dict) -> go.Figure: ), dcc.Input( id="input-daily-id", - type="text", + type="number", debounce=True, - placeholder="YYYY-MM-DD", + placeholder="ID", ), html.Div( id="daily-status-message", @@ -209,13 +209,13 @@ def create_validation_plot(data: dict) -> go.Figure: ], prevent_initial_call=True, ) -def buttons_callback( +def callbacks( daily_save_clicks: int, daily_reset_clicks: int, detail_save_clicks: int, detail_reset_clicks: int, detail_add_clicks: int, - value: str, + daily_id: int, in_daily_selected_rows: list[dict], in_detail_selected_rows: list[dict], in_detail_row_data: list[dict], @@ -324,8 +324,14 @@ def buttons_callback( # Input: Daily date elif button_id == "input-daily-id": - SELECTED_DAY = datetime.strptime(value, "%Y-%m-%d") - detail_refresh_required = True + SELECTED_DAY = next( + (d["date"] for d in DATA_DAILY["data"] if d["id"] == daily_id), + dash.no_update, + ) + if SELECTED_DAY != dash.no_update: + detail_refresh_required = True + else: + out_daily_status = "Invalid ID" # Refresh plot if plot_refresh_required: From e2ddcd2d30404391895b6a379c06345218d3bab7 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 5 Feb 2024 16:47:12 +0000 Subject: [PATCH 21/42] Fix docstring --- validated/dash_apps/finished_apps/daily_validation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 56b9e721..013a4543 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -215,12 +215,13 @@ def buttons_callback( daily_reset_clicks (int): Number of times daily-reset-button was clicked detail_save_clicks (int): Number of times detail-save-button was clicked detail_reset_clicks (int): Number of times detail-reset-button was clicked + detail_add_clicks (int): Number of times detail-add-button was clicked in_daily_selected_rows (list[dict]): Selected rows in 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[str, str, go.Figure, list[dict], list[dict], list[dict], list[dict]]: + tuple[str, str, go.Figure, list[dict], list[dict], dict, dict]: Callback outputs """ global DATA_DAILY, DATA_DETAIL From 59da5af1f85984a536869fa833571dba0b74227a Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 6 Feb 2024 10:16:03 +0000 Subject: [PATCH 22/42] Remove redundant statement --- validated/dash_apps/finished_apps/daily_validation.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 013a4543..ff9031f7 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -227,8 +227,6 @@ def buttons_callback( global DATA_DAILY, DATA_DETAIL ctx = dash.callback_context - if not ctx.triggered: - return (dash.no_update,) * 7 button_id = ctx.triggered[0]["prop_id"].split(".")[0] out_daily_status = dash.no_update From abc0b78c351cf44b1001ccfd2a17a6e70907c12e Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 6 Feb 2024 11:30:15 +0000 Subject: [PATCH 23/42] Fix daily save button --- .../dash_apps/finished_apps/daily_validation.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index ff9031f7..e9a7b5d4 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -107,7 +107,7 @@ def create_validation_plot(data: dict) -> go.Figure: """Creates plot for Validation app Args: - data (dict): Daily data + data (dict): Daily data series Returns: go.Figure: Plot @@ -123,8 +123,8 @@ def create_validation_plot(data: dict) -> go.Figure: for dataset in datasets: plot.add_trace( go.Scatter( - x=data["series"][dataset["key"]]["time"], - y=data["series"][dataset["key"]]["average"], + x=data[dataset["key"]]["time"], + y=data[dataset["key"]]["average"], name=dataset["name"], line=dict(color=dataset["color"]), mode="markers", @@ -135,7 +135,7 @@ def create_validation_plot(data: dict) -> go.Figure: return plot -plot = create_validation_plot(data=DATA_DAILY) +plot = create_validation_plot(data=DATA_DAILY["series"]) # Layout app.layout = html.Div( @@ -193,6 +193,7 @@ def create_validation_plot(data: dict) -> go.Figure: ], [ State("table_daily", "selectedRows"), + State("table_daily", "rowData"), State("table_detail", "selectedRows"), State("table_detail", "rowData"), ], @@ -205,6 +206,7 @@ def buttons_callback( detail_reset_clicks: int, detail_add_clicks: int, 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[str, str, go.Figure, list[dict], list[dict], list[dict], list[dict]]: @@ -243,7 +245,10 @@ def buttons_callback( # Button: Daily save if button_id == "daily-save-button" and in_daily_selected_rows is not None: - conditions = get_conditions(in_daily_selected_rows) + 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, @@ -319,7 +324,7 @@ def buttons_callback( minimum=minimum, maximum=maximum, ) - out_plot = create_validation_plot(DATA_DAILY) + out_plot = create_validation_plot(DATA_DAILY["series"]) # Refresh daily table if daily_refresh_required: From ae36570e2f2d5fa675e3cf50e3fbe96d5624bbb3 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 6 Feb 2024 13:52:57 +0000 Subject: [PATCH 24/42] Explicitly define negative conditions, rename global variables, docstrings --- .../finished_apps/daily_validation.py | 92 ++++++++-------- validated/functions.py | 4 +- validated/tables.py | 104 +++++++++++------- 3 files changed, 114 insertions(+), 86 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index e9a7b5d4..d400cb48 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -25,31 +25,31 @@ 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_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") +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") # Daily data DATA_DAILY = daily_validation( - station=station, - variable=variable, - start_time=start_date, - end_time=end_date, - minimum=minimum, - maximum=maximum, + 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, + station=STATION, + variable=VARIABLE, + date_of_interest=SELECTED_DAY, + minimum=MINIMUM, + maximum=MAXIMUM, ) # Tables @@ -250,13 +250,13 @@ def buttons_callback( row["state"] = row["id"] in selected_ids conditions = get_conditions(in_daily_row_data) save_to_validated( - variable=variable, - station=station, + variable=VARIABLE, + station=STATION, to_delete=conditions, - start_date=start_date, - end_date=end_date, - minimum=minimum, - maximum=maximum, + start_date=START_DATE, + end_date=END_DATE, + minimum=MINIMUM, + maximum=MAXIMUM, ) out_daily_status = f"{len(in_daily_selected_rows)} days saved to Validated" plot_refresh_required = True @@ -265,10 +265,10 @@ def buttons_callback( # Button: Daily reset elif button_id == "daily-reset-button": reset_daily_validated( - variable=variable, - station=station, - start_date=start_date, - end_date=end_date, + variable=VARIABLE, + station=STATION, + start_date=START_DATE, + end_date=END_DATE, ) out_daily_status = "Validation reset" plot_refresh_required = True @@ -281,8 +281,8 @@ def buttons_callback( row["is_selected"] = row["id"] in selected_ids save_detail_to_validated( data_list=in_detail_row_data, - variable=variable, - station=station, + variable=VARIABLE, + station=STATION, ) out_detail_status = f"{len(in_detail_selected_rows)} entries saved to Validated" plot_refresh_required = True @@ -292,8 +292,8 @@ def buttons_callback( elif button_id == "detail-reset-button": reset_detail_validated( data_list=in_detail_row_data, - variable=variable, - station=station, + variable=VARIABLE, + station=STATION, ) out_detail_status = "Validation reset" plot_refresh_required = True @@ -317,27 +317,27 @@ def buttons_callback( # Refresh plot if plot_refresh_required: DATA_DAILY = daily_validation( - station=station, - variable=variable, - start_time=start_date, - end_time=end_date, - minimum=minimum, - maximum=maximum, + station=STATION, + variable=VARIABLE, + start_time=START_DATE, + end_time=END_DATE, + minimum=MINIMUM, + maximum=MAXIMUM, ) out_plot = create_validation_plot(DATA_DAILY["series"]) - # Refresh daily table - if daily_refresh_required: - out_daily_row_data = DATA_DAILY["data"] + # Refresh daily table + if daily_refresh_required: + out_daily_row_data = DATA_DAILY["data"] # Refresh detail table if detail_refresh_required: DATA_DETAIL = detail_list( - station=station, - variable=variable, - date_of_interest=selected_day, - minimum=minimum, - maximum=maximum, + station=STATION, + variable=VARIABLE, + date_of_interest=SELECTED_DAY, + minimum=MINIMUM, + maximum=MAXIMUM, ) out_detail_row_data = DATA_DETAIL["series"] diff --git a/validated/functions.py b/validated/functions.py index 206252ff..9889159d 100755 --- a/validated/functions.py +++ b/validated/functions.py @@ -911,7 +911,7 @@ def reset_daily_validated( start_date: datetime, end_date: datetime, ): - """Removes selected daily data from the Validated table. + """Removes selected daily data from the Validated table. Args: station: Station of interest. @@ -977,7 +977,7 @@ def save_detail_to_validated( def reset_detail_validated(data_list, variable: Variable, station: Station): - """Removes selected detail data from the validated table. + """Removes detail data in selected time range from the validated table. Args: data_list: List of data covering the time range to remove. diff --git a/validated/tables.py b/validated/tables.py index cf97bd5c..9e464a1c 100644 --- a/validated/tables.py +++ b/validated/tables.py @@ -124,67 +124,92 @@ def create_columns_detail(value_columns: list) -> list: 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": [ - { - "condition": "params.data['all_validated']", - "style": {"backgroundColor": "#00CC96"}, - }, - ] + "styleConditions": create_style_condition( + condition="params.data['all_validated']", + style_true=style_validated, + style_false=style_normal, + ) }, } styles["date"] = { "cellStyle": { - "styleConditions": [ - { - "condition": "params.data['date_error'] > 0", - "style": {"backgroundColor": "#E45756"}, - }, - ] + "styleConditions": create_style_condition( + condition="params.data['date_error'] > 0", + style_true=style_error, + style_false=style_normal, + ) }, } styles["percentage"] = { "cellStyle": { - "styleConditions": [ - { - "condition": "params.data['percentage_error']", - "style": {"backgroundColor": "#E45756"}, - }, - ] + "styleConditions": create_style_condition( + condition="params.data['percentage_error']", + style_true=style_error, + style_false=style_normal, + ) }, } styles["value_difference_error_count"] = { "cellStyle": { - "styleConditions": [ - { - "condition": "params.data['value_difference_error_count'] > 0", - "style": {"backgroundColor": "#E45756"}, - }, - ] + "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": [ - { - "condition": f"params.data['suspicious_{field}s_count'] > 0", - "style": {"backgroundColor": "#E45756"}, - }, - ] + "styleConditions": create_style_condition( + condition=f"params.data['suspicious_{field}s_count'] > 0", + style_true=style_error, + style_false=style_normal, + ) }, } @@ -202,14 +227,18 @@ def create_style_conditions_detail(value_columns: list) -> dict: """ 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": {"backgroundColor": f"{col}"}, + "style": s, } - for val, col in zip([0, 2], ["#E45756", "#FFA15A"]) + for val, s in zip([0, 1, 2], [style_error, style_normal, style_warning]) ] }, } @@ -217,12 +246,11 @@ def create_style_conditions_detail(value_columns: list) -> dict: for field in value_columns + ["stdev", "value_difference"]: styles[field] = { "cellStyle": { - "styleConditions": [ - { - "condition": f"params.data['{field}_error']", - "style": {"backgroundColor": "#E45756"}, - }, - ] + "styleConditions": create_style_condition( + condition=f"params.data['{field}_error']", + style_true=style_error, + style_false=style_normal, + ) }, } From 879ce98f11ada75ea16175458fc5e5261b2f0e92 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 6 Feb 2024 15:11:17 +0000 Subject: [PATCH 25/42] Reset selection when resetting tables --- .../finished_apps/daily_validation.py | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index d400cb48..871bdcea 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -183,6 +183,8 @@ def create_validation_plot(data: dict) -> go.Figure: Output("table_detail", "rowData"), Output("table_detail", "rowTransaction"), Output("table_detail", "scrollTo"), + Output("table_daily", "selectedRows"), + Output("table_detail", "selectedRows"), ], [ Input("daily-save-button", "n_clicks"), @@ -209,7 +211,17 @@ def buttons_callback( in_daily_row_data: list[dict], in_detail_selected_rows: list[dict], in_detail_row_data: list[dict], -) -> tuple[str, str, go.Figure, list[dict], list[dict], list[dict], list[dict]]: +) -> tuple[ + str, + str, + go.Figure, + list[dict], + list[dict], + dict, + dict, + list[dict], + list[dict], +]: """Callback for buttons adding and resetting Validated data Args: @@ -223,7 +235,7 @@ def buttons_callback( in_detail_row_data (list[dict]): Full row data for table_detail Returns: - tuple[str, str, go.Figure, list[dict], list[dict], dict, dict]: + tuple[str, str, go.Figure, list[dict], list[dict], dict, dict, list[dict], list[dict]]: Callback outputs """ global DATA_DAILY, DATA_DETAIL @@ -238,13 +250,17 @@ def buttons_callback( 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 daily_refresh_required = False detail_refresh_required = False + daily_reset_selection = False + detail_reset_selection = False plot_refresh_required = False # Button: Daily save - if button_id == "daily-save-button" and in_daily_selected_rows is not None: + if button_id == "daily-save-button": 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 @@ -273,9 +289,10 @@ def buttons_callback( out_daily_status = "Validation reset" plot_refresh_required = True daily_refresh_required = True + daily_reset_selection = True # Button: Detail save - elif button_id == "detail-save-button" and in_detail_selected_rows is not None: + elif button_id == "detail-save-button": 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 @@ -298,6 +315,7 @@ def buttons_callback( out_detail_status = "Validation reset" plot_refresh_required = True detail_refresh_required = True + detail_reset_selection = True # Button: Detail new row elif button_id == "detail-add-button": @@ -329,6 +347,8 @@ def buttons_callback( # Refresh daily table if daily_refresh_required: out_daily_row_data = DATA_DAILY["data"] + if daily_reset_selection: + out_daily_selected_rows = out_daily_row_data # Refresh detail table if detail_refresh_required: @@ -340,6 +360,8 @@ def buttons_callback( maximum=MAXIMUM, ) out_detail_row_data = DATA_DETAIL["series"] + if detail_reset_selection: + out_detail_selected_rows = out_detail_row_data return ( out_daily_status, @@ -349,4 +371,6 @@ def buttons_callback( out_detail_row_data, out_detail_row_transaction, out_detail_scroll, + out_daily_selected_rows, + out_detail_selected_rows, ) From c846f95687710b4b573fbfa57dd24be24caae415 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 6 Feb 2024 15:32:20 +0000 Subject: [PATCH 26/42] Docstring --- validated/dash_apps/finished_apps/daily_validation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 6e66f8a2..18de6c2c 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -243,6 +243,7 @@ def callbacks( detail_save_clicks (int): Number of times detail-save-button was clicked detail_reset_clicks (int): Number of times detail-reset-button was clicked detail_add_clicks (int): Number of times detail-add-button was clicked + daily_id (int): ID of selected day in_daily_selected_rows (list[dict]): Selected rows in 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 From 09e3f8f465c8053c074b28a21b6ba71a97b33aeb Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 6 Feb 2024 16:17:15 +0000 Subject: [PATCH 27/42] Put tables into tabs --- .../finished_apps/daily_validation.py | 90 +++++++++++-------- 1 file changed, 55 insertions(+), 35 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 18de6c2c..fe7eda7d 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -141,40 +141,55 @@ def create_validation_plot(data: dict) -> go.Figure: # Layout app.layout = html.Div( children=[ - html.H1( - children="Daily Report", - style={"font-family": DEFAULT_FONT}, - ), - table_daily, - html.Button("Save to Validated", id="daily-save-button"), - html.Button("Reset Validated", id="daily-reset-button"), - html.Div( - children=["Open detailed view:"], - style={"font-family": DEFAULT_FONT}, - ), - dcc.Input( - id="input-daily-id", - type="number", - debounce=True, - placeholder="ID", - ), - html.Div( - id="daily-status-message", - children=[""], - style={"font-family": DEFAULT_FONT}, - ), - html.H1( - children="Detail of Selected Day", - style={"font-family": DEFAULT_FONT}, - ), - table_detail, - html.Button("Add row", id="detail-add-button"), - html.Button("Save to Validated", id="detail-save-button"), - html.Button("Reset Validated", id="detail-reset-button"), - html.Div( - id="detail-status-message", - children=[""], - style={"font-family": DEFAULT_FONT}, + dcc.Tabs( + id="tabs", + children=[ + dcc.Tab( + label="Daily Report", + id="tab-daily", + style={"font-family": DEFAULT_FONT}, + selected_style={"font-family": DEFAULT_FONT}, + children=[ + table_daily, + html.Button("Save to Validated", id="daily-save-button"), + html.Button("Reset Validated", id="daily-reset-button"), + html.Div( + children=["Open detailed view:"], + style={"font-family": DEFAULT_FONT}, + ), + dcc.Input( + id="input-daily-id", + type="number", + debounce=True, + placeholder="ID", + ), + html.Div( + id="daily-status-message", + children=[""], + style={"font-family": DEFAULT_FONT}, + ), + ], + ), + dcc.Tab( + label="Detail of Selected Day", + id="tab-detail", + disabled=True, + style={"font-family": DEFAULT_FONT}, + selected_style={"font-family": DEFAULT_FONT}, + disabled_style={"font-family": DEFAULT_FONT}, + children=[ + table_detail, + html.Button("Add row", id="detail-add-button"), + html.Button("Save to Validated", id="detail-save-button"), + html.Button("Reset Validated", id="detail-reset-button"), + html.Div( + id="detail-status-message", + children=[""], + style={"font-family": DEFAULT_FONT}, + ), + ], + ), + ], ), html.H1( children="Plot", @@ -196,6 +211,7 @@ def create_validation_plot(data: dict) -> go.Figure: Output("table_detail", "scrollTo"), Output("table_daily", "selectedRows"), Output("table_detail", "selectedRows"), + Output("tab-detail", "disabled"), ], [ Input("daily-save-button", "n_clicks"), @@ -234,6 +250,7 @@ def callbacks( dict, list[dict], list[dict], + bool, ]: """Callback for buttons adding and resetting Validated data @@ -249,7 +266,7 @@ def callbacks( in_detail_row_data (list[dict]): Full row data for table_detail Returns: - tuple[str, str, go.Figure, list[dict], list[dict], dict, dict, list[dict], list[dict]]: + tuple[str, str, go.Figure, list[dict], list[dict], dict, dict, list[dict], list[dict], bool]: Callback outputs """ global DATA_DAILY, DATA_DETAIL, SELECTED_DAY @@ -266,6 +283,7 @@ def callbacks( 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 daily_refresh_required = False detail_refresh_required = False @@ -354,6 +372,7 @@ def callbacks( ) if SELECTED_DAY != dash.no_update: detail_refresh_required = True + out_tab_detail_disabled = False else: out_daily_status = "Invalid ID" @@ -398,4 +417,5 @@ def callbacks( out_detail_scroll, out_daily_selected_rows, out_detail_selected_rows, + out_tab_detail_disabled, ) From 985dc1e288fa4c1e7d410b6facb88aa86f30b96d Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 6 Feb 2024 16:25:53 +0000 Subject: [PATCH 28/42] Add date to tab title --- validated/dash_apps/finished_apps/daily_validation.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index fe7eda7d..7a072fc1 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -212,6 +212,7 @@ def create_validation_plot(data: dict) -> go.Figure: Output("table_daily", "selectedRows"), Output("table_detail", "selectedRows"), Output("tab-detail", "disabled"), + Output("tab-detail", "label"), ], [ Input("daily-save-button", "n_clicks"), @@ -266,7 +267,7 @@ def callbacks( in_detail_row_data (list[dict]): Full row data for table_detail Returns: - tuple[str, str, go.Figure, list[dict], list[dict], dict, dict, list[dict], list[dict], bool]: + tuple[str, str, go.Figure, list[dict], list[dict], dict, dict, list[dict], list[dict], bool, str]: Callback outputs """ global DATA_DAILY, DATA_DETAIL, SELECTED_DAY @@ -284,6 +285,7 @@ def callbacks( 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 daily_refresh_required = False detail_refresh_required = False @@ -373,6 +375,9 @@ def callbacks( if SELECTED_DAY != dash.no_update: detail_refresh_required = True out_tab_detail_disabled = False + out_tab_detail_label = ( + f"Detail of Selected Day ({SELECTED_DAY.strftime('%Y-%m-%d')})" + ) else: out_daily_status = "Invalid ID" @@ -418,4 +423,5 @@ def callbacks( out_daily_selected_rows, out_detail_selected_rows, out_tab_detail_disabled, + out_tab_detail_label, ) From c181ff8bfa9bced4d91ab2a835636e7c78e82e25 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 6 Feb 2024 16:46:56 +0000 Subject: [PATCH 29/42] Switch tabs on callback --- .../dash_apps/finished_apps/daily_validation.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 7a072fc1..86021a62 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -143,10 +143,12 @@ def create_validation_plot(data: dict) -> go.Figure: children=[ dcc.Tabs( id="tabs", + value="tab-daily", children=[ dcc.Tab( label="Daily Report", id="tab-daily", + value="tab-daily", style={"font-family": DEFAULT_FONT}, selected_style={"font-family": DEFAULT_FONT}, children=[ @@ -173,6 +175,7 @@ def create_validation_plot(data: dict) -> go.Figure: dcc.Tab( label="Detail of Selected Day", id="tab-detail", + value="tab-detail", disabled=True, style={"font-family": DEFAULT_FONT}, selected_style={"font-family": DEFAULT_FONT}, @@ -191,10 +194,6 @@ def create_validation_plot(data: dict) -> go.Figure: ), ], ), - html.H1( - children="Plot", - style={"font-family": DEFAULT_FONT}, - ), dcc.Graph(id="plot", figure=plot), ] ) @@ -213,6 +212,7 @@ def create_validation_plot(data: dict) -> go.Figure: Output("table_detail", "selectedRows"), Output("tab-detail", "disabled"), Output("tab-detail", "label"), + Output("tabs", "value"), ], [ Input("daily-save-button", "n_clicks"), @@ -267,7 +267,7 @@ def callbacks( in_detail_row_data (list[dict]): Full row data for table_detail Returns: - tuple[str, str, go.Figure, list[dict], list[dict], dict, dict, list[dict], list[dict], bool, str]: + tuple[str, str, go.Figure, list[dict], list[dict], dict, dict, list[dict], list[dict], bool, str, str]: Callback outputs """ global DATA_DAILY, DATA_DETAIL, SELECTED_DAY @@ -286,6 +286,7 @@ def callbacks( 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 daily_refresh_required = False detail_refresh_required = False @@ -378,6 +379,7 @@ def callbacks( out_tab_detail_label = ( f"Detail of Selected Day ({SELECTED_DAY.strftime('%Y-%m-%d')})" ) + out_tabs_value = "tab-detail" else: out_daily_status = "Invalid ID" @@ -424,4 +426,5 @@ def callbacks( out_detail_selected_rows, out_tab_detail_disabled, out_tab_detail_label, + out_tabs_value, ) From 4479b66a93a299f792157a81788043818dcd0469 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 7 Feb 2024 09:57:46 +0000 Subject: [PATCH 30/42] Combine status messages --- .../finished_apps/daily_validation.py | 40 ++++++++----------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 86021a62..9b870e8f 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -165,11 +165,6 @@ def create_validation_plot(data: dict) -> go.Figure: debounce=True, placeholder="ID", ), - html.Div( - id="daily-status-message", - children=[""], - style={"font-family": DEFAULT_FONT}, - ), ], ), dcc.Tab( @@ -185,15 +180,15 @@ def create_validation_plot(data: dict) -> go.Figure: html.Button("Add row", id="detail-add-button"), html.Button("Save to Validated", id="detail-save-button"), html.Button("Reset Validated", id="detail-reset-button"), - html.Div( - id="detail-status-message", - children=[""], - style={"font-family": DEFAULT_FONT}, - ), ], ), ], ), + html.Div( + id="status-message", + children=[""], + style={"font-family": DEFAULT_FONT}, + ), dcc.Graph(id="plot", figure=plot), ] ) @@ -201,8 +196,7 @@ def create_validation_plot(data: dict) -> go.Figure: @app.callback( [ - Output("daily-status-message", "children"), - Output("detail-status-message", "children"), + Output("status-message", "children"), Output("plot", "figure"), Output("table_daily", "rowData"), Output("table_detail", "rowData"), @@ -242,7 +236,6 @@ def callbacks( in_detail_selected_rows: list[dict], in_detail_row_data: list[dict], ) -> tuple[ - str, str, go.Figure, list[dict], @@ -252,6 +245,8 @@ def callbacks( list[dict], list[dict], bool, + str, + str, ]: """Callback for buttons adding and resetting Validated data @@ -267,7 +262,7 @@ def callbacks( in_detail_row_data (list[dict]): Full row data for table_detail Returns: - tuple[str, str, go.Figure, list[dict], list[dict], dict, dict, list[dict], list[dict], bool, str, str]: + tuple[str, go.Figure, list[dict], list[dict], dict, dict, list[dict], list[dict], bool, str, str]: Callback outputs """ global DATA_DAILY, DATA_DETAIL, SELECTED_DAY @@ -275,8 +270,7 @@ def callbacks( ctx = dash.callback_context button_id = ctx.triggered[0]["prop_id"].split(".")[0] - out_daily_status = dash.no_update - out_detail_status = 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 @@ -309,7 +303,7 @@ def callbacks( minimum=MINIMUM, maximum=MAXIMUM, ) - out_daily_status = f"{len(in_daily_selected_rows)} days saved to Validated" + out_status = f"{len(in_daily_selected_rows)} days saved to Validated" plot_refresh_required = True daily_refresh_required = True @@ -321,7 +315,7 @@ def callbacks( start_date=START_DATE, end_date=END_DATE, ) - out_daily_status = "Validation reset" + out_status = "Validation reset" plot_refresh_required = True daily_refresh_required = True daily_reset_selection = True @@ -336,7 +330,7 @@ def callbacks( variable=VARIABLE, station=STATION, ) - out_detail_status = f"{len(in_detail_selected_rows)} entries saved to Validated" + out_status = f"{len(in_detail_selected_rows)} entries saved to Validated" plot_refresh_required = True detail_refresh_required = True @@ -347,7 +341,7 @@ def callbacks( variable=VARIABLE, station=STATION, ) - out_detail_status = "Validation reset" + out_status = "Validation reset" plot_refresh_required = True detail_refresh_required = True detail_reset_selection = True @@ -380,8 +374,9 @@ def callbacks( f"Detail of Selected Day ({SELECTED_DAY.strftime('%Y-%m-%d')})" ) out_tabs_value = "tab-detail" + out_status = "" else: - out_daily_status = "Invalid ID" + out_status = "Invalid ID" # Refresh plot if plot_refresh_required: @@ -415,8 +410,7 @@ def callbacks( out_detail_selected_rows = out_detail_row_data return ( - out_daily_status, - out_detail_status, + out_status, out_plot, out_daily_row_data, out_detail_row_data, From f88465c904f8bea2a6933636089c8966947c3f2c Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 7 Feb 2024 10:23:27 +0000 Subject: [PATCH 31/42] Menu layout --- .../finished_apps/daily_validation.py | 56 ++++++++++++++----- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 9b870e8f..123c3d9c 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -103,6 +103,45 @@ ) +# Menus +menu_daily = html.Div( + children=[ + html.Div( + children=[ + html.Button("Save to Validated", id="daily-save-button"), + html.Button("Reset Validated", id="daily-reset-button"), + ], + style={"display": "inline-block", "width": "50%"}, + ), + html.Div( + children=[ + html.Div( + children=["Open detailed view "], + style={"display": "inline-block", "font-family": DEFAULT_FONT}, + ), + dcc.Input( + id="input-daily-id", + type="number", + debounce=True, + placeholder="ID", + ), + ], + style={"display": "inline-block", "width": "50%", "text-align": "right"}, + ), + ], + style={"width": "100%"}, +) + +menu_detail = html.Div( + children=[ + html.Button("Save to Validated", id="detail-save-button"), + html.Button("Reset Validated", id="detail-reset-button"), + html.Button("Add row", id="detail-add-button"), + ], + style={"display": "inline-block"}, +) + + # Plot def create_validation_plot(data: dict) -> go.Figure: """Creates plot for Validation app @@ -153,18 +192,7 @@ def create_validation_plot(data: dict) -> go.Figure: selected_style={"font-family": DEFAULT_FONT}, children=[ table_daily, - html.Button("Save to Validated", id="daily-save-button"), - html.Button("Reset Validated", id="daily-reset-button"), - html.Div( - children=["Open detailed view:"], - style={"font-family": DEFAULT_FONT}, - ), - dcc.Input( - id="input-daily-id", - type="number", - debounce=True, - placeholder="ID", - ), + menu_daily, ], ), dcc.Tab( @@ -177,9 +205,7 @@ def create_validation_plot(data: dict) -> go.Figure: disabled_style={"font-family": DEFAULT_FONT}, children=[ table_detail, - html.Button("Add row", id="detail-add-button"), - html.Button("Save to Validated", id="detail-save-button"), - html.Button("Reset Validated", id="detail-reset-button"), + menu_detail, ], ), ], From 5d19ac0dd732bdfe5ad9e5278a59294116720b61 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 7 Feb 2024 11:18:37 +0000 Subject: [PATCH 32/42] Layout --- .../finished_apps/daily_validation.py | 74 ++++++++++++++++--- 1 file changed, 62 insertions(+), 12 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 123c3d9c..45fffa8e 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -116,8 +116,13 @@ html.Div( children=[ html.Div( - children=["Open detailed view "], - style={"display": "inline-block", "font-family": DEFAULT_FONT}, + children=["Open detailed view"], + style={ + "display": "inline-block", + "font-family": DEFAULT_FONT, + "padding-right": "5px", + "font-size": "14px", + }, ), dcc.Input( id="input-daily-id", @@ -129,16 +134,39 @@ style={"display": "inline-block", "width": "50%", "text-align": "right"}, ), ], - style={"width": "100%"}, + style={ + "background-color": "#f0f0f0", + "width": "100%", + }, ) menu_detail = html.Div( children=[ - html.Button("Save to Validated", id="detail-save-button"), - html.Button("Reset Validated", id="detail-reset-button"), - html.Button("Add row", id="detail-add-button"), + html.Div( + children=[ + html.Button("Save to Validated", id="detail-save-button"), + html.Button("Reset Validated", id="detail-reset-button"), + ], + style={ + "display": "inline-block", + "width": "50%", + }, + ), + html.Div( + children=[ + html.Button("Add row", id="detail-add-button"), + ], + style={ + "display": "inline-block", + "text-align": "right", + "width": "50%", + }, + ), ], - style={"display": "inline-block"}, + style={ + "background-color": "#f0f0f0", + "width": "100%", + }, ) @@ -152,7 +180,7 @@ def create_validation_plot(data: dict) -> go.Figure: Returns: go.Figure: Plot """ - plot = go.Figure() + fig = go.Figure() datasets = [ {"key": "measurement", "name": "Measurement", "color": "black"}, @@ -161,7 +189,7 @@ def create_validation_plot(data: dict) -> go.Figure: ] for dataset in datasets: - plot.add_trace( + fig.add_trace( go.Scatter( x=data[dataset["key"]]["time"], y=data[dataset["key"]]["average"], @@ -172,7 +200,24 @@ def create_validation_plot(data: dict) -> go.Figure: ) ) - return plot + fig.update_yaxes(title_text=f"{str(VARIABLE)} (Average)") + 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 plot = create_validation_plot(data=DATA_DAILY["series"]) @@ -183,6 +228,7 @@ def create_validation_plot(data: dict) -> go.Figure: dcc.Tabs( id="tabs", value="tab-daily", + style={"width": "100%"}, children=[ dcc.Tab( label="Daily Report", @@ -213,9 +259,13 @@ def create_validation_plot(data: dict) -> go.Figure: html.Div( id="status-message", children=[""], - style={"font-family": DEFAULT_FONT}, + style={ + "font-family": DEFAULT_FONT, + "font-size": "14px", + "min-height": "14px", + }, ), - dcc.Graph(id="plot", figure=plot), + dcc.Graph(id="plot", figure=plot, style={"width": "100%"}), ] ) From 171ee7a37385d8fb08123f06b1ec449c1b015fa8 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 7 Feb 2024 13:36:49 +0000 Subject: [PATCH 33/42] Improved plots and logic --- .../finished_apps/daily_validation.py | 145 +++++++++--------- validated/plots.py | 56 +++++++ validated/tables.py | 2 +- 3 files changed, 127 insertions(+), 76 deletions(-) create mode 100644 validated/plots.py diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 45fffa8e..9baa6c6c 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -17,6 +17,7 @@ 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 @@ -33,6 +34,7 @@ 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( @@ -171,56 +173,18 @@ # Plot -def create_validation_plot(data: dict) -> go.Figure: - """Creates plot for Validation app +plot = create_validation_plot(data=DATA_DAILY, plot_type=PLOT_TYPE) - Args: - data (dict): Daily data series - - Returns: - go.Figure: Plot - """ - 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[dataset["key"]]["time"], - y=data[dataset["key"]]["average"], - name=dataset["name"], - line=dict(color=dataset["color"]), - mode="markers", - marker_size=3, - ) - ) - - fig.update_yaxes(title_text=f"{str(VARIABLE)} (Average)") - 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 - - -plot = create_validation_plot(data=DATA_DAILY["series"]) +# 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-family": DEFAULT_FONT, "font-size": "14px"}, +) # Layout app.layout = html.Div( @@ -262,9 +226,12 @@ def create_validation_plot(data: dict) -> go.Figure: style={ "font-family": DEFAULT_FONT, "font-size": "14px", - "min-height": "14px", + "min-height": "20px", + "padding-top": "5px", + "padding-bottom": "10px", }, ), + plot_radio, dcc.Graph(id="plot", figure=plot, style={"width": "100%"}), ] ) @@ -291,6 +258,7 @@ def create_validation_plot(data: dict) -> go.Figure: Input("detail-reset-button", "n_clicks"), Input("detail-add-button", "n_clicks"), Input("input-daily-id", "value"), + Input("plot_radio", "value"), ], [ State("table_daily", "selectedRows"), @@ -307,6 +275,7 @@ def callbacks( detail_reset_clicks: int, detail_add_clicks: int, daily_id: int, + plot_radio_value: str, in_daily_selected_rows: list[dict], in_daily_row_data: list[dict], in_detail_selected_rows: list[dict], @@ -333,6 +302,7 @@ def callbacks( detail_reset_clicks (int): Number of times detail-reset-button was clicked detail_add_clicks (int): Number of times detail-add-button was clicked daily_id (int): ID of selected day + plot_radio_value (str): Value of plot radio button in_daily_selected_rows (list[dict]): Selected rows in 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 @@ -341,7 +311,7 @@ def callbacks( tuple[str, go.Figure, list[dict], list[dict], dict, dict, list[dict], list[dict], bool, str, str]: Callback outputs """ - global DATA_DAILY, DATA_DETAIL, SELECTED_DAY + global DATA_DAILY, DATA_DETAIL, SELECTED_DAY, PLOT_TYPE ctx = dash.callback_context button_id = ctx.triggered[0]["prop_id"].split(".")[0] @@ -358,10 +328,12 @@ def callbacks( out_tab_detail_label = dash.no_update out_tabs_value = dash.no_update - daily_refresh_required = False - detail_refresh_required = False - daily_reset_selection = False - detail_reset_selection = False + 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: Daily save @@ -380,8 +352,9 @@ def callbacks( 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 - daily_refresh_required = True # Button: Daily reset elif button_id == "daily-reset-button": @@ -392,9 +365,10 @@ def callbacks( 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 - daily_refresh_required = True - daily_reset_selection = True # Button: Detail save elif button_id == "detail-save-button": @@ -407,8 +381,11 @@ def callbacks( 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 - detail_refresh_required = True # Button: Detail reset elif button_id == "detail-reset-button": @@ -418,9 +395,12 @@ def callbacks( 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 - detail_refresh_required = True - detail_reset_selection = True # Button: Detail new row elif button_id == "detail-add-button": @@ -437,14 +417,14 @@ def callbacks( out_detail_row_transaction = {"add": [new_row]} out_detail_scroll = {"data": new_row} - # Input: Daily date + # Input: Daily ID elif button_id == "input-daily-id": SELECTED_DAY = next( (d["date"] for d in DATA_DAILY["data"] if d["id"] == daily_id), dash.no_update, ) if SELECTED_DAY != dash.no_update: - detail_refresh_required = True + detail_table_refresh_required = True out_tab_detail_disabled = False out_tab_detail_label = ( f"Detail of Selected Day ({SELECTED_DAY.strftime('%Y-%m-%d')})" @@ -454,8 +434,13 @@ def callbacks( else: out_status = "Invalid ID" - # Refresh plot - if plot_refresh_required: + # Plot radio + elif button_id == "plot_radio": + PLOT_TYPE = plot_radio_value + plot_refresh_required = True + + # Reload daily data + if daily_data_refresh_required: DATA_DAILY = daily_validation( station=STATION, variable=VARIABLE, @@ -464,16 +449,9 @@ def callbacks( minimum=MINIMUM, maximum=MAXIMUM, ) - out_plot = create_validation_plot(DATA_DAILY["series"]) - # Refresh daily table - if daily_refresh_required: - out_daily_row_data = DATA_DAILY["data"] - if daily_reset_selection: - out_daily_selected_rows = out_daily_row_data - - # Refresh detail table - if detail_refresh_required: + # Reload detail data + if detail_data_refresh_required: DATA_DETAIL = detail_list( station=STATION, variable=VARIABLE, @@ -481,8 +459,25 @@ def callbacks( 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"] - if detail_reset_selection: + + # Reset detail table selection + if detail_table_reset_selection: out_detail_selected_rows = out_detail_row_data return ( 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 index 9e464a1c..241eb53e 100644 --- a/validated/tables.py +++ b/validated/tables.py @@ -106,7 +106,7 @@ def create_columns_detail(value_columns: list) -> list: columns += [ { "field": c, - "headerName": c[0].upper() + c[1:], + "headerName": c.capitalize(), "filter": "agNumberColumnFilter", "editable": True, **styles[c], From 3c9350ea4448afdce2c7126cbdf50fe871de4287 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 7 Feb 2024 14:55:34 +0000 Subject: [PATCH 34/42] Bug fix --- validated/dash_apps/finished_apps/daily_validation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 9baa6c6c..a28b3a27 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -424,6 +424,7 @@ def callbacks( dash.no_update, ) if SELECTED_DAY != dash.no_update: + detail_data_refresh_required = True detail_table_refresh_required = True out_tab_detail_disabled = False out_tab_detail_label = ( From 4df05cb2c1f07239763437643657cdd8c648b9e6 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 8 Feb 2024 10:47:22 +0000 Subject: [PATCH 35/42] Add loading component --- validated/dash_apps/finished_apps/daily_validation.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index a28b3a27..d558a435 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -231,6 +231,10 @@ "padding-bottom": "10px", }, ), + dcc.Loading( + type="dot", + children=html.Div(id="loading"), + ), plot_radio, dcc.Graph(id="plot", figure=plot, style={"width": "100%"}), ] @@ -239,6 +243,7 @@ @app.callback( [ + Output("loading", "children"), Output("status-message", "children"), Output("plot", "figure"), Output("table_daily", "rowData"), @@ -281,6 +286,7 @@ def callbacks( in_detail_selected_rows: list[dict], in_detail_row_data: list[dict], ) -> tuple[ + str, str, go.Figure, list[dict], @@ -308,7 +314,7 @@ def callbacks( in_detail_row_data (list[dict]): Full row data for table_detail Returns: - tuple[str, go.Figure, list[dict], list[dict], dict, dict, list[dict], list[dict], bool, str, str]: + tuple[str, str, go.Figure, list[dict], list[dict], dict, dict, list[dict], list[dict], bool, str, str]: Callback outputs """ global DATA_DAILY, DATA_DETAIL, SELECTED_DAY, PLOT_TYPE @@ -316,6 +322,7 @@ def callbacks( ctx = dash.callback_context button_id = ctx.triggered[0]["prop_id"].split(".")[0] + out_loading = dash.no_update out_status = dash.no_update out_plot = dash.no_update out_daily_row_data = dash.no_update @@ -482,6 +489,7 @@ def callbacks( out_detail_selected_rows = out_detail_row_data return ( + out_loading, out_status, out_plot, out_daily_row_data, From d716329e74ada9753ad71bb0bab7823f7ea8b2ef Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 8 Feb 2024 11:45:24 +0000 Subject: [PATCH 36/42] Add date picker --- .../finished_apps/daily_validation.py | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index d558a435..26444937 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -126,11 +126,12 @@ "font-size": "14px", }, ), - dcc.Input( - id="input-daily-id", - type="number", - debounce=True, - placeholder="ID", + 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={"font-family": DEFAULT_FONT}, ), ], style={"display": "inline-block", "width": "50%", "text-align": "right"}, @@ -262,7 +263,7 @@ Input("detail-save-button", "n_clicks"), Input("detail-reset-button", "n_clicks"), Input("detail-add-button", "n_clicks"), - Input("input-daily-id", "value"), + Input("detail-date-picker", "date"), Input("plot_radio", "value"), ], [ @@ -279,7 +280,7 @@ def callbacks( detail_save_clicks: int, detail_reset_clicks: int, detail_add_clicks: int, - daily_id: int, + detail_date: datetime.date, plot_radio_value: str, in_daily_selected_rows: list[dict], in_daily_row_data: list[dict], @@ -424,13 +425,18 @@ def callbacks( out_detail_row_transaction = {"add": [new_row]} out_detail_scroll = {"data": new_row} - # Input: Daily ID - elif button_id == "input-daily-id": - SELECTED_DAY = next( - (d["date"] for d in DATA_DAILY["data"] if d["id"] == daily_id), - dash.no_update, + # Date picker + elif button_id == "detail-date-picker": + new_selected_day = next( + ( + d["date"] + for d in DATA_DAILY["data"] + if d["date"].strftime("%Y-%m-%d") == detail_date + ), + None, ) - if SELECTED_DAY != dash.no_update: + if new_selected_day is not None: + SELECTED_DAY = new_selected_day detail_data_refresh_required = True detail_table_refresh_required = True out_tab_detail_disabled = False From d3959cebdb35c813f2e38a1cae2a5f731ef99466 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 8 Feb 2024 13:44:07 +0000 Subject: [PATCH 37/42] Move menu bar out of tabs. Make date picker always available --- .../finished_apps/daily_validation.py | 174 ++++++++---------- 1 file changed, 81 insertions(+), 93 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 26444937..34c161a7 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -105,66 +105,41 @@ ) -# Menus -menu_daily = html.Div( +# Date picker +detail_date_picker = html.Div( children=[ html.Div( - children=[ - html.Button("Save to Validated", id="daily-save-button"), - html.Button("Reset Validated", id="daily-reset-button"), - ], - style={"display": "inline-block", "width": "50%"}, + children=["Open detailed view"], + style={ + "display": "inline-block", + "font-family": DEFAULT_FONT, + "padding-right": "5px", + "font-size": "14px", + }, ), - html.Div( - children=[ - html.Div( - children=["Open detailed view"], - style={ - "display": "inline-block", - "font-family": DEFAULT_FONT, - "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={"font-family": DEFAULT_FONT}, - ), - ], - style={"display": "inline-block", "width": "50%", "text-align": "right"}, + 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={"font-family": DEFAULT_FONT}, ), ], - style={ - "background-color": "#f0f0f0", - "width": "100%", - }, + style={"display": "inline-block", "width": "50%", "text-align": "right"}, ) -menu_detail = html.Div( +# Menu +menu = html.Div( children=[ html.Div( children=[ - html.Button("Save to Validated", id="detail-save-button"), - html.Button("Reset Validated", id="detail-reset-button"), - ], - style={ - "display": "inline-block", - "width": "50%", - }, - ), - html.Div( - children=[ - html.Button("Add row", id="detail-add-button"), + html.Button("Save to Validated", id="save-button"), + html.Button("Reset Validated", id="reset-button"), + html.Button("Add row", id="add-button", style={"display": "none"}), ], - style={ - "display": "inline-block", - "text-align": "right", - "width": "50%", - }, + style={"display": "inline-block", "width": "50%"}, ), + detail_date_picker, ], style={ "background-color": "#f0f0f0", @@ -172,6 +147,20 @@ }, ) +# Status message +status_message = ( + html.Div( + id="status-message", + children=[""], + style={ + "font-family": DEFAULT_FONT, + "font-size": "14px", + "min-height": "20px", + "padding-top": "5px", + "padding-bottom": "10px", + }, + ), +) # Plot plot = create_validation_plot(data=DATA_DAILY, plot_type=PLOT_TYPE) @@ -203,7 +192,6 @@ selected_style={"font-family": DEFAULT_FONT}, children=[ table_daily, - menu_daily, ], ), dcc.Tab( @@ -216,22 +204,12 @@ disabled_style={"font-family": DEFAULT_FONT}, children=[ table_detail, - menu_detail, ], ), ], ), - html.Div( - id="status-message", - children=[""], - style={ - "font-family": DEFAULT_FONT, - "font-size": "14px", - "min-height": "20px", - "padding-top": "5px", - "padding-bottom": "10px", - }, - ), + menu, + status_message, dcc.Loading( type="dot", children=html.Div(id="loading"), @@ -256,15 +234,15 @@ Output("tab-detail", "disabled"), Output("tab-detail", "label"), Output("tabs", "value"), + Output("add-button", "style"), ], [ - Input("daily-save-button", "n_clicks"), - Input("daily-reset-button", "n_clicks"), - Input("detail-save-button", "n_clicks"), - Input("detail-reset-button", "n_clicks"), - Input("detail-add-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("table_daily", "selectedRows"), @@ -275,13 +253,12 @@ prevent_initial_call=True, ) def callbacks( - daily_save_clicks: int, - daily_reset_clicks: int, - detail_save_clicks: int, - detail_reset_clicks: int, - detail_add_clicks: int, + save_clicks: int, + reset_clicks: int, + add_clicks: int, detail_date: datetime.date, plot_radio_value: str, + tabs_value: str, in_daily_selected_rows: list[dict], in_daily_row_data: list[dict], in_detail_selected_rows: list[dict], @@ -335,6 +312,7 @@ def callbacks( out_tab_detail_disabled = dash.no_update out_tab_detail_label = dash.no_update out_tabs_value = dash.no_update + out_add_button_style = dash.no_update daily_data_refresh_required = False detail_data_refresh_required = False @@ -344,8 +322,8 @@ def callbacks( detail_table_reset_selection = False plot_refresh_required = False - # Button: Daily save - if button_id == "daily-save-button": + # Button: Save (daily) + if button_id == "save-button" and 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 @@ -364,22 +342,8 @@ def callbacks( daily_table_refresh_required = True plot_refresh_required = True - # Button: Daily reset - elif button_id == "daily-reset-button": - 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: Detail save - elif button_id == "detail-save-button": + # Button: Save (detail) + elif button_id == "save-button" and 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 @@ -395,8 +359,22 @@ def callbacks( detail_table_refresh_required = True plot_refresh_required = True - # Button: Detail reset - elif button_id == "detail-reset-button": + # Button: Reset (daily) + elif button_id == "reset-button" and 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 button_id == "reset-button" and tabs_value == "tab-detail": reset_detail_validated( data_list=in_detail_row_data, variable=VARIABLE, @@ -410,8 +388,8 @@ def callbacks( detail_table_reset_selection = True plot_refresh_required = True - # Button: Detail new row - elif button_id == "detail-add-button": + # Button: New row (detail) + elif button_id == "add-button" and tabs_value == "tab-detail": last_id = in_detail_row_data[-1]["id"] last_time = in_detail_row_data[-1]["time"] new_row = { @@ -439,11 +417,13 @@ def callbacks( 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_style = {"display": "inline-block"} out_status = "" else: out_status = "Invalid ID" @@ -453,6 +433,13 @@ def callbacks( PLOT_TYPE = plot_radio_value plot_refresh_required = True + # Switching tabs + elif button_id == "tabs": + if tabs_value == "tab-detail": + out_add_button_style = {"display": "inline-block"} + else: + out_add_button_style = {"display": "none"} + # Reload daily data if daily_data_refresh_required: DATA_DAILY = daily_validation( @@ -507,4 +494,5 @@ def callbacks( out_tab_detail_disabled, out_tab_detail_label, out_tabs_value, + out_add_button_style, ) From 21da4926443ddad69cfb84c6b358d4967c749d99 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 8 Feb 2024 16:15:58 +0000 Subject: [PATCH 38/42] Change dissabling type for Add button, fix bug --- .../finished_apps/daily_validation.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 34c161a7..66059228 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -135,7 +135,7 @@ children=[ html.Button("Save to Validated", id="save-button"), html.Button("Reset Validated", id="reset-button"), - html.Button("Add row", id="add-button", style={"display": "none"}), + html.Button("Add row", id="add-button", disabled=True), ], style={"display": "inline-block", "width": "50%"}, ), @@ -148,20 +148,19 @@ ) # Status message -status_message = ( - html.Div( - id="status-message", - children=[""], - style={ - "font-family": DEFAULT_FONT, - "font-size": "14px", - "min-height": "20px", - "padding-top": "5px", - "padding-bottom": "10px", - }, - ), +status_message = html.Div( + id="status-message", + children=[""], + style={ + "font-family": DEFAULT_FONT, + "font-size": "14px", + "min-height": "20px", + "padding-top": "5px", + "padding-bottom": "10px", + }, ) + # Plot plot = create_validation_plot(data=DATA_DAILY, plot_type=PLOT_TYPE) @@ -234,7 +233,7 @@ Output("tab-detail", "disabled"), Output("tab-detail", "label"), Output("tabs", "value"), - Output("add-button", "style"), + Output("add-button", "disabled"), ], [ Input("save-button", "n_clicks"), @@ -276,6 +275,7 @@ def callbacks( bool, str, str, + bool, ]: """Callback for buttons adding and resetting Validated data @@ -312,7 +312,7 @@ def callbacks( out_tab_detail_disabled = dash.no_update out_tab_detail_label = dash.no_update out_tabs_value = dash.no_update - out_add_button_style = dash.no_update + out_add_button_disabled = dash.no_update daily_data_refresh_required = False detail_data_refresh_required = False @@ -423,7 +423,7 @@ def callbacks( f"Detail of Selected Day ({SELECTED_DAY.strftime('%Y-%m-%d')})" ) out_tabs_value = "tab-detail" - out_add_button_style = {"display": "inline-block"} + out_add_button_disabled = False out_status = "" else: out_status = "Invalid ID" @@ -436,9 +436,9 @@ def callbacks( # Switching tabs elif button_id == "tabs": if tabs_value == "tab-detail": - out_add_button_style = {"display": "inline-block"} + out_add_button_disabled = False else: - out_add_button_style = {"display": "none"} + out_add_button_disabled = True # Reload daily data if daily_data_refresh_required: @@ -494,5 +494,5 @@ def callbacks( out_tab_detail_disabled, out_tab_detail_label, out_tabs_value, - out_add_button_style, + out_add_button_disabled, ) From ab640fe6306853b4cd2b8c188b757ae655809ec2 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 9 Feb 2024 11:52:37 +0000 Subject: [PATCH 39/42] Preliminary working filters (without max/min) --- .../finished_apps/daily_validation.py | 98 ++++++++++++++----- 1 file changed, 76 insertions(+), 22 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 66059228..02165df2 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -25,8 +25,7 @@ app = DjangoDash("DailyValidation") -# Filters (in final app this will get data from forms) - +# 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") @@ -55,6 +54,36 @@ maximum=MAXIMUM, ) +# Filters +filters = html.Div( + children=[ + dcc.Dropdown( + id="station_drop", + options=[ + item.station_code for item in Station.objects.order_by("station_code") + ], + value=STATION.station_code, + ), + 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, + ), + 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"), + ), + # dcc.Input(id="minimum_input", type="number", value=MINIMUM), + # dcc.Input(id="maximum_input", type="number", value=MAXIMUM), + html.Button("Submit", id="submit-button"), + ] +) + # Tables table_daily = AgGrid( id="table_daily", @@ -128,7 +157,7 @@ style={"display": "inline-block", "width": "50%", "text-align": "right"}, ) -# Menu +# Table menu menu = html.Div( children=[ html.Div( @@ -178,6 +207,7 @@ # Layout app.layout = html.Div( children=[ + filters, dcc.Tabs( id="tabs", value="tab-daily", @@ -236,6 +266,7 @@ Output("add-button", "disabled"), ], [ + Input("submit-button", "n_clicks"), Input("save-button", "n_clicks"), Input("reset-button", "n_clicks"), Input("add-button", "n_clicks"), @@ -244,6 +275,10 @@ Input("tabs", "value"), ], [ + State("station_drop", "value"), + State("variable_drop", "value"), + State("date_range_picker", "start_date"), + State("date_range_picker", "end_date"), State("table_daily", "selectedRows"), State("table_daily", "rowData"), State("table_detail", "selectedRows"), @@ -252,12 +287,17 @@ prevent_initial_call=True, ) def callbacks( - save_clicks: int, - reset_clicks: int, - add_clicks: int, - detail_date: datetime.date, - plot_radio_value: str, - tabs_value: str, + 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_daily_selected_rows: list[dict], in_daily_row_data: list[dict], in_detail_selected_rows: list[dict], @@ -295,10 +335,10 @@ def callbacks( tuple[str, str, go.Figure, list[dict], list[dict], dict, dict, list[dict], list[dict], bool, str, str]: Callback outputs """ - global DATA_DAILY, DATA_DETAIL, SELECTED_DAY, PLOT_TYPE + global DATA_DAILY, DATA_DETAIL, STATION, VARIABLE, START_DATE, END_DATE, MINIMUM, MAXIMUM, SELECTED_DAY, PLOT_TYPE ctx = dash.callback_context - button_id = ctx.triggered[0]["prop_id"].split(".")[0] + input_id = ctx.triggered[0]["prop_id"].split(".")[0] out_loading = dash.no_update out_status = dash.no_update @@ -322,8 +362,22 @@ def callbacks( 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") + 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 button_id == "save-button" and tabs_value == "tab-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 @@ -343,7 +397,7 @@ def callbacks( plot_refresh_required = True # Button: Save (detail) - elif button_id == "save-button" and tabs_value == "tab-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 @@ -360,7 +414,7 @@ def callbacks( plot_refresh_required = True # Button: Reset (daily) - elif button_id == "reset-button" and tabs_value == "tab-daily": + elif input_id == "reset-button" and in_tabs_value == "tab-daily": reset_daily_validated( variable=VARIABLE, station=STATION, @@ -374,7 +428,7 @@ def callbacks( plot_refresh_required = True # Button: Reset (detail) - elif button_id == "reset-button" and tabs_value == "tab-detail": + elif input_id == "reset-button" and in_tabs_value == "tab-detail": reset_detail_validated( data_list=in_detail_row_data, variable=VARIABLE, @@ -389,7 +443,7 @@ def callbacks( plot_refresh_required = True # Button: New row (detail) - elif button_id == "add-button" and tabs_value == "tab-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 = { @@ -404,12 +458,12 @@ def callbacks( out_detail_scroll = {"data": new_row} # Date picker - elif button_id == "detail-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") == detail_date + if d["date"].strftime("%Y-%m-%d") == in_detail_date ), None, ) @@ -429,13 +483,13 @@ def callbacks( out_status = "Invalid ID" # Plot radio - elif button_id == "plot_radio": - PLOT_TYPE = plot_radio_value + elif input_id == "plot_radio": + PLOT_TYPE = in_plot_radio_value plot_refresh_required = True # Switching tabs - elif button_id == "tabs": - if tabs_value == "tab-detail": + elif input_id == "tabs": + if in_tabs_value == "tab-detail": out_add_button_disabled = False else: out_add_button_disabled = True From 0bc4df08bf1b042124764abd0f10f76c72749907 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 9 Feb 2024 15:00:21 +0000 Subject: [PATCH 40/42] Add min/max fields, nicer looking --- .../finished_apps/daily_validation.py | 111 ++++++++++++++---- 1 file changed, 86 insertions(+), 25 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index 02165df2..a6d126e7 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -57,31 +57,67 @@ # Filters filters = html.Div( children=[ - dcc.Dropdown( - id="station_drop", - options=[ - item.station_code for item in Station.objects.order_by("station_code") + 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, + ), ], - value=STATION.station_code, + style={"margin-right": "10px", "width": "250px"}, ), - dcc.Dropdown( - id="variable_drop", - options=[ - {"label": item.name, "value": item.variable_code} - for item in Variable.objects.order_by("variable_code") + 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, + ), ], - value=VARIABLE.variable_code, + style={"margin-right": "10px", "width": "250px"}, ), - 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"), + 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"}, ), - # dcc.Input(id="minimum_input", type="number", value=MINIMUM), - # dcc.Input(id="maximum_input", type="number", value=MAXIMUM), - html.Button("Submit", id="submit-button"), - ] + 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-family": DEFAULT_FONT, + "font-size": "14px", + }, ) # Tables @@ -141,7 +177,6 @@ children=["Open detailed view"], style={ "display": "inline-block", - "font-family": DEFAULT_FONT, "padding-right": "5px", "font-size": "14px", }, @@ -151,10 +186,14 @@ display_format="YYYY-MM-DD", min_date_allowed=DATA_DAILY["data"][0]["date"], max_date_allowed=DATA_DAILY["data"][-1]["date"], - style={"font-family": DEFAULT_FONT}, ), ], - style={"display": "inline-block", "width": "50%", "text-align": "right"}, + style={ + "display": "inline-block", + "width": "50%", + "text-align": "right", + "font-family": DEFAULT_FONT, + }, ) # Table menu @@ -164,7 +203,12 @@ 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), + html.Button( + "Add row", + id="add-button", + disabled=True, + style={"margin-left": "10px"}, + ), ], style={"display": "inline-block", "width": "50%"}, ), @@ -208,6 +252,12 @@ app.layout = html.Div( children=[ filters, + html.Button("Submit", id="submit-button"), + dcc.Loading( + type="dot", + children=html.Div(id="loading_top"), + ), + html.Hr(), dcc.Tabs( id="tabs", value="tab-daily", @@ -243,6 +293,7 @@ type="dot", children=html.Div(id="loading"), ), + html.Hr(), plot_radio, dcc.Graph(id="plot", figure=plot, style={"width": "100%"}), ] @@ -251,6 +302,7 @@ @app.callback( [ + Output("loading_top", "children"), Output("loading", "children"), Output("status-message", "children"), Output("plot", "figure"), @@ -279,6 +331,8 @@ 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"), @@ -298,12 +352,15 @@ def callbacks( 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[ - str, + dash.no_update, + dash.no_update, str, go.Figure, list[dict], @@ -340,6 +397,7 @@ def callbacks( 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 @@ -368,6 +426,8 @@ def callbacks( 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) + MAXIMUM = Decimal(in_maximum) daily_data_refresh_required = True detail_data_refresh_required = True daily_table_refresh_required = True @@ -536,6 +596,7 @@ def callbacks( out_detail_selected_rows = out_detail_row_data return ( + out_loading_top, out_loading, out_status, out_plot, From ce7d41330bb672d17d421532b6ca1c04bb591b27 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 9 Feb 2024 15:20:59 +0000 Subject: [PATCH 41/42] Doctring, support empty min/max --- .../finished_apps/daily_validation.py | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/validated/dash_apps/finished_apps/daily_validation.py b/validated/dash_apps/finished_apps/daily_validation.py index a6d126e7..a705296d 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -374,23 +374,29 @@ def callbacks( str, bool, ]: - """Callback for buttons adding and resetting Validated data + """Callbacks for daily validation app Args: - daily_save_clicks (int): Number of times daily-save-button was clicked - daily_reset_clicks (int): Number of times daily-reset-button was clicked - detail_save_clicks (int): Number of times detail-save-button was clicked - detail_reset_clicks (int): Number of times detail-reset-button was clicked - detail_add_clicks (int): Number of times detail-add-button was clicked - daily_id (int): ID of selected day - plot_radio_value (str): Value of plot radio button + 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[str, str, go.Figure, list[dict], list[dict], dict, dict, list[dict], list[dict], bool, str, str]: - Callback outputs + 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 @@ -426,8 +432,9 @@ def callbacks( 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) - MAXIMUM = Decimal(in_maximum) + 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 From 0d2dd68fdb2276794971beeb091194f1ca024c55 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 9 Feb 2024 16:28:53 +0000 Subject: [PATCH 42/42] Add stylesheet --- static/styles/dashstyle.css | 15 +++++++++++++++ .../finished_apps/daily_validation.py | 18 +++++------------- 2 files changed, 20 insertions(+), 13 deletions(-) create mode 100644 static/styles/dashstyle.css 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 a705296d..721762db 100644 --- a/validated/dash_apps/finished_apps/daily_validation.py +++ b/validated/dash_apps/finished_apps/daily_validation.py @@ -21,9 +21,9 @@ from validated.tables import create_columns_daily, create_columns_detail from variable.models import Variable -DEFAULT_FONT = "Open Sans, Raleway, Dosis, Ubuntu, sans-serif" - -app = DjangoDash("DailyValidation") +app = DjangoDash( + "DailyValidation", external_stylesheets=["/static/styles/dashstyle.css"] +) # Initial filters STATION: Station = Station.objects.order_by("station_code")[7] @@ -115,7 +115,6 @@ style={ "display": "flex", "justify-content": "flex-start", - "font-family": DEFAULT_FONT, "font-size": "14px", }, ) @@ -192,7 +191,6 @@ "display": "inline-block", "width": "50%", "text-align": "right", - "font-family": DEFAULT_FONT, }, ) @@ -225,7 +223,6 @@ id="status-message", children=[""], style={ - "font-family": DEFAULT_FONT, "font-size": "14px", "min-height": "20px", "padding-top": "5px", @@ -245,14 +242,14 @@ ], value=PLOT_TYPE, inline=True, - style={"font-family": DEFAULT_FONT, "font-size": "14px"}, + style={"font-size": "14px"}, ) # Layout app.layout = html.Div( children=[ filters, - html.Button("Submit", id="submit-button"), + html.Button("Submit", id="submit-button", style={"margin-top": "10px"}), dcc.Loading( type="dot", children=html.Div(id="loading_top"), @@ -267,8 +264,6 @@ label="Daily Report", id="tab-daily", value="tab-daily", - style={"font-family": DEFAULT_FONT}, - selected_style={"font-family": DEFAULT_FONT}, children=[ table_daily, ], @@ -278,9 +273,6 @@ id="tab-detail", value="tab-detail", disabled=True, - style={"font-family": DEFAULT_FONT}, - selected_style={"font-family": DEFAULT_FONT}, - disabled_style={"font-family": DEFAULT_FONT}, children=[ table_detail, ],