Python的Web可视化框架Dash(4)---基本回调

【Dash系列】Python的Web可视化框架Dash(1)---简介
【Dash系列】Python的Web可视化框架Dash(2)---安装
【Dash系列】Python的Web可视化框架Dash(3)---布局设置
【Dash系列】Python的Web可视化框架Dash(4)---基本回调
【Dash系列】Python的Web可视化框架Dash(5)---状态和预更新
【Dash系列】Python的Web可视化框架Dash(6)---交互和过滤
【Dash系列】Python的Web可视化框架Dash(7)---回调共享
【Dash系列】Python的Web可视化框架Dash(8)---核心组件

本节介绍如何实现Dash应用的回调先导入本节用到的所有包
import pandas as pd
import plotly.graph_objs as go
import dash
import dash_core_components as dcc                  # 交互式组件
import dash_html_components as html                 # 代码转html
from dash.dependencies import Input, Output         # 回调
from jupyter_plotly_dash import JupyterDash         # Jupyter中的Dash


一、交互式布局

(一) 代码

app = JupyterDash('Dash Layout')

app.layout = html.Div([
    dcc.Input(id = 'my-id', value = '初始值', type = 'text'),
    html.Div(id = 'my-div')
])

@app.callback(
    Output(component_id = 'my-div', component_property = 'children'),
    [Input(component_id = 'my-id', component_property = 'value')]
)

def update_output_div(input_value):
    return '你输入了"{}"'.format(input_value)

app

(二)效果图

(三)说明

  1. 在文本框中输入文字,输出组件的子项会立即更新。效果图显示,第一个设置的默认值,后两个,分布输入了数值100和字符串Dash;

  2. app.callback 装饰器通过声明,描述应用程序界面的“输入”与“输出”项;

  3. Dash中应用程序的【输入】和【输出】只是特定组件的属性。本例中,输入项是ID名为my-id 组件的value特性。 输出项是ID名为my-div 组件的children特性;

  4. 当输入项组件的属性值,发生更改时,将自动调用callback装饰器打包的函数,将更新的内容,作为输入项参数,返回函数的输入内容,更新输出项组件的属性值;

  5. 【输入项】和【输出项】对象的关键字参数 component_idcomponent_property,都是可选的。本例中,为了便于理解,列出了这两个关键字,通常情况下,为了让代码简明、易读,可以省略这两个关键字;

  6. 不要混淆 dash.dependencies.Inputdash_core_components.Input对象。前者只在回调函数中使用,后者才是真正的组件;

  7. Dash应用程序启动时,会自动使用输入组件的初始值,调用所有的回调函数,以填充输出组件的初始值。所以,不要在layout中设置 my-div组件的children特性,本例中,如果指定了 html.Div(id='my-div', children='Hello world') 的内容,应用启动时会被覆盖。这种方式类似于Microsoft Excel编程:当单元格的内容发生变化时,依赖于该单元格的所有单元格的内容,都将自动更新。这称为 “反应式编程” (Reactive Programming)

二、滑动条

(一) 代码

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

# 设置Dash应用程序
app = JupyterDash('Slider Update Gragh')
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]
    traces = []
    
    for val in filtered_df.continent.unique():
        df_by_continent = filtered_df[filtered_df.continent == val]
        
        traces.append(go.Scatter(
            x = df_by_continent['gdpPercap'],
            y = df_by_continent['lifeExp'],
            text = df_by_continent['country'],
            name = val,
            mode = 'markers',
            opacity = 0.8,
            marker = dict(size = 15, line = dict(width = 0.5, color = 'white'))
        ))
    
    fig = dict(
        data = traces,
        layout = go.Layout(
            xaxis = dict(type = 'log', title = '人均GDP'),
            yaxis = dict(title = '平均寿命', range = [20, 90]),
            margin = dict(l = 40, b = 40, t = 10, r = 10),
            hovermode = 'closest'
        )
    )
    
    return fig

app

(二) 效果图

