hoohoo-b
hoohoo-b

Reputation: 1251

D3: How to draw multiple Convex hulls on Groups of Force layout nodes?

I'm trying to draw convex hulls on all the groups in a force layout. But I only manage to draw half of the convex hulls. When D3 tries to draw the rest of the hulls, the console returns ERROR: the elements have not been created yet. Yet when I check the "groups" variable in the console, all the groups data are there with x, y data all nicely set up. See picture below:

I even tried delaying the drawing of the hull in the tick function, but it still doesn't work & I get the same results (as seen in picture below).

JSFiddle: Only getting half the no. of convex hulls I want

Here is the code:

<script>
    var radius = 5.5;
    var color = d3.scaleOrdinal(d3.schemeCategory20b);
    var scale = d3.scaleLinear()
        .domain([0.5, 1])
        .range([1.8, 3.8]);
    var svg2 = d3.select("#svg2");
    var w = +svg2.attr("width"),
        h = +svg2.attr("height");
    var hull = svg2.append("path")
        .attr("class", "hull");
    var groupPath = function(d) { return "M" + d3.polygonHull(d.values.map(function(i) { return [i.x, i.y]; }))
        .join("L") + "Z"; };

    function ticked() {
        link
            .attr("x1", function (d) {
                return d.source.x;
            })
            .attr("y1", function (d) {
                return d.source.y;
            })
            .attr("x2", function (d) {
                return d.target.x;
            })
            .attr("y2", function (d) {
                return d.target.y;
            });

        fnode
            .attr("cx", function (d) {
                return d.x = Math.max(radius, Math.min(w - radius, d.x));
            })
            .attr("cy", function (d) {
                return d.y = Math.max(radius, Math.min(h - radius, d.y));
            })
            .attr("r", radius);

        delayHull(6000);

    }

    function delayHull(delay) {
        setTimeout(function() {
            svg2.selectAll("path")
                .data(groups)
                .attr("d", groupPath)
                .enter()
                .append("path")
                .attr("class", "hull")
                .attr("d", groupPath);

        }, delay);
    }

    var simulation, link, fnode, groups;

    var fnodeg = svg2.append("g")
        .attr("class", "fnode");

    var linkg = svg2.append("g")
        .attr("class", "links")
        .attr("id", "linkg");

    d3.json("..//vizData//forceLayout//forceLayout_15000.json", function(error, graph) {
        if (error) throw error;
        simulation = d3.forceSimulation()
            .force("link", d3.forceLink().id(function (d) {
                return d.id;
            }).distance(30).strength(1))
            .force("charge", d3.forceManyBody().strength(-2).distanceMin(15).distanceMax(180))
            .force("center", d3.forceCenter(w / 2, h / 2))
            .force("collide", d3.forceCollide().strength(1).iterations(2));

        link = linkg.selectAll("line")
            .data(graph.links)
            .enter().append("line")
            .attr("stroke-width", function (d) {
                return scale(d.value);
            });

        fnode = fnodeg.selectAll("circle")
            .data(graph.nodes)
            .enter().append("circle")
            .attr("r", radius)
            .attr("fill", function (d) {
                return color(d.truth);
            });

        simulation
            .nodes(graph.nodes);

        simulation.force("link")
            .links(graph.links);

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

        simulation.on("tick", ticked);

        fnode.append("title")
            .text(function (d) { return d.id; });

        link.append("title")
            .text(function (d) { return d.value; });

    })
</script>

I referenced this http://bl.ocks.org/donaldh/2920551 convex hull example; he set up his "groups" variable outside the tick function, and it was ok.

What am I doing wrong???

Upvotes: 3

Views: 1871

Answers (2)

Andrew Reid
Andrew Reid

Reputation: 38181

Your issue, I believe, is that some groups only have 2 nodes. In v4 this generates a type error, as d3.polygonHull() will return null if using two points - a convex hull requires three points (presumably not in a line. Correction - they can be in a line, see Gerardo's comment on this answer and his answer as well). The following snippets are barely modified from Mike's canonical example:

This snippet demonstrates the problem:

var width = 960,
    height = 500;

var randomX = d3.randomNormal(width / 2, 60),
    randomY = d3.randomNormal(height / 2, 60),
    vertices = d3.range(2).map(function() { return [randomX(), randomY()]; });

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height)
    .on("mousemove", function() { vertices[0] = d3.mouse(this); redraw(); })
    .on("click", function() { vertices.push(d3.mouse(this)); redraw(); });

svg.append("rect")
    .attr("width", width)
    .attr("height", height);

var hull = svg.append("path")
    .attr("class", "hull");

var circle = svg.selectAll("circle");

redraw();

function redraw() {
  hull.datum(d3.polygonHull(vertices)).attr("d", function(d) { return "M" + d.join("L") + "Z"; });
   
  circle = circle.data(vertices);
  circle.enter().append("circle").attr("r", 3);
  circle.attr("transform", function(d) { return "translate(" + d + ")"; });
}
rect {
  fill: none;
  pointer-events: all;
}

.hull {
  fill: steelblue;
  stroke: steelblue;
  stroke-width: 32px;
  stroke-linejoin: round;
}

circle {
  fill: white;
  stroke: black;
  stroke-width: 1.5px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>

It seems this didn't generate an error in v3:

var width = 960,
    height = 500;

var randomX = d3.random.normal(width / 2, 60),
    randomY = d3.random.normal(height / 2, 60),
    vertices = d3.range(2).map(function() { return [randomX(), randomY()]; });

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height)
    .on("mousemove", function() { vertices[0] = d3.mouse(this); redraw(); })
    .on("click", function() { vertices.push(d3.mouse(this)); redraw(); });

svg.append("rect")
    .attr("width", width)
    .attr("height", height);

var hull = svg.append("path")
    .attr("class", "hull");

var circle = svg.selectAll("circle");

redraw();

function redraw() {
  hull.datum(d3.geom.hull(vertices)).attr("d", function(d) { return "M" + d.join("L") + "Z"; });
  
  console.log(d3.geom.hull(vertices));
  
  circle = circle.data(vertices);
  circle.enter().append("circle").attr("r", 3);
  circle.attr("transform", function(d) { return "translate(" + d + ")"; });
}
rect {
  fill: none;
  pointer-events: all;
}

.hull {
  fill: steelblue;
  stroke: steelblue;
  stroke-width: 32px;
  stroke-linejoin: round;
}

circle {
  fill: white;
  stroke: black;
  stroke-width: 1.5px;
}
<script src="https://d3js.org/d3.v3.min.js"></script>

Upvotes: 2

Gerardo Furtado
Gerardo Furtado

Reputation: 102188

Building upon Andrew's answer, you can simply push another inner array when your cluster have just two points:

if (d.values.length === 2) {
    var arr = d.values.map(function(i) {
        return [i.x, i.y];
    })
    arr.push([arr[0][0], arr[0][1]]);
    return "M" + d3.polygonHull(arr).join("L") + "Z";

Here is your code with that change only:

        var radius = 5.5;
        var color = d3.scaleOrdinal(d3.schemeCategory20b);
        var scale = d3.scaleLinear()
          .domain([0.5, 1])
          .range([1.8, 3.8]);
        var svg2 = d3.select("#svg2");
        var w = +svg2.attr("width"),
          h = +svg2.attr("height");
        var hull = svg2.append("path")
          .attr("class", "hull");
        var groupPath = function(d) {
          if (d.values.length === 2) {
            var arr = d.values.map(function(i) {
              return [i.x, i.y];
            })
            arr.push([arr[0][0], arr[0][1]]);
            return "M" + d3.polygonHull(arr).join("L") + "Z";
          } else {
            return "M" + d3.polygonHull(d.values.map(function(i) {
                return [i.x, i.y];
              }))
              .join("L") + "Z";
          }
        };

        function ticked() {
          link
            .attr("x1", function(d) {
              return d.source.x;
            })
            .attr("y1", function(d) {
              return d.source.y;
            })
            .attr("x2", function(d) {
              return d.target.x;
            })
            .attr("y2", function(d) {
              return d.target.y;
            });

          fnode
            .attr("cx", function(d) {
              return d.x = Math.max(radius, Math.min(w - radius, d.x));
            })
            .attr("cy", function(d) {
              return d.y = Math.max(radius, Math.min(h - radius, d.y));
            })
            .attr("r", radius);

          delayHull(1000);

        }

        function delayHull(delay) {
          setTimeout(function() {
            svg2.selectAll("path")
              .data(groups)
              .attr("d", groupPath)
              .enter()
              .append("path")
              .attr("class", "hull")
              .attr("d", groupPath);

          }, delay);
        }

        var simulation, link, fnode, groups;

        var fnodeg = svg2.append("g")
          .attr("class", "fnode");

        var linkg = svg2.append("g")
          .attr("class", "links")
          .attr("id", "linkg");

        d3.json('https://api.myjson.com/bins/bkzxh', function(error, graph) {
          if (error) throw error;
          simulation = d3.forceSimulation()
            .force("link", d3.forceLink().id(function(d) {
              return d.id;
            }).distance(30).strength(1))
            .force("charge", d3.forceManyBody().strength(-2).distanceMin(15).distanceMax(180))
            .force("center", d3.forceCenter(w / 2, h / 2))
            .force("collide", d3.forceCollide().strength(1).iterations(2));

          link = linkg.selectAll("line")
            .data(graph.links)
            .enter().append("line")
            .attr("stroke-width", function(d) {
              return scale(d.value);
            });

          fnode = fnodeg.selectAll("circle")
            .data(graph.nodes)
            .enter().append("circle")
            .attr("r", radius)
            .attr("fill", function(d) {
              return color(d.truth);
            });

          simulation
            .nodes(graph.nodes);

          simulation.force("link")
            .links(graph.links);

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

          simulation.on("tick", ticked);

          fnode.append("title")
            .text(function(d) {
              return d.id;
            });

          link.append("title")
            .text(function(d) {
              return d.value;
            });

        });
.links line {
  stroke: #999;
  stroke-opacity: 0.8;
}

.fnode circle {
  stroke: #fff;
  stroke-width: 1.5px;
  fill-opacity: 1;
}

.hull {
  fill: steelblue;
  stroke: steelblue;
  fill-opacity: 0.3;
  stroke-opacity: 0.3;
  stroke-width: 10px;
  stroke-linejoin: round;
}
<script src="https://d3js.org/d3.v4.js"></script>
<svg id="svg2" width="600" height="600" style="margin-left: -5px"></svg>

Upvotes: 5

Related Questions