I am making a heat map in D3 JS with Year along the X axis and Month along the Y axis. Each cell is a temperature and gets a different "fill" color based on this. My question is how can I make a color scale that maps a minTemp/maxTemp domain with a range of color codes. I have the code below so far, but that doesn't work:
var url = "https://raw.githubusercontent.com/FreeCodeCamp/ProjectReferenceData/master/global-temperature.json"
d3.json(url, function(json){
//load data from API and save in variable data
var data = json.monthlyVariance;
var baseTemp = json.baseTemperature;
//Add temperature to each object in data set
for(var i = 0; i < data.length; i++){
var temperature = baseTemp + data[i].variance
data[i].temperature = temperature;
var monthString = "";
switch(data[i].month){
case 1:
data[i].monthString = "January";
break;
case 2:
data[i].monthString = "February";
break;
case 3:
data[i].monthString = "March";
break;
case 4:
data[i].monthString = "April";
break;
case 5:
data[i].monthString = "May";
break;
case 6:
data[i].monthString = "June";
break;
case 7:
data[i].monthString = "July";
break;
case 8:
data[i].monthString = "August";
break;
case 9:
data[i].monthString = "September";
break;
case 10:
data[i].monthString = "October";
break;
case 11:
data[i].monthString = "November";
break;
case 12:
data[i].monthString = "December";
break;
}
}
//Set dimensions of div container, svg, and chart area(g element)
var margin = {top: 20, right: 40, bottom: 40, left: 80};
//Width of the chart, within SVG element
var w = 1000 - margin.left - margin.right;
//Height of the chart, within SVG element
var h = 500 - margin.top - margin.bottom;
//Create SVG element and append to #chart div container
var svg = d3.select("#chart")
.append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//Get Min Max values
var maxYear = d3.max(data, function(d){
return d.year;
});
var minYear = d3.min(data, function(d){
return d.year;
});
var maxTemp = d3.max(data, function(d){
return d.temperature;
});
var minTemp = d3.min(data, function(d){
return d.temperature;
})
//Create X scale, axis and label
var xScale = d3.scaleLinear()
.domain([minYear, maxYear])
.range([0,w]);
var xAxis = d3.axisBottom()
.scale(xScale)
.ticks(20)
.tickFormat(d3.format("d"));
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
//Create Y scale, axis and label
var cellHeight = (h / 12);
var yRange = [];
for(var i = 0; i < 12 ; i++){
yRange.push(i * cellHeight);
}
var yScale = d3.scaleOrdinal()
.domain(["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"])
.range(yRange);
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(12);
svg.append("g")
//append a g element
.attr("class", "axis")
.call(yAxis)
//call yAxis function on this g element
.selectAll(".tick text")
//select all elements with class tick and nested text element
.attr("transform", "translate(0," + (cellHeight/2) + ")");
//move all text elements half a cell height down
//Create color scale
var colors = d3.scaleOrdinal()
.domain([minTemp,maxTemp])
.range(["#5E4FA2", "#3288BD", "#66C2A5", "#ABDDA4", "#E6F598", "#FFFFBF", "#FEE08B", "#FDAE61", "#F46D43", "#D53E4F", "#9E0142"]);
//Select all rect elements in G container element, bind data and append
var cells = svg.selectAll("cells")
.data(data)
.enter()
.append("rect");
var cellAttributes = cells
.attr("x", function(d){
return xScale(d.year);
})
.attr("y", function(d){
return yScale(d.monthString);
})
.attr("width", w/(maxYear-minYear))
.attr("height", h/12)
.attr("fill", function(d){
return colors(d);
})
.attr("class", "cell");
});
I could write a long if/else statement in the fill attribute function, to map the temperature to a color code, but that is not the "D3 way" I think. How can I do it with a scale?:
var colors = d3.scaleOrdinal()
.domain([minTemp,maxTemp])
.range(["#5E4FA2", "#3288BD", "#66C2A5", "#ABDDA4", "#E6F598", "#FFFFBF", "#FEE08B", "#FDAE61", "#F46D43", "#D53E4F", "#9E0142"]);
You don't need an ordinal scale here. You need a quantize scale instead:
Quantize scales are similar to linear scales, except they use a discrete rather than continuous range. The continuous input domain is divided into uniform segments based on the number of values in (i.e., the cardinality of) the output range.
Thus, this should be your scale:
var colors = d3.scaleQuantize()
.domain([minTemp,maxTemp])
.range(["#5E4FA2", "#3288BD", "#66C2A5", "#ABDDA4", "#E6F598",
"#FFFFBF", "#FEE08B", "#FDAE61", "#F46D43", "#D53E4F", "#9E0142"]);
Here is a demo:
var data = d3.range(50);
var colors = d3.scaleQuantize()
.domain([0,50])
.range(["#5E4FA2", "#3288BD", "#66C2A5", "#ABDDA4", "#E6F598",
"#FFFFBF", "#FEE08B", "#FDAE61", "#F46D43", "#D53E4F", "#9E0142"]);
var svg = d3.select("svg");
var rects = svg.selectAll(".rects")
.data(data)
.enter()
.append("rect")
.attr("y", 10)
.attr("height", 100)
.attr("x", (d,i)=>10 + i*9)
.attr("width", 6)
.attr("fill", d=>colors(d))
.attr("stroke", "gray");
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500"></svg>
You can also use scaleLinear
, which has the advantage of interpolating between your colours (so, you'll have more than the 11 colours in your colours array). However, pay attention to set the same number of elements in the domain, using d3.ticks
:
d3.ticks(minTemp, maxTemp, 11);
Here is a demo with scaleLinear
:
var data = d3.range(50);
var colors = d3.scaleLinear()
.domain(d3.ticks(0, 50, 11))
.range(["#5E4FA2", "#3288BD", "#66C2A5", "#ABDDA4", "#E6F598",
"#FFFFBF", "#FEE08B", "#FDAE61", "#F46D43", "#D53E4F", "#9E0142"]);
var svg = d3.select("svg");
var rects = svg.selectAll(".rects")
.data(data)
.enter()
.append("rect")
.attr("y", 10)
.attr("height", 100)
.attr("x", (d,i)=>10 + i*9)
.attr("width", 6)
.attr("fill", d=>colors(d))
.attr("stroke", "gray");
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500"></svg>
thanks so much for the help, here is how I eventually did it:
demo: http://codepen.io/chemok78/full/qRXmWX/
var url = "https://raw.githubusercontent.com/FreeCodeCamp/ProjectReferenceData/master/global-temperature.json"
d3.json(url, function(json) {
//load data from API and save in variable data
var data = json.monthlyVariance;
var baseTemp = json.baseTemperature;
var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
//Add temperature to each object in data set
for (var i = 0; i < data.length; i++) {
var temperature = baseTemp + data[i].variance
data[i].temperature = temperature;
var monthString = "";
switch (data[i].month) {
case 1:
data[i].monthString = "January";
break;
case 2:
data[i].monthString = "February";
break;
case 3:
data[i].monthString = "March";
break;
case 4:
data[i].monthString = "April";
break;
case 5:
data[i].monthString = "May";
break;
case 6:
data[i].monthString = "June";
break;
case 7:
data[i].monthString = "July";
break;
case 8:
data[i].monthString = "August";
break;
case 9:
data[i].monthString = "September";
break;
case 10:
data[i].monthString = "October";
break;
case 11:
data[i].monthString = "November";
break;
case 12:
data[i].monthString = "December";
break;
}
}
//Set dimensions of div container, svg, and chart area(g element)
var margin = {
top: 40,
right: 60,
bottom: 100,
left: 100
};
//Width of the chart, within SVG element
var w = 1000 - margin.left - margin.right;
//Height of the chart, within SVG element
var h = 600 - margin.top - margin.bottom;
//Create SVG element and append to #chart div container
//SVG is nested G element
var svg = d3.select("#chart")
.append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//Get Min Max values
var maxYear = d3.max(data, function(d) {
return d.year;
});
var minYear = d3.min(data, function(d) {
return d.year;
});
var maxTemp = d3.max(data, function(d) {
return d.temperature;
});
var minTemp = d3.min(data, function(d) {
return d.temperature;
})
//Create X scale, axis and label
var xScale = d3.scaleLinear()
.domain([minYear, maxYear])
.range([0, w]);
var xAxis = d3.axisBottom()
.scale(xScale)
.ticks(20)
.tickFormat(d3.format("d"));
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
var xLabel = svg.append("text")
.text("Year")
.attr("x", w / 2)
.attr("y", h + (margin.bottom / 2.5))
.attr("font-size", "14px");
//Create Y scale, axis and label
var cellHeight = (h / 12);
var yRange = [];
for (var i = 0; i < 12; i++) {
yRange.push(i * cellHeight);
}
var yScale = d3.scaleOrdinal()
.domain(months)
.range(yRange);
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(12);
svg.append("g")
//append a g element
.attr("class", "axis")
.call(yAxis)
//call yAxis function on this g element
.selectAll(".tick text")
//select all elements with class tick and nested text element
.attr("transform", "translate(0," + (cellHeight / 2) + ")");
//move all text elements half a cell height down
var yLabel = svg.append("text")
.attr("transform", "rotate(-90)")
.attr("x", 0 - (h / 2))
.attr("y", 0 - (margin.left / 1.8))
.style("font-size", "14px")
.style("text-anchor", "middle")
.text("Month");
//Create color scale
var colorCodes = ["#5E4FA2", "#3288BD", "#66C2A5", "#ABDDA4", "#E6F598", "#FFFFBF", "#FEE08B", "#FDAE61", "#F46D43", "#D53E4F", "#9E0142"];
var colors = d3.scaleQuantile()
//quantize scale divides domain in bands according to ordinal scale range
.domain([minTemp, maxTemp])
//.domain(d3.ticks(minTemp,maxTemp,11))
.range(colorCodes);
var colorQuantiles = colors.quantiles();
colorQuantiles.unshift(0);
//save the upper ranges of each temperature quantile + 0 at the beginning (quantile function does not count 0 as start)
//Append tooltip to chart area. Fully transparant at first
var tip = d3.select("#chart").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
//Select all rect elements in G container element, bind data and append
var cells = svg.selectAll("cells")
.data(data)
.enter()
.append("rect");
var cellAttributes = cells
.attr("x", function(d) {
return xScale(d.year);
})
.attr("y", function(d) {
return yScale(d.monthString);
})
.attr("width", w / (maxYear - minYear))
.attr("height", cellHeight)
.attr("fill", function(d) {
return colors(d.temperature);
})
.attr("class", "cell")
.on("mouseover", function(d) {
tip.transition()
.style("opacity", 0.7);
tip.html("<strong>" + months[d.month - 1] + " - " + d.year + "</strong><br>" + d.temperature.toFixed(2) + " °C<br>" + d.variance.toFixed(2) + " °C")
.style("left", d3.event.pageX + "px")
.style("top", d3.event.pageY - 70 + "px");
})
.on("mouseout", function(d) {
tip.transition()
.style("opacity", 0);
})
//Create a legend
var blockWidth = 35;
var blockHeight = 20;
var legend = svg.selectAll(".legend")
.data(colorQuantiles)
.enter()
.append("g")
.attr("class", "legend")
.attr("font-size", "14px")
.attr("font-style", "PT Sans")
.attr("transform", function(d, i) {
return ("translate(" + i * blockWidth + ",0)")
});
legend.append("rect")
.attr("x", (w / 5) * 3)
.attr("y", h + (margin.bottom / 3))
.attr("width", blockWidth)
.attr("height", blockHeight)
.style("fill", function(d, i) {
return (colorCodes[i]);
});
legend.append("text")
.attr("x", ((w / 5) * 3) + (blockWidth / 2))
.attr("y", (h + (margin.bottom / 3)) + blockHeight + 15)
.text(function(d, i) {
return colorQuantiles[i].toFixed(1);
})
.style("text-anchor", "middle");
})
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With