Choropleth map scale and legend - d3.js

Let me preface this by saying I am brand new to D3.js and coding in general. I am an infographic artist and I've been using QGIS to generate maps, but am trying to use D3.js to generate a choropleth map for a story about Opioid deaths. Basically I am trying to recreate this map.
map from the Economist
I have tried to start by using this map by Mike Bostock and changing some of the parameters but am getting stuck with the color range and scale. The measurement is 1 per 100,000 population. I have a domain that starts at 1.543385761 and ends at 131.0814217.
The code I'm struggling with is around the scale input and output:
var x = d3.scaleLinear()
.domain([0, 132])
.rangeRound([600, 860]);
var color = d3.scaleThreshold()
.domain(d3.range(2, 10))
.range(d3.schemeBlues[9]);
var g = svg.append("g")
.attr("class", "key")
.attr("transform", "translate(0, 40)");
g.selectAll("rect")
.data(color.range().map(function(d) {
d = color.invertExtent(d);
if (d[0] == null) d[0] = x.domain()[0];
if (d[1] == null) d[1] = x.domain()[1];
return d;
}))
.enter().append("rect")
.attr("height", 8)
.attr("x", function(d) { return x(d[0]); })
.attr("width", function(d) { return x(d[1]) - x(d[0]); })
.attr("fill", function(d) { return color(d[0]); });
I can see that I need some bit of code that will define everything 25 and over as the darkest color. Not even sure I want that to be my final legend but I'd love to know how to reproduce that. I am shocked I was able to get this far but feel a bit lost right now. thank you in advance!

Let's examine your scale:
var color = d3.scaleThreshold()
.domain(d3.range(2, 10))
.range(d3.schemeBlues[9]);
Your domain is an array of created like so:
d3.range(2,10) // [2,3,4,5,6,7,8,9]
These are your thresholds, colors will be mapped based on values that are less than or equal to 2, more than two up to three, more than three and up to four .... and over 9. This domain is mapped to nine values defined in the range:
d3.schemeBlues[9] // ["#f7fbff", "#deebf7", "#c6dbef", "#9ecae1", #6baed6", #4292c6", "#2171b5", "#08519c", "#08306b"]
To set the thresholds for those colors so that values over 25 are one color, define the domain with array that has the appropriate threshold(s):
.domain([2,3,4,5,6,7,8,25]);
In the snippet below, this domain is applied. Rectangles have colors dependent on their location, all rectangles after the 25th (count left to right then line by line) one will be of one color.
var color = d3.scaleThreshold()
.domain([2,3,4,5,6,7,8,25])
.range(d3.schemeBlues[9]);
var svg = d3.select("body")
.append("svg")
.attr("width",500)
.attr("height",500);
var rects = svg.selectAll("rect")
.data(d3.range(100))
.enter()
.append("rect")
.attr("width",15)
.attr("height", 15)
.attr("y", function(d,i) { return Math.floor(i / 10) * 20 + 10 })
.attr("x", function(d,i) { return i % 10 * 20 })
.attr("fill", function(d) { return color(d); })
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>

Related

Rotating the text in the pie chart of d3.js and alternate filling of colors

I have this d3 code for drawing the pie chart in d3.js
/** START OF PIE CHART */
var svgCirWidth = 600, svgCirHeight = 300, radius = Math.min(svgCirWidth, svgCirHeight) / 2;
const pieContainer = d3.select("#pieChart")
.append("svg")
.attr("width", svgCirWidth)
.attr("height", svgCirHeight);
//create group element to hold pie chart
var g = pieContainer.append("g")
.attr("transform", "translate(" + 250 + "," + radius + ")");
var color = d3.scaleOrdinal(d3.schemeCategory10);
var pie = d3.pie().value(function (d) {
return d.total_up_percentage;
});
var path = d3.arc()
.outerRadius(radius)
.innerRadius(0);
var arc = g.selectAll("arc")
.data(pie(data))
.enter() //means keeps looping in the data
.append("g");
arc.append("path")
.attr("d", path)
.attr("fill", function (d) {
return color(d.data.total_up_percentage);
})
.append("text")
.text("afdaf");
var label = d3.arc()
.outerRadius(radius)
.innerRadius(0);
arc.append("text")
.attr("transform", (d) => {
return "translate(" + label.centroid(d) + ")";
})
.attr("text-anchor", "middle")
.text((d) => {
return d.data.region_iso_code + ":" + d.data.total_up_percentage + "%"
});
and this is the result of my pie
as you can see the text overlaps each other. I was wondering how can i rotate the text so it can be much more easier to read. I've tried editing the transform in the console but it won't work it just makes the text go up or down. Also I was wondering what happened to the color of my pie. It stuck on orange. It says on the documentation i read about this schemeCategory10 is that it is a 10 color code scheme. Yet it won't show the rest of the color. Is there any other way to change color?
When using an ordinal scale you should never rely on the scale's ability to infer the domain from usage: a good practice is always to explicitly set the domain.
By setting the domain you'd quickly see that this is indeed the expected behaviour: all orange slices have the same value, which is 100.
If you want different colors for those same values, use the indices instead:
.attr("fill", function (_, i) {
return color(i);
})
PS: regarding the texts, please avoid asking 2 or more different issues in a single question. Edit your question leaving just 1 issue, you can always post a new question with the other issues.

