D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
mforando
Full window
Github gist
Label Div Collision - Stars
Built with
blockbuilder.org
<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>
https://d3js.org/d3.v4.min.js