I have this circle pack layout using D3:
I have assigned mouseover and mouseout event on the circles in the screenshot, but am not able to figure out why mouseover event is being triggered multiple times for inner circle (for example A1, B1, etc..) ?
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
}
],
links: [{from: "A5", to: "B3"}, {from: "A3", to: "C"}, {from: "A2", to: "E"}, {from: "B1", to: "D"}, {from: "B2", to: "B3"}, {from: "B1", to: "C"}]
};
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;
}
const svg = d3.select("svg");
// This is for tooltip
const Tooltip = d3.select("body").append("div")
.attr("class", "tooltip-menu")
.style("opacity", 0);
const onMouseover = (e,d )=> {
console.log('d -->>', d);
e.stopPropagation();
Tooltip.style("opacity", 1);
let html = `<span>
Hi
</span>`;
Tooltip.html(html)
.style("left", (e.pageX + 10) + "px")
.style("top", (e.pageY - 15) + "px");
}
const onMouseout = (e,d ) => {
Tooltip.style("opacity", 0)
}
const container = svg.append('g')
.attr('transform', 'translate(0,0)')
const onClickNode = (e, d) => {
e.stopPropagation();
e.preventDefault();
const node = findNode(data, d.data.name);
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', (e, d) => onClickNode(e, d))
.on('mouseover',(e, d) => onMouseover(e, d))
.on('mouseout', (e, d) => onMouseout(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})`);
mergedNodes.select('circle')
.attr("fill", d => d.children ? "#ffe0e0" : "#ffefef")
.transition()
.duration(1000)
.attr('r', d => d.value)
mergedNodes.select('text')
.attr('dy', d => d.children ? d.value + 10 : 0)
.transition()
.delay(1000)
.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)
}
}
return {from, to};
});
const linkElements = container.selectAll('path.link')
.data(links.filter(l => l.from && l.to));
const addedLinks = linkElements.enter()
.append('path')
.classed('link', true)
.attr('marker-end', 'url(#arrowhead-to)')
.attr('marker-start', 'url(#arrowhead-from)');
addedLinks.merge(linkElements)
.style('visibility', 'hidden')
.transition()
.delay(750)
.attr('d', linkPath)
.style('visibility', 'visible')
linkElements.exit().remove();
}
updateGraph(data);
text {
font-family: "Ubuntu";
font-size: 12px;
}
.link {
stroke: blue;
fill: none;
}
div.tooltip-menu {
position: absolute;
text-align: center;
padding: .5rem;
background: #FFFFFF;
color: #313639;
border: 1px solid #313639;
border-radius: 8px;
pointer-events: none;
font-size: 1.3rem;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
<svg width="600" height="600">
<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>
Each inner circle is a <g> with a <circle> and <text> within it. Even though you attach the mouse events to the <g>, the mouseover event fires as you pass over the <text> element in the middle of the circle, causing the tooltip to move as your mouse around.
You can add .attr('pointer-events', 'none') to the <text> elements to prevent this:
addedNodes.append("text")
.text(d => d.data.name)
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.attr('pointer-events', 'none') // <---- HERE
.style('visibility', 'hidden')
.style('fill', 'black');
Example below:
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
}
],
links: [{from: "A5", to: "B3"}, {from: "A3", to: "C"}, {from: "A2", to: "E"}, {from: "B1", to: "D"}, {from: "B2", to: "B3"}, {from: "B1", to: "C"}]
};
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;
}
const svg = d3.select("svg");
// This is for tooltip
const Tooltip = d3.select("body").append("div")
.attr("class", "tooltip-menu")
.style("opacity", 0);
const onMouseover = (e,d )=> {
console.log('d -->>', d);
e.stopPropagation();
Tooltip.style("opacity", 1);
let html = `<span>
Hi ${d.data.name}
</span>`;
Tooltip.html(html)
.style("left", (e.pageX + 10) + "px")
.style("top", (e.pageY - 15) + "px");
}
const onMouseout = (e,d ) => {
Tooltip.style("opacity", 0)
}
const container = svg.append('g')
.attr('transform', 'translate(0,0)')
const onClickNode = (e, d) => {
e.stopPropagation();
e.preventDefault();
const node = findNode(data, d.data.name);
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', (e, d) => onClickNode(e, d))
.on('mouseover',(e, d) => onMouseover(e, d))
.on('mouseout', (e, d) => onMouseout(e, d));
addedNodes.append('circle')
.attr('stroke', 'black')
addedNodes.append("text")
.text(d => d.data.name)
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.attr('pointer-events', 'none')
.style('visibility', 'hidden')
.style('fill', 'black');
const mergedNodes = addedNodes.merge(nodeElements);
mergedNodes
.transition()
.duration(500)
.attr('transform', d => `translate(${d.x},${d.y})`);
mergedNodes.select('circle')
.attr("fill", d => d.children ? "#ffe0e0" : "#ffefef")
.transition()
.duration(1000)
.attr('r', d => d.value)
mergedNodes.select('text')
.attr('dy', d => d.children ? d.value + 10 : 0)
.transition()
.delay(1000)
.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)
}
}
return {from, to};
});
const linkElements = container.selectAll('path.link')
.data(links.filter(l => l.from && l.to));
const addedLinks = linkElements.enter()
.append('path')
.classed('link', true)
.attr('marker-end', 'url(#arrowhead-to)')
.attr('marker-start', 'url(#arrowhead-from)');
addedLinks.merge(linkElements)
.style('visibility', 'hidden')
.transition()
.delay(750)
.attr('d', linkPath)
.style('visibility', 'visible')
linkElements.exit().remove();
}
updateGraph(data);
text {
font-family: "Ubuntu";
font-size: 12px;
}
.link {
stroke: blue;
fill: none;
}
div.tooltip-menu {
position: absolute;
text-align: center;
padding: .5rem;
background: #FFFFFF;
color: #313639;
border: 1px solid #313639;
border-radius: 8px;
pointer-events: none;
font-size: 1.3rem;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
<svg width="600" height="600">
<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>
Related
A follow-on question / issue to
Programmatically open nested, collapsed (hidden) node in d3.js v4
updated for d3.js v6. The issue is the loading of external JSON data in the d3 collapsible menu visualization, and the programmatic access of nested (collapsed, hidden) nodes.
It appears that "treeData", which is the loaded Object, is not being delivered.
Uncaught ReferenceError: treeData is not defined
JSFiddle: https://jsfiddle.net/vstuart/kant09hm/6/
JSFiddle (updated with answer): https://jsfiddle.net/vstuart/kant09hm/9/
ontology_for_d3_test.json
gist: https://gist.github.com/victoriastuart/abbcf355bf1590be02f6dec297be2706
{ "name": "Root",
"children": [
{ "name": "Culture",
"children": [
{ "name": "Entertainment" },
{ "name": "LGBT" }
]
},
{ "name": "Nature",
"id": "nature",
"children": [
{ "name": "Earth",
"id": "earth",
"children": [
{ "name": "Environment" },
{ "name": "Geography" },
{ "name": "Geology" },
{ "name": "Geopolitical" },
{ "name": "Geopolitical - Countries" },
{ "name": "Geopolitical - Countries - Canada" },
{ "name": "Geopolitical - Countries - United States" },
{ "name": "Nature" },
{ "name": "Regions" }
]
},
{ "name": "Cosmos" },
{ "name": "Outer space" }
]
},
{ "name": "Humanities",
"children": [
{ "name": "History" },
{ "name": "Philosophy" },
{ "name": "Philosophy - Theology" }
]
},
{ "name": "Miscellaneous",
"children": [
{ "name": "Wikipedia",
"url": "https://wikipedia.com" },
{ "name": "Example.com",
"url": "https://example.com" }
]
},
{ "name": "Science",
"children": [
{ "name": "Biology" },
{ "name": "Health" },
{ "name": "Health - Medicine" },
{ "name": "Sociology" }
]
},
{ "name": "Technology",
"children": [
{ "name": "Computers" },
{ "name": "Computers - Hardware" },
{ "name": "Computers - Software" },
{ "name": "Computing" },
{ "name": "Computing - Programming" },
{ "name": "Internet" },
{ "name": "Space" },
{ "name": "Transportation" }
]
},
{ "name": "Society",
"children": [
{ "name": "Business" },
{ "name": "Economics" },
{ "name": "Economics - Business" },
{ "name": "Economics - Capitalism" },
{ "name": "Economics - Commerce" },
{ "name": "Economics - Finance" },
{ "name": "Politics" },
{ "name": "Public services" }
]
}
]
}
index.html [standalone working copy of JSFiddle; edit: updated with answer]
<!DOCTYPE html>
<html lang="en-US" xmlns:xlink="http://www.w3.org/1999/xlink">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<style>
.node {
cursor: pointer;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.node text {
font: 12px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
#includedContent {
position: static !important;
display: inline-block;
}
#d3_object {
width: 75%;
margin: 0.5rem 0.5rem 1rem 0.25rem;
}
</style>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<!-- <script src="https://d3js.org/d3.v4.min.js"></script> -->
<!-- <script src="https://d3js.org/d3.v5.min.js"></script> -->
<script src="https://d3js.org/d3.v6.min.js"></script>
</head>
<body>
<div id="d3_object">
<object>
<p>apple</p>
<div id="includedContent"></div>
<p>banana</p>
</object>
</div>
<script type="text/javascript">
// Set the dimensions and margins of the diagram
var margin = {top: 20, right: 90, bottom: 30, left: 90},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// ----------------------------------------
// PAN, ZOOM:
// https://www.d3-graph-gallery.com/graph/interactivity_zoom.html
// var svg = d3.select("body").append("svg")
var svg = d3.select("#includedContent").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
// d3.js v4, v5:
// .call(d3.zoom().on("zoom", function () {
// svg.attr("transform", d3.event.transform)
// d3.js v6:
.call(d3.zoom().on("zoom", function (event) {
svg.attr("transform", event.transform)
}))
.append("g")
.attr("transform", "translate("
+ margin.left + "," + margin.top + ")");
// ----------------------------------------
var i = 0,
duration = 250,
root;
// declares a tree layout and assigns the size
var treemap = d3.tree().size([height, width]);
// ----------------------------------------
// LOAD THE EXTERNAL DATA:
//d3.json("https://gist.githubusercontent.com/mbostock/4339083/raw/9585d220bef18a0925922f4d384265ef767566f5/flare.json", function(error, treeData) {
// ----------------------------------------
// d3.js v4 [ https://d3js.org/d3.v4.min.js ]
/*
d3.json("https://gist.githubusercontent.com/victoriastuart/abbcf355bf1590be02f6dec297be2706/raw/2418e5f6b7626b3c5842665a51b7d0d27f74e909/ontology_for_d3_test.json", function(error, treeData) {
if (error) throw error;
// Assigns parent, children, height, depth
root = d3.hierarchy(treeData, function(d) { return d.children; });
root.x0 = height / 2;
root.y0 = 0;
// Collapse after the second level
root.children.forEach(collapse);
update(root);
});
*/
// ----------------------------------------
// ----------------------------------------
// d3.js v5 https://d3js.org/d3.v5.min.js
// https://gist.github.com/d3noob/1a96af738c89b88723eb63456beb6510
// d3.js v6 https://d3js.org/d3.v5.min.js
// https://gist.github.com/d3noob/9de0768412ac2ce5dbec430bb1370efe
// https://stackoverflow.com/questions/49768165/code-within-d3-json-callback-is-not-executed
// https://www.tutorialsteacher.com/d3js/loading-data-from-file-in-d3js
// https://stackoverflow.com/questions/47664292/d3-json-method-doesnt-return-my-data-array
// ----------------------------------------
// LOAD EXTERNAL JSON DATA FILE (via PROMISE):
// https://stackoverflow.com/questions/49768165/code-within-d3-json-callback-is-not-executed
// https://stackoverflow.com/questions/49534470/d3-js-v5-promise-all-replaced-d3-queue
// https://www.roelpeters.be/explaining-promises-in-d3-js-the-what-and-the-why/
//
// https://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call
// This callback function should return the "treeData" Object:
d3.json("https://gist.githubusercontent.com/victoriastuart/abbcf355bf1590be02f6dec297be2706/raw/2418e5f6b7626b3c5842665a51b7d0d27f74e909/ontology_for_d3_test.json")
.then(function(treeData) {
console.log('[d3.js] treeData:', treeData, '| type:', typeof(treeData), '| length:', treeData['children'].length)
for (let i = 0; i < treeData['children'].length; i++) {
console.log('node:', treeData['children'][i].name);
if ( treeData['children'][i].id !== undefined ) {
console.log(' id:', treeData['children'][i].id);
}
}
// ASSIGN PARENT, CHILDREN, HEIGHT, DEPTH:
root = d3.hierarchy(treeData, function(d) { return d.children; });
root.x0 = height / 2;
root.y0 = 0;
// COLLAPSE AFTER THE SECOND LEVEL:
root.children.forEach(collapse);
update(root);
// ----------------------------------------
// Per answers at
// https://stackoverflow.com/questions/67527258/programmatically-open-nested-collapsed-hidden-node-in-d3-js-v4/67530786?noredirect=1#comment119390942_67530786
// https://jsfiddle.net/mrovinsky/ujwsd7qz/
// https://stackoverflow.com/questions/67549992/accessing-promised-data-in-d3-js-v6-programmatically-opening-nested-collapsed-n
const findNodeAncestors = (root, name) => {
if (root.name === name) {
return [name];
}
if (root.children)
for (let i = 0; i < root.children.length; i++) {
const chain = findNodeAncestors(root.children[i], name);
if (chain) {
chain.push(root.name);
return chain;
}
}
return null;
};
const chain = findNodeAncestors(treeData, 'Earth');
if (chain) {
console.log('[d3.js] chain:', chain)
for (let i = chain.length - 1; i >= 0; i--) {
const node = d3.select(`.node[node-name="${chain[i]}"]`);
const nodeData = node.datum();
if (!nodeData.children && nodeData.data.children) {
node.node().dispatchEvent(new Event('click'));
}
}
}
else {
console.log('[d3.js] "chain" is either "null" or "undefined"')
}
// ----------------------------------------
})
// IF (ERROR) THROW ERROR:
.catch(function(error) {
console.log('[d3.js] JSON callback function error')
if (error) throw error;
});
// ----------------------------------------
// COLLAPSE THE NODE AND ALL IT'S CHILDREN:
function collapse(d) {
if(d.children) {
d._children = d.children
d._children.forEach(collapse)
d.children = null
}
}
function update(source) {
// ASSIGNS THE X AND Y POSITION FOR THE NODES:
var treeData = treemap(root);
// COMPUTE THE NEW TREE LAYOUT:
var nodes = treeData.descendants(),
links = treeData.descendants().slice(1);
// NORMALIZE FOR FIXED-DEPTH:
nodes.forEach(function(d){ d.y = d.depth * 180});
// *************** NODES SECTION ***************
// Update the nodes...
var node = svg.selectAll('g.node')
.data(nodes, function(d) {return d.id || (d.id = ++i); });
// ENTER ANY NEW MODES AT THE PARENT'S PREVIOUS POSITION:
var nodeEnter = node.enter().append('g')
.attr('class', 'node')
// --------------------------------------------
// Per answer at
// https://stackoverflow.com/questions/67480339/programmatically-opening-d3-js-v4-collapsible-tree-nodes
.attr('node-name', d => d.data.name)
// --------------------------------------------
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
// ADD CIRCLE FOR THE NODES:
nodeEnter.append('circle')
.attr('class', 'node')
.attr('r', 1e-6)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
});
// ADD LABELS FOR THE NODES:
nodeEnter.append('text')
.attr("dy", ".35em")
.attr("x", function(d) {
return d.children || d._children ? -13 : 13;
})
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) { return d.data.name; });
// UPDATE:
var nodeUpdate = nodeEnter.merge(node);
// TRANSITION TO THE PROPER POSITION FOR THE NODE:
nodeUpdate.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// UPDATE THE NODE ATTRIBUTES AND STYLE:
nodeUpdate.select('circle.node')
.attr('r', 10)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";
})
.attr('cursor', 'pointer');
// Remove any exiting nodes
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
// ON EXIT REDUCE THE NODE CIRCLES SIZE TO 0:
nodeExit.select('circle')
.attr('r', 1e-6);
// ON EXIT REDUCE THE OPACITY OF TEXT LABELS:
nodeExit.select('text')
.style('fill-opacity', 1e-6);
// *************** LINKS SECTION ***************
// UPDATE THE LINKS:
var link = svg.selectAll('path.link')
.data(links, function(d) { return d.id; });
// ENTER ANY NEW LINKS AT THE PARENT'S PREVIOUS POSITION:
var linkEnter = link.enter().insert('path', "g")
.attr("class", "link")
.attr('d', function(d){
var o = {x: source.x0, y: source.y0}
return diagonal(o, o)
});
// UPDATE:
var linkUpdate = linkEnter.merge(link);
// TRANSITION BACK TO THE PARENT ELEMENT POSITION:
linkUpdate.transition()
.duration(duration)
.attr('d', function(d){ return diagonal(d, d.parent) });
// REMOVE ANY EXITING LINKS:
var linkExit = link.exit().transition()
.duration(duration)
.attr('d', function(d) {
var o = {x: source.x, y: source.y}
return diagonal(o, o)
})
.remove();
// STORE THE OLD POSITIONS FOR TRANSITION:
nodes.forEach(function(d){
d.x0 = d.x;
d.y0 = d.y;
});
// CREATE A CURVED (DIAGONAL) PATH FROM PARENT TO THE CHILD NODES:
function diagonal(s, d) {
path = `M ${s.y} ${s.x}
C ${(s.y + d.y) / 2} ${s.x},
${(s.y + d.y) / 2} ${d.x},
${d.y} ${d.x}`
return path
}
// ----------------------------------------
// TOGGLE CHILDREN ON CLICK:
// function click(d) {
// ***** New in d3.js v6: *****
function click(event, d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else if (d._children) {
d.children = d._children;
d._children = null;
} else {
// This was a leaf node, so redirect.
console.log('d:', d)
console.log('d.data:', d.data)
console.log('d.name:', d.name)
console.log('d.data.name:', d.data.name)
console.log('urlMap[d.data.name]:', urlMap[d.data.name])
window.location = d.data.url;
// window.open("https://www.example.com", "_self");
}
update(d);
}
// ----------------------------------------
}
// ----------------------------------------
// Per answer at
// https://stackoverflow.com/questions/67480339/programmatically-opening-d3-js-v4-collapsible-tree-nodes
///*
setTimeout(() => {
//const node = d3.select('.node[node-name="Earth"]').node();
//const node = d3.select('.node[node-name="Nature"]').node();
const node = d3.select('.node[node-name="Society"]').node();
console.log('[setTimeout()] NODE: ', node);
node.dispatchEvent(new Event('click'));
}, 2500);
//*/
// ----------------------------------------
// ----------------------------------------
// Per answer at
// https://stackoverflow.com/questions/67527258/programmatically-open-nested-collapsed-hidden-node-in-d3-js-v4/67530786?noredirect=1#comment119390942_67530786
// https://jsfiddle.net/mrovinsky/ujwsd7qz/
const findNodeAncestors = (root, name) => {
if (root.name === name) {
return [name];
}
if (root.children)
for (let i = 0; i < root.children.length; i++) {
const chain = findNodeAncestors(root.children[i], name);
if (chain) {
chain.push(root.name);
return chain;
}
}
return null;
};
const chain = findNodeAncestors(treeData, 'Earth');
// Console: "Uncaught ReferenceError: treeData is not defined"
for (let i = chain.length - 1; i >= 0; i--) {
const node = d3.select(`.node[node-name="${chain[i]}"]`);
const nodeData = node.datum();
if (!nodeData.children && nodeData.data.children) {
node.node().dispatchEvent(new Event('click'));
}
}
// ----------------------------------------
</script>
</body>
</html>
The treeData variable can be used only in the scope of the function where it's defined as an argument:
d3.json("https://...json")
.then(function(treeData) {
// Scope of the function, treeData can be used here
});
// Out of scope, treeData is not defined
The solution is to move the block that starts with const findNodeAncestors... (~25 lines) inside the function body (right after the last occurence of update with a closing brace):
update(d);
}
// ----------------------------------------
// HERE
}
I have constructed a tree graph using html5, css3 but the nodes are static. Now I wanna make that graph dynamic. Here dynamic means suppose the node count increases and there are multiple children, now the graph will be generated with the new nodes and children.
This is just an example using vanilla JavaScript. I've taken a lot of inspiration from d3, including how to store the data as a self-referencing tree of nodes and how to traverse the tree breadth-first.
I've tried to comment the code as well as possible, I hope this gives you some inspiration. I'd try to improve the positioning of the labels a bit, and/or increase the margins so they're better visible.
const data = [{
id: 0,
label: "Command Sequence Starting",
},
{
id: 1,
parents: [0],
label: "W_SCMadl_refresh",
status: "done",
},
{
id: 2,
parents: [1],
label: "W_adl_photo",
status: "done",
},
{
id: 3,
parents: [2],
label: "W_adl_collect",
status: "done",
},
{
id: 4,
parents: [3],
label: "W_adl_collect_cr",
status: "done",
},
{
id: 5,
parents: [4],
label: "W_adl_sync",
status: "aborted",
},
{
id: 6,
parents: [5],
label: "W_adl_attach",
status: "waiting",
},
{
id: 7,
parents: [6],
label: "W_adl_attach",
status: "waiting",
},
{
id: 8,
parents: [7],
label: "W_adl_publish",
status: "waiting",
},
{
id: 9,
parents: [8],
label: "W_adl_ds_ws",
status: "waiting",
},
{
id: 10,
parents: [9],
label: "W64_Shared_Preq_mkdir",
status: "waiting",
},
{
id: 11,
parents: [10, 12],
label: "W64_mkCopyPreq",
status: "waiting",
},
{
id: 12,
parents: [0],
label: "WIN64_MCCMon",
status: "done",
},
];
// Make the data array a self-referencing tree, where each node has pointers to their
// parents and their children nodes
data
.filter(d => d.parents !== undefined)
.forEach(d => {
d.parents = data.filter(p => d.parents.includes(p.id));
d.parents.forEach(p => {
if (p.children === undefined) {
p.children = [];
}
p.children.push(d);
});
});
const root = data.find(d => d.parents === undefined);
// Breadth first traversal of the tree, excuting `fn` for every node
const forEach = (root, fn) => {
const stack = [root];
while (stack.length) {
const current = stack.shift();
if (current.children) {
stack.push(...current.children);
}
fn(current);
}
};
const svg = document.querySelector(".mv-sequence svg");
const margin = {
top: 20,
bottom: 20,
right: 20,
left: 20,
};
const width = +svg.getAttribute("width") - margin.left - margin.right;
const stepHeight = 40;
const namespace = "http://www.w3.org/2000/svg";
const gContainer = document.createElementNS(namespace, "g");
gContainer.setAttribute("transform", `translate(${margin.left},${margin.top})`);
svg.appendChild(gContainer);
const linksContainer = document.createElementNS(namespace, "g");
gContainer.appendChild(linksContainer);
const nodesContainer = document.createElementNS(namespace, "g");
gContainer.appendChild(nodesContainer);
// Give node a level. First complete this loop, then start drawing, because we want to
// be robust against not all parents having a level yet
forEach(
root,
d => {
if (d === root) {
d.level = 0;
return;
}
d.level = Math.max(...d.parents.map(p => p.level)) + 1;
}
);
forEach(
root,
d => {
// Position the node based on the number of siblings.
const siblings = data.filter(n => n.level === d.level);
// If the node is an only child. The root should be in the centre,
// any other node should be in the average of it's parents
if (siblings.length === 1) {
if (d.parents === undefined) {
d.x = width / 2;
} else {
d.x = d.parents.map(p => p.x).reduce((s, v) => s + v, 0) / d.parents.length;
}
return;
}
// Otherwise, divide the space evenly for all sibling nodes
const siblingIndex = siblings.indexOf(d);
const stepWidth = width / (siblings.length - 1);
if (siblings.length % 2 === 0) {
// Even number of siblings
d.x = stepWidth * siblingIndex;
} else {
// Odd number of siblings, the center one must be in the middle
d.x = width / 2 + stepWidth * (siblingIndex - (siblings.length - 1) / 2);
}
}
);
forEach(
root,
d => {
// Append a circle and `text` for all new nodes
d.y = d.level * stepHeight;
const nodeContainer = document.createElementNS(namespace, "g");
nodeContainer.setAttribute("transform", `translate(${d.x}, ${d.y})`);
nodeContainer.classList.add("mv-command", d.status);
nodesContainer.appendChild(nodeContainer);
const circle = document.createElementNS(namespace, "circle");
circle.setAttribute("r", stepHeight / 4);
nodeContainer.appendChild(circle);
const label = document.createElementNS(namespace, "text");
label.setAttribute("dx", stepHeight / 4 + 5);
label.textContent = d.label;
nodeContainer.appendChild(label);
// Append a link from every parent to this node
(d.parents || []).forEach(p => {
const link = document.createElementNS(namespace, "path");
let path = `M${p.x} ${p.y}`;
let dx = d.x - p.x;
let dy = d.y - p.y;
if (dy > stepHeight) {
// Move down to the level of the child node
path += `v${dy - stepHeight}`;
dy = stepHeight;
}
path += `s0 ${dy / 2}, ${dx / 2} ${dy / 2}`;
path += `s${dx / 2} 0, ${dx / 2} ${dy / 2}`;
link.setAttribute("d", path)
linksContainer.appendChild(link);
})
}
);
// Finally, set the height to fit perfectly
svg.setAttribute("height", Math.max(...data.map(d => d.level)) * stepHeight + margin.top + margin.bottom);
.mv-command.done {
fill: #477738;
}
.mv-command.aborted {
fill: #844138;
}
.mv-command.waiting {
fill: #808080;
}
.mv-command.disabled {
fill: #80808080;
}
.mv-command.running {
fill: #005686;
animation: mymove 2s infinite;
}
.mv-command>text {
dominant-baseline: middle;
font-size: 12px;
}
path {
fill: none;
stroke: darkgreen;
stroke-width: 3px;
}
<div class="mv-sequence">
<svg width="200"></svg>
</div>
I'm building a bubble chart using D3 js (v5), to show some results of a research.
Basically I have a JSON with the total as the root and the children as each result.
One of the results must be highlighted and be positioned at the top center of the circle.
I managed to show this result there, but the other bubbles are overlapping (probably because I changed the position provided by the D3 hierarchy algorithm).
Is it possible to programmatically fix the position of only one bubble (the blue bubble in my example) and reposition the other bubbles, in order to not let them overlap each other?
JFiddle
const data = {
name: 'Total',
size: 1999999,
children: [{
name: 'Result A',
size: 69936,
},
{
name: 'Result b',
size: 45000,
},
{
name: 'Result C',
size: 250000,
},
{
name: 'Result D',
size: 426791,
},
{
name: 'Result E',
size: 56000,
},
{
name: 'Result F',
size: 61050,
},
{
name: 'Result G',
size: 30000,
},
],
};
// Fix this bubble at the top
const FIXED_BUBBLE_NAME = 'Result b';
const GREEN = '#90E0C2';
const BLUE = '#73A1FC';
const GRAPH_DIMENSIONS = {
WIDTH: 234,
HEIGHT: 234,
PADDING: 10,
};
const buildDataTree = () => {
const packLayout = d3
.pack()
.size([
GRAPH_DIMENSIONS.WIDTH,
GRAPH_DIMENSIONS.HEIGHT,
])
.padding(GRAPH_DIMENSIONS.PADDING);
const rootNode = d3
.hierarchy(data)
.sum((d) => d.size)
.sort((a, b) => {
return b.value - a.value;
})
return packLayout(rootNode);
};
const getSvgRoot = () => {
return d3
.select('#graph-container')
.append('svg')
.attr('id', 'graph-container-svg')
.attr('width', GRAPH_DIMENSIONS.WIDTH + GRAPH_DIMENSIONS.PADDING)
.attr('height', GRAPH_DIMENSIONS.HEIGHT + GRAPH_DIMENSIONS.PADDING)
.style('overflow', 'visible');
};
const rootNodeDataTree = buildDataTree();
const svgRoot = getSvgRoot();
const node = svgRoot
.selectAll('g')
.data(
d3
.nest()
.key((d) => d.id)
.entries(rootNodeDataTree.descendants()),
)
.join('g')
.selectAll('g')
.data(d => d.values)
.join('g');
node
.append('circle')
.attr('r', (d) => d.r)
.attr('cx', (d) => {
// if it is the selected bubble it must be at the center
if (d.data.name === FIXED_BUBBLE_NAME) {
return GRAPH_DIMENSIONS.WIDTH / 2 + d.r / 2;
}
return d.x + GRAPH_DIMENSIONS.PADDING;
})
.attr('cy', (d) => {
// if it is the selected bubble it must be at the center
if (d.data.name === FIXED_BUBBLE_NAME) {
return d.r + GRAPH_DIMENSIONS.PADDING * 2;
}
return d.y + GRAPH_DIMENSIONS.PADDING;
})
.attr('cursor', 'pointer')
.attr('stroke', (d) => (d.children ? GREEN : ''))
.attr('stroke-width', (d) => (d.children ? 2 : 0))
.attr('fill-opacity', (d) => (d.children ? 0.24 : 1))
.attr('fill', (d) => {
if (d.data.name === FIXED_BUBBLE_NAME) {
return BLUE;
} else {
return GREEN;
}
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<div id="graph-container">
</div>
Here is a fiddle with my suggestion: https://jsfiddle.net/9r8kt1e0/1/
The force simulation code is:
const simulation = d3
.forceSimulation(allNodes.filter( (d) => d.depth !== 0))
.force("collide",d3.forceCollide(d => { return d.r;}))
.force("charge", d3.forceManyBody().strength(0))
.force("center", d3.forceCenter(GRAPH_DIMENSIONS.WIDTH/2+10, GRAPH_DIMENSIONS.HEIGHT/2))
.force("bound", (alpha) => {
for (let i = 0; i < allNodes.length; ++i) {
n = allNodes[i];
n.x = pythag(n.r, n.y, n.x);
n.y = pythag(n.r, n.x, n.y);
}
})
.stop();
for (var i = 0; i < 100; ++i) {
simulation.tick();
}
Notice that we are passing a custom force called bound, this one should take care of keeping the nodes inside the parent circle.
That solves the overlapping, you would have to play a little with the forces according to your needs.
As per the documentation, a force is a function that modifies the nodes' position, you can have multiple forces, so hopefully, playing a little bit with the forces should solve your problem.
I hope it helps.
I am trying to render multiple D3 force layouts on a page. I managed to initially render the layouts, but only the last graphs' nodes can be dragged a few seconds after render.
I had the same problem a while ago. The problem came up because d3.drag() and .tick() weren't pointing to the right d3.forceSimulation. They were pointing to another d3.forceSimulation I mistakenly declared in the global namespace.
This time around I have multiple d3.forceSimulation again, but that's because I do want to render multiple force layouts.
I tried to map over each force layout's dataset and call d3.forceSimulation and tick() with each set.
Now, should tick() be called only once for all the data? Or for each layout seperately? It seems as if tick keeps working for the last graph only. So how can tick be set for all force.simulation?
A live example can be found be here
///////////////////////////////////////////////////////////
/////// Functions and variables
///////////////////////////////////////////////////////////
var FORCE = (function(nsp) {
var
width = 1080,
height = 250,
color = d3.scaleOrdinal(d3.schemeCategory10),
initForce = (nodes, links) => {
nsp.force = d3.forceSimulation(nodes)
.force("charge", d3.forceManyBody().strength(-200))
.force("link", d3.forceLink(links).distance(70))
.force("center", d3.forceCenter().x(nsp.width / 5).y(nsp.height / 2))
.force("collide", d3.forceCollide([5]).iterations([5]));
},
enterNode = (selection) => {
var circle = selection.select('circle')
.attr("r", 25)
.style("fill", 'tomato')
.style("stroke", "bisque")
.style("stroke-width", "3px")
selection.select('text')
.style("fill", "honeydew")
.style("font-weight", "600")
.style("text-transform", "uppercase")
.style("text-anchor", "middle")
.style("alignment-baseline", "middle")
.style("font-size", "10px")
.style("font-family", "cursive")
},
updateNode = (selection) => {
selection
.attr("transform", (d) => "translate(" + d.x + "," + d.y + ")")
.attr("cx", function(d) {
return d.x = Math.max(30, Math.min(width - 30, d.x));
})
.attr("cy", function(d) {
return d.y = Math.max(30, Math.min(height - 30, d.y));
})
},
enterLink = (selection) => {
selection
.attr("stroke-width", 3)
.attr("stroke", "bisque")
},
updateLink = (selection) => {
selection
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
},
updateGraph = (selection) => {
selection.selectAll('.node')
.call(updateNode)
selection.selectAll('.link')
.call(updateLink);
},
dragStarted = (d) => {
if (!d3.event.active) nsp.force.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y
},
dragging = (d) => {
d.fx = d3.event.x;
d.fy = d3.event.y
},
dragEnded = (d) => {
if (!d3.event.active) nsp.force.alphaTarget(0);
d.fx = null;
d.fy = null
},
drag = () => d3.selectAll('g.node')
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragging)
.on("end", dragEnded)
),
tick = (that) => {
that.d3Graph = d3.select(ReactDOM.findDOMNode(that));
nsp.force.on('tick', () => {
that.d3Graph.call(updateGraph)
});
};
nsp.width = width;
nsp.height = height;
nsp.enterNode = enterNode;
nsp.updateNode = updateNode;
nsp.enterLink = enterLink;
nsp.updateLink = updateLink;
nsp.updateGraph = updateGraph;
nsp.initForce = initForce;
nsp.dragStarted = dragStarted;
nsp.dragging = dragging;
nsp.dragEnded = dragEnded;
nsp.drag = drag;
nsp.tick = tick;
return nsp
})(FORCE || {})
////////////////////////////////////////////////////////////////////////////
/////// class App is the parent component of Link and Node
////////////////////////////////////////////////////////////////////////////
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
data: [{
name: "one",
id: 65,
nodes: [{
"name": "fruit",
"id": 0
},
{
"name": "apple",
"id": 1
},
{
"name": "orange",
"id": 2
},
{
"name": "banana",
"id": 3
}
],
links: [{
"source": 0,
"target": 1,
"lineID": 1
},
{
"source": 0,
"target": 2,
"lineID": 2
},
{
"source": 3,
"target": 0,
"lineID": 3
}
]
},
{
name: "two",
id: 66,
nodes: [{
"name": "Me",
"id": 0
},
{
"name": "Jim",
"id": 1
},
{
"name": "Bob",
"id": 2
},
{
"name": "Jen",
"id": 3
}
],
links: [{
"source": 0,
"target": 1,
"lineID": 1
},
{
"source": 0,
"target": 2,
"lineID": 2
},
{
"source": 1,
"target": 2,
"lineID": 3
},
{
"source": 2,
"target": 3,
"lineID": 4
},
]
}
]
}
}
componentDidMount() {
const data = this.state.data;
data.map(({
nodes,
links
}) => (
FORCE.initForce(nodes, links)
));
FORCE.tick(this)
FORCE.drag()
}
componentDidUpdate(prevProps, prevState) {
if (prevState.nodes !== this.state.nodes || prevState.links !== this.state.links) {
const data = this.state.data;
data.map(({
nodes,
links
}) => (
FORCE.initForce(nodes, links)
));
FORCE.tick(this)
FORCE.drag()
}
}
render() {
const data = this.state.data;
return (
<
div className = "result__container" >
<
h5 className = "result__header" > Data < /h5> {
data.map(({
name,
id,
nodes,
links
}) => ( <
div className = "result__box"
key = {
id
}
value = {
name
} >
<
h5 className = "result__name" > {
name
} < /h5> <
div className = {
"container__graph"
} >
<
svg className = "graph"
width = {
FORCE.width
}
height = {
FORCE.height
} >
<
g > {
links.map((link) => {
return ( <
Link key = {
link.lineID
}
data = {
link
}
/>);
})
} <
/g> <
g > {
nodes.map((node) => {
return ( <
Node data = {
node
}
label = {
node.label
}
key = {
node.id
}
/>);
})
} <
/g> < /
svg > <
/div> < /
div >
))
} <
/div>
)
}
}
///////////////////////////////////////////////////////////
/////// Link component
///////////////////////////////////////////////////////////
class Link extends React.Component {
componentDidMount() {
this.d3Link = d3.select(ReactDOM.findDOMNode(this))
.datum(this.props.data)
.call(FORCE.enterLink);
}
componentDidUpdate() {
this.d3Link.datum(this.props.data)
.call(FORCE.updateLink);
}
render() {
return ( <
line className = 'link' / >
);
}
}
///////////////////////////////////////////////////////////
/////// Node component
///////////////////////////////////////////////////////////
class Node extends React.Component {
componentDidMount() {
this.d3Node = d3.select(ReactDOM.findDOMNode(this))
.datum(this.props.data)
.call(FORCE.enterNode)
}
componentDidUpdate() {
this.d3Node.datum(this.props.data)
.call(FORCE.updateNode)
}
render() {
return ( <
g className = 'node' >
<
circle onClick = {
this.props.addLink
}
/> <
text > {
this.props.data.name
} < /text> < /
g >
);
}
}
ReactDOM.render( < App / > , document.querySelector('#root'))
.container__graph {
background-color: lightsteelblue;
}
.result__header {
background-color: aliceblue;
text-align: center;
color: cadetblue;
text-transform: uppercase;
font-family: cursive;
}
.result__name {
background-color: bisque;
text-align: center;
text-transform: uppercase;
color: chocolate;
font-family: cursive;
margin-bottom: 10px;
padding: 6px;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script>
<div id="root"></div>
For a quick answer to your question: you do need to use each individual force.on('tick',...) for each simulation, because the tick event doesn't fire after a period of inactivity. But your program is actually running into another problem that causes the other graphs to freeze.
The Problem:
The main issue is that you're creating multiple different force simulations, but attaching the same handler to all of them:
data.map(({
nodes,
links
}) => (
FORCE.initForce(nodes, links) // initializes multiple force simulations
));
FORCE.tick(this)
FORCE.drag() // attaches only one handler to them all
When you look at the code, you can see that you're reassigning nsp.force for each graph so it's only referring to the last one:
initForce = (nodes, links) => {
nsp.force = d3.forceSimulation(nodes)
...
},
This becomes a problem with your drag handler, which changes the alphaTarget, which more or less tells nsp.force (which now refers only to the last graph) that it needs to continuing simulating and to restart the simulation:
dragStarted = (d) => {
if (!d3.event.active) nsp.force.alphaTarget(0.3).restart();
// ...
},
dragEnded = (d) => {
if (!d3.event.active) nsp.force.alphaTarget(0);
// ...
},
So the reason why every graph but the last stops moving after a few seconds is because those simulation think they are complete because they aren't rewoken with a drag event.
A Solution:
As I'm sure you've encountered, it's tricky managing multiple force simulations as one big object. Since they seem to be independent, it makes more sense to create a separate instance for the force simulation for each graph. In the following code snippet, I've changed the object FORCE, to be constructor Force, that creates an instance of a force simulation, which we'll create multiple times, one for each graph.
I've also made ForceGraph, a React component to hold the graph and SVG, to separate out elements for each graph, and makes it easer for that force simulation to act on.
Here are the results:
///////////////////////////////////////////////////////////
/////// Functions and variables
///////////////////////////////////////////////////////////
function Force() {
var width = 1080,
height = 250,
enterNode = (selection) => {
var circle = selection.select('circle')
.attr("r", 25)
.style("fill", 'tomato' )
.style("stroke", "bisque")
.style("stroke-width", "3px")
selection.select('text')
.style("fill", "honeydew")
.style("font-weight", "600")
.style("text-transform", "uppercase")
.style("text-anchor", "middle")
.style("alignment-baseline", "middle")
.style("font-size", "10px")
.style("font-family", "cursive")
},
updateNode = (selection) => {
selection
.attr("transform", (d) => "translate(" + d.x + "," + d.y + ")")
.attr("cx", function(d) { return d.x = Math.max(30, Math.min(width - 30, d.x)); })
.attr("cy", function(d) { return d.y = Math.max(30, Math.min(height - 30, d.y)); })
},
enterLink = (selection) => {
selection
.attr("stroke-width", 3)
.attr("stroke","bisque")
},
updateLink = (selection) => {
selection
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
},
updateGraph = (selection) => {
selection.selectAll('.node')
.call(updateNode)
selection.selectAll('.link')
.call(updateLink);
},
color = d3.scaleOrdinal(d3.schemeCategory10),
nsp = {},
initForce = (nodes, links) => {
nsp.force = d3.forceSimulation(nodes)
.force("charge", d3.forceManyBody().strength(-200))
.force("link", d3.forceLink(links).distance(70))
.force("center", d3.forceCenter().x(nsp.width /2).y(nsp.height / 2))
.force("collide", d3.forceCollide([5]).iterations([5]));
},
dragStarted = (d) => {
if (!d3.event.active) nsp.force.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y
},
dragging = (d) => {
d.fx = d3.event.x;
d.fy = d3.event.y
},
dragEnded = (d) => {
if (!d3.event.active) nsp.force.alphaTarget(0);
d.fx = null;
d.fy = null
},
drag = (node) => {
var d3Graph = d3.select(ReactDOM.findDOMNode(node)).select('svg');
d3Graph.selectAll('g.node')
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragging)
.on("end", dragEnded));
},
tick = (node) => {
var d3Graph = d3.select(ReactDOM.findDOMNode(node)).select('svg');
nsp.force.on('tick', () => {
d3Graph.call(updateGraph)
});
};
nsp.enterNode = enterNode;
nsp.updateNode = updateNode;
nsp.enterLink = enterLink;
nsp.updateLink = updateLink;
nsp.width = width;
nsp.height = height;
nsp.initForce = initForce;
nsp.dragStarted = dragStarted;
nsp.dragging = dragging;
nsp.dragEnded = dragEnded;
nsp.drag = drag;
nsp.tick = tick;
return nsp
}
class ForceGraph extends React.Component {
constructor(props) {
super(props);
this.data = props.data;
this.force = Force();
}
initGraph() {
const data = this.data;
this.force.initForce(data.nodes, data.links)
this.force.tick(this)
this.force.drag(this)
}
componentDidMount() {
this.initGraph();
}
componentDidUpdate(prevProps, prevState) {
// TBD
}
render() {
const {name, id, nodes, links} = this.data;
const force = this.force;
return (
<div className="result__box" key={id} value={name}>
<h5 className="result__name">{name}</h5>
<div className={"container__graph"}>
<svg className="graph" width={force.width} height={force.height}>
<g>
{links.map((link) => {
return (
<Link
key={link.lineID}
data={link}
force={force}
/>);
})}
</g>
<g>
{nodes.map((node) => {
return (
<Node
data={node}
label={node.label}
force={force}
key={node.id}
/>);
})}
</g>
</svg>
</div>
</div>
);
}
};
////////////////////////////////////////////////////////////////////////////
/////// class App is the parent component of Link and Node
////////////////////////////////////////////////////////////////////////////
class App extends React.Component {
constructor(props){
super(props)
this.state = {
data: [{
name: "one",
id: 65,
nodes: [{
"name": "fruit",
"id": 0
},
{
"name": "apple",
"id": 1
},
{
"name": "orange",
"id": 2
},
{
"name": "banana",
"id": 3
}
],
links: [{
"source": 0,
"target": 1,
"lineID": 1
},
{
"source": 0,
"target": 2,
"lineID": 2
},
{
"source": 3,
"target": 0,
"lineID": 3
}
]
},
{
name: "two",
id: 66,
nodes: [{
"name": "Me",
"id": 0
},
{
"name": "Jim",
"id": 1
},
{
"name": "Bob",
"id": 2
},
{
"name": "Jen",
"id": 3
}
],
links: [{
"source": 0,
"target": 1,
"lineID": 1
},
{
"source": 0,
"target": 2,
"lineID": 2
},
{
"source": 1,
"target": 2,
"lineID": 3
},
{
"source": 2,
"target": 3,
"lineID": 4
},
]
}
]
}
}
render() {
const data = this.state.data;
return (
<div className="result__container">
<h5 className="result__header">Data</h5>
{data.map((graphData) => (<ForceGraph data={graphData} key={graphData.id} />))}
</div>
)
}
}
///////////////////////////////////////////////////////////
/////// Link component
///////////////////////////////////////////////////////////
class Link extends React.Component {
componentDidMount() {
this.d3Link = d3.select(ReactDOM.findDOMNode(this))
.datum(this.props.data)
.call(this.props.force.enterLink);
}
componentDidUpdate() {
this.d3Link.datum(this.props.data)
.call(this.props.force.updateLink);
}
render() {
return (
<line className='link' />
);
}
}
///////////////////////////////////////////////////////////
/////// Node component
///////////////////////////////////////////////////////////
class Node extends React.Component {
componentDidMount() {
this.d3Node = d3.select(ReactDOM.findDOMNode(this))
.datum(this.props.data)
.call(this.props.force.enterNode)
}
componentDidUpdate() {
this.d3Node.datum(this.props.data)
.call(this.props.force.updateNode)
}
render() {
return (
<g className='node'>
<circle onClick={this.props.addLink}/>
<text>{this.props.data.name}</text>
</g>
);
}
}
ReactDOM.render(<App />, document.querySelector('#root'))
.container__graph {
background-color: lightsteelblue;
}
.result__header {
background-color: aliceblue;
text-align: center;
color: cadetblue;
text-transform: uppercase;
font-family: cursive;
}
.result__name {
background-color: bisque;
text-align: center;
text-transform: uppercase;
color: chocolate;
font-family: cursive;
margin-bottom: 10px;
padding: 6px;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script>
<div id="root"></div>
Update: It looks like if you try to update the data for the charts, it runs into some snags. I tinkered with it a little bit and came up with this fiddle. It's not perfect, but it's a start: https://jsfiddle.net/4pyzL0tq/
Here i am try to update json data and its D3 wheel view but i Don't know why its not update the D3 sunbrust and update the data
This is my code
// in html body
//css code
svg{
width: 100% !important;
background:#ffffff;
}
.slice {
cursor: pointer;
}
.slice .main-arc {
stroke: #fff;
stroke-width: 1px;
}
.slice .hidden-arc {
fill: none;
}
.slice text {
pointer-events: none;
dominant-baseline: middle;
text-anchor: middle;
}
#tooltip { background-color: white;
padding: 3px 5px;
border: 1px solid black;
text-align: center;}
div.tooltip {
position: absolute;
text-align: left;
width: auto;
height: auto;
padding: 8px;
font: 12px sans-serif;
background: #0a2538;
border: #0a2538 1px solid;
border-radius: 0px;
pointer-events: none;
color: white;
}
//D3 Script code for https://d3js.org/d3.v4.min.js
const initItems ={
"name": "Core root",
"id":3,
"description":"Lorem ipsum dolor sit amet",
"children": [
{
"name": "VM.HSN.5.A",
"children": [
{
"name": "eiusmod",
"children": [
{"name": "G.8.1", "id": 5},
{"name": "G.8.2", "id": 4}
]
},
{
"name": "F.8.0",
"children": [
{"name": "F.8.4", "id": 1},
{"name": "F.8.5", "id":1}
]
},
{
"name": "EE.8.5-CLX",
"children": [
{"name": "EE.8.5-CLX2", "id": 1}
]
}
]
},
{
"name": "NS.8.2-CLX",
"children": [
{"name": "NS.8.2-CLX4", "id": 1},
{"name": "NS.8.2-CLX5", "id": 1},
{
"name": "NS.8.0",
"children": [
{"name": "NS.8.2", "id": 1},
{"name": "NS.8.3", "id": 1}
]
},
{"name": "NS.8.1-CLX1", "id":1},
{"name": "NS.8.1-CLX2", "id": 1},
{"name": "NS.8.1-CLX3", "id":1},
{"name": "NS.8.1-CLX4", "id": 1},
{"name": "NS.8.1-CLX5", "id": 1}
]
}
]
}
var newItems = {
"name": "Second Root",
"children": [{
"name": "A2",
"children": [{
"name": "B4",
"size": 40
}, {
"name": "B5",
"size": 30
}, {
"name": "B6",
"size": 10
}]
}, {
"name": "A3",
"children": [{
"name": "B7",
"size": 50
}, {
"name": "B8",
"size": 15
}
]
}]
}
const width = window.innerWidth,
height = window.innerHeight,
maxRadius = (Math.min(width, height) / 2) - 20;
const formatNumber = d3.format(',d');
const x = d3.scaleLinear()
.range([0, 2 * Math.PI])
.clamp(true);
const y = d3.scaleSqrt()
.range([maxRadius*.1, maxRadius]);
const color = d3.scaleOrdinal(d3.schemeCategory20);
const partition = d3.partition();
const arc = d3.arc()
.startAngle(d => x(d.x0))
.endAngle(d => x(d.x1))
.innerRadius(d => Math.max(0, y(d.y0)))
.outerRadius(d => Math.max(0, y(d.y1)));
const middleArcLine = d => {
const halfPi = Math.PI/2;
const angles = [x(d.x0) - halfPi, x(d.x1) - halfPi];
const r = Math.max(0, (y(d.y0) + y(d.y1)) / 2);
const middleAngle = (angles[1] + angles[0]) / 2;
const invertDirection = middleAngle > 0 && middleAngle < Math.PI; // On lower quadrants write text ccw
if (invertDirection) { angles.reverse(); }
const path = d3.path();
path.arc(0, 0, r, angles[0], angles[1], invertDirection);
return path.toString();
};
const textFits = d => {
const CHAR_SPACE = 6;
const deltaAngle = x(d.x1) - x(d.x0);
const r = Math.max(0, (y(d.y0) + y(d.y1)) / 2);
const perimeter = r * deltaAngle;
return d.data.name.length * CHAR_SPACE < perimeter;
};
const svg = d3.select('#vdata').append('svg')
.style('width', '100vw')
.style('height', '100vh')
.attr('viewBox', `${-width / 2} ${-height / 2} ${width} ${height}`)
.on('click', () => focusOn()); // Reset zoom on canvas click
var updateChart = function (items) {
// var root = items;
console.log(items);
//d3.json('dummy4.json', (error, root) => {
///if (error) throw error;
//start custom code
var root = d3.hierarchy(items, function(d) { return d.children })
.sum( function(d) {
if(d.children) {
return 0
} else {
return 2
}
});
//end custom code
//root = d3.hierarchy(root);
//var ad =root.sum(d => d.depth);
//root.sum(d => d.id);
const slice = svg.selectAll('g.slice')
.data(partition(root).descendants());
slice.exit().remove();
const newSlice = slice.enter()
.append('g').attr('class', 'slice')
.on('click', d => {
console.log(d.data.name);
d3.event.stopPropagation();
focusOn(d);
});
newSlice.append('title')
//.text(d => d.data.name + '\n' + d.data.id);
//.on("mouseover", mouseover);
newSlice.append('path')
.attr('class', 'main-arc')
.style('fill',colour)
//.style('fill', d => color((d.children ? d : d.parent).data.name))
//console.log(data.id)
.on("mouseover", mouseover)
.on("mouseout", mouseOutArc)
.attr('d', arc);
newSlice.append('path')
.attr('class', 'hidden-arc')
.attr('id', (_, i) => `hiddenArc${i}`)
.attr('d', middleArcLine);
const text = newSlice.append('text')
.attr('display', d => textFits(d) ? null : 'none');
// Add white contour
text.append('textPath')
.attr('startOffset','50%')
.attr('xlink:href', (_, i) => `#hiddenArc${i}` )
.text(function(d) {
return d.data.name;
})
.style('fill', 'none')
.style('stroke', '#fff')
.style('stroke-width', 5)
.style('stroke-linejoin', 'round');
text.append('textPath')
.attr('startOffset','50%')
.attr('xlink:href', (_, i) => `#hiddenArc${i}` )
.text(function(d) {
return d.data.name;
});
//});
}
// tooltip
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
function colour(d) {
//if (d.id==5) {
var colours;
console.log(d.data.id);
if(d.data.id==5)
{
// There is a maximum of two children!
colours ='#b00';
}else if(d.data.name=='Apps'){
colours='#cc0001';
}
else{
colours=color((d.children ? d : d.parent).data.name)
} // L*a*b* might be better here...
// return d3.hsl((a.h + b.h) / 2, a.s * 1.2, a.l / 1.2);
//}
return colours;
}
function mouseover(d) {
d3.select(this).style("cursor", "pointer")
var descript;
if(d.data.description!=null)
{
descript= "<br/><br/>"+d.data.description;
}else{
descript='';
}
tooltip.html(d.data.name + descript)
.style("opacity", 0.8)
.style("left", (d3.event.pageX) + 0 + "px")
.style("top", (d3.event.pageY) - 0 + "px");
}
function mouseOutArc(){
d3.select(this).style("cursor", "default")
tooltip.style("opacity", 0);
}
function focusOn(d = { x0: 0, x1: 1, y0: 0, y1: 1 }) {
// Reset to top-level if no data point specified
const transition = svg.transition()
.duration(750)
.tween('scale', () => {
const xd = d3.interpolate(x.domain(), [d.x0, d.x1]),
yd = d3.interpolate(y.domain(), [d.y0, 1]);
return t => { x.domain(xd(t)); y.domain(yd(t)); };
});
transition.selectAll('path.main-arc')
.attrTween('d', d => () => arc(d));
transition.selectAll('path.hidden-arc')
.attrTween('d', d => () => middleArcLine(d));
transition.selectAll('text')
.attrTween('display', d => () => textFits(d) ? null : 'none');
moveStackToFront(d);
//
function moveStackToFront(elD) {
svg.selectAll('.slice').filter(d => d === elD)
.each(function(d) {
this.parentNode.appendChild(this);
if (d.parent) { moveStackToFront(d.parent); }
})
}
}
updateChart(initItems);
I am trying to update json data and regenerate the D3 sunburst wheel but it not update.
On developer console it show the json data updated but it not update the text in wheel and not properly regenerate the D3 sunburst wheel.
Please help me out.!
Thanks in advance.
I used this code and it works for me great..!
setTimeout(function () {d3.selectAll("svg > *").remove(); updateChart(newItems); }, 2000);