Fitting data for D3 graph to create legend - javascript

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

Related

How do I match up text labels in a legend created in d3

I am building a data visualization project utilizing the d3 library. I have created a legend and am trying to match up text labels with that legend.
To elaborate further, I have 10 rect objects created and colored per each line of my graph. I want text to appear adjacent to each rect object corresponding with the line's color.
My Problem
-Right now, an array containing all words that correspond to each line appears adjacent to the top rect object. And that's it.
I think it could be because I grouped my data using the d3.nest function. Also, I noticed only one text element is created in the HTML. Can anyone take a look and tell me what I'm doing wrong?
JS Code
const margin = { top: 20, right: 30, bottom: 30, left: 0 },
width = 1000 - margin.left - margin.right;
height = 600 - margin.top - margin.bottom;
// maybe a translate line
// document.body.append(svg);
const div_block = document.getElementById("main-div");
// console.log(div_block);
const svg = d3
.select("svg")
.attr("width", width + margin.left + margin.right) // viewport size
.attr("height", height + margin.top + margin.bottom) // viewport size
.append("g")
.attr("transform", "translate(40, 20)"); // center g in svg
// load csv
d3.csv("breitbartData.csv").then((data) => {
// convert Count column values to numbers
data.forEach((d) => {
d.Count = +d.Count;
d.Date = new Date(d.Date);
});
// group the data with the word as the key
const words = d3
.nest()
.key(function (d) {
return d.Word;
})
.entries(data);
// create x scale
const x = d3
.scaleTime() // creaters linear scale for time
.domain(
d3.extent(
data,
// d3.extent returns [min, max]
(d) => d.Date
)
)
.range([margin.left - -30, width - margin.right]);
// x axis
svg
.append("g")
.attr("class", "x-axis")
.style("transform", `translate(-3px, 522px)`)
.call(d3.axisBottom(x))
.append("text")
.attr("class", "axis-label-x")
.attr("x", "55%")
.attr("dy", "4em")
// .attr("dy", "20%")
.style("fill", "black")
.text("Months");
// create y scale
const y = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.Count)])
.range([height - margin.bottom, margin.top]);
// y axis
svg
.append("g")
.attr("class", "y-axis")
.style("transform", `translate(27px, 0px)`)
.call(d3.axisLeft(y));
// line colors
const line_colors = words.map(function (d) {
return d.key; // list of words
});
const color = d3
.scaleOrdinal()
.domain(line_colors)
.range([
"#e41a1c",
"#377eb8",
"#4daf4a",
"#984ea3",
"#ff7f00",
"#ffff33",
"#a65628",
"#f781bf",
"#999999",
"#872ff8",
]); //https://observablehq.com/#d3/d3-scaleordinal
// craete legend variable
const legend = svg
.append("g")
.attr("class", "legend")
.attr("height", 100)
.attr("width", 100)
.attr("transform", "translate(-20, 50)");
// create legend shapes and locations
legend
.selectAll("rect")
.data(words)
.enter()
.append("rect")
.attr("x", width + 65)
.attr("y", function (d, i) {
return i * 20;
})
.attr("width", 10)
.attr("height", 10)
.style("fill", function (d) {
return color(d.key);
});
// create legend labels
legend
.append("text")
.attr("x", width + 85)
.attr("y", function (d, i) {
return i * 20 + 9;
})
// .attr("dy", "0.32em")
.text(
words.map(function (d, i) {
return d.key; // list of words
})
);
// returning an array as text
// });
svg
.selectAll(".line")
.data(words)
.enter()
.append("path")
.attr("fill", "none")
.attr("stroke", function (d) {
return color(d.key);
})
.attr("stroke-width", 1.5)
.attr("d", function (d) {
return d3
.line()
.x(function (d) {
return x(d.Date);
})
.y(function (d) {
return y(d.Count);
})(d.values);
});
});
Image of the problem:
P.S. I cannot add a JSfiddle because I am hosting this page on a web server, as that is the only way chrome can read in my CSV containing the data.
My Temporary Solution
function leg_labels() {
let the_word = "";
let num = 0;
for (i = 0; i < words.length; i++) {
the_word = words[i].key;
num += 50;
d3.selectAll(".legend")
.append("text")
.attr("x", width + 85)
.attr("y", function (d, i) {
return i + num;
})
// .attr("dy", "0.32em")
.text(the_word);
}
}
leg_labels();
Problem
Your problem has to do with this code
legend
.append("text")
.attr("x", width + 85)
.attr("y", function (d, i) {
return i * 20 + 9;
})
// .attr("dy", "0.32em")
.text(
words.map(function (d, i) {
return d.key; // list of words
})
);
You are appending only a single text element and in the text function you are returning the complete array of words, which is why all words are shown.
Solution
Create a corresponding text element for each legend rectangle and provide the correct word. There are multiple ways to go about it.
You could use foreignObject to append HTML inside your SVG, which is very helpful for text, but for single words, plain SVG might be enough.
I advise to use a g element for each legend item. This makes positioning a lot easier, as you only need to position the rectangle and text relative to the group, not to the whole chart.
Here is my example:
let legendGroups = legend
.selectAll("g.legend-item")
.data(words)
.enter()
.append("g")
.attr("class", "legend-item")
.attr("transform", function(d, i) {
return `translate(${width + 65}px, ${i * 20}px)`;
});
legendGroups
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 10)
.attr("height", 10)
.style("fill", function (d) {
return color(d.key);
});
legendGroups
.append("text")
.attr("x", 20)
.attr("y", 9)
.text(function(d, i) { return words[i].key; });
This should work as expected.
Please note the use of groups for easier positioning.

