d3.js - force力学图

现已升级为npm包,方便大家使用
新文章传送门


relation-chart

效果预览链接

可进行拖拽,缩放

d3.js - force力学图_第1张图片
demo.jpg

高亮选中的节点

d3.js - force力学图_第2张图片
高亮选中

以下是旧文章版本


D3.js V4版本详细文档

力模型:d3.forceSimulation([nodes])

用指定的节点数组创建一个新的力模拟。
默认为空数组。如果节点被指定时,将仿真的节点到指定对象数组,初始化它们的位置,
每个节点必须是一个对象,由力模型分配以下:

  • index-节点的从零开始的索引节点
  • x- 节点的当前x位置
  • y- 节点的当前y位置
  • vx- 节点当前的x- velocity
  • vy- 节点当前的y速度

力模型新加一种力

simulation.force(name,[force])函数
例如

//链接力
.force("link", d3.forceLink(config.links))
 // 万有引力
.force("charge", d3.forceManyBody().strength(-300))
// 用指定的x坐标和y坐标创建一个居中力。
.force("center", d3.forceCenter(config.width / 2, config.height / 2))
 //碰撞作用力,为节点指定一个radius区域来防止节点重叠,设置碰撞力的强度,范围[0,1], 默认为0.7。设置迭代次数,默认为1,迭代次数越多最终的布局效果越好,但是计算复杂度更高
 .force("collide", d3.forceCollide(100).strength(0.2).iterations(5))

tick计时器事件

simulation力模型会在运行中不停的调用 tick 事件,tick事件 收到 alpha系数的影响来衰减或停止

alpha系数

概念可能会比较理解,在计时器的每一帧中,仿真的alpha系数会不断削减,当alpha到达一个系数时,仿真将会停止,也就是alpha的目标系数alphaTarget,该值区间为[0,1]. 默认为0,控制力学模拟 alphaDecay 衰减率,[0-1] ,设为0则不停止 , 默认0.0228,直到0.001

  • alpha系数
  • alphaTarget 目标系数
  • alphaDecay 衰减率

力模型的停止与重启

力模型收到 alpha 影响会逐渐减弱,最后停止,也可以手动控制力模型的停止与重启

  • simulation.stop()
  • simulation.restart()

d3的拖拽事件 d3.drag()

3个事件阶段

  • start
  • drag
  • end

d3缩放平移事件 d3.zoom()

d3.zoom()
  //比例尺
  .scaleExtent([0.5,1.5])
  //transform
  .on("zoom",function(){
           this.g.attr("transform", d3.event.transform);
   });

将事件应用到 元素

常用 d3的 call() 函数来实现

 d3.select("#svg").append("circle").call(this.dra);

综合案例
案例演示

参考代码

/**
 * Created by PhpStorm.
 * User: admin
 * Date: 2018/3/22
 * Time: 13:31
 */

require('../css-module/relsMap-tooltip.less');
const d3 = require('d3');
const $ = require('jquery');

/**
 * 关系力导向图 - d3.js制作   (pc,wap通用)
 * @param id    {string} 父容器id
 * @param configPar      {JSON对象}    配置
 */
function MakeSvgPicClass(id, configPar) {
    this.config = {
        width: 375, // 总画布svg的宽
        height: 610,
        nodes: [],
        links: [],
        isHighLight: false ,    //是否启动 鼠标 hover 到节点上高亮与节点有关的节点,其他无关节点透明的功能
        isScale: true,      //是否启用缩放平移zoom功能
        scaleExtent: [0.5, 1.5],  //缩放的比例尺
        chargeStrength:-300,    //万有引力
        collide:60,        //碰撞力的大小 (节点之间的间距)
        nodeWidth: 120,     // 每个node节点所占的宽度,正方形
        margin: 20,     // node节点距离父亲div的margin
        alphaDecay:0.0228,  //控制力学模拟衰减率
        r: 30,      // 头像的半径 [30 - 45]
        relFontSize: 12 ,   //关系文字字体大小
        linkSrc: 30, // 划线时候的弧度
        linkColor:'#bad4ed',    //链接线默认的颜色
        strokeColor: '#7ecef4', // 头像外围包裹的颜色
        strokeWidth: 3, // 头像外围包裹的宽度
    };

    this.R = 30;  //画线时候的影响弧度
    $.extend(true, this.config, configPar);
    var _that = this;
    var timeout;//定时器

    
    // 1. 创建一个力学模拟器
    this.simulation = d3.forceSimulation(this.config.nodes)
    // simulation.force(name,[force])函数,添加某种力
        .force("link", d3.forceLink(this.config.links))
        // 万有引力
        .force("charge", d3.forceManyBody().strength(this.config.chargeStrength))
        // d3.forceCenter()用指定的x坐标和y坐标创建一个新的居中力。
        .force("center", d3.forceCenter(this.config.width / 2, this.config.height / 2))
        // 碰撞作用力,为节点指定一个radius区域来防止节点重叠,设置碰撞力的强度,范围[0,1], 默认为0.7。设置迭代次数,默认为1,迭代次数越多最终的布局效果越好,但是计算复杂度更高
        .force("collide", d3.forceCollide(this.config.collide).strength(0.2).iterations(5))
        // 在计时器的每一帧中,仿真的alpha系数会不断削减,当alpha到达一个系数时,仿真将会停止,也就是alpha的目标系数alphaTarget,该值区间为[0,1]. 默认为0,
        // 控制力学模拟衰减率,[0-1] ,设为0则不停止 , 默认0.0228,直到0.001
        .alphaDecay(this.config.alphaDecay)
        // 监听事件 ,tick|end ,例如监听 tick 滴答事件
        .on("tick", ()=>this.ticked());


    // 2.创建svg标签
    this.SVG = d3.select("#" + id).append("svg")
        .attr("class", "svgclass")
        .attr("width", this.config.width)
        .attr("height", this.config.height)
        // .transition().duration(750).call(d3.zoom().transform, d3.zoomIdentity);
        .call( d3.zoom().scaleExtent(this.config.scaleExtent).on("zoom", ()=> {
            if(this.config.isScale){
                _that.relMap_g.attr("transform", d3.event.transform);
            }
        }))
        .on('click',()=>$('.tooltip').remove())
        .on("dblclick.zoom", null);


    // 3.defs  标签的内容不会显示,只有调用的时候才显示
    this.defs=this.SVG.append('defs');
     // 3.1 添加箭头
    this.marker=this.defs
        .append("marker")
        .attr('id', "marker")
        .attr("markerWidth", 10)    //marker视窗的宽
        .attr("markerHeight", 10)   //marker视窗的高
        .attr("refX", this.config.r + 3*this.config.strokeWidth)            //refX和refY,指的是图形元素和marker连接的位置坐标
        .attr("refY", 4)
        .attr("orient", "auto")     //orient="auto"设置箭头的方向为自动适应线条的方向
        .attr("markerUnits", "userSpaceOnUse")  //marker是否进行缩放 ,默认值是strokeWidth,会缩放
        .append("path")
        .attr("d", "M 0 0 8 4 0 8Z")    //箭头的路径 从 (0,0) 到 (8,4) 到(0,8)
        .attr("fill", "steelblue");

    // 3.2 添加多个头像图片的 
    this.patterns = this.defs
        .selectAll("pattern.patternclass")
        .data(this.config.nodes)
        .enter()
        .append("pattern")
        .attr("class", "patternclass")
        .attr("id", function (d, index) {
            return 'avatar' + id + d.id;
        })
        // 两个取值userSpaceOnUse  objectBoundingBox
        .attr('patternUnits', 'objectBoundingBox')
        // ,x、y值的改变决定图案的位置,宽度、高度默认为pattern图案占填充图形的百分比。
        .attr("x", "0")
        .attr("y", "0")
        .attr("width", "1")
        .attr("height", "1");

    // 3.3 向 - 添加 头像
    this.patterns.append("image")
        .attr("class", "circle")
        .attr("xlink:href", function (d) {
            return d.avatar || "https://static.linkeddb.com/m/images/none.jpg"; // 修改节点头像
        })
        .attr("src", function (d) {
            return d.avatar || "https://static.linkeddb.com/m/images/none.jpg"; // 修改节点头像
        })
        .attr("height", this.config.r*2)
        .attr("width", this.config.r*2)
        .attr("preserveAspectRatio", "xMidYMin slice");

    // 3.4 名字
    this.patterns.append("rect").attr("x", "0").attr("y", 4/3*this.config.r).attr("width", 2*this.config.r).attr("height", 2/3*this.config.r).attr("fill", "black").attr("opacity", "0.5");
    this.patterns.append("text").attr("class", "nodetext")
        .attr("x", this.config.r).attr("y", (5/3*this.config.r))
        .attr('text-anchor', 'middle')
        .attr("fill", "#fff")
        .style("font-size", this.config.r/3)
        .text(function (d) {
            return d.name;
        });

    

    // 4.放关系图的容器
    this.relMap_g = this.SVG.append("g")
        .attr("class", "relMap_g")
        .attr("width", this.config.width)
        .attr("height", this.config.height);


    // 5.关系图添加线
    // 5.1  每条线是个容器,有线 和一个装文字的容器
    this.edges = this.relMap_g
    .selectAll("g.edge")
    .data(this.config.links)
    .enter()
    .append("g")
    .attr("class","edge")
    .on('mouseover', function () {
        d3.select(this).selectAll('path.links').attr('stroke-width', 4);
    })
    .on('mouseout', function () {
        d3.select(this).selectAll('path.links').attr('stroke-width', 1);
    })
    .on('click',function (d) {
        // $('.no-more').removeClass('hide').addClass('prompt');
        // if(timeout){
        //     clearTimeout(timeout);
        // }
        // console.log(d);
        // $('.no-more').html(`${d.source.name}   ${d.type}  ${d.target.name} `);
        // timeout=setTimeout(()=>$('.no-more').addClass('hide'),3000);
        
        // 浮窗展示
        event = d3.event || window.event;
        var pageX = event.pageX ? event.pageX : (event.clientX + (document.body.scrollLeft || document.documentElement.scrollLeft));
        var pageY = event.pageY ? event.pageY : (event.clientY + (document.body.scrollTop || document.documentElement.scrollTop));
        // console.log('pagex',pageX);
        // console.log('pageY',pageY);
        //阻止事件冒泡  阻止事件默认行为
        event.stopPropagation ? (event.stopPropagation()) :  (event.cancelBubble = true);
        event.preventDefault ? (event.preventDefault()) : (event.returnValue = false);

        var _html=`${d.source.name} `;
        
        _html+=` > ${d.target.name} : ${d.type}`;

        d3.selectAll('.tooltip').remove();
        var _div=d3.select('#'+id).append('div')
                .attr('class','tooltip')
                .html(_html);

        var _width=parseInt(_div.style('width'));
        console.log('width',_width);

        //判断浮窗的左上角坐标 ,如果太靠右侧/底部 边缘 ,浮窗位置平移
        console.log('dw'+document.body.clientWidth);
        if(document.body.clientWidth - pageX < _width){
            pageX=document.body.clientWidth - _width -5;
        }
        if(document.body.clientHeight - pageY < 50){
            pageY=document.body.clientHeight-50;
        }

        _div.style('top',pageY+'px')
        .style('left',pageX+'px');
    })
    .attr('fill', function (d) {
        var str = '#bad4ed';
        if (d.color) {
            str = "#" + d.color;
        }
        return str;
    })

    // 5.2 添加线
    this.links=this.edges.append("path").attr("class","links")
    .attr("d", d=> {return "M" + this.R + "," + 0 + " L" + getDis(d.source, d.target) + ",0";})
    .style("marker-end", "url(#marker)")
    // .attr("refX",this.config.r)
    .attr('stroke', (d)=> {
        var str=d.color ? "#"+d.color : this.config.linkColor ;
        return str;
    });

    // 5.3 添加关系文字的容器
    this.rect_g=this.edges.append("g").attr("class", "rect_g");

   // 5.4 添加rect
    this.rects =this.rect_g.append("rect")
    .attr("x", 40)
    .attr("y", -10)
    .attr("width", 40)
    .attr("height", 20)
    .attr("fill", "white")
    .attr('stroke',  (d)=> {
        var str=d.color ? "#"+d.color : this.config.linkColor ;
        return str;
    })

    // 5.5 文本标签  坐标(x,y)代表 文本的左下角的点
    this.texts = this.rect_g.append("text")
    .attr("x", 40)
    .attr("y", 5)
    .attr("text-anchor", "middle")  // 文本中轴对齐方式居中  start | middle | end
    .style("font-size",12).text(d=>{return d.type});


    // 6.关系图添加用于显示头像的节点
        this.circles =  this.relMap_g.selectAll("circle.circleclass")
            .data(this.config.nodes)
            .enter()
            .append("circle")
            .attr("class","circleclass")
            .style("cursor", "pointer")
            // .attr("cx", function (d) {
            //     return d.x;
            // })
            // .attr("cy", function (d) {
            //     return d.y;
            // })
            .attr("fill", function (d) {
                return ("url(#avatar" + id + d.id + ")");
            })
            .attr("stroke", "#ccf1fc")
            .attr("stroke-width", this.config.strokeWidth)
            .attr("r", this.config.r)
            .on('mouseover', function(d){
                d3.select(this).attr('stroke-width', '8');
                d3.select(this).attr('stroke', '#a3e5f9');
                if(_that.config.isHighLight){
                    _that.highlightObject(d);
                }
            })
            .on('mouseout', function(d){
                d3.select(this).attr('stroke-width', _that.config.strokeWidth);
                d3.select(this).attr('stroke', '#c5dbf0');
                if(_that.config.isHighLight){
                    _that.highlightObject(null);
                }
            })
            .on('click', function (d) {

                // // 展示方式1 :屏幕下方bar
                // $('.no-more').removeClass('hide');
                // if(timeout){
                //     clearTimeout(timeout);
                // }
                // if(d.exdata){
                //     $('.no-more').html(`${d.exdata.name}  饰  ${d.name} `);
                // }else {
                //     $('.no-more').html(`${d.name}`);
                // }
                // timeout=setTimeout(()=>$('.no-more').addClass('hide'),3000);

                // 展示方式2 :浮窗展示
                event = d3.event || window.event;
                var pageX = event.pageX ? event.pageX : (event.clientX + (document.body.scrollLeft || document.documentElement.scrollLeft));
                var pageY = event.pageY ? event.pageY : (event.clientY + (document.body.scrollTop || document.documentElement.scrollTop));
                // console.log('pagex',pageX);
                // console.log('pageY',pageY);
                //阻止事件冒泡  阻止事件默认行为
                event.stopPropagation ? (event.stopPropagation()) :  (event.cancelBubble = true);
                event.preventDefault ? (event.preventDefault()) : (event.returnValue = false);

                var _html=`${d.name}`;
                if(d.exdata){
                    _html+=`  (${d.exdata.name} 饰)`;
                }


                d3.selectAll('.tooltip').remove();
                var _div=d3.select('#'+id).append('div')
                        .attr('class','tooltip')
                        .html(_html);

                var _width=parseInt(_div.style('width'));
                console.log('width',_width);

                //判断浮窗的左上角坐标 ,如果太靠右侧/底部 边缘 ,浮窗位置平移
                console.log('dw'+document.body.clientWidth);
                if(document.body.clientWidth - pageX < _width){
                    pageX=document.body.clientWidth - _width -5;
                }
                if(document.body.clientHeight - pageY < 50){
                    pageY=document.body.clientHeight-50;
                }

                _div.style('top',pageY+'px')
                    .style('left',pageX+'px');
                
            })
            .on('mousedown', function (d) {     //监听鼠标落下
                // console.log(event);
                // console.log(window.event);
                // event = event || window.event;
                // var btnNum = event.button;
                // if (btnNum == 2) {
                //     console.log("您点击了鼠标右键!");
                // }

                // if (window.Event)
                //     document.captureEvents(Event.MOUSEUP);
                // var canClick=false;
                // function nocontextmenu()
                // {
                //     if(canClick)return;
                //     event.cancelBubble = true
                //     event.returnValue = false;
                //     canClick=true;
                //     return false;
                // }
                // function norightclick(e)
                // {
                //     if (window.Event)
                //     {
                //         if (e.which == 2 || e.which == 3)
                //             return false;
                //     }
                //     else if (event.button == 2 || event.button == 3)
                //     {
                //         event.cancelBubble = true;
                //         event.returnValue = false;
                //         return false;
                //     }
                // }
                // document.oncontextmenu = nocontextmenu;  // for IE5+
                // document.onmousedown = norightclick;  // for all others
            })
            .on('contextmenu', function () {    //鼠标右键菜单
                // event = event || window.event;
                // event.cancelBubble = true;
                // event.returnValue = false;
                // var pageX = event.pageX ? event.pageX : (event.clientX + (document.body.scrollLeft || document.documentElement.scrollLeft));
                // var pageY = event.pageY ? event.pageY : (event.clientY + (document.body.scrollTop || document.documentElement.scrollTop));
                // $('#myContextMenu').css('left', pageX);
                // $('#myContextMenu').css('top', pageY);
                // $('#myContextMenu').css('visibility ', 'visible');
                // $('#myContextMenu').show();
                // return false;
            })
            // 应用 自定义的 拖拽事件
            .call(d3.drag()
            .on('start', function (d) {
                d3.event.sourceEvent.stopPropagation();
                // restart()方法重新启动模拟器的内部计时器并返回模拟器。
                // 与simulation.alphaTarget或simulation.alpha一起使用时,此方法可用于在交互
                // 过程中进行“重新加热”模拟,例如在拖动节点时,在simulation.stop暂停之后恢复模拟。
                // 当前alpha值为0,需设置alphaTarget让节点动起来
                if (!d3.event.active) _that.simulation.alphaTarget(0.3).restart();
                d.fx = d.x;
                d.fy = d.y;
            })
            .on('drag', function (d) {
    
                // d.fx属性- 节点的固定x位置
                // 在每次tick结束时,d.x被重置为d.fx ,并将节点 d.vx设置为零
                // 要取消节点,请将节点 .fx和节点 .fy设置为空,或删除这些属性。
                d.fx = d3.event.x;
                d.fy = d3.event.y;
            })
            .on('end', function (d) {
    
                // 让alpha目标值值恢复为默认值0,停止力模型
                if (!d3.event.active) _that.simulation.alphaTarget(0);
                d.fx = null;
                d.fy = null;
            }));




    

    // 7.动态改变 线和节点的 位置
    // tick心跳函数
    this.ticked = ()=>{

        // 7.1 修改每条容器edge的位置
        this.edges.attr("transform", function (d) {
            return getTransform(d.source, d.target, getDis(d.source, d.target))
        });

        // 7.2 修改每条线link位置
        this.links.attr("d", d=> {return "M" + this.R + "," + 0 + " L" + getDis(d.source, d.target) + ",0";})
    
        
        // 7.3 修改线中关系文字text的位置 及 文字的反正
        this.texts
        .attr("x", function(d){
            // 7.3.1 根据字的长度来更新兄弟元素 rect 的宽度
            var bbox=d3.select(this).node().getBBox();
            var width = bbox.width;
            $(this).prev('rect').attr('width',width+10);   
            // 7.3.2 更新 text 的位置
            return getDis(d.source, d.target) / 2})
        .attr("transform", function (d) {
            // 7.3.3 更新文本反正
            if (d.target.x < d.source.x) {
                var x = getDis(d.source, d.target) / 2;
                return 'rotate(180 ' + x + ' ' + 0 + ')';
            } else {
                return 'rotate(0)';
            }
        });

        // 7.4 修改线中装文本矩形rect的位置
        this.rects 
        .attr("x", function(d){return getDis(d.source, d.target) / 2  - $(this).attr('width')/2})    // x 坐标为两点中心距离减去自身长度一半

        // 5.修改节点的位置
        this.circles
            .attr("cx", function (d) {
                return d.x;
            })
            .attr("cy", function (d) {
                return d.y;
            })



    };




















    /**
     *  关于缩放平移踩的大坑
     *  1. 改变需要缩放平移对象的transform来实现 ,本例子是 this.relMap_g
     *  2. 但是zoom 事件的监听应该放到整个的svg上 ,本例子是 this.SVG
     *  3. 这样 监听出来的 d3.event.transform 的 transform 数值才是正确的
     *  4. 直接把zoom 事件监听 this.relMap_g 得到的 transform 数值会爆炸大,移动非常不平顺,瞬移消失在视野
     *
     *  d3.event.transform{
     *      k:1    // 当前比例尺
     *      x:0    // x 方向移动的大小
     *      y:0    // y 方向移动的大小
     *  }
     * */
    this.zoom = d3.zoom()
        .scaleExtent(this.config.scaleExtent)
        .on("zoom", ()=> {
            if(this.config.isScale){
                _that.relMap_g.attr("transform", d3.event.transform);
            }
        });


    /**
     * d3 拖拽事件drag event 参数
     *   target- 相关的拖动行为。
     *   type - 字符串“start”, “drag” or “end”;  请参阅drag .on。
     *   subject- 拖动主题,由drag .subject定义。
     *   x- 主题的新x坐标; 请参阅drag .container。
     *   y- 主题的新y坐标; 请参阅drag .container。
     *   dx- 自上次拖动事件以来x坐标的变化。
     *   dy- 自上次拖动事件以来y坐标的变化。
     *   identifier- 字符串“鼠标”或数字触摸标识符。
     *   active - 当前活动拖动手势的数量(在开始和结束时,不包括这一个)。
     *   sourceEvent - 基础输入事件,如mousemove或touchmove。
     * */
    // d3.drag() 创建一个新的拖动行为。返回的行为拖动既是对象又是函数,通常通过选择 .call应用于选定的元素。
    // start - 新指针变为活动状态后(在mousedown或touchstart上)
    // drag - 活动指针移动后(在mousemove或touchmove上)。
    // end - 活动指针变为非活动状态后(在mouseup,touchend或touchcancel上)。
    this.drag =()=>{ 
        return d3.drag()
        .on('start', function (d) {
            d3.event.sourceEvent.stopPropagation();
            // restart()方法重新启动模拟器的内部计时器并返回模拟器。
            // 与simulation.alphaTarget或simulation.alpha一起使用时,此方法可用于在交互
            // 过程中进行“重新加热”模拟,例如在拖动节点时,在simulation.stop暂停之后恢复模拟。
            // 当前alpha值为0,需设置alphaTarget让节点动起来
            if (!d3.event.active) _that.simulation.alphaTarget(0.3).restart();
            d.fx = d.x;
            d.fy = d.y;
        })
        .on('drag', function (d) {

            // d.fx属性- 节点的固定x位置
            // 在每次tick结束时,d.x被重置为d.fx ,并将节点 d.vx设置为零
            // 要取消节点,请将节点 .fx和节点 .fy设置为空,或删除这些属性。
            d.fx = d3.event.x;
            d.fy = d3.event.y;
        })
        .on('end', function (d) {

            // 让alpha目标值值恢复为默认值0,停止力模型
            if (!d3.event.active) _that.simulation.alphaTarget(0);
            d.fx = null;
            d.fy = null;
        });
    }

    /*
     * 高亮和取消高亮
     * */
    this.highlighted = null;
    this.dependsNode = [];
    this.dependsLinkAndText = [];
    this.highlightObject = function (obj) {
        if (obj) {
            var objIndex = obj.index;
            _that.dependsNode = _that.dependsNode.concat([objIndex]);
            _that.dependsLinkAndText = _that.dependsLinkAndText.concat([objIndex]);
            _that.config.links.forEach(function (lkItem) {
                if (objIndex == lkItem['source']['index']) {
                    _that.dependsNode = _that.dependsNode.concat([lkItem.target.index]);
                } else if (objIndex == lkItem['target']['index']) {
                    _that.dependsNode = _that.dependsNode.concat([lkItem.source.index]);
                }
            });

            // 隐藏节点
            _that.SVG.selectAll('circle').filter(function (d) {
                return (_that.dependsNode.indexOf(d.index) == -1);
            }).transition().style('opacity', 0.1);
            // 隐藏线
            _that.SVG.selectAll('.edge').filter(function (d) {
                // return true;
                return ((_that.dependsLinkAndText.indexOf(d.source.index) == -1) && (_that.dependsLinkAndText.indexOf(d.target.index) == -1))
            }).transition().style('opacity', 0.1);

        } else {
            // 取消高亮
            // 恢复隐藏的线
            _that.SVG.selectAll('circle').filter(function () {
                return true;
            }).transition().style('opacity', 1);
            // 恢复隐藏的线
            _that.SVG.selectAll('.edge').filter(function (d) {
                // return true;
                return ((_that.dependsLinkAndText.indexOf(d.source.index) == -1) && (_that.dependsLinkAndText.indexOf(d.target.index) == -1))
            }).transition().style('opacity', 1);
            _that.highlighted = null,
                _that.dependsNode = [],
                _that.dependsLinkAndText = [];
        }
    };

}


// 求两点间的距离
function getDis(s, t) {
    return Math.sqrt((s.x - t.x) * (s.x - t.x) + (s.y - t.y) * (s.y - t.y));
}

// 求元素移动到目标位置所需要的 transform 属性值
function getTransform(source, target, _dis) {
    var r;
    if (target.x > source.x) {
        if (target.y > source.y) {
            r = Math.asin((target.y - source.y) / _dis)
        } else {
            r = Math.asin((source.y - target.y) / _dis);
            r = -r;
        }
    } else {
        if (target.y > source.y) {
            r = Math.asin((target.y - source.y) / _dis);
            r = Math.PI - r;
        } else {
            r = Math.asin((source.y - target.y) / _dis);
            r -= Math.PI;
        }
    }
    r = r * (180 / Math.PI);
    return "translate(" + source.x + "," + source.y + ")rotate(" + r + ")";
}

module.exports = {
    D3MakeSvgPicClass: MakeSvgPicClass
};

数据格式模板

{"nodes": [{"id": 41, "oid": "5a56e22893cc2b42a6bf001f", "type": "role", "name": "\u5468\u83b9", "avatar": "https://i.linkeddb.com/upload/image/2017920/140646844806.jpg", "url": "/movie/role/5a56e22893cc2b42a6bf001f/", "exdata": {"id": 67864, "oid": "59fa71e218521569b662b81e", "name": "\u5b59\u4fea", "avatar": "https://i.linkeddb.com/person2/070d/d9c3/0fe9aa40e9c3014d23fadcce.jpg"}}, {"id": 42, "oid": "5a56e22893cc2b42a6bf0020", "type": "role", "name": "\u6c88\u661f\u79fb", "avatar": "https://i.linkeddb.com/upload/image/2017920/140848800133.jpg", "url": "/movie/role/5a56e22893cc2b42a6bf0020/", "exdata": {"id": 146808, "oid": "59fa798518521569b66a0707", "name": "\u9648\u6653", "avatar": "https://i.linkeddb.com/person2/5fe0/efe2/375e86834b0fd8e423ed46ad.jpg"}}, {"id": 44, "oid": "5a56e22893cc2b42a6bf0022", "type": "role", "name": "\u8d75\u767d\u77f3", "avatar": "https://i.linkeddb.com/upload/image/2017920/141152124754.jpg", "url": "/movie/role/5a56e22893cc2b42a6bf0022/", "exdata": {"id": 77558, "oid": "59fa72aa18521569b6637db9", "name": "\u4efb\u91cd", "avatar": "https://i.linkeddb.com/person2/f033/143e/fd25ff01c512e568bedb139f.jpg"}}, {"id": 49, "oid": "5a56e22893cc2b42a6bf0027", "type": "role", "name": "\u5468\u8001\u56db", "avatar": "https://i.linkeddb.com/upload/image/2017920/143059527666.jpg", "url": "/movie/role/5a56e22893cc2b42a6bf0027/", "exdata": {"id": 18420, "oid": "59fa6ee218521569b65fc164", "name": "\u5218\u4f69\u7426", "avatar": "https://i.linkeddb.com/person2/6dc0/474c/800b91826457c39b325db6d1.jpg"}}, {"id": 43, "oid": "5a56e22893cc2b42a6bf0021", "type": "role", "name": "\u5434\u8058", "avatar": "https://i.linkeddb.com/upload/image/2017920/141041905393.jpg", "url": "/movie/role/5a56e22893cc2b42a6bf0021/", "exdata": {"id": 76294, "oid": "59fa729b18521569b6636ff3", "name": "\u4f55\u6da6\u4e1c", "avatar": "https://i.linkeddb.com/person2/b4ab/93bf/76a67ed28d5bcf5e6e08d49e.jpg"}}, {"id": 50, "oid": "5a56e22893cc2b42a6bf0028", "type": "role", "name": "\u6c88\u56db\u6d77", "avatar": "https://i.linkeddb.com/upload/image/2017920/143624653261.jpg", "url": "/movie/role/5a56e22893cc2b42a6bf0028/", "exdata": {"id": 20839, "oid": "59fa6eff18521569b65fe02c", "name": "\u8c22\u541b\u8c6a", "avatar": "https://i.linkeddb.com/person2/ef23/5499/0faf910b5f1c74a5e5beedbf.jpg"}}, {"id": 47, "oid": "5a56e22893cc2b42a6bf0025", "type": "role", "name": "\u5434\u851a\u6587", "avatar": "https://i.linkeddb.com/upload/image/2017920/143212760960.jpg", "url": "/movie/role/5a56e22893cc2b42a6bf0025/", "exdata": {"id": 80081, "oid": "59fa72dd18521569b663b17a", "name": "\u5f20\u6668\u5149", "avatar": "https://i.linkeddb.com/person2/d951/b1d3/01f0a1ccfa0c8d2aef80f543.jpg"}}, {"id": 51, "oid": "5a56e22893cc2b42a6bf0029", "type": "role", "name": "\u5343\u7ea2", "avatar": "https://i.linkeddb.com/upload/image/2017920/144328263786.jpg", "url": "/movie/role/5a56e22893cc2b42a6bf0029/", "exdata": {"id": 15313, "oid": "59fa6ec018521569b65f9fd4", "name": "\u5468\u4e3d\u6dc7", "avatar": "https://i.linkeddb.com/person2/aa47/0381/6cd27a6f890af3440fc813cd.jpg"}}, {"id": 48, "oid": "5a56e22893cc2b42a6bf0026", "type": "role", "name": "\u5434\u6f2a", "avatar": "https://i.linkeddb.com/upload/image/2017920/142808729350.jpg", "url": "/movie/role/5a56e22893cc2b42a6bf0026/", "exdata": {"id": 171638, "oid": "59fa7e4f18521569b66dbcd9", "name": "\u66fe\u6dc7", "avatar": "https://i.linkeddb.com/upload/image/2017920/142410770419.jpg"}}, {"id": 833, "oid": "5a56e24b93cc2b42a6bf0337", "type": "role", "name": "\u6c88\u8001\u592b\u4eba", "avatar": "https://i.linkeddb.com/upload/742a/72f2/f933743cc2563895cb28a4ba.png", "url": "/movie/role/5a56e24b93cc2b42a6bf0337/", "exdata": {"id": 74843, "oid": "59fa729018521569b66364a3", "name": "\u5510\u7fa4", "avatar": "https://i.linkeddb.com/person2/97c3/9c83/fee3e8039a4c976f192447bf.jpg"}}, {"id": 832, "oid": "5a56e24b93cc2b42a6bf0336", "type": "role", "name": "\u6c88\u592b\u4eba", "avatar": "https://i.linkeddb.com/upload/05db/a955/dda11486be4f1b94717ea8bb.png", "url": "/movie/role/5a56e24b93cc2b42a6bf0336/", "exdata": {"id": 76304, "oid": "59fa729b18521569b6636eb9", "name": "\u5218\u6d01", "avatar": "https://i.linkeddb.com/person2/e8ac/f7a2/cf19348ed16ab3700bee93b9.jpg"}}, {"id": 55, "oid": "5a56e22893cc2b42a6bf002d", "type": "role", "name": "\u6c88\u6708\u751f", "avatar": "https://i.linkeddb.com/upload/image/2017920/165524797798.jpg", "url": "/movie/role/5a56e22893cc2b42a6bf002d/", "exdata": {"id": 81417, "oid": "59fa72f718521569b663cccc", "name": "\u674e\u89e3", "avatar": "https://i.linkeddb.com/person2/8edb/31e8/c09ff047f76721dc268011fb.jpg"}}, {"id": 59, "oid": "5a56e22993cc2b42a6bf0031", "type": "role", "name": "\u5434\u9047", "avatar": "https://i.linkeddb.com/upload/image/2017920/171456600879.jpg", "url": "/movie/role/5a56e22993cc2b42a6bf0031/", "exdata": {"id": 155065, "oid": "59fa7ab118521569b66b0ae1", "name": "\u8bb8\u8bfa", "avatar": "https://i.linkeddb.com/person2/1cd8/47b9/04dc74ae4dbdc51614dd5e0d.jpg"}}, {"id": 45, "oid": "5a56e22893cc2b42a6bf0023", "type": "role", "name": "\u80e1\u548f\u6885", "avatar": "https://i.linkeddb.com/upload/image/2017920/141341851866.jpg", "url": "/movie/role/5a56e22893cc2b42a6bf0023/", "exdata": {"id": 25465, "oid": "59fa6f4c18521569b6602932", "name": "\u80e1\u674f\u513f", "avatar": "https://i.linkeddb.com/person2/dd65/89e5/6c3163f8fb99fbac5a2fde3a.jpg"}}, {"id": 46, "oid": "5a56e22893cc2b42a6bf0024", "type": "role", "name": "\u675c\u660e\u793c", "avatar": "https://i.linkeddb.com/upload/image/2017920/141611471224.jpg", "url": "/movie/role/5a56e22893cc2b42a6bf0024/", "exdata": {"id": 105183, "oid": "59fa751618521569b665e8a2", "name": "\u4fde\u704f\u660e", "avatar": "https://i.linkeddb.com/upload/2c33/76de/923efcfad5f70b2755601530.png"}}, {"id": 56, "oid": "5a56e22893cc2b42a6bf002e", "type": "role", "name": "\u5434\u6cfd", "avatar": "https://i.linkeddb.com/upload/image/2017920/165926893902.jpg", "url": "/movie/role/5a56e22893cc2b42a6bf002e/", "exdata": {"id": 156844, "oid": "59fa7af418521569b66b44e1", "name": "\u5f20\u5929\u9633", "avatar": "https://i.linkeddb.com/person2/75f6/f682/2dbdb23443d37793c5b44250.jpg"}}, {"id": 831, "oid": "5a56e24b93cc2b42a6bf0335", "type": "role", "name": "\u67e5\u5764", "avatar": "https://i.linkeddb.com/upload/48e2/810a/3675c7b3b157438ce5a42dbf.png", "url": "/movie/role/5a56e24b93cc2b42a6bf0335/", "exdata": {"id": 182836, "oid": "5a150881b0f85226cf8678e4", "name": "\u674e\u827a\u79d1", "avatar": "https://i.linkeddb.com/upload/4a66/509d/39a25ad4a12d1b6486a0ff91.jpg"}}, {"id": 54, "oid": "5a56e22893cc2b42a6bf002c", "type": "role", "name": "\u56fe\u5c14\u4e39", "avatar": "https://i.linkeddb.com/upload/image/2017920/165123228566.jpg", "url": "/movie/role/5a56e22893cc2b42a6bf002c/", "exdata": {"id": 171640, "oid": "59fa7e4f18521569b66dbc29", "name": "\u9ad8\u5723\u8fdc", "avatar": "https://i.linkeddb.com/upload/image/2017920/164932735319.jpg"}}, {"id": 57, "oid": "5a56e22893cc2b42a6bf002f", "type": "role", "name": "\u738b\u4e16\u5747", "avatar": "https://i.linkeddb.com/upload/image/2017920/170109871241.jpg", "url": "/movie/role/5a56e22893cc2b42a6bf002f/", "exdata": {"id": 139118, "oid": "59fa789918521569b6692e47", "name": "\u674e\u6cfd\u950b", "avatar": "https://i.linkeddb.com/person2/2fc9/f05b/2cdce261a4389909b416e501.jpg"}}], "links": [{"source": 0, "target": 1, "type": "\u670b\u53cb", "color": "734646"}, {"source": 0, "target": 2, "type": "\u670b\u53cb", "color": "734646"}, {"source": 0, "target": 3, "type": "\u517b\u5973", "color": "f2826a"}, {"source": 0, "target": 4, "type": "\u4e3b\u4ec6", "color": ""}, {"source": 0, "target": 5, "type": "\u4ec7\u4eba", "color": ""}, {"source": 0, "target": 6, "type": "\u513f\u5ab3", "color": "e3dce3"}, {"source": 0, "target": 7, "type": "\u59d0\u59b9", "color": ""}, {"source": 0, "target": 8, "type": "\u5ac2\u5b50", "color": ""}, {"source": 1, "target": 9, "type": "\u5b59\u5b50", "color": "6681ae"}, {"source": 1, "target": 10, "type": "\u6bcd\u5b50", "color": ""}, {"source": 1, "target": 5, "type": "\u513f\u5b50", "color": "f2826a"}, {"source": 1, "target": 11, "type": "\u5144\u5f1f", "color": ""}, {"source": 1, "target": 2, "type": "\u5408\u4f5c", "color": ""}, {"source": 1, "target": 3, "type": "\u5e2e\u52a9", "color": ""}, {"source": 1, "target": 4, "type": "\u4ec7\u4eba", "color": ""}, {"source": 1, "target": 7, "type": "\u60c5\u4eba", "color": "fcb7e9"}, {"source": 4, "target": 11, "type": "\u7ade\u4e89", "color": ""}, {"source": 4, "target": 5, "type": "\u7ade\u4e89", "color": ""}, {"source": 4, "target": 6, "type": "\u513f\u5b50", "color": "f2826a"}, {"source": 4, "target": 8, "type": "\u5144\u59b9", "color": ""}, {"source": 4, "target": 12, "type": "\u5802\u5144\u5f1f", "color": ""}, {"source": 4, "target": 2, "type": "\u6c11\u4e0e\u5b98", "color": ""}, {"source": 4, "target": 13, "type": "\u9752\u6885\u7af9\u9a6c", "color": ""}, {"source": 4, "target": 14, "type": "\u88ab\u5e2e\u52a9", "color": ""}, {"source": 2, "target": 15, "type": "\u540c\u7a97", "color": ""}, {"source": 2, "target": 14, "type": "\u5408\u4f5c", "color": ""}, {"source": 13, "target": 0, "type": "\u4ec7\u4eba\u80e1\u548f\u6885\u6b7b\u4ea1", "color": ""}, {"source": 13, "target": 14, "type": "\u6c42\u52a9", "color": ""}, {"source": 13, "target": 6, "type": "\u6c42\u52a9", "color": ""}, {"source": 14, "target": 5, "type": "\u5229\u7528", "color": ""}, {"source": 14, "target": 16, "type": "\u4e3b\u4ec6", "color": ""}, {"source": 14, "target": 0, "type": "\u4ec7\u4eba", "color": ""}, {"source": 14, "target": 1, "type": "\u4ec7\u4eba", "color": ""}, {"source": 14, "target": 6, "type": "\u8ba4\u8bc6", "color": ""}, {"source": 14, "target": 11, "type": "\u5229\u7528", "color": ""}, {"source": 8, "target": 2, "type": "\u592b\u59bb", "color": "fcb7e9"}, {"source": 8, "target": 7, "type": "\u4e92\u770b\u4e0d\u60ef", "color": ""}, {"source": 8, "target": 15, "type": "\u5144\u59b9", "color": ""}, {"source": 17, "target": 0, "type": "\u5408\u4f5c", "color": ""}, {"source": 17, "target": 1, "type": "\u7edd\u4ea4", "color": ""}, {"source": 15, "target": 0, "type": "\u5ac2\u5b50", "color": ""}, {"source": 18, "target": 0, "type": "\u4ec6\u4eba", "color": ""}, {"source": 12, "target": 14, "type": "\u88ab\u5e2e\u52a9", "color": ""}, {"source": 10, "target": 9, "type": "\u5a46\u5ab3", "color": ""}]}';

实例化案例核心代码

      let configs = {
        nodes: rolesData.nodes,
        links: rolesData.links,
        width: 375,
        height: 600,
        // isHighLight:false,
        // isScale:false
      };
      new D3MakeSvgPicClass('roleMapId',configs);

你可能感兴趣的:(d3.js - force力学图)