Clutter-free scatterplot in generalized Barycentric coordinates.
Last active
November 17, 2017 19:14
-
-
Save fandu/e2d1ecd99b1ca5c75edf2a6d81b447ab to your computer and use it in GitHub Desktop.
Barycentric Coordinates
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: mit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<script src="//d3js.org/d3.v3.min.js"></script> | |
<script> | |
var radius = 200; | |
var width = 960; | |
var height = 500; | |
var margin = { "x": width / 2 - radius, "y": height / 2 - radius }; | |
var speed = 1500; | |
var xMap = d3.scale.linear().domain([-1, 1]).range([0, radius * 2]); | |
var yMap = d3.scale.linear().domain([-1, 1]).range([0, radius * 2]); | |
var svg = d3.select("html").append("svg") | |
.attr("width", width) | |
.attr("height", height) | |
.append("g") | |
.attr("transform", "translate(" + margin.x + "," + margin.y + ")"); | |
var data = randData(); | |
var visualItems = svg.selectAll(".item").data(data.items); | |
visualItems.exit().remove(); | |
visualItems.enter() | |
.append("circle") | |
.attr("class", "item") | |
.attr("fill", "#4292c6") | |
.attr("stroke-width", 0.5) | |
.attr("stroke", "black") | |
.attr("cx", radius) | |
.attr("cy", radius) | |
.attr("r", function(d) { | |
return d.r * (radius / data.r0); | |
}); | |
repelInit(data) | |
visualItems | |
.transition().duration(speed) | |
.attr("cx", function(d) { | |
return xMap(d.px + d.ox); | |
}) | |
.attr("cy", function(d) { | |
return yMap(d.py + d.oy); | |
}); | |
repelIterating(data); | |
visualItems | |
.transition().delay(speed * 0.6).duration(speed) | |
.attr("cx", function(d) { | |
return xMap(d.px + d.ox); | |
}) | |
.attr("cy", function(d) { | |
return yMap(d.py + d.oy); | |
}); | |
svg.append("circle") | |
.attr("r", radius) | |
.attr("fill", "none") | |
.attr("stroke-width", 1) | |
.attr("stroke", "black") | |
.style("stroke-dasharray", "5, 5") | |
.attr("cx", radius) | |
.attr("cy", radius); | |
svg.selectAll(".anchor") | |
.data(data.anchors) | |
.enter().append("circle") | |
.attr("class", "anchor") | |
.attr("fill", "#e6550d") | |
.attr("r", 4) | |
.attr("cx", function(d) { | |
return xMap(d.x0); | |
}) | |
.attr("cy", function(d) { | |
return yMap(d.y0); | |
}); | |
function randData() { | |
var data = { | |
"r0": 1, | |
"items": [], | |
"anchors": [{ | |
"x0": 0, | |
"y0": -1, | |
}, { | |
"x0": 1, | |
"y0": 0, | |
}, { | |
"x0": 0, | |
"y0": 1, | |
}, { | |
"x0": -1, | |
"y0": 0, | |
}] | |
}; | |
for (var i = 0; i < data.anchors.length; i++) { | |
for (var j = 0; j < 100; j++) { | |
var weights = [Math.random(), Math.random(), Math.random(), Math.random()]; | |
weights[i] *= 10; | |
data.items.push({ "anchor_weights": weights, "r": 0.025 }); | |
} | |
} | |
return data; | |
} | |
function repelInit(data) { | |
for (var i = 0; i < data.items.length; i++) { | |
var u = data.items[i]; | |
u.ox = 0; | |
u.oy = 0; | |
var sum = 0; | |
for (var j = 0; j < u.anchor_weights.length; j++) { | |
sum = sum + u.anchor_weights[j]; | |
} | |
u.px = 0; | |
u.py = 0; | |
for (var j = 0; j < u.anchor_weights.length; j++) { | |
u.px += data.anchors[j].x0 * u.anchor_weights[j] / sum; | |
u.py += data.anchors[j].y0 * u.anchor_weights[j] / sum; | |
} | |
} | |
return data; | |
} | |
function repelIterating(data, iterations) { | |
iterations = typeof iterations !== 'undefined' ? iterations : 100; | |
for (var iter = 0; iter < iterations; iter++) { | |
var changed = false; | |
for (var i = 0; i < data.items.length; i++) { | |
for (var j = 0; j < data.items.length; j++) { | |
if (i == j) continue; | |
var ui = data.items[i]; | |
var uj = data.items[j]; | |
var dx = (ui.px + ui.ox) - (uj.px + uj.ox); | |
var dy = (ui.py + ui.oy) - (uj.py + uj.oy); | |
var d2 = dx * dx + dy * dy; | |
var rij = ui.r + uj.r; | |
var rij2 = rij * rij; | |
if (d2 < rij2) { | |
var d = Math.sqrt(d2); | |
var f = (rij - d) / rij; | |
var fx = f * dx; | |
var fy = f * dy; | |
if (d == 0) { | |
dx = Math.random() - 0.5; | |
var dx2 = dx * dx; | |
dy = Math.random() - 0.5; | |
var dy2 = dy * dy; | |
fx = Math.sqrt(dx2 / (dx2 + dy2)) * rij; | |
if (dx < 0) fx = -fx; | |
fy = Math.sqrt(dy2 / (dx2 + dy2)) * rij; | |
if (dy < 0) fy = -fy; | |
} | |
ui.ox += fx; | |
ui.oy += fy; | |
uj.ox -= fx; | |
uj.oy -= fy; | |
changed = true; | |
} | |
} | |
} | |
if (!changed) { | |
break; | |
} | |
for (var i = 0; i < data.items.length; i++) { | |
var u = data.items[i]; | |
u.ox = u.ox * 0.95; | |
u.oy = u.oy * 0.95; | |
var dx = u.px + u.ox; | |
var dy = u.py + u.oy; | |
var dx2 = dx * dx; | |
var dy2 = dy * dy; | |
var d2 = dx2 + dy2; | |
var r2 = data.r0 * data.r0; | |
if (d2 > r2) { | |
var d = Math.sqrt(d2); | |
var f = (d - data.r0) / d; | |
var fx = f * dx; | |
var fy = f * dy; | |
u.ox = u.ox - fx; | |
u.oy = u.oy - fy; | |
} | |
} | |
} | |
} | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment