Here's my use case: I'm creating a sun burst using d3. Inside each node, I would like to center the text.
It doesn't seem to work.
I've tried using the method mentioned here:
d3js - how to add the text each of the arc's centre, after animation end?
But it doesn't seem to work:
https://codepen.io/yonatankra/pen/awEYgR?editors=0010
More specifically:
var text = svg.selectAll(".node").append("text")
.attr("text-anchor","middle")
.attr("x", function(d) {
const middleRadius = d.y1 - d.y0;
arc = d3.arc().innerRadius( middleRadius - 1 ).outerRadius( middleRadius + 1 );
const x = arc.centroid(d);
return x[0]*Math.cos(x[1]);
})
.attr("y", function(d) {
const middleRadius = d.y1 - d.y0;
arc = d3.arc().innerRadius( middleRadius - 1 ).outerRadius( middleRadius + 1 );
const x = arc.centroid(d);
return middleRadius*Math.sin(x[1]);
})
/* .attr("dy", "-28") */
// .attr("dx", "6") // margin
// .attr("dy", ".35em") // vertical-align
.text(function(d) {
return d.data.name
})
I've been trying in various ways and have ready multiple guides on how to do this but nothing works as expected.
How would you change the code to enter the text in the center of the arc?
And another theoretical question - why does the arc.centroid return the same values for different arcs?
var text = svg.selectAll(".node").append("text").attr("transform", function(d){
let angle = ((d.startAngle + d.endAngle)/2);
let hyp = (d.depth * 50) + 25;
let x = Math.sin(angle) * hyp;
let y = Math.cos(angle) * hyp;
.attr("text-anchor", "middle")
.text(function(d) {
return d.data.name
})
To find the center of the arc, calculate the angle bisector for the arc let angle = ((d.startAngle + d.endAngle)/2);
The hypotenuse will be current depth * 50 50 being the (outer radius - inner radius)/2
Once we have the hypotenuse and the angle then x and y positions can be calculated with pythagoras theorem.
Set text anchor middle to align at the center of the arc.
-y is used instead of y as the graph is going upwards (which is -ve y scale)
Related
I'm using d3 to draw a pie with radio button to change the paths showed. My problem is to recalculate the label position based on the new paths. On load the labels are drawn correctly but on click the position still the same of first load.
I think that the problem is that the g's take only the first data value and i don't know how to say to take the current data values.
The function that draw the labels is
//Labels
d3.selectAll("g").select("text").transition()
.ease("linear")
.duration(500)
.style("opacity", 0).remove();
svg.selectAll("g").append("text")
.attr("transform", function (d) {
var c = arc.centroid(d),
x = c[0],
y = c[1],
// pythagorean theorem for hypotenuse
h = Math.sqrt(x * x + y * y);
return "translate(" + (x / h * labelr) + ',' +
(y / h * labelr) + ")";
})
.attr("dy", ".35em")
.style("opacity", 0)
.style("fill", "#000")
.style("font-size", 12)
.attr("text-anchor", function (d) {
// are we past the center?
return (d.endAngle + d.startAngle) / 2 > Math.PI ?
"end" : "start";
})
.text(function (d) { return d.data.label; })
.transition()
.ease("linear")
.delay(1000)
.duration(500)
.style("opacity", 1);
For more info see https://jsfiddle.net/w0ckw4tb/
Thanks
Just apply new data binding before you append new text nodes:
svg.selectAll("g").data(pie) // <== !!!
.append("text")
.attr("transform" ...
If you did not do it, this code:
.attr("transform", function (d) {
var c = arc.centroid(d),
x = c[0],
y = c[1],
// pythagorean theorem for hypotenuse
h = Math.sqrt(x * x + y * y);
return "translate(" + (x / h * labelr) + ',' +
(y / h * labelr) + ")";
})
returns the same value for transform attribute so the labels remain at the same place.
Check working fiddle.
I use the d3 chord diagram example of Andrew and want to center all text labels within the curved slice. I tried many things but was never able to center the texts. Do you know what wizzard trick there is needed?
var width = 720,
height = 720,
outerRadius = Math.min(width, height) / 2 - 10,
innerRadius = outerRadius - 24;
var formatPercent = d3.format(".1%");
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var layout = d3.layout.chord()
.padding(.04)
.sortSubgroups(d3.descending)
.sortChords(d3.ascending);
var path = d3.svg.chord()
.radius(innerRadius);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("id", "circle")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
svg.append("circle")
.attr("r", outerRadius);
d3.csv("ex_csv.csv", function(cities) {
d3.json("ex_json.json", function(matrix) {
// Compute the chord layout.
layout.matrix(matrix);
// Add a group per neighborhood.
var group = svg.selectAll(".group")
.data(layout.groups)
.enter().append("g")
.attr("class", "group")
.on("mouseover", mouseover);
// Add the group arc.
var groupPath = group.append("path")
.attr("id", function(d, i) { return "group" + i; })
.attr("d", arc)
.style("fill", function(d, i) { return cities[i].color; });
// Add a text label.
var groupText = group.append("text")
.attr("x", 6)
.attr("dy", 15);
groupText.append("textPath")
.attr("xlink:href", function(d, i) { return "#group" + i; })
.text(function(d, i) { return cities[i].name; });
// Remove the labels that don't fit. :(
groupText.filter(function(d, i) { return groupPath[0][i].getTotalLength() / 2 - 16 < this.getComputedTextLength(); })
.remove();
// Add the chords.
var chord = svg.selectAll(".chord")
.data(layout.chords)
.enter().append("path")
.attr("class", "chord")
.style("fill", function(d) { return cities[d.source.index].color; })
.attr("d", path);
}
});
});
</script>
As an aside, I would suggest looking to upgrade to v4, documentation for v2 is nearly non-existent and is very hard to help with.
You can set both the text-anchor and the startOffset property to achieve what you are looking for.
First, you'll want to set text-anchor to middle as it is easier to specify the middle point than to find the middle point and work back to find where the text should start.
Second you'll need to set a startOffset. Note that if you use 50%, the text will not appear where you want, as the total length of the text path is all sides of the closed loop (chord anchor) you are appending to. Setting it to 25 % would work if you did not have a different outer and inner radius. But, as you have an outer radius that is 24 pixels greater than the inner radius you can try something like this to calculate the number of pixels you need to offset the center of the text:
groupText.append("textPath")
.attr("xlink:href", function(d, i) { return "#group" + i; })
.text(function(d, i) { return cities[i].name; })
.attr("startOffset",function(d,i) { return (groupPath[0][i].getTotalLength() - 48) / 4 })
.style("text-anchor","middle");
I subtract 48 because the sides of the anchor are 24 pixels each (the difference in the radii). I divide by four because the path doubles back on itself. If it was a general line I would just divide by two.
This approach is a little simplistic as the outer circumference is not the same as the inner circumference of each chord anchor, so I am off by a little bit, but it should be workable.
For labels that are on the cusp of being displayed, this will be awkward: the inner radius is shorter, so the formula for deteriming if a string is short enough to be displayed may be wrong - which may lead to some characters climbing up the side of the anchor (your example also 16 pixels as the difference in radii to calculate if text is too long, rather than 24).
This is the end result:
Here is a demonstration.
So, I'm basically trying to make a multilevel circular partition (aka sunburst diagram) with D3.js (v4) and a JSON data.
I placed some labels, which must have different angles depending of their levels (circles) on the partition :
- Level < 3 must be curved and "follow" the arc radius.
- level == 3 must be straight and perpendicular of the arc radius.
I didn't use textPath tags, because I'm not really experienced in SVG and it looks overly complicated to me, and I don't really know how to use it.
here's my code (without the JSON but this is a really classical one, I can add a part of it if it is needed):
var width = 800;
var height = 800;
var radius = 400;
var formatNumber = d3.format(",d");
var x = d3.scaleLinear().range([0, 2 * Math.PI]);
var y = d3.scaleSqrt().range([0, radius]);
var arc = d3.arc()
.startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x0))); })
.endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x1))); })
.innerRadius(function(d) { return setRadius("inner", d.data.level); })
.outerRadius(function(d) { return setRadius("outer", d.data.level); });
var svg = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width/2 + "," + (height/2) + ")");
var hierarchy = d3.hierarchy(dataset)
.sum(function(d) { return d.size; });
var partition = d3.partition();
svg.selectAll("path")
.data(partition(hierarchy).descendants())
.enter().append("path")
.attr("id", function(d, i){ return "path" + i; })
.attr("d", arc)
.attr("stroke", "white")
.attr("stroke-width", "1px")
.style("fill", function(d) { return (d.data.color) ? d.data.color : 'black'; });
svg.selectAll("text")
.data(partition(hierarchy).descendants())
.enter().append("text")
.attr("transform", function(d){ return setLabelPosition(d); })
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("font-size", "18px")
.attr("fill", function(d){ return d.data.textcolor; })
.text(function(d){ if(parseInt(d.data.level) > 0 && parseInt(d.data.level) < 4){ return (d.data.name).toUpperCase(); }});
d3.select(self.frameElement)
.style("height", height + "px");
function setRadius(side, level){
var result = 0;
var innerValues = [0, 120, 180, 240, 365];
var outerValues = [0, 180, 240, 365, 400];
if(!side){
throw error;
}
if(side === "inner"){
result = innerValues[level];
}
if(side === "outer"){
result = outerValues[level];
}
return result;
};
function setLabelPosition(d){
var result = '';
var angle = 0;
var centroid = arc.centroid(d);
if(parseInt(d.data.level) === 3){
angle = (180/Math.PI * (arc.startAngle()(d) + arc.endAngle()(d))/2 - 90);
if(angle > 90){
angle = angle - 180;
}
result = "translate(" + centroid + ")rotate(" + angle + ")";
} else {
angle = (180/Math.PI * (arc.startAngle()(d) + arc.endAngle()(d))/2);
result = "translate(" + centroid + ")rotate(" + angle + ")";
}
return result;
};
And the result :
My problem is, how to curve these level 1 & 2 labels (like the one which have a red border), but keep my lvl 3 labels as they currently are.
It's really a pain in the head, and I did many search (on Google and SO) but I didn't find any satisfying answer.
A solution without using a textPath will be awesome if possible, but any advice is welcome.
Many thanks guys and sorry for my English (as you can probably see it's not my birth language).
PS : This is D3.js v4.
How can I position the nodes of a d3 tree chart with variable dimensions?
here is what I have so far, as you can see the positioning is way off
http://jsfiddle.net/cdad0jyz/1/
I have tried to use the separation method:
var tree = d3.layout.tree()
.nodeSize([10,10])
.separation(function(a, b) {
var width = 200, // here should be a value calculated for each node
distance = width / 2 + 16;
return distance;
});
In your code, you set x and y on your text elements but not on the corresponding rects. To further complicate it, each rect has a random width and height making it difficult to center it on the text.
This is one way to solve it:
nodeEnter.append("rect")
.attr("width", genRand) //rectW
.attr("height", genRand) // rectH
.attr("x", function(){
return (rectW / 2) - (d3.select(this).attr('width') / 2);
})
.attr("y", function(){
return (rectH / 2) - (d3.select(this).attr('height') / 2);
})
.attr("stroke", "black")
.attr("stroke-width", 1)
.style("fill", function (d) {
return d._children ? "lightsteelblue" : "#fff";
});
Updated fiddle.
How can I have D3.js automatically adjust font-size for each node based on their individual radius/diameter?
I use a style that allows automatic increase insize
node.append("text")
.attr("dy", ".3em")
.style("text-anchor", "middle")
.text(function(d) { return d.className.substring(0, d.r / 3); })
.style("font-size", "10px") // initial guess
//This is what gives it increased size...
.style("font-size", function(d) { return (2 * d.r - 10) / this.getComputedTextLength() * 10 + "px"; })
; * 10 + "px"; })
This effect removes the text from the smaller nodes. I also have a zoom function that I can increase a dot that originally cover 12 px to cover my entire screen.
.call(d3.behavior.zoom().scaleExtent([1, 200]).on("zoom", zoom))
Is there a way I can automatically format node-font individually; to write at appropriate sizes so when zoom-in the called node-font will appear proportionate to node-size vs a single font-size fits all?
The Right Lists Circles: NAME(SIZE) I would love a working examples to learn from. So at the image size the little green dot north of driving circle next to the P would have black unreadable words until we zoom in to see what is written on the circle. The goal is to have proportionate readable font when zoomed in..?
You can do this by dynamically setting the text size based on the size of the container. For this, you have to add the text, get its bounding box, get the bounding box of the container element and derive the correct font size based on the current font size and those bounding boxes.
The code would look something like this.
// ...
.append("text")
.text("text")
.style("font-size", "1px")
.each(getSize)
.style("font-size", function(d) { return d.scale + "px"; });
function getSize(d) {
var bbox = this.getBBox(),
cbbox = this.parentNode.getBBox(),
scale = Math.min(cbbox.width/bbox.width, cbbox.height/bbox.height);
d.scale = scale;
}
To offset text within a circle, rather than running along the diameter, I implement this differently:
dy shifts a text node up or down within the circle and is used to calculate the width or the chord to size the text.
The scale is then stored in a data attribute on the text element, rather than modifying the source data.
jsfiddle
function appendScaledText(parentGroup, textVal, dyShift) {
parentGroup
.append("text")
.attr("dy", dyShift)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "central")
.attr("font-family", "sans-serif")
.attr("fill", "white")
.text(textVal)
.style("font-size", "1px")
.each(getSize)
.style("font-size", function() {
return d3.select(this).attr("data-scale") + "px";
});
}
function getSize() {
var d3text = d3.select(this);
// in other cases could be parentElement or nextElementSibling
var circ = d3.select(this.previousElementSibling);
var radius = Number(circ.attr("r"));
var offset = Number(d3text.attr("dy"));
// TODO: this could be bounding box instead
var textWidth = this.getComputedTextLength();
// TODO: could adjust based on ratio of dy to radius
var availWidth = chordWidth(Math.abs(offset), radius);
// fixed 15% 'padding', could be more dynamic/precise based on above TODOs
availWidth = availWidth * 0.85;
d3text.attr("data-scale", availWidth / textWidth);
}
function chordWidth(dFromCenter, radius) {
if (dFromCenter > radius) return Number.NaN;
if (dFromCenter === radius) return 0;
if (dFromCenter === 0) return radius * 2;
// a^2 + b^2 = c^2
var a = dFromCenter;
var c = radius;
var b = Math.sqrt(Math.pow(c, 2) - Math.pow(a, 2)); // 1/2 of chord length
return b * 2;
}
Alternatively, you can create text label embedded with each node as follows:
this.g.append("g")
.attr("class", "labels")
.selectAll(".mytext")
.data(NODE_DATA)
.enter()
.append("text")
.text(function (d) {
return d.LabelText; // Here label text is the text that you want to show in the node
})
.style("font-size", "1px")
.attr("dy", ".35em") // You can adjust it
.each(function (d) {
var r = Number(d.Size), a = this.getComputedTextLength(),
c=0.35, // Same as dy attribute value
b = 2*Math.sqrt(r*r-c*c), s = Math.min(r, b/a);
d.fs = s;
})
.style("font-size", function (d) {
return d.fs + "px";
})