d3.js 畫力導向圖 帶箭頭 文字 方形節點 動態添加節點

svg_coordinate.js  用於計算矩形與連線的交點位置javascript

var coordinate = {};
/**
 * 判斷直線與水平線夾角
 * @param x1 點1的x座標
 * @param y1 點1的y座標
 * @param x2 點2的x座標
 * @param y2 點2的y座標
 * @returns {number}角度
 */
coordinate.findAngle= function(x1,y1,x2,y2){
    var k = (y1-y2)/(x1-x2);
    var result = Math.atan(k)*180/Math.PI;
    return result;
};
//計算箭頭座標
coordinate.findArrowPoint = function(x1,y1,x2,y2,rectWidth,rectHeight){
    var rect_x = rectWidth/2;
    var rect_y = rectHeight/2;
    var x;
    var y;
    var selection = coordinate.findSelection(x1,y1,x2,y2);
    //若是與x軸水平,並在點2的左側
    if(11 == selection){
        x = x2 - rect_x;
        y = y2;
        return [x,y]
    }
    //若是與y軸水平,並在點2的上面
    if(22 == selection){
        x = x2;
        y = y2 - rect_y;
        return [x,y]
    }
    //若是與x軸水平,並在點2的右側
    if(33 == selection){
        y = y2;
        x = x2 + rect_x;
        return [x,y]
    }
    //若是與y軸水平,並在點2的下面
    if(44 == selection){
        x = x2;
        y = y2 + rect_y;
        return [x,y]
    }
    var angle = Math.abs(coordinate.findAngle(x1,y1,x2,y2));
    var x_offset = 0;
    var y_offset = 0;
    if(1 == selection){
        if(angle == 45){
            x = x2 - rect_x;
            y = y2 - rect_y;
            return [x,y];
        }
        if(angle < 45){
            y_offset = rect_x * coordinate.tan(angle);
            y = y2 - y_offset;
            x = x2 - rect_x;
            return [x,y]
        }
        x_offset = rect_y / coordinate.tan(angle);
        y = y2 - rect_y;
        x = x2 - x_offset;
        return [x,y];
    }
    if(2 == selection){
        if(angle == 45){
            x = x2 + rect_x;
            y = y2 - rect_y;
            return [x,y];
        }
        if(angle < 45){
            y_offset = rect_x * coordinate.tan(angle);
            y = y2 - y_offset;
            x = x2 + rect_x;
            return [x,y]
        }
        x_offset = rect_y / coordinate.tan(angle);
        y = y2 - rect_y;
        x = x2 + x_offset;
        return [x,y];
    }
    if(3 == selection){
        if(angle == 45){
            x = x2 + rect_x;
            y = y2 + rect_y;
            return [x,y];
        }
        if(angle < 45){
            y_offset = rect_x * coordinate.tan(angle);
            y = y2 + y_offset;
            x = x2 + rect_x;
            return [x,y]
        }
        x_offset = rect_y / coordinate.tan(angle);
        y = y2 + rect_y;
        x = x2 + x_offset;
        return [x,y];
    }
    if(4 == selection){
        if(angle == 45){
            x = x2 - rect_x;
            y = y2 + rect_y;
            return [x,y];
        }
        if(angle < 45){
            y_offset = rect_x * coordinate.tan(angle);
            y = y2 + y_offset;
            x = x2 - rect_x;
            return [x,y]
        }
        x_offset = rect_y / coordinate.tan(angle);
        y = y2 + rect_y;
        x = x2 - x_offset;
        return [x,y];
    }
};
coordinate.tan = function (angle){
    return Math.tan(angle*Math.PI/180)
};
coordinate.findSelection = function(x1,y1,x2,y2){
    var up = 0;
    if(y1-y2 < 0 ){
        up = 1
    }else if(y1-y2 > 0){
        up = -1
    }
    var left = 0;
    if(x1 - x2 < 0){
        left = 1;
    }else if(x1 - x2 > 0){
        left = -1;
    }
    if(up > 0 && left > 0){
        return 1;
    }
    if(up > 0 && left <0){
        return 2;
    }
    if(up < 0 && left < 0){
        return 3;
    }
    if(up < 0 && left > 0){
        return 4;
    }
    if(up == 0 && left > 0){
        return 11
    }
    if(left == 0 && up > 0){
        return 22
    }
    if(up == 0 && left < 0){
        return 33
    }
    if(left == 0 && up <0){
        return 44
    }
}; 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        [role=entity]{
            fill:rgb(44, 160, 44)
        }
        [role=concept]{
            fill:rgb(0, 112, 192)
        }
    </style>
    <script src="./js/d3_v4.js"></script>
    <script type="text/javascript" src="./js/svg_coordinate.js"></script>
</head>
<body>
<svg width="960" height="500"></svg>
</body>
<script type="text/javascript">
    var nodes = [
        { name: "北京",role:"entity"},
        { name: "天津",role:"entity"},
        { name: "地區",role:"concept"},
        { name: "單位",role:"concept"},
        { name: "單位住址",role:"concept"},
        { name: "單位類型",role:"concept"},
        { name: "民營",role:"entity"}
    ];

    var links = [  { source : 0  , target: 2 } , { source : 1  , target: 2 } ,
        { source : 3  , target: 2 } , { source : 3  , target: 4 } ,
        { source : 3  , target: 5 } , { source : 6  , target: 5 }
    ];
    //畫布寬度
    var width = 1024;
    //畫布高度
    var height = 738;
    //矩形寬度
    var rect_width = 100;
    //矩形高度
    var rect_height = 50;
    //畫布對象
    var svg = d3.select("svg")
        .attr("width",width)
        .attr("height",height);

    // 經過佈局來轉換數據,而後進行繪製
    var simulation = d3.forceSimulation(nodes)
        .force("link", d3.forceLink(links).distance(200))
        .force("charge",d3.forceManyBody().strength(-100))
        .force("center",d3.forceCenter(width/2, height/2));
    //顏色對象
    var color = d3.scaleOrdinal(d3.schemeCategory20);
    // 繪製線
    var svg_links = svg.selectAll("path")
        .data(links)
        .enter()
        .append("path")
        .style("stroke","#ccc")
        .style("stroke-width",3);

    //節點對象
    var svg_nodes = svg.selectAll("rect")
        .data(nodes)
        .enter()
        .append("rect")
        .attr("width",rect_width)
        .attr("height",rect_height)
        .call(d3.drag()
            .on("start", dragstarted)
            .on("drag", dragged)
            .on("end", dragended));
    /**
     * 節點(開始)拖拽事件
     */
    function dragstarted(d) {
        if (!d3.event.active)
            simulation.alphaTarget(0.002).restart();
        d.fx = d.x;
        d.fy = d.y;
    }

    /**
     * 節點拖拽事件
     */
    function dragged(d) {
        d.fx = d3.event.x;
        d.fy = d3.event.y;
    }

    /**
     * 節點(結束)拖拽事件
     */
    function dragended(d) {
        if (!d3.event.active)
            simulation.alphaTarget(0);
    }
    //節點描述
    var svg_text = svg.selectAll("text")
        .data(nodes)
        .enter()
        .append("text")
        .style("fill","#ffffff")
        .attr("dominant-baseline","middle")
        .attr("text-anchor", "middle")//在圓圈中加上數據
        .text(function(d){return d.name;});

    //箭頭
    var marker=
        svg.append("marker")
            .attr("id", "resolved")
            .attr("markerUnits","userSpaceOnUse")
            .attr("viewBox", "0 -5 10 10")//座標系的區域
            .attr("refX",10)//箭頭座標
            .attr("refY", -1)
            .attr("markerWidth", 12)//標識的大小
            .attr("markerHeight", 12)
            .attr("orient", "auto")//繪製方向,可設定爲:auto(自動確認方向)和 角度值
            .attr("stroke-width",2)//箭頭寬度
            .append("path")
            .attr("d", "M0,-5L10,0L0,5")//箭頭的路徑
            .attr('fill','#000000');//箭頭顏色

    //繪製節點 文本 直線
    function draw(){
        svg_nodes
            .attr("x",function(d){
                return d.x - (rect_width/2);
            })
            .attr("y",function(d){
                return d.y - (rect_height/2);
            })
            .attr("role",function (d) {
                return d.role;
            });

        svg_text
            .attr("x", function(d){ return d.x; })
            .attr("y", function(d){ return d.y; });

        svg_links
            .attr("d",function(d){
                var xAndy = coordinate.findArrowPoint(d.source.x,d.source.y,d.target.x,d.target.y,rect_width,rect_height);
                return 'M '+ d.source.x + ' '+d.source.y+' L '+
                    xAndy[0]
                    + ' '
                    + xAndy[1]
            })
            .attr("marker-end", "url(#resolved)");
    }
    simulation.on("tick",draw);
    svg.call(d3.zoom().scaleExtent([0.05, 8]).on('zoom', () => {
        // 保存當前縮放的屬性值
        var transform = d3.event.transform;
        svg_nodes.attr('transform', transform);
        svg_links.attr("transform",transform);
        svg_text.attr("transform",transform);
    })).on('dblclick.zoom', null);

    var e = { name: "河北",role:"entity"};
    var f =  {source : 7  , target: 2};
    d3.timeout(function(){
        nodes.push(e);
        links.push(f);
        restart()
    }, 4000);

    //動態添加節點
    function restart() {

        svg_links = svg_links
            .data(links, (d) => { return d.source.name + "-" + d.target.name; });

        svg_links = svg_links.enter()
            .append("path")
            .style("stroke","#ccc")
            .style("stroke-width",3)
            .merge(svg_links);

        svg_nodes = svg_nodes
            .data(nodes, (d) => d.name)
            .enter()
            .append("rect")
            .attr("width",rect_width)
            .attr("height",rect_height)
            .merge(svg_nodes).call(d3.drag()
                .on("start", dragstarted)
                .on("drag", dragged)
                .on("end", dragended));
        var dom = document.querySelectorAll("rect");
        for (let len = dom.length, i = 0; i < len; i++) {
            if (d3.select(dom[i]). select('rect').empty()) {
                svg_nodes.append("rect")
                    .attr("fill", (d) => color(d.name))
                    .attr("width",rect_width)
                    .attr("height",rect_height)
            }
        }
        //節點描述
        svg_text = svg_text
            .data(nodes,(d) => d.name)
            .enter()
            .append("text")
            .style("fill","#ffffff")
            .attr("dominant-baseline","middle")
            .attr("text-anchor", "middle")//在圓圈中加上數據
            .text(function(d){return d.name;}).merge(svg_text);
        //加載節點
        simulation.nodes(nodes);
        simulation.force("link").links(links);
        simulation.alpha(1).restart();
    }

</script>
</html>