block by rveciana 3753394b3b6fd22df2c867bb02b320b4

Change shaded relief colors dynamically

Full Screen

This example shows how to use the color scale chooser and a dynamically generated shaded relief image.

Click on the color scale to change, remove or add the colors and values.

You can check a similar example that changes colors to an image with isobands

This tutorial has all the information needed to create the visualization.

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<style>

</style>
<body>

<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="geotiff.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script src="color-scale.js"></script>

<script>
var width = 680,
    height = 500;

var projection = d3.geoAzimuthalEqualArea()
    .rotate([-8.2, -46.8])
    .translate([width/2, height/2])
    .scale(12000);

var canvas = d3.select("body").append("canvas")
    .attr("width", width)
    .attr("height", height);

var context = canvas.node().getContext("2d");
d3.request("swiss.tiff")
  .responseType('arraybuffer')
  .get(function(error, tiffData){
d3.json("swiss.json", function(error, topojsonData) {
  var countries = topojson.feature(topojsonData, topojsonData.objects.country);

  var path = d3.geoPath()
      .projection(projection).context(context);

  var tiff = GeoTIFF.parse(tiffData.response);
  var image = tiff.getImage();
  var rasters = image.readRasters();
  var tiepoint = image.getTiePoints()[0];
  var pixelScale = image.getFileDirectory().ModelPixelScale;
  var geoTransform = [tiepoint.x, pixelScale[0], 0, tiepoint.y, 0, -1*pixelScale[1]];
  var invGeoTransform = [-geoTransform[0]/geoTransform[1], 1/geoTransform[1],0,-geoTransform[3]/geoTransform[5],0,1/geoTransform[5]];

  var altData = new Array(image.getHeight());
  for (var j = 0; j<image.getHeight(); j++){
      altData[j] = new Array(image.getWidth());
      for (var i = 0; i<image.getWidth(); i++){
          altData[j][i] = rasters[0][i + j*image.getWidth()];
      }
  }

  //Calculate shaded Relief. Outside the first loop, to be able to put it in a function
  var azimuth = 315;
  var angleAltitude = 45;
  var azimuthrad = azimuth*Math.PI / 180;
  var altituderad = angleAltitude*Math.PI / 180;

  var shadedData = new Array(image.getHeight());
  for (var j = 0; j<image.getHeight(); j++){
    shadedData[j] = new Array(image.getWidth());
    for (var i = 0; i<image.getWidth(); i++){
      var gradX, gradY;
      if(i==0) gradX = altData[j][i+1] - altData[j][i];
      else if(i==image.getWidth()-1) gradX = altData[j][i] - altData[j][i-1];
      else gradX = (altData[j][i+1] - altData[j][i])/2 + (altData[j][i] - altData[j][i-1])/2;

      if(j==0) gradY = altData[j+1][i] - altData[j][i];
      else if(j==image.getHeight()-1) gradY = altData[j][i] - altData[j-1][i];
      else gradY = (altData[j+1][i] - altData[j][i])/2 + (altData[j][i] - altData[j-1][i])/2;

      var slope = Math.PI/2 - Math.atan(Math.sqrt(gradX*gradX + gradY*gradY));
      var aspect = Math.atan2(-gradY, gradX);

      shadedData[j][i] = Math.sin(altituderad) * Math.sin(slope)
        + Math.cos(altituderad) * Math.cos(slope)
        * Math.cos(azimuthrad - aspect);
    }
  }

  var canvasShaded = d3.select("body").append("canvas")
      .attr("width", width)
      .attr("height", height)
      .style("display","none");

  var contextShaded = canvasShaded.node().getContext("2d");

  var idShaded = contextShaded.createImageData(width,height);
  var dataShaded = idShaded.data;
  var posShaded = 0;
  for(var j = 0; j<height; j++){
    for(var i = 0; i<width; i++){
      var pointCoords = projection.invert([i,j]);
      var px = invGeoTransform[0] + pointCoords[0]* invGeoTransform[1];
      var py = invGeoTransform[3] + pointCoords[1] * invGeoTransform[5];

      var shadedValue;
      if(Math.floor(px) >= 0 && Math.ceil(px) < image.getWidth() && Math.floor(py) >= 0 && Math.ceil(py) < image.getHeight()){
        shadedValue = 255*(1+shadedData[Math.floor(py)][Math.floor(px)])/2;

      } else {
        shadedValue = 255;
      }
      dataShaded[posShaded]   = shadedValue;
      dataShaded[posShaded+1]   = shadedValue;
      dataShaded[posShaded+2]   = shadedValue;
      dataShaded[posShaded+3]   = 255 - shadedValue;

      posShaded=posShaded+4;
    }
  }

  contextShaded.putImageData( idShaded, 0, 0);

  var scaleValues = [{value: 200, color: "#c9d5a6"},
  {value: 900, color: "#7fa67a"},
  {value: 1600, color: "#976a2f"},
  {value: 2300, color: "#79750a"},
  {value: 3000, color: "#7ab5e3"},
  {value: 3700, color: "#fefefe"},
  {value: 4400, color: "#f605cf"}];

  var scale = d3.ColorScaleChooser()
    .squareWidth(50)
    .squareHeight(30)
    .scaleValues(scaleValues)
    .title("Altitude (m)")
    .on("change", function(d, min, max){
      var scaleWidth = 256;
      d3.select("#colorScaleCanvas").remove();
      var canvasColorScale = d3.select("body").append("canvas")
          .attr("width", scaleWidth)
          .attr("height", 1)
          .attr("id", "colorScaleCanvas")
          .style("display","none");
      var contextColorScale = canvasColorScale.node().getContext("2d");
      var gradient = contextColorScale.createLinearGradient(0, 0, scaleWidth, 1);

      for (var i = 0; i < d.length; ++i) {
        var position = (d[i].value-min)/(max-min);
        if(position >= 0 && position < scaleWidth){
          gradient.addColorStop(position, d[i].color);
        }
      }
      contextColorScale.fillStyle = gradient;
      contextColorScale.fillRect(0, 0, scaleWidth, 1);

      var csImageData = contextColorScale.getImageData(0, 0, scaleWidth, 1).data;

      draw(csImageData, min, max);
    });

  var svg = d3.select("body").append("svg")
      .attr("width", 150)
      .attr("height", 500)
      .style("position", "absolute")
      .style("left", "10px")
      .style("top", "10px");
  svg.call(scale);


  function draw(csImageData, min, max){
    context.drawImage(canvasShaded.node(), 0, 0);
    context.globalAlpha = 0.7;
    context.fillStyle = '#ffffff';
    context.fillRect(0,0,width,height);
    context.fill();
    context.globalAlpha = 1;
    var scaleWidth = csImageData.length/4;
    var canvasRaster = d3.select("body").append("canvas")
        .attr("width", width)
        .attr("height", height)
        .style("display","none");

    var contextRaster = canvasRaster.node().getContext("2d");

    var id = contextRaster.createImageData(width,height);
    var data = id.data;
    var pos = 0;
    for(var j = 0; j<height; j++){
      for(var i = 0; i<width; i++){
        var pointCoords = projection.invert([i,j]);
        var px = invGeoTransform[0] + pointCoords[0]* invGeoTransform[1];
        var py = invGeoTransform[3] + pointCoords[1] * invGeoTransform[5];

        var value;
        if(Math.floor(px) >= 0 && Math.ceil(px) < image.getWidth() && Math.floor(py) >= 0 && Math.ceil(py) < image.getHeight()){
          var dist1 = (Math.ceil(px)-px)*(Math.ceil(py)-py);
          var dist2 = (px-Math.floor(px))*(Math.ceil(py)-py);
          var dist3 = (Math.ceil(px)-px)*(py-Math.floor(py));
          var dist4 = (px-Math.floor(px))*(py-Math.floor(py));
          if (dist1 != 0 || dist2!=0 || dist3!=0 || dist4!=0){
            value = altData[Math.floor(py)][Math.floor(px)]*dist1+
            altData[Math.floor(py)][Math.ceil(px)]*dist2 +
            altData[Math.ceil(py)][Math.floor(px)]*dist3 +
            altData[Math.ceil(py)][Math.ceil(px)]*dist4;
          } else {
            value = altData[Math.floor(py)][Math.floor(px)];
          }
        } else {
          value = -999;
        }

          var c = Math.round((scaleWidth-1) * ((value - min)/(max-min)));
          var alpha = 255;
          if(c<0) c=0;
          if(c>=scaleWidth) c=scaleWidth-1;
          data[pos]   = csImageData[c*4];;
          data[pos+1]   = csImageData[c*4+1];
          data[pos+2]   = csImageData[c*4+2];
          data[pos+3]   = alpha;
          pos = pos + 4

      }
    }
    contextRaster.putImageData( id, 0, 0);
    context.save();
    context.beginPath();
    path(countries);
    context.clip();

    context.drawImage(canvasRaster.node(), 0, 0);
    context.globalAlpha = 0.5;
    context.drawImage(canvasShaded.node(), 0, 0);
    context.restore();


    context.beginPath();
    context.strokeStyle = "#777";
    path(countries);
    context.stroke();
  }

});
});
</script>

</body>

color-scale.js

var d3 = d3|| {};
d3.ColorScaleChooser = function(){
  var squareWidth = 80, squareHeight = 30;
  var scaleValues = [];
  var parent = null;
  var dispatcher = d3.dispatch("change");
  var title = null;
  var addText = "Add";
  var yOffset = 0;

  function ColorScaleChooser(g){
    parent = g;


    if(title){
      g.append("text")
      .attr("x", 5)
      .attr("y", 15)
      .style("fill", "#666")
      .style("font-size", (0.6*squareHeight)+"px")
      .style("font-family", "Verdana")
      .text(title);

    yOffset = 0.6*squareHeight;
    }

    var addGroup = g.append("g")
    .attr("transform","translate(10, "+(10+yOffset)+")")
    .on("click", function(){
      var newValue = {value: 0, color: "#ffffff"};
      scaleValues.push(newValue);
      draw();
      form(newValue);
    });

    addGroup.append("rect")
    .attr("width", squareWidth)
    .attr("height", squareHeight)
    .attr("x", 0)
    .attr("y", 0)
    .style("fill","#ddd")
    .style("stroke", "#666");

    addGroup.append("path")
    .attr("d", "m -4.5,-20.5 0,16 -16,0 0,7 16,0 0,16 7,0 0,-16 16,0 0,-7 -16,0 0,-16 -7,0 z")
    .attr("transform","translate("+squareWidth/2+","+squareHeight/2+") scale("+(0.6*squareHeight/40)+")")
    .style("stroke", "#666")
    .style("fill", "#ffffff");

    addGroup.append("text")
    .attr("x", squareWidth + squareWidth/8)
    .attr("y", 30*squareHeight/40)
    .style("fill", "#666")
    .style("font-size", (0.425*squareHeight)+"px")
    .style("font-family", "Verdana")
    .text(addText);

    draw();
  }

  ColorScaleChooser.squareWidth = function(_){
    squareWidth = _;
    return ColorScaleChooser;
  };

  ColorScaleChooser.squareHeight = function(_){
    squareHeight = _;
    return ColorScaleChooser;
  };

  ColorScaleChooser.scaleValues = function(_){
    scaleValues = _;
    return ColorScaleChooser;
  };

  ColorScaleChooser.title = function(_){
    title = _;
    return ColorScaleChooser;
  };

  ColorScaleChooser.addText = function(_){
    addText = _;
    return ColorScaleChooser;
  };

  ColorScaleChooser.on = function(){
    var value = dispatcher.on.apply(dispatcher, arguments);
    return value === dispatcher ? ColorScaleChooser : value;

  };


  function draw(){

    var colorSelection = parent.selectAll(".step")
    .data(scaleValues.sort(function(a, b){
        return a.value-b.value;
    }), function(d) { return(d.value+d.color); });


    var colorSelectionEnter = colorSelection
    .enter()
    .append("g")
    .attr("class","step")
    .attr("transform", function(d, i){return"translate(10, "+((i+1)*squareHeight*1.1 + 10 + yOffset)+")";})
    .on("mouseover", function(){
      d3.select(this)
        .selectAll(".close")
        .style("visibility", "visible");
    })
    .on("mouseout", function(){
      d3.select(this)
        .selectAll(".close")
        .style("visibility", "hidden");
    });

    colorSelectionEnter.append("rect")
    .attr("width", squareWidth)
    .attr("height", squareHeight)
    .attr("x", 0)
    .attr("y", 0)
    .style("fill",function(d){ return d.color;})
    .style("stroke", "#000")
    .on("click", function(d){ form(d);});



    colorSelectionEnter.append("text")
    .attr("x", squareWidth + squareWidth/8)
    .attr("y", 30*squareHeight/40)
    .style("fill", "#333")
    .style("font-size", (0.425*squareHeight)+"px")
    .style("font-family", "Verdana")
    .text(function(d){return d.value;});

    colorSelectionEnter.append("path")
    .attr("class", "close")
    .attr("d", "m 20.5,1.86 a 20,20 0 0 0 -20,20 20,20 0 0 0 20,20 20,20 0 0 0 20,-20 20,20 0 0 0 -20,-20 z m -9.88,9 4.24,0 5.8,7.79 5.8,-7.79 4.23,0 -7.9,10.64 8.32,11.23 -4.23,0 -6.36,-8.59 -6.36,8.59 -4.23,0 8.49,-11.4 -7.77,-10.42 z")
    .style("fill", "#f00")
    .style("stroke", "#000")
    .style("visibility", "hidden")
    .attr("transform", "translate(3,3) scale(0.5)")
    .on("click", function(d){
      scaleValues = scaleValues.filter(function ( obj ) {
        return obj.value !== d.value;
      });
      draw();
    });

    colorSelection.exit()
      .transition()
      .duration(1000)
      .style("opacity", 0)
      .remove();

    colorSelection
      .transition()
      .delay(1000)
      .duration(1000)
      .attr("transform", function(d, i){return"translate(10, "+((i+1)*squareHeight*1.1 + 10 + yOffset)+")";});

    dispatcher.call("change", {},
              scaleValues,
              scaleValues.reduce(function(a, b){return a.value < b.value ? a : b ;}).value,
              scaleValues.reduce(function(a, b){return a.value > b.value ? a : b ;}).value);
  }

  function form(obj){
    d3.selectAll(".colorScaleForm")
      .remove();

    var formDiv = d3.select("body")
      .append("div")
      .attr("class", "colorScaleForm")
      .style("position", "absolute")
      .style("left", (20+d3.event.pageX)+"px")
      .style("top", d3.event.pageY+"px")
      .style("background-color","rgba(128, 128, 128, 0.55)")
      .style("border-radius","5px")
      .style("padding","5px");

    formDiv.append("label")
    .text("Value");

    formDiv.append("input")
    .attr("class", "valueField")
    .attr("type", "number")
    .attr("step", "any")
    .attr("size", 10)
    .attr("value", obj.value);

    formDiv.append("label")
    .text("Color");

    formDiv.append("input")
    .attr("class", "colorField")
    .attr("type", "color")
    .attr("value", obj.color);

    formDiv.append("button")
    .text("Set")
    .on("click", function(){
      scaleValues[scaleValues.indexOf(obj)] = {value: formDiv.select(".valueField").node().value,
                                              color: formDiv.select(".colorField").node().value};
      draw();

      formDiv.transition()
      .duration(1000)
      .style("opacity", 0)
      .remove();
    });


  }

  return ColorScaleChooser;
};


d3.ColorScaleChooser.getA = function(){

};