Skip to content

Instantly share code, notes, and snippets.

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

An experiment in visualizing differential equations based on the traditional phase line. The chart shows the qualitative behavior of a specific differential equation in a single variable. Initially, the equation is ẋ = 1 - x². The chart represents the value of across a range of values (initially from -2 to -2) as vertical lines of fixed length. The color and tilt of the lines vary based on . Positive values of tilt to the right and are colored green. Negative values of tilt to the left and are colored blue. The magnitude of determines the degree of tilt, with larger values shown with greater tilt. Lines corresponding to equilibrium points, where ẋ = 0, are colored red.

The chart exposes a physical interpretation of the equation, with x(t) representing the position of a particle in ℜ. The tilt of the line at any point indicates the direction that the particle will move from that point. The angle of the tilt represents how fast the particle will move.

You can zoom in or out to see details using the buttons as well as the mouse or touchscreen.

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 II.

Note: This implementation is something I put together in a few hours to satisfy my own curiosity. I haven’t taken the time to style the controls, etc. If there’s enough interest in the approach, though, I could be persuaded to improve it. Let me know on twitter at @jsdatavis.

<!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>
http://jsdatav.is/img/thumbnails/diffeq1.png
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment