Placing labels at the center of nodes in d3.js - javascript

I am starting with d3.js, and am trying to create a row of nodes each of which contains a centered number label.
I am able to produce the desired result visually, but the way I did it is hardly optimal as it involves hard-coding the x-y coordinates for each text element. Below is the code:
var svg_w = 800;
var svg_h = 400;
var svg = d3.select("body")
.append("svg")
.attr("width", svg_w)
.attr("weight", svg_h);
var dataset = [];
for (var i = 0; i < 6; i++) {
var datum = 10 + Math.round(Math.random() * 20);
dataset.push(datum);
}
var nodes = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("class", "node")
.attr("cx", function(d, i) {
return (i * 70) + 50;
})
.attr("cy", svg_h / 2)
.attr("r", 20);
var labels = svg.append("g")
.attr("class", "labels")
.selectAll("text")
.data(dataset)
.enter()
.append("text")
.attr("dx", function(d, i) {
return (i * 70) + 42
})
.attr("dy", svg_h / 2 + 5)
.text(function(d) {
return d;
});
The node class is custom CSS class I've defined separately for the circle elements, whereas classes nodes and labels are not explicitly defined and they are borrowed from this answer.
As seen, the positioning of each text label is hard-coded so that it appears at the center of the each node. Obviously, this is not the right solution.
My question is that how should I correctly associate each text label with each node circle dynamically so that if the positioning of a label changes along with that of a circle automatically. Conceptual explanation is extremely welcome with code example.

The text-anchor attribute works as expected on an svg element created by D3. However, you need to append the text and the circle into a common g element to ensure that the text and the circle are centered with one another.
To do this, you can change your nodes variable to:
var nodes = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(dataset)
.enter()
// Add one g element for each data node here.
.append("g")
// Position the g element like the circle element used to be.
.attr("transform", function(d, i) {
// Set d.x and d.y here so that other elements can use it. d is
// expected to be an object here.
d.x = i * 70 + 50,
d.y = svg_h / 2;
return "translate(" + d.x + "," + d.y + ")";
});
Note that the dataset is now a list of objects so that d.y and d.x can be used instead of just a list of strings.
Then, replace your circle and text append code with the following:
// Add a circle element to the previously added g element.
nodes.append("circle")
.attr("class", "node")
.attr("r", 20);
// Add a text element to the previously added g element.
nodes.append("text")
.attr("text-anchor", "middle")
.text(function(d) {
return d.name;
});
Now, instead of changing the position of the circle you change the position of the g element which moves both the circle and the text.
Here is a JSFiddle showing centered text on circles.
If you want to have your text be in a separate g element so that it always appears on top, then use the d.x and d.y values set in the first g element's creation to transform the text.
var text = svg.append("svg:g").selectAll("g")
.data(force.nodes())
.enter().append("svg:g");
text.append("svg:text")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
text.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});

The best answer came from the asker himself:
just a further observation: with only .attr("text-anchor", "middle")
for each text element, the label is at the middle horizontally but
slightly off vertically. I fixed this by adding attr("y", ".3em")
(borrowed from examples at d3.js website), which seems to work well
even for arbitrary size of node circle. However, what exactly this
additional attribute does eludes my understanding. Sure, it does
something to the y-coordinate of each text element, but why .3em in
particular? It seems almost magical to me...
Just add .attr("text-anchor", "middle") to each text element.
Example:
node.append("text")
.attr("x", 0)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });

This page describes what's going on under the svg hood when it comes to text elements. Understanding the underlying machinery and data structures helped me get a better handle on how I had to modify my code to get it working.

Related

d3 how to tie text to top right corner of view port while zooming and panning

I am creating a mapping application in d3 and want to tie some text to the top right corner of my view port. Additionally, I want the text to remain in the top right corner while I zoom and pan across the application.I think I can solve my problem by figuring out how to get the coordinates of the top right corner of my view. Knowing this information would allow me to then set the coordinates of my text element. I've tried manually setting the dimensions of the containing svg element and then moving the text to that location but interestingly this didn't work. I was hoping to be able to find the coordinates programatically rather than setting coordinates manually. How can I do this in d3/javascript?
EDIT:
My code is a modification of this code by Andy Barefoot: https://codepen.io/nb123456/pen/zLdqvM
My own zooming and panning code has essentially remained the same as the above example:
function zoomed() {
t = d3
.event
.transform
;
countriesGroup
.attr("transform","translate(" + [t.x, t.y] + ")scale(" + t.k + ")")
;
}
I'm trying to append the text at the very bottom of the code:
countriesGroup.append("text")
.attr("transform", "translate(" How do I get top right coordinates? ")")
.style("fill", "#ff0000")
.attr("font-size", "50px")
.text("This is a test");
My idea is to be able to get the top right coordinates of the view port through the code rather than setting it manually and then have the coordinates of the text update as the user zooms or pans.
To keep something in place while zooming and panning you could invert the zoom:
point == invertZoom(applyZoom(point))
This isn't particularly efficient, as we are using two operations to get to the original number. The zoom is applied here:
countriesGroup
.attr("transform","translate(" + [t.x, t.y] + ")scale(" + t.k + ")");
While the inversion would need to look something like:
text.attr("x", d3.zoom.transform.invert(point)[0])
.attr("y", d3.zoom.transform.invert(point)[1])
.attr("font-size", baseFontSize / d3.zoom.transform.k);
Where point and base font size are the original anchor point and font size. This means storing that data somewhere. In the example below I assign it as a datum to the text element:
var width = 500;
var height = 200;
var data = d3.range(100).map(function() {
return {x:Math.random()*width,y:Math.random()*height}
})
var zoom = d3.zoom()
.on("zoom",zoomed);
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height)
.call(zoom);
var g = svg.append("g")
var circles = g.selectAll()
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 5)
.attr("fill","steelblue")
var text = g.append("text")
.datum({x: width-10, y: 20, fontSize: 12})
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.style("text-anchor","end")
.attr("font-size",function(d) { return d.fontSize; })
.text("This is a test");
function zoomed() {
g.attr("transform", d3.event.transform);
var d = text.datum();
var p = d3.event.transform.invert([d.x,d.y]);
var x1 = p[0];
var y1 = p[1];
text.attr("x",x1)
.attr("y",y1)
.attr("font-size", d.fontSize / d3.event.transform.k)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Better Solution
The above is the solution to the approach you seem to be looking for. But the end result is best achieved by a different method. As I mention in my comment, the above approach goes through extra steps that can be avoided. There can also be some size/clarity changes in the text when zooming (quickly) using the above method
As noted above, you are applying the zoom here:
countriesGroup
.attr("transform","translate(" + [t.x, t.y] + ")scale(" + t.k + ")")
The zoom transform is applied only to countriesGroup, if your label happens to be in a different g (and not a child of countriesGroup), it won't be scaled or panned.
We wouldn't need to apply and invert the zoom, and we wouldn't need to update the position or font size of the text at all.
var width = 500;
var height = 200;
var data = d3.range(100).map(function() {
return {x:Math.random()*width,y:Math.random()*height}
})
var zoom = d3.zoom()
.on("zoom",zoomed);
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height)
.call(zoom);
var g = svg.append("g");
var g2 = svg.append("g"); // order does matter in layering
var circles = g.selectAll()
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 5)
.attr("fill","steelblue")
// position once and leave it alone:
var text = g2.append("text")
.attr("x", width - 10)
.attr("y", 20 )
.style("text-anchor","end")
.attr("font-size", 12)
.text("This is a test");
function zoomed() {
// apply the zoom to the g that has zoomable content:
g.attr("transform", d3.event.transform);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

how to update legend data in D3.js

I am working on one project in which I need to plot data on USA map.
Here is the link to the code.
I am getting logical error in the output. In the drop down menu of attributes, when you first select attribute as DAMAGE_PROPERTY then I get the legend which I want. But as soon as you select different attribute, previous legend gets appended to the new one. You can test it on the link. I have used .remove() property in the code to remove previously added elements.
Here is my Legend code-
var legendRectSize = 18;
var legendSpacing = 4;
var color = d3.scale.ordinal()
.domain(["<10", "10-20", "20-30", "30-40", "40-50", "50-60", "60-70", "70-80", ">80"])
.range(["#1a9850", "#66bd63", "#a6d96a","#d9ef8b","#ffffbf","#fee08b","#fdae61","#f46d43","#d73027"]);
var colorforbig=d3.scale.ordinal()
.domain(["<100","100-1000","1000-5000","5000-50000","50000-100000","100000-500000","5000000-10000000","10000000-50000000",">50000000"])
.range(["#1a9850", "#66bd63", "#a6d96a","#d9ef8b","#ffffbf","#fee08b","#fdae61","#f46d43","#d73027"]);
initlegend();
function initlegend(){
Legend=d3.select("svg")
.append("g")
.attr("id","legend");
}
function loadlegend(){
alert("in loadlegend");
var remove=d3.select("#legend")
.selectAll("g")
.remove();
console.log(remove);
var legendBox=Legend.selectAll("g")
.data(function(){
if(attr=="DAMAGE_PROPERTY"){
return colorforbig.domain();
}
else{
return color.domain();
}
})
.enter()
.append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
var height = legendRectSize;
var horz = 20;
var vert = i * height;
return "translate(" + horz + "," + vert + ")";
});
//Append a rectangle to each legend element to display the colors from the domain in the color variable
legendBox.append("rect")
.attr("width", legendRectSize)
.attr("height", legendRectSize)
.style("fill", color)
.style("stroke", color);
//Append a text element to each legend element based on the listed domains in the color variable
legendBox.append("text")
.attr("x", legendRectSize + legendSpacing)
.attr("y", legendRectSize - legendSpacing)
.text(function(d) { return d; });
}
What I want is when you select attribute DAMAGE_PROPERTY it should show different legend. For other four properties it should show different legend. So there are total two legends.

d3 - Rotate text elements in an array

I am trying to use selectAll("text") in d3 to to add an array of string values (called 'data') to my graph. I want each individual data point to be rotated at the point it is placed at, defined here as (i * (width/ data.length) + 8, 170). However, it is currently rotating the entire array set as one long string, with the first element at the (x, y) point I set. How can I appropriately apply the translate rotation to rotate each element individually?
new_svg.selectAll("text")
.data(data)
.enter().append("text")
.attr("x", function(d, i) {
return i * (width / data.length) + 8;
})
.attr("y", function(d) {
return 170;
})
.attr("dx", -barWidth/2)
.attr("text-anchor", "middle")
.attr("style", "font-size: 12; font-family: Garamond, sans-serif")
.text(function(d) { return d;})
.attr("transform", function(d) {
return "rotate(45)"
});
There was more than one text element in the array, which I was able to verify. It turns out that the problem comes from setting the x and y elements separate from the translate transform. The solution offered here worked for me: d3 x axis labels outputted as long string

Used Arc.centroid to plot circles - but text do no go along with it why?

I've used arc.Centroid to try to plot my circles on the arcs with labels. However, the labels do not stay with it?
force.on("tick", function() {
text.attr("x", function(d) { return d.x + 6; })
.attr("y", function(d) { return d.y + 4; });
node.attr("transform", function(d,i) {
return "translate(" + arc[i].centroid(d) + ")"; })
});
I have attempted to put centroid & arc[i] instead of the x & y. How can I put my circles with text? http://jsfiddle.net/xwZjN/20/
Also say if I were to have more json data, would I be able to restrict the plots only going into each section e.g. each section being a category?
Any help would be great. I think the solution may be similar to this - http://jsfiddle.net/nrabinowitz/GQDUS/
It seems that the force layout is not the right choice for your application. Try to group your symbol and text in a g element and place them at the calculated coordinates. See updated fiddle without force layout: http://jsfiddle.net/xwZjN/26/
var node = svg.selectAll("g.node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d,i) {
return "translate(" + arc[i].centroid() + ")";
});
node.append("path")
.attr("d", d3.svg.symbol().type(function(d) { return d.type; }))
// change (0,0) for exact symbol placement
.attr("transform", "translate(0,0)")
.style("fill", "blue" );
node.append("text")
.text(function(d) { return d.Name; })
// shift text in nice position
.attr("x", 10)
.attr("y", 5);

Nested SVG node creation in d3.js

I'v just started playing with d3js and find it strange that I have to create multiple selectors for each element I want to link to the background data structure for example separate selectors such as one for overlay text and one for rectangles to make an annotated bar graph.
svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr('y',function(d,i){return i*10;})
.attr('height',10)
.attr('width',function(d){return d.interestingValue})
.fill('#00ff00');
svg.selectAll("text")
.data(data)
.enter()
.append("text")
.attr('y',function(d,i){return i*10;})
.fill('#0000ff')
.text(function(d){return d.interestingValue});
Is there a more convenient way of combining these into a single selection and enter() chain that creates both the rects and the text elements?
Use a G (group) element. Use a single data-join to create the G elements, and then append your rect and text element. For example:
var g = svg.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d) { return "translate(0," + i * 10 + ")"; });
g.append("rect")
.attr("height", 10)
.attr("width", function(d) { return d.interestingValue; });
g.append("text")
.text(function(d) { return d.interestingValue; });

Categories

Resources