呆鸟的Python数据分析

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

2019-08-03  本文已影响0人  惑也

【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特性,这样用户就可以在交互界面中进行编辑。
上一篇下一篇

猜你喜欢

热点阅读