5
\$\begingroup\$

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:

  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.
  2. 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.
  3. 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>
\$\endgroup\$
3
\$\begingroup\$

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 an options 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 and dleadlag 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.

\$\endgroup\$
8
  • \$\begingroup\$ Thank you very much for your tips and advice. I will try to implement this coming weekend \$\endgroup\$ – WG- Jan 7 '16 at 22:48
  • 1
    \$\begingroup\$ Cool, I will be happy to hear how it goes. \$\endgroup\$ – Roamer-1888 Jan 8 '16 at 0:25
  • \$\begingroup\$ I will let you know by posting a snippet to this comment ;-) \$\endgroup\$ – WG- Jan 8 '16 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 the new BODEPLOT.Plot() call. \$\endgroup\$ – Roamer-1888 Jan 8 '16 at 16:59
  • \$\begingroup\$ Any joy yet ... ? \$\endgroup\$ – Roamer-1888 Jan 12 '16 at 16:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.