Skip to content

Instantly share code, notes, and snippets.

@mforando
Last active February 19, 2020 14:41
Show Gist options
  • Save mforando/f44ceb116a570be8ce2cfa42e2744b26 to your computer and use it in GitHub Desktop.
Save mforando/f44ceb116a570be8ce2cfa42e2744b26 to your computer and use it in GitHub Desktop.
Label Div Collision - Stars
license: mit
<html>
<head>
<title>Bounding Box Collision</title>
<meta charset="utf-8" />
<style>
.labels{
-webkit-box-shadow: 0px 0px 10px -5px rgba(0,0,0,0.5);
-moz-box-shadow: 0px 0px 10px -5px rgba(0,0,0,0.5);
box-shadow: 0px 0px 10px -5px rgba(0,0,0,0.5);
}
</style>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
//source = https://bl.ocks.org/emeeks/7669aa65a172bf69688ace5f6041223d
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-quadtree')) :
typeof define === 'function' && define.amd ? define(['exports', 'd3-quadtree'], factory) :
(factory((global.d3 = global.d3 || {}),global.d3));
}(this, function (exports,d3Quadtree) { 'use strict';
function bboxCollide (bbox) {
function x (d) {
return d.x + d.vx;
}
function y (d) {
return d.y + d.vy;
}
function constant (x) {
return function () {
return x;
};
}
var nodes,
boundingBoxes,
strength = 1,
iterations = 1;
if (typeof bbox !== "function") {
bbox = constant(bbox === null ? [[0,0][1,1]] : bbox)
}
function force () {
var i,
tree,
node,
xi,
yi,
bbi,
nx1,
ny1,
nx2,
ny2
var cornerNodes = []
nodes.forEach(function (d, i) {
cornerNodes.push({node: d, vx: d.vx, vy: d.vy, x: d.x + (boundingBoxes[i][1][0] + boundingBoxes[i][0][0]) / 2, y: d.y + (boundingBoxes[i][0][1] + boundingBoxes[i][1][1]) / 2})
cornerNodes.push({node: d, vx: d.vx, vy: d.vy, x: d.x + boundingBoxes[i][0][0], y: d.y + boundingBoxes[i][0][1]})
cornerNodes.push({node: d, vx: d.vx, vy: d.vy, x: d.x + boundingBoxes[i][0][0], y: d.y + boundingBoxes[i][1][1]})
cornerNodes.push({node: d, vx: d.vx, vy: d.vy, x: d.x + boundingBoxes[i][1][0], y: d.y + boundingBoxes[i][0][1]})
cornerNodes.push({node: d, vx: d.vx, vy: d.vy, x: d.x + boundingBoxes[i][1][0], y: d.y + boundingBoxes[i][1][1]})
})
var cn = cornerNodes.length
for (var k = 0; k < iterations; ++k) {
tree = d3Quadtree.quadtree(cornerNodes, x, y).visitAfter(prepareCorners);
for (i = 0; i < cn; ++i) {
var nodeI = ~~(i / 5);
node = nodes[nodeI]
bbi = boundingBoxes[nodeI]
xi = node.x + node.vx
yi = node.y + node.vy
nx1 = xi + bbi[0][0]
ny1 = yi + bbi[0][1]
nx2 = xi + bbi[1][0]
ny2 = yi + bbi[1][1]
tree.visit(apply);
}
}
function apply (quad, x0, y0, x1, y1) {
var data = quad.data
if (data) {
var bWidth = bbLength(bbi, 0),
bHeight = bbLength(bbi, 1);
if (data.node.index !== nodeI) {
var dataNode = data.node
var bbj = boundingBoxes[dataNode.index],
dnx1 = dataNode.x + dataNode.vx + bbj[0][0],
dny1 = dataNode.y + dataNode.vy + bbj[0][1],
dnx2 = dataNode.x + dataNode.vx + bbj[1][0],
dny2 = dataNode.y + dataNode.vy + bbj[1][1],
dWidth = bbLength(bbj, 0),
dHeight = bbLength(bbj, 1)
if (nx1 <= dnx2 && dnx1 <= nx2 && ny1 <= dny2 && dny1 <= ny2) {
var xSize = [Math.min.apply(null, [dnx1, dnx2, nx1, nx2]), Math.max.apply(null, [dnx1, dnx2, nx1, nx2])]
var ySize = [Math.min.apply(null, [dny1, dny2, ny1, ny2]), Math.max.apply(null, [dny1, dny2, ny1, ny2])]
var xOverlap = bWidth + dWidth - (xSize[1] - xSize[0])
var yOverlap = bHeight + dHeight - (ySize[1] - ySize[0])
var xBPush = xOverlap * strength * (yOverlap / bHeight)
var yBPush = yOverlap * strength * (xOverlap / bWidth)
var xDPush = xOverlap * strength * (yOverlap / dHeight)
var yDPush = yOverlap * strength * (xOverlap / dWidth)
if ((nx1 + nx2) / 2 < (dnx1 + dnx2) / 2) {
node.vx -= xBPush
dataNode.vx += xDPush
}
else {
node.vx += xBPush
dataNode.vx -= xDPush
}
if ((ny1 + ny2) / 2 < (dny1 + dny2) / 2) {
node.vy -= yBPush
dataNode.vy += yDPush
}
else {
node.vy += yBPush
dataNode.vy -= yDPush
}
}
}
return;
}
return x0 > nx2 || x1 < nx1 || y0 > ny2 || y1 < ny1;
}
}
function prepareCorners (quad) {
if (quad.data) {
return quad.bb = boundingBoxes[quad.data.node.index]
}
quad.bb = [[0,0],[0,0]]
for (var i = 0; i < 4; ++i) {
if (quad[i] && quad[i].bb[0][0] < quad.bb[0][0]) {
quad.bb[0][0] = quad[i].bb[0][0]
}
if (quad[i] && quad[i].bb[0][1] < quad.bb[0][1]) {
quad.bb[0][1] = quad[i].bb[0][1]
}
if (quad[i] && quad[i].bb[1][0] > quad.bb[1][0]) {
quad.bb[1][0] = quad[i].bb[1][0]
}
if (quad[i] && quad[i].bb[1][1] > quad.bb[1][1]) {
quad.bb[1][1] = quad[i].bb[1][1]
}
}
}
function bbLength (bbox, heightWidth) {
return bbox[1][heightWidth] - bbox[0][heightWidth]
}
force.initialize = function (_) {
var i, n = (nodes = _).length; boundingBoxes = new Array(n);
for (i = 0; i < n; ++i) boundingBoxes[i] = bbox(nodes[i], i, nodes);
};
force.iterations = function (_) {
return arguments.length ? (iterations = +_, force) : iterations;
};
force.strength = function (_) {
return arguments.length ? (strength = +_, force) : strength;
};
force.bbox = function (_) {
return arguments.length ? (bbox = typeof _ === "function" ? _ : constant(+_), force) : bbox;
};
return force;
}
exports.bboxCollide = bboxCollide;
Object.defineProperty(exports, '__esModule', { value: true });
}));
</script>
</head>
<style>
svg {
height: 500px;
width: 500px;
border: 1px solid lightgray;
}
#viz{
position:absolute;
display:inline-block;
width:auto;
}
#left{
width:35%;
height:200px;
display:inline-block;
vertical-align:top;
}
#right{
width:60%;
display:inline-block;}
#input{
display:block;
vertical-align:top;
}
#codeInput{
display:block;
width:95%;
height:200px;
}
.tick text{
font-family:Franklin Gothic Book;
font-size:12px;
}
#viz{
position:relative;
}
</style>
<body>
<div id="cont">
<div id="left">
<div id="input">
<span>Input CSV</span>
<textarea id="inputBox" name="Text1" cols="40" rows="15"></textarea>
</div>
</div>
<div id="right">
<div id="viz">
<svg class="main">
</svg>
</div>
</div>
</div>
</body>
<footer>
<script>
var dates = [{"Date":"1/1/2019","Label":"Event 1","Category":"Holiday", 'Color': 'red'}
,{"Date":"2/15/2019","Label":"Event 2","Category":"Event", 'Color': 'red'}
,{"Date":"3/1/2019","Label":"Event 3","Category":"Event", 'Color': 'green'}
,{"Date":"3/2/2019","Label":"Event 4","Category":"Event", 'Color': 'green'}
,{"Date":"5/5/2019","Label":"Event 5","Category":"Holiday", 'Color': 'blue'}
,{"Date":"12/25/2019","Label":"Event 6","Category":"Holiday", 'Color': 'red'}
]
var csvstring = Object.keys(dates[0]).map(function(key){return key})+ "\r\n"
dates.forEach(function(rowdata){
let row = Object.keys(rowdata).map(function(key){return rowdata[key]}).join(",");
csvstring = csvstring + row + "\r\n";
});
d3.select("#inputBox")
.node()
.value = csvstring
//date parser (require typical MM/DD/YYYY Format)
var dateparse = d3.timeParse("%m/%d/%Y")
dates.forEach(function(d){
d.date = dateparse(d.Date)
})
var colorScale = d3.scaleOrdinal()
.range(["#2B92D0","#84A743"])
var width = 800;
var height = 450;
var padwidth = 60;
d3.select("svg.main")
.style("width",width)
.style("height",height)
var xScale = d3.scaleTime()
.range([padwidth,width-padwidth])
.domain(d3.extent(dates,function(d){return d.date}))
d3.select("svg.main")
.append("g")
.attr("id","xAxis")
.attr("transform","translate(0,"+height/2 + ")")
.call(d3.axisBottom(xScale))
enterUpdateEvents(dates)
function enterUpdateEvents(dates){
xScale.domain(d3.extent(dates,function(d){return d.date}))
d3.select("#xAxis")
.transition()
.call(d3.axisBottom(xScale))
var labels = d3.select("#viz").selectAll(".labels").data(dates)
labels.enter()
.append("div")
.attr("class", "labels")
.style("display","inline-block")
.style("position","absolute")
.style("padding","5px")
.style("left", function (d) {return d.x + "px"})
.style("top", function (d) {return d.y + "px"})
.each(function(d){
d3.select(this)
.append("span")
.attr("class","label_header")
.style("font-family","Franklin Gothic Medium")
.style("display","block")
.text(function(d){return d.Label})
d3.select(this)
.append("span")
.attr("class","label_body")
.style("font-family","Franklin Gothic Book")
.style("display","block")
.style("font-size",".9em")
.text(function(d){return d3.timeFormat("%B, %Y")(d.date)})
})
.merge(labels)
.transition()
.style("left", function (d) {return d.x + "px"})
.style("top", function (d) {return d.y + "px"})
.each(function(d){
d3.select(this)
.select(".label_header")
.text(function(d){return d.Label})
d3.select(this)
.select(".label_body")
.text(function(d){return d3.timeFormat("%B, %Y")(d.date)})
})
labels.exit().remove();
var lines = d3.select("#viz").select("svg").selectAll(".lines").data(dates)
lines.enter()
.append("line")
.attr("class", "lines")
.style("stroke","black")
.style("stroke-width","3px")
.style("opacity",.2)
.merge(lines)
.attr("x1",function(d){return xScale(d.date)})
.attr("x2",function(d){return xScale(d.date)})
.attr("y1",height/2)
.attr("y2",height/2)
lines.exit().remove();
d3.select("#viz")
.selectAll(".labels")
.each(function(d){
//attach DOM dimensions for force layout
var coords = d3.select(this).node().getBoundingClientRect()
d.coords = coords
d.fx = xScale(d.date)
d.y = height/2 + Math.random()*60+10
})
var circles = d3.select("#viz").select("svg").selectAll(".circles").data(dates)
circles.enter()
.append("circle")
.attr("class", "circles")
.style("stroke","black")
.attr("cx",function(d){return xScale(d.date)})
.attr("cy",height/2)
.attr("r",5)
.attr("fill",function(d){return d.Color})
.merge(circles)
.attr("cx",function(d){return xScale(d.date)})
.attr("cy",height/2)
.attr("r",5)
.attr("fill",function(d){return d.Color})
circles.exit().remove();
}
var forceX = d3.forceX(function (d) {return xScale(d.date)}).strength(1)
var forceY = d3.forceY(function (d,i) {
if ((i+1)%2){
return height/2 + 20
}
else
{return height/2 - 20}
})
.strength(0.25)
var labelpad = 35;
var collide = d3.bboxCollide(function (d,i) {
return [[d.x-labelpad, height/2-labelpad],[d.coords.width+xScale(d.date) + labelpad, height/2 + d.coords.height + labelpad]]
})
.strength(1)
.iterations(1)
var color = d3.scaleOrdinal(d3.schemeCategory20b)
var force = d3.forceSimulation(dates)
.velocityDecay(0.35)
.alphaDecay(.2)
.force("x", forceX)
.force("y", forceY)
.force("collide", collide)
updateForceLayout()
d3.select("#inputBox")
.on("change",function(){
//parse csv
dates = csvJSON(d3.select(this).node().value);
dates.forEach(function(d){
d.date = d3.timeParse("%m/%d/%Y")(d.Date)
})
enterUpdateEvents(dates)
updateForceLayout()
})
function updateForceLayout(){
console.log(dates)
force.nodes(dates)
.restart()
.alpha(1)
/*var force = d3.forceSimulation(dates)
.velocityDecay(0.35)
.alphaDecay(.2)
.force("x", forceX)
.force("y", forceY)
.force("collide", collide)
*/
for (var i = 0, n = 15; i <= n; ++i) {
force.tick()
//apply boundaries
dates.forEach(function(d){
var capWidth = Math.min(width - d.coords.width - 30,Math.max(d.x,-100))
d.x = capWidth
var capHeight
if(d.y<height/2 - 40){
capHeight = Math.max(10,Math.min(d.y,height/2 - d.coords.height - 20))
}
else{
capHeight = Math.max(Math.min(d.y,height - d.coords.height-20),height/2 + 30)
}
d.y = capHeight
})
if(i==n){
updateNetwork()
}
}
}
//.on("tick", updateNetwork);
var nodeEnter = d3.select("svg.main")
.append("g")
.selectAll("g.node")
.data(dates)
.enter()
.append("g")
.attr("class", "node")
function updateNetwork() {
d3.selectAll(".labels")
.style("background","white")
.style("border","1px solid rgba(0,0,0,.5)")
.transition()
.style("left", function (d) {
return d.x + "px"
})
.style("top", function (d,i) {
return d.y + "px"})
.style("border-bottom",function(d,i){
if(d.y<height/2){
return "4px solid " + d.Color
}
else{
return d3.select(this).style("border")
}
})
.style("border-top",function(d,i){
if(d.y<height/2){
return "1px solid rgba(0,0,0,.5)"
}
else{
console.log(colorScale(d.Category))
return "4px solid " + d.Color
}
})
//enforce boundaries
d3.selectAll(".lines")
.transition()
.attr("x1",function(d){return xScale(d.date)})
.attr("x2",function(d){return xScale(d.date)})
.attr("y1",height/2)
.attr("y2",function(d){
if(d.y<height/2){
return d.y
}
else{
return d.y
}
})
}
function csvJSON(csv){
var lines=csv.split("\n");
var result = [];
var headers=lines[0].split(",");
for(var i=1;i<lines.length;i++){
var obj = {};
var currentline=lines[i].split(",");
for(var j=0;j<headers.length;j++){
obj[headers[j]] = currentline[j];
}
result.push(obj);
}
//return result; //JavaScript object
return (result); //JSON
}
</script>
</footer>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment