Dom Weldon
Senior Software Engineer, decisionLab
This is a talk about using Dash at work, I apologise to plotly/Dash if anything is out of date or incorrect - get in touch!
I'm a full stack python and javascript developer, why am I using python to write javascript?
Mathematical modelling consultancy based in central London.
Friday 13 - Tuesday 17 September
in Cardiff đ´ó §ó ˘ó ˇó Źó łó ż
Python Data and Python Software: different tools and approaches.
Collaboration is key, but can be very time consuming on small projects.
Want to minimise the new technologies required to get a project off the ground.
If I'm building a very basic proof-of-concept (PoC) app for a client, I want my data scientist đŠâ to take the lead and rapidly prototype a web application.
Their background probably isn't in web development.
I đˇââď¸ want to facilitate their work and deploy the tools we build.
Marc was almost ready to implement his "hello world" React app pic.twitter.com/ptdg4yteF1
— Thomas Fuchs đľ (@thomasfuchs) March 12, 2016
...and so we found Dash.
Open source, with paid consultancy options.
Project describes itself as experimental, although it recently (June) hit version 1.0
No Javascript! ...and that's a good thing?
$ pip install dash
"""Hello world example."""
import dash
import dash_html_components as html
app = dash.Dash(__name__)
app.layout = html.Div(
children=[html.H1(children="Hello EuroPython!")]
)
if __name__ == "__main__":
app.run_server(debug=True)
$ virtualenv -p python3 venv # or whatever floats your boat
$ pip install -r requirements.txt
$ python app00.py
First: how does this work in JS?
âŹď¸
Hello there!
We need a script that sets the innerHTML of Span#helloName to value of Input#helloInput, and monitors it for changes.
Javacript allows us to manipulate the DOM in the browser to do this.
React lets us do this declaratively. Rather than write our own script, we declare the value of something and how it changes.
Precise syntax isn't important, but the value of Input#helloInput would be a prop to the Span#helloName component.
A javascript function ultimately defines how the relationship works.
let spanHelloName = (helloInputValue) => (
{helloInputValue}
);
âŹď¸
Think of these a bit like excel.
They are functions that are called whenever any of their inputs change.
Give the heading an ID, my-h1
app.layout = html.Div(
children=[
html.H1(id="my-h1"),
dcc.Input(
id="my-input",
value="Basel",
type="text",
),
]
)
Set its value with a Callback: taking the value of my-input
@app.callback(
Output(
component_id="my-h1",
component_property="children",
),
[
Input(
component_id="my-input",
component_property="value",
)
],
)
def update_output_div(input_value: str) -> str:
return f"Hello {input_value}!" # python 3.6
$ python app01.py
The code defining the relationship between our components now lives in python. The đ lives on the server.
Whereas in a React app, this would all be managed inside the browser, in Dash it now involves a call to the server (to get the new value for the DIV)
I want to build an app really quickly. Using familiar technologies for data scientists. In order to build a proof-of-concept (PoC), or a very low-use alpha.
As the name implies, it's mostly geared towards building dashboards to display data. But you can also add more transactional and interactive functionality quite easily.
app.layout = html.Div(
children=[
html.H1("Titanic Dataset"),
html.H5("Filter by sex:"),
dcc.Dropdown(
id="my-dropdown",
options=[{"label": "All", "value": "both"}]
+ [
{"label": sex, "value": sex}
for sex in df.Sex.unique()
],
value="both",
),
html.Div(id="my-div"),
dash_table.DataTable(
id="my-table",
columns=[
{"name": i, "id": i} for i in df.columns
],
data=[],
),
]
)
@app.callback(
Output(
component_id="my-table", component_property="data"
),
[
Input(
component_id="my-dropdown",
component_property="value",
)
],
)
def provide_passengers(sex: str) -> Iterable[Mapping]:
if sex == "both":
return df.to_dict("rows")
return df[df.Sex == sex].to_dict("rows")
Full support for a rich suite of graphing functions.
app.layout = html.Div(
children=[
html.H1("Titanic Names"),
dcc.Graph(
id="example-graph",
figure={
"data": [
{
"x": letters,
"y": freq,
"type": "bar",
"name": "Titanic Passengers By First Name",
}
],
"layout": {
"title": (
"Number of Passengers on the Titanic By First "
"Letter of First Name"
)
},
},
),
]
)
app.layout = html.Div(
children=[
html.H1("To Do List"),
html.Ul(
id="my-ul",
children=list_tasks(data_store["tasks"]),
),
dcc.Input(id="my-input"),
html.Button(
id="my-button", n_clicks=0, children="Add"
),
]
)
# replace this with a real data store!
data_store = {"tasks": []}
def list_tasks(tasks: Iterable[str]) -> Iterable[html.Li]:
"""Return list items of tasks."""
return [html.Li(children=task) for task in tasks]
@app.callback(
Output("my-ul", "children"),
[Input("my-button", "n_clicks")],
[State("my-input", "value")],
)
def add_task(
n_clicks: int = 0,
value: str = None,
data_store: Mapping[str, Iterable] = data_store,
) -> Iterable[html.Li]:
"""Called when the button is clicked."""
if value is not None and len(value.strip()):
data_store["tasks"].append(value.strip())
return list_tasks(data_store["tasks"])
But the Dash API limits what you can do.
Dash prevents you from properly accessing Redux. Apps tend not to be "smooth" as a result.
A user interface for a machine learning tool used to detect criminal activity in the Amazon rainforest.
Some tips and lessons learned.
Dash is novel and experimental, so people are always referring to the docs.
All the docs display apps in a single file.
Result: 2,000 line single-file dash apps.
This may seem like very basic coding advice but it's important!
Separation of concerns
$ python -m app05
...because...
"""Run the app."""
from .factory import create_app
from .home import * # at dL we parameterize this instead
from .todo import *
from .shopping import *
app = create_app()
if __name__ == "__main__":
app.run_server(debug=True)
@App.callback(
Output("my-shopping-ul", "children"),
[Input("my-shopping-button", "n_clicks")],
[State("my-shopping-input", "value")],
)
def add_item(
n_clicks: int = 0,
value: str = None,
data_store: Mapping[str, Iterable] = data_store,
) -> Iterable[html.Li]:
"""Called when the button is clicked."""
if value is not None and len(value.strip()):
data_store["items"].append(value.strip())
return list_items(data_store["items"])
@App.layout(label="Shopping List", path="/shopping")
def shopping_layout() -> html.Div:
return html.Div(
children=[
html.H1("Shopping List"),
html.Ul(
id="my-shopping-ul",
children=list_items(data_store["items"]),
),
dcc.Input(id="my-shopping-input"),
html.Button(
id="my-shopping-button", n_clicks=0, children="Add Item"
),
]
)
def register_template(app: dash.dash.Dash) -> None:
"""Register the main template of the application, including css."""
app.layout = base_layout()
app.callback(
Output("page_content", "children"), [Input("url", "pathname")]
)(App.route)
def base_layout() -> html.Div:
"""Return a Dash Component which will serve as the top-level layout."""
# register the layout
return html.Div(
[dcc.Location(id='url', refresh=False),] + [html.Div(
html.Ul([
html.Li(
dcc.Link(
route["label"],
href=path
),
)
for path, route in App._ROUTES.items()
])
),
html.Div(id="page_content", children="Click link to continue"),]
)
@classmethod
def route(cls, path) -> dash.development.base_component.Component:
"""Return the desired layout."""
try:
return cls._ROUTES[path]["callable"]()
except KeyError:
return cls._not_found()
@staticmethod
def _not_found() -> html.Div:
return html.Div("Not Found")
$ python -m app06
Once we have a class to declare and manage the app, and factory functions to build it, we can start to integrate other tools to make life for our users easier.
The first such tool we've begun to use is dependency injection, using google/pinject.
If you're interested in implementing this, please contact me to talk further!
When to stop using Dash and start building a "proper" web application?
Some things Dash isn't great at...
Please contact me for more: