Built with blockbuilder.org
Last active
February 19, 2020 14:41
-
-
Save mforando/f44ceb116a570be8ce2cfa42e2744b26 to your computer and use it in GitHub Desktop.
Label Div Collision - Stars
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
<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