|
<!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;} |
|
fieldset { display: inline; border: none; padding: 0; } |
|
input, button { font-size: 14px; } |
|
input.small { width: 3em; } |
|
.brush .extent { stroke: #fff; fill-opacity: .125; shape-rendering: crispEdges; } |
|
button { cursor: pointer; } |
|
</style> |
|
</head> |
|
<body> |
|
<div id="legend"> |
|
<fieldset> |
|
<label for="function-input">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 from </label> |
|
<input id="xmin-input" type="text" class="small instant" value="-2"/> |
|
<label for="xmax-input"> to </label> |
|
<input id="xmax-input" type="text" class="small instant" value="2"/> |
|
</fieldset> |
|
<fieldset> |
|
<button id="zoomin-button">+</button> |
|
<button id="zoomout-button">−</button> |
|
</fieldset> |
|
</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: 130, right: 40, bottom: 80, left: 40}, |
|
width = 636 - margin.left - margin.right, |
|
height = 256 - margin.top - margin.bottom; |
|
|
|
// Other visualization parameters. |
|
var nPoints = 150; |
|
|
|
// Create the SVG stage for the visualization and |
|
// define its dimensions. Within that container, add a |
|
// group element (<g>) that can be transformed via |
|
// a translation to account for the margins. |
|
// Note: This construction is conventional for |
|
// D3.js, but it's a little confusing because the |
|
// svg variable references the child group element, |
|
// not the <svg> parent. |
|
var svg = d3.select("body").insert("svg","#legend") |
|
.attr("width", width + margin.left + margin.right) |
|
.attr("height", height + margin.top + margin.bottom) |
|
.append("g") |
|
.attr("transform", "translate(" + margin.left + |
|
"," + margin.top + ")"); |
|
|
|
// Define the scales that map a data value to a |
|
// position on the x-axis and to an angle for |
|
// the corresponding line. |
|
var x = d3.scale.linear(), |
|
theta = d3.scale.linear(); |
|
|
|
var render = function() { |
|
|
|
// 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; |
|
|
|
// Construct the data set, and find the maximum |
|
// y-value (irrespective of sign). |
|
var step = (xMax - xMin) / nPoints, |
|
data = d3.range(xMin, xMax + step, step) |
|
.map(function(x) { return {x: x, y: xDot(x)}; }), |
|
yMax = d3.max(data, function(d) { return Math.abs(d.y); }); |
|
|
|
// Because of rounding and floating point errors, |
|
// the equilibrium points might not be exactly 0. |
|
// To fix that, scan through the data set and |
|
// identify transitions from positive to negative |
|
// (and vice versa). On those transitions, set the |
|
// value closest to 0 to be exactly 0. |
|
for (var i=1; i<data.length; i++) { |
|
var d0 = data[i-1], |
|
d1 = data[i]; |
|
|
|
if (d0.y / Math.abs(d0.y) !== d1.y / Math.abs(d1.y)) { |
|
if (Math.abs(d0.y) <= Math.abs(d1.y)) { |
|
d0.y = 0; |
|
} else { |
|
d1.y = 0; |
|
} |
|
} |
|
} |
|
|
|
// Set the scales |
|
x.domain([xMin, xMax]) |
|
.range([0, width]); |
|
theta.domain([-yMax, yMax]) |
|
.range([-45,45]); |
|
|
|
// Define a function that will create the x-axis |
|
// when passed a data selection. |
|
var xAxis = d3.svg.axis() |
|
.scale(x) |
|
.orient("bottom"); |
|
|
|
// Create a selection for the data set. |
|
var lines = svg.selectAll("line.phase") |
|
.data(data); |
|
|
|
// Transition lines already in the DOM |
|
// to their new position and color. |
|
lines.transition().duration(750) |
|
.attr("transform", function(d) { return "translate(" + x(d.x) + "," |
|
+ height + ")rotate(" + theta(d.y) + ")"; }) |
|
.attr("stroke", function(d) { return d.y < 0 ? "#007979" : |
|
( d.y > 0 ? "#7EBD00" : "#CA0000"); }); |
|
|
|
// Add new lines for data not yet in |
|
// the DOM. |
|
lines.enter().append("line") |
|
.classed("phase", true) |
|
.attr("transform", function(d) { return "translate(" + x(d.x) + "," |
|
+ height + ")rotate(" + theta(d.y) + ")"; }) |
|
.attr("y2", -height) |
|
.attr("stroke-width","1.5px") |
|
.attr("stroke-linecap", "round") |
|
.attr("stroke", function(d) { return d.y < 0 ? "#007979" : |
|
( d.y > 0 ? "#7EBD00" : "#CA0000"); }); |
|
|
|
// 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(0," + (height + 5) + ")") |
|
.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(); |
|
|
|
// Define event handlers for the control buttons. |
|
|
|
// Update just re-renders. |
|
d3.select("#update-button").on("click", render); |
|
|
|
// 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(); |
|
}); |
|
|
|
// 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(); |
|
}); |
|
|
|
// Changes to any of the instant input controls |
|
// immediately re-render. |
|
d3.selectAll("input.instant").on("input", render) |
|
|
|
// Define a brush that lets users drag over the |
|
// graph to zoom. |
|
var brush = d3.svg.brush() |
|
.x(x) |
|
.on("brushend", brushed); |
|
|
|
// Add the rectangle that shows the brush extent. |
|
var sel = svg.append("g") |
|
.attr("class", "x brush") |
|
.call(brush) |
|
.selectAll("rect") |
|
.attr("y", -20) |
|
.attr("height", height+35); |
|
|
|
// Handle the end of a brush drag. Reset the |
|
// x domain to the brush extent and re-render. |
|
function brushed() { |
|
var scale = d3.scale.linear().domain(brush.extent()).nice(); |
|
d3.select("#xmin-input").node().value = scale.domain()[0]; |
|
d3.select("#xmax-input").node().value = scale.domain()[1]; |
|
d3.select(".x.brush .extent") |
|
.attr("x", 0) |
|
.attr("width", 0) |
|
.attr("y", -20); |
|
brush.clear(); |
|
render(); |
|
}; |
|
|
|
</script> |
|
</body> |
|
</html> |