from jupyter_dash import JupyterDash 
import dash_core_components as dcc
import dash_html_components as html
/var/folders/w4/wqf2j639555dgr__2br4lwjr0000gn/T/ipykernel_62563/4130790256.py:2: UserWarning: 
The dash_core_components package is deprecated. Please replace
`import dash_core_components as dcc` with `from dash import dcc`
  import dash_core_components as dcc
/var/folders/w4/wqf2j639555dgr__2br4lwjr0000gn/T/ipykernel_62563/4130790256.py:3: UserWarning: 
The dash_html_components package is deprecated. Please replace
`import dash_html_components as html` with `from dash import html`
  import dash_html_components as html

Dash - advanced interactivity#

To provide advanced interactivity and reactivity, that is to let new input trigger modified output, we need to provide the content as the result of a function. We can have several functions, each modifying one specific element of the UI. To reference elements in the UI and the server part, we give a unique id to each element. This id is handed to the respective function to tell it that this is the input to be used. The same applies in the reversed direction: by specifying the id of the desired UI element as a function’s output, the created result is passed to the right address to be displayed. The declaration of what is input and output is done using decorators and callbacks. This decorator takes the input and output ids as arguments.

The structure for building an advanced reactive app becomes:

  • create a JupyterDash object

  • define the UI by using the .layout() method:

    • static elements are defined here

    • every interactive item gets an unique id which will identify the element as it is given to the server/UI. For example: An info text, which will not change during the use of the app would not need an id. A button to alter the content must get an id.

  • define the server functions for the content and use the app.callback() decorator.

    • this part will do any calculations or assignments for interactive elements. The output created here also gets an id to address it in the UI in order to display it at the desired location. The definition as callback ensures that every time an input changes, the respective function (having this element as input) is run to keep the content of the UI up-to-date, e.g. after clicking a button

  • the app is run using the .run_server() method

Note that expensive operations like (down)loading a file should be placed outside the reactive environment, if possible. Else, the operation is triggered and run every time the input changes. For example when working with APIs: Place the request and loading of the data once upfront, before starting the app. Otherwise, the request will be sent unnecessarily for every change in input and soon the limit of requests per day is reached. In general, don’t change global variables inside the reactive environment.

To use the decorators and link input and output between server and UI, we need to import the functions Input and Output from dash.dependencies. We call these functions (imported from dash.dependencies) as arguments of app.callback().

Then, as arguments to Input and Output we need to specify the component_id and the component_property. The respective ids must be identical to the ones used in the UI. The properties vary, dependent on what the output of the function is:

Output:

  • 'children': to return text to the 'children' argument of html elements

  • 'figure': to return a plotly graphic to dcc.Graph()

  • 'value': to return a standard value to an interactive element

  • ('options': to create options for interactive elements like dropdowns, dependent on earlier selection)

Input:

  • 'value': use the value of an element as argument, e.g. an entered number

  • 'n_clicks': returns the number a html.Button has been clicked

  • ('options': use options from an interactive element)

Let’s look at a first example.

from dash.dependencies import Input, Output

app = JupyterDash('myFirstReactive')

app.layout = html.Div([
    html.P(children="Input: "),
    dcc.Input(id='inum', value='', type='number'),
    html.P(id='outext')
])

@app.callback(
    Output(component_id='outext', component_property='children'),
    Input(component_id='inum', component_property='value')
)
def div_outext(num):
    return f'times two: {num*2}'

app.run_server(mode='inline')

First, we instantiate the JupyterDash object.

Then, we modify the layout to contain the following elements inside the main Div:

  • a P element saying ‘Input:’ \(\rightarrow\) static, no need for an id

  • a dcc.Input() with a unique id, a default value (empty string, otherwise it would say ‘None’) and the type of the input to create the according field type in the UI

  • another P element with only an id and no children

Then, we define the server function to create the output: the function per se takes one argument and creates a string containing it after being multiplied by two.

This function, however, gets decorated with app.callback(). This is where the linking between the function and the UI happens. app.callback() gets Output() and Input() as arguments. Each of which have arguments component_id= and component_property=:

  • to Output, we pass the id of the second P element, which in the definition of the layout did not have a children argument. This omission becomes clearer when we see that we now pass component_property='children'. In prose, this means: Take the result of the decorated function and supply it to the UI element with component_id='outext' as a children element.

  • to Input we pass the component_id='inum' and component_property='value'. This means, that the value of the input variable ‘inum’, defined by the input field dcc.Input() should be taken as the input to the decorated function, where it can be processed.

Every time we change the input by entering a number or clicking the arrows in the input field, the function gets called and the output is updated.

Note that when starting an app, dash will run all available callbacks. This is why we omitted the children in the definition of the second P element when defining the layout: Any value we would enter here, would instantly be overwritten by the callback.

We will slightly alter this first example to demonstrate, that several inputs and outputs can be used by one function.

from dash.dependencies import Input, Output

app = JupyterDash()

app.layout = html.Div([
    html.Div(["Enter first and last name: ",
    dcc.Input(id='fname_i', value='otto', type='text'),
    dcc.Input(id='lname_i', value='renner', type='text')]),
    html.Br(),
    html.Div([html.P(id='fname_o', style={'float': 'left'}),
    html.P(id='lname_o', style={'float': 'left','marginLeft': 4})] )
])
    
@app.callback(
    Output(component_id='fname_o', component_property='children'),
    Output(component_id='lname_o', component_property='children'),
    Input(component_id='fname_i', component_property='value'),
    Input(component_id='lname_i', component_property='value')
)
def div_outext(fname, lname):
    if fname:
        return fname[::-1], lname[::-1]
    
app.run_server(mode='inline')
Address already in use
Port 8050 is in use by another program. Either identify and stop that program, or start the server with a different port.

To use several Inputs, we need to look at the order of the Inputs in the callback. It must match the order of the arguments in the decorated function, i.e. the first Input will be passed as the first argument to the function. In the example above, fname_i is passed to the function as the fname argument.

For several Outputs, the same applies: the order of Outputs in the callback must match the order after return.

Dash includes elements knwon from plotly, like sliders and buttons. They are implemented in dcc. Some are also part of html like html.Button (having the n_clicks attribute).

To use interactive elements, we must take their input and pass it to a function by the callback decorator. There, we can use the value to return updated content of an element.

In the following example, we will use a slider input to filter and return the copy(!) of a global dataframe.

import pandas as pd
import datetime as dt
import plotly.express as px
df = pd.read_csv('data/dji_100_days.csv', index_col=0)
df.date = pd.to_datetime(df.date)
df = df[df.date.dt.year == 2021]
df['month'] = [el.month for el in df.date]
pd.to_datetime(6).strftime("%b")
'Jan'
{n+1: pd.to_datetime(n).strftime("%b") for n in range(max(df.month))}
{1: 'Jan', 2: 'Jan', 3: 'Jan', 4: 'Jan', 5: 'Jan'}
from dash.dependencies import Input, Output

app = JupyterDash()

months = {}
app.layout = html.Div([
    dcc.Slider(
        id='comp_slider',
        min= min(df.month),
        max= max(df.month),
        marks= {n: str(n) for n in range(max(df.month))},
        value=1
        ),
    dcc.Dropdown(id='company_dd',
                options = [{'label': i, 'value': i} for i in df.symbol.unique()], value=df.symbol.unique()[0]),
    dcc.Graph(id='outgraph')
])

@app.callback(
    Output('outgraph', 'figure'),
    Input('comp_slider', 'value'),
    Input('company_dd', 'value')
)
def make_outgraph(month, sym):
    dftemp = df[df.symbol == sym]
    dftemp = dftemp[dftemp.month == month]
    fig = px.line(dftemp, x='date', y='open')
    return fig

app.run_server(mode='inline', port=8051)
/Users/jan/anaconda3/lib/python3.8/site-packages/jupyter_dash/jupyter_app.py:139: UserWarning:

The 'environ['werkzeug.server.shutdown']' function is deprecated and will be removed in Werkzeug 2.1.
df[df.month==4]
date open high low close adjClose volume unadjustedVolume change changePercent vwap label changeOverTime symbol month
4 2021-04-30 232.800000 234.490000 229.520000 230.320000 230.320000 4555579.0 4555579.0 -2.48000 -1.065 231.44333 April 30, 21 -0.01065 CRM 4
5 2021-04-29 237.320000 237.670000 232.170000 234.360000 234.360000 4146903.0 4146903.0 -2.96000 -1.247 234.73333 April 29, 21 -0.01247 CRM 4
6 2021-04-28 235.000000 238.330000 233.630000 236.880000 236.880000 4203282.0 4203282.0 1.88000 0.800 236.28000 April 28, 21 0.00800 CRM 4
7 2021-04-27 235.460000 235.840000 231.910000 234.210000 234.210000 3477159.0 3477159.0 -1.25000 -0.531 233.98667 April 27, 21 -0.00531 CRM 4
8 2021-04-26 234.040000 235.800000 232.440000 235.460000 235.460000 3484643.0 3484643.0 1.42000 0.607 234.56667 April 26, 21 0.00607 CRM 4
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2920 2021-04-08 194.419998 196.669998 194.029999 195.720001 195.720001 2391800.0 2391800.0 1.30000 0.669 195.47333 April 08, 21 0.00669 MMM 4
2921 2021-04-07 194.160004 195.350006 193.919998 194.949997 194.949997 1501500.0 1501500.0 0.78999 0.407 194.74000 April 07, 21 0.00407 MMM 4
2922 2021-04-06 195.000000 195.610001 193.990005 194.839996 194.839996 1472700.0 1472700.0 -0.16000 -0.082 194.81333 April 06, 21 -0.00082 MMM 4
2923 2021-04-05 193.889999 195.949997 193.710007 194.970001 194.970001 1963000.0 1963000.0 1.08000 0.557 194.87667 April 05, 21 0.00557 MMM 4
2924 2021-04-01 193.250000 193.919998 190.839996 192.699997 192.699997 1902100.0 1902100.0 -0.55000 -0.285 192.48666 April 01, 21 -0.00285 MMM 4

629 rows × 15 columns

    
@app.callback(
    Output(component_id='fname_o', component_property='children'),
    Output(component_id='lname_o', component_property='children'),
    Input(component_id='fname_i', component_property='value'),
    Input(component_id='lname_i', component_property='value')
)
def div_outext(fname, lname):
    if fname:
        return fname[::-1], lname[::-1]
    
app.run_server(mode='inline')
app.callback_map

Decorators#

Functions which expand the functionality of another function are called decorators (or wrappers). However, these decorators do not change the underlying function, i.e. the one that gets decorated.

This is possible since functions, just like anything in python, are objects and can get passed as arguments to other functions (we have seen this in the context of callbacks). Furthermore, functions may be defined inside other functions, using the the same syntax as usual.

Let’s look at a simple example of a function and a wrapper/decorator. The initial function text_to_wrap simply prints a string. The decorator returns the wrapper() function, which adds a line above and below the text, when printing.

def text_to_wrap():
    print('my text')
    
text_to_wrap()
def emphasize_decorator(func):
    def wrapper():
        print('##################')
        func()
        print('!!!!!!!!!!!!!!!!!!')
    return wrapper

To decorate text_to_wrap, we can assign the decorator with ‘text_to_wrap’ as argument to the initial function name. Note that we pass the function name without parentheses.

text_to_wrap = emphasize_decorator(text_to_wrap)
text_to_wrap()

To shorten this procedure, python includes a special syntax for decorators. With @emphasize_decorator (no parentheses!) before the definition of the inner function, we can achieve the same behaviour.

# emphasize_decorator is being treated as already defined

@emphasize_decorator
def print_greeting():
    print('Hello')
print_greeting()

We see, that print_greeting() has initially been defined to print ‘Hello’. With the decorator call using @, however, we have decorated it on the fly to add the emphasis lines around the text from emphasize_decorator(). Furthermore, decorators may be chained by subsequent calls of the decorator functions with the same @-syntax before the definition of the inner function.

To pass arguments through the decorator, we can use *args and **kwargs as placeholder for an arbitrary number of positional arguments and keyword arguments. Note that this is not unique for decorators, but can be used for the definition of any function! We will now update the first two examples from above: - text_to_wrap will get two arguments to print - wrapper inside emphasize_decorator will get the placeholders in it’s definition

def emphasize_decorator(func):
    def wrapper(*args, **kwargs):
        print('##################')
        func(*args, **kwargs)
        print('!!!!!!!!!!!!!!!!!!')
    return wrapper
@emphasize_decorator
def text_to_wrap(w1, w2):
    print(f"{w1}\n{w2}")
text_to_wrap('line1', 'line2')

This is what dash does with the functions defined in the server part: These functions get wrapped to be linked automatically to the UI and to get called every time, the respective input changes.

import dash
import numpy as np
import pandas as pd
import plotly.graph_objects as go
import dash_core_components as dcc
import dash_html_components as html

from dash.dependencies import Input, Output, State

cars_df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/imports-85.csv')
# Build parcats dimensions
categorical_dimensions = ['body-style', 'drive-wheels', 'fuel-type']
dimensions = [dict(values=cars_df[label], label=label) for label in categorical_dimensions]
# Build colorscale.
color = np.zeros(len(cars_df), dtype='uint8')
colorscale = [[0, '#167b7e'], [1, '#4b3268']]


def build_figure():
    fig = go.Figure(
        data=[
            go.Scatter(x=cars_df.horsepower, y=cars_df['highway-mpg'],
                       marker={'color': 'gray'}, mode='markers', selected={'marker': {'color': 'firebrick'}},
                       unselected={'marker': {'opacity': 0.4}}),
            go.Parcats(
                domain={'y': [0, 0.4]}, dimensions=dimensions,
                line={'colorscale': colorscale, 'cmin': 0,
                      'cmax': 1, 'color': color, 'shape': 'hspline'})
        ])
    fig.update_layout(
        height=800,
        xaxis={'title': 'Horsepower'},
        yaxis={'title': 'MPG', 'domain': [0.6, 1]},
        dragmode='lasso',
        hovermode='closest',
        # plot_bgcolor='rgba(0, 0, 0, 0)',
        paper_bgcolor='rgba(0, 0, 0, 0)',
        autosize=False,
        bargap=0.35,
        font={"family": "Questrial", "size": 10})
    return fig


app =JupyterDash(prevent_initial_callbacks=True)
app.layout = html.Div([dcc.Graph(figure=build_figure(), id="graph")])


@app.callback(Output("graph", "figure"), [Input("graph", "selectedData"), Input("graph", "clickData")],
              [State("graph", "figure")])
def update_color(selectedData, clickData, fig):
    selection = None
    # Update selection based on which event triggered the update.
    trigger = dash.callback_context.triggered[0]["prop_id"]
    if trigger == 'graph.clickData':
        selection = [point["pointNumber"] for point in clickData["points"]]
    if trigger == 'graph.selectedData':
        selection = [point["pointIndex"] for point in selectedData["points"]]
    # Update scatter selection
    fig["data"][0]["selectedpoints"] = selection
    # Update parcats colors
    new_color = np.zeros(len(cars_df), dtype='uint8')
    new_color[selection] = 1
    fig["data"][1]["line"]["color"] = new_color
    return fig



app.run_server()
say_whee()