ICoded
ICoded

Reputation: 341

D3v6 convex hull examples

Recently I noticed the convex hull possibilities in D3. For example this one:

var w = 960,
    h = 500,
    fill = d3.scale.category10(),
    nodes = d3.range(9).map(Object);

var groups = d3.nest().key(function(d) { return d & 3; }).entries(nodes);

var groupPath = function(d) {
    var fakePoints = [];
    if (d.values.length == 2)
    {
        //[dx, dy] is the direction vector of the line
        var dx = d.values[1].x - d.values[0].x;
        var dy = d.values[1].y - d.values[0].y;

        //scale it to something very small
        dx *= 0.00001; dy *= 0.00001;

        //orthogonal directions to a 2D vector [dx, dy] are [dy, -dx] and [-dy, dx]
        //take the midpoint [mx, my] of the line and translate it in both directions
        var mx = (d.values[0].x + d.values[1].x) * 0.5;
        var my = (d.values[0].y + d.values[1].y) * 0.5;
        fakePoints = [ [mx + dy, my - dx],
                      [mx - dy, my + dx]];
        //the two additional points will be sufficient for the convex hull algorithm
    }

    //do not forget to append the fakePoints to the input data
    return "M" + 
        d3.geom.hull(d.values.map(function(i) { return [i.x, i.y]; })
                     .concat(fakePoints))
        .join("L") 
        + "Z";
}

var groupFill = function(d, i) { return fill(i & 3); };

var vis = d3.select("#chart").append("svg")
    .attr("width", w)
    .attr("height", h);

var force = d3.layout.force()
    .nodes(nodes)
    .links([])
    .size([w, h])
    .start();

var node = vis.selectAll("circle.node")
    .data(nodes)
  .enter().append("circle")
    .attr("class", "node")
    .attr("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.y; })
    .attr("r", 8)
    .style("fill", function(d, i) { return fill(i & 3); })
    .style("stroke", function(d, i) { return d3.rgb(fill(i & 3)).darker(2); })
    .style("stroke-width", 1.5)
    .call(force.drag);

vis.style("opacity", 1e-6)
  .transition()
    .duration(1000)
    .style("opacity", 1);

force.on("tick", function(e) {

  // Push different nodes in different directions for clustering.
  var k = 6 * e.alpha;
  nodes.forEach(function(o, i) {
    o.x += i & 2 ? k : -k;
    o.y += i & 1 ? k : -k;
  });

  node.attr("cx", function(d) { return d.x; })
      .attr("cy", function(d) { return d.y; });

  vis.selectAll("path")
    .data(groups)
      .attr("d", groupPath)
    .enter().insert("path", "circle")
      .style("fill", groupFill)
      .style("stroke", groupFill)
      .style("stroke-width", 40)
      .style("stroke-linejoin", "round")
      .style("opacity", .2)
      .attr("d", groupPath);
});
   <script src="https://d3js.org/d3.v3.min.js"></script>

<div id="chart"></div>

Unfortunately its version 3 and I am searching for similar layouts made with version 6. I tried to adapt the code but most of functions changed completely, which makes it hard to adapt snippets.

In case somebody stumpled over similar D3v6 snippets, I would be glad.

UPDATE:

In addition I found this convex hull example, which use version 4. Still, both version require the d3.nest() functions. I was building a version 6 prototyp for d3.group() / d3.groups() testing, to receive the same output as d3.nest(). Unfortunately without success. Below the version 4 hull example.

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>D3v4 hull</title>
    <!-- d3.js framework -->
    <script src="https://d3js.org/d3.v4.js"></script>
    <!-- d3.js Scale Chromatic plugin -->
    <script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>

    <style>
        html,
        body,
        #canvas {
            width: 100%;
            height: 100%;
        }

        path.link {
            fill: none;
            stroke: #666;
            stroke-width: 1.5px;
            opacity: 0.8;
        }

        marker#resource {
            fill: red;
        }

        path.link.resource {
            /* stroke: green; */
        }

        path.link.property {
            stroke-dasharray: 0, 2 1;
        }

        circle {
            opacity: 1;
            stroke: #333;
            stroke-width: 1.5px;
        }

        text {
            font: 10px sans-serif;
            pointer-events: none;
        }

        text.shadow {
            stroke: #fff;
            stroke-width: 3px;
            stroke-opacity: .8;
        }

        .nodes {
            opacity: 1;
        }

    </style>

<body>
    <div id="canvas"></div>

    <script type="text/javascript">
        var data = {
            'nodes': [{
                'id': 'A',
                'label': 'A',
                'type': 'resource',
                'size': 40,
                'group': 'a',
            }, {
                'id': 'B',
                'label': 'B',
                'type': 'resource',
                'size': 40,
                'group': 'a'
            }, {
                'id': 'C',
                'label': 'C',
                'type': 'resource',
                'size': 40,
                'group': 'x',
            }, {
                'id': 'D',
                'label': 'D',
                'type': 'resource',
                'size': 40,
                'group': 'x'
            }, {
                'id': 'E',
                'label': 'E',
                'type': 'resource',
                'size': 40,
                'group': 'x'
            }, {
                'id': 'F',
                'label': 'F',
                'type': 'resource',
                'size': 40,
                'group': 'a'
            }, {
                'id': 'Q',
                'label': 'Q',
                'type': 'resource',
                'size': 40,
                'group': 'q'
            }, {
                'id': 'W',
                'label': 'Wjk',
                'type': 'resource',
                'size': 40,
                'group': 'q'
            }, {
                'id': 'Z',
                'label': 'Z',
                'type': 'resource',
                'size': 40,
                'group': 'q'
            }],
            'links': [{
                'source': 'E',
                'target': 'F',
                'type': 'resource',
                'distance': 100,
                'strength': 1
            }, {
                'source': 'A',
                'target': 'F',
                'type': 'resource',
                'distance': 100,
                'strength': 1
            }, {
                'source': 'A',
                'target': 'C',
                'type': 'resource',
                'distance': 100,
                'strength': 1
            }, {
                'source': 'Q',
                'target': 'F',
                'type': 'resource',
                'distance': 100,
                'strength': 1
            }, {
                'source': 'Q',
                'target': 'E',
                'type': 'resource',
                'distance': 100,
                'strength': 1
            }, {
                'source': 'W',
                'target': 'Q',
                'type': 'resource',
                'distance': 100,
                'strength': 1
            }]
        };

        var canvas = document.getElementById('canvas');
        var w = canvas.clientWidth, h = canvas.clientHeight;
        var color = d3.scaleOrdinal(d3.schemeSet3);
        var svg = d3.select(canvas).append('svg')
            .attr('width', w)
            .attr('height', h);
        var rectWidth = 80,
            rectHeight = 30;
        var markerWidth = 10,
            markerHeight = 6,
            cRadius = 180, // play with the cRadius value
            refX = 70, //refX = cRadius + markerWidth,
            refY = 0, //refY = -Math.sqrt(cRadius),
            drSub = cRadius + refY;

        var tocolor = "fill";
        var towhite = "stroke";
        if (outline) {
            tocolor = "stroke"
            towhite = "fill"
        }

        var focus_node = null, highlight_node = null;

        var highlight_color = "blue";
        var highlight_trans = 0.1;
        var outline = false;
        var default_node_color = "#ccc";
        //var default_node_color = "rgb(3,190,100)";
        var default_link_color = "#888";

        var g = svg.append("g")
            .attr("class", "viz");
        var net, convexHull, genCH, linkElements, nodeElements, textElements, circle, simulation, linkForce;

        var expand = {};

        var linkedByIndex = {};
        data.links.forEach(function (d) {
            linkedByIndex[d.source + "," + d.target] = true;
        });
        function isConnected(a, b) {
            return linkedByIndex[a.index + "," + b.index] || linkedByIndex[b.index + "," + a.index] || a.index == b.index;
        }
        

        var groupFill = function (d, i) { return color(d.key); };

        function getGroup(n) { return n.group; }

        function network(data, prev, cekGroup, expand) {
            var cnode, groupIndex, mappedNodes = [], mappedLinks = [], clink, tempN, tempL = [], newNodes = [], soIn, taIn, lw = 0, newLinks = [];
            if (Object.getOwnPropertyNames(expand).length == 0) {
                for (var j = 0; j < data.nodes.length; j++) {
                    groupIndex = cekGroup(data.nodes[j]);
                    expand[groupIndex] = true;
                }
                nodes = data.nodes;
                links = data.links;
            } else {
                for (var k = 0; k < data.nodes.length; k++) {
                    cnode = data.nodes[k];
                    groupIndex = cekGroup(cnode);
                    if (expand[groupIndex]) {
                        mappedNodes.push(cnode);
                        //if expand true, nodes condition expand
                    } else {
                        if (!newNodes[groupIndex]) {
                            tempN = {
                                'id': groupIndex,
                                'label': 'domain ' + groupIndex,
                                'type': 'resource',
                                'size': 30,
                                'group': groupIndex
                            };
                            newNodes[groupIndex] = tempN;
                            mappedNodes.push(tempN);
                        }
                        // if expand false, nodes condition collapse
                    }
                    //iterate through all data.nodes
                }


                for (var x = 0; x < data.links.length; x++) {
                    clink = data.links[x];
                    soIn = cekGroup(clink.source);
                    taIn = cekGroup(clink.target);
                    tempL = {};
                    // if(!expand[soIn] && expand[taIn]) {
                    //   tempL.source = newNodes[soIn];
                    // }

                    if (expand[soIn] && expand[taIn]) {
                        //console.log('if1');
                        //tempL=clink;
                        soIn = clink.source.id;
                        taIn = clink.target.id;
                    } else if (!expand[soIn] && expand[taIn]) {
                        //console.log('if2');
                        //tempL.source = newNodes[soIn];
                        soIn = soIn;
                        taIn = clink.target.id;
                    } else if (expand[soIn] && !expand[taIn]) {
                        //console.log('if3');
                        //tempL.target = newNodes[taIn];
                        taIn = taIn;
                        soIn = clink.source.id;
                    } else if (!expand[soIn] && !expand[taIn]) {
                        //console.log('if4');
                        //tempL=null;
                        if (soIn == taIn) { soIn = ''; taIn = ''; }
                    }
                    if (soIn != '' && taIn != '') {
                        tempL = {
                            'source': soIn,
                            'target': taIn,
                            'type': clink.type,
                            'distance': 50,
                            'strength': 1
                        }
                        mappedLinks.push(tempL);
                    }
                }
                nodes = mappedNodes;
                links = mappedLinks;
                // endof if expand not empty
            }

            return { nodes: nodes, links: links };
        }
    
        var offset = 0, groups, groupPath;
        function init() {
            if (simulation) {
                linkElements.remove();
                nodeElements.remove();
                genCH.remove();
                convexHull.remove();
                textElements.remove();
            }
            net = network(data, net, getGroup, expand);
            groups = d3.nest().key(function (d) { return d.group; }).entries(net.nodes);
            console.log(groups)
            
            groupPath = function (d) {
                var txt;
                if (d.values.length == 1) {
                    return "M0,0L0,0L0,0Z";
                } else {
                    return "M" +
                        d3.polygonHull(d.values.map(function (i) { return [i.x + offset, i.y + offset]; }))
                            .join("L")
                        + "Z";
                }

            };
            convexHull = g.append('g').attr('class', 'hull');
            // simulation setup with all forces
            linkForce = d3
                .forceLink()
                .id(function (link) { return link.id })
                .strength(function (link) { return 0.2 })

            var inpos = [], counterX = 1, inposY = [], counterY = 1;
            simulation = d3
                .forceSimulation()
                .force('link', d3.forceLink().id(function (d) {
        return d.id;
    }).distance(150))
                .force('forceX', d3.forceX(function (d) {
                    if (inpos[d.group]) {
                        
                        return inpos[d.group];
                    } else {
                        inpos[d.group] = w / counterX;
                        
                        counterX++;
                        return inpos[d.group];
                    }
                }))
                .force('forceY', d3.forceY(function (d) {
                    if (inposY[d.group]) {
                        
                        return inposY[d.group];
                    } else {
                        inposY[d.group] = h / (Math.random() * (d.group.length - 0 + 1) + 1);
                        
                        return inposY[d.group];
                    }
                }))
                .force('charge', d3.forceManyBody().strength(-500))
                .force('center', d3.forceCenter(w / 2, h / 2))
                .force("gravity", d3.forceManyBody(50));

            linkElements = g.append('g').attr('class', 'links').selectAll('path').data(net.links).enter().append('path')
                .attr('class', function (d) { return 'link ' + d.type; })
                

            nodeElements = g.append('g').attr('class', 'nodes').selectAll('.node')
                .data(net.nodes)
                .enter().append('g')
                .attr('class', 'node');
            // .append('circle')
            // .attr("r", cRadius)
            // .attr("fill", function(d){ return color(d.group);});
            circle = nodeElements.filter(function (d) { return d.type == 'resource'; }).append('circle')
                .attr('class', 'circle')
                .attr("r", function (d) { return d.size; })
                .attr("fill", function (d) { return color(d.group); });

            nodeElements.call(d3.drag()
                .on("start", dragstarted)
                .on("drag", dragged)
                .on("end", dragended));

            textElements = g.append("g")
                .attr("class", "texts")
                .selectAll("text")
                .data(net.nodes)
                .enter().append('text')
                .attr('text-anchor', 'middle')
                .attr('alignment-baseline', 'middle')
                .append('tspan')
                .text(function (node) { return node.label });

            simulation.nodes(net.nodes).on('tick', () => {
                genCH = convexHull.selectAll("path")
                    .data(groups)
                    .attr("d", groupPath)
                    .enter().insert("path", "circle")
                    .style("fill", groupFill)
                    .style("stroke", groupFill)
                    .style("stroke-width", 140)
                    .style("stroke-linejoin", "round")
                    .style("opacity", .5)
                    .on('click', function (d) {
                        expand[d.key] = false;
                        init();
                    })
                    .attr("d", groupPath);

                nodeElements
                    .attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; })
                // .attr('x', function (node) { console.log(node); return node.x })
                // .attr('y', function (node) { return node.y })
                textElements
                    .attr('x', function (node) { return node.x })
                    .attr('y', function (node) { return node.y })
                linkElements
                    .attr('d', function (d) {
                        var dx = d.target.x - d.source.x,
                            dy = d.target.y - d.source.y,
                            dr = Math.sqrt(dx * dx + dy * dy);
                    

                        var val = 'M' + d.source.x + ',' + d.source.y + 'A' + (dr - drSub) + ',' + (dr - drSub) + ' 0 0,1 ' + d.target.x + ',' + d.target.y;

                        var val2 = 'M' + d.source.x + ',' + d.source.y + 'L' + (d.target.x) + ',' + (d.target.y);
                        if (d.type == 'resource') return val2;
                        else return val1;
                    });
            })

            nodeElements.on("mouseover", function (d) {
                set_highlight(d);
            })
                .on("mousedown", function (d) {
                    d3.event.stopPropagation();
                    focus_node = d;
                
                    set_focus(d)
                    if (highlight_node === null) set_highlight(d)

                }).on("mouseout", function (d) {
                    exit_highlight();

                }).on("click", function (d) {
                    d3.event.stopPropagation();
                
                    setExpand(d);
                });

            simulation.force("link").links(net.links).distance(function (d) {
                if (d.source.group == d.target.group) return 85;
                else return 180;
                // if(d.type=='resource') return 300;
                // else return 150;
                // return d.distance;
            });

            function setExpand(d) {
                expand[d.id] = !expand[d.id];
                init();
            }

            function exit_highlight() {
                highlight_node = null;
                if (focus_node === null) {
                    svg.style("cursor", "move");
                    if (highlight_color != "white") {
                        circle.style(towhite, "white");
                        linkElements.style("stroke", function (o) { return (isNumber(o.score) && o.score >= 0) ? color(o.score) : default_link_color });
                    }

                }
            }

            function set_focus(d) {
                if (highlight_trans < 1) {
                    circle.style("opacity", function (o) {
                        return isConnected(d, o) ? 1 : highlight_trans;
                    });

                    linkElements.style("opacity", function (o) {
                        return o.source.index == d.index || o.target.index == d.index ? 1 : highlight_trans;
                    });
                }
            }

            function set_highlight(d) {

                svg.style("cursor", "pointer");
                // circle.style('opacity',0.7);
                if (focus_node !== null) d = focus_node;
                highlight_node = d;

                if (highlight_color != "white") {
                    circle.style(towhite, function (o) {
                        return isConnected(d, o) ? highlight_color : "white";
                    });
                    //             linkElements.style("stroke", function(o) {
                    //            return o.source.index == d.index || o.target.index == d.index ? highlight_color : ((isNumber(o.score) && o.score>=0)?color(o.score):default_link_color);
                    // });
                }
            }

            function dragstarted(d) {
                if (!d3.event.active) simulation.alphaTarget(0.3).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);
                d.fx = null;
                d.fy = null;
            }
            // endof init()
        }

        init();
        //add zoom capabilities
        var zoom_handler = d3.zoom()
            .on("zoom", zoom_actions);

        zoom_handler(svg);
        function zoom_actions() {
            g.attr("transform", d3.event.transform);
        }

        function isNumber(n) {
            return !isNaN(parseFloat(n)) && isFinite(n);
        }
    </script>

</body>

</html>

Upvotes: 1

Views: 613

Answers (1)

Robin Mackenzie
Robin Mackenzie

Reputation: 19289

Good research finding a v4 version of that force directed graph with convex hull groupings.

To convert to v6 is just a couple more steps:

As you know, replace d3.nest with d3.groups - but the missing step is to convert the output of d3.groups from an array of arrays to an array of objects. So this:

groups = d3.groups(
  net.nodes, 
  d => d.group
).map(x => ({key: x[0], values: x[1]}));

Is equivalent to this:

groups = d3.nest()
  .key(function (d) { return d.group; })
  .entries(net.nodes);

Then go through all the event handlers and update them so event is passed as an argument and drop the d3. wherevere you see d3.event.

This seems to work pretty well:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>D3v4 hull</title>
    <!-- d3.js framework 
    <script src="https://d3js.org/d3.v4.js"></script>-->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
    <!-- d3.js Scale Chromatic plugin -->
    <script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>

    <style>
        html,
        body,
        #canvas {
            width: 100%;
            height: 100%;
        }

        path.link {
            fill: none;
            stroke: #666;
            stroke-width: 1.5px;
            opacity: 0.8;
        }

        marker#resource {
            fill: red;
        }

        path.link.resource {
            /* stroke: green; */
        }

        path.link.property {
            stroke-dasharray: 0, 2 1;
        }

        circle {
            opacity: 1;
            stroke: #333;
            stroke-width: 1.5px;
        }

        text {
            font: 10px sans-serif;
            pointer-events: none;
        }

        text.shadow {
            stroke: #fff;
            stroke-width: 3px;
            stroke-opacity: .8;
        }

        .nodes {
            opacity: 1;
        }

    </style>

<body>
    <div id="canvas"></div>

    <script type="text/javascript">
        var data = {
            'nodes': [{
                'id': 'A',
                'label': 'A',
                'type': 'resource',
                'size': 40,
                'group': 'a',
            }, {
                'id': 'B',
                'label': 'B',
                'type': 'resource',
                'size': 40,
                'group': 'a'
            }, {
                'id': 'C',
                'label': 'C',
                'type': 'resource',
                'size': 40,
                'group': 'x',
            }, {
                'id': 'D',
                'label': 'D',
                'type': 'resource',
                'size': 40,
                'group': 'x'
            }, {
                'id': 'E',
                'label': 'E',
                'type': 'resource',
                'size': 40,
                'group': 'x'
            }, {
                'id': 'F',
                'label': 'F',
                'type': 'resource',
                'size': 40,
                'group': 'a'
            }, {
                'id': 'Q',
                'label': 'Q',
                'type': 'resource',
                'size': 40,
                'group': 'q'
            }, {
                'id': 'W',
                'label': 'Wjk',
                'type': 'resource',
                'size': 40,
                'group': 'q'
            }, {
                'id': 'Z',
                'label': 'Z',
                'type': 'resource',
                'size': 40,
                'group': 'q'
            }],
            'links': [{
                'source': 'E',
                'target': 'F',
                'type': 'resource',
                'distance': 100,
                'strength': 1
            }, {
                'source': 'A',
                'target': 'F',
                'type': 'resource',
                'distance': 100,
                'strength': 1
            }, {
                'source': 'A',
                'target': 'C',
                'type': 'resource',
                'distance': 100,
                'strength': 1
            }, {
                'source': 'Q',
                'target': 'F',
                'type': 'resource',
                'distance': 100,
                'strength': 1
            }, {
                'source': 'Q',
                'target': 'E',
                'type': 'resource',
                'distance': 100,
                'strength': 1
            }, {
                'source': 'W',
                'target': 'Q',
                'type': 'resource',
                'distance': 100,
                'strength': 1
            }]
        };

        var canvas = document.getElementById('canvas');
        var w = canvas.clientWidth, h = canvas.clientHeight;
        var color = d3.scaleOrdinal(d3.schemeSet3);
        var svg = d3.select(canvas).append('svg')
            .attr('width', w)
            .attr('height', h);
        var rectWidth = 80,
            rectHeight = 30;
        var markerWidth = 10,
            markerHeight = 6,
            cRadius = 180, // play with the cRadius value
            refX = 70, //refX = cRadius + markerWidth,
            refY = 0, //refY = -Math.sqrt(cRadius),
            drSub = cRadius + refY;

        var tocolor = "fill";
        var towhite = "stroke";
        if (outline) {
            tocolor = "stroke"
            towhite = "fill"
        }

        var focus_node = null, highlight_node = null;

        var highlight_color = "blue";
        var highlight_trans = 0.1;
        var outline = false;
        var default_node_color = "#ccc";
        //var default_node_color = "rgb(3,190,100)";
        var default_link_color = "#888";

        var g = svg.append("g")
            .attr("class", "viz");
        var net, convexHull, genCH, linkElements, nodeElements, textElements, circle, simulation, linkForce;

        var expand = {};

        var linkedByIndex = {};
        data.links.forEach(function (d) {
            linkedByIndex[d.source + "," + d.target] = true;
        });
        function isConnected(a, b) {
            return linkedByIndex[a.index + "," + b.index] || linkedByIndex[b.index + "," + a.index] || a.index == b.index;
        }
        

        var groupFill = function (d, i) { return color(d.key); };

        function getGroup(n) { return n.group; }

        function network(data, prev, cekGroup, expand) {
            var cnode, groupIndex, mappedNodes = [], mappedLinks = [], clink, tempN, tempL = [], newNodes = [], soIn, taIn, lw = 0, newLinks = [];
            if (Object.getOwnPropertyNames(expand).length == 0) {
                for (var j = 0; j < data.nodes.length; j++) {
                    groupIndex = cekGroup(data.nodes[j]);
                    expand[groupIndex] = true;
                }
                nodes = data.nodes;
                links = data.links;
            } else {
                for (var k = 0; k < data.nodes.length; k++) {
                    cnode = data.nodes[k];
                    groupIndex = cekGroup(cnode);
                    if (expand[groupIndex]) {
                        mappedNodes.push(cnode);
                        //if expand true, nodes condition expand
                    } else {
                        if (!newNodes[groupIndex]) {
                            tempN = {
                                'id': groupIndex,
                                'label': 'domain ' + groupIndex,
                                'type': 'resource',
                                'size': 30,
                                'group': groupIndex
                            };
                            newNodes[groupIndex] = tempN;
                            mappedNodes.push(tempN);
                        }
                        // if expand false, nodes condition collapse
                    }
                    //iterate through all data.nodes
                }


                for (var x = 0; x < data.links.length; x++) {
                    clink = data.links[x];
                    soIn = cekGroup(clink.source);
                    taIn = cekGroup(clink.target);
                    tempL = {};
                    // if(!expand[soIn] && expand[taIn]) {
                    //   tempL.source = newNodes[soIn];
                    // }

                    if (expand[soIn] && expand[taIn]) {
                        //console.log('if1');
                        //tempL=clink;
                        soIn = clink.source.id;
                        taIn = clink.target.id;
                    } else if (!expand[soIn] && expand[taIn]) {
                        //console.log('if2');
                        //tempL.source = newNodes[soIn];
                        soIn = soIn;
                        taIn = clink.target.id;
                    } else if (expand[soIn] && !expand[taIn]) {
                        //console.log('if3');
                        //tempL.target = newNodes[taIn];
                        taIn = taIn;
                        soIn = clink.source.id;
                    } else if (!expand[soIn] && !expand[taIn]) {
                        //console.log('if4');
                        //tempL=null;
                        if (soIn == taIn) { soIn = ''; taIn = ''; }
                    }
                    if (soIn != '' && taIn != '') {
                        tempL = {
                            'source': soIn,
                            'target': taIn,
                            'type': clink.type,
                            'distance': 50,
                            'strength': 1
                        }
                        mappedLinks.push(tempL);
                    }
                }
                nodes = mappedNodes;
                links = mappedLinks;
                // endof if expand not empty
            }

            return { nodes: nodes, links: links };
        }
    
        var offset = 0, groups, groupPath;
        function init() {
            if (simulation) {
                linkElements.remove();
                nodeElements.remove();
                genCH.remove();
                convexHull.remove();
                textElements.remove();
            }
            net = network(data, net, getGroup, expand);
            //groups = d3.nest().key(function (d) { return d.group; }).entries(net.nodes);
            groups = d3.groups(net.nodes, d => d.group).map(x => ({key: x[0], values: x[1]}));
            //console.log(groups)
            
            groupPath = function (d) {
                var txt;
                if (d.values.length == 1) {
                    return "M0,0L0,0L0,0Z";
                } else {
                    return "M" +
                        d3.polygonHull(d.values.map(function (i) { return [i.x + offset, i.y + offset]; }))
                            .join("L")
                        + "Z";
                }

            };
            convexHull = g.append('g').attr('class', 'hull');
            // simulation setup with all forces
            linkForce = d3
                .forceLink()
                .id(function (link) { return link.id })
                .strength(function (link) { return 0.2 })

            var inpos = [], counterX = 1, inposY = [], counterY = 1;
            simulation = d3
                .forceSimulation()
                .force('link', d3.forceLink().id(function (d) {
        return d.id;
    }).distance(150))
                .force('forceX', d3.forceX(function (d) {
                    if (inpos[d.group]) {
                        
                        return inpos[d.group];
                    } else {
                        inpos[d.group] = w / counterX;
                        
                        counterX++;
                        return inpos[d.group];
                    }
                }))
                .force('forceY', d3.forceY(function (d) {
                    if (inposY[d.group]) {
                        
                        return inposY[d.group];
                    } else {
                        inposY[d.group] = h / (Math.random() * (d.group.length - 0 + 1) + 1);
                        
                        return inposY[d.group];
                    }
                }))
                .force('charge', d3.forceManyBody().strength(-500))
                .force('center', d3.forceCenter(w / 2, h / 2))
                .force("gravity", d3.forceManyBody(50));

            linkElements = g.append('g').attr('class', 'links').selectAll('path').data(net.links).enter().append('path')
                .attr('class', function (d) { return 'link ' + d.type; })
                

            nodeElements = g.append('g').attr('class', 'nodes').selectAll('.node')
                .data(net.nodes)
                .enter().append('g')
                .attr('class', 'node');
            // .append('circle')
            // .attr("r", cRadius)
            // .attr("fill", function(d){ return color(d.group);});
            circle = nodeElements.filter(function (d) { return d.type == 'resource'; }).append('circle')
                .attr('class', 'circle')
                .attr("r", function (d) { return d.size; })
                .attr("fill", function (d) { return color(d.group); });

            nodeElements.call(d3.drag()
                .on("start", dragstarted)
                .on("drag", dragged)
                .on("end", dragended));

            textElements = g.append("g")
                .attr("class", "texts")
                .selectAll("text")
                .data(net.nodes)
                .enter().append('text')
                .attr('text-anchor', 'middle')
                .attr('alignment-baseline', 'middle')
                .append('tspan')
                .text(function (node) { return node.label });

            simulation.nodes(net.nodes).on('tick', () => {
                genCH = convexHull.selectAll("path")
                    .data(groups)
                    .attr("d", groupPath)
                    .enter().insert("path", "circle")
                    .style("fill", groupFill)
                    .style("stroke", groupFill)
                    .style("stroke-width", 140)
                    .style("stroke-linejoin", "round")
                    .style("opacity", .5)
                    .on('click', function (evt, d) {
                        expand[d.key] = false;
                        init();
                    })
                    .attr("d", groupPath);

                nodeElements
                    .attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; })
                // .attr('x', function (node) { console.log(node); return node.x })
                // .attr('y', function (node) { return node.y })
                textElements
                    .attr('x', function (node) { return node.x })
                    .attr('y', function (node) { return node.y })
                linkElements
                    .attr('d', function (d) {
                        var dx = d.target.x - d.source.x,
                            dy = d.target.y - d.source.y,
                            dr = Math.sqrt(dx * dx + dy * dy);
                    

                        var val = 'M' + d.source.x + ',' + d.source.y + 'A' + (dr - drSub) + ',' + (dr - drSub) + ' 0 0,1 ' + d.target.x + ',' + d.target.y;

                        var val2 = 'M' + d.source.x + ',' + d.source.y + 'L' + (d.target.x) + ',' + (d.target.y);
                        if (d.type == 'resource') return val2;
                        else return val1;
                    });
            })

            nodeElements.on("mouseover", function (d) {
                set_highlight(d);
            })
                .on("mousedown", function (event, d) {
                    event.stopPropagation();
                    focus_node = d;
                
                    set_focus(d)
                    if (highlight_node === null) set_highlight(d)

                }).on("mouseout", function (event, d) {
                    exit_highlight();

                }).on("click", function (event, d) {
                    event.stopPropagation();
                
                    setExpand(d);
                });

            simulation.force("link").links(net.links).distance(function (d) {
                if (d.source.group == d.target.group) return 85;
                else return 180;
                // if(d.type=='resource') return 300;
                // else return 150;
                // return d.distance;
            });

            function setExpand(d) {
                expand[d.id] = !expand[d.id];
                init();
            }

            function exit_highlight() {
                highlight_node = null;
                if (focus_node === null) {
                    svg.style("cursor", "move");
                    if (highlight_color != "white") {
                        circle.style(towhite, "white");
                        linkElements.style("stroke", function (o) { return (isNumber(o.score) && o.score >= 0) ? color(o.score) : default_link_color });
                    }

                }
            }

            function set_focus(d) {
                if (highlight_trans < 1) {
                    circle.style("opacity", function (o) {
                        return isConnected(d, o) ? 1 : highlight_trans;
                    });

                    linkElements.style("opacity", function (o) {
                        return o.source.index == d.index || o.target.index == d.index ? 1 : highlight_trans;
                    });
                }
            }

            function set_highlight(d) {

                svg.style("cursor", "pointer");
                // circle.style('opacity',0.7);
                if (focus_node !== null) d = focus_node;
                highlight_node = d;

                if (highlight_color != "white") {
                    circle.style(towhite, function (o) {
                        return isConnected(d, o) ? highlight_color : "white";
                    });
                    //             linkElements.style("stroke", function(o) {
                    //            return o.source.index == d.index || o.target.index == d.index ? highlight_color : ((isNumber(o.score) && o.score>=0)?color(o.score):default_link_color);
                    // });
                }
            }

            function dragstarted(event, d) {
                if (!event.active) simulation.alphaTarget(0.3).restart();
                d.fx = d.x;
                d.fy = d.y;
            }

            function dragged(event, d) {
                d.fx = event.x;
                d.fy = event.y;
            }

            function dragended(event, d) {
                if (!event.active) simulation.alphaTarget(0);
                d.fx = null;
                d.fy = null;
            }
            // endof init()
        }

        init();
        //add zoom capabilities
        var zoom_handler = d3.zoom()
            .on("zoom", zoom_actions);

        zoom_handler(svg);
        function zoom_actions(event) {
            g.attr("transform", event.transform);
        }

        function isNumber(n) {
            return !isNaN(parseFloat(n)) && isFinite(n);
        }
    </script>

</body>

</html>

Upvotes: 2

Related Questions