D3 二维图表的绘制系列(三十三)可缩放的矩形树图

上一篇: 可缩放的封闭图

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

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

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){
...

数据转换,这个与普通矩形树状图不同的地方在于,要处理每个子节点的布局信息,将其xy坐标都转化为相对于其父节点左上角坐标的布局,而不是整个画布。除此之外,还需要一个栈用来存储放缩下钻的层级

    /* ----------------------------数据转换------------------------  */
    const root = d3.hierarchy(data)
                    .sum((d) => d.house)
                    .sort((a,b) => a.value - b.value);

    const generateTreeMap = d3.treemap()
                    .tile(function(node, x0, y0, x1, y1){
                        d3.treemapBinary(node, 0, 0, width, height);
                        for (const child of node.children) {
                            child.x0 = x0 + child.x0 / width * (x1 - x0);
                            child.x1 = x0 + child.x1 / width * (x1 - x0);
                            child.y0 = y0 + child.y0 / height * (y1 - y0);
                            child.y1 = y0 + child.y1 / height * (y1 - y0);
                        }
                    })

    generateTreeMap(root);

    const scaleX = d3.scaleLinear().rangeRound([0, width]);
    const scaleY = d3.scaleLinear().rangeRound([0, height]);

    const stack = [root.data.name];

渲染矩形, 绑定click事件,点击子节点下钻层级,点击上方的白色横条用于返回上一层级。当下钻层级时,在当前点击节点的下面绘制其子节点,随后将其放大充满视图,并同时逐渐显示位于其下面的子节点,产生放大的特效。当返回上一层级时,绘制当前点击节点的兄弟节点,随后将当前节点缩小至原本大小,并逐渐显示兄弟节点,产生缩小的特效。我们可以看到,zoomOutzoomIn代码非常相似,因为放大和缩小都是通过改变scale函数的domain属性来巧妙实现的

/* ----------------------------渲染矩形------------------------  */

    chart.renderRect = function(group, currentRoot){

        const cells = group.selectAll('.cell')
                            .data(currentRoot.children.concat(currentRoot))
                            .join('g');

        cells.filter(d => d === currentRoot ? d.parent : d.children)  //可以点击的节点包括两类:有孩子的非当前根节点、有父节点的当前根节点
                .attr('cursor', 'pointer')
                .on('click', d => d === currentRoot ? zoomOut(currentRoot) : zoomIn(d))

        cells.attr('class', (d, i) => 'cell cell-' + i)
            .append('rect')
            .attr('fill', (d,i) => d === currentRoot ? 'white' : chart._colors(i % 10));

        cells.append('text')
            .attr('class', 'cell-text')
            .text((d) => d.data.name)
            .attr("x", 10)
            .attr("y", 20)
            .attr('stroke', config.textColor)
            .attr('fill', config.textColor)
            .text((d) => d === currentRoot ? stack.join(' -> ') : d.data.name);

        position(group, currentRoot);

        function position(group, currentRoot){
            group.selectAll('.cell')
                    .attr('transform', d => d === currentRoot ? `translate(0, -30)` : `translate(${scaleX(d.x0)},${scaleY(d.y0)})`)
                 .select('rect')
                    .attr('width', d => d === currentRoot ? width : scaleX(d.x1) - scaleX(d.x0))
                    .attr('height', d => d === currentRoot ? 30 : scaleY(d.y1) - scaleY(d.y0));
        }
        
        function zoomIn(d){
            stack.push(d.data.name);
            const oldGroup = group.attr('pointer-events', 'none');
            const newGroup = group = chart.body().append('g').attr('transform', 'translate(0, 15)').call(chart.renderRect, d);   //绘制当前点击的节点的子节点盖在当前节点上,子节点均在当前节点围成的矩形区域内

            scaleX.domain([d.x0, d.x1]);
            scaleY.domain([d.y0, d.y1]);

            chart.body().transition()
                        .duration(config.animateDuration)
                        .call(
                            t => oldGroup.transition(t)
                                    .call(position, d.parent)   //将当前点击的节点放大充满整个绘图区
                                    .remove()                   //移除当前点击的节点
                        )
                        .call(
                            t => newGroup.transition(t)
                                    .attrTween('opacity', () => d3.interpolate(0, 1))    //子节点逐渐显现
                                    .call(position, d)         //将子节点放大充满整个绘图区
                        );

        }

        function zoomOut(d){
            stack.pop();
            const oldGroup = group.attr('pointer-events', 'none');
            const newGroup = group = chart.body().append('g').attr('transform', 'translate(0, 15)').call(chart.renderRect, d.parent);  //绘制当前点击的节点的兄弟节点,兄弟节点均在绘图区外

            scaleX.domain([d.parent.x0, d.parent.x1]);
            scaleY.domain([d.parent.y0, d.parent.y1]);

            chart.body().transition()
                        .duration(config.animateDuration)
                        .call(
                            t => oldGroup.transition(t)
                                    .call(position, d)   //将当前点击的节点的子节点缩小至原本点击节点的大小
                                    .remove()
                        )
                        .call(
                            t => newGroup.transition(t)
                                    .attrTween('opacity', () => d3.interpolate(0, 1))   //点击节点和兄弟节点逐渐显示
                                    .call(position, d.parent)   //将点击节点和兄弟节点缩小充满整个绘图区
                        );
        }

    }

大功告成!!!


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

在这里插入图片描述

原创文章 255 获赞 103 访问量 29万+

猜你喜欢

转载自blog.csdn.net/zjw_python/article/details/105953169