The following code is my attempt to create an interactive bodeplot. I used JavaScript using d3.js, jQuery Mobile and math.js. The bode plot shows a lead lag filter in continuous time and several discrete time equivalents using a certain discretization technique.
I would like to have some comments on:
- How I created the bode plot using d3.js. I have the feeling that my code is bloated and can be shorten and made more concise.
- How I am handling the data generation in
updateData
. I am not really satisfied about it but I do not know how to improve that. - How can I make my code more abstract? I am namely planning to create more bode plots with different types of filters. I do not wish to copy-paste my code all the time.
Working snippet: http://plnkr.co/edit/5VDht1dbyfJ8IiNNpZbG
<!DOCTYPE html>
<html lang="en">
<head>
<title>LOG</title>
<meta charset="utf-8">
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//code.jquery.com/jquery-1.11.3.min.js"></script>
<script src="//code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
<script src="//mathjs.org/js/lib/math.js"></script>
<link rel="stylesheet" href="//code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css">
<style type="text/css">
.ui-page {
background-color: #fff;
}
svg {
font: 10px sans-serif;
}
rect {
fill: transparent;
}
.axis {
shape-rendering: crispEdges;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
}
.line {
fill: none;
stroke: steelblue;
stroke-width: 1px;
clip-path: url(#clip);
}
.grid .tick {
stroke: lightgrey;
opacity: 0.7;
shape-rendering: crispEdges;
}
.grid path {
stroke-width: 0;
}
#sliders input {
display: none;
}
.ui-slider-track {
margin-left: 0;
}
a.ui-slider-handle.ui-btn.ui-shadow {
width: 45px;
}
.sideByside .ui-block-a {
padding-right: 6px;
}
.sideByside .ui-block-b {
padding-left: 6px;
padding-right: 6px;
}
.sideByside .ui-block-c {
padding-left: 6px;
}
</style>
</head>
<body>
<div id="plotmag"></div>
<div id="plotphs" style="margin-top: -45px;"></div>
<form id="sliders">
<div data-role="fieldcontain">
<label for="slider-g">Gain:</label>
<input type="range" name="slider-g" id="slider-g" min="0.1" max="10" step="0.1" value="1" data-show-value="true" />
</div>
<div data-role="fieldcontain">
<label for="slider-fz">Freq. zero:</label>
<input type="range" name="slider-fz" id="slider-fz" min="1" max="100" step="1" value="20" data-show-value="true" />
</div>
<div data-role="fieldcontain">
<label for="slider-fp">Freq. pole:</label>
<input type="range" name="slider-fp" id="slider-fp" min="1" max="100" step="1" value="40" data-show-value="true" />
</div>
<div data-role="fieldcontain">
<label for="slider-fs">Sampling freq.:</label>
<input type="range" name="slider-fs" id="slider-fs" min="100" max="10000" step="100" value="1000" data-show-value="true" />
</div>
</form>
<script type="text/javascript">
function linspace(a,b,n) {
var every = (b-a)/(n-1),
range = [];
for (i = a; i < b; i += every)
range.push(i);
return range.length == n ? range : range.concat(b);
}
function logspace(a,b,n) {
return linspace(a,b,n).map(function(x) { return Math.pow(10,x); });
}
function cleadlag(f,filter) {
s = math.complex(0,2*math.pi*f);
return math.divide(math.add(math.multiply(filter.a[1],s),filter.a[0]),
math.add(math.multiply(filter.b[1],s),filter.b[0]));
}
function dleadlag(f,filter) {
z = math.exp(math.complex(0,2*math.pi*f/fs));
return math.divide(math.add(math.multiply(filter.a[1],z),filter.a[0]),
math.add(math.multiply(filter.b[1],z),filter.b[0]));
}
function angle(f) {
return math.atan2(f.im,f.re);
}
function deg2rad(deg) {
return deg * math.pi / 180;
}
function rad2deg(rad) {
return rad * 180 / math.pi;
}
function mag2db(mag) {
return 20 * Math.log10(mag);
}
function db2mag(db) {
return math.pow(10,db / 20);
}
var margin = {
top: 20,
right: 20,
bottom: 35,
left: 50
};
var filter = {
leadlag: {
continuous: { a: [], b: [] },
forwardeuler: { a: [], b: [] },
backwardeuler: { a: [], b: [] },
tustin: { a: [], b: [] }
}
}
var seriesMag;
var seriesPhs;
var dataMag = [];
var dataPhs = [];
var data1 = [];
var data2 = [];
var data3 = [];
var data4 = [];
var data5 = [];
var data6 = [];
var data7 = [];
var data8 = [];
var width = 600 - margin.left - margin.right;
var height = 250 - margin.top - margin.bottom;
var range = logspace(-1,4,5000);
var x = d3.scale.log()
.domain([range[0], range[range.length-1].toFixed()])
.range([0, width]);
var xGrid = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(5)
.tickSize(-height, -height, 0)
.tickFormat("");
var magY = d3.scale.linear()
.domain([-20, 20])
.range([height, 0]);
var magXAxis1 = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(1,"0.1s")
.innerTickSize(-6)
.outerTickSize(0)
.tickPadding(7)
.tickFormat("");
var magYAxis1 = d3.svg.axis()
.scale(magY)
.orient("left")
.ticks(5)
.innerTickSize(-6)
.outerTickSize(0)
.tickPadding(7);
var magXAxis2 = d3.svg.axis()
.scale(x)
.orient("top")
.ticks(5)
.innerTickSize(-6)
.tickPadding(-20)
.outerTickSize(0)
.tickFormat("");
var magYAxis2 = d3.svg.axis()
.scale(magY)
.orient("left")
.ticks(5)
.innerTickSize(6)
.tickPadding(-20)
.outerTickSize(0)
.tickFormat("");
var magYGrid = d3.svg.axis()
.scale(magY)
.orient("left")
.ticks(5)
.tickSize(-width, -width, 0)
.tickFormat("");
var magLine = d3.svg.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return magY(d.y); })
.interpolate("linear");
var magZoomXY = d3.behavior.zoom()
.x(x)
.y(magY)
.on("zoom",redraw);
var magZoomY = d3.behavior.zoom()
.y(magY)
.on("zoom",redraw);
// Create plot
var plotMag = d3.select("#plotmag").append("svg")
.attr("width",width + margin.left + margin.right)
.attr("height",height + margin.top + margin.bottom)
.append("g")
.attr("transform","translate(" + margin.left + "," + margin.top + ")");
// Append x grid
plotMag.append("g")
.attr("class","x grid")
.attr("transform","translate(0," + height + ")")
.call(xGrid);
// Append y grid
plotMag.append("g")
.attr("class","y grid")
.call(magYGrid);
// Append x axis
plotMag.append("g")
.attr("class","x1 axis")
.attr("transform","translate(0," + height + ")")
.call(magXAxis1);
// Append additional X axis
plotMag.append("g")
.attr("class","x2 axis")
.attr("transform","translate(" + [0, 0] + ")")
.call(magXAxis2);
// Append y axis
plotMag.append("g")
.attr("class","y1 axis")
.call(magYAxis1);
// Append additional y axis
plotMag.append("g")
.attr("class","y2 axis")
.attr("transform","translate(" + [width, 0] + ")")
.call(magYAxis2);
// Add y axis label
plotMag.append("text")
.attr("transform", "rotate(-90)")
.attr("y",0 - margin.left)
.attr("x",0 - (height / 2))
.attr("dy", "1em")
.style("font-size","15")
.style("text-anchor", "middle")
.text("Magnitude [dB]");
// Clip path
plotMag.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
plotMag.append("rect")
.attr("width", width)
.attr("height", height);
plotMag.append("rect")
.attr("class", "mag zoom xy")
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom)
.style("visibility", "hidden")
.attr("pointer-events", "all")
.call(magZoomXY);
plotMag.append("rect")
.attr("class", "mag zoom y")
.attr("width", margin.left)
.attr("height", height - margin.top - margin.bottom)
.attr("transform", "translate(" + -margin.left + "," + 0 + ")")
.style("visibility", "hidden")
.attr("pointer-events", "all")
.call(magZoomY);
var phsY = d3.scale.linear()
.domain([-45, 45])
.range([height, 0]);
var phsXAxis1 = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(1,"0.1s")
.innerTickSize(-6)
.outerTickSize(0)
.tickPadding(7);
var phsYAxis1 = d3.svg.axis()
.scale(phsY)
.orient("left")
.ticks(5)
.innerTickSize(-6)
.outerTickSize(0)
.tickPadding(7);
var phsXAxis2 = d3.svg.axis()
.scale(x)
.orient("top")
.ticks(5)
.innerTickSize(-6)
.tickPadding(-20)
.outerTickSize(0)
.tickFormat("");
var phsYAxis2 = d3.svg.axis()
.scale(phsY)
.orient("left")
.ticks(5)
.innerTickSize(6)
.tickPadding(-20)
.outerTickSize(0)
.tickFormat("");
var phsYGrid = d3.svg.axis()
.scale(phsY)
.orient("left")
.ticks(5)
.tickSize(-width, -width, 0)
.tickFormat("");
var phsLine = d3.svg.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return phsY(d.y); })
.interpolate("linear");
var phsZoomXY = d3.behavior.zoom()
.x(x)
.y(phsY)
.on("zoom",redraw);
var phsZoomX = d3.behavior.zoom()
.x(x)
.on("zoom",redraw);
var phsZoomY = d3.behavior.zoom()
.y(phsY)
.on("zoom",redraw);
// Create plot
var plotPhs = d3.select("#plotphs").append("svg")
.attr("width",width + margin.left + margin.right)
.attr("height",height + margin.top + margin.bottom)
.append("g")
.attr("transform","translate(" + margin.left + "," + margin.top + ")");
// Append x grid
plotPhs.append("g")
.attr("class","x grid")
.attr("transform","translate(0," + height + ")")
.call(xGrid);
// Append y grid
plotPhs.append("g")
.attr("class","y grid")
.call(phsYGrid);
// Append x axis
plotPhs.append("g")
.attr("class","x1 axis")
.attr("transform","translate(0," + height + ")")
.call(phsXAxis1);
// Append additional X axis
plotPhs.append("g")
.attr("class","x2 axis")
.attr("transform","translate(" + [0, 0] + ")")
.call(phsXAxis2);
// Append y axis
plotPhs.append("g")
.attr("class","y1 axis")
.call(phsYAxis1);
// Append additional y axis
plotPhs.append("g")
.attr("class","y2 axis")
.attr("transform","translate(" + [width, 0] + ")")
.call(phsYAxis2);
// Add x axis label
plotPhs.append("text")
.attr("transform","translate(" + (width / 2) + "," + (height + margin.bottom - 5) + ")")
.style("font-size","15")
.style("text-anchor","middle")
.text("Frequency [Hz]");
// Add y axis label
plotPhs.append("text")
.attr("transform", "rotate(-90)")
.attr("y",0 - margin.left)
.attr("x",0 - (height / 2))
.attr("dy", "1em")
.style("font-size","15")
.style("text-anchor", "middle")
.text("Phase [deg]");
// Clip path
plotPhs.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
plotPhs.append("rect")
.attr("width", width)
.attr("height", height);
plotPhs.append("rect")
.attr("class", "phs zoom xy")
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom)
.style("visibility", "hidden")
.attr("pointer-events", "all")
.call(phsZoomXY)
plotPhs.append("rect")
.attr("class", "phs zoom x")
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom)
.attr("transform", "translate(" + 0 + "," + (height - margin.top - margin.bottom) + ")")
.style("visibility", "hidden")
.attr("pointer-events", "all")
.call(phsZoomX);
plotPhs.append("rect")
.attr("class", "phs zoom y")
.attr("width", margin.left)
.attr("height", height - margin.top - margin.bottom)
.attr("transform", "translate(" + -margin.left + "," + 0 + ")")
.style("visibility", "hidden")
.attr("pointer-events", "all")
.call(phsZoomY);
function updateAxis() {
plotMag.select(".x1.axis").call(magXAxis1);
plotMag.select(".y1.axis").call(magYAxis1);
plotMag.select(".x2.axis").call(magXAxis2);
plotMag.select(".y2.axis").call(magYAxis2);
plotMag.select(".x.grid").call(xGrid);
plotMag.select(".y.grid").call(magYGrid);
plotPhs.select(".x1.axis").call(phsXAxis1);
plotPhs.select(".y1.axis").call(phsYAxis1);
plotPhs.select(".x2.axis").call(phsXAxis2);
plotPhs.select(".y2.axis").call(phsYAxis2);
plotPhs.select(".x.grid").call(xGrid);
plotPhs.select(".y.grid").call(phsYGrid);
plotPhs.selectAll(".x1.axis>.tick")
.each(function(d,i){
if (d3.select(this).select('text').text() === "") {
d3.selectAll(".x.grid>.tick:nth-child(" + (i + 1) + ")")
.style("stroke-dasharray","3,3");
}
});
seriesMag = plotMag.selectAll(".line").data(dataMag);
seriesPhs = plotPhs.selectAll(".line").data(dataPhs);
seriesMag.enter().append("path");
seriesPhs.enter().append("path");
seriesMag.attr("class","line")
.attr("d",function(d) { return magLine(d.data); })
.attr("stroke-width", function(d) { return d.width; })
.style("stroke", function(d) { return d.color; })
.style("stroke-dasharray", function(d) { return d.stroke; });
seriesPhs.attr("class","line")
.attr("d",function(d) { return phsLine(d.data); })
.attr("stroke-width", function(d) { return d.width; })
.style("stroke", function(d) { return d.color; })
.style("stroke-dasharray", function(d) { return d.stroke; });
}
function updateZoom() {
var magZoomXY = d3.behavior.zoom()
.x(x)
.y(magY)
.on("zoom",redraw);
var magZoomY = d3.behavior.zoom()
.y(magY)
.on("zoom",redraw);
var phsZoomXY = d3.behavior.zoom()
.x(x)
.y(phsY)
.on("zoom",redraw);
var phsZoomX = d3.behavior.zoom()
.x(x)
.on("zoom",redraw);
var phsZoomY = d3.behavior.zoom()
.y(phsY)
.on("zoom",redraw);
plotMag.select(".mag.zoom.xy").call(magZoomXY);
plotMag.select(".mag.zoom.y").call(magZoomY);
plotPhs.select(".phs.zoom.xy").call(phsZoomXY);
plotPhs.select(".phs.zoom.x").call(phsZoomX);
plotPhs.select(".phs.zoom.y").call(phsZoomY);
}
function updateData() {
fs = parseFloat($("#slider-fs").val());
K = parseFloat($("#slider-g").val());
fz = parseFloat($("#slider-fz").val());
fp = parseFloat($("#slider-fp").val());
wz = 2*math.pi*fz;
wp = 2*math.pi*fp;
filter.leadlag.continuous.a = [K*wp*wz, K*wp];
filter.leadlag.continuous.b = [wp*wz, wz];
filter.leadlag.forwardeuler.a = [-K*wp, K*wp*(1 + wz/fs)];
filter.leadlag.forwardeuler.b = [-wz, wz*(1 + wp/fs)];
filter.leadlag.backwardeuler.a = [K*wp*(wz/fs - 1), K*wp];
filter.leadlag.backwardeuler.b = [wz*(wp/fs - 1), wz];
filter.leadlag.tustin.a = [K*wp*(wz/fs - 2), K*wp*(2 + wz/fs)];
filter.leadlag.tustin.b = [wz*(wp/fs - 2), wz*(2 + wp/fs)];
dataMag = [];
dataPhs = [];
data1 = [];
data2 = [];
data3 = [];
data4 = [];
data5 = [];
data6 = [];
data7 = [];
data8 = [];
for (var i = 0; i < range.length; i++) {
data1.push({
x: range[i],
y: mag2db(math.abs(cleadlag(range[i],filter.leadlag.continuous)))
});
data2.push({
x: range[i],
y: rad2deg(angle(cleadlag(range[i],filter.leadlag.continuous)))
});
if (range[i] < fs/2) {
data3.push({
x: range[i],
y: mag2db(math.abs(dleadlag(range[i],filter.leadlag.forwardeuler)))
});
data4.push({
x: range[i],
y: rad2deg(angle(dleadlag(range[i],filter.leadlag.forwardeuler)))
});
data5.push({
x: range[i],
y: mag2db(math.abs(dleadlag(range[i],filter.leadlag.backwardeuler)))
});
data6.push({
x: range[i],
y: rad2deg(angle(dleadlag(range[i],filter.leadlag.backwardeuler)))
});
data7.push({
x: range[i],
y: mag2db(math.abs(dleadlag(range[i],filter.leadlag.tustin)))
});
data8.push({
x: range[i],
y: rad2deg(angle(dleadlag(range[i],filter.leadlag.tustin)))
});
}
}
dataMag.push({data: data1, width: 1, color: "blue", stroke: "0,0", legend: "Magnitude" });
dataMag.push({data: data3, width: 1, color: "red", stroke: "0,0", legend: "Sampling" });
dataMag.push({data: data5, width: 1, color: "green", stroke: "0,0", legend: "Sampling" });
dataMag.push({data: data7, width: 1, color: "yellow", stroke: "0,0", legend: "Sampling" });
dataMag.push({data: [{x: fs/2, y: -1000},{x: fs/2, y: 1000}], width: 1, color: "black", stroke: "5,5", legend: "Sampling" });
dataPhs.push({data: data2, width: 1, color: "blue", stroke: "0,0", legend: "Phase" });
dataPhs.push({data: data4, width: 1, color: "red", stroke: "0,0", legend: "Phase" });
dataPhs.push({data: data6, width: 1, color: "green", stroke: "0,0", legend: "Phase" });
dataPhs.push({data: data8, width: 1, color: "yellow", stroke: "0,0", legend: "Phase" });
dataPhs.push({data: [{x: fs/2, y: -1000},{x: fs/2, y: 1000}], width: 1, color: "black", stroke: "5,5", legend: "Sampling" });
}
function redraw() {
updateAxis();
updateZoom();
}
$(function() {
updateData();
redraw();
});
$("#sliders").change(function() {
updateData();
redraw();
});
</script>
</body>
</html>