Skip to content

Instantly share code, notes, and snippets.

@Fil
Last active July 11, 2017 12:33
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Fil/6037e056202bb3be9f172a6ab3361b53 to your computer and use it in GitHub Desktop.
Save Fil/6037e056202bb3be9f172a6ab3361b53 to your computer and use it in GitHub Desktop.
d3-cluster
license: mit
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-scale')) :
typeof define === 'function' && define.amd ? define(['exports', 'd3-scale'], factory) :
(factory((global.d3 = global.d3 || {}),global.d3));
}(this, (function (exports,d3Scale) { 'use strict';
function isArray(o) {
return Array.isArray(o);
}
function isPlainObject(o) {
return !isArray(o) && typeof o === 'object';
}
function isEqualArray(target, source) {
return target.every(function (o, i) {
if (isPlainObject(o)) {
return isEqualObject(o, source[i]);
}
if (isArray(o)) {
return isEqualArray(o, source[i]);
}
return o === source[i];
});
}
function isEqualObject(target, source) {
return Object.keys(target).every(function (key) {
return target[key] === source[key];
});
}
function isEqual(target, source) {
if (isArray(target) && isArray(source)) {
return isEqualArray(target, source);
}
if (isPlainObject(target) && isPlainObject(source)) {
return isEqualObject(target, source);
}
return target === source;
}
function cloneArray(source) {
var target = [];
source.forEach(function (o) {
if (typeof o === 'object') {
if (isArray(o)) {
target.push(cloneArray(o));
} else {
target.push(Object.assign({}, o));
}
} else {
target.push(o);
}
});
return target;
}
function cloneDeep(source) {
if (isArray(source)) {
return cloneArray(source);
}
if (isPlainObject(source)) {
return Object.assign({}, source);
}
return source;
}
// import quadtree from 'd3-quadtree';
function isMissing(arr, obj) {
return arr.every(function (d) {
return !isEqual(d, obj);
});
}
function cluster () {
var x = function (d) { return d[0]; };
var y = function (d) { return d[1]; };
var radius = function (d) { return d[2]; };
var xScale = d3Scale.scaleLinear();
var yScale = d3Scale.scaleLinear();
var centroid = function (p0, p1) { return (p1 + p0) / 2; };
function X(d, i) {
return xScale(x.call(this, d, i));
}
function Y(d, i) {
return yScale(y.call(this, d, i));
}
function clusterize(data) {
var clusteredPoints = [];
var overlappingPoints = []; // What is the point of this?
var modifiedData = data
.map(function (d, i) {
return {
x: X.call(this, d, i),
y: Y.call(this, d, i),
radius: radius.call(this, d, i),
point: cloneDeep(d) // copy of original data point
};
})
.sort(function (a, b) {
return b.radius - a.radius;
});
// var targetPoints = cloneDeep(modifiedData); // copy of data
var targetPoints = modifiedData; // copy of data
modifiedData.forEach(function (p) {
if (isMissing(overlappingPoints, p)) {
p.overlap = [];
clusteredPoints.push(p);
targetPoints.forEach(function (t) {
if (t !== p) {
var distance = Math.sqrt(Math.pow(Math.abs(t.x - p.x), 2) +
Math.pow(Math.abs(t.y - p.y), 2));
if (distance < (t.radius + p.radius)) {
t.clustered = true;
p.overlap.push(t);
overlappingPoints.push(t);
}
}
});
targetPoints = targetPoints.filter(function (d) {
return isMissing(p.overlap, d);
});
}
});
return clusteredPoints;
}
function layout(data) {
return clusterize(data);
}
// Public API
layout.x = function (_) {
if (!arguments.length) { return x; }
x = typeof _ === 'function' ? _ : x;
return layout;
};
layout.y = function (_) {
if (!arguments.length) { return y; }
y = typeof _ === 'function' ? _ : y;
return layout;
};
layout.radius = function (_) {
if (!arguments.length) { return radius; }
if (typeof _ === 'function') { radius = _; }
if (typeof _ === 'number') { radius = function () { return _; }; }
return layout;
};
layout.centroid = function (_) {
if (!arguments.length) { return centroid; }
centroid = typeof _ === 'function' ? _ : centroid;
return layout;
};
layout.xScale = function (_) {
if (!arguments.length) { return xScale; }
xScale = typeof _ === 'function' ? _ : xScale;
return layout;
};
layout.yScale = function (_) {
if (!arguments.length) { return yScale; }
yScale = typeof _ === 'function' ? _ : yScale;
return layout;
};
return layout;
}
exports.cluster = cluster;
Object.defineProperty(exports, '__esModule', { value: true });
})));
!function(n,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("d3-scale")):"function"==typeof define&&define.amd?define(["exports","d3-scale"],t):t(n.d3=n.d3||{},n.d3)}(this,function(n,t){"use strict";function e(n){return Array.isArray(n)}function r(n){return!e(n)&&"object"==typeof n}function u(n,t){return n.every(function(n,i){return r(n)?o(n,t[i]):e(n)?u(n,t[i]):n===t[i]})}function o(n,t){return Object.keys(n).every(function(e){return n[e]===t[e]})}function i(n,t){return e(n)&&e(t)?u(n,t):r(n)&&r(t)?o(n,t):n===t}function c(n){var t=[];return n.forEach(function(n){"object"==typeof n?e(n)?t.push(c(n)):t.push(Object.assign({},n)):t.push(n)}),t}function f(n){return e(n)?c(n):r(n)?Object.assign({},n):n}function a(n,t){return n.every(function(n){return!i(n,t)})}function s(){function n(n,t){return s(o.call(this,n,t))}function e(n,t){return l(i.call(this,n,t))}function r(t){var r=[],u=[],o=t.map(function(t,r){return{x:n.call(this,t,r),y:e.call(this,t,r),radius:c.call(this,t,r),point:f(t)}}).sort(function(n,t){return t.radius-n.radius}),i=o;return o.forEach(function(n){a(u,n)&&(n.overlap=[],r.push(n),i.forEach(function(t){if(t!==n){Math.sqrt(Math.pow(Math.abs(t.x-n.x),2)+Math.pow(Math.abs(t.y-n.y),2))<t.radius+n.radius&&(t.clustered=!0,n.overlap.push(t),u.push(t))}}),i=i.filter(function(t){return a(n.overlap,t)}))}),r}function u(n){return r(n)}var o=function(n){return n[0]},i=function(n){return n[1]},c=function(n){return n[2]},s=t.scaleLinear(),l=t.scaleLinear(),p=function(n,t){return(t+n)/2};return u.x=function(n){return arguments.length?(o="function"==typeof n?n:o,u):o},u.y=function(n){return arguments.length?(i="function"==typeof n?n:i,u):i},u.radius=function(n){return arguments.length?("function"==typeof n&&(c=n),"number"==typeof n&&(c=function(){return n}),u):c},u.centroid=function(n){return arguments.length?(p="function"==typeof n?n:p,u):p},u.xScale=function(n){return arguments.length?(s="function"==typeof n?n:s,u):s},u.yScale=function(n){return arguments.length?(l="function"==typeof n?n:l,u):l},u}n.cluster=s,Object.defineProperty(n,"__esModule",{value:!0})});
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="d3-cluster.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
svg {
height: 1000px;
width: 1000px;
border: 1px solid lightgray;
}
</style>
</head>
<body>
<div id="viz">
<svg class="main">
</svg>
</div>
</body>
<footer>
<script>
const colors = [
"#00a2ce",
"#4d430c",
"#b3331d",
"#b6a756"
]
const testData = []
for (let x=1;x<500;x++) {
testData.push({ x: 10 + Math.random() * 940, y: 10 + Math.random() * 480, r: 1 + Math.random() * 20, color: colors[x%4] })
}
d3.select("svg")
.selectAll("circle")
.data(testData)
.enter()
.append("circle")
.attr("r", d => d.r)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.style("fill", d => d.color)
.style("fill-opacity", 0.5)
setTimeout(() => {
const cluster = d3.cluster()
.x(d => d.x)
.y(d => d.y)
.radius(d => d.r);
d3.select("svg")
.selectAll("circle.collision")
.data(cluster(testData))
.enter()
.append("circle")
.attr("r", d => d.radius)
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.style("fill", "#FCBC34")
.style("fill-opacity", d => d.overlap.length === 0 ? 1 : 0)
.style("stroke", d => d.overlap.length === 0 ? "none" : "#FE9922")
.style("stroke-width", d => d.overlap.length + 1)
}, 1000)
</script>
</footer>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment