Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make a color scale in D3 JS to use in fill attribute?

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"]);
like image 650
chemook78 Avatar asked Jan 25 '17 10:01

chemook78


2 Answers

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>
like image 162
Gerardo Furtado Avatar answered Nov 05 '22 14:11

Gerardo Furtado


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");

})
like image 21
chemook78 Avatar answered Nov 05 '22 16:11

chemook78