Refer to Inline Labels in d3 v4.0.0-alpha 9,
label.append("rect", "text")
.datum(() => this.nextSibling.getBBox())
.attr('x', d => (d.x - labelPadding))
.attr('y', d => (d.y - labelPadding))
.attr('width', d => (d.width + (2 * labelPadding)))
.attr('height', d => (d.height + (2 * labelPadding)));
It will append a rect to text by access the element inside the datum() via this.
A label is rendered for each point in each series. Beneath this label, a white rectangle is added, whose size and position is computed automatically using element.getBBox plus a little bit of padding. The resulting label is thus legible
According to D3 v3 set datum, we should create a new selection that's not bound to the data for later use in version 4 (eg. v4.7.4). I tried to create the new selection like following, but seem like the bbox is single object instead of multiple object should be loop through in datum as above code in d3 v4.0.0-alpha 9.
const newText = label.selectAll('text');
const bbox = newText.node().getBBox();
label.append('rect', 'text')
.datum(() => bbox)
.attr('x', d => (d.x - labelPadding))
.attr('y', d => (d.y - labelPadding))
.attr('width', d => (d.width + (2 * labelPadding)))
.attr('height', d => (d.height + (2 * labelPadding)));
Your snippet is not working for a simple reason. But, before addressing that, some considerations about your question:
That code from Bostock (Inline Labels) uses D3 v4, not v3.
append("rect", "text") does not append rectangles to texts. It append rectangles to the container, be it a SVG or a group element (in this particular case, a group), before the texts.
That last bullet point is important, because the texts will be always the nextSiblings in relation to the rectangles.
That being said, we come to your snippet. When you do this:
const newText = label.selectAll('text');
const bbox = newText.node().getBBox();
You are actually doing just this:
const bbox = label.selectAll('text').node().getBBox();
And your datum function will end up being:
.datum(() => label.selectAll('text').node().getBBox();)
Well, that is substantially different from Bostock's code, since this...
label.selectAll('text').node()
... will select all text elements but will return only to the first one in the DOM. That happens because of node(), which:
Returns the first (non-null) element in this selection. (emphasis mine)
On the other hand, in Bostock's code, this...
.datum(() => this.nextSibling.getBBox())
... will point to a different DOM element at each iteration, because this.nextSibling will be a different text element every time (which, as we just saw in the beginning of this answer, is the nextSibling of the corresponding rectangle).
Related
I am having issues with my tooltip not following my mouse position on only the enter selection.
Attached is a diagram of the DOM structure of my code, where the highlighted portions represent what I am trying to select and modify. I used element as a placeholder.
In my case, g.child represents a tooltip that moves alongside (x, y) mouse co-ordinates. My main problem is that entering g.child, it doesn't behave as expected (i.e.: doesn't move and remains static in one position). However after updating g.child and element.child behaves as expected (i.e.: moves alongside the mouse position).
I wrote this code under these assumptions and welcome input if they're incorrect:
selectAll(selector) works recursively and flattens the selection, whereas select(selector) selects the first match but propagates the data forward.
by chaining svg.selectAll('g.parent).select('g.child').select('element.child')
, I would be maintaining hierarchy while selecting the final child element, giving me the ability to update it.
since the data is bound parents and specified by a unique identifier, I did not re-define using data() further down the variable chain to children elements as it's bound and propagated through parents.
I did not need to include an exit portion as I am not getting rid of any existing elements, only updating whatever is already entered.
Here is the adapted example code for my problem. I have only included the entering and updating bits of the script:
// dummy data
let data = [
{ key: 1,
light = #ffffff,
dark = #000000 },
...
, // note: n represents the nth value
{ key: n,
light = #eeeeee,
dark = #222222 }
]
//=== D3 ====
const svg = d3.select('svg')
const parents = svg
.selectAll('g.parent')
.data(data, d => d.key) // maintaining object constancy using identifier
// ENTER
const enterParents = parents
.enter()
.append('g')
.attr('class', 'parent')
.attr('width', 800)
.attr('height', 360)
enterParents
.append('element')
.attr('class', 'parent')
.attr('fill', d => d.light)
// entering nested child group g.child per g.parent
const children = parents.select('g.child')
enterChild = enterParents
.append('g')
.attr('class', 'child')
enterChild
.append('element')
.class('child')
.attr('fill', d => d.light)
// UPDATE
parents
.select('element.parent')
.attr('fill', d => d.dark)
children
.select('element.child')
.attr('fill', d => d.dark)
// adding event listener for mouse position hovering over any g.parent
parents
.on('mousemove', function(e) {
const mouse = d3.pointer(e)
const mouseX = mouse[0]
const mouseY = mouse[1]
children
.attr('transform', `translate( ${mouseX}, ${mouseY})`)
})
}
The tooltip background updates as expected, but my only issue is the tooltip not following the mouse on mousemove. It is static on entering but after updating it moves alongside the mouse.
Would my problem with the tooltip position not moving with the mouse be a result of how groups are selected, stored, and called as variables?
I figured it out, the process just involved storing the merged enter + update selections into one variable to invoke further down the code. The code in my question only involved tracking the mouse position on the updated selection, hence the mouse position only behaving as expected on update and not enter.
Below is the modified code, where the update is now performed on the merge portion of the code. If I wanted to I could separately handle updating from entering, by calling the merged variable on another line.
// dummy data
let data = [
{ key: 1,
light = #ffffff,
dark = #000000 },
...
, // note: n represents the nth value
{ key: n,
light = #eeeeee,
dark = #222222 }
]
//=== D3 ====
const svg = d3.select('svg')
const parents = svg
.selectAll('g.parent')
.data(data, d => d.key) // maintaining object constancy using identifier
// ENTER
const enterParents = parents
.enter()
.append('g')
.attr('class', 'parent')
.attr('width', 800)
.attr('height', 360)
// Storing merged g.parent groups in variable
parentsMerged = enterParents.merge(parents) // NEW
// storing elements selection in var for update
parentElements = parents.select('element.parent') // NEW
enterParents
.append('element')
.attr('class', 'parent')
// NEW - update combined using merge()
.merge(parentElements)
.attr('fill', d => d.light)
// entering nested child group g.child per g.parent
const children = parents.select('g.child')
enterChild = enterParents
.append('g')
.attr('class', 'child')
// Storing merged g.child groups in variable
childrenMerged = enterChildren.merge(children) // NEW
// storing elements selection in var for update
childElements = children.select('element.child') // NEW
enterChild
.append('element')
.attr('class', 'child')
// NEW - update combined using merge()
.merge(childElements)
.attr('fill', d => d.light)
// adding event listener for mouse position hovering over any g.parent
parentsMerged // NEW
.on('mousemove', function(e) {
const mouse = d3.pointer(e)
const mouseX = mouse[0]
const mouseY = mouse[1]
childrenMerged // NEW
.attr('transform', `translate( ${mouseX}, ${mouseY})`)
})
}
I would like to use the Collide force in D3 to prevent overlaps between nodes in a force layout, but my y-axis is time-based. I would like to only use the force on the nodes' x positions.
I have tried to combine the collide force with a forceY but if I increase the collide radius I can see that nodes get pushed off frame so the Y position is not preserved.
var simulation = d3.forceSimulation(data.nodes)
.force('links', d3.forceLink(data.links))
.force('x', d3.forceX(width/2))
.force('collision', d3.forceCollide().radius(5))
.force('y', d3.forceY( function(d) {
var date = moment(d.properties.date, "YYYY-MM-DD");
var timepos = y_timescale(date)
return timepos; }));
My hunch is that I could modify the source for forceCollide() and remove y but I am just using D3 with <script src="https://d3js.org/d3.v5.min.js"></script> and I'm not sure how to start making a custom version of the force.
Edit: I have added more context in response to the answer below:
- full code sample here
- screenshot here
Not quite enough code in the question to guarantee this is what you need, but making some assumptions:
Often when using a force layout you would allow the forces to calculate the positions and then reposition the node to a given [x,y] co-ordinate on tick e.g.
function ticked() {
nodeSelection.attr('cx', d => d.x).attr('cy', d => d.y);
}
Since you don't want the forces to affect the y co-ordinate just remove it from here i.e.
nodeSelection.attr('cx', d => d.x);
And set the y position on, say, enter:
nodeSelection = nodeSelection
.enter()
.append('circle')
.attr('class', 'node')
.attr('r', 2)
.attr('cy', d => {
// Set y position based on scale here
})
.merge(nodeSelection);
I am using d3 js to display two bar graphs on the same svg element. Both the graphs have different json data sources. When tested seperately, both the graphs are displayed perfectly.When the entire source code is combined, it doesnt
To append rectangles to the page, I use the following code twice (with changed values in the 'x' and 'y' of .attr)
svgd.selectAll("rect")
.data(dataset_dept_errors)
.enter()
.append("rect")
.attr("x", function (d, i) {
return i * 100 + padding_dept; // x position of rect as per i->0,1,2,3,...
})
.attr("y", function (d) {
return (h_dept + yshift_for_dept - yScale(d.NumRuleFailed)); //y position of rect as per (h-value) to prevent inverted range
})
.attr("width", "50") //depending upon domain->no of inputs - with of band is decided acc. to fit in svg
.attr("height", function (d) {
return yScale(d.NumRuleFailed); //depending upon domain->value of inputs - with of band is decided acc. to fit in svg
})
.attr("fill", function (d) { //colour based on values -> more errors - dark coloured bars
return "rgb(" + 100 + "," + 0 + "," + 200 + ")";
})
.attr("stroke", "black");
I have read that selectAll('rect') will be ignored or it may not work as intended the second time it is encountered, so it should be used only once on one svg.
How then should I append the next set of rectangles to my page?
EDIT1 : I have the same problem for selectAll('text') for placing multiple text elements on the same page
You could first append two g elements to your svg element, position them appropriately, and then create the graphs inside the g elements.
The problem is in the root node - svgd. Just use two separate root SVG nodes (svgd, svgd2):
var svgd = d3.select("#svg1"),
svgd2 = d3.select("#svg2");
and then:
svgd.selectAll("rect")
.data(dataset_dept_errors)
.enter()
...
svgd2.selectAll("rect")
.data(dataset_dept_errors)
.enter()
...
and you'll be okay,
I'm trying to make a pie chart with d3.js that looks like this:
Note that the labels are placed along the edges of the pie chart. Initially, I am able to draw the pie charts and properly place the text nodes (the fiddle only displays one pie chart; assume, however, that they all have data that works and is appropriate, as this one does). However, when I go to adjust the data, I can't seem to .attr(translate, transform) them to the correct region along the edge of the pie chart (or do anything to them, for that matter):
changeFunctions[i] = function (data, i) {
path.data(pie(data))
.transition()
.duration(750)
.attrTween("d", arcTween);
text.each(function (d, num) {
d3.select(this)
.text(function (t) {
return t.data.name+". "+(100 * t.data.votes/totalVotes).toFixed(0) + "%";
})
/*
.attr("transform", function (d) {
//console.log("the d", d)
var c = arc.centroid(d),
x = c[0], y = c[1],
h = Math.sqrt(x * x + y * y);
return "translate(" + (x/h * 100) + ',' + (y/h * 100) + ")";
})*/
.attr("opacity", function (t) {
return t.data.votes == 0 ? 0 : 1;
});
})
}
I have omitted the general code to draw the pie chart; it's in the jsfiddle. Basically, I draw each of the pie charts in a for loop and store this function, changeFunctions[i], in a closure, so that I have access to variables like path and text.
The path.data part of this function works; the pie chart properly adjusts its wedges. The text.each part, however, does not.
How should I go about making the text nodes update both their values and locations?
fiddle
When updating the text elements, you also need to update the data that's bound to them, else nothing will happen. When you create the elements, you're binding the data to the g element that contains the arc segment and text. By then appending path and text, the data is "inherited" to those elements. You're exploiting this fact by referencing d when setting attributes for those elements.
Probably the best way to make it work is to use the same pattern on update. That is, instead of updating only the data bound to the path elements as you're doing at the moment, update the data for the g elements. Then you can .select() the descendant path and text elements, which will again inherit the new data to them. Then you can set the attributes in the usual manner.
This requires a few changes to your code. In particular, there should be a variable for the g element selection and not just for the paths to make things easier:
var g = svg.selectAll("g.arc")
.data(pie(data));
g.enter()
.append("g").attr("class", "arc");
var path = g.append("path");
The changeFunction code changes as follows:
var gnew = g.data(pie(data));
gnew.select("path")
.transition()
.duration(750)
.attrTween("d", arcTween);
Now, to update the text, you just need to select it and reset the attributes:
gnew.select("text")
.attr("transform", function(d) { ... });
Complete demo here.
I'm new to d3.js and still a beginner in javascript in general. I've got d3 correctly drawing a single SVG rectangle based on a single value in an array. When I increase the value of the number in the area via an input field and then call the reDraw function, the rectangle changes to the new size for just a second and then switches back to the initial size. I'm thinking I must have a scoping problem, but can't figure it out. Any ideas?
var dataset, h, reDraw, svg, w;
w = 300;
h = 400;
dataset = [1000];
// Create SVG element
svg = d3.select("#left").append("svg").attr("width", w).attr("height", h);
// set size and position of bar
svg.selectAll("rect").data(dataset).enter().append("rect").attr("x", 120).attr("y", function(d) {
return h - (d / 26) - 2;
}).attr("width", 60).attr("height", function(d) {
return d / 26;
}).attr("fill", "rgb(3, 100, 0)");
// set size and position of text on bar
svg.selectAll("text").data(dataset).enter().append("text").text(function(d) {
return "$" + d;
}).attr("text-anchor", "middle").attr("x", 150).attr("y", function(d) {
return h - (d / 26) + 14;
}).attr("font-family", "sans-serif").attr("font-size", "12px").attr("fill", "white");
// grab the value from the input, add to existing value of dataset
$("#submitbtn").click(function() {
localStorage.setItem("donationTotal", Number(localStorage.getItem("donationTotal")) + Number($("#donation").val()));
dataset.shift();
dataset.push(localStorage.getItem("donationTotal"));
return reDraw();
});
// redraw the rectangle to new size
reDraw = function() {
return svg.selectAll("rect").data(dataset).attr("y", function(d) {
return h - (d / 26) - 2;
}).attr("height", function(d) {
return d / 26;
});
};
You need to tell d3 how to match new data to existing data in the reDraw function. That is, you're selecting all rectangles in reDraw and then binding data to it without telling it the relation between the old and the new. So after the call to data(), the current selection (the one you're operating on) should be empty (not sure why something happens for you at all) while the enter() selection contains the new data and the exit() selection the old one.
You have several options to make this work. You could either operate on the enter() and exit() selections in your current setup, i.e. remove the old and add the new, or restructure your data such that you're able to match old and new. You could for example use an object with appropriate attributes instead of a single number. The latter has the advantage that you could add a transition from the old to the new size.
Have a look at this tutorial for some more information on data joins.
Edit:
Turns out that this was actually not the issue, see comments.