Skip to content

Instantly share code, notes, and snippets.

@bycoffe
Created January 28, 2014 17:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bycoffe/8672107 to your computer and use it in GitHub Desktop.
Save bycoffe/8672107 to your computer and use it in GitHub Desktop.
Force layout with collision detection, sorting and variable-radius circles
<!doctype html>
<meta charset="utf-8">
<html>
<head>
<style type="text/css">
#canvas {
width: 900px;
height: 500px;
}
#canvas text {
text-anchor: middle;
font-family: Arial, sans-serif;
font-size: 12px;
}
</style>
</head>
<body>
<div class="buttons">
Sort by:
<button value="positive">Least to greatest</button>
<button value="negative">Greatest to least</button>
<button value="color">Color</button>
</div>
<div id="canvas"></div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="main.js"></script>
</body>
</html>
;(function() {
var g,
colorId,
numNodes = 50,
nodes = [],
width = 900,
height = 500,
padding = 10,
minR = 5,
maxR = 40,
position = "positive",
scales = {
x: d3.scale.linear()
.domain([0, numNodes])
.range([((width/12.5)-10)*-1, (width-20)/12.5]),
colorX: d3.scale.linear()
.domain([0, 10])
.range([((width/12.5)-10)*-1, (width-20)/12.5]),
y: d3.scale.linear()
.domain([0, numNodes])
.range([(height/25)*-1, height/25]),
r: d3.scale.sqrt()
.domain([1, 10])
.range([minR, maxR])
};
function randomRadius() {
return scales.r(Math.floor(Math.random() * 10) + 1);
}
for (i=0; i<numNodes; i++) {
colorId = Math.random() * 10;
nodes.push({
id: i,
r: randomRadius(),
cx: scales.x(i),
cy: scales.y(Math.floor(Math.random()*numNodes)),
colorId: colorId,
color: d3.scale.category10().range()[Math.floor(colorId)]
});
}
var svg = d3.select("#canvas").append("svg")
.attr({
width: width,
height: height
}),
force = d3.layout.force()
.nodes(nodes)
.links([])
.size([width, height])
.charge(function(d) {
return -1 * (Math.pow(d.r * 5.0, 2.0) / 8);
})
.gravity(2.75)
.on("tick", tick);
function update(nodes) {
g = svg.selectAll("g.node")
.data(nodes, function(d, i) {
return d.id;
});
g.enter().append("g")
.attr({
"class": "node"
});
if (g.selectAll("circle").empty()) {
circle = g.append("circle")
.attr({
r: function(d) {
return d.r;
}
})
.style({
fill: function(d) {
return d.color;
}
});
label = g.append("text")
.attr({
x: 0,
y: 3,
})
.text(function(d) {
return d.id;
});
} else {
circle.transition()
.duration(1000)
.attr({
r: function(d) {
return d.r;
}
})
}
g.exit().remove();
force.nodes(nodes).start();
}
// Adapted from http://bl.ocks.org/3116713
function collide(alpha, nodes, scale) {
quadtree = d3.geom.quadtree(nodes);
return function(d) {
r = d.r + scale.domain()[1] + padding
nx1 = d.x - r;
nx2 = d.x + r;
ny1 = d.y - r;
ny2 = d.y + r;
return quadtree.visit(function(quad, x1, y1, x2, y2) {
var l, x, y;
if (quad.point && quad.point !== d) {
x = d.x - quad.point.x;
y = d.y - quad.point.y;
l = Math.sqrt(x * x + y * y);
r = d.r + quad.point.r + padding;
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
};
// See https://github.com/mbostock/d3/wiki/Force-Layout
function tick(e) {
k = 10 * e.alpha;
nodes.forEach(function(d, i) {
d.x += k * d.cx;
d.y += k * d.cy;
});
g.each(collide(.1, nodes, scales.r))
.attr({
transform: function(d, i) {
return "translate(" + d.x + "," + d.y + ")";
}
});
};
update(nodes);
d3.selectAll("button").on("click", function() {
var sort = this.getAttribute("value");
nodes.forEach(function(node) {
if (sort === "positive") {
node.cx = scales.x(node.id);
}
else if (sort === "negative") {
node.cx = scales.x(numNodes-node.id);
}
else if (sort === "color") {
node.cx = scales.colorX(node.colorId);
}
node.r = randomRadius();
});
update(nodes);
});
}());
@bluePlayer
Copy link

bluePlayer commented Dec 7, 2016

Done some hacking. This reworked code passes JSlint test without warnings:

 `/**
    * @see https://bl.ocks.org/mbostock/4062045
    * @param {Object} d3
    */
    (function(d3) {
        'use strict';

    var g, i = 0,
        colorId,
        numNodes = 50,
        nodes = [],
        width = 900,
        height = 500,
        padding = 10,
        minR = 5,
        maxR = 40,
        position = "positive",
        circle = null,
        label = null,

        svg = d3.select("#canvas")
            .append("svg")
            .attr({width: width, height: height}),

        scales = {
            x: d3.scale.linear()
                      .domain([0, numNodes])
                      .range([(((width / 12.5) - 10) * -1), ((width - 20) / 12.5)]),
            colorX: d3.scale.linear()
                      .domain([0, 10])
                      .range([(((width / 12.5) - 10) * -1), ((width - 20) / 12.5)]),
            y: d3.scale.linear()
                      .domain([0, numNodes])
                      .range([((height / 25) * -1), (height / 25)]),
            r: d3.scale.sqrt()
                      .domain([1, 10])
                      .range([minR, maxR])
        },

        randomRadius = function () {
            return scales.r(Math.floor(Math.random() * 10) + 1);
        },

        /**
         * @see http://bl.ocks.org/3116713
         */
        collide = function (alpha, nodes, scale) {
            var quadtree = d3.geom.quadtree(nodes),
                r = 0, nx1 = 0, nx2 = 0, ny1 = 0, ny2 = 0;

            return function (d) {
                r = d.r + scale.domain()[1] + padding;
                nx1 = d.x - r;
                nx2 = d.x + r;
                ny1 = d.y - r;
                ny2 = d.y + r;

                return quadtree.visit(function(quad, x1, y1, x2, y2) {
                    var l, x, y;

                    if (quad.point && quad.point !== d) {
                        x = d.x - quad.point.x;
                        y = d.y - quad.point.y;
                        l = Math.sqrt(x * x + y * y);
                        r = d.r + quad.point.r + padding;

                        if (l < r) {
                            l = (l - r) / l * alpha;
                            d.x -= x *= l;
                            d.y -= y *= l;
                            quad.point.x += x;
                            quad.point.y += y;
                        }
                    }

                    return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
                });
            };
        },

        /**
         * @see https://github.com/mbostock/d3/wiki/Force-Layout
         * @param {Object} e
         */
        tick = function (e) {
            var k = (10 * e.alpha);

            nodes.forEach(function(d, i) {
                d.x += k * d.cx;
                d.y += k * d.cy;
            });

            g.each(collide(0.1, nodes, scales.r))
                .attr({
                    transform: function(d, i) {
                    return "translate(" + d.x + "," + d.y + ")";
                }
            });
        },

        force = d3.layout.force()
            .nodes(nodes)
            .links([])
            .size([width, height])
            .charge(function(d) {
                return -1 * (Math.pow(d.r * 5.0, 2.0) / 8);
            })
            .gravity(2.75)
            .on("tick", tick),

        update = function (nodes) {

            g = svg.selectAll("g.node")
                .data(nodes, function(d, i) {
                    return d.id;
                });

            g.enter().append("g")
                .attr({
                    "class": "node"
                });

            if (g.selectAll("circle").empty()) {

                circle = g.append("circle")
                    .attr({r: function(d) {return d.r;}})
                    .style({fill: function(d) {return d.color;}});

                label = g.append("text")
                    .attr({x: 0, y: 3})
                    .text(function(d) {return d.id;});

            } else {
                circle.transition()
                    .duration(1000)
                    .attr({r: function(d) {return d.r;}});
            }

            g.exit().remove();
            force.nodes(nodes).start();
        };

    for (i = 0; i < numNodes; i += 1) {
        colorId = (Math.random() * 10);
        nodes.push({
            id: i,
            r: randomRadius(),
            cx: scales.x(i),
            cy: scales.y(Math.floor(Math.random() * numNodes)),
            colorId: colorId,
            color: d3.scale.category10().range()[Math.floor(colorId)]
        });
    }

    update(nodes);

    d3.selectAll("button").on("click", function() {
        var sort = this.getAttribute("value");

        nodes.forEach(function(node) {

            if (sort === "positive") {
                node.cx = scales.x(node.id);
            } else if (sort === "negative") {
                node.cx = scales.x(numNodes-node.id);
            } else if (sort === "color") {
                node.cx = scales.colorX(node.colorId);
            }

            node.r = randomRadius();
        });

        update(nodes);
    });

}(d3));`

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment