第一版
说明
- 实现纯展示
- 通过
tree() .size([width, height - 30])指定了树图的大小,从而使得节点的位置会自动居中和缩放
参考文章
d3 树状布局tree - 简书 (jianshu.com)
d3.js中的树状图(一)-秋天爱美丽-专业的技术网站 (qiutianaimeili.com) <-- 这个文章对于每一步的讲解很详细,但没有完整代码
d3.js中的hierarchy/stratify-秋天… <-- d3中需要将原始的树结构数据通过hierarchy进行一次转换,该文章介绍了hierarchy的详细用法和转换后节点的属性说明
效果
代码
<!--
TODO: 编写组件说明
@author pan
@date 2022-04-29
-->
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { select } from 'd3-selection'
import { hierarchy, HierarchyPointNode, tree } from 'd3-hierarchy'
const myContainerRef = ref<HTMLDivElement>()
onMounted(() => {
const myContainerDom = myContainerRef.value as HTMLDivElement
const data = {
name: '中国',
children: [
{
name: '浙江',
children: [
{ name: '杭州', value: 100 },
{ name: '宁波', value: 200 },
{ name: '温州', value: 30 },
{ name: '绍兴', value: 50 },
],
},
{
name: '广西',
children: [
{
name: '桂林',
children: [
{ name: '秀峰区', value: 20 },
{ name: '叠彩区', value: 130 },
{ name: '象山区', value: 140 },
{ name: '七星区', value: 10 },
],
},
{ name: '南宁', value: 90 },
{ name: '柳州', value: 88 },
{ name: '防城港', value: 99 },
],
},
{
name: '黑龙江',
children: [
{ name: '哈尔滨', value: 54 },
{ name: '齐齐哈尔', value: 2 },
{ name: '牡丹江', value: 42 },
{ name: '大庆', value: 43 },
],
},
{
name: '新疆',
children: [
{ name: '乌鲁木齐', value: 1 },
{ name: '克拉玛依', value: 10 },
{ name: '吐鲁番', value: 23 },
{ name: '哈密', value: 43 },
],
},
],
}
const width = 1666
const height = 280
const svg = select(myContainerDom)
.append('svg')
.classed('chart', true)
.attr('width', width)
.attr('height', height)
const g = svg.append('g').attr('transform', 'translate(0, 20)')
// 将原始的树结构数据,转换为Node(Object)数据
const hierarchyData = hierarchy(data)
console.log('——————d3.hierarchy(data)——————')
console.log(hierarchyData)
// 获取布局管理器
const treeLayout = tree()
.size([width, height - 30]) // 设置tree的大小.size表示树的宽度/高度,它们的大小直接影响到绘制的树的大小,因为绘制树的时候,是根据width和height来分配每个节点的位置,因此width和height越大,树越大;width和height越小,树越小:
.separation((a, b) => {
// 根据是否为同一父节点设置节点距离比例
return a.parent === b.parent ? 1 : 2
})
console.log('——————treeLayout——————')
console.log(treeLayout)
// 将Node(Object)数据转换为方便d3画图的数据
const nodesData = treeLayout(hierarchyData)
console.log('——————nodesData——————')
console.log(nodesData)
// 画线
const links = g
.selectAll('.links')
.data(nodesData.descendants().slice(1)) //nodesData.descendants()返回所有节点的数据,利于我们绑定数据,slcie(1)截取root后的全部节点,防止重绘
.enter()
.append('path') //用path画线
.attr('fill', 'none')
.attr('stroke', '#313131')
.attr('stroke-width', 2)
.attr('d', d => {
const parent: HierarchyPointNode<unknown> =
d.parent as HierarchyPointNode<unknown>
return `
M${d.x},${d.y}
C${d.x},${(d.y + parent.y) / 2}
${parent.x},${(d.y + parent.y) / 2.5}
${parent.x},${parent.y}`
})
// 画圆
//当一个节点中有多个子元素时(比如本例中有text和circle),我个人喜欢用g作为容器
const nodes = g
.selectAll('.node')
.data(nodesData.descendants()) //同样是获得所有节点,便于数据绑定
.enter()
.append('g')
.attr('transform', d => {
return `translate(${d.x}, ${d.y})` //位移
})
//画圆
nodes.append('circle').style('fill', '#c03027').attr('r', 10)
//插入文字
nodes
.append('text')
.attr('dx', '.9em')
.text(d => {
const originalData = d.data as any
return originalData.name
})
})
</script>
<template>
<div class="myDiv">
<h3>表格</h3>
<div ref="myContainerRef"></div>
</div>
</template>
<style lang="scss" scoped>
.myDiv {
margin-left: 10px;
}
</style>
第二版
说明
- 基于第一版,修改了连线的风格
- 删除了
tree().size(xxx)设置,增加了tree().nodeSize(xxx),由自己控制每个节点的宽高 - 通过设置
translate方式,解决设置了nodeSize之后,树图只展示了半边的问题。
参考文章
d3.js - 指定 nodeSize 时 d3 树的中心会发生变化
效果
代码
<!--
TODO: 编写组件说明
@author pan
@date 2022-04-29
-->
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { select } from 'd3-selection'
import { hierarchy, HierarchyPointNode, tree } from 'd3-hierarchy'
const myContainerRef = ref<HTMLDivElement>()
onMounted(() => {
const myContainerDom = myContainerRef.value as HTMLDivElement
const data = {
name: '中国',
children: [
{
name: '浙江',
children: [
{ name: '杭州', value: 100 },
{ name: '宁波', value: 200 },
{ name: '温州', value: 30 },
{ name: '绍兴', value: 50 },
],
},
{
name: '广西',
children: [
{
name: '桂林',
children: [
{ name: '秀峰区', value: 20 },
{ name: '叠彩区', value: 130 },
{ name: '象山区', value: 140 },
{ name: '七星区', value: 10 },
],
},
{ name: '南宁', value: 90 },
{ name: '柳州', value: 88 },
{ name: '防城港', value: 99 },
],
},
{
name: '黑龙江',
children: [
{ name: '哈尔滨', value: 54 },
{ name: '齐齐哈尔', value: 2 },
{ name: '牡丹江', value: 42 },
{ name: '大庆', value: 43 },
],
},
{
name: '新疆',
children: [
{ name: '乌鲁木齐', value: 1 },
{ name: '克拉玛依', value: 10 },
{ name: '吐鲁番', value: 23 },
{ name: '哈密', value: 43 },
],
},
],
}
const width = 1666
const height = 380
const svg = select(myContainerDom)
.append('svg')
.classed('chart', true)
.attr('width', width)
.attr('height', height)
const g = svg
.append('g')
.attr('transform', `translate(${width / 2}, ${height / 2 - 100})`)
// 将原始的树结构数据,转换为Node(Object)数据
const hierarchyData = hierarchy(data)
console.log('——————d3.hierarchy(data)——————')
console.log(hierarchyData)
// 获取布局管理器
const treeLayout = tree()
// .size([width, height - 30]) //设置tree的大小
.nodeSize([90, 60]) //设置tree的大小
.separation((a, b) => {
// 根据是否为同一父节点设置节点距离比例
return a.parent === b.parent ? 1 : 2
})
console.log('——————treeLayout——————')
console.log(treeLayout)
// 将Node(Object)数据转换为方便d3画图的数据
const nodesData = treeLayout(hierarchyData)
console.log('——————nodesData——————')
console.log(nodesData)
// 画线
const links = g
.selectAll('.links')
.data(nodesData.descendants().slice(1)) //nodesData.descendants()返回所有节点的数据,利于我们绑定数据,slcie(1)截取root后的全部节点,防止重绘
.enter()
.append('path') //用path画线
.attr('fill', 'none')
.attr('stroke', '#313131')
.attr('stroke-width', 2)
.attr('d', d => {
const parent: HierarchyPointNode<unknown> =
d.parent as HierarchyPointNode<unknown>
return `
M${parent.x},${parent.y}
L${parent.x},${parent.y + 25}
L${d.x},${parent.y + 25}
L${d.x},${d.y - 20}`
})
// 画圆
//当一个节点中有多个子元素时(比如本例中有text和circle),我个人喜欢用g作为容器
const nodes = g
.selectAll('.node')
.data(nodesData.descendants()) //同样是获得所有节点,便于数据绑定
.enter()
.append('g')
.attr('transform', d => {
return `translate(${d.x}, ${d.y})` //位移
})
//画圆
nodes.append('circle').style('fill', '#c03027').attr('r', 10)
//插入文字
nodes
.append('text')
.attr('dx', '.9em')
.text(d => {
const originalData = d.data as any
return originalData.name
})
})
</script>
<template>
<div class="myDiv">
<h3>表格</h3>
<div ref="myContainerRef"></div>
</div>
</template>
<style lang="scss" scoped>
.myDiv {
margin-left: 10px;
}
</style>
第三版
说明
基于第二版,增加如下控制
- 连线控制:只在根节点与直属的第一个子节点和最后一个子节点之间画连线(代码详见:
.attr('stroke-width', (d: any) => {...})) - 连线风格控制:连线的转折点不再是纯粹的直角(代码详见:
.attr('d', d => {...}))
效果
代码
<!--
TODO: 编写组件说明
@author pan
@date 2022-04-29
-->
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { select } from 'd3-selection'
import { hierarchy, HierarchyPointNode, tree } from 'd3-hierarchy'
const myContainerRef = ref<HTMLDivElement>()
onMounted(() => {
const myContainerDom = myContainerRef.value as HTMLDivElement
const data = {
name: '中国',
children: [
{
name: '浙江',
children: [
{ name: '杭州', value: 100 },
{ name: '宁波', value: 200 },
{ name: '温州', value: 30 },
{ name: '绍兴', value: 50 },
],
},
{
name: '广西',
children: [
{
name: '桂林',
children: [
{ name: '秀峰区', value: 20 },
{ name: '叠彩区', value: 130 },
{ name: '象山区', value: 140 },
{ name: '七星区', value: 10 },
],
},
{ name: '南宁', value: 90 },
{ name: '柳州', value: 88 },
{ name: '防城港', value: 99 },
],
},
{
name: '黑龙江',
children: [
{ name: '哈尔滨', value: 54 },
{ name: '齐齐哈尔', value: 2 },
{ name: '牡丹江', value: 42 },
{ name: '大庆', value: 43 },
],
},
{
name: '新疆',
children: [
{ name: '乌鲁木齐', value: 1 },
{ name: '克拉玛依', value: 10 },
{ name: '吐鲁番', value: 23 },
{ name: '哈密', value: 43 },
],
},
],
}
const width = 1666
const height = 380
const svg = select(myContainerDom)
.append('svg')
.classed('chart', true)
.attr('width', width)
.attr('height', height)
const g = svg
.append('g')
.attr('transform', `translate(${width / 2}, ${height / 2 - 100})`)
// 将原始的树结构数据,转换为Node(Object)数据
const hierarchyData = hierarchy(data)
console.log('——————d3.hierarchy(data)——————')
console.log(hierarchyData)
// 获取布局管理器
const treeLayout = tree()
// .size([width, height - 30]) //设置tree的大小
.nodeSize([90, 60]) //设置tree的大小
.separation((a, b) => {
// 根据是否为同一父节点设置节点距离比例
return a.parent === b.parent ? 1 : 2
})
// 将Node(Object)数据转换为方便d3画图的数据
const nodesData = treeLayout(hierarchyData)
console.log('——————nodesData——————')
console.log(nodesData)
// 画线
const links = g
.selectAll('.links')
.data(nodesData.descendants().slice(1)) //nodesData.descendants()返回所有节点的数据,利于我们绑定数据,slcie(1)截取root后的全部节点,防止重绘
.enter()
.append('path') //用path画线
.attr('fill', 'none')
.attr('stroke', '#313131')
.attr('stroke-width', (d: any) => {
// 这里是控制节点与节点之间连线的粗细(返回0表示连线不可见),这段逻辑是控制只在父节点和子中的第一个和最后一个节点画连线
if (
d.parent.children &&
(d === d.parent.children[0] ||
d === d.parent.children[d.parent.children.length - 1])
) {
return 2
} else {
return 0
}
})
.attr('d', d => {
const parent: HierarchyPointNode<unknown> =
d.parent as HierarchyPointNode<unknown>
// 这里的起点是父节点的x,y位置,终点是子节点的x,y位置
console.log(d, parent.x, d.x)
if (parent.x > d.x) {
// 这里是根节点到左子节点的连线
// 从父节点开始的第一条斜线的起点:L${parent.x},${parent.y + 20}
// 从父节点开始的第一条斜线的终点:L${parent.x - 10},${parent.y + 25}
// 从父节点开始的第二条斜线的起点:L${d.x + 5},${parent.y + 25}
// 从父节点开始的第二条斜线的终点:L${d.x},${d.y - 30}
return `
M${parent.x},${parent.y}
L${parent.x},${parent.y + 20}
L${parent.x - 10},${parent.y + 25}
L${d.x + 5},${parent.y + 25}
L${d.x},${d.y - 30}
L${d.x},${d.y - 15}`
} else {
// 这里是根节点到右子节点的连线
// 从父节点开始的第一条斜线的起点:L${parent.x},${parent.y + 20}
// 从父节点开始的第一条斜线的终点:L${parent.x + 10},${parent.y + 25}
// 从父节点开始的第二条斜线的起点:L${d.x - 5},${parent.y + 25}
// 从父节点开始的第二条斜线的终点:L${d.x},${parent.y + 30}
return `
M${parent.x},${parent.y}
L${parent.x},${parent.y + 20}
L${parent.x + 10},${parent.y + 25}
L${d.x - 5},${parent.y + 25}
L${d.x},${parent.y + 30}
L${d.x},${d.y - 15}`
}
})
// 画圆
//当一个节点中有多个子元素时(比如本例中有text和circle),我个人喜欢用g作为容器
const nodes = g
.selectAll('.node')
.data(nodesData.descendants()) //同样是获得所有节点,便于数据绑定
.enter()
.append('g')
.attr('transform', d => {
return `translate(${d.x}, ${d.y})` //位移
})
//画圆
nodes.append('circle').style('fill', '#c03027').attr('r', 10)
//插入文字
nodes
.append('text')
.attr('dx', '.9em')
.text(d => {
const originalData = d.data as any
return originalData.name
})
})
</script>
<template>
<div class="myDiv">
<h3>表格</h3>
<div ref="myContainerRef"></div>
</div>
</template>
<style lang="scss" scoped>
.myDiv {
margin-left: 10px;
}
</style>
第四版
说明
- 线风格改变:使用贝塞尔曲线,使线段的转折点更平滑
- 放弃transform方式使树居中显示,改为使用viewBox控制树居中显示(因为原来transform方式在加入了svg的拖动,缩放功能之后有问题,viewBox方式下则无问题)
- 加入svg整体的缩放和拖动功能
如果还不理解贝塞尔曲线在svg中实际怎么设置的请看我的这篇文章:d3中关于path中实际使用贝塞尔曲线的控制点解惑
参考文章
javascript - 如何根据内容设置 SVG(由 D3.js 绘制)的高度?
d3.js - 缩放和居中 D3-Graphviz Graph
效果
代码
<!--
TODO: 编写组件说明
@author pan
@date 2022-04-29
-->
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { select } from 'd3-selection'
import { hierarchy, HierarchyPointNode, tree } from 'd3-hierarchy'
import { zoom } from 'd3-zoom'
const myContainerRef = ref<HTMLDivElement>()
onMounted(() => {
const myContainerDom = myContainerRef.value as HTMLDivElement
const data = {
name: '中国',
children: [
{
name: '浙江',
children: [
{ name: '杭州', value: 100 },
{ name: '宁波', value: 200 },
{ name: '温州', value: 30 },
{ name: '绍兴', value: 50 },
],
},
{
name: '广西',
children: [
{
name: '桂林',
children: [
{ name: '秀峰区', value: 20 },
{ name: '叠彩区', value: 130 },
{ name: '象山区', value: 140 },
{ name: '七星区', value: 10 },
],
},
{ name: '南宁', value: 90 },
{ name: '柳州', value: 88 },
{ name: '防城港', value: 99 },
],
},
{
name: '黑龙江',
children: [
{ name: '哈尔滨', value: 54 },
{ name: '齐齐哈尔', value: 2 },
{ name: '牡丹江', value: 42 },
{ name: '大庆', value: 43 },
],
},
{
name: '新疆',
children: [
{ name: '乌鲁木齐', value: 1 },
{ name: '克拉玛依', value: 10 },
{ name: '吐鲁番', value: 23 },
{ name: '哈密', value: 43 },
],
},
],
}
const width = 1666
const height = 380
const svg = select(myContainerDom)
.append('svg')
.classed('chart', true)
.attr('width', width)
.attr('height', height)
.attr('viewBox', `${-(width / 2)} ${-(height / 2)} ${width} ${height}`)
const g = svg.append('g')
// .attr('transform', `translate(${width / 2}, ${height / 2 - 100})`)
svg.call(
//@ts-ignore
zoom().on('zoom', ev => {
g.attr('transform', ev.transform)
})
)
// 将原始的树结构数据,转换为Node(Object)数据
const hierarchyData = hierarchy(data)
console.log('——————d3.hierarchy(data)——————')
console.log(hierarchyData)
// 获取布局管理器
const treeLayout = tree()
// .size([width, height - 30]) //设置tree的大小
.nodeSize([90, 60]) //设置tree的大小
.separation((a, b) => {
// 根据是否为同一父节点设置节点距离比例
return a.parent === b.parent ? 1 : 2
})
// 将Node(Object)数据转换为方便d3画图的数据
const nodesData = treeLayout(hierarchyData)
console.log('——————nodesData——————')
console.log(nodesData)
// 画线
const links = g
.selectAll('.links')
.data(nodesData.descendants().slice(1)) //nodesData.descendants()返回所有节点的数据,利于我们绑定数据,slcie(1)截取root后的全部节点,防止重绘
.enter()
.append('path') //用path画线
.attr('fill', 'none')
.attr('stroke', '#313131')
.attr('stroke-width', (d: any) => {
// 这里是控制节点与节点之间连线的粗细(返回0表示连线不可见),这段逻辑是控制只在父节点和子中的第一个和最后一个节点画连线
if (
d.parent.children &&
(d === d.parent.children[0] ||
d === d.parent.children[d.parent.children.length - 1])
) {
return 2
} else {
return 0
}
})
.attr('d', d => {
//通过三次贝塞尔曲线设置连线的弯曲程度。M:move to,即到控制点 C后设置两个控制点及终点
const parent: HierarchyPointNode<unknown> =
d.parent as HierarchyPointNode<unknown>
// 这里线条的起点是父节点的x,y位置,终点是子节点的x,y位置
if (parent.x > d.x) {
// 这里是根节点到左子节点的连线
// 从父节点开始的第一条斜线的起点:L${parent.x},${parent.y + 20}
// 从父节点开始的第一条斜线的终点(通过贝塞尔曲线使得转折点是弯曲的):Q${parent.x - 5} ${parent.y + 25} ${parent.x - 10} ${parent.y + 25}
// 从父节点开始的第二条斜线的起点:L${d.x + 5},${parent.y + 25}
// 从父节点开始的第二条斜线的终点(通过贝塞尔曲线使得转折点是弯曲的):Q${d.x} ${d.y - 35} ${d.x} ${d.y - 30}
return `
M${parent.x},${parent.y}
L${parent.x},${parent.y + 20}
Q${parent.x - 5} ${parent.y + 25} ${parent.x - 10} ${parent.y + 25}
L${d.x + 5},${parent.y + 25}
Q${d.x} ${d.y - 35} ${d.x} ${d.y - 30}
L${d.x},${d.y - 15}`
} else {
// 这里是根节点到右子节点的连线
// 从父节点开始的第一条斜线的起点:L${parent.x},${parent.y + 20}
// 从父节点开始的第一条斜线的终点(通过贝塞尔曲线使得转折点是弯曲的):Q${parent.x + 5} ${parent.y + 25} ${parent.x + 10} ${parent.y + 25}
// 从父节点开始的第二条斜线的起点:L${d.x - 5},${parent.y + 25}
// 从父节点开始的第二条斜线的终点(通过贝塞尔曲线使得转折点是弯曲的):Q${d.x} ${parent.y + 25} ${d.x} ${parent.y + 30}
return `
M${parent.x},${parent.y}
L${parent.x},${parent.y + 20}
Q${parent.x + 5} ${parent.y + 25} ${parent.x + 10} ${parent.y + 25}
L${d.x - 5},${parent.y + 25}
Q${d.x} ${parent.y + 25} ${d.x} ${parent.y + 30}
L${d.x},${d.y - 15}`
}
})
// 画圆
//当一个节点中有多个子元素时(比如本例中有text和circle),我个人喜欢用g作为容器
const nodes = g
.selectAll('.node')
.data(nodesData.descendants()) //同样是获得所有节点,便于数据绑定
.enter()
.append('g')
.attr('transform', d => {
return `translate(${d.x}, ${d.y})` //位移
})
//画圆
nodes.append('circle').style('fill', '#c03027').attr('r', 10)
//插入文字
nodes
.append('text')
.attr('dx', '.9em')
.text(d => {
const originalData = d.data as any
return originalData.name
})
})
</script>
<template>
<div class="myDiv">
<h3>表格</h3>
<div ref="myContainerRef"></div>
</div>
</template>
<style lang="scss" scoped>
.myDiv {
margin-left: 10px;
}
</style>