d3.js add second label to bar chart

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

Why is d3 ignoring my color array?

I'm trying to build out a simple color chart, as an introductory d3 exercise, and I'm already stuck.
I have the following:
var colors = ["#ffffcc","#c7e9b4","#7fcdbb","#41b6c4","#2c7fb8","#253494"];
var barHeight = 20,
barWidth = 20,
width = (barWidth + 5) * colors.length;
d3.select("body").selectAll("svg")
.data(colors)
.enter().append("rect")
.attr("class", "block")
.attr("width", barWidth)
.attr("height", barHeight - 1)
.text(function(d) { return d; })
.attr("fill", function(d) { return d; });
https://jsfiddle.net/xryamdkf/1/
The text works fine. I see the hex codes, but the height and width are definitely not respected, and I can't seem to set the color.
This works to set the color: .style("background", function(d) { return d; }) but I think that is the text background, not the rect fill.
What am I doing wrong here? How can I make 20x20 rectangles filled with color in d3?
As you are not giving any index and reference of colors array into your function the code will not understand from where to pick colors. try with below code it will help.
d3.select("body").selectAll("svg")
.data(colors).enter().append("rect")
.attr("class", "block")
.attr("width", barWidth)
.attr("height", barHeight - 1)
.text(function(d) {
return d;
})
.attr("fill", function(d,i) { return colors[i]; });
So, a few things. You should call data() on what will be an empty selection of the things you will be adding.
svg.selectAll("rect").data(colors)
.enter().append("rect")
The rect doesn't have a text property. There is an svg text node that shows text and you'll want to add it separately.
I hope this https://jsfiddle.net/xryamdkf/8/ gets you closer.

D3 change visibilty of nodes on click based on class

I have a bubble chart, where nodes are declared as below and I append for each circle a class which is decided by a array ("category") which decides its category, the variable color is d3.scale.category10() .domain(d3.range(number of elements in "category" array));.
var node = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("class", function(d) {return category[d.cluster];})
.text(function(d) { return d.text; })
.filter(function(d){ return d.count >= 1; })
.style("fill", function(d) { return color(d.cluster); })
.call(force.drag);
Next, I make a legend which depends on the categories of each of the circles with their color (as shown above). For this, I do the following
var legend = svg.selectAll(".legend")
.data(color.domain())
.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 category[d]; })
Now, what I want is that when the user clicks the legend text, then the bubbles corresponding to the category of the legend be hidden.
So I add the following to the legend, text object.
.on("click", function(d){
node.selectAll('.'+category[d]).style("visibility", "hidden");
});
But, this does not hide the nodes. Please help.
When you call node.selectAll() it will select all childs of this node that fit the selector. In your case you want to call it on document. So you have to do something like d3.selectAll('.'+category[d])

What is String or where does this come from?

While trying to understand d3 I saw the line .text(String);. I could not understand what String is suppose to be. I thought maybe its an empty string (nope), a method (i didnt see that in the api reference) and pondered what else it could be.
I commented it out below and got expected results. What I don't understand is what is String and why does it work. With this line my 3 squared boxes has text (its a internal value of the data it will represent later) while commented out it does not.
Demo
Html
<div class='chart' id='chart-10'/>
<script src="http://d3js.org/d3.v3.min.js"></script>
JS:
var w = 360;
var h = 180;
var svg = d3.select("#chart-10").append("svg")
.attr("width", w)
.attr("height", h);
var g = svg.selectAll(".data")
.data([50,150,250])
.enter().append("g")
.attr("class", "data")
.attr("transform", function(d, i) { return "translate(" + 20 * (i + 1) + ",20)"; });
g.append("circle")
.attr("class", "little")
.attr("r", 1e-6);
g.append("rect")
.attr("x", -10)
.attr("y", -10)
.attr("width", 20)
.attr("height", 20)
.style("fill", "lightgreen")
.style("stroke", "green");
g.append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
;// .text(String);
g.attr("transform", function(d, i) { return "translate(" + 20 * (i + 1) + ",20)"; });
g.select("rect").style("opacity", 1);
g.select("circle").attr("r", 1e-6);
var t = g.transition().duration(750);
t.attr("transform", function(d, i) { return "translate(" + d + ",90)"; });
t.select("circle").attr("r", Math.sqrt);
t.select("rect").style("opacity", 1e-6);
It looks like the String constructor. According to d3 documentation, as pointed out by Matt:
if value is a function, then the function is evaluated for each selected element (in order), being passed the current datum d and the current index i, with the this context as the current DOM element. The function's return value is then used to set each element's text content.
So, you set g.data to [50,150,250] a few lines before. Each number is converted to a String object by the String constructor, returned and used as the text values of your DOM nodes.

Categories