D3 二维图表的绘制系列(十七)树图

本文详细介绍了如何使用D3.js绘制基础树图,包括数据结构、关键代码实现及动画效果,展示了树图的节点渲染、文本标签及连线绘制过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上一篇: 矩形树图 https://blog.youkuaiyun.com/zjw_python/article/details/98489369

下一篇: 漏斗图 https://blog.youkuaiyun.com/zjw_python/article/details/98497967

代码结构和初始化画布的Chart对象介绍,请先看 https://blog.youkuaiyun.com/zjw_python/article/details/98182540

本图完整的源码地址: https://github.com/zjw666/D3_demo/tree/master/src/treeChart/basicTreeChart

1 图表效果

在这里插入图片描述

2 数据

{
    "name": "grandfather",
    "children": [
        {
            "name": "father",
            "children": [
                {
                    "name": "son",
                    "children": [
                        {"name": "grandson1", "house": 2},
                        {"name": "grandson2", "house": 3},
                        {"name": "grandson3", "house": 4}

                    ]
                }
            ]
        },
        {
            "name": "mother1",
            "children": [
                {
                    "name": "daughter1",
                    "children": [
                        {"name": "granddaughter1", "house": 4},
                        {"name": "granddaughter2", "house": 2}
                    ]
                },
                {
                    "name": "daughter2",
                    "children": [
                        {"name": "granddaughter3", "house": 4}
                    ]
                }
            ]
        },
        {
            "name": "mother2",
            "children": [
                {
                    "name": "son1",
                    "children": [
                        {"name": "grandson4", "house": 6},
                        {"name": "granddaughter4", "house": 1}
                    ]
                },
                {
                    
                    "name": "son2",
                    "children": [
                        {"name": "granddaughter5", "house": 2},
                        {"name": "grandson5", "house": 3},
                        {"name": "granddaughter5", "house": 2}
                    ]
                    
                }
            ]
        }

    ]
}

3 关键代码

导入数据

