d3 tree node position - javascript

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.

Related

D3.js How to update chart display with new data?

I am trying to implement an update function for my D3 sunburst diagram where i can change the data that is displayed. I am able to successfully add or remove nodes.
However, i can't seem to be able to modify the existing data.
For example, if half of my chart is removed, i would like for the other half to fill the space leaved by the delete. Same thing goes when new data get added, i would like for the existing data to shrink and take less space
Here is my update function :
function update(newData) {
root = newData;
node = root;
g = svg.datum(root)
.selectAll("g")
.data(partition.nodes(root));
var newG = g.enter()
.append("g");
g.exit().selectAll("path").transition().duration(5000).attrTween("d", arcTween(0)).remove();
path = g.selectAll("path").transition().duration(5000).attr("d", arc);
newG.append("path")
.attr("d", arc);
};
Here is how the chart is built :
function render(data) {
root = data;
node = root;
width = $(".burst-chart-container").height();
height = ($(".burst-chart-container").width() / 2);
radius = Math.min(width, height) / 2;
x = d3.scale.linear().range([0, 2 * Math.PI]);
y = d3.scale.linear().range([0, radius]);
rad = Math.min(width, height) / Math.PI - 25;
partition = d3.layout.partition().sort(null).value(function (d) { return d.size; });
arc = d3.svg.arc()
.startAngle(function (d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.endAngle(function (d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
.innerRadius(function (d) { return Math.max(0, y(d.y)); })
.outerRadius(function (d) { return Math.max(0, y(d.y + d.dy)); });
svg = d3.select(element[0]).append('svg')
.attr("width", width).attr("height", height)
.attr("class", "svgDashboard")
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2) + ")");
g = svg.datum(root).selectAll("g")
.data(partition.nodes)
.enter()
.append("g")
path = g.append("path")
.attr("d", arc)
}
I do know that path.attr("d",arc) should update the visual, but it doesn't work in my case.
I think that it has something to do with the partition layout who dosen't tell the existing arcs that they need to change, or the way that I do the selection to update the data, but I might be wrong.
Any help would be appreciated.
I found out that the path and text data were never updated. My update did only change g element data. To correct it, I simply take the parent data and put into into his child after I updated it.
g = svg.datum(root)
.selectAll("g")
.data(partition.nodes(root));
//Add this part to update child elements
g.selectAll("path").each(function () {
d3.select(this).datum(d3.select(this.parentNode).datum());
});
g.selectAll("text").each(function () {
d3.select(this).datum(d3.select(this.parentNode).datum());
});
With those changes, i am able to call my attrTween function which update the visual with the new data.

Centering text inside an arc with d3

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)

How to add legend to the chart?

How can I add legend to the chart (see fiddle)? I tried to define the legend as follows, but then the chart disappears (see this fiddle).
var height = 900, width = 900;
var gridSize = Math.floor(width / 42);
var legendElementWidth = gridSize*2;
var legend = svg.selectAll(".legend")
.data([0].concat(colorScaleDomain.quantiles()), function(d) { return d; });
legend.enter().append("g")
.attr("class", "legend");
legend.append("rect")
.attr("x", height)
.attr("y", function(d, i) { return legendElementWidth * (i-0.5); })
.attr("width", gridSize / 2 )
.attr("height", legendElementWidth)
.style("fill", function(d, i) { return colors[i]; });
legend.append("text")
.attr("class", "mono")
.text(function(d) { return "≥ " + Math.round(d) + "%"; })
.attr("x", (height) + gridSize)
.attr("y", function(d, i) { return legendElementWidth*i; } );
legend.exit().remove();
This is a list of the problems:
There is no colorScaleDomain.quantiles(). It should be colorScale.quantiles() instead.
The order of the elements is very important in an SVG, which has no z index. So, your legends...
legend.enter().append("g")
.attr("class", "legend");
...should come after the drawing code for the chart. But that step can even be ignored, because of the third problem:
Your legends are outside the SVG. I corrected that with:
var svg = d3.select("body").append("svg")
.attr("width", diameter + 200)//adding some space in the SVG
And some more magic numbers in the legends code. Change them accordingly (magic numbers are not a good practice in most situations).
Here is your updated fiddle: https://jsfiddle.net/hsq05oq9/

D3 Circle Pack Layout with a horizontal arrangement

I'm trying to create a wordcloud with the D3 pack layout with a horizontal arrangement.
Instead of limiting the width, I am limiting the height.
The pack layout automatically disposes the circles with the larger one in the center and the others around him. If the height is limited, instead of expanding the circles disposition horizontally, it reduces the size of each circle.
How can I stop the layout from resizing the circles and start adding them to the sides if there is no more space around the larger one.
I want something like this:
http://imgur.com/7MDnKHF
But I'm only achieving this:
http://jsfiddle.net/v9xjra6c/
This is my current code:
var width,
height,
diameter,
padding,
format,
pack,
svg,
node;
var initSizes = function() {
var dimensions = { width: 900, height: 288 };
width = dimensions.width;
height = dimensions.height;
diameter = Math.min(width, height);
padding = 12;
format = d3.format(',d');
};
var initLayout = function() {
pack = d3.layout.pack()
.sort(null)
.size([width, height])
.padding(padding);
};
var createSVG = function() {
svg = d3.select('.chart-container').append('svg')
.attr('width', width)
.attr('height', height)
.attr('class', 'bubble');
};
var createBubbles = function() {
var dataset = pack.nodes(DATA);
node = svg.selectAll('.node')
.data(dataset.filter(function(d) { return !d.children; }))
.enter().append('g')
.attr('class', 'node')
.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; });
node.append('title')
.text(function(d) { return d.name + ': ' + format(d.value); });
node.append('circle')
.attr('r', function(d) { return d.r; });
node.append('text')
.attr('dy', '.3em')
.style('text-anchor', 'middle')
.text(function(d) { return d.name.substring(0, d.r / 3); });
};
initSizes();
initLayout();
createSVG();
createBubbles();
Thanks!
Your solution would be like merging this Example1 + Example2
So from Example 1 I have taken the mechanism to restrict the circles with in the bounds, such that it does not go beyond the svg height and width:
function tick(e) {
node
.each(cluster(10 * e.alpha * e.alpha))
.each(collide(.5))
//max radius is 50 restricting on the width
.attr("cx", function(d) { return d.x = Math.max(50, Math.min(width - 50, d.x)); })
//max radius is 50 restricting on the height
.attr("cy", function(d) { return d.y = Math.max(50, Math.min(height - 50, d.y)); }); }
Creating a scale for making radius
//so now for your data value which ranges from 0 to 100 you will have radius range from 5 to 500
var scale = d3.scale.linear().domain([0,100]).range([5, 50]);
Make the data as per Example2
var nodes = data.map(function(d){
var i = 0,
r = scale(d.value),
d = {cluster: i, radius: r, name: d.name};
if (!clusters[i] || (r > clusters[i].radius)) {clusters[i] = d;}
return d
});
Finally result will be looking like this
Note: You can reduce the height in the code and the circles will rearrange as per the space available.
Note: You can also play around the cluster to group similar nodes as in example in my case I have made a single group cluster.
Hope this helps!

D3.js Auto font-sizing based on nodes individual radius/diameter

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";
})

Categories

Resources