D3 glasses and the Complex Plane

By: on March 12, 2012

One of the best ways to develop intuition in mathematics is to look at pictures. We often use graphs – visual representations of functions – showing the relationship between the domain of a function and the range of a function. With real-valued functions of one variable this is easy: the graph is nice and flat. What about complex-valued functions? What kind of pictures would help?

Recall that a complex number may be represented by a pair of real numbers. As such, representing a set of complex numbers – a region – is straightforward: an Argand diagram does the job just great. (Think of a Cartesian plane, and label the x-axis “real” and the y-axis “imaginary”.) Usually we just call this the z-plane, and the plane to which we map – the range – we call the w-plane.

What about a function though? The obvious visualisation is a bit hard: since visually representing a complex number requires two numbers, we’d need to use four dimensions to visual a complex function of one variable. Sadly, I left my 4-dimensional graph paper in my other bag.

Several other possibilities spring to mind: split the mapped values into their real and imaginary components and draw each separately. Since we now only have three dimensions left (the domain’s two, and one for the range), things are a bit easier. Typically we map the third dimension to a colour, giving something like a contour map. MathWorld is filled with examples.

It’s also easy to show how the shape of some region changes. Define a region in the z-plane – a disk, say – and draw in the w-plane the boundary of the region. And we can take it a step further: we can draw the mutations of a grid of lines. Right, enough maths. Let’s draw!

The basic idea in d3 is simple: you have some data in an array, you have a selection of nodes in an array, and you want to map the one onto the other. Today, we’re going to use SVG path elements – polylines – to keep things nice and simple. It has a nice jQuery-like API permitting concise descriptions of things we want to add to the DOM. First, our play area:

  d3.select("#z-plane").append("svg")
    .attr("height", 300)
    .attr("width", 300)
    .append("g")
    .attr("fill", "none")
    .attr("stroke-width", 2)
    .attr("stroke", "black");

Next, let’s make some data. We want some lines forming a grid, and those lines will be polylines with a large number of points on the lines. (Pretty pictures need lots of points for nice smooth curves.)

  function hlines(gridResolution, pointResolution) {
  return gridResolution.map(function(i) {
      return pointResolution.map(function(j) {
        return complex(j, i);
      });
    });
  }
 
  function vlines(gridResolution, pointResolution) {
    return gridResolution.map(function(i) {
      return pointResolution.map(function(j) {
        return complex(i, j);
      });
    });
  }
 
  function makeGrid(gridResolution, pointResolution) {
    return hlines(gridResolution, pointResolution)
             .concat(vlines(gridResolution, pointResolution));
  }
 
  var min_x = -2.5, max_x = 2.5;
  var interline_distance = 0.2;
  var interpoint_distance = 0.05;
  var grid = d3.range(min_x, max_x + (interline_distance / 2), interline_distance);
  var pointsOnLine = d3.range(min_x, max_x + (interpoint_distance / 2), interpoint_distance);

  var zlines = makeGrid(grid, pointsOnLine);
  var wlines = makeGrid(grid, pointsOnLine);

We map the data onto the SVG elements. Now we don’t have any SVG elements at the moment. That’s OK. Thanks to enter(), we can create any missing path elements, one per item in our data in the arr array, after we’ve bound our data to the selection of nodes.

  function makeLines(planeDiv, arr) {
    planeDiv.selectAll(".line")
      .data(arr)               // Bind to all the line class elements
      .enter().append("path")  // and add path.line element as necessary
        .attr("class", "line")
        .attr("d", lineMaker);
  }

But how do we actually display the data? d3’s scales map our domains – two continuous ranges of values from -2 to 2 – to our ranges – 300 pixel long sides:

  var x = d3.scale.linear()
    .domain([min_x, max_x])
    .range([0, h]);
  var y = d3.scale.linear()
    .domain([min_x, max_x])
    .range([h, 0]); // Invert the scale so the origin is in the BOTTOM left corner
  var lineMaker = d3.svg.line()
      .x(function(d) { return x(d[0]); })  // How we get the x-coord
      .y(function(d) { return y(d[1]); }); // How we get the y-coord

lineMaker

is a convenience factory for making the polylines. The x and y functions define how to calculate the mapped data: given a complex number d we map the real portion of that number onto the horizontal position of a polyline point, and similarly for the imaginary portion of that number. Note the y scale’s range: inverted to show the mathematically proper position of the origin instead of SVG’s standard (top left).

When we press the “Go” button, we fire off a click event:

  d3.select("#go-button").on("click", function() {
    // Recreate our Moebius transformation
    f = memoiseF();
 
    // And warn if our (yes, global) variables indicate that we do not, in
    // fact, have a Moebius transformation.
    if (czero(csub(cmult(a, d), cmult(b, c)))) {
      alert("Not a Moebius transform!nMerely a constant function f(z) = " + JSON.stringify(f(complex(1, 0))));
    }
 
    // Mutate the w-plane data in place so d3 can automatically
    // pick up the changes.
    var lastw = [0,0];
    for (var lineI = 0; lineI < wlines.length; lineI++) {
      for (var pointI = 0; pointI < wlines[lineI].length; pointI++) {
        w = f(zlines[lineI][pointI]);
        if (w == Inf) {
          // If f(z) = Inf then we pretend that Inf is really a point
          // outside of visual range that's on the ray drawn from
          // the origin through lastw. Think of the resulting point
          // as being the closest point to lastw on a circle centred
          // on the origin and of infinite radius.
 
          // Multiplying by n + 0i (n a real) means scaling without
          // rotating.
          w = cmult(lastw, complex(100, 0));
        }
        wlines[lineI][pointI] = w;
        lastw = w;
      }
    }
    drawLines(wplane, wlines);
  });

And lastly, refreshing our view is trivial:

  function drawLines(planeDiv, arr) {
    planeDiv.selectAll(".line")
      .transition()   // Change the view of the data ...
      .ease("linear") // ... and make it nice and smooth.
      .attr("d", lineMaker);
  }

In short, d3 is a neat, powerful tool for visualising data. It’s highly extensible, well-supported, and can do pretty much anything you might want to out of the box, from bullet charts to Voronoi diagrams.

FacebookTwitterGoogle+

Comments are closed