Created
March 11, 2021 04:04
-
-
Save fzyzcjy/0322eebd54d4889b03e0c3ea9fd9e965 to your computer and use it in GitHub Desktop.
Save states in url for Plotly Dash
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ast | |
import re | |
from typing import Dict, Callable, Any | |
from urllib.parse import urlparse, parse_qsl, urlencode, quote | |
import dash | |
from dash.dependencies import Input, Output, ALL | |
_COMPONENT_ID_TYPE = 'url_helper' | |
""" | |
definition: {id_inner: {property: your_value}} | |
NOTE here we use id_inner, NOT the real id (which is a dict) | |
""" | |
State = Dict[str, Dict[str, Any]] | |
def create_component_kwargs(state: State, id_inner: str, **raw_kwargs) -> Dict[str, Any]: | |
# noinspection PyDictCreation | |
kwargs = {**raw_kwargs} | |
# create "id" | |
kwargs['id'] = { | |
'type': _COMPONENT_ID_TYPE, | |
'id_inner': id_inner, | |
} | |
# apply default value | |
if id_inner in state: | |
param_key_dict = state[id_inner] | |
kwargs.update(param_key_dict) | |
return kwargs | |
_ID_PARAM_SEP = '::' | |
def _parse_url_to_state(href: str) -> State: | |
parse_result = urlparse(href) | |
query_string = parse_qsl(parse_result.query) | |
state = {} | |
for key, value in query_string: | |
if _ID_PARAM_SEP in key: | |
id, param = key.split(_ID_PARAM_SEP) | |
else: | |
id, param = key, 'value' | |
state.setdefault(id, {})[param] = ast.literal_eval(value) | |
return state | |
def _param_string(id_inner: str, property: str) -> str: | |
return id_inner if property == 'value' else id_inner + _ID_PARAM_SEP + property | |
_RE_SINGLE_QUOTED = re.compile("^'|'$") | |
def _myrepr(o: str) -> str: | |
"""Optional but chrome URL bar hates "'" """ | |
# p.s. Pattern.sub(repl, string) | |
return _RE_SINGLE_QUOTED.sub('"', repr(o)) | |
def setup(app: dash.Dash, page_layout: Callable[[State], Any]): | |
""" | |
NOTE ref: https://github.com/plotly/dash/issues/188 | |
""" | |
@app.callback( | |
Output('page-layout', 'children'), | |
inputs=[Input('url', 'href')], | |
) | |
def page_load(href: str): | |
if not href: | |
return [] | |
state = _parse_url_to_state(href) | |
print(f'page_load href={href} state={state}') | |
return page_layout(state) | |
@app.callback( | |
Output('url', 'search'), | |
# NOTE currently only support property="value"... | |
Input({'type': _COMPONENT_ID_TYPE, 'id_inner': ALL}, 'value'), | |
) | |
def update_url_state(values): | |
"""Updates URL from component values.""" | |
state = {} | |
# https://dash.plotly.com/pattern-matching-callbacks | |
inputs = dash.callback_context.inputs_list[0] | |
for input in inputs: | |
id = input['id'] | |
assert isinstance(id, Dict) | |
assert id['type'] == _COMPONENT_ID_TYPE | |
id_inner = id['id_inner'] | |
state[_param_string(id_inner, input['property'])] = _myrepr(input['value']) | |
params = urlencode(state, safe="%/:?~#+!$,;'@()*[]\"", quote_via=quote) | |
print(f'update_url_state values={values} params={params}') | |
return f'?{params}' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from urllib.parse import urlparse | |
import dash | |
import dash_core_components as dcc | |
import dash_html_components as html | |
import flask | |
from dash_visualization import dash_url_helper | |
from dash_visualization.dash_url_helper import create_component_kwargs | |
app = dash.Dash() | |
url_bar_and_content_div = html.Div([ | |
dcc.Location(id='url', refresh=False), | |
html.Div(id='page-layout') | |
]) | |
def page_layout(state: dash_url_helper.State = None): | |
state = state or {} | |
layout = [ | |
html.H2('URL State demo', id='state'), | |
# create_component(url_params, dcc.DatePickerRange, id_inner='picker'), | |
dcc.Dropdown(**create_component_kwargs( | |
state, | |
id_inner='dropdown', | |
options=[{'label': i, 'value': i} for i in ['LA', 'NYC', 'MTL']], | |
value='LA', | |
)), | |
dcc.Input(**create_component_kwargs( | |
state, | |
id_inner='input', | |
placeholder='Enter a value...', | |
value='', | |
)), | |
dcc.Slider(**create_component_kwargs( | |
state, | |
id_inner='slider', | |
min=0, | |
max=9, | |
marks={i: 'Label {}'.format(i) for i in range(10)}, | |
value=5, | |
)), | |
html.Br(), | |
] | |
return layout | |
def app_layout(): | |
# https://dash.plotly.com/urls "Dynamically Create a Layout for Multi-Page App Validation" | |
if flask.has_request_context(): # for real | |
return url_bar_and_content_div | |
# validation only | |
return html.Div([ | |
url_bar_and_content_div, | |
*page_layout() | |
]) | |
app.layout = app_layout | |
dash_url_helper.setup(app=app, page_layout=page_layout) | |
if __name__ == '__main__': | |
app.run_server(debug=True) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment