Skip to content

Instantly share code, notes, and snippets.

@sathomas
Last active August 29, 2015 14:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sathomas/88020ef489c994036d4b to your computer and use it in GitHub Desktop.
Save sathomas/88020ef489c994036d4b to your computer and use it in GitHub Desktop.
Visualizing Differential Equations, II

Continuing an experiment in visualizing differential equations based on the traditional phase line. (See also: Part I.)

The chart shows a set of solutions to a specific differential equation in a single variable. Initially, the equation is ẋ = 1 - x². The horizontal axis represents increasing time, and the vertical axis represents values of the variable x. Each line on the chart shows the evolution of x for a specific initial value x(0). Equilibrium points, where x(t) is constant for all time, are colored red. The controls permit adjustment of the range of initial values shown, as well as the number of lines and the time increment for the horizontal axis.

Feel free to experiment with different equations and parameters. The function for should be a valid JavaScript expression. There’s no error checking, though, so be careful.

This series of visualizations continues with part III.

<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>Differential Equations</title>
<link href='http://fonts.googleapis.com/css?family=Varela' rel='stylesheet'
type='text/css'>
<style>
body, input, button { font-family: Varela,sans-serif; color: #444; }
#legend { padding: 0 0 0 30px; width: 606px; }
.fieldgroup { width: 100%; padding-top: 20px; }
fieldset { display: inline; border: none; padding: 0; }
input, button { font-size: 14px; }
input.small { width: 2em; }
input[type=range] { width: 200px; padding: 0;}
.brush .extent { stroke: #fff; fill-opacity: .125; shape-rendering: crispEdges; }
button { cursor: pointer; }
</style>
</head>
<body>
<div id="legend">
<div class="fieldgroup">
<fieldset>
<label for="function-input" style="padding-left: 20px;">x' = </label>
<input id="function-input" type="text" value="1 - Math.pow(x,2)"/>
</fieldset>
<fieldset>
<button id="update-button">Update</button>
</fieldset>
<fieldset style="padding-left: 40px;">
<label for="xmin-input">x: </label>
<input id="xmin-input" type="text" class="small smooth" value="-2"/>
<label for="xmax-input"> to </label>
<input id="xmax-input" type="text" class="small smooth" value="2"/>
</fieldset>
<fieldset>
<button id="zoomin-button">+</button>
<button id="zoomout-button">−</button>
</fieldset>
</div>
<div class="fieldgroup">
<fieldset style="padding-left: 20px;">
<label for="numx-input">#x: </label>
<input id="numx-input" class="instant" type="range" min="10" max="100"
step="1" value="40"/>
</fieldset>
<fieldset style="padding-left: 40px;">
<label for="deltat-input">∆t: </label>
<input id="deltat-input" class="instant" type="range" min="1" max="100"
step="1" value="30"/>
</fieldset>
</div>
</div>
<script src='http://d3js.org/d3.v3.min.js'></script>
<script>
// Define the dimensions of the visualization. The
// width and height dimensons are conventional for
// visualizations displayed on http://jsDataV.is.
var margin = {top: 20, right: 10, bottom: 10, left: 60},
width = 636 - margin.left - margin.right,
height = 350 - margin.top - margin.bottom;
// Create the SVG stage for the visualization and
// define its dimensions.
var svg = d3.select("body").insert("svg","#legend")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
// Add a clipping path that limits the graph to its
// specified dimensions. We use the clipping path
// to prevent the graph data from extending into
// the region reserved for the axes.
svg.append("defs")
.append("clipPath")
.attr("id", "clippath")
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height);
// Within the SVG container, add a group element (<g>)
// that can be transformed via a translation to account
// for the margins. This element is restricted to the
// clipping path.
var g = svg.append("g")
.attr("clip-path", "url(#clippath)")
.attr("transform", "translate(" + margin.left +
"," + margin.top + ")");
// Define the scales that map a data value to an
// x-position on the vertical axis and to a time
// on the horizontal axis.
var x = d3.scale.linear(),
t = d3.scale.linear();
// Define a convenience function to create a line on
// the chart. The horizontal axis (which D3 refers
// to as `x`) are the time values, and the vertical
// axis (which D3 refers to as `y`) are the x-values.
// The result is that `line` is function that, when
// passed a selection with an associated array of
// data points, returns an SVG path whose coordinates
// match the t- and x-scales of the chart.
var line = d3.svg.line()
.x(function(d) { return t(d.t); })
.y(function(d) { return x(d.x); });
var render = function(smooth) {
// The specifics of the differential equation to
// visualize. We require a function `xDot(x)` that
// returns the derivative at value `x` and a
// range of x-values to graph.
eval("var xDot = function(x) { return "
+ d3.select("#function-input").node().value + "}");
var xMin = +d3.select("#xmin-input").node().value,
xMax = +d3.select("#xmax-input").node().value,
nCurves = +d3.select("#numx-input").node().value,
dt = Math.pow(10, -1 * (+d3.select("#deltat-input").node().value) / 10);
// Construct the initial conditions array.
var step = (xMax - xMin) / nCurves,
data = d3.range(nCurves)
.map(function(i) {
var x = xMin + i * step;
return [{t: 0, x: x}];
});
// Because of rounding and floating point errors,
// the equilibrium points might not have a
// derivative exactly equal to zero. To fix that,
// scan through the data set and identify
// transitions from positive to negative
// (and vice versa). On those transitions, mark
// the data point whose derivative is closest to
// zero as an explicit equilibrium point.
for (var i=1; i<data.length; i++) {
var d0 = data[i-1][0],
d1 = data[i][0],
dd0 = xDot(d0.x),
dd1 = xDot(d1.x);
if (dd0 / Math.abs(dd0) !== dd1 / Math.abs(dd1)) {
if (Math.abs(dd0) <= Math.abs(dd1)) {
d0.eqpt = true;
} else {
d1.eqpt = true;
}
}
}
// Extend the curves through time.
data = data.map(function(x0) {
var eqpt = x0[0].eqpt,
pts = [x0[0]];
for (var i=1; i<width; i++) {
var prev = pts[i-1],
newX = eqpt ? prev.x : prev.x + xDot(prev.x) * dt;
pts.push({
x: newX,
t: prev.t + dt
});
}
return pts;
})
// If any solution leaves the graph area permanently,
// truncate it when it exits. Note that a solution could
// leave the graph area and later return. In those
// cases we retain the data that's off the graph
// area.
data = data.map(function(soln) {
var toSlice = 0;
for (var i=soln.length-1; i > 0; i--) {
if ( !isNaN(soln[i].x) &&
(soln[i].x >= xMin) && (soln[i].x <= xMax) )
break;
toSlice--;
}
soln[0].oob = toSlice !== 0;
return toSlice < 0 ? soln.slice(0,toSlice) : soln;
});
// Set the scales
x.domain([xMin, xMax])
.range([height,0]);
t.domain([0, width*dt])
.range([0, width]);
// Define a function that will create the x-axis
// when passed a data selection.
var xAxis = d3.svg.axis()
.scale(x)
.orient("left");
// Create a selection for the data set.
var lines = g.selectAll(".line")
.data(data);
// Transition lines already in the DOM
// to their new position and color.
if (smooth) {
lines.transition().duration(750)
.attr("stroke", function(d) {
return d[0].eqpt ? "#ca0000" :
d[0].oob ? "#7EBD00" : "#007979";
})
.attr("d", line);
} else {
lines
.attr("stroke", function(d) {
return d[0].eqpt ? "#ca0000" :
d[0].oob ? "#7EBD00" : "#007979";
})
.attr("d", line);
}
// Add new lines for data not yet in the DOM.
lines.enter().append("path")
.classed("line", true)
.attr("fill", "none")
.attr("stroke-width", "1px")
.attr("stroke", function(d) {
return d[0].eqpt ? "#ca0000" :
d[0].oob ? "#7EBD00" : "#007979";
})
.attr("d", line);
// Any excess lines (due to rounding)
// can be deleted.
lines.exit().remove();
// Draw the x-axis, either from scratch
// or by transitioning the existing one.
if (d3.selectAll(".x.axis").size()) {
// Since an axis already exists,
// transition it to its new
// values.
svg.transition().duration(750)
.select(".x.axis").call(xAxis);
} else {
// No axis yet exists, so create on.
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate("+margin.left+","+margin.top+")")
.call(xAxis);
}
// Style the axes. As with other styles, these
// could be more easily defined in CSS. For this
// particular code, though, we're avoiding CSS
// to make it easy to extract the resulting SVG
// and paste it into a presentation.
svg.selectAll(".axis line, .axis path")
.attr("fill", "none")
.attr("stroke", "#bbbbbb")
.attr("stroke-width", "2px")
.attr("shape-rendering", "crispEdges");
svg.selectAll(".axis text")
.attr("fill", "#444")
.attr("font-size", "14");
svg.selectAll(".axis .tick line")
.attr("stroke", "#d0d0d0")
.attr("stroke-width", "1");
}
// Go ahead and draw the chart.
render(false);
// Define event handlers for the control buttons.
// Update just re-renders.
d3.select("#update-button").on("click", function() {
render(true);
});
// Zoom out increases the x domain by 50% on both ends.
d3.select("#zoomout-button").on("click", function() {
var x1 = x.domain()[1],
x0 = x.domain()[0],
extent = x1 - x0,
scale = d3.scale.linear().domain([x0-extent/2,x1+extent/2]).nice();
d3.select("#xmin-input").node().value = scale.domain()[0];
d3.select("#xmax-input").node().value = scale.domain()[1];
render(true);
});
// Zoom in decreases the x domain by 25% on both ends.
d3.select("#zoomin-button").on("click", function() {
var x1 = x.domain()[1],
x0 = x.domain()[0],
extent = x1 - x0,
scale = d3.scale.linear().domain([x0+extent/4,x1-extent/4]).nice();
d3.select("#xmin-input").node().value = scale.domain()[0];
d3.select("#xmax-input").node().value = scale.domain()[1];
render(true);
});
// Changes to any of the instant input controls
// immediately re-render.
d3.selectAll("input.instant").on("input", function() {
render(false);
});
// Changes to any of the smooth input controls
// re-renders with a smooth transition.
d3.selectAll("input.smooth").on("input", function() {
render(true);
});
</script>
</body>
</html>
http://jsdatav.is/img/thumbnails/diffeq2.png
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment