1
votes

I'm playing around with a Mapbox map (written in React) that currently gives me the following output:

enter image description here

My problem: the coloring of the GeoJSON objects is incorrect. It implies that both shapes have values of ~-4. In truth, the left-hand figure has a value of 10, and should be solid blue, while the right-hand figure, with a value of 5, should be blue-green.

Here's the code for the Mapbox GeoJSON layer:

export const COLOR_SCALE = d3
  .scaleSequential(d3.interpolateYlGnBu)
  .domain([-4, 16])
  .range([
    [255, 255, 217],
    [248, 252, 201],
    [239, 249, 189],
    [228, 244, 181],
    [213, 238, 179],
    [193, 231, 181],
    [169, 221, 183],
    [142, 211, 186],
    [115, 201, 189],
    [91, 191, 192],
    [69, 180, 194],
    [52, 167, 194],
    [40, 151, 191],
    [33, 134, 185],
    [32, 115, 178],
    [34, 96, 169],
    [35, 78, 160],
    [34, 62, 149],
    [28, 49, 133],
    [19, 38, 112],
  ]);

var layers = [
    new GeoJsonLayer({
      id: "geojson",
      data: data,
      filled: true,
      getFillColor: (f) => COLOR_SCALE(f.properties.growth),
    }),
  ];

Here's the code for the color scale:

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

Where svg is used as a React component. Finally, if needed, here's how I define continuous:

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

How can I configure COLOR_SCALE such that the shapes are properly colored? I suspect the error stems from the range values. For context, I got the RGB values from colorScale1 by iterating using console.log(colorScale1(i)).

1

1 Answers

1
votes

The coloring "implies that both shapes have values of ~-4" - this is expected given your scale's range.

A sequential scale has exactly two elements in its range array (generally specified with an interpolator rather than an array). Providing an array of more than two elements means all those beyond the first two are ignored - so you're interpolating between the 1st ( enter image description here ) and 2nd ( enter image description here ) elements in the provided range array - these elements represent values around -4, so everything will look like it is around ~ -4 when compared with a scale that has the intended range ([ enter image description here , enter image description here ]) - like the one used in the legend.

The provisioning of an array to .range will actually cause errors in some versions of d3 - the use of an interpolator only is the much more common approach

While I can't recreate your exact issue here - recyling the same scale for both (or most simply removing the range values altogether since otherwise the two color scales are identical) should work: you want the same representation for the same values whether in the legend or the map.

Below I use rectangles instead of geographic features, but it should be analagous to your situation:

var legendheight = 100;
var legendwidth = 40;
var colorscale = d3.scaleSequential(d3.interpolateYlGnBu)
  .domain([-4, 16]);

var legendscale = d3
      .scaleLinear()
      .range([0, legendheight])
      .domain(colorscale.domain());

var canvas = d3
      .select("body")
      .style("height", legendheight + "px")
      .style("width", legendwidth + "px")
      .style("position", "relative")
      .append("canvas")
      .attr("height", legendheight)
      .attr("width", 1)
      .style("height", legendheight + "px")
      .style("width", legendwidth + "px")
      .style("border", "1px solid #000")
      .style("position", "absolute")
      .style("top", 10 + "px")
      .style("left", 0 + "px")
      .node();

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

    var legendscale = d3
      .scaleLinear()
      .range([0, legendheight])
      .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);
      
      d3.select("svg")
      .append("g")
      .attr("class", "axis")
      .attr(
        "transform",
        "translate(" +
          legendwidth +
          ",10)"
      )
      .call(legendaxis);
      
      
  d3.select("svg")
    .selectAll("rect")
    .data(d3.range(30).map(function(d,i) {
       return i / 29 * 20 - 4
    }))
    .enter()
    .append("rect")
    .attr("x", (d,i)=>i%10*20+100)
    .attr("y", (d,i)=>Math.floor(i/10)*20+10)
    .attr("width",16)
    .attr("height",16)
    .attr("fill",d=>colorscale(d));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<svg width=500 height=300></svg>