' +
+ this.opts.axisLabel + '');
+ this.plot.getPlaceholder().append(elem);
+ // store height and width of label itself, for use in draw()
+ this.labelWidth = elem.outerWidth(true);
+ this.labelHeight = elem.outerHeight(true);
+ elem.remove();
+
+ this.width = this.height = 0;
+ if (this.position == 'left' || this.position == 'right') {
+ this.width = this.labelWidth + this.padding;
+ } else {
+ this.height = this.labelHeight + this.padding;
+ }
+ };
+
+ HtmlAxisLabel.prototype.cleanup = function() {
+ if (this.elem) {
+ this.elem.remove();
+ }
+ };
+
+ HtmlAxisLabel.prototype.draw = function(box) {
+ this.plot.getPlaceholder().find('#' + this.axisName + 'Label').remove();
+ this.elem = $(''
+ + this.opts.axisLabel + '
');
+ this.plot.getPlaceholder().append(this.elem);
+ if (this.position == 'top') {
+ this.elem.css('left', box.left + box.width/2 - this.labelWidth/2 +
+ 'px');
+ this.elem.css('top', box.top + 'px');
+ } else if (this.position == 'bottom') {
+ this.elem.css('left', box.left + box.width/2 - this.labelWidth/2 +
+ 'px');
+ this.elem.css('top', box.top + box.height - this.labelHeight +
+ 'px');
+ } else if (this.position == 'left') {
+ this.elem.css('top', box.top + box.height/2 - this.labelHeight/2 +
+ 'px');
+ this.elem.css('left', box.left + 'px');
+ } else if (this.position == 'right') {
+ this.elem.css('top', box.top + box.height/2 - this.labelHeight/2 +
+ 'px');
+ this.elem.css('left', box.left + box.width - this.labelWidth +
+ 'px');
+ }
+ };
+
+
+ CssTransformAxisLabel.prototype = new HtmlAxisLabel();
+ CssTransformAxisLabel.prototype.constructor = CssTransformAxisLabel;
+ function CssTransformAxisLabel(axisName, position, padding, plot, opts) {
+ HtmlAxisLabel.prototype.constructor.call(this, axisName, position,
+ padding, plot, opts);
+ }
+
+ CssTransformAxisLabel.prototype.calculateSize = function() {
+ HtmlAxisLabel.prototype.calculateSize.call(this);
+ this.width = this.height = 0;
+ if (this.position == 'left' || this.position == 'right') {
+ this.width = this.labelHeight + this.padding;
+ } else {
+ this.height = this.labelHeight + this.padding;
+ }
+ };
+
+ CssTransformAxisLabel.prototype.transforms = function(degrees, x, y) {
+ var stransforms = {
+ '-moz-transform': '',
+ '-webkit-transform': '',
+ '-o-transform': '',
+ '-ms-transform': ''
+ };
+ if (x != 0 || y != 0) {
+ var stdTranslate = ' translate(' + x + 'px, ' + y + 'px)';
+ stransforms['-moz-transform'] += stdTranslate;
+ stransforms['-webkit-transform'] += stdTranslate;
+ stransforms['-o-transform'] += stdTranslate;
+ stransforms['-ms-transform'] += stdTranslate;
+ }
+ if (degrees != 0) {
+ var rotation = degrees / 90;
+ var stdRotate = ' rotate(' + degrees + 'deg)';
+ stransforms['-moz-transform'] += stdRotate;
+ stransforms['-webkit-transform'] += stdRotate;
+ stransforms['-o-transform'] += stdRotate;
+ stransforms['-ms-transform'] += stdRotate;
+ }
+ var s = 'top: 0; left: 0; ';
+ for (var prop in stransforms) {
+ if (stransforms[prop]) {
+ s += prop + ':' + stransforms[prop] + ';';
+ }
+ }
+ s += ';';
+ return s;
+ };
+
+ CssTransformAxisLabel.prototype.calculateOffsets = function(box) {
+ var offsets = { x: 0, y: 0, degrees: 0 };
+ if (this.position == 'bottom') {
+ offsets.x = box.left + box.width/2 - this.labelWidth/2;
+ offsets.y = box.top + box.height - this.labelHeight;
+ } else if (this.position == 'top') {
+ offsets.x = box.left + box.width/2 - this.labelWidth/2;
+ offsets.y = box.top;
+ } else if (this.position == 'left') {
+ offsets.degrees = -90;
+ offsets.x = box.left - this.labelWidth/2 + this.labelHeight/2;
+ offsets.y = box.height/2 + box.top;
+ } else if (this.position == 'right') {
+ offsets.degrees = 90;
+ offsets.x = box.left + box.width - this.labelWidth/2
+ - this.labelHeight/2;
+ offsets.y = box.height/2 + box.top;
+ }
+ offsets.x = Math.round(offsets.x);
+ offsets.y = Math.round(offsets.y);
+
+ return offsets;
+ };
+
+ CssTransformAxisLabel.prototype.draw = function(box) {
+ this.plot.getPlaceholder().find("." + this.axisName + "Label").remove();
+ var offsets = this.calculateOffsets(box);
+ this.elem = $('' + this.opts.axisLabel + '
');
+ this.plot.getPlaceholder().append(this.elem);
+ };
+
+
+ IeTransformAxisLabel.prototype = new CssTransformAxisLabel();
+ IeTransformAxisLabel.prototype.constructor = IeTransformAxisLabel;
+ function IeTransformAxisLabel(axisName, position, padding, plot, opts) {
+ CssTransformAxisLabel.prototype.constructor.call(this, axisName,
+ position, padding,
+ plot, opts);
+ this.requiresResize = false;
+ }
+
+ IeTransformAxisLabel.prototype.transforms = function(degrees, x, y) {
+ // I didn't feel like learning the crazy Matrix stuff, so this uses
+ // a combination of the rotation transform and CSS positioning.
+ var s = '';
+ if (degrees != 0) {
+ var rotation = degrees/90;
+ while (rotation < 0) {
+ rotation += 4;
+ }
+ s += ' filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=' + rotation + '); ';
+ // see below
+ this.requiresResize = (this.position == 'right');
+ }
+ if (x != 0) {
+ s += 'left: ' + x + 'px; ';
+ }
+ if (y != 0) {
+ s += 'top: ' + y + 'px; ';
+ }
+ return s;
+ };
+
+ IeTransformAxisLabel.prototype.calculateOffsets = function(box) {
+ var offsets = CssTransformAxisLabel.prototype.calculateOffsets.call(
+ this, box);
+ // adjust some values to take into account differences between
+ // CSS and IE rotations.
+ if (this.position == 'top') {
+ // FIXME: not sure why, but placing this exactly at the top causes
+ // the top axis label to flip to the bottom...
+ offsets.y = box.top + 1;
+ } else if (this.position == 'left') {
+ offsets.x = box.left;
+ offsets.y = box.height/2 + box.top - this.labelWidth/2;
+ } else if (this.position == 'right') {
+ offsets.x = box.left + box.width - this.labelHeight;
+ offsets.y = box.height/2 + box.top - this.labelWidth/2;
+ }
+ return offsets;
+ };
+
+ IeTransformAxisLabel.prototype.draw = function(box) {
+ CssTransformAxisLabel.prototype.draw.call(this, box);
+ if (this.requiresResize) {
+ this.elem = this.plot.getPlaceholder().find("." + this.axisName +
+ "Label");
+ // Since we used CSS positioning instead of transforms for
+ // translating the element, and since the positioning is done
+ // before any rotations, we have to reset the width and height
+ // in case the browser wrapped the text (specifically for the
+ // y2axis).
+ this.elem.css('width', this.labelWidth);
+ this.elem.css('height', this.labelHeight);
+ }
+ };
+
+
+ function init(plot) {
+ plot.hooks.processOptions.push(function (plot, options) {
+
+ if (!options.axisLabels.show)
+ return;
+
+ // This is kind of a hack. There are no hooks in Flot between
+ // the creation and measuring of the ticks (setTicks, measureTickLabels
+ // in setupGrid() ) and the drawing of the ticks and plot box
+ // (insertAxisLabels in setupGrid() ).
+ //
+ // Therefore, we use a trick where we run the draw routine twice:
+ // the first time to get the tick measurements, so that we can change
+ // them, and then have it draw it again.
+ var secondPass = false;
+
+ var axisLabels = {};
+ var axisOffsetCounts = { left: 0, right: 0, top: 0, bottom: 0 };
+
+ var defaultPadding = 2; // padding between axis and tick labels
+ plot.hooks.draw.push(function (plot, ctx) {
+ var hasAxisLabels = false;
+ if (!secondPass) {
+ // MEASURE AND SET OPTIONS
+ $.each(plot.getAxes(), function(axisName, axis) {
+ var opts = axis.options // Flot 0.7
+ || plot.getOptions()[axisName]; // Flot 0.6
+
+ // Handle redraws initiated outside of this plug-in.
+ if (axisName in axisLabels) {
+ axis.labelHeight = axis.labelHeight -
+ axisLabels[axisName].height;
+ axis.labelWidth = axis.labelWidth -
+ axisLabels[axisName].width;
+ opts.labelHeight = axis.labelHeight;
+ opts.labelWidth = axis.labelWidth;
+ axisLabels[axisName].cleanup();
+ delete axisLabels[axisName];
+ }
+
+ if (!opts || !opts.axisLabel || !axis.show)
+ return;
+
+ hasAxisLabels = true;
+ var renderer = null;
+
+ if (!opts.axisLabelUseHtml &&
+ navigator.appName == 'Microsoft Internet Explorer') {
+ var ua = navigator.userAgent;
+ var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
+ if (re.exec(ua) != null) {
+ rv = parseFloat(RegExp.$1);
+ }
+ if (rv >= 9 && !opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) {
+ renderer = CssTransformAxisLabel;
+ } else if (!opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) {
+ renderer = IeTransformAxisLabel;
+ } else if (opts.axisLabelUseCanvas) {
+ renderer = CanvasAxisLabel;
+ } else {
+ renderer = HtmlAxisLabel;
+ }
+ } else {
+ if (opts.axisLabelUseHtml || (!css3TransitionSupported() && !canvasTextSupported()) && !opts.axisLabelUseCanvas) {
+ renderer = HtmlAxisLabel;
+ } else if (opts.axisLabelUseCanvas || !css3TransitionSupported()) {
+ renderer = CanvasAxisLabel;
+ } else {
+ renderer = CssTransformAxisLabel;
+ }
+ }
+
+ var padding = opts.axisLabelPadding === undefined ?
+ defaultPadding : opts.axisLabelPadding;
+
+ axisLabels[axisName] = new renderer(axisName,
+ axis.position, padding,
+ plot, opts);
+
+ // flot interprets axis.labelHeight and .labelWidth as
+ // the height and width of the tick labels. We increase
+ // these values to make room for the axis label and
+ // padding.
+
+ axisLabels[axisName].calculateSize();
+
+ // AxisLabel.height and .width are the size of the
+ // axis label and padding.
+ // Just set opts here because axis will be sorted out on
+ // the redraw.
+
+ opts.labelHeight = axis.labelHeight +
+ axisLabels[axisName].height;
+ opts.labelWidth = axis.labelWidth +
+ axisLabels[axisName].width;
+ });
+
+ // If there are axis labels, re-draw with new label widths and
+ // heights.
+
+ if (hasAxisLabels) {
+ secondPass = true;
+ plot.setupGrid();
+ plot.draw();
+ }
+ } else {
+ secondPass = false;
+ // DRAW
+ $.each(plot.getAxes(), function(axisName, axis) {
+ var opts = axis.options // Flot 0.7
+ || plot.getOptions()[axisName]; // Flot 0.6
+ if (!opts || !opts.axisLabel || !axis.show)
+ return;
+
+ axisLabels[axisName].draw(axis.box);
+ });
+ }
+ });
+ });
+ }
+
+
+ $.plot.plugins.push({
+ init: init,
+ options: options,
+ name: 'axisLabels',
+ version: '2.0'
+ });
+})(jQuery);
\ No newline at end of file
diff --git a/app/assets/javascripts/survey/jquery.flot.browser.js b/app/assets/javascripts/survey/jquery.flot.browser.js
new file mode 100644
index 0000000..e50a629
--- /dev/null
+++ b/app/assets/javascripts/survey/jquery.flot.browser.js
@@ -0,0 +1,98 @@
+/** ## jquery.flot.browser.js
+
+This plugin is used to make available some browser-related utility functions.
+
+### Methods
+*/
+
+(function ($) {
+ 'use strict';
+
+ var browser = {
+ /**
+ - getPageXY(e)
+
+ Calculates the pageX and pageY using the screenX, screenY properties of the event
+ and the scrolling of the page. This is needed because the pageX and pageY
+ properties of the event are not correct while running tests in Edge. */
+ getPageXY: function (e) {
+ // This code is inspired from https://stackoverflow.com/a/3464890
+ var doc = document.documentElement,
+ pageX = e.clientX + (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0),
+ pageY = e.clientY + (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
+ return { X: pageX, Y: pageY };
+ },
+
+ /**
+ - getPixelRatio(context)
+
+ This function returns the current pixel ratio defined by the product of desktop
+ zoom and page zoom.
+ Additional info: https://www.html5rocks.com/en/tutorials/canvas/hidpi/
+ */
+ getPixelRatio: function(context) {
+ var devicePixelRatio = window.devicePixelRatio || 1,
+ backingStoreRatio =
+ context.webkitBackingStorePixelRatio ||
+ context.mozBackingStorePixelRatio ||
+ context.msBackingStorePixelRatio ||
+ context.oBackingStorePixelRatio ||
+ context.backingStorePixelRatio || 1;
+ return devicePixelRatio / backingStoreRatio;
+ },
+
+ /**
+ - isSafari, isMobileSafari, isOpera, isFirefox, isIE, isEdge, isChrome, isBlink
+
+ This is a collection of functions, used to check if the code is running in a
+ particular browser or Javascript engine.
+ */
+ isSafari: function() {
+ // *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
+ // Safari 3.0+ "[object HTMLElementConstructor]"
+ return /constructor/i.test(window.top.HTMLElement) || (function (p) { return p.toString() === "[object SafariRemoteNotification]"; })(!window.top['safari'] || (typeof window.top.safari !== 'undefined' && window.top.safari.pushNotification));
+ },
+
+ isMobileSafari: function() {
+ //isMobileSafari adapted from https://stackoverflow.com/questions/3007480/determine-if-user-navigated-from-mobile-safari
+ return navigator.userAgent.match(/(iPod|iPhone|iPad)/) && navigator.userAgent.match(/AppleWebKit/);
+ },
+
+ isOpera: function() {
+ // *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
+ //Opera 8.0+
+ return (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
+ },
+
+ isFirefox: function() {
+ // *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
+ // Firefox 1.0+
+ return typeof InstallTrigger !== 'undefined';
+ },
+
+ isIE: function() {
+ // *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
+ // Internet Explorer 6-11
+ return /*@cc_on!@*/false || !!document.documentMode;
+ },
+
+ isEdge: function() {
+ // *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
+ // Edge 20+
+ return !browser.isIE() && !!window.StyleMedia;
+ },
+
+ isChrome: function() {
+ // *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
+ // Chrome 1+
+ return !!window.chrome && !!window.chrome.webstore;
+ },
+
+ isBlink: function() {
+ // *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
+ return (browser.isChrome() || browser.isOpera()) && !!window.CSS;
+ }
+ };
+
+ $.plot.browser = browser;
+})(jQuery);
diff --git a/app/assets/javascripts/survey/jquery.flot.drawSeries.js b/app/assets/javascripts/survey/jquery.flot.drawSeries.js
new file mode 100644
index 0000000..9016dbe
--- /dev/null
+++ b/app/assets/javascripts/survey/jquery.flot.drawSeries.js
@@ -0,0 +1,703 @@
+/**
+## jquery.flot.drawSeries.js
+
+This plugin is used by flot for drawing lines, plots, bars or area.
+
+### Public methods
+*/
+
+(function($) {
+ "use strict";
+
+ function DrawSeries() {
+ function plotLine(datapoints, xoffset, yoffset, axisx, axisy, ctx, steps) {
+ var points = datapoints.points,
+ ps = datapoints.pointsize,
+ prevx = null,
+ prevy = null;
+ var x1 = 0.0,
+ y1 = 0.0,
+ x2 = 0.0,
+ y2 = 0.0,
+ mx = null,
+ my = null,
+ i = 0;
+ var axisy_is_log = (axisy.options.mode == "log");
+ ctx.beginPath();
+ for (i = ps; i < points.length; i += ps) {
+ x1 = points[i - ps];
+ y1 = points[i - ps + 1];
+ x2 = points[i];
+ y2 = points[i + 1];
+
+ if (x1 === null || x2 === null) {
+ mx = null;
+ my = null;
+ continue;
+ }
+
+ if (isNaN(x1) || isNaN(x2) || isNaN(y1) || isNaN(y2)) {
+ prevx = null;
+ prevy = null;
+ continue;
+ }
+
+ if(steps){
+ if (mx !== null && my !== null) {
+ // if middle point exists, transfer p2 -> p1 and p1 -> mp
+ x2 = x1;
+ y2 = y1;
+ x1 = mx;
+ y1 = my;
+
+ // 'remove' middle point
+ mx = null;
+ my = null;
+
+ // subtract pointsize from i to have current point p1 handled again
+ i -= ps;
+ } else if (y1 !== y2 && x1 !== x2) {
+ // create a middle point
+ y2 = y1;
+ mx = x2;
+ my = y1;
+ }
+ }
+
+ // clip with ymin
+ if (y1 <= y2 && y1 < axisy.min) {
+ if (y2 < axisy.min && !axisy_is_log) {
+ // line segment is outside
+ continue;
+ }
+ // 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 && !axisy_is_log) {
+ 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) {
+ if(y1 < axisy.min && axisy_is_log){
+ y1 = axisy.min;
+ }
+ ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
+ }
+
+ prevx = x2;
+ prevy = y2;
+ if(y2 < axisy.min && axisy_is_log){
+ y2 = axisy.min;
+ }
+ ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
+ }
+ ctx.stroke();
+ }
+
+ function plotLineArea(datapoints, axisx, axisy, fillTowards, ctx, steps) {
+ var points = datapoints.points,
+ ps = datapoints.pointsize,
+ bottom = fillTowards > axisy.min ? Math.min(axisy.max, fillTowards) : axisy.min,
+ i = 0,
+ ypos = 1,
+ areaOpen = false,
+ segmentStart = 0,
+ segmentEnd = 0,
+ mx = null,
+ my = null;
+
+ // we process each segment in two turns, first forward
+ // direction to sketch out top, then once we hit the
+ // end we go backwards to sketch the bottom
+ while (true) {
+ if (ps > 0 && i > points.length + ps) {
+ break;
+ }
+
+ i += ps; // ps is negative if going backwards
+
+ var x1 = points[i - ps],
+ y1 = points[i - ps + ypos],
+ x2 = points[i],
+ y2 = points[i + ypos];
+
+ if (ps === -2) {
+ /* going backwards and no value for the bottom provided in the series*/
+ y1 = y2 = bottom;
+ }
+
+ if (areaOpen) {
+ if (ps > 0 && x1 != null && x2 == null) {
+ // at turning point
+ segmentEnd = i;
+ ps = -ps;
+ ypos = 2;
+ continue;
+ }
+
+ if (ps < 0 && i === segmentStart + ps) {
+ // done with the reverse sweep
+ ctx.fill();
+ areaOpen = false;
+ ps = -ps;
+ ypos = 1;
+ i = segmentStart = segmentEnd + ps;
+ continue;
+ }
+ }
+
+ if (x1 == null || x2 == null) {
+ mx = null;
+ my = null;
+ continue;
+ }
+
+ if(steps){
+ if (mx !== null && my !== null) {
+ // if middle point exists, transfer p2 -> p1 and p1 -> mp
+ x2 = x1;
+ y2 = y1;
+ x1 = mx;
+ y1 = my;
+
+ // 'remove' middle point
+ mx = null;
+ my = null;
+
+ // subtract pointsize from i to have current point p1 handled again
+ i -= ps;
+ } else if (y1 !== y2 && x1 !== x2) {
+ // create a middle point
+ y2 = y1;
+ mx = x2;
+ my = y1;
+ }
+ }
+
+ // clip x values
+
+ // 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 (!areaOpen) {
+ // open area
+ ctx.beginPath();
+ ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
+ areaOpen = true;
+ }
+
+ // now first check the case where both is outside
+ if (y1 >= axisy.max && y2 >= axisy.max) {
+ ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));
+ ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));
+ continue;
+ } else if (y1 <= axisy.min && y2 <= axisy.min) {
+ ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
+ ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
+ continue;
+ }
+
+ // else it's a bit more complicated, there might
+ // be a flat maxed out rectangle first, then a
+ // triangular cutout or reverse; to find these
+ // keep track of the current x values
+ var x1old = x1,
+ x2old = x2;
+
+ // clip the y values, without shortcutting, we
+ // go through all cases in turn
+
+ // clip with ymin
+ if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {
+ x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+ y1 = axisy.min;
+ } else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {
+ x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
+ y2 = axisy.min;
+ }
+
+ // clip with ymax
+ if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {
+ x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+ y1 = axisy.max;
+ } else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {
+ x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
+ y2 = axisy.max;
+ }
+
+ // if the x value was changed we got a rectangle
+ // to fill
+ if (x1 !== x1old) {
+ ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1));
+ // it goes to (x1, y1), but we fill that below
+ }
+
+ // fill triangular section, this sometimes result
+ // in redundant points if (x1, y1) hasn't changed
+ // from previous line to, but we just ignore that
+ ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));
+ ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
+
+ // fill the other rectangle if it's there
+ if (x2 !== x2old) {
+ ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
+ ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2));
+ }
+ }
+ }
+
+
+ function get_sort_indices(arr){
+ return Array.from(Array(arr.length).keys())
+ .sort((a, b) => arr[a] < arr[b] ? -1 : (arr[b] < arr[a]) | 0)
+ }
+ function sortWithIndices(toSort,indices) {
+ return indices.map(i => toSort[i]);
+ }
+ /**
+ - drawSeriesLines(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient)
+
+ This function is used for drawing lines or area fill. In case the series has line decimation function
+ attached, before starting to draw, as an optimization the points will first be decimated.
+
+ The series parameter contains the series to be drawn on ctx context. The plotOffset, plotWidth and
+ plotHeight are the corresponding parameters of flot used to determine the drawing surface.
+ The function getColorOrGradient is used to compute the fill style of lines and area.
+ */
+ function drawSeriesLines(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient) {
+ ctx.save();
+ ctx.translate(plotOffset.left, plotOffset.top);
+ ctx.lineJoin = "round";
+
+ if (series.lines.dashes && ctx.setLineDash) {
+ ctx.setLineDash(series.lines.dashes);
+ }
+
+ var datapoints = {
+ format: series.datapoints.format,
+ points: series.datapoints.points,
+ pointsize: series.datapoints.pointsize
+ };
+
+ if (series.decimate) {
+ datapoints.points = series.decimate(series, series.xaxis.min, series.xaxis.max, plotWidth, series.yaxis.min, series.yaxis.max, plotHeight);
+ }
+
+ var lw = series.lines.lineWidth;
+
+ ctx.lineWidth = lw;
+ ctx.strokeStyle = series.color;
+ var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight, getColorOrGradient);
+ if(series.lines.is_sort){
+
+ var points_group = {};
+ var indice_sort = null;
+ var point_size = datapoints.pointsize;
+ for(var ii = 0;ii < point_size; ii++){
+ var points_tmp = datapoints.points.filter(function(v,i){
+ return i % point_size == ii;
+ })
+ points_group[ii] = points_tmp;
+ if(ii == 0){
+ indice_sort = get_sort_indices(points_tmp);
+ }
+ }
+ Object.keys(points_group).forEach(function(ii){
+ points_group[ii] = sortWithIndices(points_group[ii],indice_sort);
+ })
+ points_tmp = [];
+ var arr_size = points_group[0].length;
+ for(var i = 0;i < arr_size; i++){
+ Object.keys(points_group).forEach(function(ii){
+ points_tmp.push(points_group[ii][i]);
+ })
+ }
+ datapoints.points = points_tmp;
+ }
+ if (fillStyle) {
+ ctx.fillStyle = fillStyle;
+ plotLineArea(datapoints, series.xaxis, series.yaxis, series.lines.fillTowards || 0, ctx, series.lines.steps);
+ }
+
+ if (lw > 0) {
+ plotLine(datapoints, 0, 0, series.xaxis, series.yaxis, ctx, series.lines.steps);
+ }
+
+ ctx.restore();
+ }
+
+ /**
+ - drawSeriesPoints(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient)
+
+ This function is used for drawing points using a given symbol. In case the series has points decimation
+ function attached, before starting to draw, as an optimization the points will first be decimated.
+
+ The series parameter contains the series to be drawn on ctx context. The plotOffset, plotWidth and
+ plotHeight are the corresponding parameters of flot used to determine the drawing surface.
+ The function drawSymbol is used to compute and draw the symbol chosen for the points.
+ */
+ function drawSeriesPoints(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient) {
+ function drawCircle(ctx, x, y, radius, shadow, fill) {
+ ctx.moveTo(x + radius, y);
+ ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);
+ }
+ drawCircle.fill = true;
+ function plotPoints(datapoints, radius, fill, offset, shadow, axisx, axisy, drawSymbolFn) {
+ var points = datapoints.points,
+ ps = datapoints.pointsize;
+
+ ctx.beginPath();
+ for (var i = 0; i < points.length; i += ps) {
+ var x = points[i],
+ y = points[i + 1];
+ if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) {
+ continue;
+ }
+
+ x = axisx.p2c(x);
+ y = axisy.p2c(y) + offset;
+
+ drawSymbolFn(ctx, x, y, radius, shadow, fill);
+ }
+ if (drawSymbolFn.fill && !shadow) {
+ ctx.fill();
+ }
+ ctx.stroke();
+ }
+
+ ctx.save();
+ ctx.translate(plotOffset.left, plotOffset.top);
+
+ var datapoints = {
+ format: series.datapoints.format,
+ points: series.datapoints.points,
+ pointsize: series.datapoints.pointsize
+ };
+
+ if (series.decimatePoints) {
+ datapoints.points = series.decimatePoints(series, series.xaxis.min, series.xaxis.max, plotWidth, series.yaxis.min, series.yaxis.max, plotHeight);
+ }
+
+ var lw = series.points.lineWidth,
+ radius = series.points.radius,
+ symbol = series.points.symbol,
+ drawSymbolFn;
+
+ if (symbol === 'circle') {
+ drawSymbolFn = drawCircle;
+ } else if (typeof symbol === 'string' && drawSymbol && drawSymbol[symbol]) {
+ drawSymbolFn = drawSymbol[symbol];
+ } else if (typeof drawSymbol === 'function') {
+ drawSymbolFn = drawSymbol;
+ }
+
+ // If the user sets the line width to 0, we change it to a very
+ // small value. A line width of 0 seems to force the default of 1.
+
+ if (lw === 0) {
+ lw = 0.0001;
+ }
+
+ ctx.lineWidth = lw;
+ ctx.fillStyle = getFillStyle(series.points, series.color, null, null, getColorOrGradient);
+ ctx.strokeStyle = series.color;
+ plotPoints(datapoints, radius,
+ true, 0, false,
+ series.xaxis, series.yaxis, drawSymbolFn);
+ ctx.restore();
+ }
+
+ function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) {
+ var left = x + barLeft,
+ right = x + barRight,
+ bottom = b, top = y,
+ drawLeft, drawRight, drawTop, drawBottom = false,
+ tmp;
+
+ drawLeft = drawRight = drawTop = true;
+
+ // in horizontal mode, we start the bar from the left
+ // instead of from the bottom so it appears to be
+ // horizontal rather than vertical
+ if (horizontal) {
+ drawBottom = drawRight = drawTop = true;
+ drawLeft = false;
+ left = b;
+ right = x;
+ top = y + barLeft;
+ bottom = y + barRight;
+
+ // account for negative bars
+ if (right < left) {
+ tmp = right;
+ right = left;
+ left = tmp;
+ drawLeft = true;
+ drawRight = false;
+ }
+ }
+ else {
+ drawLeft = drawRight = drawTop = true;
+ drawBottom = false;
+ left = x + barLeft;
+ right = x + barRight;
+ bottom = b;
+ top = y;
+
+ // account for negative bars
+ if (top < bottom) {
+ tmp = top;
+ top = bottom;
+ bottom = tmp;
+ drawBottom = true;
+ drawTop = false;
+ }
+ }
+
+ // clip
+ if (right < axisx.min || left > axisx.max ||
+ top < axisy.min || bottom > axisy.max) {
+ return;
+ }
+
+ if (left < axisx.min) {
+ left = axisx.min;
+ drawLeft = false;
+ }
+
+ if (right > axisx.max) {
+ right = axisx.max;
+ drawRight = false;
+ }
+
+ if (bottom < axisy.min) {
+ bottom = axisy.min;
+ drawBottom = false;
+ }
+
+ if (top > axisy.max) {
+ top = axisy.max;
+ drawTop = false;
+ }
+
+ left = axisx.p2c(left);
+ bottom = axisy.p2c(bottom);
+ right = axisx.p2c(right);
+ top = axisy.p2c(top);
+
+ // fill the bar
+ if (fillStyleCallback) {
+ c.fillStyle = fillStyleCallback(bottom, top);
+ c.fillRect(left, top, right - left, bottom - top)
+ }
+
+ // draw outline
+ if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) {
+ c.beginPath();
+
+ // FIXME: inline moveTo is buggy with excanvas
+ c.moveTo(left, bottom);
+ if (drawLeft) {
+ c.lineTo(left, top);
+ } else {
+ c.moveTo(left, top);
+ }
+
+ if (drawTop) {
+ c.lineTo(right, top);
+ } else {
+ c.moveTo(right, top);
+ }
+
+ if (drawRight) {
+ c.lineTo(right, bottom);
+ } else {
+ c.moveTo(right, bottom);
+ }
+
+ if (drawBottom) {
+ c.lineTo(left, bottom);
+ } else {
+ c.moveTo(left, bottom);
+ }
+
+ c.stroke();
+ }
+ }
+
+ /**
+ - drawSeriesBars(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient)
+
+ This function is used for drawing series represented as bars. In case the series has decimation
+ function attached, before starting to draw, as an optimization the points will first be decimated.
+
+ The series parameter contains the series to be drawn on ctx context. The plotOffset, plotWidth and
+ plotHeight are the corresponding parameters of flot used to determine the drawing surface.
+ The function getColorOrGradient is used to compute the fill style of bars.
+ */
+ function drawSeriesBars(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient) {
+ function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) {
+ var points = datapoints.points,
+ ps = datapoints.pointsize,
+ fillTowards = series.bars.fillTowards || 0,
+ calculatedBottom = fillTowards > axisy.min ? Math.min(axisy.max, fillTowards) : axisy.min;
+
+ for (var i = 0; i < points.length; i += ps) {
+ if (points[i] == null) {
+ continue;
+ }
+
+ // Use third point as bottom if pointsize is 3
+ var bottom = ps === 3 ? points[i + 2] : calculatedBottom;
+ drawBar(points[i], points[i + 1], bottom, barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth);
+ }
+ }
+
+ ctx.save();
+ ctx.translate(plotOffset.left, plotOffset.top);
+
+ var datapoints = {
+ format: series.datapoints.format,
+ points: series.datapoints.points,
+ pointsize: series.datapoints.pointsize
+ };
+
+ if (series.decimate) {
+ datapoints.points = series.decimate(series, series.xaxis.min, series.xaxis.max, plotWidth);
+ }
+
+ ctx.lineWidth = series.bars.lineWidth;
+ ctx.strokeStyle = series.color;
+
+ var barLeft;
+ var barWidth = series.bars.barWidth[0] || series.bars.barWidth;
+ switch (series.bars.align) {
+ case "left":
+ barLeft = 0;
+ break;
+ case "right":
+ barLeft = -barWidth;
+ break;
+ default:
+ barLeft = -barWidth / 2;
+ }
+
+ var fillStyleCallback = series.bars.fill ? function(bottom, top) {
+ return getFillStyle(series.bars, series.color, bottom, top, getColorOrGradient);
+ } : null;
+
+ plotBars(datapoints, barLeft, barLeft + barWidth, fillStyleCallback, series.xaxis, series.yaxis);
+ ctx.restore();
+ }
+
+ function getFillStyle(filloptions, seriesColor, bottom, top, getColorOrGradient) {
+ var fill = filloptions.fill;
+ if (!fill) {
+ return null;
+ }
+
+ if (filloptions.fillColor) {
+ return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
+ }
+
+ var c = $.color.parse(seriesColor);
+ c.a = typeof fill === "number" ? fill : 0.4;
+ c.normalize();
+ return c.toString();
+ }
+
+ this.drawSeriesLines = drawSeriesLines;
+ this.drawSeriesPoints = drawSeriesPoints;
+ this.drawSeriesBars = drawSeriesBars;
+ this.drawBar = drawBar;
+ };
+
+ $.plot.drawSeries = new DrawSeries();
+})(jQuery);
diff --git a/app/assets/javascripts/survey/jquery.flot.hover.js b/app/assets/javascripts/survey/jquery.flot.hover.js
new file mode 100644
index 0000000..2a160a3
--- /dev/null
+++ b/app/assets/javascripts/survey/jquery.flot.hover.js
@@ -0,0 +1,354 @@
+/* global jQuery */
+
+/**
+## jquery.flot.hover.js
+
+This plugin is used for mouse hover and tap on a point of plot series.
+It supports the following options:
+```js
+grid: {
+ hoverable: false, //to trigger plothover event on mouse hover or tap on a point
+ clickable: false //to trigger plotclick event on mouse hover
+}
+```
+
+It listens to native mouse move event or click, as well as artificial generated
+tap and touchevent.
+
+When the mouse is over a point or a tap on a point is performed, that point or
+the correscponding bar will be highlighted and a "plothover" event will be generated.
+
+Custom "touchevent" is triggered when any touch interaction is made. Hover plugin
+handles this events by unhighlighting all of the previously highlighted points and generates
+"plothovercleanup" event to notify any part that is handling plothover (for exemple to cleanup
+the tooltip from webcharts).
+*/
+
+(function($) {
+ 'use strict';
+
+ var options = {
+ grid: {
+ hoverable: false,
+ clickable: false
+ }
+ };
+
+ var browser = $.plot.browser;
+
+ var eventType = {
+ click: 'click',
+ hover: 'hover'
+ }
+
+ function init(plot) {
+ var lastMouseMoveEvent;
+ var highlights = [];
+
+ function bindEvents(plot, eventHolder) {
+ var o = plot.getOptions();
+
+ if (o.grid.hoverable || o.grid.clickable) {
+ eventHolder[0].addEventListener('touchevent', triggerCleanupEvent, false);
+ eventHolder[0].addEventListener('tap', generatePlothoverEvent, false);
+ }
+
+ if (o.grid.clickable) {
+ eventHolder.bind("click", onClick);
+ }
+
+ if (o.grid.hoverable) {
+ eventHolder.bind("mousemove", onMouseMove);
+
+ // Use bind, rather than .mouseleave, because we officially
+ // still support jQuery 1.2.6, which doesn't define a shortcut
+ // for mouseenter or mouseleave. This was a bug/oversight that
+ // was fixed somewhere around 1.3.x. We can return to using
+ // .mouseleave when we drop support for 1.2.6.
+
+ eventHolder.bind("mouseleave", onMouseLeave);
+ }
+ }
+
+ function shutdown(plot, eventHolder) {
+ eventHolder[0].removeEventListener('tap', generatePlothoverEvent);
+ eventHolder[0].removeEventListener('touchevent', triggerCleanupEvent);
+ eventHolder.unbind("mousemove", onMouseMove);
+ eventHolder.unbind("mouseleave", onMouseLeave);
+ eventHolder.unbind("click", onClick);
+ highlights = [];
+ }
+
+
+ function generatePlothoverEvent(e) {
+ var o = plot.getOptions(),
+ newEvent = new CustomEvent('mouseevent');
+
+ //transform from touch event to mouse event format
+ newEvent.pageX = e.detail.changedTouches[0].pageX;
+ newEvent.pageY = e.detail.changedTouches[0].pageY;
+ newEvent.clientX = e.detail.changedTouches[0].clientX;
+ newEvent.clientY = e.detail.changedTouches[0].clientY;
+
+ if (o.grid.hoverable) {
+ doTriggerClickHoverEvent(newEvent, eventType.hover, 30);
+ }
+ return false;
+ }
+
+ function doTriggerClickHoverEvent(event, eventType, searchDistance) {
+ var series = plot.getData();
+ if (event !== undefined
+ && series.length > 0
+ && series[0].xaxis.c2p !== undefined
+ && series[0].yaxis.c2p !== undefined) {
+ var eventToTrigger = "plot" + eventType;
+ var seriesFlag = eventType + "able";
+ triggerClickHoverEvent(eventToTrigger, event,
+ function(i) {
+ return series[i][seriesFlag] !== false;
+ }, searchDistance);
+ }
+ }
+
+ function onMouseMove(e) {
+ lastMouseMoveEvent = e;
+ plot.getPlaceholder()[0].lastMouseMoveEvent = e;
+ doTriggerClickHoverEvent(e, eventType.hover);
+ }
+
+ function onMouseLeave(e) {
+ lastMouseMoveEvent = undefined;
+ plot.getPlaceholder()[0].lastMouseMoveEvent = undefined;
+ triggerClickHoverEvent("plothover", e,
+ function(i) {
+ return false;
+ });
+ }
+
+ function onClick(e) {
+ doTriggerClickHoverEvent(e, eventType.click);
+ }
+
+ function triggerCleanupEvent() {
+ plot.unhighlight();
+ plot.getPlaceholder().trigger('plothovercleanup');
+ }
+
+ // trigger click or hover event (they send the same parameters
+ // so we share their code)
+ function triggerClickHoverEvent(eventname, event, seriesFilter, searchDistance) {
+ var options = plot.getOptions(),
+ offset = plot.offset(),
+ page = browser.getPageXY(event),
+ canvasX = page.X - offset.left,
+ canvasY = page.Y - offset.top,
+ pos = plot.c2p({
+ left: canvasX,
+ top: canvasY
+ }),
+ distance = searchDistance !== undefined ? searchDistance : options.grid.mouseActiveRadius;
+
+ pos.pageX = page.X;
+ pos.pageY = page.Y;
+
+ var item = plot.findNearbyItem(canvasX, canvasY, seriesFilter, distance);
+
+ if (item) {
+ // fill in mouse pos for any listeners out there
+ item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left, 10);
+ if(options.yaxis.mode == "log" && item.datapoint[1] < item.series.yaxis.autoScaledMin){
+ item.pageY = parseInt(item.series.yaxis.p2c(item.series.yaxis.autoScaledMin) + offset.top, 10);
+ }else{
+ item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top, 10);
+ }
+ }
+
+ if (options.grid.autoHighlight) {
+ // clear auto-highlights
+ for (var i = 0; i < highlights.length; ++i) {
+ var h = highlights[i];
+ if ((h.auto === eventname &&
+ !(item && h.series === item.series &&
+ h.point[0] === item.datapoint[0] &&
+ h.point[1] === item.datapoint[1])) || !item) {
+ unhighlight(h.series, h.point);
+ }
+ }
+
+ if (item) {
+ highlight(item.series, item.datapoint, eventname);
+ }
+ }
+
+ plot.getPlaceholder().trigger(eventname, [pos, item]);
+ }
+
+ function highlight(s, point, auto) {
+ if (typeof s === "number") {
+ s = plot.getData()[s];
+ }
+
+ if (typeof point === "number") {
+ var ps = s.datapoints.pointsize;
+ point = s.datapoints.points.slice(ps * point, ps * (point + 1));
+ }
+
+ var i = indexOfHighlight(s, point);
+ if (i === -1) {
+ highlights.push({
+ series: s,
+ point: point,
+ auto: auto
+ });
+
+ plot.triggerRedrawOverlay();
+ } else if (!auto) {
+ highlights[i].auto = false;
+ }
+ }
+
+ function unhighlight(s, point) {
+ if (s == null && point == null) {
+ highlights = [];
+ plot.triggerRedrawOverlay();
+ return;
+ }
+
+ if (typeof s === "number") {
+ s = plot.getData()[s];
+ }
+
+ if (typeof point === "number") {
+ var ps = s.datapoints.pointsize;
+ point = s.datapoints.points.slice(ps * point, ps * (point + 1));
+ }
+
+ var i = indexOfHighlight(s, point);
+ if (i !== -1) {
+ highlights.splice(i, 1);
+
+ plot.triggerRedrawOverlay();
+ }
+ }
+
+ function indexOfHighlight(s, p) {
+ for (var i = 0; i < highlights.length; ++i) {
+ var h = highlights[i];
+ if (h.series === s &&
+ h.point[0] === p[0] &&
+ h.point[1] === p[1]) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ function processDatapoints() {
+ triggerCleanupEvent();
+ doTriggerClickHoverEvent(lastMouseMoveEvent, eventType.hover);
+ }
+
+ function setupGrid() {
+ doTriggerClickHoverEvent(lastMouseMoveEvent, eventType.hover);
+ }
+
+ function drawOverlay(plot, octx, overlay) {
+ var plotOffset = plot.getPlotOffset(),
+ i, hi;
+
+ octx.save();
+ octx.translate(plotOffset.left, plotOffset.top);
+ for (i = 0; i < highlights.length; ++i) {
+ hi = highlights[i];
+
+ if (hi.series.bars.show) drawBarHighlight(hi.series, hi.point, octx);
+ else drawPointHighlight(hi.series, hi.point, octx, plot);
+ }
+ octx.restore();
+ }
+
+ function drawPointHighlight(series, point, octx, plot) {
+ var x = point[0],
+ y = point[1],
+ axisx = series.xaxis,
+ axisy = series.yaxis,
+ highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString();
+
+ if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) {
+ return;
+ }
+
+ var pointRadius = series.points.radius + series.points.lineWidth / 2;
+ octx.lineWidth = pointRadius;
+ octx.strokeStyle = highlightColor;
+ var radius = 1.5 * pointRadius;
+ x = axisx.p2c(x);
+ y = axisy.p2c(y);
+
+ octx.beginPath();
+ var symbol = series.points.symbol;
+ if (symbol === 'circle') {
+ octx.arc(x, y, radius, 0, 2 * Math.PI, false);
+ } else if (typeof symbol === 'string' && plot.drawSymbol && plot.drawSymbol[symbol]) {
+ plot.drawSymbol[symbol](octx, x, y, radius, false);
+ }
+
+ octx.closePath();
+ octx.stroke();
+ }
+
+ function drawBarHighlight(series, point, octx) {
+ var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(),
+ fillStyle = highlightColor,
+ barLeft;
+
+ var barWidth = series.bars.barWidth[0] || series.bars.barWidth;
+ switch (series.bars.align) {
+ case "left":
+ barLeft = 0;
+ break;
+ case "right":
+ barLeft = -barWidth;
+ break;
+ default:
+ barLeft = -barWidth / 2;
+ }
+
+ octx.lineWidth = series.bars.lineWidth;
+ octx.strokeStyle = highlightColor;
+
+ var fillTowards = series.bars.fillTowards || 0,
+ bottom = fillTowards > series.yaxis.min ? Math.min(series.yaxis.max, fillTowards) : series.yaxis.min;
+
+ $.plot.drawSeries.drawBar(point[0], point[1], point[2] || bottom, barLeft, barLeft + barWidth,
+ function() {
+ return fillStyle;
+ }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth);
+ }
+
+ function initHover(plot, options) {
+ plot.highlight = highlight;
+ plot.unhighlight = unhighlight;
+ if (options.grid.hoverable || options.grid.clickable) {
+ plot.hooks.drawOverlay.push(drawOverlay);
+ plot.hooks.processDatapoints.push(processDatapoints);
+ plot.hooks.setupGrid.push(setupGrid);
+ }
+
+ lastMouseMoveEvent = plot.getPlaceholder()[0].lastMouseMoveEvent;
+ }
+
+ plot.hooks.bindEvents.push(bindEvents);
+ plot.hooks.shutdown.push(shutdown);
+ plot.hooks.processOptions.push(initHover);
+ }
+
+ $.plot.plugins.push({
+ init: init,
+ options: options,
+ name: 'hover',
+ version: '0.1'
+ });
+})(jQuery);
diff --git a/app/assets/javascripts/survey/jquery.flot.legend.js b/app/assets/javascripts/survey/jquery.flot.legend.js
new file mode 100644
index 0000000..32741d7
--- /dev/null
+++ b/app/assets/javascripts/survey/jquery.flot.legend.js
@@ -0,0 +1,405 @@
+/* Flot plugin for drawing legends.
+
+*/
+
+(function($) {
+ var defaultOptions = {
+ legend: {
+ show: false,
+ noColumns: 1,
+ labelFormatter: null, // fn: string -> string
+ container: null, // container (as jQuery object) to put legend in, null means default on top of graph
+ position: 'ne', // position of default legend container within plot
+ margin: 5, // distance from grid edge to default legend container within plot
+ sorted: null // default to no legend sorting
+ }
+ };
+ var series = [];
+ function insertLegend() {
+ if (options.legend.container != null) {
+ $(options.legend.container).html("");
+ } else {
+ placeholder.find(".legend").remove();
+ }
+
+ if (!options.legend.show) {
+ return;
+ }
+
+ var fragments = [], entries = [], rowStarted = false,
+ lf = options.legend.labelFormatter, s, label;
+
+ // Build a list of legend entries, with each having a label and a color
+
+ for (var i = 0; i < series.length; ++i) {
+ s = series[i];
+ if (s.label) {
+ label = lf ? lf(s.label, s) : s.label;
+ if (label) {
+ entries.push({
+ label: label,
+ color: s.color
+ });
+ }
+ }
+ }
+
+ // Sort the legend using either the default or a custom comparator
+
+ if (options.legend.sorted) {
+ if ($.isFunction(options.legend.sorted)) {
+ entries.sort(options.legend.sorted);
+ } else if (options.legend.sorted == "reverse") {
+ entries.reverse();
+ } else {
+ var ascending = options.legend.sorted != "descending";
+ entries.sort(function(a, b) {
+ return a.label == b.label ? 0 : (
+ (a.label < b.label) != ascending ? 1 : -1 // Logical XOR
+ );
+ });
+ }
+ }
+
+ // Generate markup for the list of entries, in their final order
+
+ for (var i = 0; i < entries.length; ++i) {
+
+ var entry = entries[i];
+
+ if (i % options.legend.noColumns == 0) {
+ if (rowStarted)
+ fragments.push('');
+ fragments.push('');
+ rowStarted = true;
+ }
+
+ fragments.push(
+ ' | ' +
+ '' + entry.label + ' | '
+ );
+ }
+
+ if (rowStarted)
+ fragments.push('
');
+
+ if (fragments.length == 0)
+ return;
+
+ var table = '