d3.js Zoomable Circle Packing 连线实现

本文介绍如何在D3.js的ZoomableCirclePacking气泡图中添加连线以展示节点间的层级关系,并通过计算调整连线端点位置提高用户体验。

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

一.背景

d3.js是一个很强大的js可视化工具,Zoomable Circle Packing是d3里面的一种可以展示层级关系的气泡图。

Zoomable Circle Packing地址:https://observablehq.com/@d3/zoomable-circle-packing

效果图:

现在的需求是在气泡图中添加连线,以展示层级关系中节点之间的关系,但是d3库中并没有提供这样的方法。

 

但是

 

《2020_IWoR_ClusterViz-Visualizing Architectural Refactoring Opportunities》这篇文章中同样使用了气泡图,并成功实现了连线(着实佩服。。。)

项目地址:https://github.com/FrankHZFang/ClusterViz

效果图:

 

这个已经基本满足需求了,但是有个问题是,每个连线的两端都是气泡的圆心,在连线很多的情况下,很影响体验,于是我就在原来的基础上,做了一些改进。

这是本人项目中改进前后的效果:

虽然没太大的意义,但是也算是一次学习的机会吧!

二.实现

d3的图都是在一个svg上画出来的,每个气泡都有自己的坐标,所以这个问题的本质其实就是直角坐标中的解方程问题。

原理如下:

只需要求出x_offsety_offset即可。

思路是通过两个气泡的坐标求出斜率,然后结合两个气泡的半径,通过勾股定理求出。

公示如下:

解得:

 

接下来就可以分为以下四种情况:

但是由于k是有正负之分的(y_offset也是),所以归根结底只有两种情况,即(x1, y1)和(x2, y2)的左右问题。

伪代码如下:

if(x1 < x2){
	x1 += x1_offset;
	y1 += y1_offset;
	x2 -= x2_offset;
	y2 -= y2_offset;
}else{
	x1 -= x1_offset;
	y1 -= y1_offset;
	x2 += x2_offset;
	y2 += y2_offset;
}

然后对k = 0k不存在这样的情况做一下讨论就可以了,这样处理完就可以实现文章开头提到的效果啦!

三.源码

由于项目特殊,有些代码是特殊需要,我在这里把核心的代码贴出来吧,本人初学js,代码有些笨重,又需要改进的地方还望指正!

//存放连线数据中所有处理好后的坐标信息
var circleCoordinate = [];
var links = svg_global.append('g')
    .style('stroke', '#aaa')
    .attr("class", "packageLink")
    .selectAll('line')
    .data(jsonLinks)          //连线数据
    .enter().append('line');


//对连线数据中的每个坐标进行处理
jsonLinks.forEach(function (d){
        //获取两个圆(source和target)的transform属性(包含坐标信息)和半径
        var source_transform = d3.select("#" + d.source_id).attr("transform");
        var target_transform = d3.select("#" + d.target_id).attr("transform");
        var r1 = d3.select("#" + d.source_id).attr("r");
        var r2 = d3.select("#" + d.target_id).attr("r");

        //求初始情况下的两个圆心坐标
        var x1 = parseFloat(source_transform.slice(source_transform.indexOf("(") + 1, source_transform.indexOf(",")));
        var y1 = parseFloat(source_transform.slice(source_transform.indexOf(",") + 1, source_transform.indexOf(")")));
        var x2 = parseFloat(target_transform.slice(target_transform.indexOf("(") + 1, target_transform.indexOf(",")));
        var y2 = parseFloat(target_transform.slice(target_transform.indexOf(",") + 1, target_transform.indexOf(")")));

        //求斜率(考虑斜率正无穷问题)
        if(x1 !== x2){
            var k = (y2 - y1) / (x2 - x1);
        }else{
            var k;
        }

        if(typeof(k) !== "undefined"){
            //求偏移量
            var x1_offset = Math.sqrt((r1 * r1) / (k * k + 1));
            var y1_offset = Math.sqrt((r1 * r1) / (k * k + 1)) * k;
            var x2_offset = Math.sqrt((r2 * r2) / (k * k + 1));
            var y2_offset = Math.sqrt((r2 * r2) / (k * k + 1)) * k;
            if(x1 > x2){
                x1 -= x1_offset;
                y1 -= y1_offset;
                x2 += x2_offset;
                y2 += y2_offset;
            }else{
                x1 += x1_offset;
                y1 += y1_offset;
                x2 -= x2_offset;
                y2 -= y2_offset;
            }
        }else{
            if(y1 > y2){
                y1 -= r1;
                y2 += r2;
            }else if(y1 < y2){
                y1 += r1;
                y2 -= r2;
            }
        }

        //存放每个坐标信息
        var temp_coordinate = {};
        temp_coordinate["id"] = d.source_id + "_" + d.target_id;
        temp_coordinate["x1"] = x1;
        temp_coordinate["y1"] = y1;
        temp_coordinate["x2"] = x2;
        temp_coordinate["y2"] = y2;

        //添加到数组
        circleCoordinate.push(temp_coordinate);
    })

    //下面四个方法分别取每个连线的x1,y1,x2,y2
    function getTranslateX1(source_id, target_id){
        var link_id = source_id + "_" + target_id;
        return circleCoordinate.find((n) => n.id === link_id).x1;
    }

    function getTranslateY1(source_id, target_id){
        var link_id = source_id + "_" + target_id;
        return circleCoordinate.find((n) => n.id === link_id).y1;
    }

    function getTranslateX2(source_id, target_id){
        var link_id = source_id + "_" + target_id;
        return circleCoordinate.find((n) => n.id === link_id).x2;
    }

    function getTranslateY2(source_id, target_id){
        var link_id = source_id + "_" + target_id;
        return circleCoordinate.find((n) => n.id === link_id).y2;
    }


    //对连线添加坐标信息,每添加时调用上方的四个方法,向数组中查询这个连线处理后的坐标信息
    links.attr("x1", function (d) {
        return getTranslateX1(d.source_id, d.target_id) + diameter_global / 2;
        })
        .attr("y1", function (d) {
            return getTranslateY1(d.source_id, d.target_id) + diameter_global / 2;
        })
        .attr("x2", function (d) {
            return getTranslateX2(d.source_id, d.target_id) + diameter_global / 2;
        })
        .attr("y2", function (d) {
            return getTranslateY2(d.source_id, d.target_id) + diameter_global / 2;
        });

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值