【呆鳥譯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
組件的figure
與plotly.py
的figure
使用一樣的參數(shù),plotly.py是Plotly的Python開源圖庫,詳情可參閱plotly.py文檔與圖庫。
Dash組件通過響應(yīng)式方法描述屬性。回調(diào)函數(shù)可以更新各個(gè)屬性,有些屬性還可以通過用戶交互進(jìn)行更新。比如,點(diǎn)選dcc.Dropdown
組件的選項(xiàng),該組件的value
特性就會(huì)改變。
用戶交互可以改變hoverData
、clickData
、selectedData
及relayoutData
等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)
懸停數(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()
在左邊的散點(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)
點(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的圖形交互仍存在一些局限,比如:
點(diǎn)擊圖上的點(diǎn)不能累加:不能累加已經(jīng)點(diǎn)擊的圖點(diǎn)數(shù)量,也不支持對(duì)某個(gè)圖點(diǎn)進(jìn)行反選。我們正在解決這個(gè)問題,詳見https://github.com/plotly/plotly.js/issues/1848;
-
目前還不能自定義懸停交互及選擇框的樣式,我們正在解決這個(gè)問題,詳見:
這些交互圖特性可以實(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)