MichaelZinsmaier/CurvedLines

Smoothing a graph when there's a drop to zero

Closed this issue · 4 comments

Hi

I'm having the following issue:

I have a dataset which produces following graph.
curvy1
Looks slick with curvedlines!

But the y-axis are percentages, when i set its min value to 0 i get the following curve.
curvy2
The spots where it would head below the x-axis are flatted out. But it doesn't gradually go to zero...

I tried uncommenting the part in the code which did not allow negative values, but without effect. Any other propositions?

Hi,

one option to solve this problem would be to place new "virtual" points on the x axis such that the lines join the axis. However the crucial question in this case is where to place these points. One attempt is to place the points at the intersection of the x axis with a straigth line connecting a negative value with the value before and after.

see below: option: smoothZero and the according method
the result is not perfect but smoother than before.

(function($) {

var options = {
    series : {
        curvedLines : {
            active : false,
            show : false,
            fit : false,
            fill : false,
            fillColor : null,
            lineWidth : 2,
            curvePointFactor : 200,
            fitPointDist : 0.0001,
            smoothZero: true
        }
    }
};

function init(plot) {

    plot.hooks.processOptions.push(processOptions);

    //if the plugin is active register draw method
    function processOptions(plot, options) {
        if (options.series.curvedLines.active) {
            plot.hooks.draw.push(draw);
        }
    }

    //select the data sets that should be drawn with curved lines and draws them
    function draw(plot, ctx) {
        var series;
        var sdata = plot.getData();
        var offset = plot.getPlotOffset();

        for (var i = 0; i < sdata.length; i++) {
            series = sdata[i];
            if (series.curvedLines.show && series.curvedLines.lineWidth > 0) {

                axisx = series.xaxis;
                axisy = series.yaxis;

                ctx.save();
                ctx.translate(offset.left, offset.top);
                ctx.lineJoin = "round";
                ctx.strokeStyle = series.color;
                if (series.curvedLines.fill) {
                    var fillColor = series.curvedLines.fillColor == null ? series.color : series.curvedLines.fillColor;
                    var c = $.color.parse(fillColor);
                    c.a = typeof fill == "number" ? fill : 0.4;
                    c.normalize();
                    ctx.fillStyle = c.toString();
                }
                ctx.lineWidth = series.curvedLines.lineWidth;
                var points;

                //optional smooth out zero passes
                if (series.curvedLines.smoothZero) {
                    points = calculateZeroSmoothedCurvePoints(series.data, series.curvedLines);
                } else {
                    points = calculateCurvePoints(series.data, series.curvedLines);
                }

                var points 
                plotLine(ctx, points, axisx, axisy, series.curvedLines.fill);
                ctx.restore();
            }
        }
    }

    //nearly the same as in the core library
    //only ps is adjusted to 2
    function plotLine(ctx, points, axisx, axisy, fill) {

        var ps = 2;
        var prevx = null;
        var prevy = null;
        var firsty = 0;

        ctx.beginPath();

        for (var i = ps; i < points.length; i += ps) {
            var x1 = points[i - ps], y1 = points[i - ps + 1];
            var x2 = points[i], y2 = points[i + 1];

            if (x1 == null || x2 == null)
                continue;

            // clip with ymin
            if (y1 <= y2 && y1 < axisy.min) {
                if (y2 < axisy.min)
                    continue;
                // line segment is outside
                // compute new intersection point
                x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
                y1 = axisy.min;
            } else if (y2 <= y1 && y2 < axisy.min) {
                if (y1 < axisy.min)
                    continue;
                x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
                y2 = axisy.min;
            }

            // clip with ymax
            if (y1 >= y2 && y1 > axisy.max) {
                if (y2 > axisy.max)
                    continue;
                x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
                y1 = axisy.max;
            } else if (y2 >= y1 && y2 > axisy.max) {
                if (y1 > axisy.max)
                    continue;
                x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
                y2 = axisy.max;
            }

            // clip with xmin
            if (x1 <= x2 && x1 < axisx.min) {
                if (x2 < axisx.min)
                    continue;
                y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
                x1 = axisx.min;
            } else if (x2 <= x1 && x2 < axisx.min) {
                if (x1 < axisx.min)
                    continue;
                y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
                x2 = axisx.min;
            }

            // clip with xmax
            if (x1 >= x2 && x1 > axisx.max) {
                if (x2 > axisx.max)
                    continue;
                y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
                x1 = axisx.max;
            } else if (x2 >= x1 && x2 > axisx.max) {
                if (x1 > axisx.max)
                    continue;
                y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
                x2 = axisx.max;
            }

            if (x1 != prevx || y1 != prevy)
                ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));

            if (prevx == null) {
                firsty = y2;
            }
            prevx = x2;
            prevy = y2;
            ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
        }
        if (fill) {
            ctx.lineTo(axisx.p2c(axisx.max), axisy.p2c(axisy.min));
            ctx.lineTo(axisx.p2c(axisx.min), axisy.p2c(axisy.min));
            ctx.lineTo(axisx.p2c(axisx.min), axisy.p2c(firsty));
            ctx.fill();
        }
        ctx.stroke();
    }

    function calculateZeroSmoothedCurvePoints(data, curvedLinesOptions) {
            var data2 = new Array(new Array, new Array);
            var Y = 1
            var X = 0

            var j = 0;
            for (var i = 0; i < data.length; i++) {

                if (data[i][Y] < 0) {
                    if (i > 0 && data[i-1][Y] > 0) {
                        //point before exists and is over zero
                        var x1 = data[i-1][X];
                        var x2 = data[i][X];
                        var y1 = data[i-1][Y];
                        var y2 = data[i][Y];

                        var newX = ((-y1) / ((y2-y1)/(x2-x1))) + x1;
                        data2[j] = new Array(newX,0);
                        j++;
                    }   
                    data2[j] = new Array(data[i][X],0);
                    j++;                
                    if (i < (data.length-1) && data[i+1][Y] > 0) {
                        //point before exists and is over zero
                        var x1 = data[i][X];
                        var x2 = data[i+1][X];
                        var y1 = data[i][Y];
                        var y2 = data[i+1][Y];

                        var newX = ((-y1) / ((y2-y1)/(x2-x1))) + x1;
                        data2[j] = new Array(newX,0);
                        j++;
                    }                                                   
                } else {
                    data2[j] = data[i];
                    j++;
                }
            }

            return calculateCurvePoints(data2, curvedLinesOptions);         
    }

    //no real idea whats going on here code mainly from https://code.google.com/p/flot/issues/detail?id=226
    //I don't understand what nergal computes here and to be honest I didn't even try
    function calculateCurvePoints(data, curvedLinesOptions) {

        var num = curvedLinesOptions.curvePointFactor * data.length;        
        var xdata = new Array;
        var ydata = new Array;

        if (curvedLinesOptions.fit) {
            //insert a point before and after the "real" data point to force the line
            //to have a max,min at the data point
            var neigh = curvedLinesOptions.fitPointDist;
            var j = 0;

            for (var i = 0; i < data.length; i++) {

                //smooth front
                xdata[j] = data[i][0] - 0.1;

                if (i > 0) {
                    ydata[j] = data[i-1][1] * neigh + data[i][1] * (1 - neigh);
                } else {
                    ydata[j] = data[i][1];
                }
                j++;

                xdata[j] = data[i][0];
                ydata[j] = data[i][1];
                j++;

                //smooth back
                xdata[j] = data[i][0] + 0.1;
                if ((i + 1) < data.length) {
                    ydata[j] = data[i+1][1] * neigh + data[i][1] * (1 - neigh);
                } else {
                    ydata[j] = data[i][1];
                }

                j++;
            }
        } else {
            //just use the datapoints
            for (var i = 0; i < data.length; i++) {
                xdata[i] = data[i][0];
                ydata[i] = data[i][1];
            }
        }

        var n = xdata.length;

        var y2 = new Array();
        var delta = new Array();
        y2[0] = 0;
        y2[n - 1] = 0;
        delta[0] = 0;

        for (var i = 1; i < n - 1; ++i) {
            var d = (xdata[i + 1] - xdata[i - 1]);
            if (d == 0) {
                return null;
            }

            var s = (xdata[i] - xdata[i - 1]) / d;
            var p = s * y2[i - 1] + 2;
            y2[i] = (s - 1) / p;
            delta[i] = (ydata[i + 1] - ydata[i]) / (xdata[i + 1] - xdata[i]) - (ydata[i] - ydata[i - 1]) / (xdata[i] - xdata[i - 1]);
            delta[i] = (6 * delta[i] / (xdata[i + 1] - xdata[i - 1]) - s * delta[i - 1]) / p;
        }

        for (var j = n - 2; j >= 0; --j) {
            y2[j] = y2[j] * y2[j + 1] + delta[j];
        }

        var step = (xdata[n - 1] - xdata[0]) / (num - 1);

        var xnew = new Array;
        var ynew = new Array;
        var result = new Array;

        xnew[0] = xdata[0];
        ynew[0] = ydata[0];

        for ( j = 1; j < num; ++j) {
            xnew[j] = xnew[0] + j * step;

            var max = n - 1;
            var min = 0;

            while (max - min > 1) {
                var k = Math.round((max + min) / 2);
                if (xdata[k] > xnew[j]) {
                    max = k;
                } else {
                    min = k;
                }
            }

            var h = (xdata[max] - xdata[min]);

            if (h == 0) {
                return null;
            }

            var a = (xdata[max] - xnew[j]) / h;
            var b = (xnew[j] - xdata[min]) / h;

            ynew[j] = a * ydata[min] + b * ydata[max] + ((a * a * a - a) * y2[min] + (b * b * b - b) * y2[max]) * (h * h) / 6;
            // if (ynew[j] < 0.01){
            // ynew[j] = 0;
            // }
            result.push(xnew[j]);
            result.push(ynew[j]);
        }

        return result;
    }

}//end init


$.plot.plugins.push({
    init : init,
    options : options,
    name : 'curvedLines',
    version : '0.2'
});

})(jQuery);

Nice idea!
But my datapoint are not located below the x-axis, but right on the x-axis, and then the idea does not work...

example

I've found out that i could set my values to -0,2 instead of 0... Any other ideas?

Hi again,

I think what you are looking for is the "fit" option, it will force the line to have its max, min on real data points and thus avoids overshooting of the curve beyond the x axis => avoids the harsh cuts.

$.plot($("#flotFit"), [{
data : d1,
curvedLines : {
show : true,
fit: true,
fitPointDist : 0.00001
}
}

unfortunately if you do it like this you get a saddle at the 1999 data point. I tried to fix this replace the first part of the if(curvedLinesOption.fit) bracket in curvedLines.js with the following code

if (curvedLinesOptions.fit) {
//insert a point before and after the "real" data point to force the line
//to have a max,min at the data point however only if it is a lowest or highest point of the
//curve => avoid saddles

    var neigh = curvedLinesOptions.fitPointDist;
var j = 0;

for (var i = 0; i < data.length; i++) {
        var front = new Array;
    var back = new Array;

    //smooth front
    front[X] = data[i][X] - 0.1;
    if (i > 0) {
        front[Y] = data[i-1][Y] * neigh + data[i][Y] * (1 - neigh);
    } else {
        front[Y] = data[i][Y];
    }


    //smooth back
    back[X] = data[i][X] + 0.1;
    if ((i + 1) < data.length) {
        back[Y] = data[i+1][Y] * neigh + data[i][Y] * (1 - neigh);
    } else {
        back[Y] = data[i][Y];
    }

    //test for a saddle
    if ((front[Y] <= data[i][Y] && back[Y] <= data[i][Y]) || //max or partial horizontal
       (front[Y] >= data[i][Y] && back[Y] >= data[i][Y])) {  //min or partial horizontal

            //add curve points
        xdata[j] = front[X];
        ydata[j] = front[Y];
        j++;

        xdata[j] = data[i][0];
        ydata[j] = data[i][1];
        j++;

            xdata[j] = back[X];
        ydata[j] = back[Y];
        j++;                        
    } else { //saddle
        //use original point only
        xdata[j] = data[i][0];
        ydata[j] = data[i][1];
        j++;
    }
  }

} else {
...
}

I hope this solves your problem, the "saddle" removal code will be available as branch in some days

regards Michael

looking good! Thanks!