Graphing Calculator, Part 1

Sun 07 July 2013

Filed under code

Tags d3.js svg graphing-calculator

Welcome! I am teaching myself d3.js and have decided to create a rudimentary graphing calculator as a proof of concept. I'm detailing the learning process as I go so there will be pitfalls and backtracks — in other words, this is more of a whiteboard demo than a tutorial.

I am not starting from scratch — I went through Scott Murray's excellent d3 tutorial to familiarize myself with d3, and will build on that. This demo assumes familiarity with the concepts covered in his tutorial, so if you haven't gone through that yet, please do. I'll wait here.

Setting Up the Basics

Let's get started by setting up an SVG element.

var svg = d3.select('body').append('svg:svg'),
        w = 500, h = 250;
svg.attr('width', w).attr('height', h);

This is just a blank canvas. Let's put some axes and scales on [1]:

var padding = 20,
xMax = 100,
yMax = 10,
xScale = d3.scale.linear()
        .domain([0, xMax])
        .range([padding, w - padding]),
yScale = d3.scale.linear()
        .domain([0, yMax])
        .range([h - padding, padding]);
xAxis = d3.svg.axis()
        .scale(xScale)
        .orient('bottom'),
yAxis = d3.svg.axis()
        .scale(yScale)
        .orient('left');

svg.append('svg:g')
        .attr('class', "axis")
        .attr('transform', "translate(0," + (h - 20) + ")")
        .call(xAxis);
svg.append('svg:g')
        .attr('class', "axis")
        .attr('transform', "translate(20,0)")
        .call(yAxis);

and style them:

<style type="text/css">
    svg .axis path, svg .axis line {
        fill: none;
        stroke: black;
        shape-rendering: crispEdges;
    }
    svg .axis text {
        font-family: sans-serif;
        font-size: 11px;
        text-anchor: middle;
    }
</style>
0102030405060708090100012345678910

(Note that the minimums and maximums for the axes are hardcoded. This will change shortly when we start to enter data.)

Adding a Function

Scott's final example displays random points which change each time you load the page. I want to adapt that to display the points along the curve of a mathematical function. I'm going to use Math.sqrt since it's easy to display and fits nicely with the domains I hardcoded for the axes.

var plotfunc = Math.sqrt,
        plotdata = [];
var lowX = 0, highX = 100, dX = 1;
for (var i=lowX; i<highX; i+= dX) {
    plotdata.push([i, plotfunc(i)]);
}

// check that points were entered correctly
for (var i=0; i<plotdata.length; i+=10) {
    console.log("(" + plotdata[i][0] + ", " + plotdata[i][1] + ")");
}

// console output:
(0, 0)
(10, 3.1622776601683795)
(20, 4.47213595499958)
(30, 5.477225575051661)
(40, 6.324555320336759)
(50, 7.0710678118654755)
(60, 7.745966692414834)
(70, 8.366600265340756)
(80, 8.94427190999916)
(90, 9.486832980505138)

Looks like our data are valid. Now let's display them. Since I just finished Scott's scatterplot tutorial [2], I have the code left to display circles at x/y coordinates, so I will just adapt that for now.

svg.selectAll('circle')
        .data(plotdata)
    .enter()
        .append('svg:circle')
        .attr('cx', function(d, i) { return xScale(d[0]); })
        .attr('cy', function(d, i) { return yScale(d[1]); })
        .attr('r', 2);
0102030405060708090100012345678910

Very nice!

Dys-function

Let's try it with a different function to see how extensible it is:

var plotfunc = Math.exp,
...
0102030405060708090100012345678910

Oops! What's happening here? Let's try a function which doesn't grow quite as quickly.

var plotfunc = function(x) { return x; },
...
0102030405060708090100012345678910

Aha, I see what's happening. Looks like my hardcoded domains of 0-100 for x and 0-10 for y aren't sufficient for a function whose y-value gets greater than 10. Fortunately, changing the maximums on the domains is easy.

...
xMax = d3.max(plotdata, function(d) { return d[0]; }),
yMax = d3.max(plotdata, function(d) { return d[1]; }),
...
01020304050607080900102030405060708090

There it is. I am going to change the function back to sqrt for the remainder of this exercise — the dynamic maximums for my x and y domains should still work fine.

Getting In Line

The last thing I want to do today is change the way the function is plotted from a series of points to a series of lines. This seems pretty straightforward but as you will see there are some snags.

Whereas the SVG circle element requires three plot attributes (cx, cy, and r), the line element takes four: x1, y1, x2, y2. This makes sense: it's a simple line segment which is defined by two (x, y) endpoints, just like in freshman geometry.

Let's use item n in plotdata as point 1 and item n + 1 as point 2. This will obviously cause problems when item n is the final item in plotdata, so let's simply exclude that item from our data() call:

// Replace svg.selectAll('circle') and its chain with:
svg.selectAll('line')
        .data(plotdata.slice(0, plotdata.length - 1))
    .enter()
        .append('svg:line')
        .attr("x1", function(d, i) { return xScale(d[0]); })
        .attr("y1", function(d, i) { return yScale(d[1]); })
        .attr("x2", function(d, i) { return xScale(plotdata[i+1][0]); })
        .attr("y2", function(d, i) { return yScale(plotdata[i+1][1]); });
        .style('stroke', "rgb(6, 120, 155)")
        .style('fill', "rgb(6, 120, 155)");

Note that I also added stroke and fill styles to liven up the plot a bit. I could also have done this in the CSS block, which is arguably more correct because it generates less SVG code.

01020304050607080900123456789

Huh? What happened here? I do indeed have a line now, but only the points with x > 20 are being plotted. But the only thing I changed was the plot elements, from circles to lines. What is going on?

It turns out that the problem is with svg.selectAll('line'). d3's selectAll() uses CSS-style selectors to select everything inside the parent element which matches the pattern, similar to jQuery. However, there are already line elements inside the svg element. They are the ticks for the axes — 10 each. (The axes themselves are path elements so are not selected.)

Thus, when d3 performs svg.selectAll('line') and gets a preƫxisting set of 20 line segments, its enter() method excludes those and only begins appending elements which it considers to be new. [3]

The solution is simple, like most riddles when you see the answer. Since we don't expect the initial selector to return anything more than the empty set anyway, change it to something guaranteed not to exist inside the <svg> element.

svg.selectAll('#line_not_in_axis_tick')
        .data(plotdata.slice(0, plotdata.length - 1))
...
01020304050607080900123456789

And there it is! Our prettily-plotted square root curve.

Wrapping Up

The full code for this demo can be found here. There is plenty yet to do — for example, SVG provides a path element which is better suited to drawing continuous line segments than line is. I also need to work with making the domain and range even more dynamic. Look for part 2 coming soon!

Footnotes

[1]Refreshers from Scott's tutorial on axes and scales.
[2]Scatterplot tutorial is at http://alignedleft.com/tutorials/d3/making-a-scatterplot/.
[3]selectAll() and enter() really confused me when I started working with d3. I didn't get deconfused until I read Mike Bostock's Thinking With Joins.

Comments


This Is The Title Of This Page © Eric Plumb

Powered by Pelican and Twitter Bootstrap. Icons by Font Awesome and Font Awesome More