交互式可视化#
参考: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 社区论坛 中打开一个线程。