patternjavascriptMinor
Bode plot filters implemented in JavaScript
Viewed 0 times
plotjavascriptimplementedfiltersbode
Problem
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:
Working snippet: http://plnkr.co/edit/5VDht1dbyfJ8IiNNpZbG
```
LOG
.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;
}
Gain:
Freq. zero:
Freq. pole:
Sampling freq.:
function linspace(a,b,n) {
var every = (b-a)/(n-1),
range = [];
for (i = a; i .tick")
.each(function(d,i){
if (d3.select(this).select('text').text() === "") {
d3.selectAll(".x.grid>.tick:nth-chi
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
```
LOG
.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;
}
Gain:
Freq. zero:
Freq. pole:
Sampling freq.:
function linspace(a,b,n) {
var every = (b-a)/(n-1),
range = [];
for (i = a; i .tick")
.each(function(d,i){
if (d3.select(this).select('text').text() === "") {
d3.selectAll(".x.grid>.tick:nth-chi
Solution
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.
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
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 .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
// *
//
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
BODEPLOTnamespace.
- 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 anoptionsobject.
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 .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
// *
//
Code Snippets
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")
$(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');
});Context
StackExchange Code Review Q#115734, answer score: 3
Revisions (0)
No revisions yet.