Skip to content

Instantly share code, notes, and snippets.

@sathomas
Last active January 10, 2020 23:37
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sathomas/de8277149a27ea47761d to your computer and use it in GitHub Desktop.
Save sathomas/de8277149a27ea47761d to your computer and use it in GitHub Desktop.
Visualizing Differential Equations, III

The experiment in visualizing differential equations graduates to second order systems with this example. The graph is based on a phase plane diagram for an autonomous system in x and y. Each line is a solution to the system, and the animation illustrates how each solution evolves in time.

Note: The speed of the animation does not represent the speed as which each solution evolves. Faithfully reproducing the solution velocities would require a more computationally intensive animation technique that would make the visualization impractical in typical web browsers.

The default system has a stable equilibrium point at (0,0), so points near the origin spiral in towards (0,0). All solutions except the one that starts at (0,0) take an infinite amount of time to get there, however. The system also has an equilibrium on the unit circle (a circle of radius 1). That equilibrium is unstable, though. Solutions that start outside the unit circle spiral off to infinity as time increases. Solutions that start inside the unit circle are attracted to the stable equilibrium at (0,0).

In addition to defining the system of equations and the graph's range, the controls allow adjustment of the graph's behavior. Those controls include:

  • # The number of solutions to show. More solutions result in a denser grid, but can also take significantly more time to compute.
  • pts The number of points to show in each solution. More points extend the length of the solution lines (for those solutions that remain in the region of the graph), but also take more time to compute.
  • ∆t The increment of time to use for approximating the differential equations. Smaller time increments can result in smoother solution lines, but at the expense of more computational time.
  • T The total time for each animation loop. Use this control to slow down or speed up the animation.

There's also a Play/Pause button that suspends and resumes the animation.

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

This series of visualizations begins with part I.

<!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; }
svg { display: block; float: left;}
#legend { float: left; width: 160px; padding-top: 20px;}
fieldset { border: none; padding: 5px 0;}
label: { padding-left: 0;}
label.small { display: inline-block; width: 35px;}
input.med { width: 110px;}
input.small { width: 40px;}
input[type=range] { width: 100px; padding: 0;}
.axis line, .axis path {
fill: none;
stroke: #bbbbbb;
stroke-width: 2px;
shape-rendering: crispEdges;
}
.axis text {
fill: #444;
font-size: 14px;
}
path.line {
fill: none;
stroke-width: 1px;
}
.hide {
display: none;
}
</style>
</head>
<body>
<div id="legend">
<div class="fieldgroup">
<fieldset>
<label for="functionx-input">ẋ = </label>
<input id="functionx-input" type="text" class="med"
value="-y-0.5*x*(1-(x*x+y*y))"/>
</fieldset>
<fieldset>
<label for="functiony-input">ẏ = </label>
<input id="functiony-input" type="text" class="med"
value="x-0.5*y*(1-(x*x+y*y))"/>
</fieldset>
<fieldset>
<button id="update-button">Update</button>
</fieldset>
<fieldset style="padding-top:20px">
<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>
<label for="ymin-input">y: </label>
<input id="ymin-input" type="text" class="small smooth" value="-2"/>
<label for="ymax-input"> to </label>
<input id="ymax-input" type="text" class="small smooth" value="2"/>
</fieldset>
<fieldset>
<button id="zoomin-button">+</button>
<button id="zoomout-button">−</button>
</fieldset>
<fieldset style="padding-top:20px">
<label for="numgrid-input" class="small">#</label>
<input id="numgrid-input" class="instant" type="range" min="10" max="40"
step="1" value="25"/>
</fieldset>
<fieldset>
<label for="numpoints-input"" class="small"">pts</label>
<input id="numpoints-input" class="instant" type="range" min="1" max="5"
step="0.1" value="3"/>
</fieldset>
<fieldset>
<label for="deltat-input" class="small">∆t</label>
<input id="deltat-input" class="instant" type="range" min="1" max="50"
step="1" value="25"/>
</fieldset>
<fieldset style="padding-top:20px">
<label for="time-input" class="small">T</label>
<input id="time-input" class="instant" type="range" min="1000" max="20000"
step="500" value="5000"/>
</fieldset>
<fieldset>
<button id="pause-button">◼︎/►</button>
<span id="pause-label" class="hide">Paused</span>
</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: 20, bottom: 40, left: 40},
width = 466 - margin.left - margin.right,
height = 466 - 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 and
// 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 horizontal axis and to a
// y-position on the vertical axis. These are
// just basic linear scales in this visualization.
var x = d3.scale.linear(),
y = d3.scale.linear();
// Define a convenience function to create a line on
// the chart. 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 x- and y-scales of the chart.
var line = d3.svg.line()
.x(function(d) { return x(d[0]); })
.y(function(d) { return y(d[1]); });
// Variables to keep track of line animation.
var animationTime = 5000, // will be overridden in render()
interval = null,
playing = true;
// Variables to keep track of the lines in the chart.
// We need to define these outside of the render function
// so the animation function can access then.
var lines = null,
nPoints = 0;
// Define a function that will animate the lines.
// This is a function so that we can call it
// repeatedly in an interval timer.
var animateLines = function() {
// Iterate through each line in the selection.
lines.each(function() {
// Get the total path length from the
// DOM element directly and cache it
// as a property of the element (so
// we only have to compute it once
// as we loop the animation).
var len = this.dataLen || this.getTotalLength();
this.dataLen = len;
// Style the `<path>` element to its initial
// (pre-animation) state. First, disable
// transitions so that the styles take effect
// immediately. We need to do this because
// subsequent (interval timer) calls to this
// function will be made with the transition
// still set from the last time. Then set the
// dash array for the line to be dashes equal
// to the entire path length (first dash, then
// gap). And finally, set the dash offset so
// that the dashing begins a full length into
// the pattern. That will effectively make the
// entire path a gap in the dash. While we're
// at it, reset the stroke color and opacity.
var sel = d3.select(this)
.style({
"transition": "none",
"fill": "none",
"stroke-dasharray": len + ' ' + len,
"stroke-dashoffset": len,
"stroke": function(d) {
return d.length === nPoints ?
"#007979" :"#7EBD00";
},
"stroke-opacity": 1
});
// Force the browser to apply the changes we
// just specified. (Otherwise, it might cache
// them waiting to combine them with subsequent
// style updates, which we are indeed about to
// apply. Since those subsequent updates change
// the styles we just defined, we need to make
// sure the styles are applied first.)
this.getBoundingClientRect();
// Now that everything is in place, we can
// trigger the animation. We do that by
// defining a transition on the dash offset
// and then setting that property to zero.
// The transition will have the effect of
// _gradually_ reducing the dash offset from
// the entire path length down to zero.
// Cheap animation effects are us.
sel.style({
"transition": "stroke-dashoffset " +
animationTime +
"ms ease-in",
"stroke-dashoffset": 0
});
});
};
// Define a function to loop the animation. It
// returns the interval timer id. The optional
// parameter tells the function to execute the
// first animation immediately.
var loopAnimation = function(immediate) {
// Define a function that starts the animation
// within a loop.
var startLoop = function() {
// Before restarting the animation, fade
// out the lines by transitioning their
// opacity.
lines.transition()
.duration(500)
.ease("linear")
.style("stroke-opacity", 0);
// Delay after the fade-out before restarting
// the animation.
setTimeout(animateLines, 1000)
};
// If we're supposed to start the animation
// immediately, do so.
if (immediate) {
startLoop();
}
// Define an interval to repeat the animation
// and return that interval's id.
return setInterval(startLoop, animationTime*2);
};
// Define a function to draw the graph. We make this
// a function so we can call it when the user changes
// the graph parameters.
var render = function() {
// If an animation is running, cancel it.
if (interval) {
clearInterval(interval);
interval = null;
}
// Get the specifics of the differential equation to
// visualize. We require functions `xDot(x,y)` and
// `yDot(w,y) that return time derivative at the
// point (x,y) and a range of x-values and
// y-values to graph. We also get graph parameters
// from the user interface. Note that `nPoints` and
// `dt` are scaled logarithmically.
eval("var xDot = function(x,y) { return "
+ d3.select("#functionx-input").node().value + "}");
eval("var yDot = function(x,y) { return "
+ d3.select("#functiony-input").node().value + "}");
var xMin = +d3.select("#xmin-input").node().value,
xMax = +d3.select("#xmax-input").node().value,
yMin = +d3.select("#ymin-input").node().value,
yMax = +d3.select("#ymax-input").node().value,
nGrid = +d3.select("#numgrid-input").node().value,
dt = Math.pow(10, -1 * (+d3.select("#deltat-input").node().value) / 10);
nPoints = Math.pow(10, +d3.select("#numpoints-input").node().value),
animationTime = +d3.select("#time-input").node().value;
// Set the scales
x.domain([xMin, xMax]).range([0, width]);
y.domain([yMin, yMax]).range([height,0]);
// Construct the initial values array.
var stepX = (xMax - xMin) / nGrid,
stepY = (yMax - yMin) / nGrid,
data = d3.range((nGrid+1)*(nGrid+1))
.map(function(i) {
var x = xMin + (i%(nGrid+1)) * stepX,
y = yMin + Math.round(i/(nGrid+1)) * stepY;
return [[x, y]];
});
// Extend the curves through time.
data = data.map(function(x0) {
var pts = [x0[0]];
for (var i=1; i<nPoints; i++) {
var prev = pts[i-1],
newX = prev[0] + xDot(prev[0],prev[1]) * dt,
newY = prev[1] + yDot(prev[0],prev[1]) * dt;
pts.push([newX,newY]);
}
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][0]) && !isNaN(soln[i][1]) &&
(soln[i][0] >= xMin) && (soln[i][0] <= xMax) &&
(soln[i][1] >= yMin) && (soln[i][1] <= yMax))
break;
toSlice--;
}
return toSlice < 0 ? soln.slice(0,toSlice) : soln;
});
// Define functions that will create the x-axis
// and y-axis when passed a data selection.
var xAxis = d3.svg.axis()
.scale(x)
.tickSize(0, 0)
.tickPadding(10)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.tickSize(0, 0)
.tickPadding(10)
.orient("left");
// Draw the x- and y-axes, either from scratch
// or by adjusting the existing one.
if (d3.selectAll(".x.axis").size()) {
// Since an axis already exists,
// just adjust to its new values.
svg.select(".x.axis").call(xAxis);
svg.select(".y.axis").call(yAxis);
} else {
// No axes yet exist, so create them.
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate("+margin.left+","+(height+margin.top)+")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate("+margin.left+","+margin.top+")")
.call(yAxis);
}
// Delete any existing lines
g.selectAll(".line").remove();
// Add new lines corresponding to the
// current data values.
lines = g.selectAll(".line")
.data(data)
.enter()
.append("path")
.classed("line", true)
.style("stroke-opacity", 1)
.attr("d", line);
// Now that we've defined the animation function, go ahead
// and trigger it.
animateLines();
// Set up an interval to repeat the animation indefinitely.
interval = loopAnimation();
}
// Go ahead and draw the chart.
render();
// Define event handlers for the control buttons.
// Update just re-renders.
d3.select("#update-button").on("click", function() {
render();
});
// Zoom out increases the x and y domains by 50% on both ends.
d3.select("#zoomout-button").on("click", function() {
var x1 = x.domain()[1],
x0 = x.domain()[0],
extentx = x1 - x0,
scalex = d3.scale.linear().domain([x0-extentx/2,x1+extentx/2]).nice(),
y1 = y.domain()[1],
y0 = y.domain()[0],
extenty = y1 - y0,
scaley = d3.scale.linear().domain([y0-extenty/2,y1+extenty/2]).nice()
d3.select("#xmin-input").node().value = scalex.domain()[0];
d3.select("#xmax-input").node().value = scalex.domain()[1];
d3.select("#ymin-input").node().value = scaley.domain()[0];
d3.select("#ymax-input").node().value = scaley.domain()[1];
render();
});
// Zoom in decreases the x adn y domains by 25% on both ends.
d3.select("#zoomin-button").on("click", function() {
var x1 = x.domain()[1],
x0 = x.domain()[0],
extentx = x1 - x0,
scalex = d3.scale.linear().domain([x0+extentx/4,x1-extentx/4]).nice(),
y1 = y.domain()[1],
y0 = y.domain()[0],
extenty = y1 - y0,
scaley = d3.scale.linear().domain([y0+extenty/4,y1-extenty/4]).nice();
d3.select("#xmin-input").node().value = scalex.domain()[0];
d3.select("#xmax-input").node().value = scalex.domain()[1];
d3.select("#ymin-input").node().value = scaley.domain()[0];
d3.select("#ymax-input").node().value = scaley.domain()[1];
render();
});
// Catch clicks on the play/pause button.
d3.select("#pause-button").on("click", function() {
// Toggle the playing state.
playing = !playing;
// Update the class on the play/pause label to show
// or hide it as appropriate.
d3.select("#pause-label")
.classed("hide", playing);
// Restart or cancel the animation looping,
// as appropriate.
if (playing) {
interval = loopAnimation(true);
} else {
if (interval) {
clearInterval(interval);
interval = null;
}
}
});
// Changes to any of the instant input controls
// immediately re-render.
d3.selectAll("input.instant").on("input", function() {
// restart the animation if it's paused
playing = true;
d3.select("#pause-label")
.classed("hide", playing);
// and re-render the graph
render();
});
</script>
</body>
</html>
http://jsdatav.is/img/thumbnails/diffeq3.png
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment