Related
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.
I'm trying to build a tree using d3js. I'm having a 2 sided tree with a root in the center. I am showing parents towards the right of the root and showing children towards the left of the root. 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"
}],
"parent": [{
"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"
}]
};
var bgColors = ['#fd90b5', '#6ca1e9', '#fa975c', '#eb7092', '#f88962', '#a094ed', '#7f8de1'];
var dr = 0;
// Left data
var data1 = {
"name": data.name,
"children": JSON.parse(JSON.stringify(data.children))
};
// Right data
var data2 = {
"name": data.name,
"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="800" height="550"></svg>
Here my issue is that I'm unable to get the logo seen in the root node.
Thanks
Despite the img property existing in the data object, it is missing in the two objects you're actually passing to d3.hierarchy(), which are data1 and data2. Therefore, it should be:
var data1 = {
"name": data.name,
"img": data.img,
"children": JSON.parse(JSON.stringify(data.children))
};
The same for data2. Here is your code with those 2 changes:
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"
}],
"parent": [{
"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"
}]
};
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="800" height="550"></svg>
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);
});
My question is further modification of Force directed graph from here. Here from every source to target it has a arrow at the target end this works fine however if we stick to the theory that all circle are with radius 5.
However in my case I have modified the example that all the nodes will be of different radius based on a parameter, so in such scenario the arrow hides behind the target as the radius is big so the path and arrow hides beside, Im not looking for a solution to bring path and arrow in front instead what I'm trying is to find the new point. In layman language I need to subtract the radius unit from the path so that I get a point on the outer circle.
Sample fiddle is HERE
I guess some modification to this code will be required here in tick function
function tick() {
path.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" +
d.source.x + "," +
d.source.y + "A" +
dr + "," + dr + " 0 0,1 " +
d.target.x + "," +
d.target.y;
});
I tried to modify this code as HERE but not a good fix it behaving weird Can anyone please comment how can we calculate this point
This is a "classic" solution (based on this answer):
path.attr("d", function(d) {
diffX = d.target.x - d.source.x;
diffY = d.target.y - d.source.y;
pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY));
offsetX = (diffX * d.target.radius) / pathLength;
offsetY = (diffY * d.target.radius) / pathLength;
var dx = (d.target.x - offsetX) - d.source.x,
dy = (d.target.y - offsetY) - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" +
d.source.x + "," +
d.source.y + "A" +
dr + "," + dr + " 0 0,1 " +
(d.target.x - offsetX) + "," +
(d.target.y - offsetY);
});
The only problem with this math is that the arc starts and ends in the straight line going from the source to the target. That gives the impression that the arrowhead is a little bit "to the right".
Here is the demo:
// get the data
var graph = {
"nodes": [{
"name": "Ilya I",
"group": 0
}, {
"name": "Betty B",
"group": 1
}, {
"name": "Andy N",
"group": 2
}, {
"name": "Harper P",
"group": 3
}, {
"name": "Holly V",
"group": 4
}, {
"name": "Elijah W",
"group": 5
}, {
"name": "Kalvin L",
"group": 6
}, {
"name": "Chris D",
"group": 7
}, {
"name": "Alexa U",
"group": 8
}],
"links": [{
"source": 2,
"target": 5,
"value": 1,
"type": "arrow"
}, {
"source": 2,
"target": 6,
"value": 3,
"type": "arrow"
}, {
"source": 2,
"target": 7,
"value": 1,
"type": "arrow"
}, {
"source": 2,
"target": 8,
"value": 1,
"type": "arrow"
}, {
"source": 3,
"target": 2,
"value": 1,
"type": "arrow"
}, {
"source": 3,
"target": 4,
"value": 1,
"type": "arrow"
}, {
"source": 3,
"target": 6,
"value": 2,
"type": "arrow"
}, {
"source": 3,
"target": 7,
"value": 1,
"type": "arrow"
}, {
"source": 3,
"target": 8,
"value": 3,
"type": "arrow"
}, {
"source": 5,
"target": 5,
"value": 1,
"type": "arrow"
}, {
"source": 6,
"target": 2,
"value": 1,
"type": "arrow"
}]
};
var nodecolor = d3.scale.category20();
var nodes = {};
// Compute the distinct nodes from the links.
var links = graph.links;
var width = 500,
height = 400;
var force = d3.layout.force()
.nodes(graph.nodes)
.links(links)
.size([width, height])
.linkDistance(function(d) {
return 1 / d.value * 250;
})
.charge(-500)
.on("tick", tick)
.start();
// Set the range
var v = d3.scale.linear().range([0, 100]);
// Scale the range of the data
v.domain([0, d3.max(links, function(d) {
return d.value;
})]);
// asign a type per value to encode opacity
links.forEach(function(link) {
if (v(link.value) <= 25) {
link.type = "twofive";
} else if (v(link.value) <= 50 && v(link.value) > 25) {
link.type = "fivezero";
} else if (v(link.value) <= 75 && v(link.value) > 50) {
link.type = "sevenfive";
} else if (v(link.value) <= 100 && v(link.value) > 75) {
link.type = "onezerozero";
}
});
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// build the arrow.
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", 10)
.attr("refY", -1.5)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
// add the links and the arrows
var path = svg.append("svg:g").selectAll("path")
.data(force.links())
.enter().append("svg:path")
.attr("class", function(d) {
return "link " + d.type;
})
.attr("marker-end", "url(#end)");
// define the nodes
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
//.on("click", click)
//.on("dblclick", dblclick)
.call(force.drag);
// add the nodes
node.append("circle")
.attr("r", function(d) {
d.radius = d.group * 5;
return d.radius
})
.style("fill", function(d) {
return nodecolor(d.group);
});
// add the text
node.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text(function(d) {
return d.name;
});
// add the curvy lines
function tick() {
path.attr("d", function(d) {
diffX = d.target.x - d.source.x;
diffY = d.target.y - d.source.y;
pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY));
offsetX = (diffX * d.target.radius) / pathLength;
offsetY = (diffY * d.target.radius) / pathLength;
var dx = (d.target.x - offsetX) - d.source.x,
dy = (d.target.y - offsetY) - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" +
d.source.x + "," +
d.source.y + "A" +
dr + "," + dr + " 0 0,1 " +
(d.target.x - offsetX) + "," +
(d.target.y - offsetY);
});
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
// action to take on mouse click
function click() {
d3.select(this).select("text").transition()
.duration(750)
.attr("x", 22)
.style("fill", "steelblue")
.style("stroke", "lightsteelblue")
.style("stroke-width", ".5px");
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 16)
.style("fill", function(d) {
return nodecolor(d.group);
});
}
// action to take on mouse double click
function dblclick() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 6)
.style("fill", function(d) {
return nodecolor(d.group);
});
d3.select(this).select("text").transition()
.duration(750)
.attr("x", 12)
.style("stroke", "none")
.style("fill", "black")
.style("stroke", "none")
.style("font", "10px sans-serif");
}
path.link {
fill: none;
stroke: #666;
stroke-width: 1.5px;
}
path.link.twofive {
opacity: 0.25;
}
path.link.fivezero {
opacity: 0.50;
}
path.link.sevenfive {
opacity: 0.75;
}
path.link.onezerozero {
opacity: 1.0;
}
circle {
fill: #ccc;
stroke: #fff;
stroke-width: 1.5px;
}
text {
fill: #000;
pointer-events: none;
}
#content {
padding: 7px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
One way would be to show the marker in the middle
doing like this:
.attr("marker-mid", "url(#end)");
instead of
.attr("marker-end", "url(#end)");
working example here
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: