block by mgold 1cb3b740f935d155a61e

Spherical Coordinates

Full Screen

Grab the brown and black knobs. You can also dial some of the numbers on the right.

Spherical coordinates are defined by ρ (rho, the distance from the origin), θ (theta, rotation parallel to the xy-plane), and φ (phi, inclination from the north pole to the south pole). This interactive drawing shows how they relate to the Cartesian xyz coordinates. The key is the horizontal slice of radius r.

Which makes sense: when φ=0, we’re looking at the north pole, z=ρ and r=0. Then we’re left with the familiar equations:

In many textbooks, the definition of r is inserted into the definition of x and y, making them difficult to memorize (and the image above harder to see). In particular, defining r allows one to mentally construct spherical coordinates on top of polar coordinates, rather than as a separate entity. While we’re improving notation, remember that τ = 2π.

index.html

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
  <script src="https://cdn.rawgit.com/gka/d3-jetpack/dd27abb646a9aa1e5100d79b336113eb6adb8d6b/d3-jetpack.js"></script>
  <script src="d3.place.js"></script>
  <style>
    /* Yes, this should be split out as a SASS file. But it's not, because I want it to be a block.. */
    body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
    svg { width: 100%; height: 100%; }

    text { font-family: avenir, sans-serif; }
    .gray { fill: #AAAAAA }
    line {
      stroke-linecap: round;
      stroke-width: 4px;
      stroke: #444;
    }
    circle.outer {
        fill: none;
        stroke: #444;
        stroke-width: 2px;
    }
    line.sliceOutline {
        stroke: #D0D0D0;
    }
    circle.sliceOutline {
        fill: #EEEEEE;
    }
    line.z {
        stroke: #0074D9;
        stroke-width: 4px;
    }
    text.z, tspan.z { fill: #0074D9; }
    line.r-interior {
        stroke: #CE9A76;
        stroke-width: 4px;
    }
    circle.r {
        fill: #E4C6B0; /* #CE9A76; */
        stroke: #A05725;
        stroke-width: 4px;
    }
    circle.r-edge {
        fill: #A05725;
    }
    tspan.r { fill: #A05725; }
    line.r-edge { stroke: #A05725; }
    line.rho {
        fill: none;
        stroke: #B10DC9;
        stroke-width: 4px;
        stroke-linecap: round;
        shape-rendering: geometricPrecision;
    }
    tspan.rho { fill: #B10DC9; }
    line.x {
      stroke: #FF4136;
    }
    text.x, tspan.x { fill: #FF4136;}
    line.y {
      stroke: #2ECC40;
    }
    text.y, tspan.y { fill: #2ECC40;}
    path.phi {
      fill: #FFDC00;
    }
    tspan.phi { fill: #FFDC00;}
    path.theta {
      fill: #FF851B;
    }
    tspan.theta { fill: #FF851B;}
    text.descr {
        fill: #AAAAAA;
        text-anchor: end;
    }
    text.label {
      font-size: 11px;
      font-family: courier, monospace;
    }


    .grab {
      cursor: grab;
      cursor: -webkit-grab;
    }
    .grabbing {
      cursor: grabbing;
      cursor: -webkit-grabbing;
    }
  </style>
</head>

<body>
  <script>
    // Tau polyfills
    Math.TAU = 2*Math.PI;
    Math.HALFTAU = Math.PI;
    Math.QUARTERTAU = Math.PI/2;

    // Math polyfills
    Math.approx = function(d){ return Math.round(d*100)/100 }
    Math.dist = function(a,b){ return Math.sqrt(a*a + b*b) }
    Math.clamp = function(low, val, high){ return Math.max(low, Math.min(val, high)) }
    Math.near = function(a,b, prec){return Math.abs(a-b) < (prec || 0.1)}
    function isZero(d){ return Math.abs(d) <= 0.01 }

    // fractions dictionary, numbers become strings and it just works
    var fractions = d3.map();
    fractions.set(1, "");    fractions.set(1/2, "½");
    fractions.set(1/3, "⅓"); fractions.set(2/3, "⅔")
    fractions.set(1/4, "¼"); fractions.set(3/4, "¾")
    fractions.set(1/6, "⅙"); fractions.set(5/6, "⅚")
    fractions.set(1/8, "⅛"); fractions.set(3/8, "⅜")
    fractions.set(5/8, "⅝"); fractions.set(7/8, "⅞")
    var snapPrecision = 0.001;

    // margin convention
    var margin = {top: 20, right: 10, bottom: 20, left: 20};
    var width = 960 - margin.left - margin.right;
    var height = 500 - margin.top - margin.bottom;
    var svg = d3.select("body").append("svg")
      .attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.top + margin.bottom)
    .append("g")
      .translate([margin.left, margin.top])
    var R = height/2

    // All the mutable state
    var rho = 0.8, // fraction of R
        theta = 3*Math.TAU/8,
        phi = Math.TAU/8,
        r, x, y, z,
        draggingR = false;

    // snaps angles to nice values
    function snapAngles(){
        var snapPoints = fractions.keys().concat(0);
        snapPoints.forEach(function(snapPoint){
            snapPoint *= Math.TAU;
            if (!draggingR && Math.near(phi, snapPoint, 40*snapPrecision)) phi = snapPoint;
            if (Math.near(theta, snapPoint, 40*snapPrecision)) theta = snapPoint;
        })
    }

    // updates the derives state values
    function updateRXYZ(){
        r = R * rho * Math.sin(phi);
        z = R * rho * Math.cos(phi);
        x = r*Math.cos(theta);
        y = r*Math.sin(theta);
    }

    // the three main g elements
    var xzSlice = svg.append("g.xzSlice")
      .translate([0, R]);

    var xySlice = svg.append("g.xySlice")
      .translate([500, R]);

    var legend = svg.append("g.legend")
      .translate([805, 153]);

    // arc generators for phi and theta
    var phiGen = d3.svg.arc()
      .innerRadius(50)
      .outerRadius(54)
      .startAngle(0)
      .endAngle(function(d){return d})

    var thetaGen = d3.svg.arc()
      .innerRadius(50)
      .outerRadius(54)
      .startAngle(Math.QUARTERTAU)
      .endAngle(function(d){return Math.QUARTERTAU-d})

    // this function gets called any time a drag occurs
    // and at the start of execution
    function render(){
      if (rho <= 0.02) rho = 0;
      snapAngles();
      updateRXYZ();
      var sliceRadius = Math.sqrt(R*R - z*z);
      var axisOffset = sliceRadius + 5;

      // selection.place is a polyfill I wrote that appends an element of the
      // type and class given, unless it already exists, and always returns the
      // selection.
      xzSlice.place("line.sliceOutline")
        .attr({x1: -sliceRadius, y1: -z, x2: sliceRadius, y2: -z})
      xzSlice.place("circle.outer")
        .attr("r", R)
      xzSlice.place("path.phi")
        .attr("d", phiGen(phi))
        .style("display", isPhiDefined() ? null : "none")
      xzSlice.place("line.z")
        .attr({x1: 0, y1: 0, x2: 0, y2: -z})
      xzSlice.place("line.rho")
        .attr({x1: 0, y1: 0, x2: r, y2: -z})
      xzSlice.place("line.r-interior")
        .attr({x1: -r, y1: -z, x2: r, y2: -z})
      xzSlice.place("circle.r-edge")
        .attr("r", "6px")
        .translate([r, -z])
      xzSlice.place("text.z.label")
        .translate([0, -R-5])
        .text("z")
      var label = xzSlice.place("text.xy.label")
        .translate([R+5, -5])
      label.place("tspan.x")
        .text("x")
      label.place("tspan.y")
        .text("y")
      xzSlice.place("text.plane.label")
        .text("plane")
        .translate([R+4, 8])

      xySlice.place("circle.outer.sliceOutline")
        .attr("r", sliceRadius)
      xySlice.place("circle.r")
        .attr("r", Math.max(1, r))
      xySlice.place("path.theta")
        .attr("d", thetaGen(theta))
        .style("display", isThetaDefined() ? null : "none")
      xySlice.place("line.y")
        .attr({x1: x, y1: 0, x2: x, y2: -y})
      xySlice.place("line.r-edge")
        .attr({x1: 0, y1: 0, x2: x, y2: -y})
      xySlice.place("line.x")
        .attr({x1: 0, y1: 0, x2: x, y2: 0})
      xySlice.place("circle.point")
        .attr("r", "6px")
        .translate([x, -y]);
      xySlice.place("text.x.label")
        .translate([axisOffset, 0])
        .style("display", isThetaDefined() ? null : "none")
        .text("x");
      xySlice.place("text.y.label")
        .translate([0, -axisOffset])
        .style("display", isThetaDefined() ? null : "none")
        .text("y");

    ([["ρ", rho < 0.02 ? "0" : Math.approx(rho), "rho", 0, "magnitude"],
     ["θ", formatTheta(), "theta", 1, "rotation"],
     ["φ", formatPhi(), "phi", 2, "incline"],
     ["r", Math.approx(r/R), "r", 4],
     ["x", Math.approx(x/R), "x", 6],
     ["y", Math.approx(y/R), "y", 7],
     ["z", Math.approx(z/R), "z", 8]]).forEach(function(d){renderEqtn.apply(null, d)})

    }

    render(); // start everything off

    // the next part of the code handles formatting angles as indetermiante or fractions
    var indet = "indeterminate";

    function isPhiDefined(){ return !isZero(rho) }
    function formatPhi(){ return isPhiDefined() ? formatAngle(phi) : indet}

    function isThetaDefined(){ return isPhiDefined() && !(isZero(phi) || phi > 0.99*Math.HALFTAU)}
    function formatTheta(){ return isThetaDefined() ? formatAngle(theta) : indet}

    function formatAngle(ang){
        if (isZero(ang)) return "0"
        var returnMe = Math.approx(ang/Math.TAU);
        fractions.forEach(function(key, val){
            if (Math.near(ang/Math.TAU, key, snapPrecision)) {
                returnMe = val;
            }
        })
        return returnMe+"τ"
    }
    // called from the render function, remember?
    function renderEqtn(symbol, value, klass, i, descr){
        var spacing = 20;
        if (descr){
            legend.place("text.descr."+klass)
            .translate([-5, i*spacing])
            .text(descr)
        }
        var text = legend.place("text."+klass+"-eqtn")
          .translate([0, i*spacing])
        text.place("tspan.sym."+klass)
            .text(symbol)
        text.place("tspan.val")
            .text((value === indet ? " is " : " = ") + value)
            .classed("gray", value === indet)
    }

    // Add grab hooks to the two circles
    function addGrab(onDrag){
        var drag = d3.behavior.drag();
        drag.on("dragstart", function(){ d3.select(this).classed("grab", false).classed("grabbing", true)})
        drag.on("dragend",   function(){ d3.select(this).classed("grab", true).classed("grabbing", false); draggingR = false;})
        drag.on("drag", function(){
          onDrag();
          render();
        })
        return drag;
    }

    xzSlice.select("circle.r-edge").call(addGrab(function(){
        var ex = Math.max(0, d3.event.x), ey = d3.event.y;
        rho = Math.min(1, Math.dist(ex, ey)/R)
        phi = Math.atan2(ex, -ey)
    }));
    xySlice.select("circle.point").call(addGrab(function(){
        if (!isPhiDefined()) return;
        draggingR = true;
        var ex = d3.event.x, ey = d3.event.y;
        theta = Math.atan2(-ey, ex);
        if (theta < 0) theta += Math.TAU;
        r = Math.clamp(0.01, Math.dist(ex, ey), r/rho);
        rho = Math.dist(z, r)/R;
        phi = Math.atan2(r, z);
    }));

    // Add drag hooks to the three spherical coordinates (and r) in the legend
    function addDrag(sel, onDrag){
        sel.style("cursor", "ew-resize")
        var drag = d3.behavior.drag();
        drag.on("dragend", function(){ draggingR = false; })
        drag.on("drag", function(){
          onDrag();
          render();
        })
        sel.call(drag);
        return sel;
    }

    legend.select(".rho-eqtn .val").call(addDrag, function(){
        rho += d3.event.dx/50;
        rho = Math.clamp(0, rho, 1)
    })

    legend.select(".theta-eqtn .val").call(addDrag, function(){
        if (isThetaDefined()){
            theta += d3.event.dx/30;
            theta = Math.clamp(0, theta, Math.TAU)
        }
    })

    legend.select(".phi-eqtn .val").call(addDrag, function(){
        if (isPhiDefined()){
            phi += d3.event.dx/30;
            phi = Math.clamp(0, phi, Math.HALFTAU)
        }
    })

    legend.select(".r-eqtn .val").call(addDrag, function(){
        draggingR = true;
        r += d3.event.dx;
        r = Math.clamp(0.01, r, r/rho);
        rho = Math.dist(z, r)/R;
        phi = Math.atan2(r, z);
    })

  </script>
</body>

d3.place.js

/* d3.selection.place()  -  an unofficial add-on
Place one item of the given tag and class (as "tag.class") into the DOM, unless it already exists.
Returns the selection either found in or just added to the DOM.

Caveats: Used d3.jetpack append with class. Might do multiple appends for selections of more than one
element. Might be slow.
*/

d3.selection.prototype.place = function(selector) {
  sel = this.select(selector);
  if (sel.empty()){
      sel = this.append(selector)
  }
  return sel;
};