d3.js - Text position based on text width - javascript

I'm working with d3.js to generate a visualization that represents different hypotheses. Since the hypotheses are made of different parts , each word / part gets its own text element.
I want to base the x-position of each text element on the text width of the previous word including an offset. Having a hypothesis "IF x THEN y" i would need 4 text elements with "IF" having x=0, and since "IF" has a width of 10 and i use an offset of 5 "x" will get x=15 and so on.
I'm using json data that could look like this:
{[
{"id" : "id0",
"elements" : [
{
"text" : "IF",
"type" : "conditional"
},
{
"text" : "X",
"type" : "variable"
},
{
"text" : "THEN",
"type" : "conditional"},
{
"text" : "Y",
"type" : "variable"
}
]},
{"id" : "id1",
"elements" : [
{
"text" : "IF",
"type" : "conditional"
},
{
"text" : "abc",
"type" : "variable"
},
{
"text" : "THEN",
"type" : "conditional"},
{
"text" : "xyz",
"type" : "variable"
}
]}
]}
The code i am using to generate the text elements so far (each hypothesis is in a g-element is
var svg = d3.select("#viewport")
.append("svg")
.attr("width", 1200)
.attr("height", 800);
var content = svg.append("g").attr("id", "drawing");
var groups = content.selectAll().data(arr)
.enter()
.append("g")
.attr("class", function (d) {
return "hypothesis " + d["id"];
})
.each(function (d, i) {
d3.select(this).selectAll("text")
.data(d["elements"])
.enter()
.append("text")
.attr("class", function (d) {
return d.type;
})
.text(function (d) {
return d.text;
})
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("x", function (d, j) {
return j++ * 100;
})
.attr("y", 50 * (i + 1));
});
When setting the x position i want to get the width of the current text element and push it onto a variable so i can get the next new x-coordinate instead of just using a currently random offset of 100 px per word.
So the question is how can i get the calculated text width (have seen things on getBBox or similar, but it didn't work for me since i don't know where to use them) and how to apply it to the text elements. Or if there is a better way to create the elements, maybe not in a single run.
The different elements need to be styled in different colors and have to react so mouse-over later, that's why they have to be single text elements.
Thanks in advance.

I always use getComputedTextLength for these sorts of things, although getBBox would also work:
.each(function(d, i) {
var runningWidth = 0; //<-- keep a running total
...
.attr("x", function(d, j) {
var w = this.getComputedTextLength(), //<-- length of this node
x = runningWidth; //<-- previous length to return
runningWidth += w; //<-- total
return x;
})
...
Full code:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#3.5.3" data-semver="3.5.3" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body>
<div id="viewport"></div>
<script>
var arr =
[{
"id": "id0",
"elements": [{
"text": "IF",
"type": "conditional"
}, {
"text": "X",
"type": "variable"
}, {
"text": "THEN",
"type": "conditional"
}, {
"text": "Y",
"type": "variable"
}]
}, {
"id": "id1",
"elements": [{
"text": "IF",
"type": "conditional"
}, {
"text": "abc",
"type": "variable"
}, {
"text": "THEN",
"type": "conditional"
}, {
"text": "xyz",
"type": "variable"
}]
}];
var svg = d3.select("#viewport")
.append("svg")
.attr("width", 1200)
.attr("height", 800);
var content = svg.append("g").attr("id", "drawing");
var groups = content.selectAll().data(arr)
.enter()
.append("g")
.attr("class", function(d) {
return "hypothesis " + d["id"];
})
.each(function(d, i) {
var runningWidth = 0;
d3.select(this).selectAll("text")
.data(d["elements"])
.enter()
.append("text")
.attr("class", function(d) {
return d.type;
})
.text(function(d) {
return d.text;
})
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("x", function(d, j) {
var w = this.getComputedTextLength(),
x = runningWidth;
runningWidth += w;
return x;
})
.attr("y", 50 * (i + 1));
});
</script>
</body>
</html>

Related

javascript zoomable tree map click the last node

How can I capture the click event for the last node?
I followed this tutorial(http://bl.ocks.org/ganeshv/6a8e9ada3ab7f2d88022) to make the tree map. In my purpose I want to make the last node clickable, then get the node data send it to server.
I used the developer tool to get the structure of tree map.
My code in the js file is that:
function display(d) {
lastGroupArray = [];
// collectLastGroup(d);
console.log(lastGroupArray);
grandparent
.datum(d.parent)
.on("click", transition)
.select("text")
.text(name(d));
var g1 = svg.insert("g", ".grandparent")
.datum(d)
.attr("class", "depth");
var g = g1.selectAll("g")
.data(d._children)
.enter().append("g");
g.filter(function (d) { return d._children; })
.classed("children", true)
.on("click", transition);
var children = g.selectAll(".child")
.data(function(d) { return d._children || [d]; })
.enter().append("g");
children.append("rect")
.attr("class", "child")
.call(rect)
.append("title")
.text(function(d) { return d.name + " (" + formatNumber(d.value) + ")"; });
//append child text
children.append("text")
.attr("class", "ctext")
.text(function(d) { return d.name; })
.call(text2);
//append parent text
g.append("rect")
.attr("class", "parent")
.call(rect);
var ccc = g.selectAll("rect").on("click", function (d){
if(typeof d.lastGroup !== 'undefined'){
d.lastGroup.forEach( function (dd) {
// lastGroupArray.push(new LastGroup(dd));
console.log(dd.filePath);
});
}
})
My function above only can capture the last node when it has a parent.
The function will not work if the node is the last child when the treemap is loaded.
The Payee is the last node when the treemap is loaded.
Here is my JSON data:
The payee is on the bottom, it only has one parent which is the root "digit-first2".
{
"children": [
{
"lastGroup": [{
"filePath": "Jarfile/output/result/ResultFolder/digit-first2-2017-10-22T16-05-53/result1.csv",
"name": "0~2",
"value": 112
}],
"children": [
{
"lastGroup": [{
"filePath": "Jarfile/output/result/ResultFolder/digit-first2-2017-10-22T16-05-53/result3.csv",
"name": "0~2",
"value": 218
}],
"children": [{
"lastGroup": [{
"filePath": "Jarfile/output/result/ResultFolder/digit-first2-2017-10-22T16-05-53/result7.csv",
"name": "0~2",
"value": 836
}],
"name": "Payee",
"value": 836
}],
"name": "Tran Type",
"value": 218
},
{
"lastGroup": [{
"filePath": "Jarfile/output/result/ResultFolder/digit-first2-2017-10-22T16-05-53/result5.csv",
"name": "0~2",
"value": 834
}],
"name": "Payee",
"value": 834
}
],
"name": "[Code-Finger-Print, [Memo]]",
"value": 112
},
{
"lastGroup": [{
"filePath": "Jarfile/output/result/ResultFolder/digit-first2-2017-10-22T16-05-53/result2.csv",
"name": "0~2",
"value": 138
}],
"children": [{
"lastGroup": [{
"filePath": "Jarfile/output/result/ResultFolder/digit-first2-2017-10-22T16-05-53/result6.csv",
"name": "0~2",
"value": 766
}],
"name": "Payee",
"value": 766
}],
"name": "Tran Type",
"value": 138
},
{
"lastGroup": [{
"filePath": "Jarfile/output/result/ResultFolder/digit-first2-2017-10-22T16-05-53/result4.csv",
"name": "0~2",
"value": 731
}],
"name": "Payee",
"value": 731
}
],
"name": "digit-first2"
}
My function above will only work when the root is not the parent of the last node.
My question is how can I capture the click event for the node if the root is this node`s parent.
You can achieve the last node click by changing the json structure. The last child should have one more children key with all the last child's properties copied in the children key with the same name .
For example if the last child is AgglomerativeCluster . Then the structure can be
children: [
{
name: "cluster",
children: [
{ name: "AgglomerativeCluster",
children: [{name: "AgglomerativeCluster",value: 3938,count:39}] },
{ name: "CommunityStructure", value: 3812 },
]
}
Find the full implementation here https://codesandbox.io/s/affectionate-thunder-l024x
There is simple example;
function display(d) {
grandparent
.datum(d.parent)
.on("click", transition)
.select("text")
.text(name(d));
var g1 = svg.insert("g", ".grandparent")
.datum(d)
.attr("class", "depth");
var g = g1.selectAll("g")
.data(d._children)
.enter().append("g");
g.filter(function (d) { return d._children; })
.classed("children", true)
.on("click", transition);
var children = g.selectAll(".child")
.data(function (d) { return d._children || [d]; })
.enter().append("g");
children.append("rect")
.attr("class", "child")
.call(rect)
.append("title")
.text(function (d) {
return d.key + " (" + formatNumber(d.value) + ")";
});
children.append("text")
.attr("class", "ctext")
.text(function (d) { return d.key; })
.call(text2);
g.append("rect")
.attr("class", "parent")
.call(rect);
var t = g.append("text")
.attr("class", "ptext")
.attr("dy", ".75em")
t.append("tspan")
.text(function (d) { return d.key; });
t.append("tspan")
.attr("dy", "1.0em")
.text(function (d) { return formatNumber(d.value); });
t.call(text);
var isDeph = false;
g.selectAll("rect")
.style("fill", function (d) {
if (d.values != null) {
isDeph = true;
}
return d.color;
})
.attr('onclick', function (d) {
if (!isDeph) {
return d.data
}
return ''
});
function transition(d) {
if (transitioning || !d)
return;
transitioning = true;
var g2 = display(d),
t1 = g1.transition().duration(750),
t2 = g2.transition().duration(750);
// Update the domain only after entering new elements.
x.domain([d.x, d.x + d.dx]);
y.domain([d.y, d.y + d.dy]);
// Enable anti-aliasing during the transition.
svg.style("shape-rendering", null);
// Draw child nodes on top of parent nodes.
svg.selectAll(".depth").sort(function (a, b) {
return a.depth - b.depth;
});
// Fade-in entering text.
g2.selectAll("text").style("fill-opacity", 0);
// Transition to the new view.
t1.selectAll(".ptext").call(text).style("fill-opacity", 0);
t1.selectAll(".ctext").call(text2).style("fill-opacity", 0);
t2.selectAll(".ptext").call(text).style("fill-opacity", 1);
t2.selectAll(".ctext").call(text2).style("fill-opacity", 1);
t1.selectAll("rect").call(rect);
t2.selectAll("rect").call(rect);
// Remove the old node when the transition is finished.
t1.remove().each("end", function () {
svg.style("shape-rendering", "crispEdges");
transitioning = false;
});
}
return g;
}

how to set mulitple text in d3 treemap?

<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="d3.min.js"></script>
</head>
<body>
<script>
var color = d3.scale.category10();
var canvas = d3.select('body').append('svg')
.attr('width',800)
.attr('height',500)
d3.json('mydata.json',function (data){
var treemap = d3.layout.treemap()
.size([800,500])
.nodes(data)
var cells = canvas.selectAll(".cell")
.data(treemap)
.enter()
.append("g")
.attr("class","cell")
cells.append("rect")
.attr("x",function (d) { return d.x; })
.attr("y",function (d) { return d.y; })
.attr("width",function (d) { return d.dx; })
.attr("height",function (d) { return d.dy; })
.attr("fill",function (d) { return d.children ? null : color(d.parent.name); })
.attr("stroke",'#fff')
cells.append("text")
.attr("x",function (d) { return d.x + d.dx / 2 })
.attr("y",function (d) { return d.y + d.dy / 2 })
.attr("text-anchor", "middle")
.text(function (d) { return d.children ? null : d.name; })
})
</script>
</body>
</html>
this is my d3 code for creating treemap..in text part i have to display multiple text.,now it display d.name alone but i have to dispaly d.name and d.value...how to display multiple text in d3 treemap?
{
"name": "Max",
"value": 100,
"children": [
{
"name": "Sylvia",
"value": 75,
"children": [
{"name": "Craig","value": 25},
{"name": "Robin","value": 25},
{"name": "Anna","value": 25}
]
},
{
"name": "David",
"value": 75,
"children":[
{"name": "jeff", "value": 25},
{"name": "Buffy", "value": 25}
]
},
{
"name":"Mr X",
"value":75
}
]
}
this is my json file.
Simply add another text for values and adjust the y.
cells.append("text")
.attr("x",function (d) { return d.x + d.dx / 2 })
.attr("y",function (d) { return d.y + d.dy / 2 + 15 })//15 pixels below the name label.
.attr("text-anchor", "middle")
.text(function (d) { return d.children ? null : d.value; })
working code here

Cannot make a pack layout in d3? selectAll not working?

I am relatively new to coding and very new to d3. I am currently trying to use d3 with json to make a pack layout representing current presidential candidates and how many times they have talked about a certain issue during the feedback.
I wanted to start small so I made some dummy data in a .json file, it is below:
{
"name": "John Doe",
"party": "democratic",
"issues": [
{ "issue":"issue1", "value": 25 },
{ "issue":"issue2", "value": 10 },
{ "issue":"issue3", "value": 50 },
{ "issue":"issue4", "value": 40 },
{ "issue":"issue5", "value": 5 }
]
}
I want to display bubbles with "issue" as the label and "value" as the circle radius, ending up with five different sized circles on my canvas. Below is my index.html file:
var width = 800, height = 600;
var canvas = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(50, 50)");
var pack = d3.layout.pack()
.size([width, height - 50])
.padding(10);
d3.json("fakedata.json", function (data) {
var nodes = pack.nodes(data);
var node = canvas.selectAll(".node")
.data(nodes)
.enter()
.append("g")
.attr("class", "node")
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
node.append("circle")
.attr("r", function (d) { return d.r; })
.attr("fill", "steelblue")
.attr("opacity", 0.25)
.attr("stroke", "#ADADAD")
.attr("stroke-width", "2");
node.append("text")
.text(function (d) {
return d.children ? "" : d.issue;
});
});
I keep getting the error below and I think it is because node is not being set correctly.
Error: Invalid value for <g> attribute transform="translate(NaN,NaN)"
Error: Invalid value for <circle> attribute r="NaN"
Any help would be much appreciated! Thank you!
The JSON you are passing is
{
"name": "John Doe",
"party": "democratic",
"issues": [
{ "issue":"issue1", "value": 25 },
{ "issue":"issue2", "value": 10 },
{ "issue":"issue3", "value": 50 },
{ "issue":"issue4", "value": 40 },
{ "issue":"issue5", "value": 5 }
]
}
It should have been like below note the key name children instead of issues
{
"name": "John Doe",
"party": "democratic",
"children": [
{ "issue":"issue1", "value": 25 },
{ "issue":"issue2", "value": 10 },
{ "issue":"issue3", "value": 50 },
{ "issue":"issue4", "value": 40 },
{ "issue":"issue5", "value": 5 }
]
}
Working code here.
The answer is a bit late, but in case you don't want to change your data structure and you want to keep the issues property, you can explicitly tell D3 to use the issues property for children data using the children() call:
var pack = d3.layout.pack()
.size([width, height - 50])
.children(function (d) { return d.issues; })
.padding(10);

D3JS Force Layout new nodes disconnects existing nodes

When I add new nodes to D3's Force Layout, the new nodes ignore the previous nodes when positioning itself and the previous nodes becomes un-draggable. I feel I've followed the logic of:
Add elements to arrays nodes and links
Updated force.nodes(nodes) and force.links(links)
Ran through .data().enter() with new data
Called force.start()
But still results in previous nodes disconnects. The new nodes are draggable and appears to take into consideration the LAST SET of added nodes position and avoids collision, all other previous nodes are clickable still, but their positioning are ignored and not updated.
Here is a the code in PLNKR: http://plnkr.co/edit/5fXZf63s73cTO37zLjNQ?p=preview
var width = 1000;
var height = 600;
var node_w = 30;
var node_h = 30;
var text_dx = -20;
var text_dy = 20;
var new_id = 9;
var nodes = [],
links = [],
links_line,
node_circles;
var svg = d3.select("body").append("svg")
.attr("width",width)
.attr("height",height);
var nodes = [
{ "name": "Nucleus" , "size" : 25, "id" : 0 , "color":"#ac0000"},
{ "name": "one" , "size" : 5 , "id": 1 , "color": "#ac0"},
{ "name": "two" , "size" : 15 , "id": 2 , "color": "#ac0"},
{ "name": "three" , "size" : 25 , "id": 3 , "color": "#ac0"},
{ "name": "four" , "size" : 9 , "id": 4 , "color": "#ac0"},
{ "name": "five" , "size" : 12 , "id": 5 , "color": "#ac0"},
{ "name": "six" , "size" : 15 , "id": 6 , "color": "#ac0"},
{ "name": "seven" , "size" : 41 , "id": 7 , "color": "#ac0"},
{ "name": "eight" , "size" : 5 , "id": 8 , "color": "#ac0"}
];
var links = [
{ "source": 0 , "target": 1 , "link_info":"r01" },
{ "source": 1 , "target": 2 , "link_info":"r31" },
{ "source": 1 , "target": 3 , "link_info":"r02" },
{ "source": 1 , "target": 4 , "link_info":"r04" },
{ "source": 0 , "target": 5 , "link_info":"r05" },
{ "source": 0 , "target": 6 , "link_info":"r06" },
{ "source": 0 , "target": 7 , "link_info":"r87" },
{ "source": 0 , "target": 8 , "link_info":"r87" }
];
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([width, height])
.linkDistance(150)
.charge(-1400);
var drag = force.drag();
init();
function init() {
force.start();
links_line = svg.selectAll("line")
.data(links)
.enter()
.append("line")
.style("stroke", "#ac0")
.style("stroke-width", 1);
node_circles = svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.style("fill", function(d) {return d.color;})
.on("dblclick", function(d, i) {
addNodes(i);
})
.call(drag);
draw();
}
function addNodes(i) {
for (c=0; c < Math.floor(Math.random() * 20) + 4; c++) {
nodes.push({"name": "new " + new_id,"size": (Math.floor(Math.random() * 20) + 10),"id": new_id,"color": "#333"})
links.push({"source": i,"target": new_id,"link_info": "r"+i+new_id});
new_id++;
}
// Update force.nodes
force.nodes(nodes);
// Update force.links
force.links(links);
// exec init()
init();
}
function draw() {
var ticksPerRender = 1;
requestAnimationFrame(function render() {
force.tick();
//Update nodes
node_circles.attr("cx", function(d) {return d.x - d.size / 6;});
node_circles.attr("cy", function(d) {return d.y - d.size / 6;});
node_circles.attr("r", function(d) {return d.size});
//Update Location line
links_line.attr("x1", function(d) {return d.source.x;});
links_line.attr("y1", function(d) {return d.source.y;});
links_line.attr("x2", function(d) {return d.target.x;});
links_line.attr("y2", function(d) {return d.target.y;});
requestAnimationFrame(render)
});
} // draw();
Updating a d3 visualization follows an enter, update, and exit workflow (start your reading here and here).
Try this instead:
function init() {
force.start();
links_line = svg.selectAll("line")
.data(links);
links_line
.enter()
.append("line")
.style("stroke", "#ac0")
.style("stroke-width", 1);
links_line.exit().remove();
node_circles = svg.selectAll("circle")
.data(nodes);
node_circles
.enter()
.append("circle")
.style("fill", function(d) {return d.color;})
.on("dblclick", function(d, i) {
addNodes(i);
})
.call(drag);
draw();
}
Updated example.

Separated, Sorted d3 Circles displayed along the X axis

I'm trying to achieve the following display utilizing the d3js.org library:
I get the circle svg objects to display with varying radius based on an attribute I'm getting from the JSON, but where I'm getting stuck is grouping them together and displaying along a linear, horizontal axis.
Here is my JSON structure:
[
{
"category" : "Foo",
"radius" : "3"
},
{
"category" : "Bar",
"radius" : "2"
},
{
"category" : "Foo",
"radius" : "3"
},
{
"category" : "Bar",
"radius" : "1"
},
{
"category" : "Bar",
"radius" : "2"
},
{
"category" : "Foo",
"radius" : "1"
}
]
d3 Javascript
var height = 50,
width = 540;
var companyProfileVis = d3.select(".myDiv").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
d3.json("data/myData.json", function(data){
companyProfileVis.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("r", function (d) { return d.radius * 4; })
.attr("cx", function(d) { return d.radius * 20; })
.attr("cy", 20)
});
And finally my HTML
<div class="myDiv"></div>
Expanding on Pablo's answer a bit, you would also need to sort the values in the grouped elements to achieve the order you have in the picture. The code would look like this.
var nested = d3.nest()
.key(function(d) { return d.category; })
.sortValues(function(a, b) { return b.radius - a.radius; })
.entries(data);
The nested selection based on this would look as follows.
var gs = svg.selectAll("g").data(nested)
.enter().append("g")
.attr("transform", function(d, i) { return "translate(0," + (20 + i * 100) + ")"; });
gs.selectAll("circle").data(function(d) { return d.values; })
.enter().append("circle");
Note that you're moving the g elements according to the index, so you don't have to worry about the y coordinate of the circles later. The x coordinate is computed based on the index similar to how the y coordinate is computed for the g.
All that you then have to do is set a few more attributes and append the text elements. Complete demo here.
You can use d3.nest to group the data items by category, and then use nested selections to create both groups of circles.
// Nest the items by category
var nestedData = d3.nest(data)
.key(function(d) { return d.category; })
.map(data, d3.map)
.values();
This will give you the following array:
nestedData = [
[
{category: "Foo", radius: "3"},
{category: "Foo", radius: "3"},
{category: "Foo", radius: "1"}
],
[
{category: "Bar", radius: "2"},
{category: "Bar", radius: "1"},
{category: "Bar", radius: "2"}
]
]
Regards,

Categories