Skip to content

Commit 1295b28

Browse files
398 Add conditionals to sidebar
* First take on conditionals in sidebar * Fix toggles to make them predictable Add feature to change modes for date of first hospitalized and start of distancing measures * Delete unused class * Updated import Updated to reflect current structure of the base branch Co-authored-by: Brian Ross <[email protected]>
1 parent 10bf6e3 commit 1295b28

File tree

7 files changed

+106
-25
lines changed

7 files changed

+106
-25
lines changed

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"gspread",
2727
"gunicorn",
2828
"dash",
29+
"dash_daq",
2930
"dash_bootstrap_components",
3031
"numpy",
3132
"pandas",

src/chime_dash/app/pages/sidebar.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
44
#! _SIDEBAR_ELEMENTS should be considered for moving else where
55
"""
6-
from typing import List
76
from collections import OrderedDict
87
from datetime import date, datetime
8+
from typing import List
99

1010
from dash.development.base_component import ComponentMeta
11-
from dash_html_components import Nav, Div
1211
from dash_core_components import Store
12+
from dash_html_components import Nav, Div
1313

1414
from chime_dash.app.components.base import Page
15+
from chime_dash.app.services.callbacks import SidebarCallbacks
1516
from chime_dash.app.utils import ReadOnlyDict
1617
from chime_dash.app.utils.templates import (
1718
create_switch_input,
@@ -20,7 +21,6 @@
2021
create_header,
2122
create_line_break,
2223
)
23-
from chime_dash.app.services.callbacks import SidebarCallbacks
2424

2525
FLOAT_INPUT_MIN = 0.001
2626
FLOAT_INPUT_STEP = "any"
@@ -40,12 +40,19 @@
4040
###
4141
line_break_1={"type": "linebreak"},
4242
spread_parameters={"type": "header", "size": "h4"},
43+
spread_parameters_checkbox={"type": "switch", "on": False},
4344
date_first_hospitalized={
4445
"type": "date",
4546
"min_date_allowed": datetime(2019, 10, 1),
46-
"max_date_allowed": datetime(2021, 12, 31),
47+
"max_date_allowed": datetime(2021, 12, 31)
4748
},
4849
doubling_time={"type": "number", "min": FLOAT_INPUT_MIN, "step": FLOAT_INPUT_STEP},
50+
social_distancing_checkbox={"type": "switch", "on": False},
51+
social_distancing_start_date={
52+
"type": "date",
53+
"min_date_allowed": datetime(2019, 10, 1),
54+
"max_date_allowed": datetime(2021, 12, 31),
55+
},
4956
relative_contact_rate={
5057
"type": "number",
5158
"min": 0.0,
@@ -93,7 +100,7 @@
93100
"date": date.today(),
94101
},
95102
max_y_axis_value={"type": "number", "min": 10, "step": 10, "value": None},
96-
show_tables={"type": "switch", "value": False},
103+
show_tables={"type": "switch", "on": False},
97104
))
98105

99106

@@ -113,11 +120,22 @@ class Sidebar(Page):
113120
for key, value in _SIDEBAR_ELEMENTS.items()
114121
if value["type"] not in ("header", "linebreak")
115122
))
123+
116124
input_value_map = ReadOnlyDict(OrderedDict(
117-
(key, {"number": "value", "date": "date"}.get(value, "value"))
125+
(key, {"number": "value", "date": "date", "switch": "on"}.get(value, "value"))
118126
for key, value in input_type_map.items()
119127
))
120128

129+
input_state_map = ReadOnlyDict(OrderedDict(
130+
[
131+
('group_date_first_hospitalized', 'style'),
132+
('group_doubling_time', 'style'),
133+
('group_social_distancing_start_date', 'style'),
134+
('group_relative_contact_rate', 'style'),
135+
]
136+
))
137+
138+
121139
def get_html(self) -> List[ComponentMeta]:
122140
"""Initializes the view
123141
"""

src/chime_dash/app/services/callbacks.py

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1+
from collections import OrderedDict, defaultdict
2+
from datetime import datetime, date
13
from typing import List
2-
from datetime import datetime
3-
from collections import OrderedDict
44
from urllib.parse import parse_qsl, urlencode
5+
56
from dash.exceptions import PreventUpdate
67

7-
from chime_dash.app.utils.callbacks import ChimeCallback, register_callbacks
88
from chime_dash.app.utils import (
99
get_n_switch_values,
1010
parameters_deserializer,
1111
parameters_serializer,
1212
prepare_visualization_group
1313
)
14-
14+
from chime_dash.app.utils.callbacks import ChimeCallback, register_callbacks
1515
from penn_chime.model.sir import Sir
1616
from penn_chime.model.parameters import Parameters, Disposition
1717

@@ -27,7 +27,7 @@ class IndexCallbacks(ComponentCallbacks):
2727

2828
@staticmethod
2929
def toggle_tables(switch_value):
30-
return get_n_switch_values(switch_value, 3)
30+
return get_n_switch_values(not switch_value, 3)
3131

3232
@staticmethod
3333
def handle_model_change(i, sidebar_data):
@@ -63,7 +63,7 @@ def handle_model_change_helper(sidebar_mod, sidebar_data):
6363
component_instance=component_instance,
6464
callbacks=[
6565
ChimeCallback( # If user toggles show_tables, show/hide tables
66-
changed_elements={"show_tables": "value"},
66+
changed_elements={"show_tables": "on"},
6767
dom_updates={
6868
"SIR_table_container": "hidden",
6969
"new_admissions_table_container": "hidden",
@@ -98,10 +98,7 @@ class SidebarCallbacks(ComponentCallbacks):
9898
def get_formated_values(i, input_values):
9999
result = dict(zip(i.input_value_map.keys(), input_values))
100100
for key, input_type in i.input_type_map.items():
101-
# todo remove this hack needed because of how Checklist type (used for switch input) returns values
102-
if input_type == "switch":
103-
result[key] = False if result[key] == [True] else True
104-
elif input_type == "date":
101+
if input_type == "date":
105102
value = result[key]
106103
try:
107104
result[key] = datetime.strptime(value, "%Y-%m-%d").date() if value else value
@@ -194,6 +191,12 @@ def parse_hash(hash_str, sidebar_input_types):
194191
value_type = sidebar_input_types[key]
195192
if value_type == "number":
196193
parsed_value = RootCallbacks.try_parsing_number(value)
194+
elif value == 'None':
195+
parsed_value = None
196+
elif value == 'True':
197+
parsed_value = True
198+
elif value == 'False':
199+
parsed_value = False
197200
else:
198201
parsed_value = value
199202
hash_dict[key] = parsed_value
@@ -203,6 +206,7 @@ def parse_hash(hash_str, sidebar_input_types):
203206
def hash_changed(sidebar_input_types, hash_str=None, root_data=None):
204207
if hash_str:
205208
hash_dict = RootCallbacks.parse_hash(hash_str, sidebar_input_types)
209+
# Fix that empty values encodes to 'None' string in url
206210
result = RootCallbacks.get_inputs(hash_dict, sidebar_input_types.keys())
207211
# Don't update the data store if it already contains the same data
208212
if result == root_data:
@@ -224,7 +228,42 @@ def stores_changed(inputs_keys, root_mod, sidebar_mod, root_data, sidebar_data):
224228
new_val = RootCallbacks.get_inputs(root_data, inputs_keys)
225229
else:
226230
raise PreventUpdate
227-
return ["#{}".format(urlencode(new_val))] + list(new_val.values())
231+
232+
# Spread parameters toggle handling
233+
if sidebar_data.get('inputs_dict', {}).get('spread_parameters_checkbox'):
234+
styles = {
235+
'date_first_hospitalized': None,
236+
'doubling_time': {'display': 'none'}
237+
}
238+
new_val['doubling_time'] = None
239+
new_val['date_first_hospitalized'] = date(year=2020, month=4, day=1)
240+
else:
241+
styles = {
242+
'date_first_hospitalized': {'display': 'none'},
243+
'doubling_time': None
244+
}
245+
new_val['date_first_hospitalized'] = None
246+
if new_val['doubling_time'] is None:
247+
new_val['doubling_time'] = 1
248+
249+
# Social distancing handler
250+
if sidebar_data.get('inputs_dict', {}).get('social_distancing_checkbox'):
251+
styles.update({
252+
'social_distancing_start_date': None,
253+
'relative_contact_rate': None
254+
})
255+
if not new_val['social_distancing_start_date']:
256+
new_val['social_distancing_start_date'] = new_val['current_date']
257+
else:
258+
styles.update({
259+
'social_distancing_start_date': {'display': 'none'},
260+
'relative_contact_rate': {'display': 'none'}
261+
})
262+
new_val['relative_contact_rate'] = 0
263+
264+
if not styles['date_first_hospitalized']:
265+
print(sidebar_data.get('inputs_dict', {}).get('spread_parameters_checkbox'))
266+
return ["#{}".format(urlencode(new_val))] + list(new_val.values()) + list(styles.values())
228267

229268
def __init__(self, component_instance):
230269
sidebar = component_instance.components["sidebar"]
@@ -235,7 +274,12 @@ def hash_changed_helper(hash_str=None, root_data=None):
235274
return RootCallbacks.hash_changed(sidebar_input_types, hash_str, root_data)
236275

237276
def stores_changed_helper(root_mod, sidebar_mod, root_data, sidebar_data):
238-
return RootCallbacks.stores_changed(sidebar_inputs.keys(), root_mod, sidebar_mod, root_data, sidebar_data)
277+
return RootCallbacks.stores_changed(sidebar_inputs.keys(),
278+
root_mod,
279+
sidebar_mod,
280+
root_data,
281+
sidebar_data)
282+
239283
super().__init__(
240284
component_instance=component_instance,
241285
callbacks=[
@@ -248,6 +292,7 @@ def stores_changed_helper(root_mod, sidebar_mod, root_data, sidebar_data):
248292
ChimeCallback(
249293
changed_elements={"root-store": "modified_timestamp", "sidebar-store": "modified_timestamp"},
250294
dom_updates={"location": "hash", **sidebar_inputs},
295+
dom_states=sidebar.input_state_map,
251296
callback_fn=stores_changed_helper,
252297
stores=["root-store", "sidebar-store"],
253298
),

src/chime_dash/app/templates/en/sidebar.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ population: Regional population
33
market_share: Hospital market share (%)
44
current_hospitalized: Currently hospitalized COVID-19 patients
55

6+
spread_parameters_checkbox: I know the date of the first hospitalized case.
67
spread_parameters: Spread and Contact Parameters
78
date_first_hospitalized: Date of first hospitalized case (enter this date to have CHIME estimate the initial doubling time)
89
doubling_time: Doubling time in days (up to today. This overwrites date of first hospitalized estimation)
910
relative_contact_rate: Social distancing (% reduction in social contact going forward)
11+
social_distancing_checkbox: Social distancing measures have been implemented
12+
social_distancing_start_date: Date of social distancing measures effect (may be delayed from implementation)
1013

1114
severity_parameters: Severity Parameters
1215
hospitalized_rate: Hospitalization rate (% of total infections)

src/chime_dash/app/utils/__init__.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,8 @@ def build_csv_download(df):
104104

105105
def get_n_switch_values(input_value, elements_to_update) -> List[bool]:
106106
result = []
107-
boolean_input_value = False
108-
if input_value == [True]:
109-
boolean_input_value = True
110107
for _ in repeat(None, elements_to_update):
111-
# todo Fix once switch values make sense. Currently reported as "None" for off and "[False]" for on
112-
result.append(not boolean_input_value)
108+
result.append(input_value)
113109
return result
114110

115111

src/chime_dash/app/utils/callbacks.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ def __init__(self,
1010
changed_elements: Mapping,
1111
callback_fn: Callable,
1212
dom_updates: Mapping = None,
13+
dom_states: Mapping = None,
1314
stores: Iterable = None,
1415
memoize: bool = True
1516
):
@@ -26,13 +27,20 @@ def __init__(self,
2627
Output(component_id=component_id, component_property=component_property)
2728
for component_id, component_property in dom_updates.items()
2829
)
30+
if dom_states:
31+
self.outputs.extend(
32+
Output(component_id=component_id, component_property=component_property)
33+
for component_id, component_property in dom_states.items()
34+
)
35+
2936
if stores:
3037
self.stores.extend(
3138
State(component_id=component_id, component_property="data")
3239
for component_id in stores
3340
)
3441

3542
def wrap(self, app: Dash):
43+
print(f'Registering callback: \nOutputs: \n{self.outputs}, \nInputs:\n{self.inputs}, \nStore: \n{self.stores} \nUsing: {self.callback_fn}\n\n')
3644
if self.memoize:
3745
@lru_cache(maxsize=32)
3846
@app.callback(self.outputs, self.inputs, self.stores)

src/chime_dash/app/utils/templates.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Utility functions for localization templates
33
templates themselves can be found in app/templates/en
44
"""
5+
import dash_daq as daq
6+
57
from typing import Dict, Any, Optional
68

79
from os import path
@@ -146,6 +148,7 @@ def create_number_input(
146148
idx, defaults, min_val=data.get("min", None), max_val=data.get("max", None)
147149
)
148150
return FormGroup(
151+
id=f'group_{idx}',
149152
children=[
150153
Label(html_for=idx, children=content[idx], style=LABEL_STYLE),
151154
Input(id=idx, debounce=debounce, **input_kwargs),
@@ -182,7 +185,14 @@ def create_date_input(
182185
"initial_visible_month"
183186
] = _get_default_values(idx, defaults)
184187

188+
if 'style' in input_kwargs:
189+
style = {'style': input_kwargs.pop('style')}
190+
else:
191+
style = {}
192+
185193
return FormGroup(
194+
id=f'group_{idx}',
195+
**style,
186196
children=[
187197
Label(html_for=idx, children=content[idx], style=LABEL_STYLE),
188198
DatePickerSingle(
@@ -205,8 +215,8 @@ def create_switch_input(idx: str, data: Dict[str, Any], content: Dict[str, str])
205215
content: Localization text
206216
defaults: Parameters to infer defaults
207217
"""
208-
return Checklist(
209-
id=idx, switch=True, options=[{"label": content[idx], "value": True}],
218+
return daq.BooleanSwitch(
219+
id=idx, on=False, label=content[idx]
210220
)
211221

212222

0 commit comments

Comments
 (0)