d3.js add second label to bar chart - javascript

2 part question:
I have a bar chart with created using multiple arrays. These arrays contain the % wins of baseball teams; the relevant team colours; and their names.
I can create one set of labels on the chart, either the names or the win %. However I can't get both on at the same time. See below.
The code I am using is:
let WinsLabel = svgContainer.selectAll("text")
.data(d3.zip(TeamArray, WinPercArray, Colours));
WinsLabel.enter()
.append("text")
.attr("fill", "black")
.attr("x", function(d, i) {
return 45 + (i * 50);
})
.attr("y", 700)
.transition()
.duration(1000)
.attr("x", function(d,i){
return 70 + (i*50);
})
.attr("y", function(d){
return 685 - d[1];
})
.attr("text-anchor","middle")
.attr("font-family", "sans-serif")
.attr("font-size", "15px")
.attr("fill", "black")
.text(function(d){
return d[1]/10 + "%";
});
let TeamLabel = svgContainer.selectAll("text")
.data(d3.zip(TeamArray, WinPercArray, Colours));
TeamLabel.enter()
.append("text")
.attr("fill", "black")
.attr("x", function(d, i) {
return 45 + (i * 50);
})
.attr("y", 700)
.transition()
.duration(1000)
.attr("x", function(d,i){
return 70 + (i*50);
})
.attr("y", function(d){
return 700 - d[1]/2;
})
.attr("text-anchor","middle")
.attr("font-family", "sans-serif")
.attr("font-size", "15px")
.attr("fill", "white")
.text(function(d){
return d[0];
});
When I run the code with both scripts, only the win % shows up, but the names don't. In order to get the names to show up I have to remove the first label.
The 2 parts to my question are:
How would I get both sets of labels to show up at the same time?
How can I get the names to be arranged vertically in the rectangles/bars?

D3 stands for data driven something; and it's core principle is based on linking elements / selection, with data. When you set data, (var selection = selectAll(...).data(...)), you get 3 cases to think about:
Some existing elements can be linked to certain item in new data. You access them using selection
Some elements cannot be linked to any item in new data. You access them using selection.exit()
Some items in new data cannot be linked to any element from selection. You access them by using selection.enter()
In its simplest case, the linking between data and elements is made by index -- ie first element in selection is linked with first item in data array, second with second, and so on. The d3 cannot find element for the data item (= gets put into .enter() selection) if and only if (in this by-index context) the index of that data item is bigger than the size of the selection.
On your initial select
let WinsLabel = svgContainer.selectAll("text")
.data(d3.zip(TeamArray, WinPercArray, Colours));
The selection is empty, since there are no text tags yet. And since its empty, all of the to-be-created placeholders are inside .enter() selection. However, on your next select for the other label type
let TeamLabel = svgContainer.selectAll("text")
.data(d3.zip(TeamArray, WinPercArray, Colours));
The selection is of the size of the passed data, and thus .enter() selection is empty; it's the TeamLabel selection that contains all of the old elements (percentage label text tags), but they got their data values reassigned.
Andrew proposed one solution to assign classes, but personally I'd take all elements that relate to same team and put it under one group.
var TeamArray = ["Yankees", "Rays", "RedSox", "Jays","Orioles", "Twin", "Indians", "WhiteSox", "Detroit", "Royals", "Astros", "Rangers", "A's", "Angels","Mariners"];
var WinPercArray = [653, 609, 540, 400, 300, 667, 521, 458, 383, 347, 660, 511, 500, 458, 442];
var Colours = ["#003087", "#092C5C", "#BD3039", "#134A8E", "#DF4601", "#002B5C", "#0C2340", "#C4CED4", "#FA4616", "#BD9B60", "#EB6E1F", "#C0111F", "#003831", "#003263", "#005C5C"];
var data = d3.zip(TeamArray, WinPercArray, Colours);
var svg = d3.select('body').append('svg').attr('height', 300).attr('width', 800);
var teams = svg.selectAll('g.teams')
.data(data);
var scale = d3.scaleLinear()
.domain([0, 1000])
.range([200, 0]);
var teamsEnter = teams.enter()
.append('g')
.classed('team', true)
.attr('transform', function(d, i){
return 'translate(' + (i*50) + ',0)';
})
teamsEnter.append('rect')
.attr('width', 50)
.attr('y', function(d) { return scale(d[1]); })
.attr('height', function(d) { return scale(0) - scale(d[1]); })
.style('fill', function(d) { return d[2]; });
teamsEnter.append('text')
.attr('x', 25)
.attr('y', function(d) { return scale(d[1]) - 30; })
.text(function(d){ return d[0]; });
teamsEnter.append('text')
.attr('x', 25)
.attr('y', function(d) { return scale(d[1]) - 15; })
.text(function(d){ return d[1]; });
text {
text-anchor: middle;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Groups in some way act as encapsulation of inner items, so you can mentally separate data binding to groups (ie when to create / update / delete it), from actual logic that takes place when working with its children

Related

Moving and Changing Text Labels while Sorting Bar Chart

I'm trying to have my text labels move and change while sorting. I have a multidimensional array including bar height, width, and opacity. A kind contributor helped me out yesterday with getting the bars sorted correctly, but I'd also like to get the text labels to move and change at the top of each bar as the bar sorts and moves.
var s = -1;
svg.on("click", function() {
s = (s + 1) % 3;
var xPosition = parseFloat(d3.select(this).attr("x")) xScale.bandwidth() / 2;
var yPosition = parseFloat(d3.select(this).attr("y")) + 14;
svg.selectAll("rect")
.sort(function(a, b) {
return d3.ascending(a[s], b[s]);
})
.transition()
.delay(function(d, i) {
return i * 65;
})
.duration(250)
.attr("x", function(d, i) {
return xScale(i);
});
svg.selectAll("text")
.append("text")
.text(function(d) {
return d[s];
})
.attr("text-anchor", "left")
.attr("x", xPosition)
.attr("y", yPosition)
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "red");
});
As you can see, I'm quite lost as to where to go moving forward. I took the xPosition and yPosition variable idea from a textbook but it doesn't seem to change anything. Also, the text labels never change while I sort through height, width, and opacity. Thanks for the help.
When you do this...
svg.selectAll("text")
.append("text")
//etc...
... you are appending <text> elements inside <text> elements, which makes no sense.
Instead of that, assuming that your texts were previously created, just select them and sort them accordingly:
svg.selectAll("text")
.text(function(d) {
return d[s];
})
.attr("x", xPosition)
.attr("y", yPosition);

