mmz
mmz

Reputation: 1151

D3: How to create a custom, skewed color scale?

I created the following color scale:

enter image description here

Here's the code:

  function continuous(selector_id, colorscale) {
    var legendheight = 200,
      legendwidth = 80,
      margin = { top: 10, right: 60, bottom: 10, left: 2 };

    var canvas = d3
      .select(selector_id)
      .style("height", legendheight + "px")
      .style("width", legendwidth + "px")
      .style("position", "relative")
      .append("canvas")
      .attr("height", legendheight - margin.top - margin.bottom)
      .attr("width", 1)
      .style("height", legendheight - margin.top - margin.bottom + "px")
      .style("width", legendwidth - margin.left - margin.right + "px")
      .style("border", "1px solid #000")
      .style("position", "absolute")
      .style("top", margin.top + "px")
      .style("left", margin.left + "px")
      .node();

    var ctx = canvas.getContext("2d");

    var legendscale = d3
      .scaleLinear()
      .range([1, legendheight - margin.top - margin.bottom])
      .domain(colorscale.domain());

    var image = ctx.createImageData(1, legendheight);
    d3.range(legendheight).forEach(function (i) {
      var c = d3.rgb(colorscale(legendscale.invert(i)));
      image.data[4 * i] = c.r;
      image.data[4 * i + 1] = c.g;
      image.data[4 * i + 2] = c.b;
      image.data[4 * i + 3] = 255;
    });
    ctx.putImageData(image, 0, 0);

    var legendaxis = d3.axisRight().scale(legendscale).tickSize(6).ticks(8);

    var svg = d3
      .select(selector_id)
      .append("svg")
      .attr("height", legendheight + "px")
      .attr("width", legendwidth + "px")
      .style("position", "absolute")
      .style("left", "0px")
      .style("top", "0px");

    svg
      .append("g")
      .attr("class", "axis")
      .attr(
        "transform",
        "translate(" +
          (legendwidth - margin.left - margin.right + 3) +
          "," +
          margin.top +
          ")"
      )
      .call(legendaxis);
  }

  useEffect(() => {
    var colorScale = d3.scaleSequential(d3.interpolateRdYlGn).domain([-4, 16]);
    var svg = continuous("#legend1", colorScale);
  }, []);

Since my data is skewed towards values above 0, I want to create a custom scale with skewed red-to-green coloring and a transition occurring at 0 rather than 6. However, I'm having trouble implementing custom scaling functionality.

I followed suggestions from this post but was unable to implement the functionality, namely:

var custom = d3.scaleLinear()
  .domain([0,       50,       90,        95,       100])
  .range(['#edfc1b','#ec6f3b','#bc2e67','#7c0093', '#0b0074']);

At the moment, all I'm getting from the code above is: enter image description here

So, my question: how can I get the full domain plotted if I have an array of values such as [0, 50, 90, 95, 100]?

Alternatively, is there a better way to create a custom color scale in D3?

Upvotes: 1

Views: 641

Answers (1)

Rodrigo Divino
Rodrigo Divino

Reputation: 1931

The code that defines the scale is correct:

  var custom = d3.scaleLinear()
    .domain([0,       50,       90,        95,       100])
    .range(['#edfc1b','#ec6f3b','#bc2e67','#7c0093', '#0b0074']);

It is the code that translate this color scale to a spatial scale that is the issue:

  var legendscale = d3
      .scaleLinear()
      .range([1, legendheight - margin.top - margin.bottom])
      .domain(colorscale.domain())

The code above creates a scale with a range of length 2, and a domain of length 5. The scaleLinear needs to arrays of equal lengths in the domain and range, otherwise it will not do a 1-to-1 map as expected.

A quick fix when the length of the domain is fixed (in your case, 5) is to manually expand the range with the intermediate steps.

  var max = legendheight - margin.top - margin.bottom;
  var legendscale = d3
      .scaleLinear()
      .range([1, max / 4, 2*(max / 4), 3*(max / 4), max])
      .domain(colorscale.domain())

Upvotes: 1

Related Questions