Python的Web可视化框架Dash(6)---交互和过滤

【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 arrow as ar
import numpy as np
import pandas as pd
import json
from textwrap import dedent as d
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, State  # 回调
from jupyter_plotly_dash import JupyterDash         # Jupyter中的Dash
import ipywidgets as widgets                        # Jupyter的滑动条插件


一、概述

  1. dash_core_components 库包含一个叫 Graph 的组件;

  2. Graph 组件使用开源的 plotly.js (JavaScript图形库) 渲染交互式数据的可视化;

  3. Plotly.js 支持超过35种数据图,可以生成高清的SVG矢量图和高性能的WebGL图;

  4. dcc.Graph 组件的 figurePlotly.pyfigure 使用一样的参数。详情可参阅plotly.py文档与图库

  5. Dash 组件由一组属性以声明方式进行描述,所有这些属性都可以通过回调函数进行更新。有些属性还可以通过用户交互进行更新,比如,点选下拉按钮 dcc.Dropdown 组件的选项,该组件的属性 value 值就会改变;

  6. dcc.Graph 组件有四个属性,可以通过用户交互进行更新:鼠标悬停(hoverData)、单击(clickData)、选择区域数据(selectedData)、更改布局(relayoutData)

二、打印属性

(一) 代码

app = JupyterDash('Print_id', height=1000, width = 1000)

styles = dict(pre = dict(border = 'thin lightgrey solid', overflowX = 'scroll'))
app.layout = html.Div([
    dcc.Graph(
        id = 'basic-interactions',
        figure = dict(data = [dict(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 = 'trace01',
                                   mode = 'markers',
                                   marker = dict(size = 12)),
                              dict(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 = 'trace02',
                                   mode = 'markers',
                                   marker = dict(size = 12))],
                      layout = dict(clickmode = 'event+select'))),
    html.Div(className = 'row',
             children = [
                 html.Div(className = 'three columns',
                          children = [
                              dcc.Markdown(d("""
                                  **一、悬停数据**
                                  
                                  将鼠标悬停在图中的值上。
                                  """)),
                              html.Pre(id = 'hover-data', style = styles['pre'])]),
                 html.Div(className = 'three columns',
                          children = [
                              dcc.Markdown(d("""
                                  **二、点击数据**
                                  
                                  用鼠标点击图上的点。
                                  """)),
                              html.Pre(id = 'click-data', style = styles['pre'])]),
                 html.Div(className = 'three columns',
                          children = [
                              dcc.Markdown(d("""
                                  **三、选择数据**

                                  使用菜单的套索或方框工具,选择图上的点。

                                  请注意:如果layout.clickmode ='event + select',
                                  若单击的同时按住shift键,则选择数据的同时,也会累计或取消累计所选的数据。
                                  """)),
                              html.Pre(id = 'selected-data', style = styles['pre'])]),
                 html.Div(className = 'three columns',
                          children = [
                              dcc.Markdown(d("""
                                  **四、缩放与改变数据布局**

                                  在图形上点击并拖拽,
                                  或点击图形菜单的缩放按钮实现缩放。
                                  点击图例也可以激活此事件。
                                  """)),
                              html.Pre(id = 'relayout-data', style = styles['pre'])])
             ]
    )
])

@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_repayout_data(relayoutData):
    return json.dumps(relayoutData, indent = 2)

app

(二) 示意图



三、鼠标悬停时更新图表

(一) 代码

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

app = JupyterDash('update_graph_id', width = 1000, height = 960)
app.layout = html.Div([

    # 设置2个下拉按钮、4个单选按钮
    html.Div([
        html.Div([
            dcc.Dropdown(
                id = 'crossfilter-xaxis-column',
                options = [{'label': i, 'value': i} for i in df['Indicator Name'].unique()],
                value = 'Fertility rate, total (births per woman)'),
            dcc.RadioItems(
                id = 'crossfilter-xaxis-type',
                options = [{'label': i, 'value': i} for i in ['线性', '日志']],
                value = '线性',
                labelStyle = dict(display = 'inline-block'))],
            style = dict(width = '49%', display = 'inline-block')),
        
        html.Div([
            dcc.Dropdown(
                id = 'crossfilter-yaxis-column',
                options = [{'label': i, 'value': i} for i in df['Indicator Name'].unique()],
                value = 'Life expectancy at birth, total (years)'),
            dcc.RadioItems(
                id = 'crossfilter-yaxis-type',
                options = [{'label': i, 'value': i} for i in ['线性', '日志']],
                value = '线性',
                labelStyle = dict(display = 'inline-block'))],
            style = dict(width = '49%', display = 'inline-block', float = 'right'))],
        
        style = dict(borderBottom = 'thin lightgrey solid', 
                     backgroundColor = 'rgb(250, 250, 250)', padding = '10px 5px')),
    
    # 设置交互数据对象及默认值
    html.Div([
        dcc.Graph(
            id = 'crossfilter-indicator-scatter',
            hoverData = dict(points = [{'customdata': 'Japan'}]))],
        style = dict(width = '49%', display = 'inline-block', padding = '0 20')),
    
    # 设置交互的子图表
    html.Div([
        dcc.Graph(id = 'x-time-series'),
        dcc.Graph(id = 'y-time-series')],
        style = dict(width = '49%', display = 'inline-block')),
    
    # 设置日期滑动条
    html.Div(
        dcc.Slider(
            id = 'crossfilter-year--slider',
            min = df['Year'].min(),
            max = df['Year'].max(),
            value = df['Year'].max(),
            marks = {str(y): str(y) for y in df['Year'].unique()}),
        style = dict(widht = '49%', padding = '0px 20px 20px 20px')
    )
])


