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>
1 Answer 1
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.
The d3.js code is very similar for the two plots. You could save some 150 lines of d3.js code by writing a generalized function and passing in a few parameters.
Other than that, I wouldn't agonize over the number of lines. Graphics code is often bulky.
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.
updateData()
will simplify considerably by taking an object-oriented approach to defining the filters and providing each filter with its own .calculate()
method.
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.
Yes indeed. Object-oriented filters will help considerably. Then you need a mechanism for defining filters in your "user-code".
In re-factoring the code to provide a Filter()
constructor, you might also consider :
- Reducing the number of global members by (eg) wrapping everything in a
BODEPLOT
namespace. - Allowing for more than one pair of plots on a single page by factoring much of the code as a
Plot()
constructor. - Allowing plot functions (like
cleadlag
,dleadlag
) to be defined internally or externally. This may be necessary as a consequence of allowing the dynamic definition of filters. - Making the code less reliant on hard-coded settings by allowing the
Plot()
constructor to accept anoptions
object.
Here's the kind or code you might end up with, meeting all the above objectives (except generalizing the d3.js code, which remains very bulky).
var BODEPLOT = (function(jQuery, Math, math, d3) {
// ********************************
// *** start: private functions ***
// ********************************
function linspace(a, b, n) {
var every = (b-a)/(n-1),
range = [];
for (var 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 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);
}
function purgeNulls(val) {
return val;
}
// ******************************
// *** fin: private functions ***
// ******************************
// ***************************
// *** start: private vars ***
// ***************************
var filters = {};
var plotFunctions = {};
// *************************
// *** fin: private vars ***
// *************************
// *********************************
// *** start: Plot() constructor ***
// *********************************
function Plot(options) {
var that = this;
// A `settings` object, which can be overridden by `options`
this.settings = $.extend(true, {
'width': 600,
'height': 250,
'margin': { 'top': 20, 'right': 20, 'bottom': 35, 'left': 50 },
'logspace': { 'a':-1, 'b': 4, 'n': 5000 },
'filters': []
}, options);
var settings = this.settings; // shorthand for immediate use below
this.dataMag = [];
this.dataPhs = [];
var width = settings.width - settings.margin.left - settings.margin.right;
var height = settings.height - settings.margin.top - settings.margin.bottom;
this.range = logspace(settings.logspace.a, settings.logspace.b, settings.logspace.n);
this.x = d3.scale.log()
.domain([this.range[0], this.range[this.range.length-1].toFixed()])
.range([0, width]);
this.xGrid = d3.svg.axis()
.scale(this.x)
.orient("bottom")
.ticks(5)
.tickSize(-height, -height, 0)
.tickFormat("");
this.magY = d3.scale.linear()
.domain([-20, 20])
.range([height, 0]);
this.magXAxis1 = d3.svg.axis()
.scale(this.x)
.orient("bottom")
.ticks(1,"0.1s")
.innerTickSize(-6)
.outerTickSize(0)
.tickPadding(7)
.tickFormat("");
this.magYAxis1 = d3.svg.axis()
.scale(this.magY)
.orient("left")
.ticks(5)
.innerTickSize(-6)
.outerTickSize(0)
.tickPadding(7);
this.magXAxis2 = d3.svg.axis()
.scale(this.x)
.orient("top")
.ticks(5)
.innerTickSize(-6)
.tickPadding(-20)
.outerTickSize(0)
.tickFormat("");
this.magYAxis2 = d3.svg.axis()
.scale(this.magY)
.orient("left")
.ticks(5)
.innerTickSize(6)
.tickPadding(-20)
.outerTickSize(0)
.tickFormat("");
this.magYGrid = d3.svg.axis()
.scale(this.magY)
.orient("left")
.ticks(5)
.tickSize(-width, -width, 0)
.tickFormat("");
this.magLine = d3.svg.line()
.x(function(d) { return that.x(d.x); })
.y(function(d) { return that.magY(d.y); })
.interpolate("linear");
var magZoomXY = d3.behavior.zoom()
.x(this.x)
.y(this.magY)
.on("zoom", this.redraw.bind(this));
var magZoomY = d3.behavior.zoom()
.y(this.magY)
.on("zoom", this.redraw.bind(this));
// Create plot
this.plotMag = d3.select('#'+settings.plotmagID).append("svg")
.attr("width",width + settings.margin.left + settings.margin.right)
.attr("height",height + settings.margin.top + settings.margin.bottom)
.append("g")
.attr("transform","translate(" + settings.margin.left + "," + settings.margin.top + ")");
// Append x grid
this.plotMag.append("g")
.attr("class","x grid")
.attr("transform","translate(0," + height + ")")
.call(this.xGrid);
// Append y grid
this.plotMag.append("g")
.attr("class","y grid")
.call(this.magYGrid);
// Append x axis
this.plotMag.append("g")
.attr("class","x1 axis")
.attr("transform","translate(0," + height + ")")
.call(this.magXAxis1);
// Append additional X axis
this.plotMag.append("g")
.attr("class","x2 axis")
.attr("transform","translate(" + [0, 0] + ")")
.call(this.phsXAxis2);
// Append y axis
this.plotMag.append("g")
.attr("class","y1 axis")
.call(this.magYAxis1);
// Append additional y axis
this.plotMag.append("g")
.attr("class","y2 axis")
.attr("transform","translate(" + [width, 0] + ")")
.call(this.magYAxis2);
// Add y axis label
this.plotMag.append("text")
.attr("transform", "rotate(-90)")
.attr("y",0 - settings.margin.left)
.attr("x",0 - (height / 2))
.attr("dy", "1em")
.style("font-size","15")
.style("text-anchor", "middle")
.text("Magnitude [dB]");
// Clip path
this.plotMag.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
this.plotMag.append("rect")
.attr("width", width)
.attr("height", height);
this.plotMag.append("rect")
.attr("class", "mag zoom xy")
.attr("width", width - settings.margin.left - settings.margin.right)
.attr("height", height - settings.margin.top - settings.margin.bottom)
.style("visibility", "hidden")
.attr("pointer-events", "all")
.call(magZoomXY);
this.plotMag.append("rect")
.attr("class", "mag zoom y")
.attr("width", settings.margin.left)
.attr("height", height - settings.margin.top - settings.margin.bottom)
.attr("transform", "translate(" + -settings.margin.left + "," + 0 + ")")
.style("visibility", "hidden")
.attr("pointer-events", "all")
.call(magZoomY);
this.phsY = d3.scale.linear()
.domain([-45, 45])
.range([height, 0]);
this.phsXAxis1 = d3.svg.axis()
.scale(this.x)
.orient("bottom")
.ticks(1,"0.1s")
.innerTickSize(-6)
.outerTickSize(0)
.tickPadding(7);
this.phsYAxis1 = d3.svg.axis()
.scale(this.phsY)
.orient("left")
.ticks(5)
.innerTickSize(-6)
.outerTickSize(0)
.tickPadding(7);
this.phsXAxis2 = d3.svg.axis()
.scale(this.x)
.orient("top")
.ticks(5)
.innerTickSize(-6)
.tickPadding(-20)
.outerTickSize(0)
.tickFormat("");
this.phsYAxis2 = d3.svg.axis()
.scale(this.phsY)
.orient("left")
.ticks(5)
.innerTickSize(6)
.tickPadding(-20)
.outerTickSize(0)
.tickFormat("");
this.phsYGrid = d3.svg.axis()
.scale(this.phsY)
.orient("left")
.ticks(5)
.tickSize(-width, -width, 0)
.tickFormat("");
this.phsLine = d3.svg.line()
.x(function(d) { return that.x(d.x); })
.y(function(d) { return that.phsY(d.y); })
.interpolate("linear");
var phsZoomXY = d3.behavior.zoom()
.x(this.x)
.y(this.phsY)
.on("zoom", this.redraw.bind(this));
var phsZoomX = d3.behavior.zoom()
.x(this.x)
.on("zoom", this.redraw.bind(this));
var phsZoomY = d3.behavior.zoom()
.y(this.phsY)
.on("zoom", this.redraw.bind(this));
// Create plot
this.plotPhs = d3.select('#'+settings.plotphsID).append("svg")
.attr("width",width + settings.margin.left + settings.margin.right)
.attr("height",height + settings.margin.top + settings.margin.bottom)
.append("g")
.attr("transform","translate(" + settings.margin.left + "," + settings.margin.top + ")");
// Append x grid
this.plotPhs.append("g")
.attr("class","x grid")
.attr("transform","translate(0," + height + ")")
.call(this.xGrid);
// Append y grid
this.plotPhs.append("g")
.attr("class","y grid")
.call(this.phsYGrid);
// Append x axis
this.plotPhs.append("g")
.attr("class","x1 axis")
.attr("transform","translate(0," + height + ")")
.call(this.phsXAxis1);
// Append additional X axis
this.plotPhs.append("g")
.attr("class","x2 axis")
.attr("transform","translate(" + [0, 0] + ")")
.call(this.phsXAxis2);
// Append y axis
this.plotPhs.append("g")
.attr("class","y1 axis")
.call(this.phsYAxis1);
// Append additional y axis
this.plotPhs.append("g")
.attr("class","y2 axis")
.attr("transform","translate(" + [width, 0] + ")")
.call(this.phsYAxis2);
// Add x axis label
this.plotPhs.append("text")
.attr("transform","translate(" + (width / 2) + "," + (height + settings.margin.bottom - 5) + ")")
.style("font-size","15")
.style("text-anchor","middle")
.text("Frequency [Hz]");
// Add y axis label
this.plotPhs.append("text")
.attr("transform", "rotate(-90)")
.attr("y",0 - settings.margin.left)
.attr("x",0 - (height / 2))
.attr("dy", "1em")
.style("font-size","15")
.style("text-anchor", "middle")
.text("Phase [deg]");
// Clip path
this.plotPhs.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
this.plotPhs.append("rect")
.attr("width", width)
.attr("height", height);
this.plotPhs.append("rect")
.attr("class", "phs zoom xy")
.attr("width", width - settings.margin.left - settings.margin.right)
.attr("height", height - settings.margin.top - settings.margin.bottom)
.style("visibility", "hidden")
.attr("pointer-events", "all")
.call(phsZoomXY)
this.plotPhs.append("rect")
.attr("class", "phs zoom x")
.attr("width", width - settings.margin.left - settings.margin.right)
.attr("height", height - settings.margin.top - settings.margin.bottom)
.attr("transform", "translate(" + 0 + "," + (height - settings.margin.top - settings.margin.bottom) + ")")
.style("visibility", "hidden")
.attr("pointer-events", "all")
.call(phsZoomX);
this.plotPhs.append("rect")
.attr("class", "phs zoom y")
.attr("width", settings.margin.left)
.attr("height", height - settings.margin.top - settings.margin.bottom)
.attr("transform", "translate(" + -settings.margin.left + "," + 0 + ")")
.style("visibility", "hidden")
.attr("pointer-events", "all")
.call(phsZoomY);
}
Plot.prototype.redraw = function () {
this.updateAxis();
this.updateZoom();
}
Plot.prototype.updateAxis = function () {
this.plotMag.select(".x1.axis").call(this.magXAxis1);
this.plotMag.select(".y1.axis").call(this.magYAxis1);
this.plotMag.select(".x2.axis").call(this.magXAxis2);
this.plotMag.select(".y2.axis").call(this.magYAxis2);
this.plotMag.select(".x.grid").call(this.xGrid);
this.plotMag.select(".y.grid").call(this.magYGrid);
this.plotPhs.select(".x1.axis").call(this.phsXAxis1);
this.plotPhs.select(".y1.axis").call(this.phsYAxis1);
this.plotPhs.select(".x2.axis").call(this.phsXAxis2);
this.plotPhs.select(".y2.axis").call(this.phsYAxis2);
this.plotPhs.select(".x.grid").call(this.xGrid);
this.plotPhs.select(".y.grid").call(this.phsYGrid);
this.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");
}
});
var seriesMag = this.plotMag.selectAll(".line").data(this.dataMag);
var seriesPhs = this.plotPhs.selectAll(".line").data(this.dataPhs);
seriesMag.enter().append("path");
seriesPhs.enter().append("path");
seriesMag.attr("class","line")
.attr("d",function(d) { return this.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 this.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; });
};
Plot.prototype.updateZoom = function () {
var magZoomXY = d3.behavior.zoom()
.x(this.x)
.y(this.magY)
.on("zoom", this.redraw.bind(this));
var magZoomY = d3.behavior.zoom()
.y(this.magY)
.on("zoom", this.redraw.bind(this));
var phsZoomXY = d3.behavior.zoom()
.x(this.x)
.y(this.phsY)
.on("zoom", this.redraw.bind(this));
var phsZoomX = d3.behavior.zoom()
.x(this.x)
.on("zoom", this.redraw.bind(this));
var phsZoomY = d3.behavior.zoom()
.y(this.phsY)
.on("zoom", this.redraw.bind(this));
this.plotMag.select(".mag.zoom.xy").call(magZoomXY);
this.plotMag.select(".mag.zoom.y").call(magZoomY);
this.plotPhs.select(".phs.zoom.xy").call(phsZoomXY);
this.plotPhs.select(".phs.zoom.x").call(phsZoomX);
this.plotPhs.select(".phs.zoom.y").call(phsZoomY);
};
Plot.prototype.updateData = function (fs, K, fz, fp) {
this.dataMag.length = 0;
this.dataPhs.length = 0;
$.each(this.settings.filters, function(key, filterName) {
var filter = filters[filterName];
if(filter) {
var plotData = filter.calculate(this.range, fs, K, fz, fp);
if(plotData && plotData.mag && plotData.phs) {
this.dataMag.push(plotData.mag);
this.dataPhs.push(plotData.phs);
}
}
});
var refernceline = {data: [{x:fs/2, y:-1000},{x:fs/2, y:1000}], width:1, color:'black', stroke:'5,5', legend:'Sampling' };
this.dataMag.push(refernceline);
this.dataPhs.push(refernceline);
};
// *******************************
// *** fin: Plot() constructor ***
// *******************************
// ***********************************
// *** start: Filter() constructor ***
// ***********************************
function Filter(options) {
this.settings = $.extend(true, {
'legend1': '',
'legend2': '',
'color': 'black',
'plotConstraintFn': function() { return true; },
'filterFn': null,
'plotFn': ''
}, options);
}
Filter.prototype.calculate = function(range, fs, K, fz, fp) {
var filterFn = this.settings.filterFn,
plotConstraintFn = this.settings.plotConstraintFn,
plotFn = plotFunctions[this.settings.plotFn],
filter, rawValue, data1, data2;
if(filterFn && plotConstraintFn && plotFn) {
filter = filterFn(fs, K, 2*math.pi*fz, 2*math.pi*fp); // an {a:[], b:[]} object
data1 = range.map(function(x) {
var rawValue = plotFn(x, fs, filter);
return (plotConstraintFn(x, fs)) ? { x:x, y:mag2db(math.abs(rawValue)) } : false;
}).filter(purgeNulls), // js Array.filter() method
data2 = range.map(function(x) {
var rawValue = plotFn(x, fs, filter);
return (plotConstraintFn(x, fs)) ? { x:x, y:rad2deg(angle(rawValue)) } : false;
}).filter(purgeNulls); // js Array.filter() method
return {
'mag': {data:data1, width:1, color:this.settings.color, stroke:'0,0', legend:this.settings.legend1 },
'phs': {data:data2, width:1, color:this.settings.color, stroke:'0,0', legend:this.settings.legend2 }
};
}
};
// *********************************
// *** fin: Filter() constructor ***
// *********************************
// **********************
// *** start: setters ***
// **********************
function setPlotFn(name, fn) {
plotFunctions[name] = fn;
};
function setFilter(name, filter) {
filters[name] = filter;
};
// ********************
// *** fin: setters ***
// ********************
// *** Plot functions ***
setPlotFn('cleadlag', function(x, fs, filter) {
var s = math.complex(0, 2 * math.pi * x);
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]));
});
setPlotFn('dleadlag', function(x, fs, filter) {
var z = math.exp(math.complex(0, 2 * math.pi * x / 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]));
});
return {
Plot: Plot,
Filter: Filter,
setFilter: setFilter,
setPlotFn: setPlotFn
};
})(jQuery, Math, math, d3);
tested only for js parsing
So that's your BODEPLOT "library".
To use it, you would write something like this :
$(function() {
// *** Filters ***
BODEPLOT.setFilter('continuous', new BODEPLOT.Filter({
'legend1': 'Magnitude',
'legend2': 'Phase',
'color': 'blue',
'plotConstraintFn': function(x, fs) { return true; },
'filterFn': function(fs, K, wz, wp) { return { a:[K*wp*wz, K*wp], b:[wp*wz, wz] }; },
'plotFn': 'cleadlag'
}));
BODEPLOT.setFilter('forwardeuler', new BODEPLOT.Filter({
'legend1': 'Sampling',
'legend2': 'Phase',
'color': 'red',
'plotConstraintFn': function(x, fs) { return x < fs/2; },
'filterFn': function(fs, K, wz, wp) { return { a:[-K*wp, K*wp*(1 + wz/fs)], b:[-wz, wz*(1 + wp/fs)] }; },
'plotFn': 'dleadlag'
}));
BODEPLOT.setFilter('backwardeuler', new BODEPLOT.Filter({
'legend1': 'Sampling',
'legend2': 'Phase',
'color': 'green',
'plotConstraintFn': function(x, fs) { return x < fs/2; },
'filterFn': function(fs, K, wz, wp) { return { a:[K*wp*(wz/fs - 1), K*wp], b:[wz*(wp/fs - 1), wz] }; },
'plotFn': 'dleadlag'
}));
BODEPLOT.setFilter('tustin', new BODEPLOT.Filter({
'legend1': 'Sampling',
'legend2': 'Phase',
'color': 'yellow',
'plotConstraintFn': function(x, fs) { return x < fs/2; },
'filterFn': function(fs, K, wz, wp) { return { a:[K*wp*(wz/fs - 2), K*wp*(2 + wz/fs)], b:[wz*(wp/fs - 2), wz*(2 + wp/fs)] }; },
'plotFn': 'dleadlag'
}));
// create a `Plot()` instance.
var bodePlot = new BODEPLOT.Plot({
'plotmagID': "plotmag",
'plotphsID': "plotphs",
filters: ['continuous', 'forwardeuler', 'backwardeuler', 'tustin']
});
// ***** Attach event handler to DOM elements *****
$("#sliders input").change(function() {
bodePlot.updateData(
$("#slider-fs").val(),
$("#slider-g").val(),
$("#slider-fz").val(),
$("#slider-fp").val()
);
bodePlot.redraw();
}).eq(0).trigger('change');
});
tested only for js parsing
To demonstrate the possibilities, I have defined :
- plot-functions:
cleadlag
anddleadlag
internally. - filters:
continuous
,forwardeuler
,backwardeuler
,tustin
externally.
In practice, you may define both plot-functions and filters either internally or externally.
It's very easy to make mistakes when re-factoring to this extent, so I doubt that my code will work first time. Typical errors will be out-of-scope vars, and this
referring to the wrong object. Happy debugging.
-
\$\begingroup\$ Thank you very much for your tips and advice. I will try to implement this coming weekend \$\endgroup\$WG-– WG-2016年01月07日 22:48:09 +00:00Commented Jan 7, 2016 at 22:48
-
1\$\begingroup\$ Cool, I will be happy to hear how it goes. \$\endgroup\$Roamer-1888– Roamer-18882016年01月08日 00:25:36 +00:00Commented Jan 8, 2016 at 0:25
-
\$\begingroup\$ I will let you know by posting a snippet to this comment ;-) \$\endgroup\$WG-– WG-2016年01月08日 13:52:18 +00:00Commented Jan 8, 2016 at 13:52
-
\$\begingroup\$ I got it working after debugging about half-a-dozen lines (I'll let you have the pleasure of doing it for yourself - error messages in the console help a lot). Performance on my computer was very poor so I tried reducing the number of line segments per curve from 5000, and found 200 to be a good compromise. You can do this with option
'logspace': { 'a':-1, 'b': 4, 'n': 200 },
in thenew BODEPLOT.Plot()
call. \$\endgroup\$Roamer-1888– Roamer-18882016年01月08日 16:59:45 +00:00Commented Jan 8, 2016 at 16:59 -
\$\begingroup\$ Any joy yet ... ? \$\endgroup\$Roamer-1888– Roamer-18882016年01月12日 16:33:19 +00:00Commented Jan 12, 2016 at 16:33
Explore related questions
See similar questions with these tags.