【呆鳥譯Py】Dash用戶指南04_交互式數(shù)據(jù)圖

【呆鳥譯Py】Python交互式數(shù)據(jù)分析報(bào)告框架~Dash介紹
【呆鳥譯Py】Dash用戶指南01-02_安裝與應(yīng)用布局
【呆鳥譯Py】Dash用戶指南03_交互性簡介
【呆鳥譯Py】Dash用戶指南04_交互式數(shù)據(jù)圖
【呆鳥譯Py】Dash用戶指南05_使用State進(jìn)行回調(diào)

4. 交互圖

交互式可視化

dash_core_components庫包含一個(gè)叫Graph的組件。 Graph組件使用開源的plotly.js(JavaScript圖形庫)渲染交互式數(shù)據(jù)可視圖。Plotly.js支持超過35種數(shù)據(jù)圖,可以生成高清的SVG矢量圖和高性能的WebGL圖。

dash_core_components.Graph組件的figureplotly.pyfigure使用一樣的參數(shù),plotly.py是Plotly的Python開源圖庫,詳情可參閱plotly.py文檔與圖庫

Dash組件通過響應(yīng)式方法描述屬性。回調(diào)函數(shù)可以更新各個(gè)屬性,有些屬性還可以通過用戶交互進(jìn)行更新。比如,點(diǎn)選dcc.Dropdown 組件的選項(xiàng),該組件的value 特性就會(huì)改變。

用戶交互可以改變hoverDataclickDataselectedDatarelayoutData等4個(gè)dcc.Graph 組件屬性。鼠標(biāo)懸停、點(diǎn)擊數(shù)據(jù)點(diǎn)或選擇圖中某個(gè)區(qū)域的點(diǎn)時(shí),這些屬性會(huì)相應(yīng)更新。

下面的例子簡單介紹了上述屬性。

import json
from textwrap import dedent as d
import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html

app = dash.Dash(__name__)

app.css.append_css(
    {"external_url": "https://codepen.io/chriddyp/pen/bWLwgP.css"})

styles = {
    'pre': {
        'border': 'thin lightgrey solid',
        'overflowX': 'scroll'
    }
}

app.layout = html.Div([
    dcc.Graph(
        id='basic-interactions',
        figure={
            'data': [
                {
                    'x': [1, 2, 3, 4],
                    'y': [4, 1, 3, 5],
                    'text': ['a', 'b', 'c', 'd'],
                    'customdata': ['c.a', 'c.b', 'c.c', 'c.d'],
                    'name': 'Trace 1',
                    'mode': 'markers',
                    'marker': {'size': 12}
                },
                {
                    'x': [1, 2, 3, 4],
                    'y': [9, 4, 1, 4],
                    'text': ['w', 'x', 'y', 'z'],
                    'customdata': ['c.w', 'c.x', 'c.y', 'c.z'],
                    'name': 'Trace 2',
                    'mode': 'markers',
                    'marker': {'size': 12}
                }
            ]
        }
    ),

    html.Div(className='row', children=[
        html.Div([
            dcc.Markdown(d("""
                **懸停數(shù)據(jù)**

                將鼠標(biāo)懸停在圖中的值上。
            """)),
            html.Pre(id='hover-data', style=styles['pre'])
        ], className='three columns'),

        html.Div([
            dcc.Markdown(d("""
                **點(diǎn)擊數(shù)據(jù)**

                用鼠標(biāo)點(diǎn)擊圖上的點(diǎn)。
            """)),
            html.Pre(id='click-data', style=styles['pre']),
        ], className='three columns'),

        html.Div([
            dcc.Markdown(d("""
                **選擇數(shù)據(jù)**

                使用菜單的套索或方框工具,選擇圖上的點(diǎn)。
            """)),
            html.Pre(id='selected-data', style=styles['pre']),
        ], className='three columns'),

        html.Div([
            dcc.Markdown(d("""
                **縮放與改變數(shù)據(jù)布局**

                在圖形上點(diǎn)擊并拖拽,
                或點(diǎn)擊圖形菜單的縮放按鈕實(shí)現(xiàn)縮放。
                點(diǎn)擊圖例也可以激活此事件。
            """)),
            html.Pre(id='relayout-data', style=styles['pre']),
        ], className='three columns')
    ])
])

@app.callback(
    Output('hover-data', 'children'),
    [Input('basic-interactions', 'hoverData')])
def display_hover_data(hoverData):
    return json.dumps(hoverData, indent=2)

@app.callback(
    Output('click-data', 'children'),
    [Input('basic-interactions', 'clickData')])
def display_click_data(clickData):
    return json.dumps(clickData, indent=2)

@app.callback(
    Output('selected-data', 'children'),
    [Input('basic-interactions', 'selectedData')])
def display_selected_data(selectedData):
    return json.dumps(selectedData, indent=2)

@app.callback(
    Output('relayout-data', 'children'),
    [Input('basic-interactions', 'relayoutData')])
def display_selected_data(relayoutData):
    return json.dumps(relayoutData, indent=2)

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

懸停數(shù)據(jù)

將鼠標(biāo)懸停在圖中的值上。

點(diǎn)擊數(shù)據(jù)

用鼠標(biāo)點(diǎn)擊圖形上的點(diǎn)。

選擇數(shù)據(jù)

使用圖形菜單的套索或方框工具,選擇圖形上的點(diǎn)。

縮放與改變數(shù)據(jù)布局

在圖形上點(diǎn)擊并拖拽,或點(diǎn)擊圖形菜單的縮放按鈕實(shí)現(xiàn)縮放。點(diǎn)擊圖例也可以激活此事件。

鼠標(biāo)懸停時(shí)更新圖形

下面的代碼對(duì)上一章的世界指標(biāo)器示例進(jìn)行了升級(jí),升級(jí)內(nèi)容為,當(dāng)鼠標(biāo)懸停在散點(diǎn)圖上時(shí),時(shí)間序列會(huì)隨之更新。

import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objs as go
import pandas as pd

app = dash.Dash()

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

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

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

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

        html.Div([
            dcc.Dropdown(
                id='crossfilter-yaxis-column',
                options=[{'label': i, 'value': i} for i in available_indicators],
                value='Life expectancy at birth, total (years)'
            ),
            dcc.RadioItems(
                id='crossfilter-yaxis-type',
                options=[{'label': i, 'value': i} for i in ['Linear', 'Log']],
                value='Linear',
                labelStyle={'display': 'inline-block'}
            )
        ], style={'width': '49%', 'float': 'right', 'display': 'inline-block'})
    ], style={
        'borderBottom': 'thin lightgrey solid',
        'backgroundColor': 'rgb(250, 250, 250)',
        'padding': '10px 5px'
    }),

    html.Div([
        dcc.Graph(
            id='crossfilter-indicator-scatter',
            hoverData={'points': [{'customdata': 'Japan'}]}
        )
    ], style={'width': '49%', 'display': 'inline-block', 'padding': '0 20'}),
    html.Div([
        dcc.Graph(id='x-time-series'),
        dcc.Graph(id='y-time-series'),
    ], style={'display': 'inline-block', 'width': '49%'}),

    html.Div(dcc.Slider(
        id='crossfilter-year--slider',
        min=df['Year'].min(),
        max=df['Year'].max(),
        value=df['Year'].max(),
        step=None,
        marks={str(year): str(year) for year in df['Year'].unique()}
    ), style={'width': '49%', 'padding': '0px 20px 20px 20px'})
])

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

    return {
        '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'],
            customdata=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={
                'title': xaxis_column_name,
                'type': 'linear' if xaxis_type == 'Linear' else 'log'
            },
            yaxis={
                'title': yaxis_column_name,
                'type': 'linear' if yaxis_type == 'Linear' else 'log'
            },
            margin={'l': 40, 'b': 30, 't': 10, 'r': 0},
            height=450,
            hovermode='closest'
        )
    }

