D3 text positioning inside g element - javascript

I am new to D3 and I am trying to add text inside rectangle using D3 v5. I have written following code for same.
rootSVG = d3.select('.rootSVG')
.selectAll('rect')
.data(data)
.enter()
.append('g')
.attr('transform', (d, i, elements) => {
return 'translate(0, ' + i * 21 + ')';
});
rootSVG.append('rect')
.attr('height', 20)
.attr('width', 100)
.style('fill', 'green')
.on('mouseover', (d, i, elements) => {
d3.select(elements[i])
.transition()
.duration(500)
.style('fill', 'red');
})
.on('mouseout', (d, i, elements) => {
d3.select(elements[i])
.transition()
.duration(500)
.style('fill', 'green');
});
rootSVG.append('text')
.attr('x', 10)
.text((d, i, elements) => {
return d.name;
});
I am getting the following results in the browser.
As you can see in above picture, rectangle elements are getting placed fine but text elements are off. Why this behaviour is happening even though they belong to same group? How to I make sure that text always stays inside the rectangle?
JSFiddle https://jsfiddle.net/ysm0hfzn/4/

This is due to how SVG draws text.
It's somewhat different from playing with divs and all in "traditional" HTML: you could think of it as an actual graphics framework.
The problem here is that the text element's baseline is, by default, its bottom edge. Which means that, when drawing text at (0, 0), the text element's bottom-left corner will be at (0, 0)
You could change your text elements' dominant baseline by adding the following CSS to your code:
g > text {
dominant-baseline: text-before-edge;
}
This would allow your texts to be drawn inside your gs, vertically.
As a side note, the horizontal-axis equivalent of the baseline for text is determined by the text-anchor property. If you wanted to center your text inside their g, you could simply:
anchor the text elements in to their middle: text-anchor: middle
center the anchor within your g: x="50"
Here's a code snippet demonstrating how to use the properties I mentionned in your example.
g > text {
dominant-baseline: text-before-edge;
text-anchor: middle;
}
<svg>
<g>
<rect width="100" height="20" fill="LimeGreen"></rect>
<text x="50">Letter</text>
</g>
<g transform="translate(0, 22)">
<rect width="100" height="20" fill="LimeGreen"></rect>
<text x="50">Number</text>
</g>
</svg>

Related

Appending title void transition animation in Javascript

I am trying to add transition() and append("title") to allow animation and hover over tooltip at the same time on my scatterplot. But when I add transition, tooltip no longer pops out.
const circles = g.merge(gEnter).selectAll('circle').data(data);
circles
.enter().append('circle').merge(circles).transition().duration(2000)
.attr('cy', d => yScale(yValue(d)))
.attr('cx', d => xScale(xValue(d)))
.attr('r', d => (d.Score*1.29)+1)
.attr("fill", d=>colorArray(d.Type))
.append("title").text(d => (d.Country_or_region+ "\n" +"Rank: "+ d.Overall_rank))
Everything after .transition() is a transition, and there is no .append() method for a transition. If you inspect the console you'll probably see an error.
That said, if for whatever reason you want append the title only after the transition started, you can use the .on("start") method:
.on("start", function() {
d3.select(this).append("title").text("foo")
})
Here is the demo, there is no title at the beginning. After the transition starts you'll get the title when hovering over the circle:
const circle = d3.select("circle");
circle.transition()
.duration(20000)
.delay(2000)
.attr("cx", 100)
.on("start", function() {
d3.select(this).append("title").text("foo")
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg>
<circle r="20" cx="20" cy="100"></circle>
</svg>

How can I dynamically resize an SVG rect based on text width?

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.

How to select elements according to an attribute(fill color) in D3.js with SelectAll

I have sets of circles on a map like 10 red circles, 10 blue circles, 10 green circles. How can i select only red circles using d3 selectAll or select?
Or is there any other methods than that?
the color has been given like this(as the value of "fill" in "style" attribute,
feature = g.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("id", function (d) {
return d.ArtistID + d.FollowerID;
})
.style("stroke", "black")
.style("opacity", .6)
.style("fill", function (d) {
if (d.ArtistID == 1) {
return "red"
} else if (d.ArtistID == 2) {
return "blue"
} else {
return "green"
};
})
.attr("r", 10);
so, the circles will be drawn like this,
<circle id="10" r="10" transform="translate(695,236)" style="stroke: rgb(0, 0, 0); opacity: 0.6; fill: rgb(255, 255, 0);"></circle>
I want to select the circles of red color. Cam somebody help?
Thanks in Advance.
You're asking to select based on the value of the style attribute. The fill property is nested within the style attribute; it is not a direct attribute of the DOM node. So you won't be able to select it using an attribute selector (ie the method #LarsKotthoff linked to).
You can switch to setting the fill using the SVG fill attributes, .attr('fill', ...) instead of using style('fill'), which will allow you to later select it using an attribute selector.
That aside, selecting via the fill attribute doesn't seem too elegant. If your design evolves and you start using a different fill color, you'll have to change the attribute selector as well. Assigning it a class and selecting based on the class is more appropriate.
Perhaps the best is to select based on data. Eg:
g.selectAll('circle')
.filter(function(d) { return d.ArtistID == 1; })
.style('fill', function(d, i) {
// here you can affect just the red circles (bc their ArtistID is 1)
})

How to display names next to bubbles in d3js motion chart

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>

D3 SVG I want to highlight an array of elements by mouse dragging

I have an SVG container with a number of elements each containing a rect and some text elements in a horizontal array.
I want to click on one element and drag to one of the others. The first element and element dragged over should highlight - as it would in a normal text editor selection.
at the end I want to know the first and last elements selected so that I can access their data.
I tried this using d3 drag behaviour but:
1. the intermediate elements can't be highlighted
2. the dragend does not tell me which element was the final one.
Also tried using mouse events but:
1. I can highlight each intermediate item but not easily remove the highlight if the mouse is moved back towards the beginning.
2. if the mouse is moved out of the container I can miss the mouse up events leaving highlighted elements.
3. I still don't really know upon which element I am finishing unless I collect all the mouse over events.
I don't actually want to move the selected elements - just know the first and last one selected.
I created this fiddle to illustrate the problem: http://jsfiddle.net/avowkind/up54b/5/
HTML
<svg class='demosvg' version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" >
<rect width="400" height="220"></rect>
<g id="container" transform="translate(10,10)"></g>
</svg>
Javascript
var mydata = [1, 2, 3, 4, 5];
/* each box is a group with a rect and text
positioned according to the data value
on drag/drop we want to know the dragged element, and the one it is dropped on.
we would like to highlight all the intervening elements too.
Ultimately we just want a result e.g. 2 was dragged to 5
*/
var boxg = d3.select('#container').selectAll('g').data(mydata).enter()
.append('svg:g')
.attr('id', function (d, i) {
return "b" + d;
})
.attr('transform', function (d, i) { return "translate(" + d * 50 + ",80)";})
.call(d3.behavior.drag()
.on("dragstart", function (d, i) {d3.select(this).classed("selected", true); })
.on("drag", function (d, i) {
// which element are we over here - to highlight it
var el = document.elementFromPoint(d3.event.x, d3.event.y);
console.log(el);
})
.on("dragend", function (d, i) {
console.log(d);
console.log(this);
console.log(d3.event);
// turn off all highlights
d3.selectAll('#container g').classed("selected", false);
// Which box got dropped on here ?
})
);
boxg.append('svg:rect')
.classed('box', true)
.attr('width', 40).attr('height', 40);
boxg.append('svg:text')
.text(function(d,i) { return d; })
.attr('dx', 15).attr('dy', 20);
CSS
.demosvg { fill:silver;}
.box { fill:lightblue;}
text { fill:black;}
.selected .box{ fill:gold;}
Thanks Andrew

Categories