Skip to content

Commit

Permalink
Merge pull request #96 from JGreenlee/datepicker-fixes
Browse files Browse the repository at this point in the history
📅 Datepicker: set initial range, use unambiguous date format, use `arrow`, dropdown for `timezone` option
  • Loading branch information
shankari authored Feb 5, 2024
2 parents 3726113 + 719199a commit 5a96e9e
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 126 deletions.
156 changes: 91 additions & 65 deletions app_sidebar_collapsible.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
For more details on building multi-page Dash applications, check out the Dash documentation: https://dash.plot.ly/urls
"""
import os
from datetime import date, timedelta
import arrow

import dash
import dash_bootstrap_components as dbc
Expand All @@ -23,6 +23,7 @@
if os.getenv('DASH_DEBUG_MODE', 'True').lower() == 'true':
logging.basicConfig(level=logging.DEBUG)

from utils.datetime_utils import iso_to_date_only
from utils.db_utils import query_uuids, query_confirmed_trips, query_demographics
from utils.permissions import has_permission
import flask_talisman as flt
Expand Down Expand Up @@ -124,78 +125,107 @@
className="sidebar",
)


content = html.Div([
# Global Date Picker
html.Div(
# Global controls including date picker and timezone selector
def make_controls():
# according to docs, DatePickerRange will accept YYYY-MM-DD format
today_date = arrow.now().format('YYYY-MM-DD')
last_week_date = arrow.now().shift(days=-7).format('YYYY-MM-DD')
tomorrow_date = arrow.now().shift(days=1).format('YYYY-MM-DD')
return html.Div([
# Global Date Picker
dcc.DatePickerRange(
id='date-picker',
display_format='D/M/Y',
start_date_placeholder_text='D/M/Y',
end_date_placeholder_text='D/M/Y',
min_date_allowed=date(2010, 1, 1),
max_date_allowed=date.today(),
initial_visible_month=date.today(),
), style={'margin': '10px 10px 0 0', 'display': 'flex', 'justify-content': 'right'}
),

# Pages Content
dcc.Loading(
type='default',
fullscreen=True,
children=html.Div(dash.page_container, style={
"margin-left": "5rem",
"margin-right": "2rem",
"padding": "2rem 1rem",
})
),
])
display_format='D MMM Y',
start_date=last_week_date,
end_date=today_date,
min_date_allowed='2010-1-1',
max_date_allowed=tomorrow_date,
initial_visible_month=today_date,
),
html.Div([
html.Span('Query trips using: ', style={'margin-right': '10px'}),
dcc.Dropdown(
id='date-picker-timezone',
options=[
{'label': 'UTC Time', 'value': 'utc'},
{'label': 'My Local Timezone', 'value': 'local'},
# {'label': 'Local Timezone of Trips', 'value': 'trips'},
],
value='utc',
clearable=False,
searchable=False,
style={'width': '220px'},
),
],
style={'margin': '10px 10px 0 0',
'display': 'flex',
'justify-content': 'right',
'align-items': 'center'},

),
],
style={'margin': '10px 10px 0 0',
'display': 'flex',
'flex-direction': 'column',
'align-items': 'end'}
)

page_content = dcc.Loading(
type='default',
fullscreen=True,
children=html.Div(dash.page_container, style={
"margin-left": "5rem",
"margin-right": "2rem",
"padding": "2rem 1rem",
})
)


home_page = [
def make_home_page(): return [
sidebar,
content,
html.Div([make_controls(), page_content])
]


def make_layout(): return html.Div([
dcc.Location(id='url', refresh=False),
dcc.Store(id='store-trips', data={}),
dcc.Store(id='store-uuids', data={}),
dcc.Store(id='store-demographics', data={}),
dcc.Store(id='store-trajectories', data={}),
html.Div(id='page-content', children=make_home_page()),
])
app.layout = make_layout

# Load data stores
@app.callback(
Output("store-demographics", "data"),
Input('date-picker', 'start_date'),
Input('date-picker', 'end_date'),
Output("store-uuids", "data"),
Input('date-picker', 'start_date'), # these are ISO strings
Input('date-picker', 'end_date'), # these are ISO strings
Input('date-picker-timezone', 'value'),
)
def update_store_demographics(start_date, end_date):
df = query_demographics()
records = {}
for key, dataframe in df.items():
records[key] = dataframe.to_dict("records")
def update_store_uuids(start_date, end_date, timezone):
(start_date, end_date) = iso_to_date_only(start_date, end_date)
dff = query_uuids(start_date, end_date, timezone)
records = dff.to_dict("records")
store = {
"data": records,
"length": len(records),
}
return store

app.layout = html.Div(
[
dcc.Location(id='url', refresh=False),
dcc.Store(id='store-trips', data={}),
dcc.Store(id='store-uuids', data={}),
dcc.Store(id='store-demographics', data= {}),
dcc.Store(id ='store-trajectories', data = {}),
html.Div(id='page-content', children=home_page),
]
)


# Load data stores
@app.callback(
Output("store-uuids", "data"),
Output("store-demographics", "data"),
Input('date-picker', 'start_date'),
Input('date-picker', 'end_date'),
Input('date-picker-timezone', 'value'),
)
def update_store_uuids(start_date, end_date):
start_date_obj = date.fromisoformat(start_date) if start_date else None
end_date_obj = date.fromisoformat(end_date) if end_date else None
dff = query_uuids(start_date_obj, end_date_obj)
records = dff.to_dict("records")
def update_store_demographics(start_date, end_date, timezone):
df = query_demographics()
records = {}
for key, dataframe in df.items():
records[key] = dataframe.to_dict("records")
store = {
"data": records,
"length": len(records),
Expand All @@ -205,17 +235,13 @@ def update_store_uuids(start_date, end_date):

@app.callback(
Output("store-trips", "data"),
Input('date-picker', 'start_date'),
Input('date-picker', 'end_date'),
Input('date-picker', 'start_date'), # these are ISO strings
Input('date-picker', 'end_date'), # these are ISO strings
Input('date-picker-timezone', 'value'),
)
def update_store_trips(start_date, end_date):
if not start_date or not end_date:
end_date_obj = date.today()
start_date_obj = end_date_obj - timedelta(days=7)
else:
start_date_obj = date.fromisoformat(start_date)
end_date_obj = date.fromisoformat(end_date)
df = query_confirmed_trips(start_date_obj, end_date_obj)
def update_store_trips(start_date, end_date, timezone):
(start_date, end_date) = iso_to_date_only(start_date, end_date)
df = query_confirmed_trips(start_date, end_date, timezone)
records = df.to_dict("records")
# logging.debug("returning records %s" % records[0:2])
store = {
Expand All @@ -239,10 +265,10 @@ def display_page(search):
return get_cognito_login_page('Unsuccessful authentication, try again.', 'red')

if is_authenticated:
return home_page
return make_home_page()
return get_cognito_login_page()

return home_page
return make_home_page()

extra_csp_url = [
"https://raw.githubusercontent.com",
Expand Down
21 changes: 8 additions & 13 deletions pages/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
The workaround is to check if the input value is None.
"""
from dash import dcc, html, Input, Output, callback, register_page, dash_table
from datetime import date, timedelta
# Etc
import logging
import pandas as pd
Expand All @@ -13,6 +12,7 @@
from utils import permissions as perm_utils
from utils import db_utils
from utils.db_utils import query_trajectories
from utils.datetime_utils import iso_to_date_only
register_page(__name__, path="/data")