# 回调2个下拉按钮 + 4个单选按钮
@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]
    
    return 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'],
            customdata = dff[dff['Indicator Name'] == yaxis_column_name]['Country Name'],
            mode = 'markers',
            marker = dict(size = 15, opacity = 0.5, line = dict(width = 0.5, color = 'white')))],
        
        layout = dict(
            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': 30, 't': 10, 'r': 0}, height=720, hovermode = 'closest'
        )
    )

# 设置子图表
def create_time_series(df, axis_type, title):
    return dict(
        data = [go.Scatter(
            x = df['Year'],
            y = df['Value'],
            mode = 'lines+markers')],
        layout = dict(
            height = 360, margin = {'l': 20, 'b': 30, 't': 10, 'r': 10}, 
            xaxis = dict(showgrid = False),
            yaxis = dict(type = 'linear' if axis_type == '线性' else '日志'),
            annotations = [
                dict(x = 0, y = 0.85, text = title, 
                     showarrow = False, align = 'left', 
                     bgcolor = 'rgba(255, 255, 255, 0.5)', 
                     xref = 'paper', yref = 'paper', 
                     xanchor = 'left', yanchor = 'bottom')]
        )
    )

# 回调--设置上子图表的交互
@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 = f"<b>{country_name}</b><br>{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)

app

(二) 示意图

  • 在左侧图表区域滑动鼠标,会更新右侧的图表
  • 效果

四、通用的交叉筛选器

(一) 代码

# 数据源
np.random.seed(0)    # 使每次生成的随机数相同
df = pd.DataFrame({f"Column {i}": np.random.rand(30) + i*10 for i in range(6)})

app = JupyterDash('Cross_filter_id', width = 1000)
app.layout = html.Div(
    className = 'row',
    children = [
        html.Div(dcc.Graph(id = 'g1', config = dict(displayModeBar = False)), className = 'four columns'),
        html.Div(dcc.Graph(id = 'g2', config = dict(displayModeBar = False)), className = 'four columns'),
        html.Div(dcc.Graph(id = 'g3', config = dict(displayModeBar = False)), className = 'four columns'),
    ]
)

# high_light为返回函数的函数
def high_light(x, y):
    
    def callback(*selectedDatas):
        selectedpoints = df.index
        # 按选定的点,从Dataframe中过滤数据
        for i, selected_data in enumerate(selectedDatas):
            if selected_data is not None:
                selected_index = [p['customdata'] for p in selected_data['points']]
                if len(selected_index) > 0:
                    selectedpoints = np.intersect1d(selectedpoints, selected_index)
                    
        # 设置图表
        fig = dict(
            data = [dict(
                x = df[x], y = df[y], text = df.index, textposition = 'top', selectedpoints = selectedpoints, 
                customdata = df.index, type = 'scatter', mode = 'markers+text', 
                textfont = dict(color = 'rgba(30, 30, 30, 1)'),
                marker = dict(color = 'rgba(0, 116, 217, 0.7)', size = 12, 
                              line = dict(color = 'rgb(0, 116, 217)', width = 0.5)),
                unselected = dict(marker = dict(opacity = 0.3), textfont = dict(color = 'rgba(0, 0, 0, 0)'))
            )], # 使未选择的透明
            
            layout = dict(clickmode = 'event+select', showlegend = False, hovermode = 'closest', height = 230,
                          margin = {'l': 15, 'r': 0, 'b': 15, 't': 5}, dragmode = 'select')
        )
        
        # 将矩形显示到先前选定的区域
        shape = dict(type = 'rect', line = dict(width = 1, dash = 'dot', color = 'darkgrey'))
        if selectedDatas[0] and selectedDatas[0]['range']:
            fig['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:
            fig['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 fig
    
    return callback

# app.callback是一个装饰器
app.callback(
    Output('g1', 'figure'),
    [Input('g1', 'selectedData'),
     Input('g2', 'selectedData'),
     Input('g3', 'selectedData')]
)(high_light('Column 0', 'Column 1'))

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

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

app

(二) 示意图

  • 在任一子图表筛选或单击数据,其它子图表会对应更新
  • 效果

(三) 说明

  1. 示例是对由6列数据集,两两组成的3个散点图表,进行交叉筛选的通用示例。对单个散点图进行选择数据区域(或单击),则另外2个散点图会仅显示筛选的数据;
  2. 对多维数据集进行筛选和可视化,最好选用平行坐标图的方式。

五、Dash交互局限性

  1. 点击图上的点不能累加:不能累加已经点击的图点数量,也不支持对某个图点进行反选;
  2. 目前还不能自定义悬停交互及选择框的样式。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258

推荐阅读更多精彩内容