bring child node close to the parent node in d3 - javascript

I am trying to develop a tree graph where there is a center node which will have 4 child node. Those
child node will have 7 different nodes but those 7 different nodes should be shown just near its parent node like in the attached diagram. If i try to decrease the value to bring them closer, one of the side(either left side or right side) of the tree gets messed up.
here is what I have done
line.link {
stroke: black;
}
line.hard--link {
stroke: black;
stroke-width: 2px;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.3/d3.min.js"></script>
</head>
<body>
<svg className='spider-graph-svg'>
</svg>
<script>
var data = {
"name": "root#gmail.com",
"children": [{
"name": "Person Name 1",
"children": [{
"name": "Branch 4.1"
}, {
"name": "Branch 4.2"
}, {
"name": "Branch 4.2"
},
{
"name": "Branch 4.2"
}, {
"name": "Branch 4.2"
},
{
"name": "Branch 4.2"
}
]
}, {
"name": "Person name 2",
"children": [{
"name": "Branch 4.1"
}, {
"name": "Branch 4.2"
}, {
"name": "Branch 4.2"
},
{
"name": "Branch 4.2"
}, {
"name": "Branch 4.2"
},
{
"name": "Branch 4.2"
}
]
}, {
"name": "Person Name 3",
"children": [{
"name": "Branch 4.1"
}, {
"name": "Branch 4.2"
}, {
"name": "Branch 4.2"
},
{
"name": "Branch 4.2"
}, {
"name": "Branch 4.2"
},
{
"name": "Branch 4.2"
}
]
}, {
"name": "Person Name 4",
"children": [{
"name": "Branch 4.1"
}, {
"name": "Branch 4.2"
}, {
"name": "Branch 4.2"
},
{
"name": "Branch 4.2"
}, {
"name": "Branch 4.2"
},
{
"name": "Branch 4.2"
}
]
}]
};
const LAST_CHILDREN_WIDTH = 13;
let flagForChildren = false;
let groups = [];
data.children.forEach(d => {
let a = [];
if (d.children.length > 0) {
flagForChildren = true;
}
for (let i = 0; i < d.children.length; i += 2) {
let b = d.children.slice(i, i + 2);
if (b[0] && b[1]) {
a.push(Object.assign(b[0], {
children: [b[1]]
}));
} else {
let child = b[0];
if (i >= 6) {
child = Object.assign(child, {
children: [{
name: "..."
}]
});
}
a.push(child);
}
}
d.children = a;
groups.push(d);
});
data.children = groups;
let split_index = Math.round(data.children.length / 2);
let rectangleHeight = 45;
let leftData = {
name: data.name,
children: JSON.parse(JSON.stringify(data.children.slice(0, split_index)))
};
let leftDataArray = [];
leftDataArray.push(leftData);
// Right data
let rightData = {
name: data.name,
children: JSON.parse(JSON.stringify(data.children.slice(split_index)))
};
// Create d3 hierarchies
let right = d3.hierarchy(rightData);
let left = d3.hierarchy(leftData);
// Render both trees
drawTree(right, "right");
drawTree(left, "left");
// draw single tree
function drawTree(root, pos) {
let SWITCH_CONST = 1;
if (pos === "left") {
SWITCH_CONST = -1;
}
const margin = {
top: 20,
right: 120,
bottom: 20,
left: 120
},
width = window.innerWidth - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
let svg = d3
.select("svg")
.attr("height", height + margin.top + margin.bottom)
.attr("width", width + margin.right + margin.left)
.attr('view-box', '0 0 ' + (width + margin.right) + ' ' + (height + margin.top + margin.bottom))
.style("margin-top", "20px")
.style("margin-left", "88px");
const div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// Shift the entire tree by half it's width
let g = svg.append("g").attr("transform", "translate(" + width / 2 + ",0)");
let deductWidthValue = flagForChildren ? 0 : width * 0.33;
// Create new default tree layout
let tree = d3
.tree()
// Set the size
// Remember the tree is rotated
// so the height is used as the width
// and the width as the height
.size([height - 50, SWITCH_CONST * (width - deductWidthValue) / 2])
.separation((a, b) => a.parent === b.parent ? 4 : 4.25);
tree(root);
let nodes = root.descendants();
let links = root.links();
// Set both root nodes to be dead center vertically
nodes[0].x = height / 2;
// Create links
let link = g
.selectAll(".link")
.data(links)
.enter();
link
.append("line")
.attr("class", function(d) {
if (d.target.depth === 2) {
return 'link'
} else {
return 'hard--link'
}
})
.attr("x1", function(d) {
if (
d.target.depth === 3
) {
return 0;
}
return d.source.y + 100 / 2; //d.source.y + 100/2
})
.attr("x2", function(d) {
if (
d.target.depth === 3
) {
return 0;
} else if (d.target.depth === 2) {
return d.target.y;
}
return d.target.y + 100 / 2; //d.target.y + 100/2;
})
.attr("y1", function(d) {
if (
d.target.depth === 3
) {
return 0;
}
return d.source.x + 50 / 2;
})
.attr("y2", function(d) {
if (
d.target.depth === 3
) {
return 0;
} else if (d.target.depth === 2) {
return d.target.x + LAST_CHILDREN_WIDTH / 2;
}
return d.target.x + 50 / 2;
});
//Rectangle width
let node = g
.selectAll(".node")
.data(nodes)
.enter()
.append("g")
.on("mouseover", function(d) {
const dynamicLength = (d.data.topic_name && d.data.topic_name.length) ||
(d.data.name && d.data.name.length);
const rectWidth = dynamicLength <= 3 ? '60px' : `${dynamicLength * 8}px`;
div.transition()
.duration(200)
.style("opacity", 1);
div.html(d.data.topic_name || d.data.name)
.style("left", (d3.event.pageX) + "px")
.style("width", rectWidth)
.style("text-anchor", "middle")
.style("vertical-align", "baseline")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", d => {
div.transition()
.duration(500)
.style("opacity", 0);
})
.attr("class", function(d) {
return "node" + (d.children ? " node--internal" : " node--leaf");
})
.attr("transform", function(d) {
if (d.parent && d.parent.parent) { // this is the leaf node
if (d.parent.parent.parent) {
return (
"translate(" +
d.parent.y +
"," +
(d.x + LAST_CHILDREN_WIDTH + 15) +
")"
);
}
return "translate(" + d.y + "," + d.x + ")";
}
// Select the node with height 2
if (d.height === 2) {
//Lets line this up with its 2nd child (index = 1)
//If y of this child is <0, it means the parent and the child
//both are on the left
//side (with margin of 20 between parent and child)
if (d.children[1]['y'] < 0) {
return "translate(" + (d.children[1]['y'] + LAST_CHILDREN_WIDTH + 20) + "," + d.children[1]['x'] + ")"
// Else both parent and child are on the right.
//Now we also need to take into consideration the width
//of the rectangle (with margin of 20 between parent and child)
} else {
return "translate(" + (d.children[1]['y'] - rectangleWidth(d) - 20) + "," + (d['x']) + ")"
}
} else {
//This is the root of the tree.
//Subtract d.y by half of rectangleWidth because we need it to be in the center
//Same for d.x
return "translate(" + (d.y - (rectangleWidth(d) / 2)) + "," + (d.x - (rectangleHeight / 2)) + ")";
}
});
// topic rect
node
.append("rect")
.attr("height", (d, i) => d.parent && d.parent.parent ? 15 : rectangleHeight)
.attr("width", (d, i) => d.parent && d.parent.parent ? 15 : rectangleWidth(d))
.attr("rx", (d, i) => d.parent && d.parent.parent ? 5 : 5)
.attr("ry", (d, i) => d.parent && d.parent.parent ? 5 : 5)
// topic edges
node.append('line')
.attr('x1', d => {
if (d.depth === 2) {
return 10
}
})
.attr('x2', d => {
if (d.depth === 2) {
return 10
}
})
.attr('y1', d => {
if (d.depth === 2) {
if (d.children) {
return 0;
}
return 40;
}
})
.attr('y2', d => {
if (d.depth === 2) {
return 40
}
})
.attr('class', 'hard--link')
// topic names
node
.append("text")
.attr("dy", function(d, i) {
return d.parent && d.parent.parent ? 10 : rectangleHeight / 2;
})
.attr("dx", function(d, i) {
if (!(d.parent && d.parent.parent)) {
return 12;
} else {
return 20;
}
})
.style("fill", function(d, i) {
return d.parent && d.parent.parent ? "Black" : "White";
})
.text(function(d) {
let name = d.data.topic_name || d.data.name;
return name.length > 12 ? `${name.substring(0, 12)}...` : name;
})
.style("text-anchor", function(d) {
if (d.parent && d.parent.parent) {
return pos === "left" && "end"
}
})
.style("font-size", "12")
.attr("transform", function(d) {
if (d.parent && d.parent.parent) {
return pos === "left" ? "translate(-30,0)" : "translate(5,0)"
}
})
}
function rectangleWidth(d) {
const MIN_WIDTH = 50;
const MAX_WIDTH = 100;
let dynamicLength = 6;
if (d.data.topic_name) {
dynamicLength = d.data.topic_name.length;
} else if (d.data.name) {
dynamicLength = d.data.name.length;
}
dynamicLength = dynamicLength < 3 ? MIN_WIDTH : MAX_WIDTH;
return dynamicLength;
}
</script>
</body>
</html>
the expected design
expected design of the last nodes

The x and y for the nodes is being calculated by d3 but the placement doesn't look right maybe because the height and the width of the rects weren't taken into account.
So I made a few changes to the code in the section where you are translating the rects based on d3's calculated x and y like so:
.attr("transform", function (d) {
if (d.parent && d.parent.parent) { // this is the leaf node
if (d.parent.parent.parent) {
return (
"translate(" +
d.parent.y +
"," +
(d.x + LAST_CHILDREN_WIDTH + 15) +
")"
);
}
return "translate(" + d.y + "," + d.x + ")";
}
// Select the node with height 2
if (d.height == 2) {
//Lets line this up with its 2nd child (index = 1)
//If y of this child is <0, it means the parent and the child
//both are on the left
//side (with margin of 20 between parent and child)
if (d.children[1]['y'] < 0) {
return "translate(" + (d.children[1]['y'] + LAST_CHILDREN_WIDTH + 20) + "," + d.children[1]['x'] + ")"
// Else both parent and child are on the right.
//Now we also need to take into consideration the width
//of the rectangle (with margin of 20 between parent and child)
} else {
return "translate(" + (d.children[1]['y'] - rectangleWidth(d) - 20) + "," + (d['x']) + ")"
}
} else {
//This is the root of the tree.
//Subtract d.y by half of rectangleWidth because we need it to be in the center
//Same for d.x
return "translate(" + (d.y - (rectangleWidth(d) / 2)) + "," + (d.x - (rectangleHeight / 2)) + ")";
}
});
Here's the fiddle with these changes.
body {
background: black
}
rect {
fill: darkgreen
}
line {
stroke: lightgreen;
stroke-width: 1
}
text {
font-family: 'Calibri';
}
.tooltip {
color: white
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.3/d3.min.js"></script>
</head>
<body>
<svg className='spider-graph-svg'>
</svg>
<script>
var data = {
"name": "root#gmail.com",
"children": [{
"name": "Person Name 1",
"children": [{
"name": "Branch 4.1"
}, {
"name": "Branch 4.2"
}, {
"name": "Branch 4.2"
},
{
"name": "Branch 4.2"
}, {
"name": "Branch 4.2"
},
{
"name": "Branch 4.2"
}
]
}, {
"name": "Person name 2",
"children": [{
"name": "Branch 4.1"
}, {
"name": "Branch 4.2"
}, {
"name": "Branch 4.2"
},
{
"name": "Branch 4.2"
}, {
"name": "Branch 4.2"
},
{
"name": "Branch 4.2"
}
]
}, {
"name": "Person Name 3",
"children": [{
"name": "Branch 4.1"
}, {
"name": "Branch 4.2"
}, {
"name": "Branch 4.2"
},
{
"name": "Branch 4.2"
}, {
"name": "Branch 4.2"
},
{
"name": "Branch 4.2"
}
]
}, {
"name": "Person Name 4",
"children": [{
"name": "Branch 4.1"
}, {
"name": "Branch 4.2"
}, {
"name": "Branch 4.2"
},
{
"name": "Branch 4.2"
}, {
"name": "Branch 4.2"
},
{
"name": "Branch 4.2"
}
]
}]
};
const LAST_CHILDREN_WIDTH = 13;
let flagForChildren = false;
let groups = [];
data.children.forEach(d => {
let a = [];
if (d.children.length > 0) {
flagForChildren = true;
}
for (let i = 0; i < d.children.length; i += 2) {
let b = d.children.slice(i, i + 2);
if (b[0] && b[1]) {
a.push(Object.assign(b[0], {
children: [b[1]]
}));
} else {
let child = b[0];
if (i >= 6) {
child = Object.assign(child, {
children: [{
name: "..."
}]
});
}
a.push(child);
}
}
d.children = a;
groups.push(d);
});
data.children = groups;
let split_index = Math.round(data.children.length / 2);
let rectangleHeight = 45;
let leftData = {
name: data.name,
children: JSON.parse(JSON.stringify(data.children.slice(0, split_index)))
};
let leftDataArray = [];
leftDataArray.push(leftData);
// Right data
let rightData = {
name: data.name,
children: JSON.parse(JSON.stringify(data.children.slice(split_index)))
};
// Create d3 hierarchies
let right = d3.hierarchy(rightData);
let left = d3.hierarchy(leftData);
console.log(right.descendants())
// Render both trees
drawTree(right, "right");
drawTree(left, "left");
// draw single tree
function drawTree(root, pos) {
let SWITCH_CONST = 1;
if (pos === "left") {
SWITCH_CONST = -1;
}
const margin = {
top: 10,
right: 10,
bottom: 10,
left: 10
},
width = window.innerWidth - margin.left - margin.right,
height = window.innerHeight - margin.top - margin.bottom;
let svg = d3
.select("svg")
.attr("height", height + margin.top + margin.bottom)
.attr("width", width + margin.right + margin.left)
.attr('view-box', '0 0 ' + (width + margin.right) + ' ' + (height + margin.top + margin.bottom))
/* .style("margin-top", "20px")
.style("margin-left", "88px"); */
const div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// Shift the entire tree by half it's width
let g = svg.append("g").attr("transform", "translate(" + width / 2 + ",0)");
let deductWidthValue = flagForChildren ? 0 : width * 0.33;
// Create new default tree layout
let tree = d3
.tree()
// Set the size
// Remember the tree is rotated
// so the height is used as the width
// and the width as the height
.size([height - 50, SWITCH_CONST * (width - deductWidthValue) / 2])
.separation((a, b) => a.parent === b.parent ? 4 : 4.25);
tree(root);
let nodes = root.descendants();
let links = root.links();
// Set both root nodes to be dead center vertically
nodes[0].x = height / 2;
// Create links
let link = g
.selectAll(".link")
.data(links)
.enter();
link
.append("line")
.attr("class", function(d) {
if (d.target.depth === 2) {
return 'link'
} else {
return 'hard--link'
}
})
.attr("x1", function(d) {
if (d.target.depth === 3) {
return 0;
} else if (d.target.depth === 2) {
if (d.source.y < 0) {
return (d.source.y + 100 / 2) - 100;
} else {
return (d.source.y + 100 / 2)
}
}
return 0; //d.source.y + 100/2
})
.attr("x2", function(d) {
if (d.target.depth === 3) {
return 0;
} else if (d.target.depth === 2) {
return d.target.y + 10;
} else if (d.target.depth === 1) {
if (d.target.y < 0) {
return d.target.y - 100 / 2
} else {
return d.target.y + 100 / 2;
}
}
return d.target.y + 100 / 2; //d.target.y + 100/2;
})
.attr("y1", function(d) {
if (d.target.depth === 3) {
return 0;
} else if (d.target.depth === 1) {
return (d.source.x + 50 / 2) - 20;
} else {
return d.source.x + 50 / 2;
}
})
.attr("y2", function(d) {
if (d.target.depth === 3) {
return 0;
} else if (d.target.depth === 2) {
return d.target.x + LAST_CHILDREN_WIDTH / 2;
}
return d.target.x + 50 / 2;
});
//Rectangle width
let node = g
.selectAll(".node")
.data(nodes)
.enter()
.append("g")
.on("mouseover", function(d) {
const dynamicLength = (d.data.topic_name && d.data.topic_name.length) ||
(d.data.name && d.data.name.length);
const rectWidth = dynamicLength <= 3 ? '60px' : `${dynamicLength * 8}px`;
div.transition()
.duration(200)
.style("opacity", 1);
div.html(d.data.topic_name || d.data.name)
.style("left", (d3.event.pageX) + "px")
.style("width", rectWidth)
.style("text-anchor", "middle")
.style("vertical-align", "baseline")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", d => {
div.transition()
.duration(500)
.style("opacity", 0);
})
.attr("class", function(d) {
return "node" + (d.children ? " node--internal" : " node--leaf");
})
.attr("transform", function(d) {
if (d.parent && d.parent.parent) { // this is the leaf node
if (d.parent.parent.parent) {
return (
"translate(" +
d.parent.y +
"," +
(d.x + LAST_CHILDREN_WIDTH + 15) +
")"
);
}
return "translate(" + d.y + "," + d.x + ")";
}
// Select the node with height 2
if (d.height == 2) {
//Lets line this up with its 2nd child (index = 1)
//If y of this child is <0, it means the parent and the child
//both are on the left
//side (with margin of 20 between parent and child)
if (d.children[1]['y'] < 0) {
return "translate(" + (d.children[1]['y'] + LAST_CHILDREN_WIDTH + 20) + "," + d.children[1]['x'] + ")"
// Else both parent and child are on the right.
//Now we also need to take into consideration the width
//of the rectangle (with margin of 20 between parent and child)
} else {
return "translate(" + (d.children[1]['y'] - rectangleWidth(d) - 20) + "," + (d['x']) + ")"
}
} else {
//This is the root of the tree.
//Subtract d.y by half of rectangleWidth because we need it to be in the center
//Same for d.x
return "translate(" + (d.y - (rectangleWidth(d) / 2)) + "," + (d.x - (rectangleHeight / 2)) + ")";
}
})
.on('click', function(d) {
console.log(d)
});
// topic rect
node
.append("rect")
.attr("height", (d, i) => d.parent && d.parent.parent ? 15 : rectangleHeight)
.attr("width", (d, i) => d.parent && d.parent.parent ? 15 : rectangleWidth(d))
.attr("rx", (d, i) => d.parent && d.parent.parent ? 5 : 5)
.attr("ry", (d, i) => d.parent && d.parent.parent ? 5 : 5)
// topic edges
node.append('line')
.attr('x1', d => {
if (d.depth === 2) {
return 10
}
})
.attr('x2', d => {
if (d.depth === 2) {
return 10
}
})
.attr('y1', d => {
if (d.depth === 2) {
if (d.children) {
return 0;
}
return 40;
}
})
.attr('y2', d => {
if (d.depth === 2) {
return 40
}
})
.attr('class', 'hard--link')
// topic names
node
.append("text")
.attr("dy", function(d, i) {
return d.parent && d.parent.parent ? 10 : rectangleHeight / 2;
})
.attr("dx", function(d, i) {
if (!(d.parent && d.parent.parent)) {
return 12;
} else {
return 20;
}
})
.style("fill", function(d, i) {
return d.parent && d.parent.parent ? "White" : "White";
})
.text(function(d) {
let name = d.data.topic_name || d.data.name;
return name.length > 12 ? `${name.substring(0, 12)}...` : name;
})
.style("text-anchor", function(d) {
if (d.parent && d.parent.parent) {
return pos === "left" && "end"
}
})
.style("font-size", "12")
.attr("transform", function(d) {
if (d.parent && d.parent.parent) {
return pos === "left" ? "translate(-30,0)" : "translate(5,0)"
}
})
}
function rectangleWidth(d) {
const MIN_WIDTH = 50;
const MAX_WIDTH = 100;
let dynamicLength = 6;
if (d.data.topic_name) {
dynamicLength = d.data.topic_name.length;
} else if (d.data.name) {
dynamicLength = d.data.name.length;
}
dynamicLength = dynamicLength < 3 ? MIN_WIDTH : MAX_WIDTH;
return dynamicLength;
}
</script>
</body>
</html>

Related

D3.js node links overflowing into sibling nodes when new nodes are being added

I am working on a D3.js V4/5 implementation of building a vertical flow-chart. I am adding a new node to the flow-chart's decision box by clicking on a "decision" box and it's corresponding "diamond"/"rect" shape.
From Mike Bostocks answer here for adding/removing nodes :- https://github.com/d3/d3-hierarchy/issues/139
I've followed step 1 which is :- "Derive an entirely new tree by calling d3.hierarchy after modifying your data (or passing in a different children accessor function to do filtering".
So when a user tries to add a new node I am modifying the actual tree/children , computing the hierarchy and calling the update() method. Something like below
JS Fiddle:- http://jsfiddle.net/rs3owt6g/6/
function updateAfterAddingNode() {
root = d3.hierarchy(treeData[0], function(d) {
return d.children;
});
root.x0 = height/2;
root.y0 = 0;
update(root);
}
Actual Issue:
Everything seems to be working fine until the point when I try to add 2 decision nodes to a decision node and more decision nodes underneath it. The Links connecting the nodes passes through the other sibling node.
To replicate the issue in fiddle:
To add a new node click on the orange diamond which appears on click of a node.
Add 2 sibling nodes ( 1 action/rect and 1 decision node) to left and right respectively. For the decision node add 2 decision nodes and for these 2 decision nodes add another 2 decison nodes.
Below picture can give you a clarity. As you can see, the left highlighted path goes through the "New Action" node instead of staying put at the earlier location when all nodes are added. Also, the distance between siblings grows a lot more when more children are added.
var margin = {
top: 20,
right: 120,
bottom: 20,
left: 120,
},
width = 960 - margin.right - margin.left,
height = 800 - margin.top - margin.bottom;
function generateEmptyDecisionBox(condition) {
return condition === 'False' ? [{
"name": "newDecision",
"id": "newId",
"type": "decision",
"value": "notSure",
"condition": `${condition}`,
}, {}] : [{}, {
"name": "newDecision",
"id": "newId",
"type": "decision",
"value": "notSure",
"condition": `${condition}`,
}];
}
function generateEmptyActionBox(condition) {
return condition === 'False' ? [{
"name": "newAction",
"id": "newId",
"type": "action",
"value": "notSure",
"condition": `${condition}`,
}, {}] : [{}, {
"name": "newAction",
"id": "newId",
"type": "action",
"value": "notSure",
"condition": `${condition}`,
}];
}
var selectedNode;
var selectedLink;
var treeData = [{
"name": "Root",
"type": "decision",
"id": "root",
"children": [{
"name": "analytics",
"condition": "False",
"type": "decision",
"value": "a+b",
"id": "child1",
"children": [{
"name": "distinction",
"type": "action",
"id": "child2",
"condition": "True",
"value": "5",
}, {
"name": "nonDistinction",
"type": "action",
"id": "child3",
"condition": "False",
"value": "4",
"children": [],
}],
},
{
"condition": "True",
"name": "division",
"type": "decision",
"value": "a-b",
"id": "child33",
"children":[{
"condition": "True",
"name": "division1",
"type": "decision",
"value": "a-b",
"id": "child44",
"children":[{
"condition": "True",
"name": "division1.1",
"type": "decision",
"value": "a-b",
"id": "child599",
"children":[{
"condition": "True",
"name": "division1.1.34",
"type": "decision",
"value": "a-b",
"id": "child234234",
"children":[{
"condition": "True",
"name": "division1.1.434",
"type": "decision",
"value": "a-b",
"id": "child35343",
"children":[],
}],
},{
"condition": "True",
"name": "division1.1.2",
"type": "decision",
"value": "a-b",
"id": "child77",
"children":[{
"condition": "True",
"name": "division1.1.1",
"type": "decision",
"value": "a-b",
"id": "child1222",
"children":[],
},{
"condition": "True",
"name": "division1.1.1",
"type": "decision",
"value": "a-b",
"id": "child66",
"children":[],
}],
}],
},{
"condition": "True",
"name": "NODE HAVING OVERLAP ISSUE",
"type": "decision",
"value": "a-b",
"id": "child9090",
"children":[],
}],
},
{
"condition": "True",
"name": "division2",
"type": "decision",
"value": "a-b",
"id": "child55",
"children":[{
"condition": "True",
"name": "division2.1",
"type": "decision",
"value": "a-b",
"id": "child88",
"children":[{
"condition": "True",
"name": "division2.1.1",
"type": "decision",
"value": "a-b",
"id": "child99",
"children":[],
}],
}],
},
],
},
],
}];
var i = 0,
duration = 1000,
rectW = 120,
rectH = 60;
var treeMap = d3.tree()
.nodeSize([150, 180]);
//LINK FUNCTION TO DRAW LINKS
var linkFunc = function(d) {
var source = {
x: d.parent.x + rectW / 2,
y: d.parent.y + (rectH / 2),
};
var target = {
x: d.x + (rectW / 2),
y: d.y + 3,
};
// This is where the line bends
var inflection = {
x: target.x,
y: source.y,
};
var radius = 5;
var result = "M" + source.x + ',' + source.y;
if (source.x < target.x && d.data.type) {
// Child is to the right of the parent
result += ' H' + (inflection.x - radius);
} else if (d.data.type) {
result += ' H' + (inflection.x + radius);
} else {
return;
}
// Curve the line at the bend slightly
result += ' Q' + inflection.x + ',' + inflection.y + ' ' + inflection.x + ',' + (inflection.y + radius);
result += 'V' + target.y;
return result;
};
// END OF LINK FUNC //
const zoomSvg = d3.select('.tree-diagram')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g');
const svg = zoomSvg.append('g')
.attr('transform', 'translate(' + 300 + ',' + 20 + ')');
const attachZoom = d3.select('svg');
attachZoom.call(d3.zoom().on('zoom', () => {
zoomSvg.attr('transform', d3.event.transform)
}))
// ADD ARROW TO THE BOTTOM POINTING TO THE NEXT DECISION.
svg.append("svg:defs")
.selectAll("marker")
.data(["end"]) // Different link/path types can be defined here
.enter()
.append("svg:marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", 0.5)
.attr("markerWidth", 4)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
//necessary so that zoom knows where to zoom and unzoom from
/* zm.translate([350, 20]); */
root = d3.hierarchy(treeData[0], function(d) {
return d.children;
});
root.x0 = 0;
root.y0 = 0;
update(root);
d3.select(".tree-diagram")
.style("height", "1000px");
// END OF DRAW TREEE //
function update(source) {
const treeData = treeMap(root);
const treeRoot = d3.hierarchy(root);
// d3.tree(treeRoot);
// 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 * 90;
});
// Update the nodes…
var node = svg.selectAll("g.node")
.data(nodes, function(d) {
return d.data.id || (d.id = ++i);
});
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter()
.append("g")
.attr('transform', 'translate(' + source.x0 + ', ' + source.y0 + ')')
.attr("class", "node")
.on("click", click);
// .on("blur", onNodeBlur);
nodeEnter.append('path')
.attr('d', function(d) {
if (d.data.type === 'decision') {
return 'M 60 0 120 30 60 60 0 30 Z';
} else if (d.data.type === 'action') {
return 'M 0 0 120 0 120 60 0 60 Z';
} else {
return 'M -100 -10 -10 -10 -10 -10 -10 -10Z';
}
})
.attr("stroke-width", 1)
.attr('class', 'myPaths')
.style("fill", function(d) {
return "lightsteelblue";
});
nodeEnter.append("text")
.attr("x", rectW / 2)
.attr("y", rectH / 2)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.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.x) + "," + (d.y) + ")";
});
nodeUpdate.select('path.myPaths')
.attr("d", function(d) {
if (d.data.type === 'decision') {
return 'M 60 0 120 30 60 60 0 30 Z';
} else if (d.data.type === 'action') {
return 'M 0 0 120 0 120 60 0 60 Z';
} else {
return 'M -100 -10 -10 -10 -10 -10 -10 -10Z';
}
});
var nodeExit = node.exit()
.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.x + "," + source.y + ")";
})
.remove();
// Update the links…
var link = svg.selectAll(".link")
.data(links, function(d) {
return d.data.id;
})
.classed('link1', true);
// Enter any new links at the parent's previous position.
var linkEnter = link.enter()
.insert("g", "g")
.attr("class", "link");
linkEnter.append('path')
.on('click', function(d, i) {
selectedLink = d;
// Use the native SVG interface to get the bounding box to
// calculate the center of the path
var bbox = this.getBBox();
var x;
var y;
if (d.parent.x < d.x) {
// Child is to the right of the parent
x = bbox.x + bbox.width;
y = bbox.y;
plusButton
.attr('transform', 'translate(' + x + ', ' + y + ')')
.classed('hide', false);
} else {
x = bbox.x;
y = bbox.y;
plusButton
.attr('transform', 'translate(' + x + ', ' + y + ')')
.classed('hide', false);
}
})
.on('blur', function(d, i) {
plusButton
.classed('hide', true);
})
.attr("marker-end", "url(#end)");
// Add Link Texts.
linkEnter.append('text');
// Merge the new and the existing links before setting `d` and `text` on all of them
link = linkEnter.merge(link);
link.select('path')
.attr("d", linkFunc);
link.select('text')
.text(function(d, i) {
if (d.parent.x < d.x) {
return 'True';
} else {
return 'False';
}
})
.attr('transform', function(d) {
if (d.parent.x < d.x && d.data.type) {
return 'translate(' + (d.x + rectW / 2) + ',' + (d.parent.y + rectH) + ')';
} else if (d.data.type) {
return 'translate(' + (d.parent.x + rectW / 2) + ',' + (d.y + rectH) + ')';
} else {
return;
}
});
//LinkUpdate
var linkUpdate = linkEnter.merge(link);
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", linkFunc);
// Transition links to their new position.
// Transition exiting nodes to the parent's new position.
link.exit()
.transition()
.duration(duration)
.attr("d", linkFunc)
.remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
// ON CLICK OF NODES
function click(d) {
if (d.data.type === 'action') {
return;
}
selectedNode = d;
if (!(d.data.children && d.data.children[0] && Object.keys(d.data.children[0]).length)) {
diamondImageFalse
.attr('transform', 'translate(' + (d.x - 20) + ', ' + (d.y + 35) + ')')
.classed('hide', false);
rectangleShapeFalse.attr('transform', 'translate(' + (d.x - 20) + ', ' + d.y + ')').classed('hide', false);
}
if (!(d.data.children && d.data.children[1] && Object.keys(d.data.children[1]).length)) {
diamondImage
.attr('transform', 'translate(' + (d.x + 110) + ', ' + (d.y + 35) + ')')
.classed('hide', false);
rectangleShape.attr('transform', 'translate(' + (d.x + 110) + ', ' + d.y + ')').classed('hide', false);
}
}
// oN CALL
function addElement(d, truthy) {
d.children = null;
d.children = generateEmptyDecisionBox(truthy);
update(root);
}
// draw elements //
function drawDiamond(centroid) {
// Start at the top
var result = 'M' + centroid.x + ',' + (centroid.y - rectH / 2);
// Go right
result += 'L' + (centroid.x + rectW / 2) + ',' + centroid.y;
// Bottom
result += 'L' + centroid.x + ',' + (centroid.y + rectH / 2);
// Left
result += 'L' + (centroid.x - rectW / 2) + ',' + centroid.y;
// Close the shape
result += 'Z';
return result;
}
function drawRect(centroid) {
// Start at the top left
var result = 'M' + (centroid.x - rectW / 2) + ',' + (centroid.y - rectH / 2);
// Go right
result += 'h' + rectW;
// Go down
result += 'v' + rectH;
// Left
result += 'h-' + rectW;
// Close the shape
result += 'Z';
return result;
}
var plusButton = svg
.append('g')
.classed('button', true)
.classed('hide', true)
.on('click', function() {
/* addElement(selectedLink.source); */
removeAllButtonElements();
});
plusButton
.append('rect')
.attr('transform', 'translate(-8, -8)') // center the button inside the `g`
.attr('width', 16)
.attr('height', 16)
.attr('rx', 2);
plusButton
.append('path')
.attr('d', 'M-6 0 H6 M0 -6 V6');
var rectangleShape = svg.append('g')
.classed('conditionImage', true)
.classed('hide', true)
.on('click', function() {
addActionOrDecision(selectedNode, 'action', 'True');
removeAllButtonElements();
});
rectangleShape
.append('rect')
.attr('width', 30)
.attr('height', 20)
.style('fill', 'orange');
var diamondImage = svg.append('g')
.classed('conditionSvg', true)
.classed('hide', true)
.classed('scale', true)
.on('click', function() {
addActionOrDecision(selectedNode, 'decision', 'True');
removeAllButtonElements();
});
diamondImage
.append('path')
.attr('d', 'M 15 0 30 15 15 30 0 15 Z')
.style("fill", 'orange');
var rectangleShapeFalse = svg.append('g')
.classed('conditionImage', true)
.classed('hide', true)
.on('click', function() {
addActionOrDecision(selectedNode, 'action', 'False');
removeAllButtonElements();
});
rectangleShapeFalse
.append('rect')
.attr('width', 30)
.attr('height', 20)
.style('fill', 'orange');
var diamondImageFalse = svg.append('g')
.classed('conditionImage', true)
.classed('hide', true)
.classed('scale', true)
.on('click', function() {
addActionOrDecision(selectedNode, 'decision', 'False');
// addElement(selectedNode, 'False');
removeAllButtonElements();
});
diamondImageFalse
.append('path')
.attr('d', 'M 15 0 30 15 15 30 0 15 Z')
.style("fill", 'orange');
function removeAllButtonElements() {
plusButton.classed('hide', true);
diamondImage.classed('hide', true);
rectangleShape.classed('hide', true);
diamondImageFalse.classed('hide', true);
rectangleShapeFalse.classed('hide', true);
}
function addActionOrDecision(selectedNode, nodeType, conditionType) {
const parentNodeId = selectedNode.parent.data.id;
const selectedNodeId = selectedNode.data.id;
// find the selected node from the actual treeData
const foundRule = getNodeFromNodeId(treeData, selectedNodeId);
const newRuleId = Math.random();
const newNodeToAdd = {
"condition": conditionType,
"name": nodeType === 'decision' ? 'New Decision' : 'New Action',
"type": nodeType,
"value": "",
"id": newRuleId,
"parent": parentNodeId,
"children": [],
};
const clonedNewNode = { ...newNodeToAdd
};
if (conditionType === 'False' && foundRule.children) {
// foundRule.children[0] = newNodeToAdd;
foundRule.children.splice(0, 1, clonedNewNode);
if (!(foundRule.children[1] && Object.keys(foundRule.children[1]))) {
foundRule.children[1] = {};
}
} else {
// foundRule.children[1] = newNodeToAdd;
foundRule.children.splice(1, 1, clonedNewNode);
if (!(foundRule.children[0] && Object.keys(foundRule.children[0]))) {
founRule.children[0] = {};
}
}
// find the node and add a child to it.
updateTree();
}
function updateTree() {
root = d3.hierarchy(treeData[0], function(d) {
return d.children;
});
root.x0 = height / 2;
root.y0 = 0;
update(root);
}
function getNodeFromNodeId(nodes, nodeId) {
for (const node of nodes) {
const currNode = node;
if (currNode) {
if (currNode.id === nodeId) {
return currNode;
} else if (currNode.children) {
const childResult = getNodeFromNodeId(currNode.children, nodeId);
if (childResult) {
return childResult;
}
}
}
}
return null;
}
.node {
cursor: pointer;
outline: none !important;
}
.node text {
font: 10px sans-serif;
}
.button>path {
stroke: blue;
stroke-width: 1.5;
/* outline: none; */
}
.button>rect {
fill: #ddd;
stroke: grey;
stroke-width: 1px;
}
.conditionalSvg {
/* outline: none; */
display: none;
}
.hide {
/* display: none; */
opacity: 0 !important;
/* pointer-events: none; */
}
.link:hover {
outline: none !important;
cursor: pointer;
stroke-width: 3px;
}
.link path {
/* outline: none !important; */
fill: none;
stroke: darkgray;
stroke-width: 2px;
}
.link path:hover {
cursor: pointer;
stroke-width: 4px;
}
.link text {
font: 10px sans-serif;
}
.colorBlue {
background-color: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<div class="tree-diagram">
</div>
Also just wanted to take a confirmation on the way the nodes are being added in the addActionOrDecision let me know if that's a good way to do it. I am basically finding the parent node from the actual data and adding a copy of a newly created node to the parent node's children ( in the actual treeData ).
Edit :- This is how it looks when I keep adding more nodes , the left side's nodes are mixed up with the right side one's and the nodes/links are messed up.
UX on how it should probably look like :- (Add a bend to the link and adjust the entire tree to the left or right accordingly ? )
EDIT :-
Modified JsFiddle to show the issue during the initial launch :- http://jsfiddle.net/c3yz4bj0/3/
I solved this by writing a custom tree.separation() function. It's very similar to the default one, but differs in that it places nodes farther apart if only one of the two nodes has any children. This prevents the overlap. Normally, if two nodes both have children, those children will be the reason why they are not overlapping, but sometimes this doesn't work.
var margin = {
top: 20,
right: 120,
bottom: 20,
left: 120,
},
width = 960 - margin.right - margin.left,
height = 800 - margin.top - margin.bottom;
function generateEmptyDecisionBox(condition) {
return condition === 'False' ? [{
"name": "newDecision",
"id": "newId",
"type": "decision",
"value": "notSure",
"condition": `${condition}`,
}, {}] : [{}, {
"name": "newDecision",
"id": "newId",
"type": "decision",
"value": "notSure",
"condition": `${condition}`,
}];
}
function generateEmptyActionBox(condition) {
return condition === 'False' ? [{
"name": "newAction",
"id": "newId",
"type": "action",
"value": "notSure",
"condition": `${condition}`,
}, {}] : [{}, {
"name": "newAction",
"id": "newId",
"type": "action",
"value": "notSure",
"condition": `${condition}`,
}];
}
var selectedNode;
var selectedLink;
var treeData = [{
"name": "Root",
"type": "decision",
"id": "root",
"children": [{
"name": "analytics",
"condition": "False",
"type": "decision",
"value": "a+b",
"id": "child1",
"children": [{
"name": "distinction",
"type": "action",
"id": "child2",
"condition": "True",
"value": "5",
}, {
"name": "nonDistinction",
"type": "action",
"id": "child3",
"condition": "False",
"value": "4",
"children": [],
}],
},
{
"condition": "True",
"name": "division",
"type": "decision",
"value": "a-b",
"id": "child33",
"children": [{
"condition": "True",
"name": "division1",
"type": "decision",
"value": "a-b",
"id": "child44",
"children": [{
"condition": "True",
"name": "division1.1",
"type": "decision",
"value": "a-b",
"id": "child599",
"children": [{
"condition": "True",
"name": "division1.1.34",
"type": "decision",
"value": "a-b",
"id": "child234234",
"children": [{
"condition": "True",
"name": "division1.1.434",
"type": "decision",
"value": "a-b",
"id": "child35343",
"children": [],
}],
}, {
"condition": "True",
"name": "division1.1.2",
"type": "decision",
"value": "a-b",
"id": "child77",
"children": [{
"condition": "True",
"name": "division1.1.1",
"type": "decision",
"value": "a-b",
"id": "child1222",
"children": [],
}, {
"condition": "True",
"name": "division1.1.1",
"type": "decision",
"value": "a-b",
"id": "child66",
"children": [],
}],
}],
}, {
"condition": "True",
"name": "NODE HAVING OVERLAP ISSUE",
"type": "decision",
"value": "a-b",
"id": "child9090",
"children": [{
"condition": "True",
"name": "division1.1.1",
"type": "decision",
"value": "a-b",
"id": "child909090",
"children": [],
}],
}],
},
{
"condition": "True",
"name": "division2",
"type": "decision",
"value": "a-b",
"id": "child55",
"children": [{
"condition": "True",
"name": "division2.1",
"type": "decision",
"value": "a-b",
"id": "child88",
"children": [{
"condition": "True",
"name": "division2.1.1",
"type": "decision",
"value": "a-b",
"id": "child99",
"children": [],
}],
}],
},
],
},
],
}];
var i = 0,
duration = 1000,
rectW = 120,
rectH = 60;
var treeMap = d3.tree()
.nodeSize([140, 120])
.separation(function(a, b) {
// If they have the same parent
if(a.parent === b.parent) {
// and are either both leaf nodes or both not leaf nodes
// or have only one child (which results in a straight line down)
if((a.children === undefined || a.children.length <= 1) ===
(b.children === undefined || b.children.length <= 1)) {
return 1;
}
// else, increase the size between them
return 2;
}
// If they have the same depth, mark them as such so we can avoid them later
if(a.depth === b.depth) {
a.data.avoidRight = b;
b.data.avoidLeft = a;
}
return 2;
});
//LINK FUNCTION TO DRAW LINKS
var linkFunc = function(d) {
var source = {
x: d.source.x + rectW / 2,
y: d.source.y + (rectH / 2),
};
var target = {
x: d.target.x + (rectW / 2),
y: d.target.y + 3,
};
// This is where the line bends
var inflection = {
x: target.x,
y: source.y,
};
var radius = 5;
var result = "M" + source.x + ',' + source.y;
if(!d.source.data.type) {
return;
}
if(source.x < target.x) {
// Child is to the right of the parent
if(d.source.data.avoidRight !== undefined && inflection.x > d.source.data.avoidRight.x) {
// There is some node that we should try to avoid first
result += ' H' + (d.source.data.avoidRight.x - 2 * radius);
result += ' V' + (d.source.data.avoidRight.y + rectH + radius);
inflection.y = d.source.data.avoidLeft.y + rectH + radius;
}
result += ' H' + (inflection.x - radius);
} else {
// Child is to the left of parent
if(d.source.data.avoidLeft !== undefined && inflection.x < d.source.data.avoidLeft.x + rectW) {
result += ' H' + (d.source.data.avoidLeft.x + rectW + 2 * radius);
result += ' V' + (d.source.data.avoidLeft.y + rectH + radius);
inflection.y = d.source.data.avoidLeft.y + rectH + radius;
}
result += ' H' + (inflection.x + radius);
}
// Curve the line at the bend slightly
result += ' Q' + inflection.x + ',' + inflection.y + ' ' + inflection.x + ',' + (inflection.y + radius);
result += 'V' + target.y;
return result;
};
// END OF LINK FUNC //
const zoomSvg = d3.select('.tree-diagram')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g');
const svg = zoomSvg.append('g')
.attr('transform', 'translate(' + 300 + ',' + 20 + ')');
const attachZoom = d3.select('svg');
attachZoom.call(d3.zoom().on('zoom', () => {
zoomSvg.attr('transform', d3.event.transform)
}))
// ADD ARROW TO THE BOTTOM POINTING TO THE NEXT DECISION.
svg.append("svg:defs")
.selectAll("marker")
.data(["end"]) // Different link/path types can be defined here
.enter()
.append("svg:marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", 0.5)
.attr("markerWidth", 4)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
//necessary so that zoom knows where to zoom and unzoom from
/* zm.translate([350, 20]); */
root = d3.hierarchy(treeData[0], function(d) {
return d.children;
});
root.x0 = 0;
root.y0 = 0;
update(root);
d3.select(".tree-diagram")
.style("height", "1000px");
// END OF DRAW TREEE //
function update(source) {
const treeData = treeMap(root);
// Compute the new tree layout.
var nodes = treeData.descendants(),
links = treeData.links();
// Normalize for fixed-depth.
nodes.forEach(function(d) {
d.y = d.depth * 90;
});
// Update the nodes…
var node = svg.selectAll("g.node")
.data(nodes, function(d) {
return d.data.id || (d.id = ++i);
});
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter()
.append("g")
.attr('transform', 'translate(' + source.x0 + ', ' + source.y0 + ')')
.attr("class", "node")
.on("click", click);
// .on("blur", onNodeBlur);
nodeEnter.append('path')
.attr('d', function(d) {
if (d.data.type === 'decision') {
return 'M 60 0 120 30 60 60 0 30 Z';
} else if (d.data.type === 'action') {
return 'M 0 0 120 0 120 60 0 60 Z';
} else {
return 'M -100 -10 -10 -10 -10 -10 -10 -10Z';
}
})
.attr("stroke-width", 1)
.attr('class', 'myPaths')
.style("fill", function(d) {
return "lightsteelblue";
});
nodeEnter.append("text")
.attr("x", rectW / 2)
.attr("y", rectH / 2)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.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.x) + "," + (d.y) + ")";
});
nodeUpdate.select('path.myPaths')
.attr("d", function(d) {
if (d.data.type === 'decision') {
return 'M 60 0 120 30 60 60 0 30 Z';
} else if (d.data.type === 'action') {
return 'M 0 0 120 0 120 60 0 60 Z';
} else {
return 'M -100 -10 -10 -10 -10 -10 -10 -10Z';
}
});
var nodeExit = node.exit()
.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.x + "," + source.y + ")";
})
.remove();
// Update the links…
var link = svg.selectAll(".link")
.data(links, function(d) {
return d.source.data.id + " " + d.target.data.id;
})
.classed('link1', true);
// Enter any new links at the parent's previous position.
var linkEnter = link.enter()
.insert("g", "g")
.attr("class", "link");
linkEnter.append('path')
.on('click', function(d, i) {
selectedLink = d;
// Use the native SVG interface to get the bounding box to
// calculate the center of the path
var bbox = this.getBBox();
var x;
var y;
if (d.target.x < d.source.x) {
// Child is to the right of the parent
x = bbox.x + bbox.width;
y = bbox.y;
plusButton
.attr('transform', 'translate(' + x + ', ' + y + ')')
.classed('hide', false);
} else {
x = bbox.x;
y = bbox.y;
plusButton
.attr('transform', 'translate(' + x + ', ' + y + ')')
.classed('hide', false);
}
})
.on('blur', function(d, i) {
plusButton
.classed('hide', true);
})
.attr("marker-end", "url(#end)");
// Add Link Texts.
linkEnter.append('text');
// Merge the new and the existing links before setting `d` and `text` on all of them
link = linkEnter.merge(link);
link.select('path')
.attr("d", linkFunc);
link.select('text')
.text(function(d, i) {
if (d.source.x < d.target.x) {
return 'True';
} else {
return 'False';
}
})
.attr('transform', function(d) {
if (d.source.x < d.target.x && d.target.data.type) {
return 'translate(' + (d.target.x + rectW / 2) + ',' + (d.source.y + rectH) + ')';
} else {
return null;
}
});
//LinkUpdate
var linkUpdate = linkEnter.merge(link);
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", linkFunc);
// Transition links to their new position.
// Transition exiting nodes to the parent's new position.
link.exit()
.transition()
.duration(duration)
.attr("d", linkFunc)
.remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
// ON CLICK OF NODES
function click(d) {
if (d.data.type === 'action') {
return;
}
selectedNode = d;
if (!(d.data.children && d.data.children[0] && Object.keys(d.data.children[0]).length)) {
diamondImageFalse
.attr('transform', 'translate(' + (d.x - 20) + ', ' + (d.y + 35) + ')')
.classed('hide', false);
rectangleShapeFalse.attr('transform', 'translate(' + (d.x - 20) + ', ' + d.y + ')').classed('hide', false);
}
if (!(d.data.children && d.data.children[1] && Object.keys(d.data.children[1]).length)) {
diamondImage
.attr('transform', 'translate(' + (d.x + 110) + ', ' + (d.y + 35) + ')')
.classed('hide', false);
rectangleShape.attr('transform', 'translate(' + (d.x + 110) + ', ' + d.y + ')').classed('hide', false);
}
}
// oN CALL
function addElement(d, truthy) {
d.children = null;
d.children = generateEmptyDecisionBox(truthy);
update(root);
}
// draw elements //
function drawDiamond(centroid) {
// Start at the top
var result = 'M' + centroid.x + ',' + (centroid.y - rectH / 2);
// Go right
result += 'L' + (centroid.x + rectW / 2) + ',' + centroid.y;
// Bottom
result += 'L' + centroid.x + ',' + (centroid.y + rectH / 2);
// Left
result += 'L' + (centroid.x - rectW / 2) + ',' + centroid.y;
// Close the shape
result += 'Z';
return result;
}
function drawRect(centroid) {
// Start at the top left
var result = 'M' + (centroid.x - rectW / 2) + ',' + (centroid.y - rectH / 2);
// Go right
result += 'h' + rectW;
// Go down
result += 'v' + rectH;
// Left
result += 'h-' + rectW;
// Close the shape
result += 'Z';
return result;
}
var plusButton = svg
.append('g')
.classed('button', true)
.classed('hide', true)
.on('click', function() {
/* addElement(selectedLink.source); */
removeAllButtonElements();
});
plusButton
.append('rect')
.attr('transform', 'translate(-8, -8)') // center the button inside the `g`
.attr('width', 16)
.attr('height', 16)
.attr('rx', 2);
plusButton
.append('path')
.attr('d', 'M-6 0 H6 M0 -6 V6');
var rectangleShape = svg.append('g')
.classed('conditionImage', true)
.classed('hide', true)
.on('click', function() {
addActionOrDecision(selectedNode, 'action', 'True');
removeAllButtonElements();
});
rectangleShape
.append('rect')
.attr('width', 30)
.attr('height', 20)
.style('fill', 'orange');
var diamondImage = svg.append('g')
.classed('conditionSvg', true)
.classed('hide', true)
.classed('scale', true)
.on('click', function() {
addActionOrDecision(selectedNode, 'decision', 'True');
removeAllButtonElements();
});
diamondImage
.append('path')
.attr('d', 'M 15 0 30 15 15 30 0 15 Z')
.style("fill", 'orange');
var rectangleShapeFalse = svg.append('g')
.classed('conditionImage', true)
.classed('hide', true)
.on('click', function() {
addActionOrDecision(selectedNode, 'action', 'False');
removeAllButtonElements();
});
rectangleShapeFalse
.append('rect')
.attr('width', 30)
.attr('height', 20)
.style('fill', 'orange');
var diamondImageFalse = svg.append('g')
.classed('conditionImage', true)
.classed('hide', true)
.classed('scale', true)
.on('click', function() {
addActionOrDecision(selectedNode, 'decision', 'False');
// addElement(selectedNode, 'False');
removeAllButtonElements();
});
diamondImageFalse
.append('path')
.attr('d', 'M 15 0 30 15 15 30 0 15 Z')
.style("fill", 'orange');
function removeAllButtonElements() {
plusButton.classed('hide', true);
diamondImage.classed('hide', true);
rectangleShape.classed('hide', true);
diamondImageFalse.classed('hide', true);
rectangleShapeFalse.classed('hide', true);
}
function addActionOrDecision(selectedNode, nodeType, conditionType) {
const parentNodeId = selectedNode.parent.data.id;
const selectedNodeId = selectedNode.data.id;
// find the selected node from the actual treeData
const foundRule = getNodeFromNodeId(treeData, selectedNodeId);
const newRuleId = Math.random();
const newNodeToAdd = {
"condition": conditionType,
"name": nodeType === 'decision' ? 'New Decision' : 'New Action',
"type": nodeType,
"value": "",
"id": newRuleId,
"parent": parentNodeId,
"children": [],
};
const clonedNewNode = { ...newNodeToAdd
};
if (conditionType === 'False' && foundRule.children) {
// foundRule.children[0] = newNodeToAdd;
foundRule.children.splice(0, 1, clonedNewNode);
if (!(foundRule.children[1] && Object.keys(foundRule.children[1]))) {
foundRule.children[1] = {};
}
} else {
// foundRule.children[1] = newNodeToAdd;
foundRule.children.splice(1, 1, clonedNewNode);
if (!(foundRule.children[0] && Object.keys(foundRule.children[0]))) {
founRule.children[0] = {};
}
}
// find the node and add a child to it.
updateTree();
}
function updateTree() {
root = d3.hierarchy(treeData[0], function(d) {
return d.children;
});
root.x0 = height / 2;
root.y0 = 0;
update(root);
}
function getNodeFromNodeId(nodes, nodeId) {
for (const node of nodes) {
const currNode = node;
if (currNode) {
if (currNode.id === nodeId) {
return currNode;
} else if (currNode.children) {
const childResult = getNodeFromNodeId(currNode.children, nodeId);
if (childResult) {
return childResult;
}
}
}
}
return null;
}
.node {
cursor: pointer;
outline: none !important;
}
.node text {
font: 10px sans-serif;
}
.button>path {
stroke: blue;
stroke-width: 1.5;
/* outline: none; */
}
.button>rect {
fill: #ddd;
stroke: grey;
stroke-width: 1px;
}
.conditionalSvg {
/* outline: none; */
display: none;
}
.hide {
/* display: none; */
opacity: 0 !important;
/* pointer-events: none; */
}
.link:hover {
outline: none !important;
cursor: pointer;
stroke-width: 3px;
}
.link path {
/* outline: none !important; */
fill: none;
stroke: darkgray;
stroke-width: 2px;
}
.link path:hover {
cursor: pointer;
stroke-width: 4px;
}
.link text {
font: 10px sans-serif;
}
.colorBlue {
background-color: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<div class="tree-diagram">
</div>
EDIT you were right about wrapping the links around the nodes, since adding a child to the overlapping node should always trigger a relayout. I wrapped the link around it roughly, using just straight corners. You could make that smoother using the Q logic around inflections currently contained in linkFunc.

How to Show all the child nodes when there are many

I'm working on a D3 visualization that shows the parent and child relationship.
I'm able to visualize the data perfect when I've less number of children, but when the number of children is more, the child nodes get overlapped. how can I modify my chart so that all the nodes towards the left of the root (right) are visible? like having the nodes in a circular fashion around the root, having some nodes near to the root, and some far in some order. What would bee the best way to show it?
Here is my code.
var data = {
"name": "Root",
"img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png",
"children": [{
"name": "3",
"img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
}, {
"name": "4",
"img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
}, {
"name": "1",
"img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
}, {
"name": "2",
"img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
}, {
"name": "1",
"img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
}, {
"name": "2",
"img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
}, {
"name": "1",
"img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
}, {
"name": "2",
"img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
}, {
"name": "1",
"img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
}, {
"name": "2",
"img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
}, {
"name": "1",
"img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
}, {
"name": "2",
"img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
}, {
"name": "1",
"img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
}, {
"name": "2",
"img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
}, {
"name": "1",
"img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
}, {
"name": "2",
"img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
}],
"parent": [{
"name": "1",
"img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
}]
};
var bgColors = ['#fd90b5', '#6ca1e9', '#fa975c', '#eb7092', '#f88962', '#a094ed', '#7f8de1'];
var dr = 0;
// Left data
var data1 = {
"name": data.name,
"img": data.img,
"children": JSON.parse(JSON.stringify(data.children))
};
// Right data
var data2 = {
"name": data.name,
"img": data.img,
"children": JSON.parse(JSON.stringify(data.parent))
};
// Create d3 hierarchies
var right = d3.hierarchy(data1);
var left = d3.hierarchy(data2);
// Render both trees
drawTree(right, "right")
drawTree(left, "left")
// draw single tree
function drawTree(root, pos) {
var refType;
if (pos == 'left')
refType = 'left';
else
refType = 'right';
var SWITCH_CONST = 1;
if (pos === "left") {
SWITCH_CONST = -1;
}
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height")
var g = svg.append("g").attr("transform", "translate(" + width / 2 + ",0)");
var tree = d3.tree()
.size([height, SWITCH_CONST * (width - 150) / 2]);
tree(root)
var nodes = root.descendants();
var links = root.links();
nodes[0].x = height / 2
// Create links
var link = g.selectAll(".link")
.data(links)
.enter()
link.append("path")
.attr("class", "link")
.attr("d", function(d) {
//first return returns a curve and the second will return straight lines in
//return "M" + d.target.y + "," + d.target.x + "C" + (d.target.y + d.source.y) / 2.5 + "," + d.target.x + " " + (d.target.y + d.source.y) / 2 + "," + d.source.x + " " + d.source.y + "," + d.source.x;
return "M" + d.target.y + "," + d.target.x + "A" + dr + "," + dr + " 1,0 0 " + d.source.y + "," + d.source.x;
});
link.append("text")
.attr("font-family", "Arial, Helvetica, sans-serif")
.attr("fill", "Black")
.style("font", "normal 12px Arial")
.attr("transform", function(d) {
return "translate(" +
((d.source.y + d.target.y) / 2) + "," +
((d.source.x + d.target.x) / 2) + ")";
})
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.data(nodes)
.text(refType);
// Create nodes
var node = g.selectAll(".node")
.data(nodes)
.enter()
.append("g")
.attr("class", function(d) {
return "node" + (d.children ? " node--internal" : " node--leaf");
})
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
})
node.append('circle')
.attr('class', 'icon-wrap')
.attr('x', 0)
.attr('y', 0)
.attr('r', 25)
.style('fill', 'black');
node.append('image')
.attr('href', d => d.data.img)
.attr('x', '-25')
.attr('y', '-25')
.attr('height', '50')
.attr('width', '50');
node.append("text")
.attr("dy", 45)
.style("text-anchor", "middle")
.text(d => d.data.name);
}
.node circle {
fill: #999;
}
.node text {
font: 12px sans-serif;
}
.node--internal circle {
fill: #555;
}
.link {
fill: none;
stroke: #555;
stroke-opacity: 0.4;
stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg width="700" height="400"></svg>
Here is how it looks with less number of children.
Please let me know how can I optimize my code to fit a maximum of 30 child nodes.
Thanks
A possible solution is to create a recursive function which will adjust your root data coordinates in your drawTree function.
Here is an recursive function which will stagger the nodes left and right. Note the comment where it is mentioned which part of the code controls the calculations of the coordinates.
function adjustClashes(data, siblings = 1, index = 1, radius = 20, height = 400) {
//can the node fit in the current x level?
// if not adjust it
let heightneeded = siblings * radius * 2;
if (heightneeded > height) {
// the code in this if statement will control the calculations for your new coordinates
// In the simplest case we adjust the nodes by staggering odd and even nodes
if (index % 2 != 0){
data.y = data.y + (radius * 2)
} else {
data.y = data.y - (radius * 2)
}
}
// if there are children go deeper and perform same adjustment
if (data.children) {
data.children.forEach((f, i) => {
return adjustClashes( f, data.children.length, i )
})
} else {
return;
}
// finally return the data
return data
}
Full snippet:
var data = {
name: "Root",
img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png",
children: [
{
name: "3",
img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
},
{
name: "4",
img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
},
{
name: "1",
img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
},
{
name: "2",
img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
},
{
name: "1",
img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
},
{
name: "2",
img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
},
{
name: "1",
img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
},
{
name: "2",
img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
},
{
name: "1",
img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
},
{
name: "2",
img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
},
{
name: "1",
img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
},
{
name: "2",
img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
},
{
name: "1",
img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
},
{
name: "2",
img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
},
{
name: "1",
img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
},
{
name: "2",
img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
}
],
parent: [
{
name: "1",
img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
}
]
};
var bgColors = [
"#fd90b5",
"#6ca1e9",
"#fa975c",
"#eb7092",
"#f88962",
"#a094ed",
"#7f8de1"
];
var dr = 0;
// Left data
var data1 = {
name: data.name,
img: data.img,
children: JSON.parse(JSON.stringify(data.children))
};
// Right data
var data2 = {
name: data.name,
img: data.img,
children: JSON.parse(JSON.stringify(data.parent))
};
// Create d3 hierarchies
var right = d3.hierarchy(data1);
var left = d3.hierarchy(data2);
// Render both trees
drawTree(right, "right");
drawTree(left, "left");
// draw single tree
function drawTree(root, pos) {
var refType;
if (pos == "left") refType = "left";
else refType = "right";
var SWITCH_CONST = 1;
if (pos === "left") {
SWITCH_CONST = -1;
}
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var g = svg.append("g").attr("transform", "translate(" + width / 2 + ",0)");
var tree = d3.tree().size([height, (SWITCH_CONST * (width - 150)) / 2]);
tree(root);
function adjustClashes(
data,
siblings = 1,
index = 1,
radius = 20,
height = 400
) {
//can the node fit in the current x level?
// if not adjust it
let heightneeded = siblings * radius * 2;
if (heightneeded > height) {
// the code in this if statement will control the calculations for your new coordinates
// In the simplest case we adjust the nodes by staggering odd and even nodes
if (index % 2 != 0) {
data.y = data.y + radius * 2;
} else {
data.y = data.y - radius * 2;
}
}
// if there are children go deeper and perform same adjustment
if (data.children) {
data.children.forEach((f, i) => {
return adjustClashes(f, data.children.length, i);
});
} else {
return;
}
// finally return the data
return data;
}
root = adjustClashes(root);
var nodes = root.descendants();
var links = root.links();
nodes[0].x = height / 2;
// Create links
var link = g.selectAll(".link").data(links).enter();
link
.append("path")
.attr("class", "link")
.attr("d", function (d) {
//first return returns a curve and the second will return straight lines in
//return "M" + d.target.y + "," + d.target.x + "C" + (d.target.y + d.source.y) / 2.5 + "," + d.target.x + " " + (d.target.y + d.source.y) / 2 + "," + d.source.x + " " + d.source.y + "," + d.source.x;
return (
"M" +
d.target.y +
"," +
d.target.x +
"A" +
dr +
"," +
dr +
" 1,0 0 " +
d.source.y +
"," +
d.source.x
);
});
link
.append("text")
.attr("font-family", "Arial, Helvetica, sans-serif")
.attr("fill", "Black")
.style("font", "normal 12px Arial")
.attr("transform", function (d) {
return (
"translate(" +
(d.source.y + d.target.y) / 2 +
"," +
(d.source.x + d.target.x) / 2 +
")"
);
})
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.data(nodes)
.text(refType);
// Create nodes
var node = g
.selectAll(".node")
.data(nodes)
.enter()
.append("g")
.attr("class", function (d) {
return "node" + (d.children ? " node--internal" : " node--leaf");
})
.attr("transform", function (d) {
return "translate(" + d.y + "," + d.x + ")";
});
node
.append("circle")
.attr("class", "icon-wrap")
.attr("x", 0)
.attr("y", 0)
.attr("r", 25)
.style("fill", "black");
node
.append("image")
.attr("href", (d) => d.data.img)
.attr("x", "-25")
.attr("y", "-25")
.attr("height", "50")
.attr("width", "50");
node
.append("text")
.attr("dy", 45)
.style("text-anchor", "middle")
.text((d) => d.data.name);
}
.node circle {
fill: #999;
}
.node text {
font: 12px sans-serif;
}
.node--internal circle {
fill: #555;
}
.link {
fill: none;
stroke: #555;
stroke-opacity: 0.4;
stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg width="700" height="400"></svg>
Update
The following CodePen.io pen shows for example a 3 level staggering of child nodes and even adds a slight margin to space it out more evenly.
To do this simply add margin as a default parameter to the adjustClashes function and change the if statement as follows:
if (heightneeded > height) {
// the code in this if statement will control the calculations for your new coordinates
// In the simplest case we adjust the nodes by staggering odd and even nodes
if (index % 3 == 0) {
data.y = data.y - radius * 2 - margin ;
} else if (index % 3 == 1) {
data.y = data.y;
} else {
data.y = data.y + radius * 2 + margin;
}
}

d3 javascript series chart

I am trying to create this particular d3 application where a series of data can be dynamically displayed like this. Each segment contains two pieces of data.
The first step is to print the circles so there is sufficient space between the series but also the largest circle is always under the smaller circle.
//version 3 -- with correct labels and legend--
http://jsfiddle.net/0ht35rpb/33/
//******version 2 fiddle******
http://jsfiddle.net/1oka61mL/10/
-- How to set the diagonal labels properly - same angles, aligned properly?
-- Add legend?
-- Mask the bottom pointers in an opposite color then continue the line in a different color?
//******Latest Jsfiddle******
http://jsfiddle.net/0ht35rpb/26/
var width = 600;
var height = 400;
var svg = d3.select('svg').attr("width", width).attr("height", height);
//Count
//Checkins
//Popularity
var data = [{
"name": "Twitter",
"items": [{
"id": 0,
"label": "Count",
"value": 200
}, {
"id": 1,
"label": "Checkins",
"value": 1000
}, {
"id": 2,
"label": "Popularity",
"value": 30
}]
}, {
"name": "Facebook",
"items": [{
"id": 0,
"label": "Count",
"value": 500
}, {
"id": 1,
"label": "Checkins",
"value": 300
}, {
"id": 2,
"label": "Popularity",
"value": 740
}]
}, {
"name": "Ebay",
"items": [{
"id": 0,
"label": "Count",
"value": 4000
}, {
"id": 1,
"label": "Checkins",
"value": 1000
}, {
"id": 2,
"label": "Popularity",
"value": 40
}]
}, {
"name": "Foursquare",
"items": [{
"id": 0,
"label": "Count",
"value": 2000
}, {
"id": 1,
"label": "Checkins",
"value": 3000
}, {
"id": 2,
"label": "Popularity",
"value": 4500
}]
}];
var outerRadius = [];
// organise the data.
// Insert indices and sort items in each series
// keep a running total of max circle size in each series
// for later positioning
var x = 0;
var totalWidth = d3.sum(
data.map(function(series) {
series.items.forEach(function(item, i) {
item.index = i;
});
series.items.sort(function(a, b) {
return b.value - a.value;
});
var maxr = Math.sqrt(series.items[0].value);
outerRadius.push(maxr);
x += maxr;
series.xcentre = x;
x += maxr;
return maxr * 2;
})
);
// make scales for position and colour
var scale = d3.scale.linear().domain([0, totalWidth]).range([0, width]);
//var colScale = d3.scale.category10();
function colores_google(n) {
var colores_g = ["#f7b363", "#448875", "#c12f39", "#2b2d39", "#f8dd2f"];
return colores_g[n % colores_g.length];
}
// add a group per series, position the group according to the values and position scale we calculated above
var groups = svg.selectAll("g").data(data);
groups.enter().append("g");
groups.attr("transform", function(d) {
return ("translate(" + d.xcentre + ",0)");
});
// then add circles per series, biggest first as items are sorted
// colour according to index (the property we inserted previously so we can
// keep track of their original position in the series)
var circles = groups.selectAll("circle").data(function(d) {
return d.items;
}, function(d) {
return d.index;
});
circles.enter().append("circle").attr("cy", height / 2).attr("cx", 0);
circles
.attr("r", function(d) {
return Math.sqrt(d.value);
})
.style("fill", function(d) {
return colores_google(d.index);
});
var labelsgroups = svg.selectAll("text").data(data);
labelsgroups.enter().append("text");
labelsgroups
.attr("y", function(d, i) {
d.y = 300;
d.cy = 200;
return 300;
})
.attr("x", function(d) {
d.x = d.xcentre;
d.cx = d.xcentre;
return d.xcentre;
})
.text(function(d) {
return d.name;
})
.each(function(d) {
var bbox = this.getBBox();
d.sx = d.x - bbox.width / 2 - 2;
d.ox = d.x + bbox.width / 2 + 2;
d.sy = d.oy = d.y + 5;
})
.attr("text-anchor", "middle");
var pointersgroups = svg.selectAll("path.pointer").data(data);
pointersgroups.enter().append("path");
pointersgroups
.attr("class", "pointer")
.attr("marker-end", "url(#circ)");
pointersgroups
.attr("d", function(d) {
return "M" + (d.xcentre) + "," + (d.oy - 25) + "L" + (d.xcentre) + "," + (d.sy - 25) + " " + d.xcentre + "," + (d.cy);
})
function fetchValue(items, label) {
for (i = 0; i <= items.length; i++) {
if (items[i].label == label) {
return items[i].value;
}
}
}
function fetchRadius(items, label) {
for (i = 0; i <= items.length; i++) {
if (items[i].label == label) {
return Math.sqrt(items[i].value);
}
}
}
/*
var labels1groups = svg.selectAll(".label1").data(data);
labels1groups.enter().append("text");
labels1groups
.attr("class", "label1")
.attr("y", function(d, i) {
d.y = 100;
d.cy = 100;
return 100;
})
.attr("x", function(d) {
d.x = d.xcentre;
d.cx = d.xcentre+50;
return d.xcentre+50;
})
.text(function(d) {
return fetchValue(d.items, "Count");
})
.attr("transform", function(d, i) {
return "translate(" + (15 * i) + "," + (i * 45) + ") rotate(-45)";
})
.each(function(d) {
var bbox = this.getBBox();
d.sx = d.x - bbox.width / 2 - 2;
d.ox = d.x + bbox.width / 2 + 2;
d.sy = d.oy = d.y ;
})
.attr("text-anchor", "left");
*/
var gridSize = 100;
var labels1groups = svg.selectAll(".label2")
.data(data)
.enter().append("text")
.text(function(d) {
return fetchValue(d.items, "Count");
//return d;
})
.attr("x", function(d, i) {
d.x = i * gridSize + 50;
d.cx = i * gridSize + 50;
return i * gridSize;
})
.attr("y", function(d, i) {
d.y = 105;
d.cy = 50;
return 0;
})
.attr("transform", function(d, i) {
return "translate(" + gridSize / 2 + ", -6)" +
"rotate(-45 " + ((i + 0.5) * gridSize) + " " + (-6) + ")";
})
.each(function(d) {
var bbox = this.getBBox();
d.sx = d.x - bbox.width / 2 - 2;
d.ox = d.x + bbox.width / 2 + 2;
d.sy = d.oy = d.y;
})
.style("text-anchor", "end")
.attr("class", function(d, i) {
return ((i >= 8 && i <= 16) ?
"timeLabel mono axis axis-worktime" :
"timeLabel mono axis");
});
var pointers1groups = svg.selectAll("path.pointer1").data(data);
pointers1groups.enter().append("path");
pointers1groups
.attr("class", "pointer1")
.attr("marker-end", "url(#circ)");
pointers1groups
.attr("d", function(d, i) {
//d.y = outerRadius[i];
//d.y = d.oy - d.cy;
//fetchRadius(d.items, "Count");
//(d.xcentre+100)
// + " " + d.cx + "," + d.cy
//return "M "+ (d.xcentre) +" 25 ,L "+ dist +" 75";
return "M" + (d.xcentre) + "," + (d.y + d.oy - fetchRadius(d.items, "Count") - 10) + "L" + (d.xcentre + 80) + "," + d.cy;
})
//Older Jsfiddle
http://jsfiddle.net/59bunh8u/51/
var rawData = [{
"name": "Twitter",
"items" : [
{
"label" : "15 billion",
"unit" : "per day",
"value" : 1500
},
{
"label" : "450 checkins",
"unit" : "per day",
"value" : 450
}
]
},
{
"name": "Facebook",
"items" : [
{
"label" : "5 billion",
"unit" : "per day",
"value" : 5000
},
{
"label" : "2000 checkins",
"unit" : "per day",
"value" : 2000
}
]
}];
$.each(rawData, function(index, value) {
var total = 0;
var layerSet = [];
var ratios = [25, 100];
$.each(value["items"], function(i, v) {
total += v["value"];
});
value["total"] = total;
});
var w = $this.data("width");
var h = $this.data("height");
var el = $this;
var margin = {
top: 65,
right: 90,
bottom: 5,
left: 150
};
var svg = d3.select(el[0]).append("svg")
.attr("class", "series")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var defs = svg.append("svg:defs");
$.each(rawData, function(i, v) {
circleDraw(i, v["items"]);
});
//calculates where each element should be placed
function calculateDistance (d, i, items) {
var dcx = 0;
for (var k = 0; k < i; k++) {
dcx += Math.sqrt(items[k].value);
}
return dcx + 10 * i;
}
function getPercentage(value, total) {
return ((value / total) * 100);
}
function circleDraw(index, data){
data.sort(function(a, b) {
return parseFloat(b.value) - parseFloat(a.value);
});
var circlelayer = svg.append("g")
.attr("class", "circlelayer");
var circle = circlelayer.selectAll("circle")
.data(data);
circle.enter().append("circle")
.attr("class", function(d, i) {
if (i == 0) {
return "blue";
}
return "gold";
})
.attr("cy", 60)
.attr("cx", function(d, i) {
return calculateDistance(d, index, data);
})
.attr("r", function(d, i) {
return Math.sqrt(d.value);
});
circle.exit().remove();
}
Here is how you could drawn the lines:
groups.append("line")
.attr("class", "pointer1")
.attr("marker-end", "url(#circ)")
.attr("x1", 0)
.attr("y1", height / 2)
.attr("x2", 0)
.attr("y2",height - 10);
var linesG = svg.selectAll(".slines").data(data)
.enter().append("g")
.attr("transform", function(d) {
return ("translate(" + d.xcentre + "," + height/2 +") rotate(-45)");
});
linesG.append("line")
.attr("class", "pointer1")
.attr("marker-end", "url(#circ)")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", 150)
.attr("y2", 0);
linesG.append("text")
.text(function(d) {
return fetchValue(d.items, "Count");
})
.attr("text-anchor", "end")
.attr("y", -5)
.attr("x", 150);
Updated jsfiddle
I've managed to get the diagonal markers and pointers in alignment, pointing to the correct circle colors to represent that set. I am keen to fine tune this chart and have more control over the padding and chart width/height parameters. The chart looks stable but would be keen to test it with different values and sized data sets.
/LATEST/
http://jsfiddle.net/0ht35rpb/33/
var width = 760;
var height = 400;
var svg = d3.select('#serieschart')
.append("svg:svg")
.attr("width", width)
.attr("height", height);
//Count
//Checkins
//Popularity
var data = [{
"name": "Twitter",
"items": [{
"id": 0,
"label": "Count",
"value": 200
}, {
"id": 1,
"label": "Checkins",
"value": 1000
}, {
"id": 2,
"label": "Popularity",
"value": 30
}]
}, {
"name": "Facebook",
"items": [{
"id": 0,
"label": "Count",
"value": 500
}, {
"id": 1,
"label": "Checkins",
"value": 300
}, {
"id": 2,
"label": "Popularity",
"value": 740
}]
}, {
"name": "Ebay",
"items": [{
"id": 0,
"label": "Count",
"value": 4000
}, {
"id": 1,
"label": "Checkins",
"value": 1000
}, {
"id": 2,
"label": "Popularity",
"value": 40
}]
}, {
"name": "Foursquare",
"items": [{
"id": 0,
"label": "Count",
"value": 2000
}, {
"id": 1,
"label": "Checkins",
"value": 3000
}, {
"id": 2,
"label": "Popularity",
"value": 4500
}]
}];
var legend_group = svg.append("g")
.attr("class", "legend")
.attr("width", 80)
.attr("height", 100)
.append("svg:g")
.attr("class", "legendsection")
.attr("transform", "translate(0,30)");
var legend = legend_group.selectAll("circle").data(data[0].items);
legend.enter().append("circle")
.attr("cx", 70)
.attr("cy", function(d, i) {
return 15 * i;
})
.attr("r", 7)
.attr("width", 18)
.attr("height", 18)
.style("fill", function(d, i) {
return colores_google(i);
});
legend.exit().remove();
var legendtext = legend_group.selectAll("text").data(data[0].items);
legendtext.enter().append("text")
.attr("class", "labels")
.attr("dy", function(d, i) {
return 15 * i;
})
.attr("text-anchor", function(d) {
return "start";
})
.text(function(d) {
return d.label;
});
legendtext.exit().remove();
var m = [80, 20, 20, 10];
var w =+ width - m[0];
var h =+ height - m[1];
var chart = svg.append("g")
.attr("class", "serieschart")
.attr("width", w)
.attr("height", h);
var outerRadius = [];
// organise the data.
// Insert indices and sort items in each series
// keep a running total of max circle size in each series
// for later positioning
var x = 0;
var totalWidth = d3.sum(
data.map(function(series) {
series.items.forEach(function(item, i) {
item.index = i;
});
series.items.sort(function(a, b) {
return b.value - a.value;
});
var maxr = Math.sqrt(series.items[0].value);
outerRadius.push(maxr);
x += maxr;
series.xcentre = x;
x += maxr;
return maxr * 2;
})
);
// make scales for position and colour
var scale = d3.scale.linear().domain([0, totalWidth]).range([0, w]);
//var colScale = d3.scale.category10();
function colores_google(n) {
var colores_g = ["#f7b363", "#448875", "#c12f39", "#2b2d39", "#f8dd2f"];
return colores_g[n % colores_g.length];
}
function fetchValue(items, label) {
for (i = 0; i <= items.length; i++) {
if (items[i].label == label) {
return items[i].value;
}
}
}
function fetchRadius(items, label) {
for (i = 0; i <= items.length; i++) {
if (items[i].label == label) {
return Math.sqrt(items[i].value);
}
}
}
// add a group per series, position the group according to the values and position scale we calculated above
var groups = chart.selectAll("g.seriesGroup").data(data);
var newGroups = groups.enter().append("g").attr("class", "seriesGroup");
newGroups.append("text")
.attr("class", "seriesName")
.attr("text-anchor", "middle");
newGroups.append("line")
.attr("class", "seriesName")
.attr("y1", h - 40)
.attr("y2", h / 2);
newGroups.append("text")
.attr("class", "datumValue")
.attr("y", 10)
//.attr("transform", "rotate(-45)")
;
newGroups.append("g").attr("class", "circleGroup");
newGroups.append("g").attr("class", "datumLine")
.append("line")
.attr("class", "datumValue")
.attr("y2", 40);
var focus = "Count";
groups.attr("transform", function(d) {
return "translate(" + scale(d.xcentre) + ",0)";
});
groups.select("text.seriesName")
.text(function(d) {
return d.name;
})
.attr("y", h - 20);
groups.select("text.datumValue")
.text(function(d) {
return fetchValue(d.items, focus);
})
.attr("transform", function(d) {
return "translate(" + ((h / 2) - 20 - scale(fetchRadius(d.items, focus))) + ",20) rotate(-45)";
});
groups.select("line.datumValue")
.attr("y1", function(d) {
return (h / 2) - scale(fetchRadius(d.items, focus));
})
.attr("x2", function(d) {
return (h / 2) - scale(fetchRadius(d.items, focus) + 20);
});
// then add circles per series, biggest first as items are sorted
// colour according to index (the property we inserted previously so we can
// keep track of their original position in the series)
var circles = groups
.select(".circleGroup")
.selectAll("circle").data(function(d) {
return d.items;
}, function(d) {
return d.index;
});
circles.enter().append("circle").attr("cy", h / 2).attr("cx", 0);
circles
.attr("r", function(d) {
return scale(Math.sqrt(d.value));
})
.style("fill", function(d) {
return colores_google(d.index);
});

d3.js Common Trait Chart

http://jsfiddle.net/NYEaX/1791/
In creating a relationship chart - that shows common traits - I am struggling to create the curved arcs that will match the position of the dots.
What is the best way of plotting these arcs so it dips below the horizon?
var data = [{
"userName": "Rihanna",
"userImage": "https://encrypted-tbn1.gstatic.com/images?q=tbn:ANd9GcSTzjaQlkAJswpiRZByvgsb3CVrfNNLLwjFHMrkZ_bzdPOWdxDE2Q",
"userDetails": [{
"Skills & Expertise": [{
"id": 2,
"tag": "Javascript"
}, {
"id": 3,
"tag": "Design"
}],
"Location": [{
"id": 0,
"tag": "London"
}, {
"id": 1,
"tag": "Germany"
}],
"Company": [{
"id": 0,
"tag": "The Old County"
}]
}]
}, {
"userName": "Brad",
"userImage": "https://lh3.googleusercontent.com/-XdASQvEzIzE/AAAAAAAAAAI/AAAAAAAAAls/5vbx7yVLDnc/photo.jpg",
"userDetails": [{
"Skills & Expertise": [{
"id": 0,
"tag": "JAVA"
}, {
"id": 1,
"tag": "PHP"
}, {
"id": 2,
"tag": "Javascript"
}],
"Location": [{
"id": 0,
"tag": "London"
}],
"Company": [{
"id": 0,
"tag": "The Old County"
}, {
"id": 1,
"tag": "Bakerlight"
}]
}]
}]
var viz = d3.select("#viz")
.append("svg")
.attr("width", 600)
.attr("height", 600)
.append("g")
.attr("transform", "translate(40,100)")
var patternsSvg = viz
.append('g')
.attr('class', 'patterns');
function colores_google(n) {
var colores_g = ["#f7b363", "#448875", "#c12f39", "#2b2d39", "#f8dd2f"];
return colores_g[n % colores_g.length];
}
function getRadius(d) {
var count = d.commonTags.split(",").length;
var ratio = count * 2.3;
if (count == 1) {
ratio = 8;
}
return ratio;
}
//create patterns for user images
$.each(data, function(index, value) {
var defs = patternsSvg.append('svg:defs');
defs.append('svg:pattern')
.attr('id', index + "-" + value.userName.toLowerCase())
.attr('width', 1)
.attr('height', 1)
.append('svg:image')
.attr('xlink:href', value.userImage)
.attr('x', 0)
.attr('y', 0)
.attr('width', 75)
.attr('height', 75);
console.log(value.userDetails[0]);
});
//create common data assement
var data1 = [{
"commonLabel": "Groups",
"commonTags": "test1, test2, test3, test4, test5, test6, test7"
}, {
"commonLabel": "Skills & Expertise",
"commonTags": "test1, test2, test3, test1, test2, test3, test1, test2, test3, test1, test2"
}, {
"commonLabel": "Location",
"commonTags": "test1"
}, {
"commonLabel": "Company",
"commonTags": "test1"
}]
//add curved paths
var distanceBetween = 70;
var pathStart = -400;
var path = viz.append("svg:g").selectAll("path")
.data(data1)
path
.enter().append("svg:path")
.attr("class", function(d) {
return "link "
})
path.attr("d", function(d, i) {
var sx = 0;
var tx = 235;
var sy = 120;
var ty = 120;
pathStart += 125;
var dx = 0;
var dy = getRadius(d) + (distanceBetween * i) - pathStart;
var dr = Math.sqrt(dx * dx + dy * dy);
console.log("dy", dy);
return "M" + sx + "," + sy + "A" + dr + "," + dr + " 0 0,1 " + tx + "," + ty;
});
//add curved paths
//create circles to hold the user images
var circle = viz.append("svg:g").selectAll("circle")
.data(data);
//enter
circle
.enter()
.append("svg:circle")
.attr("id", function(d) {
return d.userName;
})
.attr("r", function(d) {
return "30";
})
.attr("cx", function(d, i) {
return "235" * i;
})
.attr("cy", function(d, i) {
return "120";
})
.style("fill", function(d, i) {
return "url(#" + i + "-" + d.userName.toLowerCase() + ")";
})
var distanceBetween = 65;
var circle = viz.append("svg:g").selectAll("circle")
.data(data1);
//enter
circle
.enter()
.append("svg:circle")
.attr("id", function(d) {
return d.commonLabel;
})
.attr("r", function(d) {
return getRadius(d);
})
.attr("cx", function(d, i) {
return 125;
})
.attr("cy", function(d, i) {
return distanceBetween * i;
})
.style("fill", function(d, i) {
return colores_google(i);
});
var text = viz.append("svg:g").selectAll("g")
.data(data1)
text
.enter().append("svg:g");
text.append("svg:text")
.attr("text-anchor", "middle")
.attr("x", "125")
.attr("y", function(d, i) {
return getRadius(d) + 15 + (distanceBetween * i);
})
.text(function(d) {
return d.commonLabel;
})
.attr("id", function(d) {
return "text" + d.commonLabel;
});
var counters = viz.append("svg:g").selectAll("g")
.data(data1)
counters
.enter().append("svg:g");
counters.append("svg:text")
.attr("text-anchor", "middle")
.attr("x", "125")
.attr("y", function(d, i) {
return ((getRadius(d) / 2) + (distanceBetween * i)) - 3;
})
.text(function(d) {
var count = d.commonTags.split(",").length;
if (count > 1) {
return count;
}
})
.attr("id", function(d) {
return "textcount" + d.commonLabel;
});
Live demo:
http://jsfiddle.net/blackmiaool/p58a0w3h/1/
First of all, you should remove all the magic numbers to keep the code clean and portable.
This one, for example:
pathStart += 125;
How to draw arcs correctly is a math problem. The code is as blow:
path.attr("d", function (d, i) {
const sx = 0;
const sy = height/2;
const a=width/2;
const b = ((data1.length-1)/2-i)*distanceBetween;
const c = Math.sqrt(a * a + b * b);
const angle=Math.atan(a/b);
let r;
if(b===0){
r=0;
}else{
r=1/2*c/Math.cos(angle);//also equals c/b*(c/2)
// r=c/b*(c/2);
}
return `M${sx},${sy} A${r},${r} 0 0,${b>0?1:0} ${width},${height/2}`;
});
And the diagram:

D3js - Trying to update links in a Force Layout

I'd like to update links and nodes of a force directed layout at run-time. But the behavior is strange, because sometimes it does not add new links and sometimes it does not remove old links. Do you have any suggestions?
Any help would be appreciated.
Here is my code:
network.js
// Network View size
var width = 1280,
height = 500
var radius = 200;
var robj = 8;
var scaled_radius = d3.scale.linear()
.domain([0, 7000000000000])
.range([10, 40]);
var svg_network = d3.select(document.createElementNS(d3.ns.prefix.svg, "svg"))
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.gravity(1.0)
.distance(100)
.charge(-60)
.size([width, height]);
var timestamp_info = svg_network.append("text")
.attr("dx", 10)
.attr("dy", 10);
function graph_network_start(flowz)
{
/* ------------------------ */
/* DATA PREPROCESSING */
/* ------------------------ */
flowz = network_preprocess(flowz)
/* ------------------------ */
/* NODE POSITIONING */
/* ------------------------ */
nodePositioning(flowz.activeNodes);
var x = d3.scale.linear().domain([0, flowz.activeNodes.length]).range([0, 180]);
// Bind link and node data to DOM elements
var link = svg_network.selectAll(".link").data(flowz.flow, function(d) { return d.source + "-" + d.target; });
var node = svg_network.selectAll("g.node").data(flowz.activeNodes)//, function(d,i) {return i;});
/* ------------------------ */
/* UPDATE LINKS */
/* ------------------------ */
link.exit().transition().duration(10).remove();
link.enter().append("line")
.attr("id",function(d){return d.source.ID + "-" + d.target.ID;})
.attr("class", "link")
.style("opacity",0)
.transition()
.duration(1000)
.style("opacity",1);
/* ------------------------ */
/* UPDATE NODES */
/* ------------------------ */
node.exit().transition().duration(100).remove();
var newNode = node.enter().append("svg:g")
.attr("id", function(d) {return d.ID})
.attr("class", "node")
.call(force.drag).on("click", function(d){
if (nodes[d.ID].name == "Workstations Site 1") {
selectedSite=1; bubble_visualize(selectedTimestamp, selectedSite);showMode(2);
} else if (nodes[d.ID].name == "Workstations Site 2") {
selectedSite=2; bubble_visualize(selectedTimestamp, selectedSite);showMode(2);
} else if (nodes[d.ID].name == "Workstations Site 3") {
selectedSite=3; bubble_visualize(selectedTimestamp, selectedSite);showMode(2);
}})
.on("mouseover", fade(.1)).on("mouseout", fade(1))
newNode.append("circle")
.attr("r", 8) //function(d){return scaled_radius(d.output)})
.style("fill", color)
.style("stroke", "black" )
.style("opacity",0)
.transition()
.duration(1000)
.style("opacity",1);
newNode.append("text")
.attr("dx", robj + 2)
.attr("dy", ".1em")
.text(function(d) { return nodes[d.ID].name })
.style("opacity",0)
.transition()
.duration(1000)
.style("opacity",1);
//if (nodes[d.ID].name == "Workstations Site" || nodes[d.ID].name == "Workstations Site 2" || nodes[d.ID].name == "Workstations Site 3") return nodes[d.ID].name });
/* ------------------------ */
/* UPDATE INFOS */
/* ------------------------ */
timestamp_info.text(function(d) { return new Date(flowz.timestamp).toString()});
force
.nodes(flowz.activeNodes)
.links(flowz.flow)
.on("tick", tick);
force.start();
var linkedByIndex = {};
flowz.flow.forEach(function(d) {
linkedByIndex[d.source + "," + d.target] = 1;
});
function color(d){
return "steelblue"
}
function isConnected(a, b) {
return linkedByIndex[a.index + "," + b.index] || linkedByIndex[b.index + "," + a.index] || a.index == b.index;
}
function tick()
{
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
}
function fade(opacity) {
return function(d) {
node.style("stroke-opacity", function(o) {
thisOpacity = isConnected(d, o) ? 1 : opacity;
this.setAttribute('fill-opacity', thisOpacity);
return thisOpacity;
});
link.style("stroke-opacity", function(o) {
return o.source === d || o.target === d ? 1 : opacity;
});
};
}
function nodePositioning(nodesparam){
for (var i=0; i<nodesparam.length; i++)
{
if(nodes[nodesparam[i].ID].name == "Internet")
{
nodesparam[i].x=width/2;
nodesparam[i].y=30;
}
else if (nodes[nodesparam[i].ID].name == "Firewall")
{
nodesparam[i].x=width/2;
nodesparam[i].y=50;
}
else if (nodes[nodesparam[i].ID].name == "172.0.0.1")
{
nodesparam[i].x=width/2;
nodesparam[i].y=120;
}
else
{
nodesparam[i].x = width/2 + (radius - robj) * Math.cos(Math.PI + i * (Math.PI / nodesparam.length +1)) ;
nodesparam[i].y = height/2 + (radius - robj) * Math.sin(Math.PI + i * (Math.PI / nodesparam.length +1)) ;
}
nodesparam[i].fixed = true;
}
}
function positionX(node, index)
{
if(node.name == "Internet" || node.name == "Firewall" || node.name == "127.0.0.1")
{
return width/2;
}
else
{
return (width/2 + (radius - robj) * Math.cos(Math.PI + index * (Math.PI / nodes.length +1))) ;
}
}
function positionY(node, index)
{
if(node.name == "Internet")
{
return 30;
}
else if (node.name == "Firewall")
{
return 50;
}
else if (node.name == "172.0.0.1")
{
return 120;
}
else
{
return (height/2 + (radius - robj) * Math.sin(Math.PI + index * (Math.PI / nodes.length + 1))) ;;
}
}
function network_preprocess(flowz){
for(var i=+0; i<flowz.flow.length;i++)
{
for(var j=0; j<flowz.activeNodes.length;j++)
{
if(flowz.activeNodes[j].ID == flowz.flow[i].source)
flowz.flow[i].source = j;
if(flowz.activeNodes[j].ID == flowz.flow[i].target)
flowz.flow[i].target = j;
}
}
return flowz
}
}
links.json excerpt
[
{
"timestamp": 1364795760000,
"flow": [
{
"source": 0,
"target": 1,
"value": 15540
}
],
"activeNodes": [
{
"output": 15540,
"ID": 0
},
{
"output": 0,
"ID": 1
}
]
},
{
"timestamp": 1364795880000,
"flow": [
{
"source": 2,
"target": 1,
"value": 2960
},
{
"source": 0,
"target": 1,
"value": 14800
}
],
"activeNodes": [
{
"output": 14800,
"ID": 0
},
{
"output": 0,
"ID": 1
},
{
"output": 2960,
"ID": 2
}
]
}
]
EDIT: 1
I could narrow it down. The links.source and links.target elements are being destroyed by my code. For some reason they are changed from int to objects:
Output at time[0]:
Object {source: 3, target: 4, value: 213143231}
Object {source: 5, target: 4, value: 448560}
and when I go to another dataset and back to time[0]:
Object {source: Object, target: Object, value: "213143231"}
Object {source: Object, target: Object, value: "448560"}
Even the values get changed into strings. Maybe it happens inside of this code excerpt:
function graph_network_start(flowz)
{
flowz = network_preprocess(flowz);
...
var link = svg_network.selectAll(".link").data(flowz.flow, function(d) { console.log(d); return d.source + "-" + d.target; });
var node = svg_network.selectAll("g.node").data(flowz.activeNodes, function(d) {return d.ID})
...
force
.nodes(flowz.activeNodes)
.links(flowz.flow)
.on("tick", tick);
force.start();
function network_preprocess(flowz){
/*
We have to remove not existing nodes and add new nodes to our network nodes
*/
// Remove every node which does not exist anymore
var activeSet = new HashSet();
activeSet.addAll(flowz.activeNodes);
var toRemove = network_nodes.complement(activeSet).values();
for(var i=0; i<toRemove.length; i++)
{
network_nodes.remove(toRemove[i]);
}
// Add new nodes
var activenodes = flowz.activeNodes;
for(var i=0; i<activenodes.length; i++)
{
if (! network_nodes.contains(activenodes[i]) )
{
network_nodes.add(activenodes[i])
}
}
// Order nodes
var ufzuffu = network_nodes.values();
ufzuffu.sort(function(a,b){
var keya = a.ID;
var keyb = b.ID;
if(keya < keyb) return -1;
if(keya > keyb) return 1;
return 0;
});
flowz.activeNodes = ufzuffu;
// Edit links
for(var i=0; i<flowz.flow.length;i++)
{
for(var j=0; j<flowz.activeNodes.length;j++)
{
if(flowz.activeNodes[j].ID == flowz.flow[i].source)
{
flowz.flow[i].source = +j;
continue;
}
if(flowz.activeNodes[j].ID == flowz.flow[i].target)
{
flowz.flow[i].target = +j;
}
}
}
return flowz
}
}
EDIT 2
Well .. looks like I found a workaround:
var link = svg_network.selectAll(".link").data(flowz.flow, function(d) {
if(d.source.hasOwnProperty("ID"))
return d.source.index + "-" + d.target.index;
else
return d.source + "-" + d.target;
});
It works but I still dont know why it randomly(?) generates objects out of integers ..

Categories