Using scaleQuantile with "viridis" color scheme

I have data whose values have a range (0, 100) but most of them have values ranging between 80 and 100.
Example of data: 97.00 93.30 92.20 92.70 91.10 89.10 89.90 89.10 89.70 88.90
89.00 89.30 88.76 88.46 87.45 85.05
I have to do a visualization using colors and using a linear scale is not the best because it does not allow me to distinguish colors quite easily.
So I thought about using a scaleQuantile.
I read this post that uses colors from black to red but I would like to use the Viridis scale.
How can I do that?
This is my piece of code:
var colorScale = d3.scaleQuantile(d3.interpolateViridis)
.domain([0, 100]);
// other code
var cells = svg.selectAll('rect')
.data(data)
.enter().append('g').append('rect')
.attr('class', 'cell')
.attr('width', cellSize)
.attr('height', cellSize)
.attr("rx", 4)
.attr("ry", 4)
.attr('y', function(d) {
return yScale(d.nuts_name);
})
.attr('x', function(d) {
return xScale(d.year);
})
.attr('fill', function(d) {
return colorScale(d.value);
}
})
Thanks
You have two problems here:
The domain in a quantile scale, unlike a quantize scale, is not a range between two values. It has to be the array with all the values. The API is clear about that:
If domain is specified, sets the domain of the quantile scale to the specified set of discrete numeric values. (emphasis mine)
That's not the correct way to use d3.interpolateViridis. Again, the API is clear:
Given a number t in the range [0,1], returns the corresponding color from the “viridis” perceptually-uniform color scheme
So, a simple solution is creating the quantile scale in such a way that it returns a number from 0 to 1 according to your data array (here, I'm creating 10 bins):
var colorScale = d3.scaleQuantile()
.domain(data)
.range(d3.range(0, 1.1, 0.1));
And then pass that value to d3.interpolateViridis:
d3.interpolateViridis(colorScale(d))
Here is a demo. The first row of <divs> use the data as they are, the second one uses a sorted array:
var data = [97.00, 93.30, 92.20, 92.70, 91.10, 89.10, 89.90, 89.10, 89.70, 88.90, 89.00, 89.30, 88.76, 88.46, 87.45, 85.05];
var sortedData = data.concat().sort();
var colorScale = d3.scaleQuantile()
.domain(data)
.range(d3.range(0, 1.1, 0.1));
var divs = d3.select("body").selectAll(null)
.data(data)
.enter()
.append("div")
.attr("class", "cell")
.style("background-color", function(d) {
return d3.interpolateViridis(colorScale(d))
});
d3.select("body").append("div")
.style("height", "40px")
var div2 = d3.select("body").selectAll(null)
.data(sortedData)
.enter()
.append("div")
.attr("class", "cell")
.style("background-color", function(d) {
return d3.interpolateViridis(colorScale(d))
});
.cell {
width: 20px;
height: 20px;
margin: 2px;
display: inline-block;
}
<script src="https://d3js.org/d3.v4.min.js"></script>

Use pies as points in a line chart and change the radius of the pie

I have a line chart (or, more properly a connected scatterplot) where I plot pies/donuts around the points. So, there is a data set that specifies the date and mood for plotting the points along with two other parameters, pos and neg, providing the values to go into the pie chart. The overall data that describes the points is called plotPoints.
That all works great, but what I still would like to do is to set the radius of the pie to be a function of the sum of pos + neg.
When I plot out the points, I can access all of the data with a function(d). In each pie, however, function(d) returns the data about the slices, one at a time. d contains the data for the current slice, not the overall data. For each pie, the first time arc is called it has the frequency for the first pie slice and the second time it is called it has the frequency for the second pie slice.
How do I refer to the current plotPoints properties when drawing the arc so that I can change the radius of the pie/donut to represent the sum of plotPoints[i].pos + plotPoints[i].neg?
The relevant code looks like this:
var arc = d3.svg.arc()
.outerRadius(radius - 10)
.innerRadius(8);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d; });
var p = moodChart.selectAll(".pieContainer")
.data(plotPoints).enter()
.append("g")
.attr("class","pieContainer")
.attr("transform", function(d,i) {return "translate(" + (x(d.date)) + "," + (y(d.mood)) + ")"});
p.append("title")
.text(function(d) { return shortDateFormat(d.date) +", " + d.mood.toFixed(2) });
var g = p.selectAll(".arc")
.data(function (d) {return pie([d.neg, d.pos])})
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.style("fill", function(d,i) { return i==0 ? "brown" : "green"; });
It's tough to answer this authoritatively without a bit more code/data to look at but in this situation I usually stash the needed variables in my data-bindings so they are available later:
var g = p.selectAll(".arc")
.data(function (d) {
var total = d.neg + d.pos,
pie_data = pie([d.neg, d.pos]),
point_arc = d3.svg.arc()
.outerRadius((total * radius) - 10) //<-- set radius based on total
.innerRadius((total * radius) - 8);
pie_data.forEach(function(d1){
d1.data.arc = point_arc; //<-- stash the arc for this point in our data bindings
});
return pie_data;
});
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", function(d){
return d.data.arc
})
.style("fill", function(d,i) { return i==0 ? "brown" : "green"; });

