|
<!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> |