D3.js change width of container after it is drawn - javascript

I have a seemingly simple d3.js problem. I am creating a tree from a set of json data. The tree is composed of labels that are composed of a rectangle container that wrap around some text. I would like to change the width of the rectangle according to the length of the text. I understand I should be doing something like this one, but I am struggling to understand how.
Here is my JS code (stripped down of most unnecessary frills):
var rectW = 140, rectH = 40;
// Declare the nodes.
var node = draw.selectAll('g.node')
.data(nodes, function(d) { return d.id; });
// Enter the nodes.
var nodeLabel = node.enter().append('g')
.attr('transform', function(d) { return 'translate(' + source.x0 + ',' + source.y0 + ')'; });
var nodeRect = nodeLabel.append('rect')
.attr('width', rectW)
.attr('height', rectH);
var nodeText = nodeLabel.append('text')
.attr('x', rectW / 2)
.attr('y', rectH / 2)
.text(function (d) { return d.name; });
As you can see, I create an SVG group to which I append both the container rectangle and the contained text.
Now, I would like to retrieve the length of each text element, and use it to change the width of the corresponding rectangle element. How can I do that? I tried with every possible combination of D3 directives I could think of, but my knowledge of the library is not enough advanced to suit my purposes.
UPDATE
Thanks to Geraldo Furtado's answer, I managed to fix this issue by adding the following:
// This arranges the width of the rectangles
nodeRect.attr("width", function() {
return this.nextSibling.getComputedTextLength() + 20;
})
// This repositions texts to be at the center of the rectangle
nodeText.attr('x', function() {
return (this.getComputedTextLength() + 20) /2;
})

This is the current structure of your nodes:
<g>
<rect></rect>
<text></text>
</g>
That being the case, the texts are the nextSiblings of the rectangles. Therefore, all you need to get the length of the texts is using nextSibling in the rectangle selection:
nodeRect.attr("width", function() {
return this.nextSibling.getComputedTextLength() + rectW
})
Here I'm adding rectW to keep the same padding on the left and right, since you're putting the texts to start at rectW / 2.
If you're not sure about the relationship between the texts and rectangles (who is the first child, who is the last child...), you can go up, select the group element and then select the text inside it:
nodeRect.attr("width", function() {
return d3.select(this.parentNode).select("text").node().getComputedTextLength() + rectW
})
Here is a basic demo:
var data = [{
name: "some text",
x: 10,
y: 10
},
{
name: "A very very very long text here",
x: 100,
y: 50
},
{
name: "Another text, this time longer than the previous one",
x: 25,
y: 100
},
{
name: "some short text here",
x: 220,
y: 150
}
];
var svg = d3.select("svg");
var rectW = 140,
rectH = 30;
var node = svg.selectAll(null)
.data(data);
var nodeLabel = node.enter().append('g')
.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
var nodeRect = nodeLabel.append('rect')
.attr('width', rectW)
.attr('height', rectH)
.style("fill", "none")
.style("stroke", "gray")
var nodeText = nodeLabel.append('text')
.attr('x', rectW / 2)
.attr('y', rectH / 2)
.style("dominant-baseline", "central")
.text(function(d) {
return d.name;
});
nodeRect.attr("width", function() {
return this.nextSibling.getComputedTextLength() + rectW
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="300"></svg>
For storing the computed width and using it later on, you can set another property. For instance:
nodeRect.attr("width", function(d) {
return d.rectWidth = this.nextSibling.getComputedTextLength() + rectW
});
Here is the demo, look at the console:
var data = [{
name: "some text",
x: 10,
y: 10
},
{
name: "A very very very long text here",
x: 100,
y: 50
},
{
name: "Another text, this time longer than the previous one",
x: 25,
y: 100
},
{
name: "some short text here",
x: 220,
y: 150
}
];
var svg = d3.select("svg");
var rectW = 140,
rectH = 30;
var node = svg.selectAll(null)
.data(data);
var nodeLabel = node.enter().append('g')
.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
var nodeRect = nodeLabel.append('rect')
.attr('width', rectW)
.attr('height', rectH)
.style("fill", "none")
.style("stroke", "gray")
var nodeText = nodeLabel.append('text')
.attr('x', rectW / 2)
.attr('y', rectH / 2)
.style("dominant-baseline", "central")
.text(function(d) {
return d.name;
});
nodeRect.attr("width", function(d) {
return d.rectWidth = this.nextSibling.getComputedTextLength() + rectW
});
nodeLabel.each(function(d) {
console.log(d)
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="300"></svg>

You can add the following line at the end of your script, after the text has been set:
nodeRect
.transition()
.attr("width",function(){
return Math.max(rectW,nodeText.node().getComputedTextLength())
}).delay(0).duration(500)
I used Math.max to set it to rectW if its large enough or expand if necessary. You can perhaps adapt that part.

Related

Labels and Dividing Lines in D3

Below is a fiddle I have created using a basic D3 pie chart. I am new to D3 and not sure how to do a couple of things. First I would like to correct the labels. As you can see, the 'Correct' label is flying off the edge of the graph currently. But I am not sure how to fix this?
Second, I would like to add a black line around the graph and between the data slices to give the green and red contrast. If you have any ideas of how to do either, I would greatly appreciate it. Thanks!
https://jsfiddle.net/01qew1jk/
JS:
let incorrect = this.resLength - this.points;
let correct = this.points;
let data = [{
'name': 'Correct',
'points': correct
},
{
'name': 'Incorrect',
'points': incorrect
}];
let width = 400, height = 400, radius = Math.min(width, height)/2;
let color = d3.scaleOrdinal().range(['#15B61F', '#DA1821']);
let pie = d3.pie().value(function(d){
return d.points;
})(data);
let arc = d3.arc().outerRadius(radius - 10).innerRadius(0);
let labelArc = d3.arc().outerRadius(radius - 40).innerRadius(radius - 40);
let svg = d3.select('#pie').append('svg').style('display', 'block').style('margin', '0 auto').attr('width', width).attr('height', height).append('g').attr('transform', 'translate(' + width/2 + ',' + height/2 + ')').attr('class', 'float-center');
let g = svg.selectAll('arc').data(pie).enter().append('g').attr('class', 'arc');
g.append('path').attr('d', arc).style('fill', function(d){
return color(d.data.name)
})
g.append('text').attr('transform', function(d) {
return 'translate(' + labelArc.centroid(d ) + ')';
}).text(function(d) {
return d.data.name;
}).style('fill', '#FFF')
For positioning the texts, don't create a new arc generator with different outer and inner radii. Just use the same arc generator you used for the paths:
g.append('text').attr("text-anchor", "middle")
.attr('transform', function(d) {
return 'translate(' + arc.centroid(d) + ')';
//same generator ------^
})
.text(function(d) {
return d.data.name;
})
.style('fill', '#FFF')
To add that "black line", just set the path stroke:
.style("stroke", "black");
Here is your code with those changes:
let incorrect = 3
let correct = 2
let data = [{
'name': 'Correct',
'points': correct
}, {
'name': 'Incorrect',
'points': incorrect
}];
let width = 400,
height = 400,
radius = Math.min(width, height) / 2;
let color = d3.scaleOrdinal().range(['#15B61F', '#DA1821']);
let pie = d3.pie().value(function(d) {
return d.points;
})(data);
let arc = d3.arc().outerRadius(radius - 10).innerRadius(0);
let svg = d3.select('body').append('svg').style('display', 'block').style('margin', '0 auto').attr('width', width).attr('height', height).append('g').attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')').attr('class', 'float-center');
let g = svg.selectAll('arc').data(pie).enter().append('g').attr('class', 'arc');
g.append('path').attr('d', arc).style('fill', function(d) {
return color(d.data.name)
}).style("stroke", "black")
g.append('text').attr("text-anchor", "middle")
.attr('transform', function(d) {
return 'translate(' + arc.centroid(d) + ')';
}).text(function(d) {
return d.data.name;
}).style('fill', '#FFF')
<script src="https://d3js.org/d3.v4.min.js"></script>

D3 v4 Update-Pattern for Groups

Given the following layout:
<g>
... // many nodes
<g>
<circle></circle>
<text></text>
</g>
...
</g>
How would a correct update pattern look like in d3 v4?
What do I have to use as parameter in merge(?), how often do I have to call merge (only on node? node + circle + text ?)
I created an working example on fiddle: https://jsfiddle.net/cvvfsg97/6/
Code:
function update(items) {
node = nodeLayer.selectAll(".node")
.data(items, function(d) { return d.id; })
node = node.enter() // insert
.append("g")
.attr("class", "node");
node.append("circle") // insert
.attr("r", 2.5)
.attr('class', 'circle')
.merge(nodeLayer.selectAll('.node > circle')) // is this correct?! // merge
.attr('fill', 'red') // just for testing purposes
.exit().remove(); // exit
node.append("text") // insert
.attr("dy", 3)
.text(function(d) { return d.name; })
.merge(nodeLayer.selectAll('.node > text')) // is this correct?! // merge
.attr('fill', 'green') // just for testing purposes
.exit().remove();
node.merge(nodeLayer.selectAll('.node')) // is this correct?! // merge
.attr('class', 'anotherClass')
.exit().remove(); // does not work // exit
}
Could someone bring some clarity in terms of how to use enter(), merge(), exit() in groups?
I potentially like to do changes in every stage for every element.
Update: I simplified the example, I don't need links or a force-layout. My question is only about the update-pattern, not about forces. The updated jsfiddle does not have the force-layout.
You are over complicating the pattern. Here's your update function written properly:
function update(items) {
var node = nodeLayer.selectAll(".node") // bind the data, this is update
.data(items, function(d) {
return d.id;
});
node.exit().remove(); // exit, remove the g
nodeEnter = node.enter() // enter, append the g
.append("g")
.attr("class", "node");
nodeEnter.append("circle") // enter, append the circle on the g
.attr("r", 2.5)
.attr('class', 'circle')
.attr('fill', 'red');
nodeEnter.append("text") // enter, append the text on the g
.attr("dy", 3)
.text(function(d) {
return d.name;
})
.attr('fill', 'green');
node = nodeEnter.merge(node); // enter + update on the g
node.attr('transform', function(d){ // enter + update, position the g
return 'translate(' + d.x + ',' + d.y + ')';
});
node.select("text") // enter + update on subselection
.text(function(d) {
return d.name;
});
}
Here it is running with multiple calls:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>
var nodeLayer = d3.select('body')
.append('svg')
.attr('width',500)
.attr('height',500);
update([
{
id: 1,
name: 'A',
x: Math.random() * 500,
y: Math.random() * 500
},{
id: 2,
name: 'B',
x: Math.random() * 500,
y: Math.random() * 500
},{
id: 3,
name: 'C',
x: Math.random() * 500,
y: Math.random() * 500
}
]);
setTimeout(function(){
update([
{
id: 1,
name: 'A',
x: Math.random() * 500,
y: Math.random() * 500
},{
id: 4,
name: 'This is a new name...',
x: Math.random() * 500,
y: Math.random() * 500
},{
id: 3,
name: 'C',
x: Math.random() * 500,
y: Math.random() * 500
}
]);
}, 3000);
function update(items) {
var node = nodeLayer.selectAll(".node")
.data(items, function(d) {
return d.id;
});
node.exit().remove(); // exit, remove the g
nodeEnter = node.enter() // enter the g
.append("g")
.attr("class", "node");
nodeEnter.append("circle") // enter the circle on the g
.attr("r", 2.5)
.attr('class', 'circle')
.attr('fill', 'red');
nodeEnter.append("text") // enter the text on the g
.attr("dy", 3)
.attr('fill', 'green');
node = nodeEnter.merge(node); // enter + update
node.attr('transform', function(d){
return 'translate(' + d.x + ',' + d.y + ')';
});
node.select("text")
.text(function(d) {
return d.name;
});
}
</script>
</body>
</html>
I've done this recently in my code - I use select(subSelector) on the current selection that contains the elements. In your example I would change it as follows:
function update(items) {
var node = nodeLayer.selectAll(".node")
.data(items, function(d) { return d.id; })
var nodeEnter = node.enter()
.append("g")
.attr("class", "node");
nodeEnter.append("circle")
.attr("r", 2.5)
.attr('class', 'circle')
.merge(node.select('circle'))
.attr('fill', 'red');
nodeEnter.append("text") // insert
.attr("dy", 3)
.text(function(d) { return d.name; })
.merge(node.select('text'))
.attr('fill', 'green');
// You only need to call remove on the group, all the other exit().remove() calls are superfluous
node.exit().remove();
simulation
.nodes(items);
}
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
nodeLayer = svg.append('g'),
node;
var list = [];
var links = [];
var simulation = d3.forceSimulation(list)
.force("charge", d3.forceManyBody().strength(-1000))
.force("link", d3.forceLink(links).distance(200))
.force("center", d3.forceCenter(width / 2, height / 2))
.on("tick", ticked);
function createNodes(index) {
links.push({
'source': 0,
'target': index
})
list.push({
"id": index,
"name": "server " + index
});
return list;
}
var iteration = 0;
update(createNodes(iteration)); // just simulating updates
d3.interval(function(timer) {
iteration++;
update(createNodes(iteration));
}, 1000); //<-- this was commented out incorrectly just now
function update(items) {
var dataJoin = nodeLayer.selectAll(".node")
.data(items, function(d) {
return d.id;
});
node = dataJoin.enter() // insert
.append("g")
.attr("class", "node");
node.append("circle") // insert
.attr("r", 2.5)
.attr('class', 'circle')
.merge(dataJoin) // not the class, the actual selected group you called enter() on
.select('.circle')
.style('fill', 'red') // just for testing purposes
.exit().remove(); // exit
node.append("text") // insert
.attr("dy", 3)
.attr('class', 'text')
.text(function(d) {
return d.name;
})
.merge(dataJoin)
.select('.text')
.style('fill', 'green') // fill is a style
dataJoin.exit().remove();
simulation.nodes(list);
simulation.force("link").links(links);
simulation.alpha(1).restart();
}
function ticked() {
node.attr("transform", function(d) {
var a = d.x || 0;
var b = d.y || 0;
return "translate(" + a + ", " + b + ")";
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.4.1/d3.min.js"></script>
<svg height="300" width="300"></svg>
there are bunch of bugs,
first of all 'update'(misnomer) should be called with the list of all the data nodes not just the change.
you might want to explore https://bl.ocks.org/ and copy how people are doing force directed graphs. you need links to have forces.
the idea behind merge is to compare the new list with the old list, which means you need to use dataJoin or the group that has the data/id attached.
I am not an expert, do take a look at all the examples of force-directed graphs and see how they update/merge. (there's more than 1 way to update/restart the graph)

How do I draw text in a rectangle in D3?

I am using this calendar example: http://bl.ocks.org/KathyZ/c2d4694c953419e0509b and I want to put the value that appears in the mouseover inside each cell so it's always visible. I've tried adding this, which I thought would print '!!!' in each cell but it doesn't:
rect.append("text")
attr("dx", "+.65em")
.attr("dy", "+.65em")
.attr("opacity", "1")
.text(function(d) { return '!!!'; });
but it doesn't do anything
As I said in my comments, you want to group your rect and text in a g element. Here's the simplest example:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#3.5.3" data-semver="3.5.3" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
</head>
<body>
<script>
var data = [{
x: 20,
y: 30,
text: "Hi"
}, {
x: 100,
y: 200,
text: "bye"
}];
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 500);
var g = svg.selectAll('.someClass')
.data(data)
.enter()
.append("g")
.attr("class","someClass")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
g.append("rect")
.attr("width", 40)
.attr("height", 40)
.style("fill", "red");
g.append("text")
.style("fill", "black")
.text(function(d) {
return d.text;
})
</script>
</body>
</html>
For the specific code you are looking at .day becomes a g:
var g = svg.selectAll(".day")
.data(function(d) {
return d3.time.days(new Date(d, 0, 1), new Date(d + 1, 0, 1));
})
.enter().append("g")
.attr("class", "day")
.attr("transform", function(d){
var month_padding = 1.2 * cellSize*7 * ((month(d)-1) % (no_months_in_a_row));
var x = day(d) * cellSize + month_padding;
var week_diff = week(d) - week(new Date(year(d), month(d)-1, 1) );
var row_level = Math.ceil(month(d) / (no_months_in_a_row));
var y = (week_diff*cellSize) + row_level*cellSize*8 - cellSize/2 - shift_up;
return "translate(" + x + "," + y + ")";
});
var rect = g.append("rect"))
.attr("width", cellSize)
.attr("height", cellSize)
.datum(format);
g.append("text")
.text("!!!")
.style("fill", "black");
// etc, etc, etc....
the text attribute doesn't mean anything for a rect object. you want to add a separate text element:
.enter().append("svg:text") and then
.text(function(d) { return '!!!' });
and you can style the text element accordingly.

Select d3 node by its datum

I’d like to select a node in a callback without using d3.select(this).
I have some code that draws a pie…
function drawPie(options) {
options || (options = {});
var data = options.data || [],
element = options.element,
radius = options.radius || 100,
xOffset = Math.floor(parseInt(d3.select(element).style('width'), 10) / 2),
yOffset = radius + 20;
var canvas = d3.select(element)
.append("svg:svg")
.data([data])
.attr("width", options.width)
.attr("height", options.height)
.append("svg:g")
.attr("transform", "translate(" + xOffset + "," + yOffset + ")");
var arc = d3.svg.arc()
.outerRadius(radius);
var pie = d3.layout.pie()
.value(function(data) {
return data.percentageOfSavingsGoalValuation;
});
var arcs = canvas.selectAll("g.slice")
.data(pie)
.enter()
.append("svg:g")
.attr("class", "slice");
arcs.append("svg:path")
.on("mouseover", divergeSlice);
You’ll notice at the end I have a call to divergeSlice(). That looks like this:
function divergeSlice(datum, index) {
var angle = (datum.endAngle + datum.startAngle) / 2,
x = Math.sin(angle) * 10,
y = -Math.cos(angle) * 10;
d3.select(this)
.transition()
.attr("transform", "translate(" + x + ", " + y + ")");
}
This works, but I’d like to accomplish this without using this as I mentioned earlier. When I log the datum object, I get something like the following:
{
data: {
uniqueID: "XX00X0XXXX00"
name: "Name of value"
percentageOfValuation: 0.4
totalNetAssetValue: 0
}
endAngle: 5.026548245743669
innerRadius: 80
outerRadius: 120
startAngle: 2.5132741228718345
value: 0.4
}
How could I use d3.select() to find a path that holds datum.data.uniqueID that is equal to "XX00X0XXXX00"?
You can't do this directly with .select() as that uses DOM selectors. What you can do is select all the candidates and then filter:
d3.selectAll("g")
.filter(function(d) { return d.data.uniqueID === myDatum.data.uniqueID; });
However, it would be much easier to simply assign this ID as an ID to the DOM element and then select based on that:
var arcs = canvas.selectAll("g.slice")
.data(pie)
.enter()
.append("svg:g")
.attr("id", function(d) { return d.data.uniqueID; })
.attr("class", "slice");
d3.select("#" + myDatum.data.uniqueID);

("svg:rect") element does not appear on the screen when run

<script type="text/javascript">
var divElem = d3.select("#svgpathSVGdata");
data for squares
var jsonsquare = [{
x: 40,
y: 170,
width: 120,
height: 120,
label: "technology"
},
];
setting the canvas with of course width & height declared already
var svgcanvas = divElem.append("svg:svg")
.attr("width", w).attr("height", h);
This is the relevant coding... for some reason it doesn't generate a square
var square = svgcanvas.selectAll("rect").data(jsonsquare);
circle.enter().append('svg:rect').append("svg:g")
.attr('opacity', 0)
.attr("cursor", "pointer");
}).attr("x", function(d) {
return d.x;
}).attr("y", function(d) {
return d.y;
}).attr("width", function(d) {
return d.width;
}).attr("height", function(d) {
return d.height;
});
linking label to the squares
svgcanvas.selectAll("text").data(jsonsquare).enter().append("svg:text").text(function(d) {
return d.label;
}).attr("x", function(d) {
return d.x + 10;
}).attr("y", function(d) {
return d.y + 10;
});
</script>​​​​​
You're assigning the rectangle's attributes to the group (i.e, svg:g) beneath it. In the line below, remove the .append("svg:g") and it should work.
square.enter().append('svg:rect').append("svg:g")
Also, you're setting opacity to zero:
.attr('opacity', 0)
This makes your rectangle invisible. Set it to 1 instead for full visibility, or some number in between for a translucent fill effect.

Categories