This is the know problem in chrome. I tried multiple solutions.
But my bad luck and understanding i am not able to fix.
Could someone help me. I spent lot of time. could not resolve this. Label this links not working in chrome
var nodes ;
var links;
var linkNodeMap = {};
var width = window.innerWidth-30,
height = window.innerHeight-140,
root = {
"nodes": [
{
"type": "S",
"id": "1",
"name": "1001"
},
{
"type": "S",
"id": "2",
"name": "3"
},
{
"id": "10.10.0.5",
"name": "h5"
},
{
"id": "10.10.0.3",
"name": "h3"
}
],
"links": [
{
"source": 1,
"p1":3,
"p2":4,
"index":0,
"target": 0
},
{
"source": 1,
"p1":5,
"p2":6,
"index":0,
"target": 0
},
{
"source": 1,
"p1":3,
"p2":4,
"index":0,
"target": 2
},
{
"source": 1,
"p1":3,
"p2":4,
"index":0,
"target": 3
}
]
};
document.getElementById("refresh").disabled = false;
var force = d3.layout.force()
.linkDistance(100)
.charge(-120)
.gravity(.03)
.size([width, height])
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.style("border", "1px solid black")
.attr("height", height);
var g = svg.append("g");
var link = g.selectAll("path");
var node = svg.selectAll(".node");
var sourcePortText = g.selectAll("text.label");
var destinationPortText = g.selectAll("text.label");
function updateLinkCount (link) {
var linkcount = getLinkCount(link);
if(linkcount == undefined)
linkcount = 0;
linkcount = linkcount + 1;
link.index = linkcount;
linkNodeMap[getKey(link)] = linkcount;
}
function getLinkCount(link) {
isAvaiable = linkNodeMap[link.source.id + '-' + link.target.id];
if(isAvaiable != undefined)
return isAvaiable
return linkNodeMap[link.target.id + '-' + link.source.id];
}
function getKey(link) {
isAvaiable = linkNodeMap[link.source.id + '-' + link.target.id];
if(isAvaiable != undefined)
return link.source.id + '-' + link.target.id
return link.target.id + '-' + link.source.id;
}
function update() {
nodes = root.nodes;
links = root.links;
// Restart the force layout.
force
.nodes(nodes)
.links(links)
.start();
// Update links.
link = link.data(links, function(d) {
return d.source.id + '-' + d.p1 + '-' + d.target.id + '-' +d.p2; });
link.exit().remove();
link.enter().append("path")
.attr("class", "link")
.attr("id", function(d) {
return d.source.id + '-' + d.p1 + '-' + d.target.id + '-' +d.p2; });
links.forEach(function(link) {
updateLinkCount(link);
});
// Update nodes.
node = node.data(nodes, function(d) { return d.id; });
node.exit().remove();
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.call(force.drag);
nodeEnter.append("circle")
.attr("r", 20)
.attr("id", function(d) { return d.id; });
nodeEnter.append("text")
.attr("dy", ".35em")
.text(function(d) { return d.name; });
node.select("circle")
.style("fill", color);
node.select("text")
.text(function(d) { return d.name; });
sourcePortText = sourcePortText.data(links, function(d) {
return d.source.id + '-' + d.p1 + '-' + d.target.id + '-' +d.p2;})
sourcePortText.enter().append("text")
.attr("dy", 4)
.attr("font-size", 10)
.attr("fill", "black")
.append("textPath")
.attr("startOffset","25%")
.attr("class", "textPath")
.attr("xlink:href", function(d) { return '#' + d.source.id + '-' + d.p1 + '-' + d.target.id + '-' +d.p2;})
.text(function(d) { return d.p1 });
sourcePortText.exit().remove();
destinationPortText = destinationPortText.data(links, function(d) {
return d.source.id + '-' + d.p1 + '-' + d.target.id + '-' +d.p2;})
destinationPortText.enter().append("text")
.attr("class", "label")
.attr("dy", 4)
.attr("font-size", 10)
.attr("fill", "black")
.append("textPath")
.attr("class", "textPath")
.attr("startOffset","75%")
.attr("xlink:href", function(d) { return '#' + d.source.id + '-' + d.p1 + '-' + d.target.id + '-' +d.p2;})
.text(function(d) { return d.p2;});
destinationPortText.exit().remove();
}
function tick() {
link.attr("d", linkArc);
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
sourcePortText.attr("x",function(d){ return xpos(d.source, d.target); });
sourcePortText.attr("y",function(d){ return ypos(d.source, d.target); });
svg.selectAll(".textPath").attr("xlink:href",
function(d) {
return "#"+d.source.id + '-' + d.p1 + '-' + d.target.id + '-' +d.p2;
})
destinationPortText.attr("x",function(d){ return xpos(d.target, d.source); });
destinationPortText.attr("y",function(d){ return ypos(d.target, d.source); });
}
function linkArc(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = 0;
var linkCount = getLinkCount(d);
if(linkCount > 1) {
dr = Math.sqrt(dx * dx + dy * dy);
// if there are multiple links between these two nodes, we need generate different dr for each path
dr = dr/(1 + (1/linkCount) * (linkCount - d.index));
}
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
}
function xpos(s, t) {
var angle = Math.atan2(t.y - s.y, t.x - s.x);
return 35 * Math.cos(angle) + s.x;
};
function ypos(s, t) {
var angle = Math.atan2(t.y - s.y, t.x - s.x);
return 35 * Math.sin(angle) + s.y;
};
function color(d) {
return d.type == 'S' ? "#c6dbef": "#fd8d3c";
}
function refresh() {
linkNodeMap = {};
update();
}
update();
My fildle:
http://jsfiddle.net/pkolanda/Lmdag990/6/
Related
I have a d3 chart that have both left and right paths/nodes. now what i'm trying to do is that on click of a node i want to append the same data ( same tree with left and right nodes ) and this new tree will be populated/centerd according to the clicked nodes x and y values, so i tried to add a new g with the x and y values i got from the object clicked.
like this
var g = svg.append("g")
.attr("transform", "translate(" + d.x * 2 + "," + d.y + ")");
drawTree2(left, "left", d);
drawTree2(right, "right", d);
but its not working, please help
var data = {
"name": "Root",
"children": [{
"name": "Branch 1"
}, {
"name": "Branch 2",
}, {
"name": "Branch 3"
}, {
"name": "Branch 4",
}, {
"name": "Branch 5"
},
{
"name": "Branch 6"
}, {
"name": "Branch 7",
}, {
"name": "Branch 8"
}, {
"name": "Branch 9",
}, {
"name": "Branch 10"
}
]
};
var split_index = Math.round(data.children.length / 2)
// Left data
var data1 = {
"name": data.name,
"children": JSON.parse(JSON.stringify(data.children.slice(0, split_index)))
};
// Right data
var data2 = {
"name": data.name,
"children": JSON.parse(JSON.stringify(data.children.slice(split_index)))
};
// Create d3 hierarchies
var right = d3.hierarchy(data1);
var left = d3.hierarchy(data2);
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var g = svg.append("g")
.attr("transform", "translate(" + width / 2 + ",0)");
// Render both trees
drawTree(right, "right")
drawTree(left, "left")
function drawTree(root, pos) {
var SWITCH_CONST = 1;
if (pos === "left") {
SWITCH_CONST = -1;
}
// Create new default tree layout
var 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, SWITCH_CONST * (width - 150) / 2]);
tree(root)
var nodes = root.descendants();
var links = root.links();
// Set both root nodes to be dead center vertically
nodes[0].x = height / 2
// Create links
var link = g.selectAll(".link." + pos)
.data(links)
.join(
enter => enter.append("path"),
update => update,
exit => exit.remove()
)
.attr("class", "link " + pos)
.attr("d", function(d) {
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;
});
// Create nodes
var node = g.selectAll(".node." + pos)
.data(nodes)
.join(
enter => {
const n = enter
.append("g")
.on("click", (e, d) => {
drawSecondTree(d);
});
n.append("circle").attr("r", 2.5);
n.append("text").attr("y", -10).style("text-anchor", "middle");
return n;
},
update => update,
exit => exit.remove()
)
.attr("class", function(d) {
return "node " + pos + (d.children ? " node--internal" : " node--leaf");
})
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
})
.select("text")
.text(function(d) {
return d.data.name
});
}
function drawSecondTree(d) {
var g = svg.append("g")
.attr("transform", "translate(" + d.x * 2 + "," + d.y + ")");
drawTree2(left, "left", d);
drawTree2(right, "right", d);
function drawTree2(root, pos, d) {
console.log(d.x, d.y);
//return false;
var SWITCH_CONST = 1;
if (pos === "left") {
SWITCH_CONST = -1;
}
// Create new default tree layout
var 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, SWITCH_CONST * (width - 150) / 2]);
tree(root)
var nodes = root.descendants();
var links = root.links();
// Set both root nodes to be dead center vertically
nodes[0].x = d.y;
// Create links
var link = g.selectAll(".link." + pos)
.data(links)
.join(
enter => enter.append("path"),
update => update,
exit => exit.remove()
)
.attr("class", "link " + pos)
.attr("d", function(d) {
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;
});
// Create nodes
var node = g.selectAll(".node." + pos)
.data(nodes)
.join(
enter => {
const n = enter
.append("g")
.on("click", (e, d) => toggle(d, pos, pos === "left" ? left : right));
n.append("circle").attr("r", 2.5);
n.append("text").attr("y", -10).style("text-anchor", "middle");
return n;
},
update => update,
exit => exit.remove()
)
.attr("class", function(d) {
return "node " + pos + (d.children ? " node--internal" : " node--leaf");
})
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
})
.select("text")
.text(function(d) {
return 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/6.2.0/d3.min.js"></script>
<svg width="900" height="600"></svg>
I think I understand what you're trying to get at.
It's hard to explain because the way this is setup is a bit confusing. Basically, the main issue is the way you are creating the new g element's "center point" (the translate(x,y) part). The reason it's confusing is because you are switching X and Y coordinates in certain places. Maybe you can't get around that with how you want the map to look, which is fine, it's just hard to follow along.
I made the following changes in the drawSecondTree function (note the "added" and "updated" items):
function drawSecondTree(d) {
var gX = (width / 2) + d.y; // ********** added
var gY = d.x - gX; // ********** added
var g = svg.append("g")
.attr("transform", "translate(" + gX + "," + gY + ")"); // ********** updated
drawTree2(left, "left", d);
drawTree2(right, "right", d);
function drawTree2(root, pos, d) {
console.log(d.x, d.y);
//return false;
var SWITCH_CONST = 1;
if (pos === "left") {
SWITCH_CONST = -1;
}
// Create new default tree layout
var 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, SWITCH_CONST * (width - 150) / 2]);
tree(root)
var nodes = root.descendants();
var links = root.links();
// Set both root nodes to be dead center vertically
// nodes[0].x = d.y;
nodes[0].x = (width / 2) + d.y; // ********** updated
// Create links
var link = g.selectAll(".link." + pos)
.data(links)
.join(
enter => enter.append("path"),
update => update,
exit => exit.remove()
)
.attr("class", "link " + pos)
.attr("d", function(d) {
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;
});
// Create nodes
var node = g.selectAll(".node." + pos)
.data(nodes)
.join(
enter => {
const n = enter
.append("g")
.on("click", (e, d) => toggle(d, pos, pos === "left" ? left : right));
n.append("circle").attr("r", 2.5);
n.append("text").attr("y", -10).style("text-anchor", "middle");
return n;
},
update => update,
exit => exit.remove()
)
.attr("class", function(d) {
return "node " + pos + (d.children ? " node--internal" : " node--leaf");
})
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
})
.select("text")
.text(function(d) {
return d.data.name
});
}
}
And here's a codepen of the full code working (with overlap of the nodes).
I would also recommend looking into using the viewBox of the svg element and figuring out how to zoom out when you add more nodes by clicking on them.
I am trying to recreate this example:
http://bl.ocks.org/cingraham/7663357#index.html
The problem is my results are as shown in the picture.
The error I get is:
TypeError: theState[0][0] is null
But since I am just copying the code as is why am I even getting this error? and can anyone help me figure out how to fix it?
Thanks
Edited:
It is here (below) where the code is creating problems...
Specifically not entering .attr("stroke-width", function(d, i) {.
function clicked(selected) {
var selname = selected.id;
var homex = path.centroid(selected)[0];
var homey = path.centroid(selected)[1];
g.selectAll(".goingline")
.attr("stroke-dasharray", 0)
.remove()
g.selectAll(".goingline")
.data(going)
.enter().append("path")
.attr("class", "goingline")
.attr("d", function(d, i) {
var abb = d.abbrev;
var finalval = coming[i][selname] - going[i][selname];
var theState = d3.select("#" + abb);
if (!isNaN(finalval)) {
var startx = path.centroid(theState[0][0].__data__)[0];
var starty = path.centroid(theState[0][0].__data__)[1];
if (finalval > 0) {
return "M" + startx + "," + starty + " Q" + (startx + homex) / 2 + " " + (starty + homey) / 1.5 + " " + homex + " " + homey;
}
else {
return "M" + homex + "," + homey + " Q" + (startx + homex) / 2 + " " + (starty + homey) / 2.5 + " " + startx + " " + starty;
}
}
})
.call(transition)
.attr("stroke-width", function(d, i) {
var finalval = coming[i][selname] - going[i][selname]; console.log("test1")
return lineSize(parseFloat(Math.abs(finalval)));
})
.attr("stroke", function(d, i) {
var finalval = coming[i][selname] - going[i][selname]; console.log("test1")
if (finalval > 0) {
return "#65a89d";
}
else {
return "#a96a46";
}
})
.attr("fill", "none")
.attr("opacity", 0.5)
.attr("stroke-linecap", "round")
.on("mouseover", function(d){
return toolOver2(d, this);
})
.on("mousemove", function(d, i){
var m = d3.mouse(this);
mx = m[0];
my = m[1];
return toolMove2(mx, my, selname, d.state, coming[i][selname], going[i][selname]);
})
.on("mouseout", function(d) {
return toolOut2(d, this);
});
}
When I'm working with a tree graph in d3, I get the behavior I want up through adding a node in a tree graph. However, when I'm adding a node to the graph, the node (and path) get placed, but the nodes aren't balanced out and the previous nodes do not move.
I've posted a working version of the entire code here: https://codepen.io/auser/pen/mwwVJL
The JS code (for completeness sake is):
const pathGraph = (eleName, treeData, opts = {}) => {
var margin = { top: 40, right: 90, bottom: 50, left: 90 },
width = 660 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
const treemap = d3.tree().size([width, height]);
const svg = d3
.select(eleName)
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
let g = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
const updateData = newData => {
// assigns the data to a hierarchy using parent-child relationships
let nodes = treemap(d3.hierarchy(newData));
const color = d3.scaleOrdinal(d3.schemeCategory10).domain(d3.range(0, 8));
// adds the links between the nodes
const link = g
.selectAll(".link")
.data(nodes.descendants().slice(1))
.enter()
.append("path")
.attr("class", "link")
.style("stroke-width", 1)
.attr("d", function(d) {
return (
"M" + d.x + "," + d.y + "C" + d.x + "," + (d.y + d.parent.y) / 2 + " " + d.parent.x + "," + (d.y + d.parent.y) / 2 + " " + d.parent.x + "," + d.parent.y
);
});
link.exit().remove();
// adds each node as a group
const node = g
.selectAll(".node")
.data(nodes.descendants())
.enter()
.append("g")
.attr("class", function(d) {
return "node" + (d.children ? " node--internal" : " node--leaf");
})
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
node.exit().remove();
const parentTree = (d) => {
let nodeLinks = []
while(d.parent) {
nodeLinks.push(d)
d = d.parent
}
nodeLinks = nodeLinks.concat(d)
return nodeLinks
}
const activeLink = (d, o) => {
if (d === o || d.parent === o) return true;
}
node
.on('mouseover', function(d) {
const data = d3.select(this)
const linkedNodes = parentTree(d)
link
.style('stroke-width', o => activeLink(d, o) ? 4 : 1)
.style('stroke', o => activeLink(d, o) ? 'red' : '#333')
.transition(500)
rect
.style('stroke-width', o => activeLink(d, o) ? 4 : 1)
.transition(500)
})
.on('mouseout', d => {
const data = d3.select(this);
// console.log('d ->', d)
link
.style('stroke-width', 1)
.style('stroke', '#333')
rect
.style('stroke-width', 1)
})
// adds the circle to the node
const rect = node
.append("rect")
.attr("height", 50)
.attr("width", 50)
.style("fill", (d, i) => color(i))
.attr("x", "-0.7em");
// adds the text to the node
node
.append("text")
.attr("dy", ".52em")
.attr("y", function(d) {
return d.children ? -18 : 20;
})
.attr("dx", "-.2em")
.style("text-anchor", "middle")
.text(function(d) {
return d.data.name;
});
};
updateData(treeData);
return updateData;
};
const data = {
"name": "Root",
"children": [
{
"name": "A",
"children": [
{ "name": "B" },
{ "name": "C" }
]
},
{ "name": "D" },
{ "name": "E",
"children":[
{ "name": "F"}
] }
]
};
const mount = document.querySelector('#treea')
const updateData = pathGraph(mount, data)
setTimeout(function() {
data.children[2].children.push({ name: "H" })
updateData(data, { update: true })
}, 2000)
.tree .node rect, .tree .node circle {
fill: blue;
rounding: 5px;
}
.tree .link {
fill: none;
stroke: #222;
stroke-opacity: 1;
stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<svg id="treea" class="tree"></svg>
Any help would be greatly appreciated...
You need a proper "enter", "update" and "exit" selections. Like this:
//this is the update selection
const link = g
.selectAll(".link")
.data(nodes.descendants().slice(1));
//this is the enter selection, up to the 'merge'
link.enter()
.append("path")
.attr("class", "link")
.merge(link)//from now one, update + enter
.style("stroke-width", 1)
.attr("d", function(d) {
return (
"M" + d.x + "," + d.y + "C" + d.x + "," +
(d.y + d.parent.y) / 2 + " " + d.parent.x + "," +
(d.y + d.parent.y) / 2 + " " + d.parent.x + "," + d.parent.y
);
});
//this is the exit selection
link.exit().remove();
Here is the updated Codepen: https://codepen.io/anon/pen/RggVPO?editors=0010
And here the Stack snippet:
const pathGraph = (eleName, treeData, opts = {}) => {
var margin = { top: 40, right: 90, bottom: 50, left: 90 },
width = 660 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
const treemap = d3.tree().size([width, height]);
const svg = d3
.select(eleName)
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
let g = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
const updateData = newData => {
// assigns the data to a hierarchy using parent-child relationships
let nodes = treemap(d3.hierarchy(newData));
const color = d3.scaleOrdinal(d3.schemeCategory20).domain(d3.range(0, 8));
// adds the links between the nodes
const link = g
.selectAll(".link")
.data(nodes.descendants().slice(1));
link.enter()
.append("path")
.attr("class", "link")
.merge(link)
.style("stroke-width", 1)
.attr("d", function(d) {
return (
"M" + d.x + "," + d.y + "C" + d.x + "," + (d.y + d.parent.y) / 2 + " " + d.parent.x + "," + (d.y + d.parent.y) / 2 + " " + d.parent.x + "," + d.parent.y
);
}).lower();
link.exit().remove();
const node = g
.selectAll(".node")
.data(nodes.descendants())
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
const nodeEnter = node.enter()
.append("g")
.attr("class", function(d) {
return "node " + (d.children ? " node--internal" : " node--leaf");
})
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
node.exit().remove();
const parentTree = (d) => {
let nodeLinks = []
while(d.parent) {
nodeLinks.push(d)
d = d.parent
}
nodeLinks = nodeLinks.concat(d)
return nodeLinks
}
const activeLink = (d, o) => {
if (d === o || d.parent === o) return true;
}
node
.on('mouseover', function(d) {
const data = d3.select(this)
const linkedNodes = parentTree(d)
link
.style('stroke-width', o => activeLink(d, o) ? 4 : 1)
.style('stroke', o => activeLink(d, o) ? 'red' : '#333')
.transition(500)
rect
.style('stroke-width', o => activeLink(d, o) ? 4 : 1)
.transition(500)
})
.on('mouseout', d => {
const data = d3.select(this);
// console.log('d ->', d)
link
.style('stroke-width', 1)
.style('stroke', '#333')
rect
.style('stroke-width', 1)
})
// adds the circle to the node
const rect = nodeEnter
.append("rect")
.attr("height", 50)
.attr("width", 50)
.style("fill", (d, i) => color(i))
.style('padding', 5)
.attr("rx", 6)
.attr("ry", 6)
.attr("x", "-0.7em");
// adds the text to the node
nodeEnter
.append("text")
.attr("dy", "0.8em")
.attr("y", function(d) {
return d.children ? -18 : 20;
})
.attr("dx", "0.8em")
.style("text-anchor", "middle")
.text(function(d) {
return d.data.name;
});
node.select("rect").attr("x", "-0.7em");
};
updateData(treeData);
return updateData;
};
const data = {
"name": "Root",
"children": [
{
"name": "A",
"children": [
{ "name": "B" },
{ "name": "C" }
]
},
{ "name": "D" },
{ "name": "E",
"children":[
{ "name": "F"}
] }
]
};
const mount = document.querySelector('#treea')
const updateData = pathGraph(mount, data)
setTimeout(function() {
data.children[2].children.push({ name: "H" })
updateData(data, { update: true })
}, 2000)
.tree .node rect, .tree .node circle {
fill: blue;
rounding: 5px;
}
.tree .link {
fill: none;
stroke: #222;
stroke-opacity: 1;
stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<svg id="treea" class="tree"></svg>
PS: Things are more complicated to the nodes, because you have groups with rectangles and texts. I quickly changed the selections, but I suggest you to refactor that part of the code accordingly.
Does anyone have any suggestions for formatting and feeding underlying data using Tableau's getData() function into this D3 Doughnut Chart? I understand I need to create an array for the underlying data but am unsure how to do so. The underlying data is in this JSON format:
[[
{
"value":"Thais Sissman",
"formattedValue":"Thais Sissman"
},
{
"value":"-0.6860335195530726",
"formattedValue":"-68.6%"
},
{
"value":"-3.3156",
"formattedValue":"($3)"
},
{
"value":"793",
"formattedValue":"793"
},
{
"value":"4.833000000000001",
"formattedValue":"$5"
}
],
[
{
"value":"Lela Donovan",
"formattedValue":"Lela Donovan"
},
{
"value":"0.0875",
"formattedValue":"8.8%"
},
{
"value":"0.4641",
"formattedValue":"$0"
},
{
"value":"792",
"formattedValue":"792"
},
{
"value":"5.304",
"formattedValue":"$5"
}
]]
Here is my JavaScript code:
$(document).ready(function initViz() {
var containerDiv = document.getElementById("vizContainer"),
url = "https://SERVER-NAME/t/gfm/views/Superstore/Customers?:embed=y&:showShareOptions=true&:display_count=no&:showVizHome=no",
options = {
hideTabs: true,
hideToolbar: true,
onFirstInteractive: function () {
document.getElementById('getData').disabled = false; // Enable our button
}
};
viz = new tableau.Viz(containerDiv, url, options);
});
//when viz is loaded (onFirstInteractive), request data
function getSummaryData() {
options = {
maxRows: 0, // Max rows to return. Use 0 to return all rows
ignoreAliases: false,
ignoreSelection: true,
includeAllColumns: false
};
sheet = viz.getWorkbook().getActiveSheet();
//if active tab is a worksheet, get data from that sheet
if (sheet.getSheetType() === 'worksheet') {
sheet.getSummaryDataAsync(options).then(function (t) {
buildMenu(t);
});
//if active sheet is a dashboard get data from a specified sheet
} else {
worksheetArray = viz.getWorkbook().getActiveSheet().getWorksheets();
for (var i = 0; i < worksheetArray.length; i++) {
worksheet = worksheetArray[i];
sheetName = worksheet.getName();
if (sheetName == 'CustomerRank') {
worksheetArray[i].getSummaryDataAsync(options).then(function (t) {
var data = t.getData();
var columns = t.getColumns();
table = t;
var tgt = document.getElementById("dataTarget");
tgt.innerHTML = "<h4>Underlying Data:</h4><p>" + JSON.stringify(table.getData()) + "</p>";
buildVisual(t);
});
}
}
}
}
function buildVisual(table) {
//the data returned from the tableau API
var columns = table.getColumns();
var data = table.getData();
//convert to field:values convention
function reduceToObjects(cols,data) {
var fieldNameMap = $.map(cols, function(col) { return col.getFieldName(); });
var dataToReturn = $.map(data, function(d) {
return d.reduce(function(memo, value, idx) {
memo[fieldNameMap[idx]] = value.value; return memo;
}, {});
});
return dataToReturn;
}
var niceData = reduceToObjects(columns, data);
var svg = d3.select('body')
.append('svg')
.attr('width', 400)
.attr('height', 200)
.attr('id', 'chart');
// add an SVG group element for each user
var series = svg.selectAll('g.series')
.data(d3.keys(dataTarget))
.enter()
.append('g')
.attr('class', 'series');
var width = 300,
height = 300,
radius = Math.min(width, height) / 2;
var color = d3.scale.category20();
var pie = d3.layout.pie()
.sort(null);
var piedata = pie(niceData.data);
var arc = d3.svg.arc()
.innerRadius(radius - 100)
.outerRadius(radius - 50);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll("path")
.data(piedata)
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc);
svg.selectAll("text").data(piedata)
.enter()
.append("text")
.attr("text-anchor", "middle")
.attr("x", function(d) {
var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
d.cx = Math.cos(a) * (radius - 75);
return d.x = Math.cos(a) * (radius - 20);
})
.attr("y", function(d) {
var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
d.cy = Math.sin(a) * (radius - 75);
return d.y = Math.sin(a) * (radius - 20);
})
.text(function(d) { return d.value; })
.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;
});
svg.append("defs").append("marker")
.attr("id", "circ")
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("refX", 3)
.attr("refY", 3)
.append("circle")
.attr("cx", 3)
.attr("cy", 3)
.attr("r", 3);
svg.selectAll("path.pointer").data(piedata).enter()
.append("path")
.attr("class", "pointer")
.style("fill", "none")
.style("stroke", "black")
.attr("marker-end", "url(#circ)")
.attr("d", function(d) {
if(d.cx > d.ox) {
return "M" + d.sx + "," + d.sy + "L" + d.ox + "," + d.oy + " " + d.cx + "," + d.cy;
} else {
return "M" + d.ox + "," + d.oy + "L" + d.sx + "," + d.sy + " " + d.cx + "," + d.cy;
}
});
}
I'm working on a pie chart mock. That I need to try and match the designs to have the label extruding out with a horizontal line attached to the slice ticks. Is this possible? It would be a bonus to have the black dots form on the segments.
http://jsfiddle.net/BxLHd/15/
Here is the code for the tick marks. Would it be a case of creating another set of lines that intersect?
//draw tick marks
var label_group = d3.select('#'+pieId+' .label_group');
lines = label_group.selectAll("line").data(filteredData);
lines.enter().append("svg:line")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", function(d){
if(d.value > threshold){
return -that.r-3;
}else{
return -that.r;
}
})
.attr("y2", function(d){
if(d.value > threshold){
return -that.r-8;
}
else{
return -that.r;
}
})
.attr("stroke", "gray")
.attr("transform", function(d) {
return "rotate(" + (d.startAngle+d.endAngle)/2 * (180/Math.PI) + ")";
});
lines.transition()
.duration(this.tweenDuration)
.attr("transform", function(d) {
return "rotate(" + (d.startAngle+d.endAngle)/2 * (180/Math.PI) + ")";
});
lines.exit().remove();
Here's a proof of concept (using a different example than yours as a basis as there's quite a lot of code in yours). This is the basic approach:
For each label, compute the start and end of the line underneath it. This is done by drawing the label and getting its bounding box.
This gives two points on the pointer path, the third is the center of the respective segment. This is computed while computing the positions of the labels.
These three points become part of the data. Now draw paths for each of the data elements, using the three points computed before.
Add an SVG marker at the end of each path for the dot.
Here's the code to do it, step by step.
.attr("x", function(d) {
var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
d.cx = Math.cos(a) * (radius - 75);
return d.x = Math.cos(a) * (radius - 20);
})
.attr("y", function(d) {
var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
d.cy = Math.sin(a) * (radius - 75);
return d.y = Math.sin(a) * (radius - 20);
})
This is computing the x and y positions of the labels outside the segments. We also compute the position of the final point of the pointer path, in the center of the segment. That is, both in the middle between start and end angle and between inner and outer radii. This is added to the data.
.text(function(d) { return d.value; })
.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;
});
After adding the text label (in this case, simply the value), we get for each the bounding box and compute the remaining two points for the path, just below the text to the left and just below to the right.
svg.selectAll("path.pointer").data(piedata).enter()
.append("path")
.attr("class", "pointer")
.style("fill", "none")
.style("stroke", "black")
.attr("marker-end", "url(#circ)")
.attr("d", function(d) {
if(d.cx > d.ox) {
return "M" + d.sx + "," + d.sy + "L" + d.ox + "," + d.oy + " " + d.cx + "," + d.cy;
} else {
return "M" + d.ox + "," + d.oy + "L" + d.sx + "," + d.sy + " " + d.cx + "," + d.cy;
}
});
Now we can actually add the paths. They are a straightforward connection of the three points computed before, with a marker added at the end. The only thing to watch out for is that, depending on whether the label is on the left or the right of the chart, the path needs to start at the lower left of the label or the lower right. This is the if statement here.
Complete demo here.
Here is the plugin code that should allow multiple instances of the pie chart - along with being able to update each pie chart with a new set of data.
I am open to ways to enhance the code. I feel it still looks a bit bulky - especially the way I am reseting the selector on update. Any suggestions to streamline this?
http://jsfiddle.net/Qh9X5/1318/
$(document).ready(function() {
(function( $ ){
var methods = {
el: "",
init : function(options) {
var clone = jQuery.extend(true, {}, options["data"]);
methods.el = this;
methods.setup(clone);
},
setup: function(dataset){
this.width = 300;
this.height = 300;
this.radius = Math.min(this.width, this.height) / 2;
this.color = d3.scale.category20();
this.pie = d3.layout.pie()
.sort(null);
this.arc = d3.svg.arc()
.innerRadius(this.radius - 100)
.outerRadius(this.radius - 50);
this.svg = d3.select(methods.el["selector"]).append("svg")
.attr("width", this.width)
.attr("height", this.height)
.append("g")
.attr("class", "piechart")
.attr("transform", "translate(" + this.width / 2 + "," + this.height / 2 + ")");
//this.update(dataset[0].segments);
},
oldPieData: "",
pieTween: function(d, i){
var that = this;
var theOldDataInPie = methods.oldPieData;
// Interpolate the arcs in data space
var s0;
var e0;
if(theOldDataInPie[i]){
s0 = theOldDataInPie[i].startAngle;
e0 = theOldDataInPie[i].endAngle;
} else if (!(theOldDataInPie[i]) && theOldDataInPie[i-1]) {
s0 = theOldDataInPie[i-1].endAngle;
e0 = theOldDataInPie[i-1].endAngle;
} else if(!(theOldDataInPie[i-1]) && theOldDataInPie.length > 0){
s0 = theOldDataInPie[theOldDataInPie.length-1].endAngle;
e0 = theOldDataInPie[theOldDataInPie.length-1].endAngle;
} else {
s0 = 0;
e0 = 0;
}
var i = d3.interpolate({startAngle: s0, endAngle: e0}, {startAngle: d.startAngle, endAngle: d.endAngle});
return function(t) {
var b = i(t);
return methods.arc(b);
};
},
removePieTween: function(d, i) {
var that = this;
s0 = 2 * Math.PI;
e0 = 2 * Math.PI;
var i = d3.interpolate({startAngle: d.startAngle, endAngle: d.endAngle}, {startAngle: s0, endAngle: e0});
return function(t) {
var b = i(t);
return methods.arc(b);
};
},
update: function(dataSet){
var that = this;
methods.el = this;
methods.svg = d3.select(methods.el["selector"] + " .piechart");
this.piedata = methods.pie(dataSet);
//__slices
this.path = methods.svg.selectAll("path.pie")
.data(this.piedata);
this.path.enter().append("path")
.attr("class", "pie")
.attr("fill", function(d, i) {
return methods.color(i);
})
.transition()
.duration(300)
.attrTween("d", methods.pieTween);
this.path
.transition()
.duration(300)
.attrTween("d", methods.pieTween);
this.path.exit()
.transition()
.duration(300)
.attrTween("d", methods.removePieTween)
.remove();
//__slices
//__labels
var labels = methods.svg.selectAll("text")
.data(this.piedata);
labels.enter()
.append("text")
.attr("text-anchor", "middle")
labels
.attr("x", function(d) {
var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
d.cx = Math.cos(a) * (methods.radius - 75);
return d.x = Math.cos(a) * (methods.radius - 20);
})
.attr("y", function(d) {
var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
d.cy = Math.sin(a) * (methods.radius - 75);
return d.y = Math.sin(a) * (methods.radius - 20);
})
.text(function(d) {
return d.value;
})
.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;
})
.transition()
.duration(300)
labels
.transition()
.duration(300)
labels.exit()
.transition()
.duration(300)
//__labels
//__pointers
methods.svg.append("defs").append("marker")
.attr("id", "circ")
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("refX", 3)
.attr("refY", 3)
.append("circle")
.attr("cx", 3)
.attr("cy", 3)
.attr("r", 3);
var pointers = methods.svg.selectAll("path.pointer")
.data(this.piedata);
pointers.enter()
.append("path")
.attr("class", "pointer")
.style("fill", "none")
.style("stroke", "black")
.attr("marker-end", "url(#circ)");
pointers
.attr("d", function(d) {
if(d.cx > d.ox) {
return "M" + d.sx + "," + d.sy + "L" + d.ox + "," + d.oy + " " + d.cx + "," + d.cy;
} else {
return "M" + d.ox + "," + d.oy + "L" + d.sx + "," + d.sy + " " + d.cx + "," + d.cy;
}
})
.transition()
.duration(300)
pointers
.transition()
.duration(300)
pointers.exit()
.transition()
.duration(300)
//__pointers
this.oldPieData = this.piedata;
}
};
$.fn.piechart = function(methodOrOptions) {
if ( methods[methodOrOptions] ) {
return methods[ methodOrOptions ].apply( this, Array.prototype.slice.call( arguments, 1 ));
} else if ( typeof methodOrOptions === 'object' || ! methodOrOptions ) {
// Default to "init"
return methods.init.apply( this, arguments );
} else {
$.error( 'Method ' + methodOrOptions + ' does not exist' );
}
};
})(jQuery);
var dataCharts = [
{
"data": [
{
"segments": [
53245, 28479, 19697, 24037, 40245
]
}
]
},
{
"data": [
{
"segments": [
855, 79, 97, 237, 245
]
}
]
},
{
"data": [
{
"segments": [
22, 79, 97, 12, 245
]
}
]
},
{
"data": [
{
"segments": [
122, 279, 197, 312, 545
]
}
]
}
];
var clone = jQuery.extend(true, {}, dataCharts);
//__invoke concentric
$('[data-role="piechart"]').each(function(index) {
var selector = "piechart"+index;
$(this).attr("id", selector);
var options = {
data: clone[0].data
}
$("#"+selector).piechart(options);
$("#"+selector).piechart('update', clone[0].data[0].segments);
});
$(".testers a").on( "click", function(e) {
e.preventDefault();
var clone = jQuery.extend(true, {}, dataCharts);
var min = 0;
var max = 3;
//__invoke pie chart
$('[data-role="piechart"]').each(function(index) {
pos = Math.floor(Math.random() * (max - min + 1)) + min;
$("#"+$(this).attr("id")).piechart('update', clone[pos].data[0].segments);
});
});
});
To conclude I've wrapped the very latest code for this in a jquery plugin. Its now possible to develop multiple pie charts with these labels.
LATEST CODE - ** http://jsfiddle.net/Qh9X5/1336/ - removes label properly on exit.
$(document).ready(function() {
(function( $ ){
var methods = {
el: "",
init : function(options) {
var clone = jQuery.extend(true, {}, options["data"]);
methods.el = this;
methods.setup(clone, options["width"], options["height"], options["r"], options["ir"]);
},
getArc: function(radius, innerradius){
var arc = d3.svg.arc()
.innerRadius(innerradius)
.outerRadius(radius);
return arc;
},
setup: function(dataset, w, h, r, ir){
var padding = 80;
this.width = w;
this.height = h;
this.radius = r
this.innerradius = ir;
this.color = d3.scale.category20();
this.pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.total; });
this.arc = this.getArc(this.radius, this.innerradius);
this.svg = d3.select(methods.el["selector"]).append("svg")
.attr("width", this.width + padding)
.attr("height", this.height + padding)
.append("g")
.attr("class", "piechart")
.attr("transform", "translate(" + ((this.width/2) + (padding/2)) + "," + ((this.height/2) + (padding/2)) + ")");
this.segments = this.svg.append("g")
.attr("class", "segments");
this.labels = this.svg.append("g")
.attr("class", "labels");
this.pointers = this.svg.append("g")
.attr("class", "pointers");
},
oldPieData: "",
pieTween: function(r, ir, d, i){
var that = this;
var theOldDataInPie = methods.oldPieData;
// Interpolate the arcs in data space
var s0;
var e0;
if(theOldDataInPie[i]){
s0 = theOldDataInPie[i].startAngle;
e0 = theOldDataInPie[i].endAngle;
} else if (!(theOldDataInPie[i]) && theOldDataInPie[i-1]) {
s0 = theOldDataInPie[i-1].endAngle;
e0 = theOldDataInPie[i-1].endAngle;
} else if(!(theOldDataInPie[i-1]) && theOldDataInPie.length > 0){
s0 = theOldDataInPie[theOldDataInPie.length-1].endAngle;
e0 = theOldDataInPie[theOldDataInPie.length-1].endAngle;
} else {
s0 = 0;
e0 = 0;
}
var i = d3.interpolate({startAngle: s0, endAngle: e0}, {startAngle: d.startAngle, endAngle: d.endAngle});
return function(t) {
var b = i(t);
return methods.getArc(r, ir)(b);
};
},
removePieTween: function(r, ir, d, i) {
var that = this;
s0 = 2 * Math.PI;
e0 = 2 * Math.PI;
var i = d3.interpolate({startAngle: d.startAngle, endAngle: d.endAngle}, {startAngle: s0, endAngle: e0});
return function(t) {
var b = i(t);
return methods.getArc(r, ir)(b);
};
},
update: function(dataSet){
var that = this;
methods.el = this;
var r = $(methods.el["selector"]).data("r");
var ir = $(methods.el["selector"]).data("ir");
methods.svg = d3.select(methods.el["selector"] + " .piechart");
methods.segments = d3.select(methods.el["selector"] + " .segments");
methods.labels = d3.select(methods.el["selector"] + " .labels");
methods.pointers = d3.select(methods.el["selector"] + " .pointers");
dataSet.forEach(function(d) {
d.total = +d.value;
});
this.piedata = methods.pie(dataSet);
//__slices
this.path = methods.segments.selectAll("path.pie")
.data(this.piedata);
this.path.enter().append("path")
.attr("class", "pie")
.attr("fill", function(d, i) {
return methods.color(i);
})
.transition()
.duration(300)
.attrTween("d", function(d, i) {
return methods.pieTween(r, ir, d, i);
});
this.path
.transition()
.duration(300)
.attrTween("d", function(d, i) {
return methods.pieTween(r, ir, d, i);
});
this.path.exit()
.transition()
.duration(300)
.attrTween("d", function(d, i) {
return methods.removePieTween(r, ir, d, i);
})
.remove();
//__slices
//__labels
var labels = methods.labels.selectAll("text")
.data(this.piedata);
labels.enter()
.append("text")
.attr("text-anchor", "middle")
labels
.attr("x", function(d) {
var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
d.cx = Math.cos(a) * (ir+((r-ir)/2));
return d.x = Math.cos(a) * (r + 20);
})
.attr("y", function(d) {
var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
d.cy = Math.sin(a) * (ir+((r-ir)/2));
return d.y = Math.sin(a) * (r + 20);
})
.text(function(d) {
return d.data.label;
})
.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;
})
.transition()
.duration(300)
labels
.transition()
.duration(300)
labels.exit()
.transition()
.duration(300)
//__labels
//__pointers
methods.pointers.append("defs").append("marker")
.attr("id", "circ")
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("refX", 3)
.attr("refY", 3)
.append("circle")
.attr("cx", 3)
.attr("cy", 3)
.attr("r", 3);
var pointers = methods.pointers.selectAll("path.pointer")
.data(this.piedata);
pointers.enter()
.append("path")
.attr("class", "pointer")
.style("fill", "none")
.style("stroke", "black")
.attr("marker-end", "url(#circ)");
pointers
.attr("d", function(d) {
if(d.cx > d.ox) {
return "M" + d.sx + "," + d.sy + "L" + d.ox + "," + d.oy + " " + d.cx + "," + d.cy;
} else {
return "M" + d.ox + "," + d.oy + "L" + d.sx + "," + d.sy + " " + d.cx + "," + d.cy;
}
})
.transition()
.duration(300)
pointers
.transition()
.duration(300)
pointers.exit()
.transition()
.duration(300)
//__pointers
this.oldPieData = this.piedata;
}
};
$.fn.piechart = function(methodOrOptions) {
if ( methods[methodOrOptions] ) {
return methods[ methodOrOptions ].apply( this, Array.prototype.slice.call( arguments, 1 ));
} else if ( typeof methodOrOptions === 'object' || ! methodOrOptions ) {
// Default to "init"
return methods.init.apply( this, arguments );
} else {
$.error( 'Method ' + methodOrOptions + ' does not exist' );
}
};
})(jQuery);
var dataCharts = [
{
"data": [
{
"segments": [
{
"label": "apple",
"value": 53245
},
{
"label": "cherry",
"value": 145
},
{
"label": "pear",
"value": 2245
},
{
"label": "bananana",
"value": 15325
}
]
}
]
},
{
"data": [
{
"segments": [
{
"label": "milk",
"value": 532
},
{
"label": "cheese",
"value": 145
},
{
"label": "grapes",
"value": 22
}
]
}
]
},
{
"data": [
{
"segments": [
{
"label": "pineapple",
"value": 1532
},
{
"label": "orange",
"value": 1435
},
{
"label": "grapes",
"value": 22
}
]
}
]
},
{
"data": [
{
"segments": [
{
"label": "lemons",
"value": 133
},
{
"label": "mango",
"value": 435
},
{
"label": "melon",
"value": 2122
}
]
}
]
}
];
var clone = jQuery.extend(true, {}, dataCharts);
//__invoke concentric
$('[data-role="piechart"]').each(function(index) {
var selector = "piechart"+index;
$(this).attr("id", selector);
var options = {
data: clone[0].data,
width: $(this).data("width"),
height: $(this).data("height"),
r: $(this).data("r"),
ir: $(this).data("ir")
}
$("#"+selector).piechart(options);
$("#"+selector).piechart('update', clone[0].data[0].segments);
});
$(".testers a").on( "click", function(e) {
e.preventDefault();
var clone = jQuery.extend(true, {}, dataCharts);
var min = 0;
var max = 3;
//__invoke pie chart
$('[data-role="piechart"]').each(function(index) {
pos = Math.floor(Math.random() * (max - min + 1)) + min;
$("#"+$(this).attr("id")).piechart('update', clone[pos].data[0].segments);
});
});
});