Description
In Dash, changes to component properties in the front-end trigger user-supplied Python functions (@app.callback). This framework is very flexible: users have full control over the Python code that they write in their app.callback functions.
However, app.callback functions are simple. They filter some data or change the color of the chart or display some text. These data transformations, although simple, currently need to be processed entirely over the network in the Python backend. This makes the apps slower than they could be (because of the network delay) and less portable than they could be (because they require a running Python server).
Clientside Callbacks will introduce an interface for describing data transformation relationships between components in JavaScript. These data transformations will happen entirely in the client-side without passing data over the network. The Clientside callbacks Dash framework will enable developers to swap out their python-driven @app.callback functions with javascript-executed data transformations, enabling more performant apps.
As a quick background, here is what Dash Apps currently look like in Python:
The first part describes what the app looks like.
These classes just declaratively describe the names and the props of the React
components that they generate. These objects get serialized as JSON.
app.layout = html.Div([
html.H1('Example'),
comonents.TextInput(
id='my-text-input',
value='Initial value'
),
components.Dropdown(
id='my-dropdown',
options=[
'Option A', 'Option B', 'Option C'
]
value='Option A'
),
components.Graph(
id='my-graph'
figure={ # Just a plotly figure
'data': [...],
'layout': {...}
}
)
])
The second part of dash apps describe the relationship between graphs.
This sets up "sources" ("inputs") and "sinks" ("outputs")
in the Dash front-end. Whenever any of the Input properties change, the
AJAX call gets made.
It's Reactive like a spreadsheet: as inputs change, the new values propagate
down the dependency graph, updating components in the correct order:
@callback(
Input(
id='my-dropdown',
property='value'
),
Input(
id='my-text-input',
property='value'
),
Output(
id='my-graph',
property='figure'
)
)
def update_graph(new_dropdown_value, new_text_input_value):
# compute a new figure based off of the new values of the dropdown
# or text input
return {
'data': [{
'x': [1, 2, 3],
'y': ({
'Option A': [3, 1, 2],
'Option B': [4, 3, 5],
'Option C': [1, 2, 4]
})[dropdown]
}],
'layout': {'title': text}
}
These reactive updates happen entirely server-side through HTTP requests.
(This allows dash developers to do complicated updates or analytics through
their python context).
I think that we could extend this framework to work client-side as well.
Instead of a custom function defining how Inputs ("sources")
update Outputs ("sinks"), we could define a library of transformations components and a syntax for relating input properties to output properties. These transformations could be just be functional React components.
Here are some conceptual examples:
from dash.serverless import Selector as S
layout = Div([
Input(id='my-input', value='initial-value'),
H3(children=S('my-input', 'value'))
])
In this example, we're setting the "children" property of the HTML H3
element
to just be the "value" of the "my-input" component. When "value" changes,
the content of the H3
element updates to match the new value.
I'm wrapping the ID and property with S
to denote that the string represents a "reactive"
property corresponding to the component with the specified ID and that component's
property. (The actual API might be different, just using s
for conceptual purposes.)
Now, consider a "Dataset" component and a "Graph":
layout = Div([
Dataset(
id='my-dataset'
columns={
'column-1': [1, 2, 3],
'column-2': [3, 1, 4]
},
column_names={
'column-1': 'My first column',
'column-2': 'My second column'
}
),
Graph(
figure={
'data': [{
'x': S('my-dataset', 'column-1'),
'y': S('my-dataset', 'column-2')
}]
}
)
])
Note that not all components actually get rendered in the DOM. In this case,
the Dataset component isn't actually visible. It's just included as state.
If you wanted to view it as a table, it would look like:
layout = Div([
Dataset(
id='my-dataset'
columns={
'column-1': [1, 2, 3],
'column-2': [3, 1, 4]
},
column_names={
'column-1': 'My first column',
'column-2': 'My second column'
}
),
Table(data='::my-dataset.columns'),
Graph(
figure={
'data': [{
'x': S('my-dataset', 'columns', 'column-1'),
'y': S('my-dataset', 'columns', 'column-2')
}]
}
)
])
You can imagine how there might be several datasets and several graphs in one
(like a dashboard or a report).
layout = Div([
Dataset(id='dataset-1', columns={...}),
Dataset(id='dataset-2', columns={...}),
Dataset(id='dataset-3', columns={...}),
Graph(id='graph-1',
data=[{'x': S('dataset-1', 'columns', 'column-1'), ...}]
),
Graph(id='graph-2',
data=[{'x': S('dataset-2', 'columns', 'column-1'), ...}]
),
Graph(id='graph-3',
data=[{'x': S('dataset-3, 'columns', 'column-1'), ...}]
)
])
Now, we would also need a library for lightweight data transformations. I'm thinking something like Ramda.
import dash.clientside.transformations as T
import dash.clientside.selector as S
df = pd.DataFrame([
{'col-1': 1, 'col-2': 5, 'col-3': 10},
{'col-1': 2, 'col-2': 6, 'col-3': 11},
{'col-1': 3, 'col-2': 7, 'col-3': 12},
# ...
])
app.layout = html.Div([
# "Virtual" component that doesn't render anything
# to the screen, it just contains the data for other
# components to reference
dcc.Dataset(
id='my-dataset',
columns=df.columns,
rows=df.to_dict(),
),
# `Table` renders an actual table to the screen
dcc.Table(
rows=S('my-dataset', 'rows'),
columns=S('my-dataset', 'rows')
),
dcc.Graph(
figure={
# T.pluck('col-1', [{'col-1': 1}, {'col-1': 2}]) -> [1, 2]
'x': T.pluck(
'col-1',
S('my-dataset', 'rows')
),
'y': T.pluck(
'col-2',
S('my-dataset', 'rows')
)
}
)
])
Or, extending this with dynamic components:
app.layout = html.Div([
dcc.Dataset(
id='my-dataset',
columns=df.columns,
rows=df.to_dict(),
),
dcc.Dropdown(
id='my-dropdown',
options=[
{'option': i, 'label': i}
for i in df.columns
],
value=df.columns[0]
),
dcc.Graph(
figure={
'x': T.pluck(
'col-1',
S('my-dataset', 'rows')
),
'y': T.pluck(
S('my-dropdown', 'value'),
S('my-dataset', 'rows')
)
}
)
])
Here are some high-level architectural requirements and goals for this work item:
- The Dash Apps will still be created with Python
- The clientside callbacks will be executed in JavaScript
- The existing set of Dash components will be available (with the exception of network-connected components like the
mapbox
charts) - We will introduce a new syntax or language ("data transformation language") for declaratively describing the relationships and simple operations between component properties. This language will have a Python interface, will be serialized as JSON, and executed in JavaScript.
- This client-side data transformation will be available in server-connected Dash apps as well, enabling certain simple updates to happen quickly
- The app will not run arbitrary JavaScript, it will be designed in a way to be safe from XSS injections
- The "data transformation language" will operations like filtering, plucking values from nested objects, sorting, and arithmetic. It will draw inspiration from functional programming libraries and languages that enable concise, chainable, and immutable data transformations. See Ramda (http://ramdajs.com/) for an example.