D3 二维图表的绘制系列(二十九)径向树图

上一篇: 关系图-弦图

代码结构和初始化画布的Chart对象介绍,请先看 这里

本图完整的源码地址: 这里

1 图表效果

在这里插入图片描述

2 数据

{
    "name": "letter",
    "children": [
        {
            "name": "a",
            "children": [
                {
                    "name": "a1",
                    "children": [
                        {"name": "a11", "value": 2},
                        {"name": "a12", "value": 3},
                        {"name": "a13", "value": 4},
                        {"name": "a14", "value": 5},
                        {"name": "a15", "value": 5},
                        {"name": "a16", "value": 7}
                    ]
                }
            ]
        },
        {
            "name": "b",
            "children": [
                {
                    "name": "b1",
                    "children": [
                        {"name": "b11", "value": 4},
                        {"name": "b12", "value": 2},
                        {"name": "b13", "value": 4},
                        {"name": "b14", "value": 4},
                        {"name": "b15", "value": 4},
                        {"name": "b16", "value": 4}
                    ]
                },
                {
                    "name": "b2",
                    "children": [
                        {"name": "b21", "value": 4},
                        {"name": "b22", "value": 5},
                        {"name": "b23", "value": 4},
                        {"name": "b24", "value": 4}
                    ]
                }
            ]
        },
        {
            "name": "c",
            "children": [
                {
                    "name": "c1",
                    "children": [
                        {"name": "c11", "value": 6},
                        {"name": "c12", "value": 1}
                    ]
                },
                {

                    "name": "c2",
                    "children": [
                        {"name": "c21", "value": 2},
                        {"name": "c22", "value": 3},
                        {"name": "c23", "value": 2},
                        {"name": "c24", "value": 3},
                        {"name": "c25", "value": 3},
                        {"name": "c26", "value": 3},
                        {"name": "c27", "value": 3},
                        {"name": "c28", "value": 3},
                        {"name": "c29", "value": 3}
                    ]

                }
            ]
        }

    ]
}

3 关键代码

导入数据

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

一些样式配置参数,与基础树图类似

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

首先运用d3.hierarchy将数据转换为节点树,接着调用d3.cluster定义树图的布局信息,注意径向树图的size参数不再是长宽,而是弧度和半径

/* ----------------------------数据转换------------------------  */
    chart._nodeId = 0;  //用于标识数据唯一性

    const root = d3.hierarchy(data);

    const generateTree = d3.cluster()
                    .nodeSize([10, 10])
                    .separation((a,b) => a.parent === b.parent ? 1 : 3)
                    .size([2 * Math.PI, d3.min([chart.getBodyWidth(), chart.getBodyHeight()]) / 2 * 0.8]);

    generateTree(root);

接着渲染节点,节点由circle元素表示,与基础树图的动画效果类似,要达到点击伸缩子树的功能,则需要对元素的enterexit阶段添加过渡效果,enter时子树要从点击位置逐渐放大,而exit时子树要逐渐缩小到父节点的新位置

/* ----------------------------渲染节点------------------------  */
    chart.renderNode = function(){
        let nodes = d3.select('.groups');

        if (nodes.empty()){
            nodes =  chart.body()
                        .append('g')
                        .attr('class', 'groups')
                        .attr('transform', 'translate(' + chart.getBodyWidth()/2 + ',' + chart.getBodyHeight()/2 + ')');
        }

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

        chart.groupsEnter = groups.enter()
                                    .append('g')
                                    .attr('class', (d) => 'g ' + d.data.name)
                                    .attr('transform', (d) => {
                                        if (chart.first) return 'translate(' + chart.oldY * Math.cos(chart.oldX - Math.PI/2) + ',' + chart.oldY * Math.sin(chart.oldX - Math.PI/2) + ')';  //首次渲染,子树从(0,0)点开始放缩,否则,从点击位置开始放缩
                                    })

        chart.groupsEnter.append('circle')
                            .attr('r', config.pointSize)
                            .attr('cx', 0)
                            .attr('cy', 0)
                            .attr('stroke', config.pointStroke);

        chart.groupsEnter.merge(groups)
                            .transition().duration(config.animateDuration)
                            .attr('transform', (d) => {
                                return 'translate(' + d.y * Math.cos(d.x - Math.PI/2) + ',' + d.y * Math.sin(d.x - Math.PI/2) + ')';
                            })
                            .selectAll('circle')
                                .attr('fill', (d) => d._children ? config.hoverColor: config.pointFill);

        groups.exit()
                .attr('transform-origin', () => chart.targetNode.y * Math.cos(chart.targetNode.x - Math.PI/2) + ' ' + chart.targetNode.y * Math.sin(chart.targetNode.x - Math.PI/2))  //子树逐渐缩小到新位置
                .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('x', 0)
                .attr('y', 0)
                .attr('class', 'text')
                .attr('text-anchor', 'middle')
                .attr('dy', 3)
                .text((d) => d.data.name)
                .attr('transform', function(d){
                    const offsetRadius = d.children? -(this.getBBox().width + config.pointSize) : this.getBBox().width + config.pointSize;
                    const translate = d.depth === 0 ? 'translate(' +  this.getBBox().width/2 + ',' + -this.getBBox().height/2 + ')' : 'translate(' + offsetRadius * Math.cos(d.x - Math.PI/2) + ',' + offsetRadius * Math.sin(d.x - Math.PI/2) + ')';
                    let angle = (180 * d.x / Math.PI) % 360;
                    if (angle > 90 && angle < 270) {
                        angle = angle + 180;
                    }
                    const rotate =  d.depth === 0 ? 'rotate(0)': 'rotate(' +  angle + ')';
                    return translate + ' ' + rotate;
                });
    }

渲染节点之间的连线,这里直接使用d3.linkRadial进行生成path,注意连线与节点类似,也需要相同的放缩过渡效果

/* ----------------------------渲染连线------------------------  */
    chart.renderLines = function(){

        let link = d3.select('.links');

        if (link.empty()){
            link =  chart.body()
                        .insert('g', '.groups')
                        .attr('class', 'links')
                        .attr('transform', 'translate(' + chart.getBodyWidth()/2 + ',' + chart.getBodyHeight()/2 + ')')
        }

        const links = link
                        .selectAll('.link')
                        .data(root.links().map((item) => {
                            item.id = item.source.id + '-' + item.target.id;   // 为链接添加id
                            return item;
                        }), (d) => d.id );

            links.enter()
                    .append('path')
                    .attr('class', 'link')
                    .attr('fill', 'none')
                    .attr('stroke', config.lineStroke)
                    .attr('transform-origin', () => chart.oldY * Math.cos(chart.oldX - Math.PI/2) + ' ' + chart.oldY * Math.sin(chart.oldX - Math.PI/2))
                    .attr('transform', 'scale(0.01)')
                  .merge(links)
                    .transition().duration(config.animateDuration)
                    .attr('transform', 'scale(1)')
                    .attr('d', d3.linkRadial()
                                .angle((d) => d.x)
                                .radius((d) => d.y)
                     );

            links.exit()
                    .attr('transform-origin', () => chart.targetNode.y * Math.cos(chart.targetNode.x - Math.PI/2) + ' ' + chart.targetNode.y * Math.sin(chart.targetNode.x - Math.PI/2))  //子树逐渐缩小到新位置
                    .transition().duration(config.animateDuration)
                    .attr('transform', 'scale(0.01)')
                    .remove();

    }

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

/* ----------------------------绑定鼠标交互事件------------------------  */
    chart.addMouseOn = function(){
        d3.selectAll('.g')
            .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;  //被点击的节点,该节点的x和y坐标随后将被更新
        }
    }

大功告成!!!

发布了250 篇原创文章 · 获赞 88 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/zjw_python/article/details/101280901
今日推荐