背景

在另一篇文章 Python 可交互的网络图可视化工具 中总结了一些提供 Python API 的网络图可视化交互工具库,例如 Plotly、PyVis、PyEcharts 等,其各有千秋。在逛 github 时突然发现另一个眼前一亮的可视化库 —— bqplot,同样提供了网络图的可视化 Python 接口,而且功能更加强大更好看,因此学习下 bqplot 中较为关注的 network graph 网络图可视化方法。

bqplot

bqplot 是基于图形语法构建的用于 Jupyter 的交互式 2D 绘图库,具有以下特点:

  • 用 Python 语言提供统一的可视化框架;
  • bqplot 利用 widget 基础提供第一个在 Python 和 JAVAScript 代码之间通信的绘图库;
  • bqplot 的可视化是基于 D3.js 和 SVG 的,支持快速交互和漂亮的动画;

源码仓库:https://github.com/bqplot/bqplot

官方文档:https://bqplot.readthedocs.io/

bqplot 提供非常多的交互图绘制接口,支持的图表格式如下所示:

  • Bars: Bar mark
  • Bins: Backend histogram mark
  • Boxplot: Boxplot mark
  • Candles: OHLC mark
  • FlexLine: Flexible lines mark
  • Graph: Network mark
  • GridHeatMap: Grid heatmap mark
  • HeatMap: Heatmap mark
  • Hist: Histogram mark
  • Image: Image mark
  • Label: Label mark
  • Lines: Lines mark
  • Map: Geographical map mark
  • Market Map: Tile map mark
  • Pie: Pie mark
  • Scatter: Scatter mark
  • Mega Scatter: webgl-based Scatter mark

其它类型图表形式暂不关注,本文主要学习下其中的 Network graph 绘制方法。

Graph:Network mark 中需要提供节点和边数据。

节点属性如下

AttributeTypeDescriptionDefault
labelstrnode labelmandatory attribute
label_display{center, outside, none}label display optionscenter
shape{circle, ellipse, rect}node shapecircle
shape_attrsdictnode SVG attributes{‘r’: 15}

边属性如下

AttributeTypeDescriptionDefault
sourceintsource node indexmandatory attribute
targetinttarget node indexmandatory attribute
valuefloatvalue of the link. Use np.nan if you do not want a link-

绘图实践

安装方式:pip install bqplot

首先导入必要的库

1
2
3
4
5
import numpy as np
from bqplot import Graph, LinearScale, ColorScale, Figure, Tooltip
from ipywidgets import Layout

fig_layout = Layout(width='600px', height='600px')

👇🏻以下学习下图各个属性的设置。

有向图

如果不指定节点位置默认为 force layout

1
2
3
4
5
6
7
8
9
10
11
12
13
node_data = [
dict(label='A', shape='rect'),
dict(label='B', shape='ellipse'),
dict(label='C', shape='ellipse'),
dict(label='D', shape='rect'),
dict(label='E', shape='ellipse'),
dict(label='F', shape='circle'),
dict(label='G', shape='ellipse'),
]
link_data = [{'source': s, 'target': t} for s, t in np.random.randint(0, 7, (10, 2)) if s != t]
graph = Graph(node_data=node_data, link_data=link_data, charge=-600, colors=['lightblue'] * 7)
graph.link_type = 'arc' # arc, line, slant_line
Figure(marks=[graph], layout=fig_layout)

效果图如下:

有向图

固定位置

可以根据 x, y 坐标设定每个节点的绝对位置,注意设定位置后节点不可拖动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
node_data = list('ABCDEFG')

#using link matrix to set links
link_matrix = np.zeros((7, 7))
xs = LinearScale()
ys = LinearScale()
x = [80, 150, 200, 250, 250, 250, 300]
y = [3, 1.5, 5, 9, 7, 5, 2]
graph = Graph(node_data=node_data, link_matrix=link_matrix, link_type='arc',
colors=['lightblue'] * 7,
scales={'x': xs, 'y': ys, }, x=x, y=y,
directed=True)

Figure(marks=[graph], layout=fig_layout)

效果图如下:

节点固定

颜色模式

点颜色模式:设定图 color 属性值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
node_data = list('ABCDEFG')

#using link matrix to set links
link_matrix = np.zeros((7, 7))
xs = LinearScale(min=0, max=500)
ys = LinearScale(min=0, max=10)
cs = ColorScale(scheme='Reds')
x = [100, 200, 200, 300, 300, 300, 300]
y = [2, .5, 4, 8, 6, 4, 1]
graph3 = Graph(node_data=node_data, link_matrix=link_matrix, link_type='line',
color=np.random.rand(7),
scales={'x': xs, 'y': ys, 'color': cs}, x=x, y=y,
directed=False)
Figure(marks=[graph3], layout=fig_layout)

效果图如下:

节点颜色

边颜色模式:设定图 link_color 属性值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
node_data = list('ABCDEFG')

link_data = [{'source': s, 'target': t, 'value': np.random.rand()} for s, t in np.random.randint(0, 7, (20, 2))]

xs = LinearScale()
ys = LinearScale()
lcs = ColorScale(scheme='Reds')
x = [100, 200, 200, 300, 300, 300, 300]
y = [2, .5, 4, 8, 6, 4, 1]
graph4 = Graph(node_data=node_data, link_data=link_data, link_type='line',
colors=['lightblue'], directed=False,
scales={'x': xs, 'y': ys, 'link_color': lcs},
x=x, y=y, color=np.random.rand(7))
Figure(marks=[graph4], layout=fig_layout)

效果图如下:

边颜色

定制节点

可以自定义节点属性及其节点悬浮和点击的 Action。

1
2
3
4
5
6
7
8
9
10
11
node_data = [
{'label': 'A', 'shape': 'circle', 'shape_attrs': {'r': 20}, 'foo': 1},
{'label': 'B', 'shape': 'rect', 'shape_attrs': {'rx': 10, 'ry': 10, 'width': 40}, 'foo': 2},
{'label': 'C', 'shape': 'ellipse', 'foo': 4},
{'label': 'D', 'shape': 'rect', 'shape_attrs': {'width': 30, 'height': 30}, 'foo': 100},
]

link_data = [{'source': s, 'target': t, 'value': np.random.rand()} for s, t in np.random.randint(0, 4, (8, 2))]

graph5 = Graph(node_data=node_data, link_data=link_data, link_distance=150)
Figure(marks=[graph5], layout=fig_layout)

节点添加 tooltips:

1
2
tooltip = Tooltip(fields=['label', 'foo'], formats=['', '', ''])
graph5.tooltip = tooltip

为 tooltip 添加一个折线图:

1
2
3
4
5
import bqplot.pyplot as plt
plt.clear()
line = plt.plot(np.cumsum(np.random.randn(20)))
# hover on nodes to see the plot
graph5.tooltip = plt.current_figure()

设置选中、悬浮操作:

1
2
3
4
5
graph5.hovered_style = {'stroke': 'red'}
graph5.unhovered_style = {'opacity': '0.4'}

graph5.selected_style = {'opacity': '1', 'stroke': 'white', 'stroke-width': '2.5'}
graph5.selected

设置点击事件:

1
2
3
4
5
def print_event(self, target):
print(target)

graph5.on_background_click(print_event)
graph5.on_element_click(print_event)

效果如下:

节点事件

总结

整体而言,bqplot 符合大部分绘图需求,存在的问题是网络图只能在 Jupyter 中交互,保存时格式只有 png 或者 svg,期待后续功能迭代…