I am currently appending text nodes to an SVG as follows:
var node = svg.selectAll(...);
node.append("text")
.attr("dy", 10)
.attr("dx", 4)
.text(function (d) { return d["name"]; });
There are currently about 10 nodes, each with a name.
I want to add a rectangle below each of the text nodes, using the correct width, I have tried this:
node.select<SVGTSpanElement>("text")
.each(function(x, i) {
console.log(this.getComputedTextLength());
node.append("rect")
.attr("fill", "#cccccc05")
.attr("width", this.getComputedTextLength())
.attr("height", 20)
});
My problem is that (kind of obviously) I am creating 10 rectangles per node, not one for each node.
How do I include the calculation for text width, and add a single rectangle per text element?
Without refactoring your code too much, simply change where you're appending the rectangles. In this case, the parent node of the texts themselves:
node.select<SVGTSpanElement>("text")
.each(function(x, i) {
console.log(this.getComputedTextLength());
d3.select(this.parentNode).append("rect")
.attr("fill", "#cccccc05")
.attr("width", this.getComputedTextLength())
.attr("height", 20)
});
However, the most idiomatic way is using each() for the node selection, not for the selection of texts inside it. Then, you get the text length for each node element, something like this:
node.each(function(x, i) {
d3.select(this).append("rect")
.attr("fill", "#cccccc05")
.attr("width", d3.select(this).select("text").node().getComputedTextLength())
.attr("height", 20)
});
Related
In an SVG graph I create node elements consisting of a rectangle and some text. The amount of text can differ significantly, hence I'd like to set the width of the rect based on the width of the text.
Here's the creation of the rectangles with D3.js (using fixed width and height values):
var rects = nodeEnter.append("rect")
.attr("width", rectW)
.attr("height", rectH);
followed by the text element:
var nodeText = nodeEnter.append("text")
.attr("class", "node-text")
.attr("y", rectH / 2)
.attr("dy", ".35em")
.text(function (d) {
return d.data.name;
});
nodeText // The bounding box is valid not before the node addition happened actually.
.attr("x", function (d) {
return (rectW - this.getBBox().width) / 2;
});
As you can see, currently I center the text in the available space. Then I tried to set the widths of the rects based on their text, but I never get both, the rect element and the text HTML element (for getBBox()) at the same time. Here's one of my attempts:
rects.attr("width",
d => this.getBBox().width + 20
);
but obviously this is wrong as it refers to rects not the text.
What's the correct approach here?
I would use getComputedTextLength to measure the text. I don't know if there is an equivalent for this in D3.js My answer is using plain javascript and is assuming that the rect and the text center is {x:50,y:25 } and you are using text{dominant-baseline:middle;text-anchor:middle;}
let text_length = txt.getComputedTextLength();
rct.setAttributeNS(null,"width",text_length )
rct.setAttributeNS(null,"x",(50 - text_length/2) )
svg{border:1px solid}
text{dominant-baseline:middle;text-anchor:middle;}
<svg viewBox="0 0 100 50">
<rect x="25" y="12.5" width="50" height="25" stroke="black" fill="none" id="rct" />
<text x="50" y="25" id="txt">Test text</text>
</svg>
Alternatively instead of txt.getComputedTextLength() you may use txt.textLength.baseVal.value
The solution is pretty simple when you remember that the this binding in the attr() call refers to the associated HTML (SVG) element:
rects.attr("width",
d => this.parentNode.childNodes[1].getComputedTextLength() + 20
);
The rect is the first element in a list of SVG elements that make up the displayed node. The text for that node is at index 1 (as follows from the append calls).
Normally I would comment, but I don't have enough reputation points.
The accepted answer has the right idea, but it doesn't work, how he coded it. The first problem is, he uses an arrow function instead of an anonymus function. In arrow functions, this has a different scope. So use an anonymus function here.
The second problem is the order of rect and text, as you can see in the source code, in the question. Since rect is appended before text, the parent node doesn't have the child text yet. So you have to just append the rect, then append the text and set its attrs and then set the attrs of rect. So the solution is:
var rects = nodeEnter.append("rect")
var nodeText = nodeEnter.append("text")
.attr("class", "node-text")
.attr("y", rectH / 2)
.attr("dy", ".35em")
.text(function (d) {
return d.data.name;
});
nodeText // The bounding box is valid not before the node addition happened actually.
.attr("x", function (d) {
return (rectW - this.getBBox().width) / 2;
});
rect
.attr('width', function () {
return this.parentNode.childNodes[1].getComputedTextLength();
})
.attr("height", rectH);
Note: If you don't need the parameter d, you don't have to accept it, like I did.
I've created a DOM element which, when it fires a mouseover event, creates new DOM elements such as labels or a tooltip. Unfortunately, sometimes those elements are created underneath the mouse's current position. This causes the mouseleave event of that DOM element to fire, which is often responsible for removing the newly created DOM element.
In other words, when the new elements are created, the mouse is no longer "hovering" over the DOM element that originally fired the event, but the new DOM element. The browser reads this as a "mouseleave" event, which then fires the "mouseleave" function. This event, as it so happens, then removes the new element – even though the user has not moved the mouse.
This causes a cycle to occur, where the new elements are created and removed in rapid-fire, causing the blinking effect when a certain part of the DOM element is moused over. Here's a simplified version of the problem:
https://jsfiddle.net/KingOfCramers/8wbfjap4/2/
var svg = d3.select("svg").style("background-color","grey")
var data = ["This is a circle"]
var circle = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx",100)
.attr("cy",100)
.attr("r", 20)
.style("fill","red")
circle
.on("mouseover",function(d){
var xPos = d3.select(this).attr("cx")
var yPos = d3.select(this).attr("cy")
svg
.append("text").text(d)
.attr("x",xPos)
.attr("y",yPos)
})
.on("mouseleave",function(){
d3.select("text").remove();
})
Obviously this example is silly, but in instances where the data is much more crowded, simply moving the label by 10 or 15 pixels up or down is not a practical solution. I also cannot just create the label relative to the mouse cursor, because I will often be creating many at once using D3 data, for multiple DOM elements. What do most people do to solve this issue?
Thanks.
If you don't need to interact with that new element, just use pointer-events: none;:
.attr("pointer-events", "none")
Here is your code with that change:
var svg = d3.select("svg").style("background-color", "grey")
var data = ["This is a circle"]
var circle = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", 100)
.attr("cy", 100)
.attr("r", 20)
.style("fill", "red")
circle
.on("mouseover", function(d) {
var xPos = d3.select(this).attr("cx")
var yPos = d3.select(this).attr("cy")
svg
.append("text").text(d)
.attr("x", xPos)
.attr("y", yPos)
.attr("pointer-events", "none")
})
.on("mouseleave", function() {
d3.select("text").remove();
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width=500 height=500></svg>
I am trying to select a specific node and do some animation on it.
This is how I try to select it:
var selectedToEnlarge = node.filter(function (d, i) {
return d.name == selectedVal;
});
This variable actually return an array containing my node. Please see attached image:
The node returned is the one I need.
Now I try to animate it a bit, like this:
selectedToEnlarge.transition().duration(2000)
.attr("stroke-width", 20)
.attr("r", 10)
.transition()
.duration(2000)
.attr('stroke-width', 0.5)
.attr("r", 200)
.ease('sine')
But nothing is happening. I am assuming it is something with selectors.
Any insight is appreaciated :)
In Mike Bostocks example http://bost.ocks.org/mike/nations/ there is so much data that putting the names of the countries there would make it chaotic, but for a smaller project I would like to display it.
I found this in the source:
var dot = svg.append("g")
.attr("class", "dots")
.selectAll(".dot")
.data(interpolateData(2004))
.enter().append("circle")
.attr("class", "dot")
.style("fill", function(d) { return color(d); })
.text(function(d) { return d.name; })
.call(position)
.sort(order);
dot.append("title")
.text(function(d) { return d.name; });
But somehow a title never shows up. Does anybody have an idea, how to display the name, next to the bubble?
As the other answer suggests, you need to group your elements together. In addition, you need to append a text element -- the title element only displays as a tooltip in SVG. The code you're looking for would look something like this.
var dot = svg.append("g")
.attr("class", "dots")
.selectAll(".dot")
.data(interpolateData(2004))
.enter()
.append("g")
.attr("class", "dot")
.call(position)
.sort(order);
dot.append("circle")
.style("fill", function(d) { return color(d); });
dot.append("text")
.attr("y", 10)
.text(function(d) { return d.name; });
In the call to position, you would need to set the transform attribute. You may have to adjust the coordinates of the text element.
Unfortunately grouping the text and circles together will not help in this case. The bubbles are moved by changing their position attributes (cx and cy), but elements do not have x and y positions to move. They can only be moved with a transform-translate. See: https://www.dashingd3js.com/svg-group-element-and-d3js
Your options here are:
1) rewrite the position function to calculate the position difference (change in x and change in y) between the elements current position and its new position and apply that to the . THIS WOULD BE VERY DIFFICULT.
or 2) Write a parallel set of instructions to setup and move the tags. Something like:
var tag = svg.append("g")
.attr("class", "tag")
.selectAll(".tag")
.data(interpolateData(2004))
.enter().append("text")
.attr("class", "tag")
.attr("text-anchor", "left")
.style("fill", function(d) { return color(d); })
.text(function(d) { return d.name; })
.call(tagposition)
.sort(order);
You will need a separate tagposition function since text needs 'x' and 'y' instead of 'cx', 'cy', and 'r' attributes. Don't forget to update the "displayYear" function to change the tag positions as well. You will probably want to offset the text from the bubbles, but making sure the text does not overlap is a much more complicated problem: http://bl.ocks.org/thudfactor/6688739
PS- I called them tags since 'label' already means something in that example.
you have to wrap the circle element and text together , it should look like
<country>
<circle ></circle>
<text></text>
</country>
I am a newbie in d3.js
I am trying to add labels to my nodes.
But whatever I tried is not working..
My code is here:
http://jsfiddle.net/Ymtg5/1/
Its a mash up between http://bl.ocks.org/christophermanning/4208494
and force directed graphs.
Basically I am reading a json file and creating the said graph.
Now I want to add labels to node exactly like http://bl.ocks.org/mbostock/950642
I tried adding these lines
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name });
But its not working.
Any help. suggestions..
Thanks
Most probably the problem is that your JSON classes don't have a "name".
Right, this was not the problem
The relevant part of your code is as follows:
var node = svg.selectAll("path.node")
.data(nodes)
.enter().append("path").attr("class", "node")
.style("fill", function(d) { return fill(d.value); })
.style("stroke", function(d) { return d3.rgb(fill(d.value)).darker(); })
.call(force.drag);
// HERE should go node manipulation to add the text
force
.nodes(nodes)
.links(links)
.on("tick", tick)
.start();
function tick() {
//node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; );
node.attr("d", function(d) { return clip({"type":"F {"type":"Point","coordin...
link.attr("d", function(d) { return clip({"type":"Feature","geometry":{ ...
I have inserted a comment line where your node manipulation should go, if you want to add labels to the nodes. You are doing that inside the tick function (well, I think you are trying to do it there, the code isn't in the fiddle), and that function should be only for manipulation of the attr of the nodes. The place to create the text and append it to the node is outside of the function.