I am working on this graph in d3. There is one problem, these circles are overlapping (A and F). As the data is dynamic, how can I make sure that no circles should overlap each other. Any leads would be appreciated.
Attaching the code below:
const svgWidth = 1000;
const svgHeight = 1000;
const QUAD1 = 'quadrant1';
const QUAD2 = 'quadrant2';
const QUAD3 = 'quadrant3';
const QUAD4 = 'quadrant4';
let mouseEnterTimeout, mouseOutTimeout;
const data = {
name: "root",
children: [
{
name: "A",
children: [
{name: "A1", value: 7}, {name: "A2", value: 8}, {name: "A3", value: 9}, {name: "A4", value: 10}, {name: "A5", value: 10}
]
},
{
name: "B",
children: [
{name: "B1", value: 11}, {name: "B2", value: 7}, {name: "B3", value: 8},
]
},
{
name: "C",
value: 10
},
{
name: "D",
value: 10
},
{
name: "E",
value: 10
},
{
name: "F",
children: [
{name: "F1", value: 11}, {name: "F2", value: 7}, {name: "F3", value: 8},{name: "F4", value: 5}, {name: "F5", value: 3}, {name: "F6", value: 10}
]
}
],
links: [{from: "A5", to: "B3", colorCode: 'green'}, {from: "B3", to: "A5", colorCode: 'blue'}, {from: "A3", to: "C",colorCode: 'blue'}, {from: "A2", to: "E", colorCode: 'blue'}, {from: "B1", to: "D", colorCode: 'red'}, {from: "B2", to: "B3", colorCode: 'blue'}, {from: "B1", to: "C", colorCode: 'red'}]
};
const cloneObj = item => {
if (!item) { return item; } // null, undefined values check
let types = [ Number, String, Boolean ],
result;
// normalizing primitives if someone did new String('aaa'), or new Number('444');
types.forEach(function(type) {
if (item instanceof type) {
result = type( item );
}
});
if (typeof result == "undefined") {
if (Object.prototype.toString.call( item ) === "[object Array]") {
result = [];
item.forEach(function(child, index, array) {
result[index] = cloneObj( child );
});
} else if (typeof item == "object") {
// testing that this is DOM
if (item.nodeType && typeof item.cloneNode == "function") {
result = item.cloneNode( true );
} else if (!item.prototype) { // check that this is a literal
if (item instanceof Date) {
result = new Date(item);
} else {
// it is an object literal
result = {};
for (let i in item) {
result[i] = cloneObj( item[i] );
}
}
} else {
// depending what you would like here,
// just keep the reference, or create new object
if (false && item.constructor) {
// would not advice to do that, reason? Read below
result = new item.constructor();
} else {
result = item;
}
}
} else {
result = item;
}
}
return result;
}
const findNode = (parent, name) => {
if (parent.name === name)
return parent;
if (parent.children) {
for (let child of parent.children) {
const found = findNode(child, name);
if (found) {
return found;
}
}
}
return null;
}
const findNodeAncestors = (parent, name) => {
if (parent.name === name)
return [parent];
const children = parent.children || parent._children;
if (children) {
for (let child of children) {
const found = findNodeAncestors(child, name);
//console.log('FOUND: ', found);
if (found) {
return [...found, parent];
}
}
}
return null;
}
let markerCount = 0;
const marker = (color, val) => {
val = val+(markerCount++);
svg.append("svg:defs").selectAll("marker")
.data([val])
.enter().append("svg:marker")
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 60)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.style("fill", color)
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
return "url(#" +val+ ")";
}
//Node click code
const nodeColor = '#ffefef';
const isNeighborLink = (node, link) => {
return link.from.data.name === node.name || link.to.data.name === node.name
}
const getNodeColor = (node, neighbors, count, isSelectEvent, prevOpacity) => {
if(isSelectEvent) {
if (neighbors.indexOf(node.name) < 0 && !node.selected && selectedNodeCount) {
return 0.2;
}
return 1;
} else {
if(selectedNodeCount) {
if (neighbors.indexOf(node.name) > -1 && !node.selected) {
return selectedNodeCount ? 0.2 : 1;
}
return prevOpacity;
} else {
return 1;
}
}
}
const getTextColor = (node, neighbors) => {
if (neighbors.indexOf(node.name) < 0 ) {
return 'gray';
}
return;
}
const getLinkColor = (node, link) => {
return isNeighborLink(node, link) ? 'green' : '#E5E5E5';
}
const getNeighbors = (node) => {
return data.links.reduce((neighbors, link) => {
if (link.to === node.name) {
neighbors.push(link.from)
} else if (link.from === node.name) {
neighbors.push(link.to)
}
return neighbors
}, [node.name])
}
const svg = d3.select("svg");
// This is for tooltip
const findPopoverQuad = (popoverNode, x, y, offset) => {
const viewPortH = window.innerHeight;
const viewPortW = window.innerWidth;
const nodeDimensions = popoverNode.getBoundingClientRect();
if (x >= viewPortW/2 && y >= viewPortH/2) {
return {
x: -(nodeDimensions.width-offset),
y: -(nodeDimensions.height-offset)
};
} else if (x >= viewPortW/2 && y < viewPortH/2) {
return {
x: -(nodeDimensions.width-offset),
y: offset
};
} else if( x < viewPortW/2 && y >= viewPortH/2) {
return {
x: offset,
y: -(nodeDimensions.height-offset)
};
} else {
return {
x: offset,
y: offset
}
}
}
const closePopOver = () => {
const popoverNode = d3.select('#popover').node();
popoverNode.innerHTML = "";
d3.select('#popover')
.style('visibility', 'hidden')
}
const onMouseEnter = (e,d )=> {
console.log('circle mouse enter')
e.stopPropagation();
if (mouseEnterTimeout) {
clearTimeout(mouseEnterTimeout)
}
mouseEnterTimeout = setTimeout(function() {
const popoverNode = d3.select('#popover').node();
const node = findNode(data, d.data.name);
const children = (node.children || node._children) || [];
children.forEach((c) => {
const p = document.createElement("p")
p.innerHTML = `${c.name} : ${c.value}`;
popoverNode.append(p)
});
if(children.length === 0 ) {
popoverNode.append('No children')
}
let offset = 0;
const transformValues = findPopoverQuad(popoverNode, e.x, e.y, offset);
d3.select('#popover')
.style('visibility', 'visible')
.on('mouseenter', function(e) {
console.log('popover mouse enter')
if(mouseOutTimeout) {
clearTimeout(mouseOutTimeout);
}
})
.on('mouseleave', (e) => {
closePopOver()
})
.transition()
.duration(500)
.style('transform', `
translate(${e.x+transformValues.x}px,
${e.y+transformValues.y}px)
`)
}, 100)
}
const onMouseLeave = (e,d ) => {
console.log('circle mouse leave')
if(mouseOutTimeout) {
clearTimeout(mouseOutTimeout);
}
mouseOutTimeout = setTimeout(function() {
const popoverNode = d3.select('#popover').node();
popoverNode.innerHTML = "";
d3.select('#popover')
.style('visibility', 'hidden')
}, 100)
}
const onSelectNode = (node, ref) => {
if(node) {
let isPerviousSelectedNode = node['selected'];
node['selected'] = !isPerviousSelectedNode;
// node['selected'] = node['selected'] ? false: true;
isPerviousSelectedNode ? selectedNodeCount-- : selectedNodeCount++;
if(selectedNodeCount < 0 ) {
selectedNodeCount = 0;
}
d3.select(ref.firstChild).style('fill', node.selected ? '#fb7777': nodeColor);
const neighbors = getNeighbors(node);
circleElements
.transition()
.duration(200)
.style('opacity', function(node) {
let prevOpacity = d3.select(this).style('opacity');
return getNodeColor(node.data, neighbors, selectedNodeCount, !isPerviousSelectedNode, prevOpacity);
})
linkElements
.transition()
.duration(200)
.style('stroke', link => getLinkColor(node, link))
.attr("marker-end", link => marker(getLinkColor(node, link), link.from.data.name + link.to.data.name+ '_click'))
textElements
.transition()
.duration(200)
.style('fill', node => getTextColor(node, neighbors))
}
}
const container = svg.append('g')
.attr('transform', 'translate(0,0)');
let circleElements, textElements, linkElements;
let selectedNodeCount = 0;
const onClickNode = (e, d, ref) => {
e.stopPropagation();
e.preventDefault();
const node = findNode(data, d.data.name);
if( d.depth == 2 || (node && !(node.children || node._children))) {
onSelectNode(node, ref);
}
if(node.children && !node._children) {
/*node._children = node.children;*/
node._children = cloneObj(node.children);
node.children = undefined;
node.value = 20;
updateGraph(data);
} else {
if (node._children && !node.children) {
//node.children = node._children;
node.children = cloneObj(node._children);
node._children = undefined;
node.value = undefined;
updateGraph(data);
}
}
}
const updateGraph = graphData => {
const pack = data => d3.pack()
.size([600, 600])
.padding(0)
(d3.hierarchy(data)
.sum(d => d.value * 3.5)
.sort((a, b) => b.value - a.value));
const root = pack(graphData);
const nodes = root.descendants().slice(1);
const nodeElements = container
.selectAll("g.node")
.data(nodes, d => d.data.name);
const addedNodes = nodeElements.enter()
.append("g")
.classed('node', true)
.style('cursor', 'pointer')
.on('click', function (e, d) {onClickNode(e, d, this)})
.on('mouseenter',(e, d) => onMouseEnter(e, d))
.on('mouseleave', (e, d) => onMouseLeave(e, d));
addedNodes.append('circle')
.attr('stroke', 'black')
addedNodes.append("text")
.text(d => d.data.name)
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.style('visibility', 'hidden')
.style('fill', 'black');
const mergedNodes = addedNodes.merge(nodeElements);
mergedNodes
.transition()
.duration(500)
.attr('transform', d => `translate(${d.x},${d.y})`);
circleElements = mergedNodes.select('circle')
.attr("fill", d => d.children ? "#ffe0e0" : "#ffefef")
.attr('r', d => d.value);
textElements = mergedNodes.select('text')
.attr('dy', d => d.children ? d.value + 10 : 0)
.style('visibility', 'visible');
const exitedNodes = nodeElements.exit()
exitedNodes.select('circle')
.transition()
.duration(500)
.attr('r', 1);
exitedNodes.select('text')
.remove();
exitedNodes
.transition()
.duration(750)
.remove();
const linkPath = d => {
let length = Math.hypot(d.from.x - d.to.x, d.from.y - d.to.y);
if(length == 0 ) {
return ''; // This means its a connection inside collapsed node
}
const fd = d.from.value / length;
const fx = d.from.x + (d.to.x - d.from.x) * fd;
const fy = d.from.y + (d.to.y - d.from.y) * fd;
const td = d.to.value / length;
const tx = d.to.x + (d.from.x - d.to.x) * td;
const ty = d.to.y + (d.from.y - d.to.y) * td;
return `M ${fx},${fy} L ${tx},${ty}`;
};
const links = data.links.map(link => {
let from = nodes.find(n => n.data.name === link.from);
if (!from) {
const ancestors = findNodeAncestors(data, link.from);
for (let index = 1; !from && index < ancestors.length -1; index++) {
from = nodes.find(n => n.data.name === ancestors[index].name)
}
}
let to = nodes.find(n => n.data.name === link.to);
if (!to) {
const ancestors = findNodeAncestors(data, link.to);
for (let index = 1; !to && index < ancestors.length -1; index++) {
to = nodes.find(n => n.data.name === ancestors[index].name)
}
}
let colorCode = link.colorCode;
return {from, to, colorCode};
});
const linkElem = container.selectAll('path.link')
.data(links.filter(l => l.from && l.to));
linkElements = linkElem.enter()
.append('path')
.classed('link', true)
.attr("stroke", function(d){
return d.colorCode;
})
.attr("marker-end", function(d){
return marker(d.colorCode, d.from.data.name+d.to.data.name);
})
linkElements.merge(linkElem)
.style('visibility', 'hidden')
.transition()
.delay(750)
.attr('d', linkPath)
.style('visibility', 'visible')
}
updateGraph(data);
text {
font-family: "Ubuntu";
font-size: 12px;
}
.link {
fill: none;
}
.popover {
position: absolute;
top: 0; left: 0;
visibility: hidden;
width :150px;
max-height:300px;
background: white;
border: 1px solid blue;
padding: 10px;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
</head>
<body>
<svg width="1000" height="1000">
<defs>
<marker id="arrowhead-to" markerWidth="10" markerHeight="7"
refX="10" refY="3.5" orient="auto">
<polygon fill="blue" points="0 0, 10 3.5, 0 7" />
</marker>
<marker id="arrowhead-from" markerWidth="10" markerHeight="7"
refX="0" refY="3.5" orient="auto">
<polygon fill="blue" points="10 0, 0 3.5, 10 7" />
</marker>
</defs>
</svg>
<div id='popover' class='popover'>
</div>
</body>
</html>