(三)说明

  1. 本例中,app的输入是 Slider 的属性 value,app的输出是 Graph 的属性 figure。当 Slider 的 value 变化时,Dash用新值调用回调函数 update_figure,该函数使用此新值过滤数据框,构造 figure 对象,并将其返回到Dash应用程序中,作为输出;

  2. 使用关键字参数进行组件描述,很重要。通过Dash交互性,使用回调函数,可以动态地更新这些特性。如:更新组件的 children 属性从而更新文本内容、更新 dcc.Graph 组件的 figure 属性从而更新数据、更新组件的 style 属性从而更新画布样式、更新 dcc.Dropdown 组件的 options 从而更新下拉菜单;

  3. 将数据加载至内存并进行计算的代价很高,所以尽量在应用的全局范围内下载或查询数据,避免在回调函数里进行这类操作,确保用户访问或与应用交互时,数据(df)已经载入至内存。本例中 df 获取的数据是全局的,可以被回调函数读取;

  4. 回调函数不会修改原始数据,只是通过Pandas的过滤器来筛选数据,并创建DataFrame的副本。这点非常重要:不要在回调函数范围之外更改变量。如果在全局状态下调整回调函数,某一用户的会话就可能影响下一用户的会话,特别是应用部署在多进程或多线程的环境时,这些修改可能会导致跨会话数据分享出现问题;

三、多重输入

(一) 代码

# 数据
df = pd.read_csv(
    'https://gist.githubusercontent.com/chriddyp/'
    'cb5392c35661370d95f300086accea51/raw/'
    '8e0768211f6b747c0db42a9ce9a0937dafcbd8b2/'
    'indicators.csv')

# 设置Dash
app = JupyterDash('many input')
app.layout = html.Div([
    html.Div([
        html.Div([
            dcc.Dropdown(
                id = 'xaxis-column',
                options = [{'label': i, 'value': i} for i in df['Indicator Name'].unique()],
                value = 'Fertility rate, total (births per woman)'),
            dcc.RadioItems(
                id = 'xaxis-type',
                options = [{'label': i, 'value': i} for i in ['线性', '日志']],
                value = '线性',
                labelStyle = dict(display = 'inline-block'))],
            style = dict(width = '48%', display = 'inline-block')
        ),
        html.Div([
            dcc.Dropdown(
                id = 'yaxis-column',
                options = [{'label': i, 'value': i} for i in df['Indicator Name'].unique()],
                value = 'Life expectancy at birth, total (years)'),
            dcc.RadioItems(
                id = 'yaxis-type',
                options = [{'label': i, 'value': i} for i in ['线性', '日志']],
                value = '线性',
                labelStyle = dict(display = 'inline-block'))],
            style = dict(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]
    result = dict(
        data = [go.Scatter(
            x = dff[dff['Indicator Name'] == xaxis_column_name]['Value'],
            y = dff[dff['Indicator Name'] == yaxis_column_name]['Value'],
            text = dff[dff['Indicator Name'] == yaxis_column_name]['Country Name'],
            mode = 'markers',
            marker = {'size': 15, 'opacity': 0.5, 'line': {'width': 0.5, 'color': 'white'}}
        )],
        layout = go.Layout(
            xaxis = dict(title = xaxis_column_name, type = 'linear' if xaxis_type == '线性' else '日志'),
            yaxis = dict(title = yaxis_column_name, type = 'linear' if yaxis_type == '线性' else '日志'),
            margin = {'l': 40, 'b': 40, 't': 10, 'r': 0},
            hovermode = 'closest'
        )
    )
    return result

app

(二) 效果图

(三)说明

  1. 在Dash中,任何“ Output”都可以有多个“ Input”组件;

  2. 本例中,将五个输入组件:2个下拉菜单(Dropdown)组件、2个单选按钮(RadioItems)组件、1个滑动条(Slider)组件,绑定到1个输出组件(Graph 组件的figure特性);

  3. 回调函数的第二个参数,列表中列举了所有的五个输入项dash.dependencies.Input

  4. 示例中,Dropdown、Slider、RadioItems这些组件的value特性变化时,就会调用update_graph函数;

  5. update_graph函数的输入参数,就是这些组件Input特性的当前值或更新值,按其指定的顺序排列;

  6. 即使每次只修改一个Input特性,比如用户一次只能修改一个下拉菜单的值,但Dash会采集所有绑定组件Input 特性的当前值,并传递给回调函数,确保总能获得该应用当前状态的值。

四、多重输出

(一) 代码

app = JupyterDash('many output')
app.layout = html.Div([
    dcc.RadioItems(
        id = 'button-a',
        options = [{'label': i, 'value': i} for i in ['北京', '天津', '上海']],
        value = '北京'),
    html.Div(id = 'output-a'),
    
    dcc.RadioItems(
        id = 'button-b',
        options = [{'label': i, 'value': i} for i in ['东城区', '西城区', '朝阳区']],
        value = '朝阳区'),
    html.Div(id = 'output-b')
])

@app.callback(
    Output('output-a', 'children'),
    [Input('button-a', 'value')]
)
def callback_a(button_value):
    return f"已选中{button_value}"

@app.callback(
    Output('output-b', 'children'),
    [Input('button-b', 'value')]
)
def callback_a(button_value):
    return f"已选中{button_value}"

app

(二) 效果图

(三)说明

  1. 一个Dash回调函数只能更新一个输出属性。要想实现多重输出,需要编写多个函数;

  2. 具体方法:将需要更新的所有属性,作为列表添加到装饰器中,并从回调中返回多个输出项。如果两个输出依赖于相同的计算密集型中间结果,例如慢速数据库查询,推荐使用该方法;

  3. 组合输出并不总是一个好主意:1)如果输出依赖于某些但不是所有相同的输入,则将它们分开可以避免不必要的更新;2)如果它们具有相同的输入,但使用这些输入进行独立计算,则将回调分开,可以实现并行运行它们;

五、链式回调

(一) 代码

app = JupyterDash('Chained Callbacks')
all_options = {
    '北京': ['东城区', '西城区', '朝阳区'],
    '上海': ['黄浦区', '静安区', '普陀区']
}

app.layout = html.Div([
    dcc.RadioItems(
        id = 'countries-dropdown',
        options = [{'label': k, 'value': k} for k in all_options.keys()],
        value = '北京'),
    html.Hr(),
    dcc.RadioItems(id = 'cities-dropdown'),
    html.Hr(),
    html.Div(id = 'display-selected-values')
])

@app.callback(
    Output('cities-dropdown', 'options'),
    [Input('countries-dropdown', 'value')])
def set_cities_options(select_country):
    return [{'label': i, 'value': i} for i in all_options[select_country]]

@app.callback(
    Output('cities-dropdown', 'value'),
    [Input('cities-dropdown', 'options')])
def set_cities_value(available_options):
    return available_options[0]['value']

@app.callback(
    Output('display-selected-values', 'children'),
    [Input('countries-dropdown', 'value'),
     Input('cities-dropdown', 'value')])
def set_display_children(select_country, select_city):
    return f"{select_city}是{select_country}的辖区。"

app

(二) 效果图

(三)说明

  1. 链式回调:将输出和输入链接在一起,即一个回调函数的输出是另一个回调函数的输入;
  2. 此模式用于创建动态UI,其中一个输入组件更新下一个输入组件的可用选项;
  3. 第二个单选按钮RadioItems的选项,基于第一个回调函数传递的单选按钮RadioItems中选择的值;
  4. 第二个回调函数设置了options特性改变时的初始值:将自身设置为options数组中的第一个值;
  5. 最后的回调函数,显示了每个组件中的可选内容。如果更改了城市单选按钮RadioItems组件的value属性,则Dash将等待,直到value更新状态组件后,再调用最后的回调函数。

六、小结

  1. Dash应用是基于下述简单但强大的原则进行构建的:通过响应式与函数式的Python回调函数,自定义声明式的UI;
  2. 声明式组件中的每个元素属性,都可以通过回调函数和属性子集进行更新,比如dcc.Dropdown的value特性,这样用户就可以在交互界面中进行编辑。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容