Skip to content

Instantly share code, notes, and snippets.

@fzyzcjy
Created March 11, 2021 04:04
Show Gist options
  • Save fzyzcjy/0322eebd54d4889b03e0c3ea9fd9e965 to your computer and use it in GitHub Desktop.
Save fzyzcjy/0322eebd54d4889b03e0c3ea9fd9e965 to your computer and use it in GitHub Desktop.
Save states in url for Plotly Dash
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}'
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