Conditionally change background image of D3 circles - d3.js

I am trying to conditionally change the background of some circles created with D3 but I am unable to get the if else logic to correctly pick out the correct picture. The code goes right to the final else statement for the default.gif. I don't see any errors in the console. All the images are in the same directory as the html file.
var diameter = 500, //max size of the bubbles
format = d3.format(",d"),
color = d3.scaleOrdinal(d3.schemeCategory20c); //color category
var bubble = d3.pack()
.size([diameter, diameter])
.padding(1.5);
var svg = d3.select("body")
.append("svg")
.attr("width", diameter)
.attr("height", diameter)
.attr("class", "bubble");
d3.csv("fruit.csv", function(error, data){
if (error) throw error;
//convert numerical values from strings to numbers
data = data.map(function(d){ d.value = +d["Amount"]; return d; });
//Sets up a hierarchy of data object
var root = d3.hierarchy({children:data})
.sum(function(d) { return d.value; })
.sort(function(a, b) { return b.value - a.value; });
//Once we have hierarchal data, run bubble generator
bubble(root);
//setup the chart
var bubbles = svg.selectAll(".bubble")
.data(root.children)
.enter();
//create the bubbles
bubbles.append("circle")
.attr("class", "circle")
.attr("r", function(d){ return d.r; })
.attr("cx", function(d){ return d.x; })
.attr("cy", function(d){ return d.y; })
.append("defs")
.append("pattern")
.append("image")
.attr("xlink:href", function(d) {
if ( d.Fruit == "Apple") {
return "apple.jpg";
}
else if (d.Fruit == "Pear") {
return "pear.jpg"
}
else if (d.Fruit == "Banana") {
return "banana.jpg";
}
else if (d.Fruit == "Strawberry") {
return "strawberry.jpg";
}
else if (d.Fruit == "Grapes") {
return "grapes.jpg";
}
else { return "default.gif"; }
});
//.style("fill", function(d) { return color(d.value); });
//format the text for each bubble
bubbles.append("text")
.attr("x", function(d){ return d.x; })
.attr("y", function(d){ return d.y + 5; })
.attr("text-anchor", "middle")
.text(function(d){ return d.data["Fruit"]; })
.style("fill","white")
.style("font-family", "Helvetica Neue, Helvetica, Arial, san-serif")
.style("font-size", "12px");
});
The csv file contains the following data:
Fruit,Amount
Apple,32
Pear,13
Banana,25
Grapes,29
Strawberry,36

As I said in my comment, I'm surprised that you're seeing anything as the circles' background, since your code for creating/referencing the patterns is not correct. For a quick explanation, check my answer here.
However, I'll address only your main question here, which is the if statement.
The problem is that you're using d3.hierarchy to create your data array. Because of that, all data are inside node.data. According the API:
The returned node and each descendant has the following properties:
node.data - the associated data, as specified to the constructor.
Therefore, for your conditionals, instead of:
d.Fruit == "Apple"
It should be:
d.data.Fruit == "Apple"
And the same for d.data.Fruit == "Pear", d.data.Fruit == "Banana" etc...

Related

How can i get the key of an element in stack-layout in d3?

I want to add a toolkit that show the type of the disaster, which is the key of the stack datum, how can i get it?
The format of .csv file is like this: (Forgive me can not take pictures)
AllNaturalDisasters,Drought,Earthquake,ExtremeTemperature,ExtremeWeather,Flood,Impact,Landslide,MassMovementDry,VolcanicActivity,Wildfire,Year
5,2,null,null,1,1,null,null,null,1,null,1900
2,null,2,null,null,null,null,null,null,null,null,1901
Here I create a stack
var stack = d3.stack()
.keys(["Drought", "Earthquake", "ExtremeTemperature", "ExtremeWeather", "Flood", "Impact", "Landslide", "MassMovementDry", "VolcanicActivity", "Wildfire"]);
and then I pass it my data:var series = stack(dataset);. dataset is the all data from the csv file. Then I create a chart using stack-layout, like this:
var groups = svg.selectAll("g")
.data(series)
.enter()
.append("g")
.style("fill", function(d, i) {
return colors(i);
});
var rects = groups.selectAll("rect")
.data(function(d) { return d; })
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return yScale(d[1]);
})
.attr("height", function(d) {
return yScale(d[0]) - yScale(d[1]);
})
.attr("width", xScale.bandwidth())
.append("title")
.text(function (d) {
return d.data.Year;
});
The problem is right here:
.append("title")
.text(function (d) {
return d.data.Year;
});
I want to add a toolkit to show the type of the disaster, which is the key of this datum in series , how can I get it instead of the year?!
Each rectangle contains information on the column (year of disaster), but each g has information on the "row" (type of disaster).
The stack produces a nested array, the parent level (which we use to create the g elements) contains the key, or type of disaster
The child level represents the columns, which contains the year.
The grandchild level just contains individual rectangles.
So, we can get a key by selecting the parent g:
.append("title")
.text(function() {
var rect = this.parentNode; // the rectangle, parent of the title
var g = rect.parentNode; // the g, parent of the rect.
return d3.select(g).datum().key; // now we get the key.
})
Of course this could be simplified a bit, but I broke it out to comment it better.
This allows for more flexible sorting - rather than relying on fixed indexes.
Here it is using your data:
var csv = d3.csvParse(d3.select("pre").text());
var stack = d3.stack().keys(["Drought", "Earthquake", "ExtremeTemperature", "ExtremeWeather", "Flood", "Impact", "Landslide", "MassMovementDry", "VolcanicActivity", "Wildfire"]);
var series = stack(csv);
var colors = d3.scaleOrdinal()
.range(d3.schemeCategory10);
var xScale = d3.scaleBand()
.domain([0,1])
.range([0,300])
var yScale = d3.scaleLinear()
.domain([0,6])
.range([200,0]);
var svg = d3.select("svg");
var groups = svg.selectAll("g")
.data(series)
.enter()
.append("g")
.style("fill", function(d, i) {
return colors(i);
});
var rects = groups.selectAll("rect")
.data(function(d) { return d; })
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return yScale(d[1]);
})
.attr("height", function(d) {
return yScale(d[0]) - yScale(d[1]);
})
.attr("width", xScale.bandwidth())
.append("title")
.text(function (d) {
var rect = this.parentNode;
var g = rect.parentNode;
return d3.select(g).datum().key;
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="400" height="300"></svg>
<pre>AllNaturalDisasters,Drought,Earthquake,ExtremeTemperature,ExtremeWeather,Flood,Impact,Landslide,MassMovementDry,VolcanicActivity,Wildfire,Year
5,2,0,0,1,1,0,0,0,1,0,1900
2,0,2,0,0,0,0,0,0,0,0,1901</pre>
Well, I have fixed this problem by a very 'low' method. I have created a simple function:
function getKeys(d) {
return series[parseInt(groups.selectAll("rect").data().indexOf(d) / series[0].length)].key;
}
Well, it so simple and crude, and I still want to know a more efficient method!!!

Problems about update, enter, exit data and select in d3.js

I'm doing a visual project to show natural disaster in 1900-2018 using d3. I want add an interactive action that one can choose the first year and last year to show.
Originally I create the picture as the following:
d3.csv("output.csv", rowConventer, function (data) {
dataset = data;
var xScale = d3.scaleBand()
.domain(d3.range(dataset.length))
.range([padding, width - padding])
.paddingInner(0.05);
var yScale = d3.scaleLinear()
.domain([0,
d3.max(dataset, function (d) {
return d.AllNaturalDisasters;
})])
.range([height - padding, padding])
.nice();
stack = d3.stack().keys(["Drought", "Earthquake", "ExtremeTemperature", "ExtremeWeather", "Flood", "Impact", "Landslide", "MassMovementDry", "VolcanicActivity", "Wildfire"]);
series = stack(dataset);
gr = svg.append("g");
groups = gr.selectAll("g")
.data(series)
.enter()
.append("g")
.style("fill", function(d, i) {
return colors(i);
})
.attr("class", "groups");
rects = groups.selectAll("rect")
.data(function(d) { return d; })
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return yScale(d[1]);
})
.attr("height", function(d) {
return yScale(d[0]) - yScale(d[1]);
})
.attr("width", xScale.bandwidth())
.append("title")
.text(function (d) {
var rect = this.parentNode;// the rectangle, parent of the title
var g = rect.parentNode;// the g, parent of the rect.
return d.data.Year + ", " + d3.select(g).datum().key + "," + (d[1]-d[0]);
});
d3.select("button")
.on("click", choosePeriod);
I have simplified some code to make my question simple. At the last row, I add an event listener to achieve what I described above. And the update function is choosePeriod. Now it is as following:
function choosePeriod() {
firstYear = parseInt(document.getElementById("FirstYear").value);
lastYear = parseInt(document.getElementById("LastYear").value);
d3.csv("output.csv", rowConventer, function (newdata) {
dataset = newdata;
series=stack(dataset);
groups.data(series);
groups.selectAll("rect")
.data(function (d) {
return d;
})
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScales(i);
})
.attr("y", function(d) {
return yScales(d[1]);
})
.attr("height", function(d) {
return yScales(d[0]) - yScales(d[1]);
})
.attr("width", xScales.bandwidth())
.append("title")
.text(function (d) {
var rect = this.parentNode;// the rectangle, parent of the title
var g = rect.parentNode;// the g, parent of the rect.
return d.data.Year + ", " + d3.select(g).datum().key + "," + (d[1]-d[0]);
});
groups.selectAll("rect")
.data(function (d) {
return d;
})
.exit()
.remove();
})
}
The change of dataset is achieved by rowConventer, which is not important in this question. Now the functionchoosePeriod is not running as envisioned! Theenter and the exit and update are all not work well! The whole picture is a mess! What I want is, for instance, if I input the firstYear=1900 and the lastYear=2000, then the picture should be updated with the period 1900-2000 to show. How can I achieve it?
I am unfamiliar the arrangement of the entire structure, I mean, at some place using d3.select() by class or id instead of label is better, right?
It looks like you've dealt with the enter and the exit selections. The only bit you're missing is the update selection, which will deal with the rectangles that already exist and don't need adding or removing. To do this copy your update pattern but just remove the enter().append() bit, e.g.:
groups.selectAll("rect")
.data(function (d) {
return d;
})
.attr("x", function(d, i) {
return xScales(i);
})
.attr("y", function(d) {
return yScales(d[1]);
})
.attr("height", function(d) {
return yScales(d[0]) - yScales(d[1]);
})
.attr("width", xScales.bandwidth())
.append("title")
.text(function (d) {
var rect = this.parentNode;// the rectangle, parent of the title
var g = rect.parentNode;// the g, parent of the rect.
return d.data.Year + ", " + d3.select(g).datum().key + "," + (d[1]-d[0]);
})

Circles keep accumulating and are not merging correctly

I am trying to merge these circles but I keep getting a graph of accumulating circles as opposed to circles moving across the graph?
What am I missing?
I have attached the code below. This function is called updatechart. It corresponds to a slider. So whenever I move the slider across the screen. I corresponding year it lands on is where the updated circle data should move.
var filteredyears = d3.nest()
.key(function(d) {
if(year === d.year){
return d;
}
}).entries(globaldataset);
var circled = svg.selectAll('.countries')
.data(filteredyears[1].values);
var circledEnter = circled.enter()
circled.merge(circledEnter);
circledEnter.append("circle").attr("cx", function(d) {
return xScale(d.gdpPercap);
})
.attr("cy", function(d) {
return yScale(d.lifeExp);
})
.attr('transform', "translate("+[40,30]+")")
.attr( 'r', function(d) {
return rScale(d.population) / 100})
.style("fill", function(d) {
if(d.continent == 'Asia'){
return '#fc5a74';
} else if (d.continent == 'Europe') {
return '#fee633';
} else if (d.continent == 'Africa') {
return '#24d5e8';
} else if (d.continent == 'Americas') {
return '#82e92d';
} else if (d.continent == 'Oceania') {
return '#fc5a74';
}
})
.style("stroke", "black");
circled.exit().remove();
You have a couple of issues using the merge() method, which is indeed quite hard to understand initially.
First, you have to reassign your selection:
circled = circled.merge(circledEnter);
Now, from this point on, apply the changes to circled, not to circledEnter:
circled.attr("//etc...
Besides that, your exit selection won't work, since you're calling it on the merged selection. Put it before the merge.
Finally, append goes to the circledEnter selection, before merging, as well as all attributes that don't change.
Here is a very basic demo showing it:
var svg = d3.select("svg"),
color = d3.scaleOrdinal(d3.schemeCategory10);
render();
function render() {
var data = d3.range(~~(1 + Math.random() * 9));
var circles = svg.selectAll("circle")
.data(data);
circles.exit().remove();
var circlesEnter = circles.enter()
.append("circle")
.attr("r", 5)
.attr("fill", function(d) {
return color(d);
});
circles = circlesEnter.merge(circles);
circles.attr("cx", function() {
return 5 + Math.random() * 290
})
.attr("cy", function() {
return 5 + Math.random() * 140
});
}
d3.interval(render, 1000);
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>

general update pattern III, update values not showing up

I am working on the modification of Mike Bostock's general update pattern III block and having a hard time understanding why, though the enter and exit values show up, the update values are not. I've read that assigning the specific value instead of using the data array value will help, as with a key, but this did not work. How do I modify this so entering values show up with their fill style, red color? I have read SO posts and re-read "How Selections Work" but still can't make it work.
Here is the code:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
text {
font: bold 28px monospace;
}
.enter {
fill: green;
}
.update {
fill: red;
}
.exit {
fill: blue;
}
</style>
<body>
<script src="../d3.v3.js"></script>
<script>
function randomData() {
return Math.floor(Math.random() * 200);
}
var the_values = [];
function randomEntry() {
var numlist = [];
var randomEntry;
var maximum,minimum;
maximum = 10; minimum = 1
var random_in_range = Math.floor(Math.random() * (maximum - minimum + 1)) + minimum;
var length_of_array = random_in_range;
console.log("length_of_array", length_of_array);
for (i = 0; i < length_of_array; i++) {
numlist.push([randomData(), randomData()]);
}
return numlist;
}
the_values = randomEntry();
console.log("the_values", the_values);
var width = 360,
height = 400;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(32," + (height / 2) + ")");
function update(data) {
// DATA JOIN
// Join new data with old elements, if any.
var text = svg.selectAll("text")
.data(data, function(d) {
return d;
})
.attr("transform", "translate(20," + (30) + ")");
var circles = svg.selectAll("circle")
.data(data, function(d) {
return d;
})
.attr("transform", "translate(20," + (30) + ")");
// UPDATE
// Update old elements as needed.
circles.attr("class", "update")
.transition()
.duration(750)
.attr("opacity", 0.3)
.attr("cx", function(d, i) {
return d[0];
})
.attr("cy", function(d, i) {
return d[1];
})
text.attr("class", "update")
.transition()
.duration(750)
.attr("x", function(d, i) {
return d[0];
})
.attr("y", function(d, i) {
return d[1];
})
// ENTER
// Create new elements as needed.
circles.enter().append("circle")
.attr("class", "enter")
.attr("opacity", 0.3)
.attr("r", 25)
.attr("cx", function(d, i) {
return d[0];
})
.attr("cy", function(d, i) {
return d[1];
})
.style("fill-opacity", 1e-6)
.transition()
.duration(750)
.attr("r", 30)
.style("fill-opacity", 1);
text.enter().append("text")
.attr("class", "enter")
.attr("dy", ".25em")
.attr("x", function(d) {
return d[0];
})
.attr("y", function(d) {
return d[1];
})
.style("fill-opacity", 1e-6)
.text(function(d) {
return d[0];
})
.transition()
.duration(750)
.style("fill-opacity", 1);
// EXIT
// Remove old elements as needed.
text.exit()
.attr("class", "exit")
.transition()
.duration(750)
.attr("y", 60)
.style("fill-opacity", 1e-6)
.remove();
circles.exit()
.attr("class", "exit")
.transition()
.duration(750)
.style("fill-opacity", 1e-6)
.remove();
}
// The initial display.
update(the_values);
// Grab a random sample of letters from the alphabet, in alphabetical order.
setInterval(function() {
update(randomEntry());
}, 1500);
</script>
From a quick glance at your code, it seems to be doing what you are looking for. Your enter circles are actually filled green, so you are actually seeing those. Updates are changed to red, but you don't see many of those because you are picking a few random numbers from 1-200. It's just unlikely that you will end up with any in the update selection, because that means that you selected the same number twice in a row.
To see some update circles, change:
return Math.floor(Math.random() * 200);
To:
return Math.floor(Math.random() * 10);
This throws the positions off, but you should soon see some red circles.
The reason is that in the update function you are always changing the whole array of input.
You are doing:
setInterval(function() {
update(randomEntry());//this will change the full array set
}, 1500);
This should have been:
setInterval(function() {
the_values.forEach(function(d){
//change the data set for update
})
update(the_values);
}, 1500);
Please note above i have not created a new array but I am passing the same array with changes to the update function.
Working fiddle here
Hope this helps!

Dynamically adjust bubble radius of counties?

Following the County Bubbles example, it's easy to add a bubble for each county. This is how it is added in the example:
svg.append("g")
.attr("class", "bubble")
.selectAll("circle")
.data(topojson.feature(us, us.objects.counties).features
.sort(function(a, b) { return b.properties.population - a.properties.population; }))
.enter().append("circle")
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("r", function(d) { return radius(d.properties.population); })
.append("title")
.text(function(d) {
return d.properties.name
+ "\nPopulation " + formatNumber(d.properties.population);
});
However, rather than using a variable from the json file (population), I need to update the radii according to a variable which dynamically changes (so I cannot put it in the json file beforehand as was done in the example). I call updateRadii() when a county is clicked, which needs access to the FIPS.
var currFIPS,
flowByFIPS;
var g = svg.append("g");
queue()
.defer(d3.json, "us.json")
.defer(d3.csv, "census.csv", function(d) {
return {
work: +d.workplace,
home: +d.residence,
flow: +d.flow
}
})
.await(ready);
function ready(error, us, commute) {
// Counties
g.append("g")
.attr("class", "counties")
.selectAll("path")
.data(topojson.feature(us, us.objects.counties).features)
.enter().append("path")
.attr("d", path)
.on("click", function(d) {
// Get FIPS of selected county
currFIPS = d.id;
// Filter on selected county (i.e., grab
// people who work in the selected county)
var data = commute.filter(function(d) {
return d.work == currFIPS;
});
// Create d3.map for where these people live
flowByFIPS = d3.map(data, function(d) {
return d.home;
});
// Update radii at "home" counties to reflect flow
updateRadii();
});
// Bubbles
g.append("g")
.attr("class", "counties")
.selectAll("circle")
.data(topojson.feature(us, us.objects.counties).features)
.enter().append("circle")
.attr("id", function(d) { return d.id; })
.attr("transform", function(d) {
return "translate(" + path.centroid(d) + ")";
})
.attr("r", 0); // invisible before a county is clicked
}
function updateRadii() {
svg.selectAll(".counties circle")
.transition()
.duration(300)
.attr("r", function(d) {
return flowByFIPS.get(d.id).flow
});
}
According to the error code, I believe that the circles do not have an id (FIPS code) attached. How do I get them to have an id? (I tried nesting the circle with the path using .each as explained in this answer, but could not get it working.)
Note that the above code works for updating fill on paths (rather than circles). For example, sub updateRadii(); for updateFill(); with the function as:
function updateFill() {
svg.selectAll(".counties path")
.transition()
.duration(300)
.attr("fill", function(d) {
return flowByFIPS.get(d.id).color; // e.g., "#444"
});
}
The problem here is that you don't supply d3 with data in the update function. I will recommend you update the data loaded from the file on the clicks, and from there you update the svg.
var update = function() {
g.selectAll(".country")
.data(data)
.attr("r", function(d) { return d.properties.flow_pct });
};
var data = topojson.feature(us, us.objects.counties).features;
data.forEach(function(x) { x.properties.flow_pct = /* calc the value */; })
g.append("g")
.attr("class", "counties")
.selectAll(".country")
.data(data)
.enter()
.append("circle")
.attr("class", "country")
.attr("transform", function(d) {
return "translate(" + path.centroid(d) + ")";
})
.on("click", function(d) {
// more code
data.forEach(function(x) { x.properties.flow_pct = /* calc the NEW value */; })
update();
});
update();
I've tried to use as much as the same code as before, but still trying to straiten it a bit out. The flow is now more d3-like, since the update function works on the changed data.
Another plus which this approach is both first render and future updates use the same logic for finding the radius.
It turns out to be an obvious solution. I forgot to check for the cases where a flow doesn't exist. The code above works if updateRadii() is changed to:
function updateRadii() {
svg.selectAll(".counties circle")
.transition()
.duration(300)
.attr("r", function(d) {
if (currFIPS == d.id) {
return 0;
}
var county = flowByFIPS.get(d.id);
if (typeof county === "undefined") {
return 0;
} else {
return county.flow;
}
});
}

Resources