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

30,392

Solution 1

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>

Solution 2

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

})
Share:
30,392
chemook78
Author by

chemook78

Updated on July 09, 2022

Comments

  • chemook78
    chemook78 almost 2 years

    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"]);
    
  • chemook78
    chemook78 over 7 years
    both work!! thanks. But how does D3 interpolate the range using scaleLinear()? Does it know the strings in the range are color codes?
  • chemook78
    chemook78 over 7 years
    thats cool! any D3 way I can get the values of the domain segments that are mapped to the color codes? Or should i just divide max domain value by the length of range and work with that. Want to create a legend
  • Gerardo Furtado
    Gerardo Furtado over 7 years
    If you use a quantile scale, you can do console.log(colors.quantiles()).
  • chemook78
    chemook78 over 7 years
    thanks! How to do it using a quantize scale? So I want to now what temp range is mapped to the first color, what temp range is mapped to second color etc.
  • chemook78
    chemook78 over 7 years
    I can do this stackoverflow.com/questions/20550840/how-to-get-quantize-val‌​ues, but I wonder if there is build in D3 method for this?
  • Gerardo Furtado
    Gerardo Furtado over 7 years
    No. As I told you, three is for a quantile scale, but not for a quantize. You can try a quantile, it's almost the same scale...
  • Gerardo Furtado
    Gerardo Furtado over 7 years
    @chemook78 have a look at my answer here: stackoverflow.com/a/39323169/5768908
  • Eng Soon Cheah
    Eng Soon Cheah over 4 years
    but how to change to image?
  • Gerardo Furtado
    Gerardo Furtado over 4 years
    @EngSoonCheah I have no idea what you're asking.