def create_time_series(dff, axis_type, title):
    return {
        'data': [go.Scatter(
            x=dff['Year'],
            y=dff['Value'],
            mode='lines+markers'
        )],
        'layout': {
            'height': 225,
            'margin': {'l': 20, 'b': 30, 'r': 10, 't': 10},
            'annotations': [{
                'x': 0, 'y': 0.85, 'xanchor': 'left', 'yanchor': 'bottom',
                'xref': 'paper', 'yref': 'paper', 'showarrow': False,
                'align': 'left', 'bgcolor': 'rgba(255, 255, 255, 0.5)',
                'text': title
            }],
            'yaxis': {'type': 'linear' if axis_type == 'Linear' else 'log'},
            'xaxis': {'showgrid': False}
        }
    }

@app.callback(
    dash.dependencies.Output('x-time-series', 'figure'),
    [dash.dependencies.Input('crossfilter-indicator-scatter', 'hoverData'),
     dash.dependencies.Input('crossfilter-xaxis-column', 'value'),
     dash.dependencies.Input('crossfilter-xaxis-type', 'value')])
def update_y_timeseries(hoverData, xaxis_column_name, axis_type):
    country_name = hoverData['points'][0]['customdata']
    dff = df[df['Country Name'] == country_name]
    dff = dff[dff['Indicator Name'] == xaxis_column_name]
    title = '<b>{}</b><br>{}'.format(country_name, xaxis_column_name)
    return create_time_series(dff, axis_type, title)

@app.callback(
    dash.dependencies.Output('y-time-series', 'figure'),
    [dash.dependencies.Input('crossfilter-indicator-scatter', 'hoverData'),
     dash.dependencies.Input('crossfilter-yaxis-column', 'value'),
     dash.dependencies.Input('crossfilter-yaxis-type', 'value')])
def update_x_timeseries(hoverData, yaxis_column_name, axis_type):
    dff = df[df['Country Name'] == hoverData['points'][0]['customdata']]
    dff = dff[dff['Indicator Name'] == yaxis_column_name]
    return create_time_series(dff, axis_type, yaxis_column_name)

if __name__ == '__main__':
    app.run_server()
013

在左邊的散點(diǎn)圖上懸停鼠標(biāo),會(huì)看到右邊的線形圖根據(jù)懸停的點(diǎn)進(jìn)行了更新。


通用交叉篩選器示例

下面的示例針對(duì)6列數(shù)據(jù)進(jìn)行常見的交叉篩選。可以使用每個(gè)散點(diǎn)圖的篩選器對(duì)底層數(shù)據(jù)集進(jìn)行篩選。

import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html

import numpy as np
import pandas as pd

app = dash.Dash()
    
np.random.seed(0)
df = pd.DataFrame({
    'Column {}'.format(i): np.random.rand(50) + i*10
    for i in range(6)})

app.layout = html.Div([
    html.Div(
        dcc.Graph(
            id='g1',
            # if selectedData is not specified then it is initialized as None
            selectedData={'points': [], 'range': None},
            config={'displayModeBar': False}
        ), className='four columns'
    ),
    html.Div(
        dcc.Graph(
            id='g2',
            selectedData={'points': [], 'range': None},
            config={'displayModeBar': False}
        ), className='four columns'),
    html.Div(
        dcc.Graph(
            id='g3',
            selectedData={'points': [], 'range': None},
            config={'displayModeBar': False}
        ), className='four columns')
], className='row')

def highlight(x, y):
    def callback(*selectedDatas):
        index = df.index

        # filter the dataframe by the selected points
        for i, hover_data in enumerate(selectedDatas):
            selected_index = [
                p['customdata'] for p in selectedDatas[i]['points']
                # the first trace that includes all the data
                if p['curveNumber'] == 0
            ]
            if len(selected_index) > 0:
                index = np.intersect1d(index, selected_index)

        dff = df.iloc[index, :]

        color = 'rgb(125, 58, 235)'

        trace_template = {
            'marker': {
                'color': color,
                'size': 12,
                'line': {'width': 0.5, 'color': 'white'}
            }
        }
        figure = {
            'data': [
                # the first trace displays all of the points
                # it is dimmed by setting opacity to 0.1
                dict({
                    'x': df[x], 'y': df[y], 'text': df.index,
                    'customdata': df.index,
                    'mode': 'markers', 'opacity': 0.1
                }, **trace_template),

                # the second trace is plotted on top of the first trace and
                # displays the filtered points
                dict({
                    'x': dff[x], 'y': dff[y], 'text': dff.index,
                    'mode': 'markers+text', 'textposition': 'top',
                }, **trace_template),
            ],
            'layout': {
                'margin': {'l': 15, 'r': 0, 'b': 15, 't': 5},
                'dragmode': 'select',
                'hovermode': 'closest',
                'showlegend': False
            }
        }

        # Display a rectangle to highlight the previously selected region
        shape = {
            'type': 'rect',
            'line': {
                'width': 1,
                'dash': 'dot',
                'color': 'darkgrey'
            }
        }
        if selectedDatas[0]['range']:
            figure['layout']['shapes'] = [dict({
                'x0': selectedDatas[0]['range']['x'][0],
                'x1': selectedDatas[0]['range']['x'][1],
                'y0': selectedDatas[0]['range']['y'][0],
                'y1': selectedDatas[0]['range']['y'][1]
            }, **shape)]
        else:
            figure['layout']['shapes'] = [dict({
                'type': 'rect',
                'x0': np.min(df[x]),
                'x1': np.max(df[x]),
                'y0': np.min(df[y]),
                'y1': np.max(df[y])
            }, **shape)]

        return figure

    return callback

app.css.append_css({
    'external_url': 'https://codepen.io/chriddyp/pen/bWLwgP.css'})

# app.callback is a decorator which means that it takes a function
# as its argument.
# highlight is a function "generator": it's a function that returns function
app.callback(
    Output('g1', 'figure'),
    [Input('g1', 'selectedData'),
     Input('g2', 'selectedData'),
     Input('g3', 'selectedData')]
)(highlight('Column 0', 'Column 1'))

app.callback(
    Output('g2', 'figure'),
    [Input('g2', 'selectedData'),
     Input('g1', 'selectedData'),
     Input('g3', 'selectedData')]
)(highlight('Column 2', 'Column 3'))

app.callback(
    Output('g3', 'figure'),
    [Input('g3', 'selectedData'),
     Input('g1', 'selectedData'),
     Input('g2', 'selectedData')]
)(highlight('Column 4', 'Column 5'))

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

點(diǎn)擊和拖拽任意圖形可以篩選不同區(qū)域。對(duì)于每次選擇,每個(gè)圖中最后選定的區(qū)域會(huì)激活3個(gè)圖形的回調(diào)函數(shù)。Pandas的DataFrame基于選定的點(diǎn)進(jìn)行篩選,選定的點(diǎn)也會(huì)重新繪制圖形,選定區(qū)域以線型方框的形式顯示。
注意,對(duì)多維數(shù)據(jù)集進(jìn)行篩選和可視化,最好選用平行坐標(biāo)圖這種方式。

Dash的局限性

Dash的圖形交互仍存在一些局限,比如:

這些交互圖特性可以實(shí)現(xiàn)很多效果。如果需要我們幫助研究你遇到的問題,可以在Dash社區(qū)論壇上開個(gè)帖子。

下一章介紹Dash的最后一個(gè)概念:用dash.dependencies.State進(jìn)行回調(diào)。對(duì)于包含表格和按鈕的UI界面,State非常有用。

【呆鳥譯Py】Python交互式數(shù)據(jù)分析報(bào)告框架~Dash介紹
【呆鳥譯Py】Dash用戶指南01-02_安裝與應(yīng)用布局
【呆鳥譯Py】Dash用戶指南03_交互性簡介
【呆鳥譯Py】Dash用戶指南04_交互式數(shù)據(jù)圖
【呆鳥譯Py】Dash用戶指南05_使用State進(jìn)行回調(diào)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。