D3数据可视化:树状结构、层次布局与粒子模拟
1. 径向树绘制
在绘制径向树时,需为极坐标合理选择两个坐标的范围。树的原点位于图形中心,对应的
<g>
元素也会相应放置。
1.1 代码示例
.sort( (a,b)=>b.height-a.height )
);
var g = d3.select( "#radial" ).append( "g" )
.attr( "transform", "translate(150, 150)" );
var h = function( r, phi ) { return r*Math.sin(phi) }
var v = function( r, phi ) { return -r*Math.cos(phi) }
g.selectAll( "line" ).data( nodes.links() ).enter()
.append( "line" ).attr( "stroke", "red" )
.attr( "x1", d=>h(d.source.y, d.source.x) )
.attr( "y1", d=>v(d.source.y, d.source.x) )
.attr( "x2", d=>h(d.target.y, d.target.x) )
.attr( "y2", d=>v(d.target.y, d.target.x) );
g.selectAll( "circle" ).data( nodes.descendants() ).enter()
.append( "circle" ).attr( "r", 5 )
.attr( "cx", d=>h(d.y, d.x) )
.attr( "cy", d=>v(d.y, d.x) );
1.2 具体解释
-
定义了两个函数
h和v来处理极坐标。 -
使用
<line>元素将边表示为直线,需明确访问每个链接中源节点和目标节点的坐标。 - 也可使用径向链接生成器来绘制边,示例代码如下:
g.selectAll("path").data( nodes.links() ).enter().append("path")
.attr( "d", d3.linkRadial().angle(d=>d.x).radius(d=>d.y) )
.attr( "stroke", "red" ).attr( "fill", "none" );
2. 包含层次结构的面积图
有时需要对层次结构中的信息进行“汇总”,例如文件系统中的目录,它既包含自身的文件,也间接包含所有子目录中的文件。挑战在于同时可视化三个信息:
- 每个组件的个体大小。
- 每个组件及其所有子组件的累积大小。
- 父子层次结构。
2.1 D3节点函数
D3的节点抽象提供了两个有用的成员函数:
-
count()
:返回接收节点下方子树中的叶子节点数(包括接收节点本身,因此对于叶子节点,
count()
返回1)。
-
sum()
:为每个节点计算访问器函数,该函数必须返回一个非负数字,然后计算当前节点下方子树中这些数字的累积和。
这两个函数都会将结果分配给每个节点的成员变量
value
,在计算任何需要节点上设置
value
属性的函数之前,必须显式调用
count()
和
sum()
。
2.2 布局示例
D3包含多种布局将这些信息转换为图形排列,如
d3.treemap()
、
d3.partition()
和
d3.pack()
。下面是使用
d3.treemap()
布局创建树状图的示例:
function makeTreemap() {
d3.json( "filesys.json" ).then( function(json) {
var sc = d3.scaleOrdinal( d3.schemeReds[8] );
var nodes = d3.hierarchy(json, d=>d.kids).sum(d=>d.size)
.sort((a,b) => b.height-a.height || b.value-a.value);
d3.treemap().size( [300,300] ).padding(5)(nodes);
var g = d3.select( "#treemap" ).append( "g" );
g.selectAll( "rect" ).data( nodes.descendants() ).enter()
.append( "rect" )
.attr( "x", d=>d.x0 ).attr( "y", d=>d.y0 )
.attr( "width", d=>d.x1-d.x0 )
.attr( "height", d=>d.y1-d.y0 )
.attr( "fill", d=>sc(d.depth) ).attr( "stroke", "red" );
g.selectAll( "text" ).data( nodes.leaves() ).enter()
.append( "text" )
.attr( "text-anchor", "middle" ).attr( "font-size", 10 )
.attr( "x", d=>(d.x0+d.x1)/2 )
.attr( "y", d=>(d.y0+d.y1)/2+2 )
.text( d=>d.data.name );
} );
}
2.3 操作步骤
-
从输入文件创建D3节点对象树,并计算
sum()函数,将每个节点的size成员视为其值。 - 按节点高度降序排序,若高度相同则按值降序排序。
- 调用布局机制,指定每个元素周围的额外填充。
-
d3.treemap()布局计算每个矩形的两个角位置,而非<rect>元素所需的宽度和高度。 - 仅叶子节点接收文本标签。
3. 基于力的粒子排列
当图中需要排列的元素数量众多,或者它们必须满足的约束条件复杂时,可能难以找到最优排列。此时,迭代松弛方案可能会有所帮助,即从元素的任意初始配置开始,应用约束条件,并让元素做出相应移动。D3提供了一个工具来模拟一组受约束元素的行为,以确定令人满意的排列。
3.1 模拟设置
模拟操作基于一个节点或粒子数组。粒子是一个任意的、可能为空的对象。模拟会自动创建以下成员:
x
、
y
(位置),
vx
、
vy
(速度)和
index
(作为标识符的数字索引)。若粒子有
fx
、
fy
(固定位置)成员,则该粒子将被视为静止。
模拟创建后会自动在“后台”运行,每帧动画进行一次模拟步骤。可以使用
stop()
停止,
restart()
重新启动,停止后可使用
tick()
手动推进。可使用
on()
注册事件处理程序,在每次迭代步骤后(创建平滑动画)或结束时(显示最终配置)调用。
3.2 控制收敛
模拟旨在创建一个“布局”,因此应收敛到最终配置并停止。收敛主要通过参数
alpha
控制,默认情况下,
alpha
在每一步按常数因子减小,若低于
alphaMin()
设置的阈值,模拟停止。大多数交互会将每一步要应用的更改乘以当前
alpha
参数值,使增量变化随模拟进行而减小。可使用
alphaDecay()
控制
alpha
参数的减小速度,若要让模拟永远运行,可保持
alpha
参数不变。
为帮助收敛并避免虚假振荡和不稳定,所有粒子都受到普通摩擦的影响,可使用
velocityDecay()
设置摩擦系数。
3.3 约束和交互
D3定义了几种预定义的约束或交互作用于粒子之间,这些交互被创建为实例并传递给模拟,可使用
force()
函数将交互实例添加到模拟中。
以下是一些内置交互及其作用:
| 交互函数 | 描述 |
| — | — |
|
d3.forceCenter( x, y )
| 每次模拟步骤后对所有粒子的位置进行整体调整,使系统的质心保持在指定位置。 |
|
d3.forceCollide( radius )
| 粒子对之间的软核排斥,与粒子中心的重叠量成线性关系,若不重叠则为零。可全局设置粒子半径或通过访问器函数为每个粒子设置不同半径,还可设置全局强度参数,可迭代约束以强化效果。 |
|
d3.forceLink( [ links ] )
| 粒子对之间的软约束,若两个粒子形成“链接”,该约束会使它们保持特定距离,与偏离期望距离的偏差成线性关系。可全局或为每个链接单独设置期望距离和交互强度。 |
|
d3.forceManyBody()
| 粒子对之间的交互,与它们距离的平方成反比,根据强度参数的符号可吸引或排斥,强度可全局设置或通过每个粒子的访问器设置,交互范围可在短程和长程截断,默认使用近似算法,通过参数
theta
控制。 |
|
d3.forceX( x )
、
d3.forceY( y )
、
d3.forceRadial( r, x, y )
| 软约束,旨在将所有粒子固定在一维直线或圆周上,通过驱动粒子位置的单个分量向绝对值靠近。前两个交互分别使
x
和
y
坐标趋近于指定值,第三个使粒子到指定半径和中心的圆上最近点的距离最小化。强度可全局或通过每个粒子的访问器设置。 |
3.4 示例
3.4.1 创建网络布局
function makeNetwork() {
d3.json( "network.json" ).then( res => {
var svg = d3.select( "#net" )
var scC = d3.scaleOrdinal( d3.schemePastel1 )
d3.shuffle( res.ps ); d3.shuffle( res.ln );
d3.forceSimulation( res.ps )
.force("ct", d3.forceCenter( 300, 300 ) )
.force("ln",
d3.forceLink( res.ln ).distance(40).id(d=>d.id) )
.force("hc", d3.forceCollide(10) )
.force("many", d3.forceManyBody() )
.on( "end", function() {
svg.selectAll( "line" ).data( res.ln ).enter()
.append( "line" ).attr( "stroke", "black" )
.attr( "x1", d=>d.source.x )
.attr( "y1", d=>d.source.y )
.attr( "x2", d=>d.target.x )
.attr( "y2", d=>d.target.y );
svg.selectAll("circle").data(res.ps).enter()
.append("circle")
.attr( "r", 10 ).attr( "fill", (d,i) => scC(i) )
.attr( "cx", d=>d.x ).attr( "cy", d=>d.y )
svg.selectAll("text").data(res.ps).enter()
.append("text")
.attr( "x", d=>d.x ).attr( "y", d=>d.y+4 )
.attr( "text-anchor", "middle" )
.attr( "font-size", 10 )
.text( d=>d.id );
} )
} );
}
此模拟使用了四种不同的交互:链接交互、软核排斥、长程排斥的“多体”交互和“居中”交互。模拟结果是非确定性的,若最终布局不理想,可重新运行模拟,通过打乱粒子和链接的顺序提供不同的起始配置。
3.4.2 动画粒子
function makeSimul() {
var ps = [ { x: 350, y: 300, vx: 0, vy: 1 },
{ x: 250, y: 300, vx: 0, vy: -1 } ];
var ln = [ { index: 0, source: ps[0], target: ps[1] } ];
var cs1 = d3.select( "#simul" ).select( "#c1" );
var cs2 = d3.select( "#simul" ).select( "#c2" );
var sim = d3.forceSimulation( ps )
.alphaDecay( 0 ).alphaMin( -1 ).velocityDecay( 0 )
.force("ln", d3.forceLink(ln).distance(50).strength(0.01))
.on( "tick", function() {
cs1.attr( "cx", ps[0].x ).attr( "cy", ps[0].y );
cs2.attr( "cx", ps[1].x ).attr( "cy", ps[1].y );
} );
}
该示例使用模拟框架创建物理系统的动画,两个通过弹簧连接的粒子相互反弹。模拟参数设置为不收敛,但由于
d3.forceLink()
的实现使用了预期更新算法,振荡会逐渐衰减。若要创建物理上准确的模拟,需编写自己的交互或修改现有交互。
4. 数组操作
D3包含一些改变数组结构的函数,以下是
d3.range()
函数的介绍:
| 函数 | 描述 |
| — | — |
|
d3.range(start, stop, step)
| 返回一个均匀间隔的数字数组,范围从
start
(包含)到
stop
(不包含),通过不断将
step
加到
start
得到。步长不必是整数,也可以为负数。若只提供一个参数,则视为
stop
,此时
start
默认值为 0,
step
默认值为 1;若提供两个参数,则视为
start
和
stop
,
step
仍默认值为 1。 |
5. 数组操作的更多应用与实践
5.1
d3.range()
函数的使用示例
以下通过几个简单的示例来进一步理解
d3.range()
函数的使用:
// 只提供一个参数
let arr1 = d3.range(5);
console.log(arr1); // 输出: [0, 1, 2, 3, 4]
// 提供两个参数
let arr2 = d3.range(2, 7);
console.log(arr2); // 输出: [2, 3, 4, 5, 6]
// 提供三个参数,且步长为负数
let arr3 = d3.range(10, 5, -1);
console.log(arr3); // 输出: [10, 9, 8, 7, 6]
通过这些示例可以清晰地看到
d3.range()
函数根据不同参数组合生成数组的方式。
5.2 数组操作在可视化中的应用
在数据可视化中,数组操作常常用于生成数据序列,为后续的可视化布局提供基础。例如,在创建柱状图时,可以使用
d3.range()
生成柱子的索引,然后结合其他 D3 函数进行数据绑定和图形绘制。以下是一个简单的柱状图示例:
// 生成数据
let data = d3.range(10).map(() => Math.random() * 100);
// 创建 SVG 元素
let svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 300);
// 创建比例尺
let xScale = d3.scaleBand()
.domain(d3.range(data.length))
.range([0, 500])
.padding(0.1);
let yScale = d3.scaleLinear()
.domain([0, d3.max(data)])
.range([300, 0]);
// 绘制柱子
svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", (d, i) => xScale(i))
.attr("y", d => yScale(d))
.attr("width", xScale.bandwidth())
.attr("height", d => 300 - yScale(d))
.attr("fill", "steelblue");
这个示例展示了如何使用
d3.range()
生成数据索引,结合比例尺和数据绑定来创建一个简单的柱状图。
6. 总结与展望
6.1 总结
本文介绍了 D3 在数据可视化中的多种应用,包括径向树绘制、包含层次结构的面积图创建、基于力的粒子排列以及数组操作。
-
径向树绘制
:通过合理选择极坐标范围,使用 D3 提供的函数和元素(如
<line>
和
<circle>
)可以实现径向树的可视化。
-
包含层次结构的面积图
:利用 D3 的节点函数(
count()
和
sum()
)和布局(如
d3.treemap()
)可以同时可视化组件的个体大小、累积大小和父子层次结构。
-
基于力的粒子排列
:D3 的模拟工具可以帮助处理复杂的元素排列问题,通过设置模拟参数和应用各种交互(如
d3.forceCenter()
、
d3.forceLink()
等)可以实现不同的布局效果。
-
数组操作
:
d3.range()
函数可以方便地生成均匀间隔的数字数组,为数据可视化提供基础。
6.2 展望
D3 在数据可视化领域具有强大的功能和广泛的应用前景。未来,可以进一步探索以下方面:
-
更复杂的布局和交互
:尝试使用更多的 D3 布局和交互函数,创建更加复杂和动态的可视化效果,如 3D 可视化、实时交互等。
-
与其他技术的结合
:将 D3 与其他前端技术(如 React、Vue 等)结合,提高可视化应用的开发效率和用户体验。
-
数据处理和分析
:结合 D3 的数据处理能力和其他数据分析工具,对数据进行更深入的挖掘和分析,为可视化提供更有价值的数据支持。
总之,D3 为数据可视化提供了丰富的工具和方法,通过不断学习和实践,可以创造出更加精彩和有价值的可视化作品。
6.3 流程图总结
graph LR
A[数据可视化] --> B[径向树绘制]
A --> C[包含层次结构的面积图]
A --> D[基于力的粒子排列]
A --> E[数组操作]
B --> B1[选择极坐标范围]
B --> B2[使用元素绘制]
C --> C1[使用节点函数]
C --> C2[应用布局]
D --> D1[设置模拟参数]
D --> D2[应用交互]
E --> E1[使用 d3.range() 生成数组]
E --> E2[应用于可视化]
这个流程图总结了本文介绍的主要内容和流程,从数据可视化的整体概念出发,分别介绍了不同的可视化方法及其具体步骤。
超级会员免费看
807

被折叠的 条评论
为什么被折叠?