intro = """## Data"""
Expand All @@ -38,10 +38,10 @@ def clean_location_data(df):
df['data.end_loc.coordinates'] = df['data.end_loc.coordinates'].apply(lambda x: f'({x[0]}, {x[1]})')
return df

def update_store_trajectories(start_date_obj,end_date_obj):
def update_store_trajectories(start_date: str, end_date: str, tz: str):
global store_trajectories
df = query_trajectories(start_date_obj,end_date_obj)
records = df.to_dict("records")
df = query_trajectories(start_date, end_date, tz)
records = df.to_dict("records")
store = {
"data": records,
"length": len(records),
Expand All @@ -59,9 +59,9 @@ def update_store_trajectories(start_date_obj,end_date_obj):
Input('store-trajectories', 'data'),
Input('date-picker', 'start_date'),
Input('date-picker', 'end_date'),
Input('date-picker-timezone', 'value'),
)
def render_content(tab, store_uuids, store_trips, store_demographics, store_trajectories, start_date, end_date):
def render_content(tab, store_uuids, store_trips, store_demographics, store_trajectories, start_date, end_date, timezone):
data, columns, has_perm = None, [], False
if tab == 'tab-uuids-datatable':
data = store_uuids["data"]
Expand Down Expand Up @@ -97,14 +97,9 @@ def render_content(tab, store_uuids, store_trips, store_demographics, store_traj
elif tab == 'tab-trajectories-datatable':
# Currently store_trajectories data is loaded only when the respective tab is selected
#Here we query for trajectory data once "Trajectories" tab is selected
if not start_date or not end_date:
end_date_obj = date.today()
start_date_obj = end_date_obj - timedelta(days=7)
else:
start_date_obj = date.fromisoformat(start_date)
end_date_obj = date.fromisoformat(end_date)
(start_date, end_date) = iso_to_date_only(start_date, end_date)
if store_trajectories == {}:
store_trajectories = update_store_trajectories(start_date_obj,end_date_obj)
store_trajectories = update_store_trajectories(start_date, end_date, timezone)
data = store_trajectories["data"]
if data:
columns = list(data[0].keys())
Expand Down
15 changes: 5 additions & 10 deletions pages/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"""
from uuid import UUID
from datetime import date, timedelta
from dash import dcc, html, Input, Output, callback, register_page
import dash_bootstrap_components as dbc

Expand All @@ -19,6 +18,7 @@
import emission.core.get_database as edb

from utils.permissions import has_permission
from utils.datetime_utils import iso_to_date_only

register_page(__name__, path="/")

Expand Down Expand Up @@ -176,19 +176,14 @@ def generate_plot_sign_up_trend(store_uuids):
@callback(
Output('fig-trips-trend', 'figure'),
Input('store-trips', 'data'),
Input('date-picker', 'start_date'),
Input('date-picker', 'end_date'),
Input('date-picker', 'start_date'), # these are ISO strings
Input('date-picker', 'end_date'), # these are ISO strings
)
def generate_plot_trips_trend(store_trips, start_date, end_date):
df = pd.DataFrame(store_trips.get("data"))
trend_df = None
if not start_date or not end_date:
end_date_obj = date.today()
start_date_obj = end_date_obj - timedelta(days=7)
else:
start_date_obj = date.fromisoformat(start_date)
end_date_obj = date.fromisoformat(end_date)
(start_date, end_date) = iso_to_date_only(start_date, end_date)
if not df.empty and has_permission('overview_trips_trend'):
trend_df = compute_trips_trend(df, date_col = "trip_start_time_str")
fig = generate_barplot(trend_df, x = 'date', y = 'count', title = f"Trips trend({start_date_obj} to {end_date_obj})")
fig = generate_barplot(trend_df, x = 'date', y = 'count', title = f"Trips trend({start_date} to {end_date})")
return fig
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ python-jose==3.3.0
flask==2.2.5
flask-talisman==1.0.0
dash_auth==2.0.0
arrow==1.3.0
32 changes: 32 additions & 0 deletions utils/datetime_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import arrow

MAX_EPOCH_TIME = 2 ** 31 - 1


def iso_range_to_ts_range(start_date: str, end_date: str, tz: str):
"""
Returns a tuple of (start_ts, end_ts) as epoch timestamps, given start_date and end_date in
ISO format and the timezone mode in which the dates should be resolved to timestamps ('utc' or 'local')
"""
start_ts, end_ts = None, MAX_EPOCH_TIME
if start_date is not None:
if tz == 'utc':
start_ts = arrow.get(start_date).timestamp()
elif tz == 'local':
start_ts = arrow.get(start_date, tzinfo='local').timestamp()
if end_date is not None:
if tz == 'utc':
end_ts = arrow.get(end_date).replace(
hour=23, minute=59, second=59).timestamp()
elif tz == 'local':
end_ts = arrow.get(end_date, tzinfo='local').replace(
hour=23, minute=59, second=59).timestamp()
return (start_ts, end_ts)


def iso_to_date_only(*iso_strs: str):
"""
For each ISO date string in the input, returns only the date part in the format 'YYYY-MM-DD'
e.g. '2021-01-01T00:00:00.000Z' -> '2021-01-01'
"""
return [iso_str[:10] if iso_str else None for iso_str in iso_strs]
Loading

0 comments on commit 5a96e9e

Please sign in to comment.