d3 elements within a group not being removed properly

I have a table with filtered data that's working properly and now I'm trying to make a corresponding barchart. The barchart consists of a group for each bar, with two text elements and a rect inside of it. The exit selection successfully removes the g element but the internal rect and text somehow ends up in another g.
function updateSvg(data) {
parameters.svg = d3.select(".svg")
.attr("height", data.length * parameters.barHeight)
var life_expectancy = d3.extent(data.map(getter('life_expectancy')));
var min = life_expectancy[0];
var max = life_expectancy[1];
var x = d3.scale.linear()
.domain([0, max])
.range([0, parameters.svgWidth])
// Data join.
var groups = parameters.svg.selectAll("g")
.data(data)
// Enter.
var groupsEnter = groups.enter().append("g").attr("transform", function(d, i) { return "translate(0," + i * parameters.barHeight + ")"; })
// Update.
var bars = groups.append("rect")
.attr("width", function(d) { return x(d.life_expectancy)})
.attr("height", parameters.barHeight - 1)
var labels = groups.append("text")
.attr("x", 20)
.attr("y", parameters.barHeight / 2)
.attr("dy", ".35em")
.text(function(d) { return d.name; })
var values = groups.append("text")
.attr("x", function(d) { return x(d.life_expectancy) - 50; })
.attr("y", parameters.barHeight / 2)
.attr("dy", ".35em")
.text(function(d) { return d.life_expectancy})
// Exit.
groups.exit().remove()
}
Here's what I have working so far: http://chrisdaly.github.io/D3/World%20Countries%20Rank/table.html. If you untick all the continents except Oceania for example and inspect the bars, it shows a tonne of different rects etc hidden underneath the correct one. Any guidance is appreciated!
The problem is here
groups.exit().remove()
On slider motion the values with in the country array will change but none of the g group DOM will get removed because the array still has the same number of array elements. So on that g group you go on appending rect and text.
groups.append("rect")
.attr("width", function(d) { return x(d.life_expectancy)})
.attr("height", parameters.barHeight - 1)
Now when you tick off Americas the g tag for USA will go which is what exit function does. Reason: your array is filtered has no record for USA.
But the g for Asia countries and others you append the text and rect again thus it keeps growing.
Best way out is when you update do this to remove all rect and text:
groups.selectAll("text").remove();
groups.selectAll("rect").remove();

Transitions in D3 - Duplicate bubbles instead of transitioning bubbles

I'm trying to create something similar to this example: Wealth and Health of Nations:
My data comes from a JSON file, just like the example, but when I add the transitions, I'm getting duplicate bubbles. Instead of the bubble transitioning from point A to point B I'm getting 2 bubbles (one for point A, one for point B). Generally speaking, the transition is not able to differentiate between 2 data points for the same bubble or 2 separate bubbles.
Looking at the example, I'm missing the interpolate and bisect functions. I haven't been able to grasp how they work and what exactly i'm doing wrong. Is this what's causing the problem in my graph?
Also, can someone give me an example on how bisectors and interpolate works in d3?
Code:
g = d3.select("#animation")
.append("svg")
.attr("width", width)
.attr("height", height);
x_extent = [0, 100];
x_scale = d3.scale.linear().domain(x_extent).range([margin + 20, width - 30]);
y_extent = [0, 60];
y_scale = d3.scale.linear().domain(y_extent).range([height - margin, margin]);
r_scale = d3.scale.linear().domain([0, d3.max(jsondata, function (d) { return d.MSVMMboe; })]).range([2, 30]);
g.selectAll("circle").data(jsondata, function (d) { return d.EffectiveDate; }).enter().append("circle")
.attr("cx", function (d) { return x_scale(d.PercentageComplete * 100) })
.attr("cy", function (d) { return y_scale(d.GPoS * 100) })
.attr("r", function (d) { return r_scale(d.MSVMMboe) })
.attr("stroke", "blue")
.attr("stroke-width", 1)
.attr("opacity", 0.6)
.attr("fill", "red");
//add transition
g.selectAll("circle").data(jsondata, function (d) { return d.EffectiveDate; })
.transition()
.duration(1000);
You haven't told the transition what you want to change. You need to add some attribute changes for example. Have a look at the d3 website for examples and tutorials.

Resources