Skip to content

Instantly share code, notes, and snippets.

@jinroh
Last active March 26, 2024 07:54
Show Gist options
  • Save jinroh/7524988 to your computer and use it in GitHub Desktop.
Save jinroh/7524988 to your computer and use it in GitHub Desktop.
Fourier series visualisation with d3.js.

From Wikipedia:

In mathematics, a Fourier series decomposes periodic functions or periodic signals into the sum of a (possibly infinite) set of simple oscillating functions, namely sines and cosines (or complex exponentials).

Use the bottom right form to change the visualized serie.

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<style>
body {
font: 13px/13px "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: auto;
position: relative;
width: 960px;
}
form {
position: absolute;
bottom: 10px;
right: 10px;
}
.hide {
display: none;
}
.coeff .dot {
fill: hsla(207, 63%, 27%, 0.2);
}
.coeff.last .dot {
fill: hsla(207, 63%, 27%, 1.0);
}
.coeff .circle {
fill: none;
stroke: hsl(0, 0%, 70%);
}
.coeff.first .circle {
fill: none;
stroke: hsl(0, 0%, 30%);
}
.coeff.last .circle {
display: none;
}
.graph {
fill: none;
stroke: steelblue;
stroke-width: 3px;
}
.trace {
fill: none;
stroke: steelblue;
}
.proj {
fill: none;
stroke: #000;
}
.axis {
stroke: hsl(0, 0%, 70%);
}
</style>
</head>
<body>
<form>
<p>
<select id="type">
<option value="square">Square</option>
<option value="triangle">Triangle</option>
<option value="sawtooth">Sawtooth</option>
<option value="fibonacci">Fibonacci</option>
<option value="pulse">Pulse</option>
</select>
<input id="size" type="number" value="6" min="1" max="40" step="1">
</p>
<p><input id="freq" type="range" value="0.3" min="0.01" max="0.5" step="0.01"> <label>Speed</label></p>
</form>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
(function() {
"use strict";
var π = Math.PI
var τ = 2 * Math.PI
var types = {
square: function(n) {
return (((n + 1) % 2) ? 0 : 1) / n;
},
triangle: function(n) {
if (!(n % 2)) return 0;
return ((n % 4 === 1) ? 1 : -1) / (n * n);
},
sawtooth: function(n) {
return ((n % 2) ? -1 : 1) / (n + 1);
},
fibonacci: function(n) {
var fst = 0.01, sec = 0.01, add;
for (var i = 0; i < n; i++) {
add = fst + sec;
fst = sec;
sec = add;
}
return add;
},
pulse: function(n) {
return 0.1;
}
};
function FT(A, N, φ) {
φ = φ || 0;
return function(x) {
var n = -1, y = 0;
while (++n < N) {
y += A[n] * Math.sin(τ * (n + 1) * x + φ);
}
return y;
}
}
function once(fn) {
var exec = false;
return function() {
if (exec) return;
exec = true,
fn && fn();
};
}
var
margin = {top: 40, right: 100, bottom: 40, left: 150},
W = 960,
H = 500,
w = W - margin.left - margin.right,
h = H - margin.top - margin.bottom,
radius = 140,
theta = 0,
xmax = 1.5,
rate = 1 / 60,
tDomain = d3.range(0, 1.1, 1 / 1000), // trace domain
gDomain = d3.range(0, xmax, xmax / 1000), // graph domain
C = types.square, // coeffiecients
L = 6, // size
F = 0.3, // frequence
yCirc = d3.scale.linear().domain([-1, 1]).range([h/2 + radius, h/2 - radius]),
xCirc = d3.scale.linear().domain([-1, 1]).range([0, 2 * radius]),
rAxis = d3.scale.linear().domain([0, 1]).range([0, radius]),
xAxis = d3.scale.linear().range([radius, W - margin.left]),
Fxy, fx, fy,
draw, timer, data = [];
var graph = d3.svg.line()
.x(function(d) { return xAxis(d); })
.y(function(d) { return yCirc(fy(theta - d)); });
var proj = d3.svg.line()
.x(function(d) { return xCirc(d.x); })
.y(function(d) { return yCirc(d.y); });
var trace = d3.svg.line()
.x(function(d) { return xCirc(fx(d)); })
.y(function(d) { return yCirc(fy(d)); });
function gTransform(d) {
return "translate(" + xCirc(d.x) + "," + yCirc(d.y) + ")";
}
function hTransform(d) {
return "translate(" + xAxis(d.f) + "," + yCirc(0) + ")";
}
var svg = d3.select("body")
.append("svg")
.attr("width", W)
.attr("height", H)
svg.append("line")
.attr("class", "axis")
.attr("y1", margin.top + yCirc(0)).attr("x1", 0)
.attr("y2", margin.top + yCirc(0)).attr("x2", W);
svg.append("line")
.attr("class", "axis")
.attr("x1", margin.left + xCirc(0)).attr("y1", 0)
.attr("x2", margin.left + xCirc(0)).attr("y2", H);
var vis = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var gPath = vis.append("path").attr("class", "graph");
var tPath = vis.append("path").attr("class", "trace");
var pPath = vis.append("path").attr("class", "proj");
function cache() {
var A;
if (typeof C === "function") {
A = d3.range(1, L + 1).map(C);
} else {
A = C.slice(0, L);
}
fx = FT(A, L - 1, π/2);
fy = FT(A, L - 1, 0),
Fxy = A.map(function(a, i) {
return { X: FT(A, i, π/2), Y: FT(A, i, 0), r: Math.abs(a) };
});
}
function calc() {
if (!Fxy) cache();
Fxy.forEach(function(f, i) {
var d = data[i] || (data[i] = {x:0,y:0,r:0});
d.x = f.X(theta);
d.y = f.Y(theta);
d.r = f.r;
d.f = i + 1;
});
data.length = Fxy.length;
return data;
}
function coeff() {
var co = vis.selectAll(".coeff").data(calc());
// exit
co.exit().remove();
// enter
var en = co.enter().append("g").attr("class", "coeff");
en.append("circle").attr("class", "circle");
en.append("circle").attr("class", "dot").attr("r", 3);
// update
co.classed("last", function(d, i) { return i === L - 1; });
co.classed("first", function(d, i) { return i === 0; });
co.select(".circle").attr("r", function(d) { return rAxis(d.r); })
return co;
}
function drawGraph() {
xAxis.domain([0, xmax]);
coeff().attr("transform", gTransform);
var last = data[data.length - 1];
pPath.attr("d", proj([last, {x:0,y:last.y}]));
gPath.attr("d", graph(gDomain));
tPath.attr("d", trace(tDomain));
}
function drawHisto() {
xAxis.domain([1, L]);
coeff().attr("transform", hTransform);
}
function toggle(callback) {
var tran;
tran = (draw === drawGraph) ? hTransform : gTransform;
draw = (draw === drawGraph) ? drawHisto : drawGraph;
coeff().transition()
.duration(1000)
.attr("transform", tran)
.each("end", once(callback));
}
function toggleGraph() {
xAxis.domain([0, xmax]);
toggle(function() {
pPath.classed("hide", false);
gPath.classed("hide", false);
tPath.classed("hide", false);
play();
});
}
function toggleHisto() {
xAxis.domain([1, L]);
pPath.classed("hide", true);
gPath.classed("hide", true);
tPath.classed("hide", true);
pause(); toggle(drawHisto);
}
function play() {
if (timer) return;
(function loop() {
drawGraph();
theta += F * rate;
timer = setTimeout(loop, rate * 1000);
})();
}
function pause() {
if (!timer) return;
clearTimeout(timer);
timer = null;
}
function redraw() {
cache(); draw();
}
d3.select("svg").on("click", function() { (draw === drawHisto) ? toggleGraph() : toggleHisto(); });
d3.select("#freq").on("change", function() { F = +this.value; redraw(); });
d3.select("#size").on("change", function() { L = +this.value; redraw(); });
d3.select("#type").on("change", function() { C = types[this.value]; redraw(); });
draw = drawGraph;
play();
})();
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment