dash第二章:dash的回调函数


在上一章中,我们了解到dash.layout描述了应用程序的外观,是一个组件的层次结构树。dash_html_components库为所有html标记提供类,关键字参数来描述html属性,如样式、类名和id。dash_core_components库生成更高级别的组件,如控件和图形。
本章介绍如何使用回调函数(callback functions)制作Dash应用程序:当输入组件的属性发生更改时,Dash会自动调用这些函数,以更新另一个组件(输出)中的某些属性。
为了获得最佳的用户交互和图表加载性能,生产DASH应用程序应该考虑Job Queue、HPC、DATASADER和DASH的水平缩放能力。
让我们从一个交互式Dash应用程序的简单示例开始。

一个简单的Dash应用

运行以下代码:

import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output

app = dash.Dash(__name__)

app.layout = html.Div([
    html.H6("Change the value in the text box to see callbacks in action!"),
    html.Div([
        "Input: ",
        dcc.Input(id='my-input', value='initial value', type='text')
    ]),
    html.Br(),
    html.Div(id='my-output'),

])


@app.callback(
    Output(component_id='my-output', component_property='children'),
    Input(component_id='my-input', component_property='value')
)
# 我们下面定义的这个函数会自动被回填函数调用
def update_output_div(input_value):
    return 'Output: {}'.format(input_value)


if __name__ == '__main__':
    app.run_server(debug=True)

运行结果如下:
image

下面来解释代码的意思:

  1. 我们的app中的输入和输出被描述为@app.callback调试器中的参数
  2. 在本例中,我们的dcc.Input(id= ,value= ,type= )中,id='my-input'相当于这个输入框的名字是my-inputvalue='initial value'指的是输入框中的默认值是initial value,这也是为什么我们在回调函数中写Input(component_id='my-input', component_property='value')的原因,component_id='my-input'表示回调函数中的输入就是前面dcc.Input中的值。对于dcc.Output也是同理。
  3. 每当输入属性发生更改时,回调装饰器包装的函数将被自动调用。Dash为这个回调函数提供输入属性的新值作为其参数,Dash用函数返回的任何内容更新输出组件的属性。
  4. component_idcomponent_property关键字是可选的(每个对象只有两个参数)。为了清晰起见,本示例中包含了这些内容,但为了简洁易读,本文档的其余部分将省略这些内容。
  5. 千万别把dash.dependencies.Inputdcc.Input搞混了。前者仅在这些回调定义中使用,后者是一个实际组件。
  6. 请注意,我们没有在布局中为my_output组件的children属性设置值。Dash应用程序启动时,它会自动使用输入组件的初始值调用所有回调,以填充输出组件的初始状态。在本例中,如果将div组件指定为html.Div(id='my-output',children='Hello world'),它将在应用程序启动时被覆盖,也就是说写了等于白写,还不如不写。

这有点像用Microsoft Excel编程:每当一个单元格(输入)发生变化时,依赖该单元格(输出)的所有单元格都会自动更新。因为输出会自动对输入的变化做出反应,所以这被称为“反应式编程”(Reactice Programming)。

还记得每个组件是如何完全通过一组关键字参数来描述的吗?我们在Python中设置的这些参数成为组件的属性,这些属性现在很重要。借助Dash的交互性,我们可以使用回调动态更新这些属性中的任何一个。我们通常会更新HTML组件的children属性以显示新文本(记住,children组件负责组件的内容)或dcc.Graphfigure属性以显示新数据。我们还可以更新组件的style,甚至更新dcc.Dropdown中的可用选项!

让我们来看看dcc.Slider如何更新dcc.Graph的吧?

带数字和滑块的Dash应用程序布局

import dash
from dash import dcc
from dash import html
import plotly.express as px
import pandas as pd
from dash.dependencies import Input, Output

df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv')

app = dash.Dash(__name__)

app.layout = html.Div([
    dcc.Graph(id='graph-with-slider'),
    dcc.Slider(
        id='year-slider',
        min=df['year'].min(),
        max=df['year'].max(),
        value=df['year'].min(),
        marks={str(year): str(year) for year in df['year'].unique()},
        step=None
    )
])


@app.callback(
    Output('graph-with-slider', 'figure'),
    Input('year-slider', 'value'))
def update_figure(selected_year):
    filtered_df = df[df.year == selected_year]

    fig = px.scatter(filtered_df, x="gdpPercap", y="lifeExp",
                     size="pop", color="continent", hover_name="country",
                     log_x=True, size_max=55)

    fig.update_layout(transition_duration=500)

    return fig


if __name__ == '__main__':
    app.run_server(debug=True)

注:代码中的csv文件需要FQ才能下载

运行结果如下:
image

在本例中,dcc.Slidervalue属性是这个app的输入,dcc.Grahpfigure属性是这个app的输出。无论何时dcc.Slidervalue被改变,Dash都会调用update_figure这个回调函数来实时更新图像。这个函数会根据滑块的值自动过滤DataFrame中的数据,然后将处理过的数据返回给figure,画出新的图像。

这里有几点值得注意的地方:

  1. 我们再此代码的最开头用Pandas这个库来加载数据:df = pd.read_csv('''')。这个dataframe(df)是一个全局变量,能够被回调函数调用。
  2. 将数据加载到内存可能会很昂贵。通过在应用程序开始时而不是在回调函数中加载查询数据,我们可以确保此操作只在应用程序服务器启动时执行一次。当用户访问应用程序或与应用程序交互时,该数据(df)已经存在于内存中。如果可能的话,昂贵的初始化(如下载或查询数据)应该在应用程序的全局范围内完成,而不是在回调函数中完成。
  3. 回调不会修改原始数据,它只通过使用pandas进行过滤来创建dataframe的副本。这一点很重要:回调绝不能修改其范围之外的变量。如果回调修改了全局状态,那么一个用户的会话可能会影响下一个用户的会话,并且当应用部署在多个进程或线程上时,这些修改不会在会话之间共享。
  4. 我们正在使用layout.transition来像动画一样对数据进行随时间而做平滑的改变。

具有多个输入的Dash应用程序

在Dash中,任何输出都可以有多个输入组件。下面是一个简单的示例,它将五个输入(两个dcc.Dropdown组件、两个dcc.radiotems组件和一个dcc.Slider组件的value属性)绑定到一个输出组件(dcc.Graph组件的figure属性)。注意这个应用程序是如何运行的。callback如何在输出之后列出所有五个输入项。

import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output
import plotly.express as px

import pandas as pd

app = dash.Dash(__name__)

df = pd.read_csv('https://plotly.github.io/datasets/country_indicators.csv')

available_indicators = df['Indicator Name'].unique()

app.layout = html.Div([
    html.Div([

        html.Div([
            dcc.Dropdown(
                id='xaxis-column',
                options=[{'label': i, 'value': i} for i in available_indicators],
                value='Fertility rate, total (births per woman)'
            ),
            dcc.RadioItems(
                id='xaxis-type',
                options=[{'label': i, 'value': i} for i in ['Linear', 'Log']],
                value='Linear',
                labelStyle={'display': 'inline-block'}
            )
        ], style={'width': '48%', 'display': 'inline-block'}),

        html.Div([
            dcc.Dropdown(
                id='yaxis-column',
                options=[{'label': i, 'value': i} for i in available_indicators],
                value='Life expectancy at birth, total (years)'
            ),
            dcc.RadioItems(
                id='yaxis-type',
                options=[{'label': i, 'value': i} for i in ['Linear', 'Log']],
                value='Linear',
                labelStyle={'display': 'inline-block'}
            )
        ], style={'width': '48%', 'float': 'right', 'display': 'inline-block'})
    ]),

    dcc.Graph(id='indicator-graphic'),

    dcc.Slider(
        id='year--slider',
        min=df['Year'].min(),
        max=df['Year'].max(),
        value=df['Year'].max(),
        marks={str(year): str(year) for year in df['Year'].unique()},
        step=None
    )
])


@app.callback(
    Output('indicator-graphic', 'figure'),
    Input('xaxis-column', 'value'),
    Input('yaxis-column', 'value'),
    Input('xaxis-type', 'value'),
    Input('yaxis-type', 'value'),
    Input('year--slider', 'value'))
def update_graph(xaxis_column_name, yaxis_column_name,
                 xaxis_type, yaxis_type,
                 year_value):
    dff = df[df['Year'] == year_value]

    fig = px.scatter(x=dff[dff['Indicator Name'] == xaxis_column_name]['Value'],
                     y=dff[dff['Indicator Name'] == yaxis_column_name]['Value'],
                     hover_name=dff[dff['Indicator Name'] == yaxis_column_name]['Country Name'])

    fig.update_layout(margin={'l': 40, 'b': 40, 't': 10, 'r': 0}, hovermode='closest')

    fig.update_xaxes(title=xaxis_column_name,
                     type='linear' if xaxis_type == 'Linear' else 'log')

    fig.update_yaxes(title=yaxis_column_name,
                     type='linear' if yaxis_type == 'Linear' else 'log')

    return fig


if __name__ == '__main__':
    app.run_server(debug=True)

输出如下:
image

在这个例子中,只要dcc.Dropdown,dcc.Sliderdcc.RadioItems中的任意一个改变,图像就会发生对应的变化!

回调的输入参数是每个“输入”属性的当前值,按指定的顺序排列。

即使一次只更改一个输入(即,用户在给定时刻只能更改一个下拉列表的值),Dash也会收集所有指定输入属性的当前状态,并将它们传递给回调函数。这些回调函数始终保证接收应用程序的更新状态。

让我们扩展我们的示例以包括多个输出。

具有多个输出的Dash应用程序

到目前为止,我们编写的所有回调只更新一个输出属性。我们还可以一次更新多个输出:列出所有要在应用程序中更新的属性。回调,并从回调中返回多项。如果两个输出依赖于相同的计算密集型中间结果(意思就是如果两个输出的计算相同且很复杂,那么这么做就能减少计算量,节省时间),例如缓慢的数据库查询,这一点尤其有用。
复制并运行下面代码:

import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

app.layout = html.Div([
    dcc.Input(
        id='num-multi',
        type='number',
        value=5
    ),
    html.Table([
        html.Tr([html.Td(['x', html.Sup(2)]), html.Td(id='square')]),
        html.Tr([html.Td(['x', html.Sup(3)]), html.Td(id='cube')]),
        html.Tr([html.Td([2, html.Sup('x')]), html.Td(id='twos')]),
        html.Tr([html.Td([3, html.Sup('x')]), html.Td(id='threes')]),
        html.Tr([html.Td(['x', html.Sup('x')]), html.Td(id='x^x')]),
    ]),
])


@app.callback(
    Output('square', 'children'),
    Output('cube', 'children'),
    Output('twos', 'children'),
    Output('threes', 'children'),
    Output('x^x', 'children'),
    Input('num-multi', 'value'))
def callback_a(x):
    return x**2, x**3, 2**x, 3**x, x**x


if __name__ == '__main__':
    app.run_server(debug=True)

首先让我来对上述代码做一个简单的说明:html.Tabel()是创建一个表格,里面的值是一个列表,列表中元素是html.Tr(),每个html.Tr()代表一行,而html.Td()代表的是一个格子。

代码的运行结果如下:
image

警告:合并输出并不总是一个好主意:

  • 如果输出依赖于相同输入的部分(而不是全部),那么将它们分开可以避免不必要的更新。
  • 如果输出具有相同的输入,但它们使用这些输入执行非常不同的计算,保持回调分离可以允许它们并行运行。

带链接回调的Dash应用程序

您还可以将输出和输入链接在一起:一个回调函数的输出可以作为另一个回调函数的输入。
此模式可用于创建动态UI,例如,一个输入组件更新另一个输入组件的可用选项。下面是一个简单的例子。
复制并运行下面的代码:

# -*- coding: utf-8 -*-
import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

all_options = {
    'America': ['New York City', 'San Francisco', 'Cincinnati'],
    'Canada': [u'Montréal', 'Toronto', 'Ottawa']
}
app.layout = html.Div([
    dcc.RadioItems(
        id='countries-radio',
        options=[{'label': k, 'value': k} for k in all_options.keys()],
        value='America'
    ),

    html.Hr(),

    dcc.RadioItems(id='cities-radio'),

    html.Hr(),

    html.Div(id='display-selected-values')
])

#选择不同的国家后,下面的表会显示对应国家的城市
@app.callback(
    Output('cities-radio', 'options'),
    Input('countries-radio', 'value'))
def set_cities_options(selected_country):
    return [{'label': i, 'value': i} for i in all_options[selected_country]]

#当选择新的国家后,需要设置一个默认城市,所以统一设置为第一个城市
@app.callback(
    Output('cities-radio', 'value'),
    Input('cities-radio', 'options'))
def set_cities_value(available_options):
    return available_options[0]['value']

#依据国家和城市输出一句话
@app.callback(
    Output('display-selected-values', 'children'),
    Input('countries-radio', 'value'),
    Input('cities-radio', 'value'))
def set_display_children(selected_country, selected_city):
    return u'{} is a city in {}'.format(
        selected_city, selected_country,
    )


if __name__ == '__main__':
    app.run_server(debug=True)

基于第一个dcc.RadioItems的选项,第一个回调更新第二个dcc.RadioItems中的可用选项。

options属性更改时,第二个回调将设置初始值:它将其设置为该选项数组中的第一个值。

最后一个回调显示每个组件的选定值。如果更改国家/地区dcc.RadioItems的值,Dash将等到城市组件的值更新后再调用最终回调。这可以防止您的回调被称为不一致的状态,如“American”和“Montreal”。

带状态的Dash应用程序

在某些情况下,应用程序中可能会有一个类似于“表单”的模式。在这种情况下,你可能希望程序在用户彻底输入完信息后再进行运算,而不是用户一改变其中的部分信息,程序就马上更新。
将回调直接附加到输入值可以如下所示:

# -*- coding: utf-8 -*-
import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output

external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

app.layout = html.Div([
    dcc.Input(id="input-1", type="text", value="Montréal"),
    dcc.Input(id="input-2", type="text", value="Canada"),
    html.Div(id="number-output"),
])


@app.callback(
    Output("number-output", "children"),
    Input("input-1", "value"),
    Input("input-2", "value"),
)
def update_output(input1, input2):
    return u'Input 1 is "{}" and Input 2 is "{}"'.format(input1, input2)


if __name__ == "__main__":
    app.run_server(debug=True)

在本例中,只要输入描述的任何属性发生更改,就会触发回调函数。State解决了个问题,它能够允许你输入额外值而不调动回调函数!这里有一个和前面相似的程序,但是两个dcc.Input组件拥有State和新的按钮。说人话就是在输入的后面加了一个启动回调函数的按钮。

复制并运行下面的代码:

# -*- coding: utf-8 -*-
import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output, State
import dash_design_kit as ddk

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

app.layout = html.Div([
    dcc.Input(id='input-1-state', type='text', value='Montréal'),
    dcc.Input(id='input-2-state', type='text', value='Canada'),
    html.Button(id='submit-button-state', n_clicks=0, children='Submit'),
    html.Div(id='output-state')
])


@app.callback(Output('output-state', 'children'),
              Input('submit-button-state', 'n_clicks'),
              State('input-1-state', 'value'),
              State('input-2-state', 'value'))
def update_output(n_clicks, input1, input2):
    return u'''
        The Button has been pressed {} times,
        Input 1 is "{}",
        and Input 2 is "{}"
    '''.format(n_clicks, input1, input2)


if __name__ == '__main__':
    app.run_server(debug=True)

代码的运行结果如下:
image
你可以尝试一下改变输入框里的值,你会发现只有点击SUBMIT时下面的值才会改变,这就是State的效果。

在本例中,更改dcc.Input()中的文本不会触发回调,但单击按钮会触发回调。即使它们不会触发回调函数本身,但是dcc.Input当前的值仍然会传递到回调函数中。
注意,我们通过监听htmln_clicks属性来判断是否触发回调。n_clicks是一个属性,它在每次单击组件时都会递增。dash_html_components 库中的每个组件都可以使用它,但最好和按钮配合使用。

本章总结

我们已经在Dash中介绍了回调的基本原理。Dash应用程序基于一组简单但功能强大的原则构建:用户界面可通过反应式回调进行定制。组件的每个属性/属性都可以作为回调的输出进行修改,而属性的子集(例如dcc.Dropdown组件的value属性)可以由用户通过与页面交互进行编辑。

下一章我们将学习如何将回调函数应用到画图中。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM