The isolines connect the points with the same value in the raster. It’s widely used since is easy to understand and can be drawn on the top of isobands or other raster representations.

Calculating these isolines can be a bit difficult, that’s why I adapted the MarchingSquares.js library to use with a geoPath: raster-marching-squares

The important code parts are these. You can find the whole code here.

var intervalsZ = [1400, 1420, 1440, 1460, 1480, 1500, 1520, 1540];
var bandsTemp = rastertools.isolines(zData, geoTransform, intervalsZ);
var colorScale = d3.scaleSequential(d3.interpolateYlOrRd)
    .domain([1400, 1540]);
bandsTemp.features.forEach(function(d, i) {
    context.beginPath();
    context.globalAlpha = 1;
    context.lineWidth = 2;
    context.strokeStyle = colorScale(intervalsZ[i]);
    path(d);
    context.stroke();
});
  • The raster-marching-squares isolines function is called with the raster data, the geoTransform and an array with the intervals
  • For each of the generated lines, a canvas path is drawn. I’ve colored the lines to show how to do it, although the most frequent case is drawing black or dark grey lines over other data

SVG

The SVG version is very similar, in this case. You can find the whole code here.

The main change is:

bandsTemp.features.forEach(function(d, i) {
  svg.insert("path", ".streamline")
      .datum(d)
      .attr("d", path)
      .style("fill", colorScale(intervalsTemp[i]))
      .style("stroke", "None");
});
  • Of course, the difference is that the line is drawn appending paths to the SVG

Adding labels

Isolines without labels are very difficult to understand. Adding labels is quite difficult, specially using Canvas. Canvas hasn’t got any function to know the position of the path in certain length, nor the angle. That’s why I created the svg-path-properties library, that does exactly that.

You can find the whole code here.

As you can see in the image, the isoline behind each label is erased. But under this label there is a background. This effect is not possible to achieve using Canvas, so a hidden canvas is created, where erasing is not a problem. Once the isolines and labels layer is done, is combined with the background:

var hiddenCanvas = d3.select("body").append("canvas")
      .attr("width", width)
      .attr("height", height)
      .attr("id", "hiddenCanvas")
      .style("display","none");
var hiddenContext = hiddenCanvas.node().getContext("2d");

var hiddenPath = d3.geoPath()
    .projection(projection).context(hiddenContext);
var hiddenPath2 = d3.geoPath()
        .projection(projection);
  • The hidden canvas needs a context, and since the geoPath needs a context too, a new one is createLinearGradient
  • Another geoPath, this time without context, is created to be past to the svg-path-properties library
var properties = spp.svgPathProperties(hiddenPath2(d));
var separation = 150;

for(var j = 0; j< Math.floor(properties.getTotalLength()/separation); j++){
  var pos = properties.getPropertiesAtLength(75 + separation*j);

  var degrees = Math.atan(pos.tangentY/pos.tangentX);
  var text = d.properties[0].value;
  hiddenContext.save();

  hiddenContext.translate(pos.x, pos.y);
  hiddenContext.rotate(degrees);

  hiddenContext.font="15px Georgia";

  hiddenContext.clearRect(-2-hiddenContext.measureText(text).width/2 , -8, 4 + hiddenContext.measureText(text).width, 19);
  hiddenContext.fillStyle = "#500";
  hiddenContext.fillText(text, -hiddenContext.measureText(text).width/2, 7.5);
  hiddenContext.restore();
}
  • After drawing the isoline, that’s how the labels are drawing
  • We will draw many labels, one each 150 px with the for statement
    • If not, sine the isoline can be split in several isolines, some large parts could be unlabeled
  • The label is placed and rotated with the translate and rotate methods, as in the arrows and barbs example
  • The clearRect method removes the isoline under the label so it can be read easily. Fortunately, Canvas has methods to know the text size, so the amount of space to delete is easy to get

SVG

In this case, the code changes a little more, and it’s not as easy as it could seem. The first idea I had was to use the stroke-dashoffset and stroke-dasharray properties of the isolines to create the holes to put the labels in. The problem comes with the paths that aren’t continuous (there can be more than one isoline per value).

So, using the same approach as in the Canvas case, the important code parts are these. You can find the whole code here.

var maskZones = svg.append("defs")
  .append("mask")
  .attr("id", "labelsMask")
  .attr("x", 0)
  .attr("y", 0)
  .attr("width",width)
  .attr("height", height);

maskZones.append("rect")
  .attr("x", 0)
  .attr("y", 0)
  .attr("width",width)
  .attr("height", height)
  .attr("fill", "white");
  • Creates an SVG mask element with a big white rectangle occupying all the image
    • The white zones are the ones to show at 100% of opacity
  • The id is important, since it will be used with the lines to hide the label parts
svg.insert("path", ".streamline")
    .datum(d)
    .attr("d", path)
    .attr("mask","url(#labelsMask)")
    .style("stroke", colorScale(intervalsZ[i]))
    .style("stroke-width", "2px")
    .style("fill", "None");
  • The isolines are drawn as usual with SVG, but adding the mask attribute
    • Note how the id of the mask is used, as in a CSS
for(var j = 0; j< Math.floor(properties.getTotalLength()/separation); j++){
  var pos = properties.getPropertiesAtLength(75 + separation*j);
  var degrees = (180/Math.PI)*Math.atan(pos.tangentY/pos.tangentX);


  svg.append("text")
    .attr("x", -bbox.width/2)
    .attr("y", 7.5)
    .attr("font-family", "Georgia")
    .attr("font-size","15px")
    .attr("transform", "translate("+pos.x+", "+pos.y+")rotate("+degrees+")")
    .text(text);

  maskZones.append("rect")
  .attr("x", -2-bbox.width/2)
  .attr("y", -8)
  .attr("width", bbox.width+4)
  .attr("height", bbox.height)
  .attr("fill", "black")
  .attr("transform", "translate("+pos.x+", "+pos.y+")rotate("+degrees+")");

  }
  • The iteration for each label is done as in the Canvas version
  • The text is added in the usual way
    • The position only moves the text to make it centered at 0,0
    • The transform is used to move and rotate the text at the correct positions (order is important)
      • The translate part must be done this way or the rotate doesn’t take the center of the text as the origin, putting the label at random positions
  • A black rectangle (opacity = 0) is created with the bounding box of each label and appended to the mask
    • This is the part that actually removes the line parts to show the labels properly