携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情
引入D3模块
- 引入整个D3模块。
<!-- D3模块 -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://unpkg.com/d3-sankey@0"></script>
- 桑基图布局计算需要使用
d3.sankey()
函数。该函数不在d3
整体包中,需要单独引入。
数据
const data = {
nodes: [
{ name: 'a' },
{ name: 'b' },
{ name: 'c' },
{ name: 'd' },
{ name: 'e' },
{ name: 'f' },
{ name: 'g' },
{ name: 'h' },
{ name: 'i' }
],
links: [
{ source: 'a', target: 'd', value: 10 },
{ source: 'a', target: 'i', value: 2 },
{ source: 'b', target: 'd', value: 8 },
{ source: 'b', target: 'e', value: 6 },
{ source: 'c', target: 'e', value: 5 },
{ source: 'b', target: 'e', value: 2 },
{ source: 'b', target: 'i', value: 4 },
{ source: 'd', target: 'f', value: 3 },
{ source: 'd', target: 'g', value: 4 },
{ source: 'd', target: 'h', value: 5 },
{ source: 'e', target: 'g', value: 7 },
{ source: 'e', target: 'f', value: 3 },
{ source: 'e', target: 'h', value: 5 }
]
}
- 数据中
nodes
代表节点,links
代表连线数据。所以source
和target
的值必须是节点中的值。
添加画布
- 初始化画布。
// 画布
const width = 500
const height = 500
const margin = 30
const svg = d3
.select('.d3Chart')
.append('svg')
.attr('width', width)
.attr('height', height)
.style('background-color', '#1a3055')
// 图
const chart = svg.append('g').attr('transform', `translate(${margin}, ${margin})`)
比例尺和配置信息
const colorScale = d3.scaleOrdinal(d3.schemeSet3)
- 颜色比例尺。
const sankey = d3
.sankey()
.nodeWidth(30)
.nodePadding(20)
.size([width - 2 * margin, height - 2 * margin])
.nodeId((d) => d.name)
d3.sankey()
桑基图生成器。.nodeWidth(30)
设置节点的宽度。.nodePadding(30)
设置每列节点之间的垂直间隔。.size([x,y])
设置桑基图的范围。从[0,0]点到[x,y]。.nodeId()
设置了id,节点id访问器设置为指定的函数,并返回此Sankey生成器。
- 创建桑基图生成器。
const { nodes, links } = sankey({
nodes: data.nodes,
links: data.links
})
- 使用桑基图生成器,获取节点位置信息(
nodes
)和连线位置信息(links
)。
绘制桑基图
chart
.append('g')
.selectAll()
.data(nodes)
.join('g')
.attr('class', 'node')
.attr('indexName', (d) => d.name)
.append('rect')
.attr('fill', (d, i) => colorScale(d.name))
.attr('x', (d) => d.x0)
.attr('y', (d) => d.y0)
.attr('height', (d) => d.y1 - d.y0)
.attr('width', (d) => d.x1 - d.x0)
.append('title')
.text((d) => `${d.name}`)
- 创建一个节点绘制组,绑定节点数据(
nodes
)。 - 给每个节点创建一个绘制组,并在其内部绘制
rect
和title
。 indexName
为自定义属性用做标识符,在交互中使用。
chart
.append('g')
.attr('fill', 'none')
.selectAll()
.data(links)
.join('path')
.attr('indexName', (d) => d.source.name + '-' + d.target.name)
.attr('d', d3.sankeyLinkHorizontal())
.attr('stroke', (d, i) => colorScale(d.source.name))
.attr('stroke-width', (d) => d.width)
.attr('stroke-opacity', '0.5')
.append('title')
.text((d) => `${d.value.toLocaleString()}`)
d3.sankeyLinkHorizontal()
sankey 插件中的API,根据连线数据生成路径坐标点。
- 创建一个连线绘制组,绑定连线数据(
links
)。绘制路径图形形成连线。
chart
.selectAll('.node')
.append('text')
.attr('class', 'text')
.attr('x', (d) => (d.x0 + d.x1) / 2)
.attr('y', (d) => (d.y0 + d.y1) / 2)
.attr('stroke', '#000000')
.attr('text-anchor', 'middle')
.attr('dy', 6)
.text((d) => d.name)
- 通过标识符(
.node
)获取节点绘制组。节点对象已经和数据绑定过,这里可以直接得到数据进行文本添加。
添加交互
d3.selectAll('.node')
.on('mouseover', function (e, d) {
d3.selectAll('.node, path').attr('fill-opacity', '0.1').attr('stroke-opacity', '0.1')
d3.selectAll('[indexName*=' + d.name + ']')
.attr('fill-opacity', '1')
.attr('stroke-opacity', '0.6')
})
.on('mouseleave', function () {
d3.selectAll('.node, path').attr('fill-opacity', '1').attr('stroke-opacity', '0.5')
})
- 选中节点绘制组,添加鼠标事件。
- 修改所有节点和连线的透明度为
0.1
。通过属性选择获取当前节点相关的节点和连线,设置其透明度加深。
// 连线
d3.selectAll('path')
.on('mouseover', function (e, d) {
d3.selectAll('.node, path').attr('fill-opacity', '0.1').attr('stroke-opacity', '0.1')
const hoverNodes = d3.select(e.target).attr('stroke-opacity', '0.5').attr('indexName').split('-')
hoverNodes.forEach((name) => {
d3.selectAll('[indexName=' + name + ']').attr('fill-opacity', '1')
})
})
.on('mouseleave', function () {
d3.selectAll('.node, path').attr('fill-opacity', '1').attr('stroke-opacity', '0.5')
})
- 选中连线对象,添加鼠标事件。
- 修改所有节点和连线的透明度为
0.1
。获取当前自定义属性indexName
,因为连线是和两端节点都有关联,使用循环都进行透明度修改。