I'm trying to get the widths of a bunch of text elements I have created with d3.js
This is how I'm creating them:
var nodesText = svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d.name;
})
.attr("x", function(d, i) {
return i * (w / dataset.length);
})
.attr("y", function(d) {
return 45;
});
I'm then using the width to create rectangles the same size as the text's boxes
var nodes = svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d, i) {
return i * (w / dataset.length);
})
.attr("y", function(d) {
return 25;
})
.attr("width", function(d, i) {
//To Do: find width of each text element, after it has been generated
var textWidth = svg.selectAll("text")
.each(function () {
return d3.select(this.getComputedTextLength());
});
console.log(textWidth);
return textWidth;
})
.attr("height", function(d) {
return 30;
})
I tried using the Bbox method from here but I don't really understand it. I think selecting the actual element is where I'm going wrong really.
I would make the length part of the original data:
var nodesText = svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d.name;
})
.attr("x", function(d, i) {
return i * (w / dataset.length);
})
.attr("y", function(d) {
return 45;
})
.each(function(d) {
d.width = this.getBBox().width;
});
and then later
var nodes = svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("width", function(d) { return d.width; });
You can use getBoundingClientRect()
Example:
.style('top', function (d) {
var currElemHeight = this.getBoundingClientRect().height;
}
edit: seems like its more appropriate for HTML elements. for SVG elements you can use getBBbox() instead.
d3.selectAll returns a selection. You can get each of the elements by navigating through the array in the _groups property. When you are determining the width of a rectangle, you can use its index to get the corresponding text element:
.attr('width', function (d, i) {
var textSelection = d3.selectAll('text');
return textSelection._groups[0][i].getComputedTextLength();
});
The _groups property of d3's selection has a list of nodes at [0]. This list contains all of the selected elements, which you can access by index. It's important that you get the SVG element so that you can use the getComputedTextLength method.
You may also want to consider creating the rect elements first, then the text elements, and then going back to the rectangles to edit the width attribute, so that the text elements are on top of the rectangles (in case you want to fill the rectangles with color).
Update:
It's typically preferred that you don't access _groups, though, so a safer way to get the matching text element's width would be:
.attr('width', function (d, i) {
return d3.selectAll('text').filter(function (d, j) { return i === j; })
.node().getComputedTextLength();
});
Using node safely retrieves the element, and filter will find the text element which matches index.
Related
2 part question:
I have a bar chart with created using multiple arrays. These arrays contain the % wins of baseball teams; the relevant team colours; and their names.
I can create one set of labels on the chart, either the names or the win %. However I can't get both on at the same time. See below.
The code I am using is:
let WinsLabel = svgContainer.selectAll("text")
.data(d3.zip(TeamArray, WinPercArray, Colours));
WinsLabel.enter()
.append("text")
.attr("fill", "black")
.attr("x", function(d, i) {
return 45 + (i * 50);
})
.attr("y", 700)
.transition()
.duration(1000)
.attr("x", function(d,i){
return 70 + (i*50);
})
.attr("y", function(d){
return 685 - d[1];
})
.attr("text-anchor","middle")
.attr("font-family", "sans-serif")
.attr("font-size", "15px")
.attr("fill", "black")
.text(function(d){
return d[1]/10 + "%";
});
let TeamLabel = svgContainer.selectAll("text")
.data(d3.zip(TeamArray, WinPercArray, Colours));
TeamLabel.enter()
.append("text")
.attr("fill", "black")
.attr("x", function(d, i) {
return 45 + (i * 50);
})
.attr("y", 700)
.transition()
.duration(1000)
.attr("x", function(d,i){
return 70 + (i*50);
})
.attr("y", function(d){
return 700 - d[1]/2;
})
.attr("text-anchor","middle")
.attr("font-family", "sans-serif")
.attr("font-size", "15px")
.attr("fill", "white")
.text(function(d){
return d[0];
});
When I run the code with both scripts, only the win % shows up, but the names don't. In order to get the names to show up I have to remove the first label.
The 2 parts to my question are:
How would I get both sets of labels to show up at the same time?
How can I get the names to be arranged vertically in the rectangles/bars?
D3 stands for data driven something; and it's core principle is based on linking elements / selection, with data. When you set data, (var selection = selectAll(...).data(...)), you get 3 cases to think about:
Some existing elements can be linked to certain item in new data. You access them using selection
Some elements cannot be linked to any item in new data. You access them using selection.exit()
Some items in new data cannot be linked to any element from selection. You access them by using selection.enter()
In its simplest case, the linking between data and elements is made by index -- ie first element in selection is linked with first item in data array, second with second, and so on. The d3 cannot find element for the data item (= gets put into .enter() selection) if and only if (in this by-index context) the index of that data item is bigger than the size of the selection.
On your initial select
let WinsLabel = svgContainer.selectAll("text")
.data(d3.zip(TeamArray, WinPercArray, Colours));
The selection is empty, since there are no text tags yet. And since its empty, all of the to-be-created placeholders are inside .enter() selection. However, on your next select for the other label type
let TeamLabel = svgContainer.selectAll("text")
.data(d3.zip(TeamArray, WinPercArray, Colours));
The selection is of the size of the passed data, and thus .enter() selection is empty; it's the TeamLabel selection that contains all of the old elements (percentage label text tags), but they got their data values reassigned.
Andrew proposed one solution to assign classes, but personally I'd take all elements that relate to same team and put it under one group.
var TeamArray = ["Yankees", "Rays", "RedSox", "Jays","Orioles", "Twin", "Indians", "WhiteSox", "Detroit", "Royals", "Astros", "Rangers", "A's", "Angels","Mariners"];
var WinPercArray = [653, 609, 540, 400, 300, 667, 521, 458, 383, 347, 660, 511, 500, 458, 442];
var Colours = ["#003087", "#092C5C", "#BD3039", "#134A8E", "#DF4601", "#002B5C", "#0C2340", "#C4CED4", "#FA4616", "#BD9B60", "#EB6E1F", "#C0111F", "#003831", "#003263", "#005C5C"];
var data = d3.zip(TeamArray, WinPercArray, Colours);
var svg = d3.select('body').append('svg').attr('height', 300).attr('width', 800);
var teams = svg.selectAll('g.teams')
.data(data);
var scale = d3.scaleLinear()
.domain([0, 1000])
.range([200, 0]);
var teamsEnter = teams.enter()
.append('g')
.classed('team', true)
.attr('transform', function(d, i){
return 'translate(' + (i*50) + ',0)';
})
teamsEnter.append('rect')
.attr('width', 50)
.attr('y', function(d) { return scale(d[1]); })
.attr('height', function(d) { return scale(0) - scale(d[1]); })
.style('fill', function(d) { return d[2]; });
teamsEnter.append('text')
.attr('x', 25)
.attr('y', function(d) { return scale(d[1]) - 30; })
.text(function(d){ return d[0]; });
teamsEnter.append('text')
.attr('x', 25)
.attr('y', function(d) { return scale(d[1]) - 15; })
.text(function(d){ return d[1]; });
text {
text-anchor: middle;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Groups in some way act as encapsulation of inner items, so you can mentally separate data binding to groups (ie when to create / update / delete it), from actual logic that takes place when working with its children
I have an array with names of images, I want to crate an svg canvas and position them in correct rows and columns so that it will not overlap. Here is my code for that.
<script type="text/javascript">
var imagesObjects = ["1.png","2.png","3.png","4.png","6.png","3.png","4.png","6.png","1.png","2.png","3.png","4.png","6.png","1.png","2.png"];
var iconsArea = d3.select("#icons").data(imagesObjects)
.enter()
.append("svg:image")
.attr("xlink:href", function(d){
console.log(d);
return "images/"+d;
})
.attr("width", 20)
.attr("height", 20)
.attr("x", function(d, i){
return i*10;
})
.attr("y",function(d, i){
return i*10;
});
</script>
But When I run this, My tags won't append to the canvas. All I get is a blank canvas. Can anyone point my mistake
I'm not quite sure why you say svg canvas, because that are two different things, but you are appending svg:image so i assume you are working with an svg.
Is there an svg element with id="icons"? You have to select all the images you want create as a placeholder and bind the data to it. basically I just added the .selectAll("image").
var iconsArea = d3.select("#icons").selectAll("image")
.data(imagesObjects)
.enter()
.append("svg:image")
.attr("xlink:href", function(d){
console.log(d);
return "images/"+d;
})
.attr("width", 20)
.attr("height", 20)
.attr("x", function(d, i){
return i*10;
})
.attr("y",function(d, i){
return i*10;
});
I am trying to use the bubble chart example as a template to build a visualisation. I have my JSON as a flat-hierarchy, such that there is one element called children and that holds an array of objects that I want to visualise.
The JSON looks like this:
{
"children":[
{
"acc":"Q15019",
"uid":"SEPT2_HUMAN",
"sym":"SEPT2",
"name":"Septin-2",
"alt_ids":"",
"ratio":0.5494271087884398,
"pval":0.990804718
},
...,
{
"acc":"Q16181",
"uid":"SEPT7_HUMAN",
"sym":"SEPT7",
"name":"Septin-7",
"alt_ids":"",
"ratio":1.1949912048567823,
"pval":0.511011887
}
]
}
I have modified the example code as follows:
var diameter = 960,
format = d3.format(",d"),
color = d3.scale.quantile().range(colorbrewer.RdBu[9]);
var bubble = d3.layout.pack()
.sort(null)
.size([diameter, diameter])
.padding(1.5);
var svg = d3.select("body").append("svg")
.attr("width", diameter)
.attr("height", diameter)
.attr("class", "bubble");
d3.json("datagraph.json", function(datagraph) {
var node = svg.selectAll(".node")
.data(bubble.nodes(datagraph))
.enter().append("g")
.attr("class", "node")
.attr("id", function(d) { return d.acc; })
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
//node.append("title").text(function(d) { return d.className + ": " + format(d.value); });
node.append("circle")
.attr("r", function(d) { return 30; })
.style("fill", function(d) {
if(d.ratio == null)
return "#ffffff";
else
return color(d.ratio);
});
node.append("text")
.attr("dy", ".3em")
.style("text-anchor", "middle")
.text(function(d) { return d.acc; });
});
The resultant HTML has a ton of <g> tags responding to each element except they are never translated to the right position, but instead sort of sit on top of each other on the top left corner. By investigating in Firebug, I figured this happens presumably because the pack() algorithm does not get the objects one at a time, but the whole array as a single element, thus individual elements don't get .x and .y values.
If I change the .nodes() argument to datagraph.children I get the elements one at a time in nodes() iteration, but oddly enough I get a single <g> object. Since I don't need to flatten a hierarchy I skipped the classes(root) function, in the example. What I am wondering is whether or not the packageName attribute plays any role in the nodes()?
How can I resolve this issue?
You haven't specified a value accessor:
var bubble = d3.layout.pack()
.sort(null)
.size([diameter, diameter])
.padding(1.5)
.value(function(d) { return d.pval; }) //<- must return a number
Example here.
I'm not sure if I've grouped my elements properly, but my layout in d3 is like so:
var circleGroup = svg.selectAll("g")
.data(nodeList)
.enter()
.append("g")
This creates a bunch a groups, I need a circle in each group:
circleGroup.append("circle")
.attr("cx", function(d,i){
return coordinates[i][0];
})
.attr("cy", function(d,i){
return coordinates[i][1];
})
.attr("r", function(d){
return 10;
})
.attr("fill", "white");
The data itself doesn't actually have any coordinate data so I dynamically arrange them in a circle and just position them based on index. I also add some labels. I repeat coordinates[i][0] here but is there a way to access the "cx" and "cy" attributes of the circles? I tried a few forms of d3.select(this) but I'm getting nothing.
circleGroup.append("text")
.attr("x", function(d,i){
return coordinates[i][0];
})
.attr("y", function(d,i){
return coordinates[i][1];
})
.style("text-anchor","middle")
.text(function(d,i){
return d;
});
Don't mess with indices, this is hard to maintain and error prone. Instead of that, given your specific tree structure, use node.previousSibling:
circleGroup.append("text")
.attr("x", function() {
return d3.select(this.previousSibling).attr("cx");
})
.attr("y", function() {
return d3.select(this.previousSibling).attr("cy");
})
Here is a demo using (most of) your code:
var svg = d3.select("svg")
var circleGroup = svg.selectAll("g")
.data(d3.range(5))
.enter()
.append("g");
circleGroup.append("circle")
.attr("cx", function(d, i) {
return 20 + Math.random() * 280;
})
.attr("cy", function(d, i) {
return 20 + Math.random() * 130;
})
.attr("r", function(d) {
return 10;
})
.style("opacity", 0.2);
circleGroup.append("text")
.attr("x", function() {
return d3.select(this.previousSibling).attr("cx");
})
.attr("y", function() {
return d3.select(this.previousSibling).attr("cy");
})
.style("text-anchor", "middle")
.text("Foo");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
I have a data variable which contains the following:
[Object { score="2.8", word="Blue"}, Object { score="2.8", word="Red"}, Object { score="3.9", word="Green"}]
I'm interested in modifying a piece of a D3 graph http://bl.ocks.org/3887051 to display the legend, which would be the list of the "word", for my data set.
The legend script looks like this (from link above):
var ageNames = d3.keys(data[0]).filter(function(key) { return key !== "State"; });
var legend = svg.selectAll(".legend")
.data(ageNames.slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
How do I modify their ageNames function to display the "word" set from my data? I'm not sure how they're utilizing the d3.keys. Is there another way to do it?
This should work more or less, but you may need to reverse() (as the original example does) or otherwise rearrange the elements of words, in order to correctly map a word to the right color. Depends on how you've implemented your graph.
var words = yourDataArray.map(function(entry) { return entry.word; });
var legend = svg.selectAll(".legend")
.data(words)
// The rest stays the same