d3.json('./data.json').then(function(data){
....

一些样式参数配置,例如节点,线条的颜色等

const config = {
        margins: {top: 80, left: 50, bottom: 50, right: 50},
        textColor: 'black',
        title: '基础树图',
        hoverColor: 'gray',
        animateDuration: 1000,
        pointSize: 5,
        pointFill: 'white',
        pointStroke: 'red',
        paddingLeft: 20,
        lineStroke: 'gray'
    }

数据转换,树图与矩阵树图类似,都是树形层次性数据结构,因此需要将数据转化为一系列节点,对于树图,其布局算法是d3.tree,传入处理后的数据后,将自动为每个节点添加布局信息

/* ----------------------------数据转换------------------------  */
	chart._nodeId = 0;  //用于标识数据唯一性
	
    const root = d3.hierarchy(data);

    const generateTree = d3.tree()
                    .size([chart.getBodyHeight(), chart.getBodyWidth()*0.8]);
    
    generateTree(root);

渲染树的节点,其实渲染出树的节点很简单,直接使用circle元素就可以了,难点在于过渡的动画效果,我们想做出类似于Echart树图的效果,点击节点时会放缩子树,并重新布局。要做到这种效果就必须对节点的各个阶段,enterupdateexit添加对应的动画效果。

/* ----------------------------渲染节点------------------------  */
    chart.renderNode = function(){

        const groups = chart.body().selectAll('.g')
                                    .data(root.descendants(), (d) => d.id || (d.id = ++chart._nodeId));

        const groupsEnter = groups.enter()
                                    .append('g')
                                    .attr('class', (d) => 'g g-' + d.id)
                                    .attr('transform-origin', (d) => {    //子树从点击位置逐渐放大
                                        if (d.parent){
                                            return chart.oldY + config.paddingLeft + ' ' + chart.oldX;
                                        }
                                        return d.y + config.paddingLeft + ' ' + d.x;
                                    })
                                    .attr('transform', (d) => {    //首次渲染进入不放缩
                                        if (d.parent && chart.first) return 'scale(0.01)' + 'translate(' + (chart.oldY + config.paddingLeft) + ',' + chart.oldX + ')';
                                        return 'scale(1)' + 'translate(' + (d.y + config.paddingLeft) + ',' + d.x + ')';
                                    })
                      
              groupsEnter.append('circle')
                            .attr('r', config.pointSize)
                            .attr('cx', 0)
                            .attr('cy', 0)
                            .attr('fill', config.pointFill)
                            .attr('stroke', config.pointStroke);

              groupsEnter.merge(groups)
                            .transition().duration(config.animateDuration)
                            .attr('transform', (d) => 'translate(' + (d.y + config.paddingLeft) + ',' + d.x + ')')
                            .select('circle')
                                .attr('fill', (d) => d._children ? config.hoverColor : config.pointFill);
            
              groups.exit()   
                        .attr('transform-origin', (d) => (chart.targetNode.y + config.paddingLeft) + ' ' + chart.targetNode.x)  //子树逐渐缩小到新位置
                        .transition().duration(config.animateDuration)
                        .attr('transform', 'scale(0.01)')
                        .remove();
        

    }

树的节点渲染好了,那么节点的文本标签位置也就定了下来

/* ----------------------------渲染文本标签------------------------  */
    chart.renderText = function(){
        d3.selectAll('.text').remove();

        const groups = d3.selectAll('.g');

        groups.append('text')
              .attr('class', 'text')
              .text((d) => d.data.name.length<5?d.data.name:d.data.name.slice(0,3) + '...')
              .attr('dy', function(){
                  return chart.textDy || (chart.textDy = this.getBBox().height/4);
              })
              .attr('text-anchor', (d) =>{
                  return d.children ? 'end' : 'start';
              })
              .attr('dx', (d) =>{
                return d.children ? -config.pointSize*1.5 : config.pointSize*1.5;
            });
    }

接下来渲染节点之间的连线,这里使用d3.path绘制贝塞尔曲线,并选取两节点的中间点作为控制点,过渡动画效果与节点类似,通过scale实现

/* ----------------------------渲染连线------------------------  */
    chart.renderLines = function(){
        const nodesExceptRoot = root.descendants().slice(1);

        const links = chart.body().selectAll('.link')
                                .data(nodesExceptRoot, (d) => d.id || (d.id = ++chart._nodeId));
        
              links.enter()
                     .insert('path', '.g')
                     .attr('class', 'link')
                     .attr('transform-origin', (d) => {
                        if (d.parent){           //连线从点击位置逐渐放大
                            return chart.oldY + config.paddingLeft + ' ' + chart.oldX;
                        }
                        return d.y + config.paddingLeft + ' ' + d.x;
                    })
                    .attr('transform', (d) => {                //首次渲染进入不放缩
                        if (d.parent && chart.first) return 'scale(0.01)';
                        return 'scale(1)';
                    })
                   .merge(links)
                     .transition().duration(config.animateDuration)
                     .attr('d', (d) => {
                        return generatePath(d, d.parent);
                     })
                     .attr('transform', 'scale(1)')
                     .attr('fill', 'none')
                     .attr('stroke', config.lineStroke)
              
              links.exit()
                     .attr('transform-origin', (d) => {    //连线逐渐缩小到新位置
                         return chart.targetNode.y + config.paddingLeft + ' ' + chart.targetNode.x;
                     })
                     .transition().duration(config.animateDuration)
                     .attr('transform', 'scale(0.01)')
                     .remove();
        
        function generatePath(node1, node2){
            const path = d3.path();

            path.moveTo(node1.y + config.paddingLeft, node1.x);
            path.bezierCurveTo(
                                (node1.y + node2.y)/2 + config.paddingLeft, node1.x, 
                                (node1.y + node2.y)/2 + config.paddingLeft, node2.x, 
                                node2.y + config.paddingLeft, node2.x
                              );
            return path.toString();
        }
    }

最后绑定鼠标交互事件,当点击某个节点隐藏子树时,将其children属性设置为null,并暂存其子树数据,重新触发布局。当点击某各节点显现子树时,将暂存的子树数据拿出并重新赋值children属性,并重新布局,如此达到子树切换的效果。

/* ----------------------------绑定鼠标交互事件------------------------  */
    chart.addMouseOn = function(){

        d3.selectAll('.g circle')
            .on('click', function(d){
                toggle(d);
                generateTree(root);
                chart.renderNode();
                chart.renderLines();
                chart.renderText();
                chart.addMouseOn();
            });

        function toggle(d){
            chart.first = true;
            if (d.children){
                d._children = d.children;
                d.children = null;
            }else{
                d.children = d._children;
                d._children = null;
            }
            chart.oldX = d.x;  //点击位置x坐标
            chart.oldY = d.y;  //点击位置y坐标
            chart.targetNode = d;  //被点击的节点
        }
    }

大功告成!!!


如果觉得这篇文章帮助了您,请打赏一个小红包鼓励作者继续创作哦!!!

在这里插入图片描述

### 使用D3.js实现树图可视化 #### D3.js简介及其优势 D3.js是一个用于通过可编程方式操作基于数据的文档的JavaScript库。它利用HTML、SVG以及CSS来创建丰富的交互式图和图形界面[^1]。 相比于其他的数据可视化工具,如ECharts,在灵活性方面,D3.js提供了更底层的操作能力,允许开发者完全自定义视觉效果;而在性能现上,对于大规模复杂数据集处理时也展现出色的能力。特别是当涉及到定制化需求较高的场景下,比如构建特定结构的树形图,D3.js无疑是更好的选择。 #### 创建树图的基础准备 为了能够顺利地使用D3.js绘制树状图,建议先掌握以下几个方面的基础知识: - **熟悉HTML/CSS/JS**: 这些是Web前端开发的核心技术栈; - **理解JSON格式**: 大多数情况下,输入给D3.js的数据源都是以这种轻量级交换格式呈现出来的; - **学习SVG基本概念**: 作为矢量图形标准之一,SVG非常适合用来描述复杂的二维图像,而D3.js正是依赖于该技术来进行绘图工作。 #### 实现过程概述 具体到实际编码环节,则可以按照如下思路展开: ##### 准备环境并加载必要的资源文件 确保页面中已经引入了最新的d3.min.js版本,并设置好容器元素以便后续挂载生成的内容。 ```html <script src="https://d3js.org/d3.v7.min.js"></script> <div id="tree-container"></div> ``` ##### 定义数据模型 通常来说,树型结构可以用嵌套的对象数组示出来,其中每个对象代一个节点,包含名称和其他属性字段。 ```javascript const data = { name: &#39;root&#39;, children: [ {name: &#39;childA&#39;}, {name: &#39;childB&#39;, children:[ {name:&#39;subChild&#39;} ]} ] }; ``` ##### 构建布局算法实例 借助内置函数`d3.tree()`或者其变体形式(例如`d3.cluster()`),可以根据传入的数据自动计算出各个节点的位置坐标信息。 ```javascript let treeLayout = d3.tree().size([width, height]); // 或者采用聚类样式显示 // let clusterLayout = d3.cluster().size([height, width]); ``` ##### 绘制链接线与节点标记 最后一步就是运用之前提到过的SVG特性,分别针对连接边和顶点部分编写对应的DOM片段,再经由D3的选择器机制附加至目标区域之下。 ```javascript svg.append(&#39;g&#39;) .selectAll(&#39;.link&#39;) .data(treeLayout.links(nodes)) .enter() .append(&#39;path&#39;) .attr(&#39;class&#39;,&#39;link&#39;) .attr(&#39;d&#39;,d=>`M${d.source.x},${d.source.y}C${(d.target.x+d.source.x)/2},${d.source.y}, ${(d.target.x+d.source.x)/2},${d.target.y},${d.target.x},${d.target.y}`); svg.selectAll(&#39;.node&#39;) .data(nodes) .enter() .append(&#39;circle&#39;) .attr(&#39;r&#39;,5); ``` 上述代码展示了如何用最简单的线条和圆形来达父子关系链路及端点位置。 #### 高级功能拓展 除了以上介绍的基本形态之外,还可以进一步探索更加多样化的展现手法,例如带有标签说明的路径文字标注[^2],或是尝试不同的分支排列模式等,从而满足不同应用场景下的特殊要求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值