交互式可视化#
参考:Part 4. Interactive Graphing and Crossfiltering | Dash for Python Documentation | Plotly
dcc库包含一个名为Graph的组件。
Graph使用开源 plotly.js JavaScript 图形库呈现交互式数据可视化。plotly.js 支持超过 35 种图表类型,并以矢量质量 SVG 和高性能 WebGL 呈现图表。
dcc.Graph 组件中的 figure 参数与 Plotly 的开源 Python 图形库 plotly.py 使用的图形参数相同。请查看 plotly.py 文档和画廊 以了解更多信息。
Dash 组件通过一组属性声明性地描述。所有这些属性都可以通过回调函数进行更新,但是这些属性的子集只能通过用户交互来更新,例如,当您单击dcc.Dropdown组件中的某个选项时,该组件的value属性将发生更改。
dcc.Graph 组件具有四个可以通过用户交互更改的属性:hoverData,clickData,selectedData,relayoutData。当您将鼠标悬停在点上,单击点或选择图形中的点区域时,这些属性会更新。
为了获得最佳的用户交互和图表加载性能,生产环境的 Dash 应用程序应考虑 Dash Enterprise 的 Job Queue, HPC, Datashader, 和 horizontal scaling。
这是一个在屏幕上打印这些属性的简单示例。
import json
from dash import dcc, html
from dash.dependencies import Input, Output
import plotly.express as px
import pandas as pd
from app import app
import dash
dash.register_page(__name__)
styles = {
'pre': {
'border': 'thin lightgrey solid',
'overflowX': 'scroll'
}
}
df = pd.DataFrame({
"x": [1, 2, 1, 2],
"y": [1, 2, 3, 4],
"customdata": [1, 2, 3, 4],
"fruit": ["apple", "apple", "orange", "orange"]
})
fig = px.scatter(df, x="x", y="y", color="fruit", custom_data=["customdata"])
fig.update_layout(clickmode='event+select')
fig.update_traces(marker_size=20)
layout = html.Div([
dcc.Graph(
id='basic-interactions',
figure=fig
),
html.Div(className='row', children=[
html.Div([
dcc.Markdown("""
**Hover Data**
Mouse over values in the graph.
"""),
html.Pre(id='hover-data', style=styles['pre'])
], className='three columns'),
html.Div([
dcc.Markdown("""
**Click Data**
Click on points in the graph.
"""),
html.Pre(id='click-data', style=styles['pre']),
], className='three columns'),
html.Div([
dcc.Markdown("""
**Selection Data**
Choose the lasso or rectangle tool in the graph's menu
bar and then select points in the graph.
Note that if `layout.clickmode = 'event+select'`, selection data also
accumulates (or un-accumulates) selected data if you hold down the shift
button while clicking.
"""),
html.Pre(id='selected-data', style=styles['pre']),
], className='three columns'),
html.Div([
dcc.Markdown("""
**Zoom and Relayout Data**
Click and drag on the graph to zoom or click on the zoom
buttons in the graph's menu bar.
Clicking on legend items will also fire
this event.
"""),
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_relayout_data(relayoutData):
return json.dumps(relayoutData, indent=2)
悬停更新图#
当我们将鼠标悬停在散点图中的点上时,让我们通过更新时间序列来更新上一章中的世界指标示例。
from dash import dcc, html
import pandas as pd
import plotly.express as px
from dash.dependencies import Input, Output
from app import app
import dash
dash.register_page(__name__)
df = pd.read_csv('https://plotly.github.io/datasets/country_indicators.csv')
available_indicators = df['Indicator Name'].unique()
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(),
marks={str(year): str(year) for year in df['Year'].unique()},
step=None
), style={'width': '49%', 'padding': '0px 20px 20px 20px'})
])
@app.callback(
Output('crossfilter-indicator-scatter', 'figure'),
[Input('crossfilter-xaxis-column', 'value'),
Input('crossfilter-yaxis-column', 'value'),
Input('crossfilter-xaxis-type', 'value'),
Input('crossfilter-yaxis-type', 'value'),
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]
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_traces(
customdata=dff[dff['Indicator Name'] == yaxis_column_name]['Country Name'])
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')
fig.update_layout(
margin={'l': 40, 'b': 40, 't': 10, 'r': 0}, hovermode='closest')
return fig
def create_time_series(dff, axis_type, title):
fig = px.scatter(dff, x='Year', y='Value')
fig.update_traces(mode='lines+markers')
fig.update_xaxes(showgrid=False)
fig.update_yaxes(type='linear' if axis_type == 'Linear' else 'log')
fig.add_annotation(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)
fig.update_layout(height=225, margin={'l': 20, 'b': 30, 'r': 10, 't': 10})
return fig
@app.callback(
Output('x-time-series', 'figure'),
[Input('crossfilter-indicator-scatter', 'hoverData'),
Input('crossfilter-xaxis-column', 'value'),
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(
Output('y-time-series', 'figure'),
[Input('crossfilter-indicator-scatter', 'hoverData'),
Input('crossfilter-yaxis-column', 'value'),
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)
尝试将鼠标悬停在左侧散点图中的点上。请注意,右侧的折线图是如何根据您悬停的点进行更新的。
通用交叉过滤食谱#
这是对六列数据集进行交叉过滤的更通用的示例。每个散点图的选择都会过滤基础数据集。
from dash import dcc, html
import numpy as np
import pandas as pd
from dash.dependencies import Input, Output
import plotly.express as px
from app import app
import dash
dash.register_page(__name__)
# make a sample data frame with 6 columns
df = pd.DataFrame({f'Col {str(i+1)}': np.random.rand(30) for i in range(6)})
layout = html.Div([
html.Div(
dcc.Graph(id='g1', config={'displayModeBar': False}),
className='four columns'
),
html.Div(
dcc.Graph(id='g2', config={'displayModeBar': False}),
className='four columns'
),
html.Div(
dcc.Graph(id='g3', config={'displayModeBar': False}),
className='four columns'
)
], className='row')
def get_figure(df, x_col, y_col, selectedpoints, selectedpoints_local):
if selectedpoints_local and selectedpoints_local['range']:
ranges = selectedpoints_local['range']
selection_bounds = {'x0': ranges['x'][0], 'x1': ranges['x'][1],
'y0': ranges['y'][0], 'y1': ranges['y'][1]}
else:
selection_bounds = {'x0': np.min(df[x_col]), 'x1': np.max(df[x_col]),
'y0': np.min(df[y_col]), 'y1': np.max(df[y_col])}
# set which points are selected with the `selectedpoints` property
# and style those points with the `selected` and `unselected`
# attribute. see
# https://medium.com/@plotlygraphs/notes-from-the-latest-plotly-js-release-b035a5b43e21
# for an explanation
fig = px.scatter(df, x=df[x_col], y=df[y_col], text=df.index)
fig.update_traces(selectedpoints=selectedpoints,
customdata=df.index,
mode='markers+text', marker={'color': 'rgba(0, 116, 217, 0.7)', 'size': 20}, unselected={'marker': {'opacity': 0.3}, 'textfont': {'color': 'rgba(0, 0, 0, 0)'}})
fig.update_layout(margin={'l': 20, 'r': 0, 'b': 15,
't': 5}, dragmode='select', hovermode=False)
fig.add_shape(dict({'type': 'rect',
'line': {'width': 1, 'dash': 'dot', 'color': 'darkgrey'}},
**selection_bounds))
return fig
# this callback defines 3 figures
# as a function of the intersection of their 3 selections
@app.callback(
Output('g1', 'figure'),
Output('g2', 'figure'),
Output('g3', 'figure'),
Input('g1', 'selectedData'),
Input('g2', 'selectedData'),
Input('g3', 'selectedData')
)
def callback(selection1, selection2, selection3):
selectedpoints = df.index
for selected_data in [selection1, selection2, selection3]:
if selected_data and selected_data['points']:
selectedpoints = np.intersect1d(selectedpoints,
[p['customdata'] for p in selected_data['points']])
return [get_figure(df, "Col 1", "Col 2", selectedpoints, selection1),
get_figure(df, "Col 3", "Col 4", selectedpoints, selection2),
get_figure(df, "Col 5", "Col 6", selectedpoints, selection3)]
尝试单击并拖动任何图以过滤不同区域。在每次选择时,将使用每个图的最新选定区域触发三个图形回调。根据所选点过滤熊猫数据帧,并以突出显示所选点的方式重新绘制图形,并将所选区域绘制为虚线矩形。
顺便说一句,如果您发现自己过滤和可视化高维数据集,则应考虑检查 并行坐标图表类型。
当前的局限性#
目前,图形交互存在一些限制。
当前无法自定义悬停交互或选择框的样式。这个问题正在 plotly/plotly.js#1847 中处理。
这些交互式绘图功能可以做很多事情。如果需要帮助来探索用例,请在 Dash 社区论坛 中打开一个线程。