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