How to append 2 items in a d3 v4 force layout that updates?

I have a d3 v4 force layout and currently in my updateForceLayout function that I call when I add or remove a node I have this:
// Add and remove message nodes
const join = d3
.select(".container")
.selectAll("g")
.data(data);
// Remove old groups
join
.exit()
.remove();
// Create the new groups
const groups = join
.enter()
.append("g")
.attr("class", "outer");
// Merge the new groups with the existing groups
// and apply an appropriate translation
const innerJoin = groups
.merge(join)
.attr("transform", d => `translate(${d.x},${d.y})`)
.selectAll(".inner")
.data(d => d.m);
// Remove old ones
innerJoin
.exit()
.remove()
let newCircles = innerJoin
.enter()
.append('rect')
.attr("class", "innerbox")
.attr("y", (d,i) => {
return -65 - (i * 15)
})
.attr("width", 70)
.attr("height", 10)
.style("fill", "green")
newCircles.merge(join)
This is a nested selection so it's a bit more complex but the problem is simple and only lies in the last part. I want to be able to add text and a rect to "newCircles", but can only add one or the other. When I try this at the end instead:
let newCircles = innerJoin
.enter()
.append('g')
newCircles
.append('rect')
.attr("class", "innerbox")
.attr("y", (d,i) => {
return -65 - (i * 15)
})
.attr("width", 70)
.attr("height", 10)
.style("fill", "green")
newCircles
.append("text")
.attr("class", "innertext")
.text(function(d){return d})
.attr("y", (d,i) => {
return -55 - (i * 15)
})
.style("white-space", "pre");
My force layout bugs out and one of the nodes jumps off of the screen when I update the nodes. I am assuming I am not adding or removing or merging correctly now that I have a group with 2 elements inside and not just 1 element. What is the correct way to achieve this?

D3.js Trying to use histogram layout to draw rectangles

So I'm sort of new to Javascript and I am trying to create a histogram using d3.js. I've been following tutorials and examples of previously created histograms in d3 but cannot figure out how to make my rectangles appear.
My histogram currently contains 4 bins with the numbers 1, 2, 3 and 4 in each bin symbolizing a color attribute of each data point in my dataset. When I do console.log(d) in the .attr "x" function it will appear as an a kind of array with 4 different indices, each with the total number of data points in my dataset with that specific color. Now I'm trying to make that "array" into rectangles but my width and height functions aren't correct. If someone could explain what d.dx and d.y do any why they're wrong that would be helpful. I'm using d3.v3.min.js as my script src value
d3.csv("data.csv", function(data) {
var map = data.map(function (i) { return parseInt(i.color); })
var histogram = d3.layout.histogram()
.bins(4)(map)
var canvas = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 500);
var bars = canvas.selectAll(".bar")
.data(histogram)
.enter()
.append("g")
bars.append("rect")
.attr("x", function (d)
{
//console.log(d)
return d.x * 5; })
.attr("y", 0)
.attr("width",function(d) { return d.dx; })
.attr("height", function(d) { d.y; })
.attr("fill", "steelblue");
});
I updated your plunk as follows.
bars.append("rect")
.attr("x", function(d) { return d.x*100; })
.attr("y", 50)
.attr("height", function(d) { return d.y * 10;})
.attr("width", function(d) { return d.dx*50;})
.attr("fill", "steelblue")
.on("mouseout", function()
{
d3.select(this)
.attr("fill", "steelblue");
})
.on("mouseover", function()
{
d3.select(this)
.attr("fill", "orange");
});
Your code seems to work fine, only your elements are overlapping (also, d3 v4 was referenced instead of v3). What I did is:
multiply d.x by 50 to space the elements
multiplied d.dx by 50 to reduce the overlapping
As to your former questions:
d.x corresponds to the extent of a bin, in your case 0.75 (4 ranges make between 1 and 4 make 0.75: 1+(0.75*4)=4)
*d.y corresponds to the 'height' of a bin, i.e. the number of elements.

Get width of d3.js SVG text element after it's created

I'm trying to get the widths of a bunch of text elements I have created with d3.js
This is how I'm creating them:
var nodesText = svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d.name;
})
.attr("x", function(d, i) {
return i * (w / dataset.length);
})
.attr("y", function(d) {
return 45;
});
I'm then using the width to create rectangles the same size as the text's boxes
var nodes = svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d, i) {
return i * (w / dataset.length);
})
.attr("y", function(d) {
return 25;
})
.attr("width", function(d, i) {
//To Do: find width of each text element, after it has been generated
var textWidth = svg.selectAll("text")
.each(function () {
return d3.select(this.getComputedTextLength());
});
console.log(textWidth);
return textWidth;
})
.attr("height", function(d) {
return 30;
})
I tried using the Bbox method from here but I don't really understand it. I think selecting the actual element is where I'm going wrong really.
I would make the length part of the original data:
var nodesText = svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d.name;
})
.attr("x", function(d, i) {
return i * (w / dataset.length);
})
.attr("y", function(d) {
return 45;
})
.each(function(d) {
d.width = this.getBBox().width;
});
and then later
var nodes = svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("width", function(d) { return d.width; });
You can use getBoundingClientRect()
Example:
.style('top', function (d) {
var currElemHeight = this.getBoundingClientRect().height;
}
edit: seems like its more appropriate for HTML elements. for SVG elements you can use getBBbox() instead.
d3.selectAll returns a selection. You can get each of the elements by navigating through the array in the _groups property. When you are determining the width of a rectangle, you can use its index to get the corresponding text element:
.attr('width', function (d, i) {
var textSelection = d3.selectAll('text');
return textSelection._groups[0][i].getComputedTextLength();
});
The _groups property of d3's selection has a list of nodes at [0]. This list contains all of the selected elements, which you can access by index. It's important that you get the SVG element so that you can use the getComputedTextLength method.
You may also want to consider creating the rect elements first, then the text elements, and then going back to the rectangles to edit the width attribute, so that the text elements are on top of the rectangles (in case you want to fill the rectangles with color).
Update:
It's typically preferred that you don't access _groups, though, so a safer way to get the matching text element's width would be:
.attr('width', function (d, i) {
return d3.selectAll('text').filter(function (d, j) { return i === j; })
.node().getComputedTextLength();
});
Using node safely retrieves the element, and filter will find the text element which matches index.

Fitting data for D3 graph to create legend

I have a data variable which contains the following:
[Object { score="2.8", word="Blue"}, Object { score="2.8", word="Red"}, Object { score="3.9", word="Green"}]
I'm interested in modifying a piece of a D3 graph http://bl.ocks.org/3887051 to display the legend, which would be the list of the "word", for my data set.
The legend script looks like this (from link above):
var ageNames = d3.keys(data[0]).filter(function(key) { return key !== "State"; });
var legend = svg.selectAll(".legend")
.data(ageNames.slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
How do I modify their ageNames function to display the "word" set from my data? I'm not sure how they're utilizing the d3.keys. Is there another way to do it?
This should work more or less, but you may need to reverse() (as the original example does) or otherwise rearrange the elements of words, in order to correctly map a word to the right color. Depends on how you've implemented your graph.
var words = yourDataArray.map(function(entry) { return entry.word; });
var legend = svg.selectAll(".legend")
.data(words)
// The rest stays the same

Categories