diff --git a/curvedLines.js b/curvedLines.js index fae118f..1ad40f9 100644 --- a/curvedLines.js +++ b/curvedLines.js @@ -1,6 +1,7 @@ /* The MIT License Copyright (c) 2011 by Michael Zinsmaier and nergal.dev + Copyright (c) 2012 by Thomas Ritou Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -21,397 +22,322 @@ THE SOFTWARE. */ -/* + /* - ____________________________________________________ + ____________________________________________________ - what it is: - ____________________________________________________ + what it is: + ____________________________________________________ - curvedLines is a plugin for flot, that tries to display lines in a smoother way. - The plugin is based on nergal.dev's work https://code.google.com/p/flot/issues/detail?id=226 - and further extended with a mode that forces the min/max points of the curves to be on the - points. Both modes are achieved through adding of more data points - => 1) with large data sets you may get trouble - => 2) if you want to display the points too, you have to plot them as 2nd data series over the lines + curvedLines is a plugin for flot, that tries to display lines in a smoother way. + The plugin is based on nergal.dev's work https://code.google.com/p/flot/issues/detail?id=226 + and further extended with a mode that forces the min/max points of the curves to be on the + points. Both modes are achieved through adding of more data points + => 1) with large data sets you may get trouble + => 2) if you want to display the points too, you have to plot them as 2nd data series over the lines - This is version 0.3 of curvedLines so it will probably not work in every case. However - the basic form of use descirbed next works (: - Feel free to further improve the code + This is version 0.5 of curvedLines so it will probably not work in every case. However + the basic form of use descirbed next works (: - ____________________________________________________ + Feel free to further improve the code - how to use it: - ____________________________________________________ + ____________________________________________________ - var d1 = [[5,5],[7,3],[9,12]]; + how to use it: + ____________________________________________________ - var options = { series: { curvedLines: { active: true }}}; + var d1 = [[5,5],[7,3],[9,12]]; - $.plot($("#placeholder"), [{data = d1, curvedLines: { show: true}}], options); - _____________________________________________________ + var options = { series: { curvedLines: { active: true }}}; - options: - _____________________________________________________ + $.plot($("#placeholder"), [{data = d1, lines: { show: true}, curvedLines: {apply: true}}], options); - fill: bool true => lines get filled - fillColor: null or the color that should be used for filling - active: bool true => plugin can be used - show: bool true => series will be drawn as curved line - fit: bool true => forces the max,mins of the curve to be on the datapoints - lineWidth: int width of the line - curvePointFactor int defines how many "virtual" points are used per "real" data point to - emulate the curvedLines - fitPointDist: int defines the x axis distance of the additional two points that are used - to enforce the min max condition. (you will get curvePointFactor * 3 * |datapoints| - "virtual" points if fit is true) - */ + _____________________________________________________ -/* - * v0.1 initial commit - * v0.15 negative values should work now (outcommented a negative -> 0 hook hope it does no harm) - * v0.2 added fill option (thanks to monemihir) and multi axis support (thanks to soewono effendi) - * v0.3 improved saddle handling and added basic handling of Dates - * - */ + options: + _____________________________________________________ -(function($) { - - var options = { - series : { - curvedLines : { - active : false, - show : false, - fit : false, - fill : false, - fillColor : null, - lineWidth : 2, - curvePointFactor : 20, - fitPointDist : 0.0001 - } - } - }; + active: bool true => plugin can be used + apply: bool true => series will be drawn as curved line + fit: bool true => forces the max,mins of the curve to be on the datapoints + curvePointFactor int defines how many "virtual" points are used per "real" data point to + emulate the curvedLines + fitPointDist: int defines the x axis distance of the additional two points that are used + to enforce the min max condition. (you will get curvePointFactor * 3 * |datapoints| + "virtual" points if fit is true) + + + line options (since v0.5 curved lines use flots line implementation for drawing + => line options like fill, show ... are supported out of the box) - function init(plot) { + */ - plot.hooks.processOptions.push(processOptions); + /* + * v0.1 initial commit + * v0.15 negative values should work now (outcommented a negative -> 0 hook hope it does no harm) + * v0.2 added fill option (thanks to monemihir) and multi axis support (thanks to soewono effendi) + * v0.3 improved saddle handling and added basic handling of Dates + * v0.4 rewritten fill option (thomas ritou) mostly from original flot code (now fill between points rather than to graph bottom), corrected fill Opacity bug + * v0.5 rewritten instead of implementing a own draw function CurvedLines is now based on the processDatapoints flot hook (credits go to thomas ritou). + * This change breakes existing code however CurvedLines are now just many tiny straight lines to flot and therefore all flot lines options (like gradient fill, + * shadow) are now supported out of the box + */ - //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 fill = 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, dataX, dataY, data; - - //convenience check for x or y values if they are Dates if so apply .getTime() - //only check on first value mixing numeric and Date fields in one input array is not allowed - if (series.data[0][0] instanceof Date || series.data[0][1] instanceof Date) { - data = series.data.map(getTimeFromDate); - } else { - //default case - data = series.data; - } + (function($) { - var points = calculateCurvePoints(data, series.curvedLines); - plotLine(ctx, points, axisx, axisy, series.curvedLines.fill); - ctx.restore(); + var options = { + series : { + curvedLines : { + active : false, + apply: false, + fit : false, + curvePointFactor : 20, + fitPointDist : 0.0001 } } - } - - //helper method that convertes Dates to a numeric representation - function getTimeFromDate(timeElement) { - var xVal = timeElement[0]; - var yVal = timeElement[1]; - var ret = new Array; - - if (timeElement[0] instanceof Date) { - ret[0] = xVal.getTime(); - } else { - ret[0] = xVal; - } + }; - if (timeElement[1] instanceof Date) { - ret[1] = yVal.getTime(); - } else { - ret[1] = yVal; - } - - return ret; - } - - //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; - } + function init(plot) { - // 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; - } + plot.hooks.processOptions.push(processOptions); - // 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; - } + //if the plugin is active register processDatapoints method + function processOptions(plot, options) { - // 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 (options.series.curvedLines.active) { + plot.hooks.processDatapoints.push(processDatapoints); } + } - if (x1 != prevx || y1 != prevy) - ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); - - if (prevx == null) { - firsty = y2; + //only if the plugin is active + function processDatapoints(plot, series, datapoints) { + if (series.curvedLines.apply == true) { + if (series.lines.fill) { + + var pointsTop = calculateCurvePoints(datapoints, series.curvedLines, 1) + ,pointsBottom = calculateCurvePoints(datapoints, series.curvedLines, 2); //flot makes sur for us that we've got a second y point if fill is true ! + + //Merge top and bottom curve + datapoints.pointsize = 3; + datapoints.points = []; + var j = 0; + var k = 0; + var i = 0; + var ps = 2; + while (i < pointsTop.length || j < pointsBottom.length) { + if (pointsTop[i] == pointsBottom[j]) { + datapoints.points[k] = pointsTop[i]; + datapoints.points[k + 1] = pointsTop[i + 1]; + datapoints.points[k + 2] = pointsBottom[j + 1]; + j += ps; + i += ps; + + } else if (pointsTop[i] < pointsBottom[j]) { + datapoints.points[k] = pointsTop[i]; + datapoints.points[k + 1] = pointsTop[i + 1]; + datapoints.points[k + 2] = k > 0 ? datapoints.points[k-1] : null; + i += ps; + } else { + datapoints.points[k] = pointsBottom[j]; + datapoints.points[k + 1] = k > 1 ? datapoints.points[k-2] : null; + datapoints.points[k + 2] = pointsBottom[j + 1]; + j += ps; + } + k += 3; + } + + if (series.lines.lineWidth > 0) {//Let's draw line in separate series + var newSerie = $.extend({}, series); + newSerie.lines = $.extend({}, series.lines); + newSerie.lines.fill = undefined; + newSerie.label = undefined; + newSerie.datapoints = {}; + //Redefine datapoints to top only (else it can have null values which will open the cruve !) + newSerie.datapoints.points = pointsTop; + newSerie.datapoints.pointsize = 2; + newSerie.curvedLines.apply = false; + //Don't redo curve point calculation as datapoint is copied to this new serie + //We find our series to add the line just after the fill (so other series you wanted above this one will still be) + var allSeries = plot.getData(); + for ( i = 0; i < allSeries.length; i++) { + if (allSeries[i] == series) { + plot.getData().splice(i + 1, 0, newSerie); + break; + } + } + + series.lines.lineWidth = 0; + } + + } else if (series.lines.lineWidth > 0) { + datapoints.points = calculateCurvePoints(datapoints, series.curvedLines, 1); + datapoints.pointsize = 2; + } } - 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(); - } //no real idea whats going on here code mainly from https://code.google.com/p/flot/issues/detail?id=226 //if fit option is selected additional datapoints get inserted before the curve calculations in nergal.dev s code. - function calculateCurvePoints(data, curvedLinesOptions) { - - var num = curvedLinesOptions.curvePointFactor * data.length; - var xdata = new Array; - var ydata = new Array; - - var X = 0; - var Y = 1; - - 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; + function calculateCurvePoints(datapoints, curvedLinesOptions, yPos) { - for (var i = 0; i < data.length; i++) { + var points = datapoints.points, ps = datapoints.pointsize; + var num = curvedLinesOptions.curvePointFactor * (points.length / ps); - var front = new Array; - var back = new Array; + var xdata = new Array; + var ydata = 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]; - } + var X = 0; + var Y = yPos; + + var curX = -1; + var curY = -1; + var j = 0; + 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 fpDist = curvedLinesOptions.fitPointDist; + + for (var i = 0; i < points.length; i += ps) { + + var front = new Array; + var back = new Array; + curX = i; + curY = i + yPos; + + //add point to front + front[X] = points[curX] - fpDist; + front[Y] = points[curY]; + + //add point to back + back[X] = points[curX] + fpDist; + back[Y] = points[curY]; + + + //get points (front and back) Y value for saddle test + var frontPointY = points[curY]; + var backPointY = points[curY]; + if (i >= ps) { + frontPointY = points[curY - ps]; + } + if ((i + ps) < points.length) { + backPointY = points[curY + ps]; + } + + //test for a saddle + if ((frontPointY <= points[curY] && backPointY <= points[curY]) || //max or partial horizontal + (frontPointY >= points[curY] && backPointY >= points[curY])) {//min or partial horizontal - //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]; + + xdata[j] = points[curX]; + ydata[j] = points[curY]; 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 {//saddle + //use original point only + xdata[j] = points[curX]; + ydata[j] = points[curY]; + j++; + } + + } + } else { + //just use the datapoints + for (var i = 0; i < points.length; i += ps) { + curX = i; + curY = i + yPos; + + xdata[j] = points[curX]; + ydata[j] = points[curY]; j++; } + } + var n = xdata.length; - } - } else { - //just use the datapoints - for (var i = 0; i < data.length; i++) { - xdata[i] = data[i][0]; - ydata[i] = data[i][1]; - } - } + var y2 = new Array(); + var delta = new Array(); + y2[0] = 0; + y2[n - 1] = 0; + delta[0] = 0; - var n = xdata.length; + for (var i = 1; i < n - 1; ++i) { + var d = (xdata[i + 1] - xdata[i - 1]); + if (d == 0) { + return null; + } - var y2 = new Array(); - var delta = new Array(); - y2[0] = 0; - y2[n - 1] = 0; - delta[0] = 0; + 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 i = 1; i < n - 1; ++i) { - var d = (xdata[i + 1] - xdata[i - 1]); - if (d == 0) { - return null; + for (var j = n - 2; j >= 0; --j) { + y2[j] = y2[j] * y2[j + 1] + delta[j]; } - 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; - } + var step = (xdata[n - 1] - xdata[0]) / (num - 1); - for (var j = n - 2; j >= 0; --j) { - y2[j] = y2[j] * y2[j + 1] + delta[j]; - } + var xnew = new Array; + var ynew = new Array; + var result = new Array; - var step = (xdata[n - 1] - xdata[0]) / (num - 1); + xnew[0] = xdata[0]; + ynew[0] = ydata[0]; - var xnew = new Array; - var ynew = new Array; - var result = new Array; + result.push(xnew[0]); + result.push(ynew[0]); - xnew[0] = xdata[0]; - ynew[0] = ydata[0]; + for ( j = 1; j < num; ++j) { + xnew[j] = xnew[0] + j * step; - result.push(xnew[0]); - result.push(ynew[0]); + var max = n - 1; + var min = 0; - for ( j = 1; j < num; ++j) { - xnew[j] = xnew[0] + j * step; + while (max - min > 1) { + var k = Math.round((max + min) / 2); + if (xdata[k] > xnew[j]) { + max = k; + } else { + min = k; + } + } - var max = n - 1; - var min = 0; + var h = (xdata[max] - xdata[min]); - while (max - min > 1) { - var k = Math.round((max + min) / 2); - if (xdata[k] > xnew[j]) { - max = k; - } else { - min = k; + if (h == 0) { + return null; } - } - var h = (xdata[max] - xdata[min]); + var a = (xdata[max] - xnew[j]) / h; + var b = (xnew[j] - xdata[min]) / h; - if (h == 0) { - return null; + ynew[j] = a * ydata[min] + b * ydata[max] + ((a * a * a - a) * y2[min] + (b * b * b - b) * y2[max]) * (h * h) / 6; + + result.push(xnew[j]); + result.push(ynew[j]); } - 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; } - return result; - } - - }//end init + }//end init + $.plot.plugins.push({ + init : init, + options : options, + name : 'curvedLines', + version : '0.5' + }); - $.plot.plugins.push({ - init : init, - options : options, - name : 'curvedLines', - version : '0.3' - }); + })(jQuery); -})(jQuery); diff --git a/example.js b/example.js index a102006..91126d9 100644 --- a/example.js +++ b/example.js @@ -1,8 +1,8 @@ -
- - \ No newline at end of file + $.plot($("#flotOrig"), [{data: d1, lines: { show: true, lineWidth: 3}, curvedLines: {apply:true}}, {data: d1, points: { show: true }}], options); +}); \ No newline at end of file diff --git a/exampleFillMultiAxis.js b/exampleFillMultiAxis.js index f63ada2..39e1c38 100644 --- a/exampleFillMultiAxis.js +++ b/exampleFillMultiAxis.js @@ -1,8 +1,7 @@ - - - \ No newline at end of file + }); \ No newline at end of file diff --git a/exampleFit.js b/exampleFit.js index fc37d66..bfe50ee 100644 --- a/exampleFit.js +++ b/exampleFit.js @@ -1,7 +1,7 @@ - - - \ No newline at end of file + $.plot($("#flotFit"), [{data: d1, lines: { show: true, lineWidth: 3}, curvedLines: {apply:true, fit: true, fitPointDist: 0.000001}}, {data: d1, points: { show: true }}], options); +}); \ No newline at end of file