1691 lines
164 KiB
JavaScript
1691 lines
164 KiB
JavaScript
|
"use strict";function _typeof(t){return(_typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function _typeof(t){return(_typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}!function(r){var t=function(t,e){var n=e.getElementsByClassName(t)[0];if(!n&&((n=document.createElement("canvas")).className=t,n.style.direction="ltr",n.style.position="absolute",n.style.left="0px",n.style.top="0px",e.appendChild(n),!n.getContext))throw new Error("Canvas is not available.");this.element=n;var o=this.context=n.getContext("2d");this.pixelRatio=r.plot.browser.getPixelRatio(o);var i=r(e).width(),a=r(e).height();this.resize(i,a),this.SVGContainer=null,this.SVG={},this._textCache={}};function f(e,t){e.transform.baseVal.clear(),t&&t.forEach(function(t){e.transform.baseVal.appendItem(t)})}t.prototype.resize=function(t,e){t=t<10?10:t,e=e<10?10:e;var n=this.element,o=this.context,i=this.pixelRatio;this.width!==t&&(n.width=t*i,n.style.width=t+"px",this.width=t),this.height!==e&&(n.height=e*i,n.style.height=e+"px",this.height=e),o.restore(),o.save(),o.scale(i,i)},t.prototype.clear=function(){this.context.clearRect(0,0,this.width,this.height)},t.prototype.render=function(){var t=this._textCache;for(var e in t)if(hasOwnProperty.call(t,e)){var n=this.getSVGLayer(e),o=t[e],i=n.style.display;for(var a in n.style.display="none",o)if(hasOwnProperty.call(o,a)){var r=o[a];for(var s in r)if(hasOwnProperty.call(r,s)){for(var l,c=r[s],u=c.positions,p=0;u[p];p++)if((l=u[p]).active)l.rendered||(n.appendChild(l.element),l.rendered=!0);else if(u.splice(p--,1),l.rendered){for(;l.element.firstChild;)l.element.removeChild(l.element.firstChild);l.element.parentNode.removeChild(l.element)}0===u.length&&(c.measured?c.measured=!1:delete r[s])}}n.style.display=i}},t.prototype.getSVGLayer=function(t){var e,n=this.SVG[t];n||(this.SVGContainer?e=this.SVGContainer.firstChild:(this.SVGContainer=document.createElement("div"),this.SVGContainer.className="flot-svg",this.SVGContainer.style.position="absolute",this.SVGContainer.style.top="0px",this.SVGContainer.style.left="0px",this.SVGContainer.style.height="100%",this.SVGContainer.style.width="100%",this.SVGContainer.style.pointerEvents="none",this.element.parentNode.appendChild(this.SVGContainer),(e=document.createElementNS("http://www.w3.org/2000/svg","svg")).style.width="100%",e.style.height="100%",this.SVGContainer.appendChild(e)),(n=document.createElementNS("http://www.w3.org/2000/svg","g")).setAttribute("class",t),n.style.position="absolute",n.style.top="0px",n.style.left="0px",n.style.bottom="0px",n.style.right="0px",e.appendChild(n),this.SVG[t]=n);return n},t.prototype.getTextInfo=function(t,e,n,o,i){var a,r,s,l;e=""+e,a="object"===_typeof(n)?n.style+" "+n.variant+" "+n.weight+" "+n.size+"px/"+n.lineHeight+"px "+n.family:n,null==(r=this._textCache[t])&&(r=this._textCache[t]={}),null==(s=r[a])&&(s=r[a]={});var c=e.replace(/0|1|2|3|4|5|6|7|8|9/g,"0");if(!(l=s[c])){var u=document.createElementNS("http://www.w3.org/2000/svg","text");if(-1!==e.indexOf("<br>"))m(e,u,-9999);else{var p=document.createTextNode(e);u.appendChild(p)}u.style.position="absolute",u.style.maxWidth=i,u.setAttributeNS(null,"x",-9999),u.setAttributeNS(null,"y",-9999),"object"===_typeof(n)?(u.style.font=a,u.style.fill=n.fill):"string"==typeof n&&u.setAttribute("class",n),this.getSVGLayer(t).appendChild(u);var h=u.getBBox();for(l=s[c]={width:h.width,height:h.height,measured:!0,element:u,positions:[]};u.firstChild;)u.removeChild(u.firstChild);u.parentNode.removeChild(u)}return l.measured=!0,l},t.prototype.addText=function(t,e,n,o,i,a,r,s,l,c){var u=this.getTextInfo(t,o,i,a,r),p=u.positions;"center"===s?e-=u.width/2:"right"===s&&(e-=u.width),"middle"===l?n-=u.height/2:"bottom"===l&&(n-=u.height),n+=.75*u.height;for(var h,d=0;p[d];d++){if((h=p
|
||
|
//# sourceMappingURL=jquery.flot.js.map
|
||
|
|
||
|
/* eslint-disable */
|
||
|
/* Flot plugin for automatically redrawing plots as the placeholder resizes.
|
||
|
|
||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||
|
Licensed under the MIT license.
|
||
|
|
||
|
It works by listening for changes on the placeholder div (through the jQuery
|
||
|
resize event plugin) - if the size changes, it will redraw the plot.
|
||
|
|
||
|
There are no options. If you need to disable the plugin for some plots, you
|
||
|
can just fix the size of their placeholders.
|
||
|
|
||
|
*/
|
||
|
|
||
|
/* Inline dependency:
|
||
|
* jQuery resize event - v1.1 - 3/14/2010
|
||
|
* http://benalman.com/projects/jquery-resize-plugin/
|
||
|
*
|
||
|
* Copyright (c) 2010 "Cowboy" Ben Alman
|
||
|
* Dual licensed under the MIT and GPL licenses.
|
||
|
* http://benalman.com/about/license/
|
||
|
*/
|
||
|
(function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this);
|
||
|
|
||
|
/* eslint-enable */
|
||
|
(function ($) {
|
||
|
var options = { }; // no options
|
||
|
|
||
|
function init(plot) {
|
||
|
function onResize() {
|
||
|
var placeholder = plot.getPlaceholder();
|
||
|
|
||
|
// somebody might have hidden us and we can't plot
|
||
|
// when we don't have the dimensions
|
||
|
if (placeholder.width() === 0 || placeholder.height() === 0) return;
|
||
|
|
||
|
plot.resize();
|
||
|
plot.setupGrid();
|
||
|
plot.draw();
|
||
|
}
|
||
|
|
||
|
function bindEvents(plot, eventHolder) {
|
||
|
plot.getPlaceholder().resize(onResize);
|
||
|
}
|
||
|
|
||
|
function shutdown(plot, eventHolder) {
|
||
|
plot.getPlaceholder().unbind("resize", onResize);
|
||
|
}
|
||
|
|
||
|
plot.hooks.bindEvents.push(bindEvents);
|
||
|
plot.hooks.shutdown.push(shutdown);
|
||
|
}
|
||
|
|
||
|
$.plot.plugins.push({
|
||
|
init: init,
|
||
|
options: options,
|
||
|
name: 'resize',
|
||
|
version: '1.0'
|
||
|
});
|
||
|
})(jQuery);
|
||
|
|
||
|
/* Flot plugin for plotting textual data or categories.
|
||
|
|
||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||
|
Licensed under the MIT license.
|
||
|
|
||
|
Consider a dataset like [["February", 34], ["March", 20], ...]. This plugin
|
||
|
allows you to plot such a dataset directly.
|
||
|
|
||
|
To enable it, you must specify mode: "categories" on the axis with the textual
|
||
|
labels, e.g.
|
||
|
|
||
|
$.plot("#placeholder", data, { xaxis: { mode: "categories" } });
|
||
|
|
||
|
By default, the labels are ordered as they are met in the data series. If you
|
||
|
need a different ordering, you can specify "categories" on the axis options
|
||
|
and list the categories there:
|
||
|
|
||
|
xaxis: {
|
||
|
mode: "categories",
|
||
|
categories: ["February", "March", "April"]
|
||
|
}
|
||
|
|
||
|
If you need to customize the distances between the categories, you can specify
|
||
|
"categories" as an object mapping labels to values
|
||
|
|
||
|
xaxis: {
|
||
|
mode: "categories",
|
||
|
categories: { "February": 1, "March": 3, "April": 4 }
|
||
|
}
|
||
|
|
||
|
If you don't specify all categories, the remaining categories will be numbered
|
||
|
from the max value plus 1 (with a spacing of 1 between each).
|
||
|
|
||
|
Internally, the plugin works by transforming the input data through an auto-
|
||
|
generated mapping where the first category becomes 0, the second 1, etc.
|
||
|
Hence, a point like ["February", 34] becomes [0, 34] internally in Flot (this
|
||
|
is visible in hover and click events that return numbers rather than the
|
||
|
category labels). The plugin also overrides the tick generator to spit out the
|
||
|
categories as ticks instead of the values.
|
||
|
|
||
|
If you need to map a value back to its label, the mapping is always accessible
|
||
|
as "categories" on the axis object, e.g. plot.getAxes().xaxis.categories.
|
||
|
|
||
|
*/
|
||
|
|
||
|
(function ($) {
|
||
|
var options = {
|
||
|
xaxis: {
|
||
|
categories: null
|
||
|
},
|
||
|
yaxis: {
|
||
|
categories: null
|
||
|
}
|
||
|
};
|
||
|
|
||
|
function processRawData(plot, series, data, datapoints) {
|
||
|
// if categories are enabled, we need to disable
|
||
|
// auto-transformation to numbers so the strings are intact
|
||
|
// for later processing
|
||
|
|
||
|
var xCategories = series.xaxis.options.mode === "categories",
|
||
|
yCategories = series.yaxis.options.mode === "categories";
|
||
|
|
||
|
if (!(xCategories || yCategories)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var format = datapoints.format;
|
||
|
|
||
|
if (!format) {
|
||
|
// FIXME: auto-detection should really not be defined here
|
||
|
var s = series;
|
||
|
format = [];
|
||
|
format.push({ x: true, number: true, required: true, computeRange: true});
|
||
|
format.push({ y: true, number: true, required: true, computeRange: true });
|
||
|
|
||
|
if (s.bars.show || (s.lines.show && s.lines.fill)) {
|
||
|
var autoScale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero));
|
||
|
format.push({ y: true, number: true, required: false, defaultValue: 0, computeRange: autoScale });
|
||
|
if (s.bars.horizontal) {
|
||
|
delete format[format.length - 1].y;
|
||
|
format[format.length - 1].x = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
datapoints.format = format;
|
||
|
}
|
||
|
|
||
|
for (var m = 0; m < format.length; ++m) {
|
||
|
if (format[m].x && xCategories) {
|
||
|
format[m].number = false;
|
||
|
}
|
||
|
|
||
|
if (format[m].y && yCategories) {
|
||
|
format[m].number = false;
|
||
|
format[m].computeRange = false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function getNextIndex(categories) {
|
||
|
var index = -1;
|
||
|
|
||
|
for (var v in categories) {
|
||
|
if (categories[v] > index) {
|
||
|
index = categories[v];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return index + 1;
|
||
|
}
|
||
|
|
||
|
function categoriesTickGenerator(axis) {
|
||
|
var res = [];
|
||
|
for (var label in axis.categories) {
|
||
|
var v = axis.categories[label];
|
||
|
if (v >= axis.min && v <= axis.max) {
|
||
|
res.push([v, label]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
res.sort(function (a, b) { return a[0] - b[0]; });
|
||
|
|
||
|
return res;
|
||
|
}
|
||
|
|
||
|
function setupCategoriesForAxis(series, axis, datapoints) {
|
||
|
if (series[axis].options.mode !== "categories") {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!series[axis].categories) {
|
||
|
// parse options
|
||
|
var c = {}, o = series[axis].options.categories || {};
|
||
|
if ($.isArray(o)) {
|
||
|
for (var i = 0; i < o.length; ++i) {
|
||
|
c[o[i]] = i;
|
||
|
}
|
||
|
} else {
|
||
|
for (var v in o) {
|
||
|
c[v] = o[v];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
series[axis].categories = c;
|
||
|
}
|
||
|
|
||
|
// fix ticks
|
||
|
if (!series[axis].options.ticks) {
|
||
|
series[axis].options.ticks = categoriesTickGenerator;
|
||
|
}
|
||
|
|
||
|
transformPointsOnAxis(datapoints, axis, series[axis].categories);
|
||
|
}
|
||
|
|
||
|
function transformPointsOnAxis(datapoints, axis, categories) {
|
||
|
// go through the points, transforming them
|
||
|
var points = datapoints.points,
|
||
|
ps = datapoints.pointsize,
|
||
|
format = datapoints.format,
|
||
|
formatColumn = axis.charAt(0),
|
||
|
index = getNextIndex(categories);
|
||
|
|
||
|
for (var i = 0; i < points.length; i += ps) {
|
||
|
if (points[i] == null) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
for (var m = 0; m < ps; ++m) {
|
||
|
var val = points[i + m];
|
||
|
|
||
|
if (val == null || !format[m][formatColumn]) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (!(val in categories)) {
|
||
|
categories[val] = index;
|
||
|
++index;
|
||
|
}
|
||
|
|
||
|
points[i + m] = categories[val];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function processDatapoints(plot, series, datapoints) {
|
||
|
setupCategoriesForAxis(series, "xaxis", datapoints);
|
||
|
setupCategoriesForAxis(series, "yaxis", datapoints);
|
||
|
}
|
||
|
|
||
|
function init(plot) {
|
||
|
plot.hooks.processRawData.push(processRawData);
|
||
|
plot.hooks.processDatapoints.push(processDatapoints);
|
||
|
}
|
||
|
|
||
|
$.plot.plugins.push({
|
||
|
init: init,
|
||
|
options: options,
|
||
|
name: 'categories',
|
||
|
version: '1.0'
|
||
|
});
|
||
|
})(jQuery);
|
||
|
|
||
|
/* Flot plugin for rendering pie charts.
|
||
|
|
||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||
|
Licensed under the MIT license.
|
||
|
|
||
|
The plugin assumes that each series has a single data value, and that each
|
||
|
value is a positive integer or zero. Negative numbers don't make sense for a
|
||
|
pie chart, and have unpredictable results. The values do NOT need to be
|
||
|
passed in as percentages; the plugin will calculate the total and per-slice
|
||
|
percentages internally.
|
||
|
|
||
|
* Created by Brian Medendorp
|
||
|
|
||
|
* Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars
|
||
|
|
||
|
The plugin supports these options:
|
||
|
|
||
|
series: {
|
||
|
pie: {
|
||
|
show: true/false
|
||
|
radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto'
|
||
|
innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect
|
||
|
startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result
|
||
|
tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show)
|
||
|
offset: {
|
||
|
top: integer value to move the pie up or down
|
||
|
left: integer value to move the pie left or right, or 'auto'
|
||
|
},
|
||
|
stroke: {
|
||
|
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#FFF')
|
||
|
width: integer pixel width of the stroke
|
||
|
},
|
||
|
label: {
|
||
|
show: true/false, or 'auto'
|
||
|
formatter: a user-defined function that modifies the text/style of the label text
|
||
|
radius: 0-1 for percentage of fullsize, or a specified pixel length
|
||
|
background: {
|
||
|
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#000')
|
||
|
opacity: 0-1
|
||
|
},
|
||
|
threshold: 0-1 for the percentage value at which to hide labels (if they're too small)
|
||
|
},
|
||
|
combine: {
|
||
|
threshold: 0-1 for the percentage value at which to combine slices (if they're too small)
|
||
|
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined
|
||
|
label: any text value of what the combined slice should be labeled
|
||
|
}
|
||
|
highlight: {
|
||
|
opacity: 0-1
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
More detail and specific examples can be found in the included HTML file.
|
||
|
|
||
|
*/
|
||
|
|
||
|
(function($) {
|
||
|
// Maximum redraw attempts when fitting labels within the plot
|
||
|
|
||
|
var REDRAW_ATTEMPTS = 10;
|
||
|
|
||
|
// Factor by which to shrink the pie when fitting labels within the plot
|
||
|
|
||
|
var REDRAW_SHRINK = 0.95;
|
||
|
|
||
|
function init(plot) {
|
||
|
var canvas = null,
|
||
|
target = null,
|
||
|
options = null,
|
||
|
maxRadius = null,
|
||
|
centerLeft = null,
|
||
|
centerTop = null,
|
||
|
processed = false,
|
||
|
ctx = null;
|
||
|
|
||
|
// interactive variables
|
||
|
|
||
|
var highlights = [];
|
||
|
|
||
|
// add hook to determine if pie plugin in enabled, and then perform necessary operations
|
||
|
|
||
|
plot.hooks.processOptions.push(function(plot, options) {
|
||
|
if (options.series.pie.show) {
|
||
|
options.grid.show = false;
|
||
|
|
||
|
// set labels.show
|
||
|
|
||
|
if (options.series.pie.label.show === "auto") {
|
||
|
if (options.legend.show) {
|
||
|
options.series.pie.label.show = false;
|
||
|
} else {
|
||
|
options.series.pie.label.show = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// set radius
|
||
|
|
||
|
if (options.series.pie.radius === "auto") {
|
||
|
if (options.series.pie.label.show) {
|
||
|
options.series.pie.radius = 3 / 4;
|
||
|
} else {
|
||
|
options.series.pie.radius = 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ensure sane tilt
|
||
|
|
||
|
if (options.series.pie.tilt > 1) {
|
||
|
options.series.pie.tilt = 1;
|
||
|
} else if (options.series.pie.tilt < 0) {
|
||
|
options.series.pie.tilt = 0;
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
plot.hooks.bindEvents.push(function(plot, eventHolder) {
|
||
|
var options = plot.getOptions();
|
||
|
if (options.series.pie.show) {
|
||
|
if (options.grid.hoverable) {
|
||
|
eventHolder.unbind("mousemove").mousemove(onMouseMove);
|
||
|
}
|
||
|
if (options.grid.clickable) {
|
||
|
eventHolder.unbind("click").click(onClick);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) {
|
||
|
var options = plot.getOptions();
|
||
|
if (options.series.pie.show) {
|
||
|
processDatapoints(plot, series, data, datapoints);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
plot.hooks.drawOverlay.push(function(plot, octx) {
|
||
|
var options = plot.getOptions();
|
||
|
if (options.series.pie.show) {
|
||
|
drawOverlay(plot, octx);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
plot.hooks.draw.push(function(plot, newCtx) {
|
||
|
var options = plot.getOptions();
|
||
|
if (options.series.pie.show) {
|
||
|
draw(plot, newCtx);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
function processDatapoints(plot, series, datapoints) {
|
||
|
if (!processed) {
|
||
|
processed = true;
|
||
|
canvas = plot.getCanvas();
|
||
|
target = $(canvas).parent();
|
||
|
options = plot.getOptions();
|
||
|
plot.setData(combine(plot.getData()));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function combine(data) {
|
||
|
var total = 0,
|
||
|
combined = 0,
|
||
|
numCombined = 0,
|
||
|
color = options.series.pie.combine.color,
|
||
|
newdata = [],
|
||
|
i,
|
||
|
value;
|
||
|
|
||
|
// Fix up the raw data from Flot, ensuring the data is numeric
|
||
|
|
||
|
for (i = 0; i < data.length; ++i) {
|
||
|
value = data[i].data;
|
||
|
|
||
|
// If the data is an array, we'll assume that it's a standard
|
||
|
// Flot x-y pair, and are concerned only with the second value.
|
||
|
|
||
|
// Note how we use the original array, rather than creating a
|
||
|
// new one; this is more efficient and preserves any extra data
|
||
|
// that the user may have stored in higher indexes.
|
||
|
|
||
|
if ($.isArray(value) && value.length === 1) {
|
||
|
value = value[0];
|
||
|
}
|
||
|
|
||
|
if ($.isArray(value)) {
|
||
|
// Equivalent to $.isNumeric() but compatible with jQuery < 1.7
|
||
|
if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) {
|
||
|
value[1] = +value[1];
|
||
|
} else {
|
||
|
value[1] = 0;
|
||
|
}
|
||
|
} else if (!isNaN(parseFloat(value)) && isFinite(value)) {
|
||
|
value = [1, +value];
|
||
|
} else {
|
||
|
value = [1, 0];
|
||
|
}
|
||
|
|
||
|
data[i].data = [value];
|
||
|
}
|
||
|
|
||
|
// Sum up all the slices, so we can calculate percentages for each
|
||
|
|
||
|
for (i = 0; i < data.length; ++i) {
|
||
|
total += data[i].data[0][1];
|
||
|
}
|
||
|
|
||
|
// Count the number of slices with percentages below the combine
|
||
|
// threshold; if it turns out to be just one, we won't combine.
|
||
|
|
||
|
for (i = 0; i < data.length; ++i) {
|
||
|
value = data[i].data[0][1];
|
||
|
if (value / total <= options.series.pie.combine.threshold) {
|
||
|
combined += value;
|
||
|
numCombined++;
|
||
|
if (!color) {
|
||
|
color = data[i].color;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for (i = 0; i < data.length; ++i) {
|
||
|
value = data[i].data[0][1];
|
||
|
if (numCombined < 2 || value / total > options.series.pie.combine.threshold) {
|
||
|
newdata.push(
|
||
|
$.extend(data[i], { /* extend to allow keeping all other original data values
|
||
|
and using them e.g. in labelFormatter. */
|
||
|
data: [[1, value]],
|
||
|
color: data[i].color,
|
||
|
label: data[i].label,
|
||
|
angle: value * Math.PI * 2 / total,
|
||
|
percent: value / (total / 100)
|
||
|
})
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (numCombined > 1) {
|
||
|
newdata.push({
|
||
|
data: [[1, combined]],
|
||
|
color: color,
|
||
|
label: options.series.pie.combine.label,
|
||
|
angle: combined * Math.PI * 2 / total,
|
||
|
percent: combined / (total / 100)
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return newdata;
|
||
|
}
|
||
|
|
||
|
function draw(plot, newCtx) {
|
||
|
if (!target) {
|
||
|
return; // if no series were passed
|
||
|
}
|
||
|
|
||
|
var canvasWidth = plot.getPlaceholder().width(),
|
||
|
canvasHeight = plot.getPlaceholder().height(),
|
||
|
legendWidth = target.children().filter(".legend").children().width() || 0;
|
||
|
|
||
|
ctx = newCtx;
|
||
|
|
||
|
// WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE!
|
||
|
|
||
|
// When combining smaller slices into an 'other' slice, we need to
|
||
|
// add a new series. Since Flot gives plugins no way to modify the
|
||
|
// list of series, the pie plugin uses a hack where the first call
|
||
|
// to processDatapoints results in a call to setData with the new
|
||
|
// list of series, then subsequent processDatapoints do nothing.
|
||
|
|
||
|
// The plugin-global 'processed' flag is used to control this hack;
|
||
|
// it starts out false, and is set to true after the first call to
|
||
|
// processDatapoints.
|
||
|
|
||
|
// Unfortunately this turns future setData calls into no-ops; they
|
||
|
// call processDatapoints, the flag is true, and nothing happens.
|
||
|
|
||
|
// To fix this we'll set the flag back to false here in draw, when
|
||
|
// all series have been processed, so the next sequence of calls to
|
||
|
// processDatapoints once again starts out with a slice-combine.
|
||
|
// This is really a hack; in 0.9 we need to give plugins a proper
|
||
|
// way to modify series before any processing begins.
|
||
|
|
||
|
processed = false;
|
||
|
|
||
|
// calculate maximum radius and center point
|
||
|
maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2;
|
||
|
centerTop = canvasHeight / 2 + options.series.pie.offset.top;
|
||
|
centerLeft = canvasWidth / 2;
|
||
|
|
||
|
if (options.series.pie.offset.left === "auto") {
|
||
|
if (options.legend.position.match("w")) {
|
||
|
centerLeft += legendWidth / 2;
|
||
|
} else {
|
||
|
centerLeft -= legendWidth / 2;
|
||
|
}
|
||
|
if (centerLeft < maxRadius) {
|
||
|
centerLeft = maxRadius;
|
||
|
} else if (centerLeft > canvasWidth - maxRadius) {
|
||
|
centerLeft = canvasWidth - maxRadius;
|
||
|
}
|
||
|
} else {
|
||
|
centerLeft += options.series.pie.offset.left;
|
||
|
}
|
||
|
|
||
|
var slices = plot.getData(),
|
||
|
attempts = 0;
|
||
|
|
||
|
// Keep shrinking the pie's radius until drawPie returns true,
|
||
|
// indicating that all the labels fit, or we try too many times.
|
||
|
do {
|
||
|
if (attempts > 0) {
|
||
|
maxRadius *= REDRAW_SHRINK;
|
||
|
}
|
||
|
attempts += 1;
|
||
|
clear();
|
||
|
if (options.series.pie.tilt <= 0.8) {
|
||
|
drawShadow();
|
||
|
}
|
||
|
} while (!drawPie() && attempts < REDRAW_ATTEMPTS)
|
||
|
|
||
|
if (attempts >= REDRAW_ATTEMPTS) {
|
||
|
clear();
|
||
|
target.prepend("<div class='error'>Could not draw pie with labels contained inside canvas</div>");
|
||
|
}
|
||
|
|
||
|
if (plot.setSeries && plot.insertLegend) {
|
||
|
plot.setSeries(slices);
|
||
|
plot.insertLegend();
|
||
|
}
|
||
|
|
||
|
// we're actually done at this point, just defining internal functions at this point
|
||
|
function clear() {
|
||
|
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||
|
target.children().filter(".pieLabel, .pieLabelBackground").remove();
|
||
|
}
|
||
|
|
||
|
function drawShadow() {
|
||
|
var shadowLeft = options.series.pie.shadow.left;
|
||
|
var shadowTop = options.series.pie.shadow.top;
|
||
|
var edge = 10;
|
||
|
var alpha = options.series.pie.shadow.alpha;
|
||
|
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
|
||
|
|
||
|
if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) {
|
||
|
return; // shadow would be outside canvas, so don't draw it
|
||
|
}
|
||
|
|
||
|
ctx.save();
|
||
|
ctx.translate(shadowLeft, shadowTop);
|
||
|
ctx.globalAlpha = alpha;
|
||
|
ctx.fillStyle = "#000";
|
||
|
|
||
|
// center and rotate to starting position
|
||
|
ctx.translate(centerLeft, centerTop);
|
||
|
ctx.scale(1, options.series.pie.tilt);
|
||
|
|
||
|
//radius -= edge;
|
||
|
for (var i = 1; i <= edge; i++) {
|
||
|
ctx.beginPath();
|
||
|
ctx.arc(0, 0, radius, 0, Math.PI * 2, false);
|
||
|
ctx.fill();
|
||
|
radius -= i;
|
||
|
}
|
||
|
|
||
|
ctx.restore();
|
||
|
}
|
||
|
|
||
|
function drawPie() {
|
||
|
var startAngle = Math.PI * options.series.pie.startAngle;
|
||
|
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
|
||
|
var i;
|
||
|
// center and rotate to starting position
|
||
|
|
||
|
ctx.save();
|
||
|
ctx.translate(centerLeft, centerTop);
|
||
|
ctx.scale(1, options.series.pie.tilt);
|
||
|
//ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera
|
||
|
|
||
|
// draw slices
|
||
|
ctx.save();
|
||
|
|
||
|
var currentAngle = startAngle;
|
||
|
for (i = 0; i < slices.length; ++i) {
|
||
|
slices[i].startAngle = currentAngle;
|
||
|
drawSlice(slices[i].angle, slices[i].color, true);
|
||
|
}
|
||
|
|
||
|
ctx.restore();
|
||
|
|
||
|
// draw slice outlines
|
||
|
if (options.series.pie.stroke.width > 0) {
|
||
|
ctx.save();
|
||
|
ctx.lineWidth = options.series.pie.stroke.width;
|
||
|
currentAngle = startAngle;
|
||
|
for (i = 0; i < slices.length; ++i) {
|
||
|
drawSlice(slices[i].angle, options.series.pie.stroke.color, false);
|
||
|
}
|
||
|
|
||
|
ctx.restore();
|
||
|
}
|
||
|
|
||
|
// draw donut hole
|
||
|
drawDonutHole(ctx);
|
||
|
|
||
|
ctx.restore();
|
||
|
|
||
|
// Draw the labels, returning true if they fit within the plot
|
||
|
if (options.series.pie.label.show) {
|
||
|
return drawLabels();
|
||
|
} else return true;
|
||
|
|
||
|
function drawSlice(angle, color, fill) {
|
||
|
if (angle <= 0 || isNaN(angle)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (fill) {
|
||
|
ctx.fillStyle = color;
|
||
|
} else {
|
||
|
ctx.strokeStyle = color;
|
||
|
ctx.lineJoin = "round";
|
||
|
}
|
||
|
|
||
|
ctx.beginPath();
|
||
|
if (Math.abs(angle - Math.PI * 2) > 0.000000001) {
|
||
|
ctx.moveTo(0, 0); // Center of the pie
|
||
|
}
|
||
|
|
||
|
//ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera
|
||
|
ctx.arc(0, 0, radius, currentAngle, currentAngle + angle / 2, false);
|
||
|
ctx.arc(0, 0, radius, currentAngle + angle / 2, currentAngle + angle, false);
|
||
|
ctx.closePath();
|
||
|
//ctx.rotate(angle); // This doesn't work properly in Opera
|
||
|
currentAngle += angle;
|
||
|
|
||
|
if (fill) {
|
||
|
ctx.fill();
|
||
|
} else {
|
||
|
ctx.stroke();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function drawLabels() {
|
||
|
var currentAngle = startAngle;
|
||
|
var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius;
|
||
|
|
||
|
for (var i = 0; i < slices.length; ++i) {
|
||
|
if (slices[i].percent >= options.series.pie.label.threshold * 100) {
|
||
|
if (!drawLabel(slices[i], currentAngle, i)) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
currentAngle += slices[i].angle;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
|
||
|
function drawLabel(slice, startAngle, index) {
|
||
|
if (slice.data[0][1] === 0) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// format label text
|
||
|
var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter;
|
||
|
|
||
|
if (lf) {
|
||
|
text = lf(slice.label, slice);
|
||
|
} else {
|
||
|
text = slice.label;
|
||
|
}
|
||
|
|
||
|
if (plf) {
|
||
|
text = plf(text, slice);
|
||
|
}
|
||
|
|
||
|
var halfAngle = ((startAngle + slice.angle) + startAngle) / 2;
|
||
|
var x = centerLeft + Math.round(Math.cos(halfAngle) * radius);
|
||
|
var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt;
|
||
|
|
||
|
var html = "<span class='pieLabel' id='pieLabel" + index + "' style='position:absolute;top:" + y + "px;left:" + x + "px;'>" + text + "</span>";
|
||
|
target.append(html);
|
||
|
|
||
|
var label = target.children("#pieLabel" + index);
|
||
|
var labelTop = (y - label.height() / 2);
|
||
|
var labelLeft = (x - label.width() / 2);
|
||
|
|
||
|
label.css("top", labelTop);
|
||
|
label.css("left", labelLeft);
|
||
|
|
||
|
// check to make sure that the label is not outside the canvas
|
||
|
if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (options.series.pie.label.background.opacity !== 0) {
|
||
|
// put in the transparent background separately to avoid blended labels and label boxes
|
||
|
var c = options.series.pie.label.background.color;
|
||
|
if (c == null) {
|
||
|
c = slice.color;
|
||
|
}
|
||
|
|
||
|
var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;";
|
||
|
$("<div class='pieLabelBackground' style='position:absolute;width:" + label.width() + "px;height:" + label.height() + "px;" + pos + "background-color:" + c + ";'></div>")
|
||
|
.css("opacity", options.series.pie.label.background.opacity)
|
||
|
.insertBefore(label);
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
} // end individual label function
|
||
|
} // end drawLabels function
|
||
|
} // end drawPie function
|
||
|
} // end draw function
|
||
|
|
||
|
// Placed here because it needs to be accessed from multiple locations
|
||
|
|
||
|
function drawDonutHole(layer) {
|
||
|
if (options.series.pie.innerRadius > 0) {
|
||
|
// subtract the center
|
||
|
layer.save();
|
||
|
var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius;
|
||
|
layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color
|
||
|
layer.beginPath();
|
||
|
layer.fillStyle = options.series.pie.stroke.color;
|
||
|
layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false);
|
||
|
layer.fill();
|
||
|
layer.closePath();
|
||
|
layer.restore();
|
||
|
|
||
|
// add inner stroke
|
||
|
layer.save();
|
||
|
layer.beginPath();
|
||
|
layer.strokeStyle = options.series.pie.stroke.color;
|
||
|
layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false);
|
||
|
layer.stroke();
|
||
|
layer.closePath();
|
||
|
layer.restore();
|
||
|
|
||
|
// TODO: add extra shadow inside hole (with a mask) if the pie is tilted.
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//-- Additional Interactive related functions --
|
||
|
|
||
|
function isPointInPoly(poly, pt) {
|
||
|
for (var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) {
|
||
|
((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) ||
|
||
|
(poly[j][1] <= pt[1] && pt[1] < poly[i][1])) &&
|
||
|
(pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0]) &&
|
||
|
(c = !c);
|
||
|
}
|
||
|
return c;
|
||
|
}
|
||
|
|
||
|
function findNearbySlice(mouseX, mouseY) {
|
||
|
var slices = plot.getData(),
|
||
|
options = plot.getOptions(),
|
||
|
radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius,
|
||
|
x, y;
|
||
|
|
||
|
for (var i = 0; i < slices.length; ++i) {
|
||
|
var s = slices[i];
|
||
|
if (s.pie.show) {
|
||
|
ctx.save();
|
||
|
ctx.beginPath();
|
||
|
ctx.moveTo(0, 0); // Center of the pie
|
||
|
//ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here.
|
||
|
ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false);
|
||
|
ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false);
|
||
|
ctx.closePath();
|
||
|
x = mouseX - centerLeft;
|
||
|
y = mouseY - centerTop;
|
||
|
|
||
|
if (ctx.isPointInPath) {
|
||
|
if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) {
|
||
|
ctx.restore();
|
||
|
return {
|
||
|
datapoint: [s.percent, s.data],
|
||
|
dataIndex: 0,
|
||
|
series: s,
|
||
|
seriesIndex: i
|
||
|
};
|
||
|
}
|
||
|
} else {
|
||
|
// excanvas for IE doesn;t support isPointInPath, this is a workaround.
|
||
|
var p1X = radius * Math.cos(s.startAngle),
|
||
|
p1Y = radius * Math.sin(s.startAngle),
|
||
|
p2X = radius * Math.cos(s.startAngle + s.angle / 4),
|
||
|
p2Y = radius * Math.sin(s.startAngle + s.angle / 4),
|
||
|
p3X = radius * Math.cos(s.startAngle + s.angle / 2),
|
||
|
p3Y = radius * Math.sin(s.startAngle + s.angle / 2),
|
||
|
p4X = radius * Math.cos(s.startAngle + s.angle / 1.5),
|
||
|
p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5),
|
||
|
p5X = radius * Math.cos(s.startAngle + s.angle),
|
||
|
p5Y = radius * Math.sin(s.startAngle + s.angle),
|
||
|
arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]],
|
||
|
arrPoint = [x, y];
|
||
|
|
||
|
// TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt?
|
||
|
|
||
|
if (isPointInPoly(arrPoly, arrPoint)) {
|
||
|
ctx.restore();
|
||
|
return {
|
||
|
datapoint: [s.percent, s.data],
|
||
|
dataIndex: 0,
|
||
|
series: s,
|
||
|
seriesIndex: i
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
ctx.restore();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
function onMouseMove(e) {
|
||
|
triggerClickHoverEvent("plothover", e);
|
||
|
}
|
||
|
|
||
|
function onClick(e) {
|
||
|
triggerClickHoverEvent("plotclick", e);
|
||
|
}
|
||
|
|
||
|
// trigger click or hover event (they send the same parameters so we share their code)
|
||
|
|
||
|
function triggerClickHoverEvent(eventname, e) {
|
||
|
var offset = plot.offset();
|
||
|
var canvasX = parseInt(e.pageX - offset.left);
|
||
|
var canvasY = parseInt(e.pageY - offset.top);
|
||
|
var item = findNearbySlice(canvasX, canvasY);
|
||
|
|
||
|
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)) {
|
||
|
unhighlight(h.series);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// highlight the slice
|
||
|
|
||
|
if (item) {
|
||
|
highlight(item.series, eventname);
|
||
|
}
|
||
|
|
||
|
// trigger any hover bind events
|
||
|
|
||
|
var pos = { pageX: e.pageX, pageY: e.pageY };
|
||
|
target.trigger(eventname, [pos, item]);
|
||
|
}
|
||
|
|
||
|
function highlight(s, auto) {
|
||
|
//if (typeof s == "number") {
|
||
|
// s = series[s];
|
||
|
//}
|
||
|
|
||
|
var i = indexOfHighlight(s);
|
||
|
|
||
|
if (i === -1) {
|
||
|
highlights.push({ series: s, auto: auto });
|
||
|
plot.triggerRedrawOverlay();
|
||
|
} else if (!auto) {
|
||
|
highlights[i].auto = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function unhighlight(s) {
|
||
|
if (s == null) {
|
||
|
highlights = [];
|
||
|
plot.triggerRedrawOverlay();
|
||
|
}
|
||
|
|
||
|
//if (typeof s == "number") {
|
||
|
// s = series[s];
|
||
|
//}
|
||
|
|
||
|
var i = indexOfHighlight(s);
|
||
|
|
||
|
if (i !== -1) {
|
||
|
highlights.splice(i, 1);
|
||
|
plot.triggerRedrawOverlay();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function indexOfHighlight(s) {
|
||
|
for (var i = 0; i < highlights.length; ++i) {
|
||
|
var h = highlights[i];
|
||
|
if (h.series === s) {
|
||
|
return i;
|
||
|
}
|
||
|
}
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
function drawOverlay(plot, octx) {
|
||
|
var options = plot.getOptions();
|
||
|
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
|
||
|
|
||
|
octx.save();
|
||
|
octx.translate(centerLeft, centerTop);
|
||
|
octx.scale(1, options.series.pie.tilt);
|
||
|
|
||
|
for (var i = 0; i < highlights.length; ++i) {
|
||
|
drawHighlight(highlights[i].series);
|
||
|
}
|
||
|
|
||
|
drawDonutHole(octx);
|
||
|
|
||
|
octx.restore();
|
||
|
|
||
|
function drawHighlight(series) {
|
||
|
if (series.angle <= 0 || isNaN(series.angle)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
//octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString();
|
||
|
octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor
|
||
|
octx.beginPath();
|
||
|
if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) {
|
||
|
octx.moveTo(0, 0); // Center of the pie
|
||
|
}
|
||
|
octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false);
|
||
|
octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false);
|
||
|
octx.closePath();
|
||
|
octx.fill();
|
||
|
}
|
||
|
}
|
||
|
} // end init (plugin body)
|
||
|
|
||
|
// define pie specific options and their default values
|
||
|
var options = {
|
||
|
series: {
|
||
|
pie: {
|
||
|
show: false,
|
||
|
radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value)
|
||
|
innerRadius: 0, /* for donut */
|
||
|
startAngle: 3 / 2,
|
||
|
tilt: 1,
|
||
|
shadow: {
|
||
|
left: 5, // shadow left offset
|
||
|
top: 15, // shadow top offset
|
||
|
alpha: 0.02 // shadow alpha
|
||
|
},
|
||
|
offset: {
|
||
|
top: 0,
|
||
|
left: "auto"
|
||
|
},
|
||
|
stroke: {
|
||
|
color: "#fff",
|
||
|
width: 1
|
||
|
},
|
||
|
label: {
|
||
|
show: "auto",
|
||
|
formatter: function(label, slice) {
|
||
|
return "<div style='font-size:x-small;text-align:center;padding:2px;color:" + slice.color + ";'>" + label + "<br/>" + Math.round(slice.percent) + "%</div>";
|
||
|
}, // formatter function
|
||
|
radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value)
|
||
|
background: {
|
||
|
color: null,
|
||
|
opacity: 0
|
||
|
},
|
||
|
threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow)
|
||
|
},
|
||
|
combine: {
|
||
|
threshold: -1, // percentage at which to combine little slices into one larger slice
|
||
|
color: null, // color to give the new slice (auto-generated if null)
|
||
|
label: "Other" // label to give the new slice
|
||
|
},
|
||
|
highlight: {
|
||
|
//color: "#fff", // will add this functionality once parseColor is available
|
||
|
opacity: 0.5
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
$.plot.plugins.push({
|
||
|
init: init,
|
||
|
options: options,
|
||
|
name: "pie",
|
||
|
version: "1.1"
|
||
|
});
|
||
|
})(jQuery);
|
||
|
|
||
|
/* Flot plugin for stacking data sets rather than overlaying them.
|
||
|
|
||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||
|
Licensed under the MIT license.
|
||
|
|
||
|
The plugin assumes the data is sorted on x (or y if stacking horizontally).
|
||
|
For line charts, it is assumed that if a line has an undefined gap (from a
|
||
|
null point), then the line above it should have the same gap - insert zeros
|
||
|
instead of "null" if you want another behaviour. This also holds for the start
|
||
|
and end of the chart. Note that stacking a mix of positive and negative values
|
||
|
in most instances doesn't make sense (so it looks weird).
|
||
|
|
||
|
Two or more series are stacked when their "stack" attribute is set to the same
|
||
|
key (which can be any number or string or just "true"). To specify the default
|
||
|
stack, you can set the stack option like this:
|
||
|
|
||
|
series: {
|
||
|
stack: null/false, true, or a key (number/string)
|
||
|
}
|
||
|
|
||
|
You can also specify it for a single series, like this:
|
||
|
|
||
|
$.plot( $("#placeholder"), [{
|
||
|
data: [ ... ],
|
||
|
stack: true
|
||
|
}])
|
||
|
|
||
|
The stacking order is determined by the order of the data series in the array
|
||
|
(later series end up on top of the previous).
|
||
|
|
||
|
Internally, the plugin modifies the datapoints in each series, adding an
|
||
|
offset to the y value. For line series, extra data points are inserted through
|
||
|
interpolation. If there's a second y value, it's also adjusted (e.g for bar
|
||
|
charts or filled areas).
|
||
|
|
||
|
*/
|
||
|
|
||
|
(function ($) {
|
||
|
var options = {
|
||
|
series: { stack: null } // or number/string
|
||
|
};
|
||
|
|
||
|
function init(plot) {
|
||
|
function findMatchingSeries(s, allseries) {
|
||
|
var res = null;
|
||
|
for (var i = 0; i < allseries.length; ++i) {
|
||
|
if (s === allseries[i]) break;
|
||
|
|
||
|
if (allseries[i].stack === s.stack) {
|
||
|
res = allseries[i];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return res;
|
||
|
}
|
||
|
|
||
|
function addBottomPoints (s, datapoints) {
|
||
|
var formattedPoints = [];
|
||
|
for (var i = 0; i < datapoints.points.length; i += 2) {
|
||
|
formattedPoints.push(datapoints.points[i]);
|
||
|
formattedPoints.push(datapoints.points[i + 1]);
|
||
|
formattedPoints.push(0);
|
||
|
}
|
||
|
|
||
|
datapoints.format.push({
|
||
|
x: false,
|
||
|
y: true,
|
||
|
number: true,
|
||
|
required: false,
|
||
|
computeRange: s.yaxis.options.autoScale !== 'none',
|
||
|
defaultValue: 0
|
||
|
});
|
||
|
datapoints.points = formattedPoints;
|
||
|
datapoints.pointsize = 3;
|
||
|
}
|
||
|
|
||
|
function stackData(plot, s, datapoints) {
|
||
|
if (s.stack == null || s.stack === false) return;
|
||
|
|
||
|
var needsBottom = s.bars.show || (s.lines.show && s.lines.fill);
|
||
|
var hasBottom = datapoints.pointsize > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y);
|
||
|
// Series data is missing bottom points - need to format
|
||
|
if (needsBottom && !hasBottom) {
|
||
|
addBottomPoints(s, datapoints);
|
||
|
}
|
||
|
|
||
|
var other = findMatchingSeries(s, plot.getData());
|
||
|
if (!other) return;
|
||
|
|
||
|
var ps = datapoints.pointsize,
|
||
|
points = datapoints.points,
|
||
|
otherps = other.datapoints.pointsize,
|
||
|
otherpoints = other.datapoints.points,
|
||
|
newpoints = [],
|
||
|
px, py, intery, qx, qy, bottom,
|
||
|
withlines = s.lines.show,
|
||
|
horizontal = s.bars.horizontal,
|
||
|
withsteps = withlines && s.lines.steps,
|
||
|
fromgap = true,
|
||
|
keyOffset = horizontal ? 1 : 0,
|
||
|
accumulateOffset = horizontal ? 0 : 1,
|
||
|
i = 0, j = 0, l, m;
|
||
|
|
||
|
while (true) {
|
||
|
if (i >= points.length) break;
|
||
|
|
||
|
l = newpoints.length;
|
||
|
|
||
|
if (points[i] == null) {
|
||
|
// copy gaps
|
||
|
for (m = 0; m < ps; ++m) {
|
||
|
newpoints.push(points[i + m]);
|
||
|
}
|
||
|
|
||
|
i += ps;
|
||
|
} else if (j >= otherpoints.length) {
|
||
|
// for lines, we can't use the rest of the points
|
||
|
if (!withlines) {
|
||
|
for (m = 0; m < ps; ++m) {
|
||
|
newpoints.push(points[i + m]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
i += ps;
|
||
|
} else if (otherpoints[j] == null) {
|
||
|
// oops, got a gap
|
||
|
for (m = 0; m < ps; ++m) {
|
||
|
newpoints.push(null);
|
||
|
}
|
||
|
|
||
|
fromgap = true;
|
||
|
j += otherps;
|
||
|
} else {
|
||
|
// cases where we actually got two points
|
||
|
px = points[i + keyOffset];
|
||
|
py = points[i + accumulateOffset];
|
||
|
qx = otherpoints[j + keyOffset];
|
||
|
qy = otherpoints[j + accumulateOffset];
|
||
|
bottom = 0;
|
||
|
|
||
|
if (px === qx) {
|
||
|
for (m = 0; m < ps; ++m) {
|
||
|
newpoints.push(points[i + m]);
|
||
|
}
|
||
|
|
||
|
newpoints[l + accumulateOffset] += qy;
|
||
|
bottom = qy;
|
||
|
|
||
|
i += ps;
|
||
|
j += otherps;
|
||
|
} else if (px > qx) {
|
||
|
// we got past point below, might need to
|
||
|
// insert interpolated extra point
|
||
|
if (withlines && i > 0 && points[i - ps] != null) {
|
||
|
intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px);
|
||
|
newpoints.push(qx);
|
||
|
newpoints.push(intery + qy);
|
||
|
for (m = 2; m < ps; ++m) {
|
||
|
newpoints.push(points[i + m]);
|
||
|
}
|
||
|
|
||
|
bottom = qy;
|
||
|
}
|
||
|
|
||
|
j += otherps;
|
||
|
} else { // px < qx
|
||
|
if (fromgap && withlines) {
|
||
|
// if we come from a gap, we just skip this point
|
||
|
i += ps;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
for (m = 0; m < ps; ++m) {
|
||
|
newpoints.push(points[i + m]);
|
||
|
}
|
||
|
|
||
|
// we might be able to interpolate a point below,
|
||
|
// this can give us a better y
|
||
|
if (withlines && j > 0 && otherpoints[j - otherps] != null) {
|
||
|
bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx);
|
||
|
}
|
||
|
|
||
|
newpoints[l + accumulateOffset] += bottom;
|
||
|
|
||
|
i += ps;
|
||
|
}
|
||
|
|
||
|
fromgap = false;
|
||
|
|
||
|
if (l !== newpoints.length && needsBottom) {
|
||
|
newpoints[l + 2] += bottom;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// maintain the line steps invariant
|
||
|
if (withsteps && l !== newpoints.length && l > 0 &&
|
||
|
newpoints[l] !== null &&
|
||
|
newpoints[l] !== newpoints[l - ps] &&
|
||
|
newpoints[l + 1] !== newpoints[l - ps + 1]) {
|
||
|
for (m = 0; m < ps; ++m) {
|
||
|
newpoints[l + ps + m] = newpoints[l + m];
|
||
|
}
|
||
|
|
||
|
newpoints[l + 1] = newpoints[l - ps + 1];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
datapoints.points = newpoints;
|
||
|
}
|
||
|
|
||
|
plot.hooks.processDatapoints.push(stackData);
|
||
|
}
|
||
|
|
||
|
$.plot.plugins.push({
|
||
|
init: init,
|
||
|
options: options,
|
||
|
name: 'stack',
|
||
|
version: '1.2'
|
||
|
});
|
||
|
})(jQuery);
|
||
|
|
||
|
/* Flot plugin for showing crosshairs when the mouse hovers over the plot.
|
||
|
|
||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||
|
Licensed under the MIT license.
|
||
|
|
||
|
The plugin supports these options:
|
||
|
|
||
|
crosshair: {
|
||
|
mode: null or "x" or "y" or "xy"
|
||
|
color: color
|
||
|
lineWidth: number
|
||
|
}
|
||
|
|
||
|
Set the mode to one of "x", "y" or "xy". The "x" mode enables a vertical
|
||
|
crosshair that lets you trace the values on the x axis, "y" enables a
|
||
|
horizontal crosshair and "xy" enables them both. "color" is the color of the
|
||
|
crosshair (default is "rgba(170, 0, 0, 0.80)"), "lineWidth" is the width of
|
||
|
the drawn lines (default is 1).
|
||
|
|
||
|
The plugin also adds four public methods:
|
||
|
|
||
|
- setCrosshair( pos )
|
||
|
|
||
|
Set the position of the crosshair. Note that this is cleared if the user
|
||
|
moves the mouse. "pos" is in coordinates of the plot and should be on the
|
||
|
form { x: xpos, y: ypos } (you can use x2/x3/... if you're using multiple
|
||
|
axes), which is coincidentally the same format as what you get from a
|
||
|
"plothover" event. If "pos" is null, the crosshair is cleared.
|
||
|
|
||
|
- clearCrosshair()
|
||
|
|
||
|
Clear the crosshair.
|
||
|
|
||
|
- lockCrosshair(pos)
|
||
|
|
||
|
Cause the crosshair to lock to the current location, no longer updating if
|
||
|
the user moves the mouse. Optionally supply a position (passed on to
|
||
|
setCrosshair()) to move it to.
|
||
|
|
||
|
Example usage:
|
||
|
|
||
|
var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } };
|
||
|
$("#graph").bind( "plothover", function ( evt, position, item ) {
|
||
|
if ( item ) {
|
||
|
// Lock the crosshair to the data point being hovered
|
||
|
myFlot.lockCrosshair({
|
||
|
x: item.datapoint[ 0 ],
|
||
|
y: item.datapoint[ 1 ]
|
||
|
});
|
||
|
} else {
|
||
|
// Return normal crosshair operation
|
||
|
myFlot.unlockCrosshair();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
- unlockCrosshair()
|
||
|
|
||
|
Free the crosshair to move again after locking it.
|
||
|
*/
|
||
|
|
||
|
(function ($) {
|
||
|
var options = {
|
||
|
crosshair: {
|
||
|
mode: null, // one of null, "x", "y" or "xy",
|
||
|
color: "rgba(170, 0, 0, 0.80)",
|
||
|
lineWidth: 1
|
||
|
}
|
||
|
};
|
||
|
|
||
|
function init(plot) {
|
||
|
// position of crosshair in pixels
|
||
|
var crosshair = {x: -1, y: -1, locked: false, highlighted: false};
|
||
|
|
||
|
plot.setCrosshair = function setCrosshair(pos) {
|
||
|
if (!pos) {
|
||
|
crosshair.x = -1;
|
||
|
} else {
|
||
|
var o = plot.p2c(pos);
|
||
|
crosshair.x = Math.max(0, Math.min(o.left, plot.width()));
|
||
|
crosshair.y = Math.max(0, Math.min(o.top, plot.height()));
|
||
|
}
|
||
|
|
||
|
plot.triggerRedrawOverlay();
|
||
|
};
|
||
|
|
||
|
plot.clearCrosshair = plot.setCrosshair; // passes null for pos
|
||
|
|
||
|
plot.lockCrosshair = function lockCrosshair(pos) {
|
||
|
if (pos) {
|
||
|
plot.setCrosshair(pos);
|
||
|
}
|
||
|
|
||
|
crosshair.locked = true;
|
||
|
};
|
||
|
|
||
|
plot.unlockCrosshair = function unlockCrosshair() {
|
||
|
crosshair.locked = false;
|
||
|
crosshair.rect = null;
|
||
|
};
|
||
|
|
||
|
function onMouseOut(e) {
|
||
|
if (crosshair.locked) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (crosshair.x !== -1) {
|
||
|
crosshair.x = -1;
|
||
|
plot.triggerRedrawOverlay();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function onMouseMove(e) {
|
||
|
var offset = plot.offset();
|
||
|
if (crosshair.locked) {
|
||
|
var mouseX = Math.max(0, Math.min(e.pageX - offset.left, plot.width()));
|
||
|
var mouseY = Math.max(0, Math.min(e.pageY - offset.top, plot.height()));
|
||
|
|
||
|
if ((mouseX > crosshair.x - 4) && (mouseX < crosshair.x + 4) && (mouseY > crosshair.y - 4) && (mouseY < crosshair.y + 4)) {
|
||
|
if (!crosshair.highlighted) {
|
||
|
crosshair.highlighted = true;
|
||
|
plot.triggerRedrawOverlay();
|
||
|
}
|
||
|
} else {
|
||
|
if (crosshair.highlighted) {
|
||
|
crosshair.highlighted = false;
|
||
|
plot.triggerRedrawOverlay();
|
||
|
}
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (plot.getSelection && plot.getSelection()) {
|
||
|
crosshair.x = -1; // hide the crosshair while selecting
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width()));
|
||
|
crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height()));
|
||
|
plot.triggerRedrawOverlay();
|
||
|
}
|
||
|
|
||
|
plot.hooks.bindEvents.push(function (plot, eventHolder) {
|
||
|
if (!plot.getOptions().crosshair.mode) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
eventHolder.mouseout(onMouseOut);
|
||
|
eventHolder.mousemove(onMouseMove);
|
||
|
});
|
||
|
|
||
|
plot.hooks.drawOverlay.push(function (plot, ctx) {
|
||
|
var c = plot.getOptions().crosshair;
|
||
|
if (!c.mode) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var plotOffset = plot.getPlotOffset();
|
||
|
|
||
|
ctx.save();
|
||
|
ctx.translate(plotOffset.left, plotOffset.top);
|
||
|
|
||
|
if (crosshair.x !== -1) {
|
||
|
var adj = plot.getOptions().crosshair.lineWidth % 2 ? 0.5 : 0;
|
||
|
|
||
|
ctx.strokeStyle = c.color;
|
||
|
ctx.lineWidth = c.lineWidth;
|
||
|
ctx.lineJoin = "round";
|
||
|
|
||
|
ctx.beginPath();
|
||
|
if (c.mode.indexOf("x") !== -1) {
|
||
|
var drawX = Math.floor(crosshair.x) + adj;
|
||
|
ctx.moveTo(drawX, 0);
|
||
|
ctx.lineTo(drawX, plot.height());
|
||
|
}
|
||
|
if (c.mode.indexOf("y") !== -1) {
|
||
|
var drawY = Math.floor(crosshair.y) + adj;
|
||
|
ctx.moveTo(0, drawY);
|
||
|
ctx.lineTo(plot.width(), drawY);
|
||
|
}
|
||
|
if (crosshair.locked) {
|
||
|
if (crosshair.highlighted) ctx.fillStyle = 'orange';
|
||
|
else ctx.fillStyle = c.color;
|
||
|
ctx.fillRect(Math.floor(crosshair.x) + adj - 4, Math.floor(crosshair.y) + adj - 4, 8, 8);
|
||
|
}
|
||
|
ctx.stroke();
|
||
|
}
|
||
|
ctx.restore();
|
||
|
});
|
||
|
|
||
|
plot.hooks.shutdown.push(function (plot, eventHolder) {
|
||
|
eventHolder.unbind("mouseout", onMouseOut);
|
||
|
eventHolder.unbind("mousemove", onMouseMove);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
$.plot.plugins.push({
|
||
|
init: init,
|
||
|
options: options,
|
||
|
name: 'crosshair',
|
||
|
version: '1.0'
|
||
|
});
|
||
|
})(jQuery);
|
||
|
|
||
|
/*
|
||
|
Axis label plugin for flot
|
||
|
|
||
|
Derived from:
|
||
|
Axis Labels Plugin for flot.
|
||
|
http://github.com/markrcote/flot-axislabels
|
||
|
|
||
|
Original code is Copyright (c) 2010 Xuan Luo.
|
||
|
Original code was released under the GPLv3 license by Xuan Luo, September 2010.
|
||
|
Original code was rereleased under the MIT license by Xuan Luo, April 2012.
|
||
|
|
||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||
|
a copy of this software and associated documentation files (the
|
||
|
"Software"), to deal in the Software without restriction, including
|
||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||
|
permit persons to whom the Software is furnished to do so, subject to
|
||
|
the following conditions:
|
||
|
|
||
|
The above copyright notice and this permission notice shall be
|
||
|
included in all copies or substantial portions of the Software.
|
||
|
|
||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||
|
*/
|
||
|
|
||
|
(function($) {
|
||
|
"use strict";
|
||
|
|
||
|
var options = {
|
||
|
axisLabels: {
|
||
|
show: true
|
||
|
}
|
||
|
};
|
||
|
|
||
|
function AxisLabel(axisName, position, padding, placeholder, axisLabel, surface) {
|
||
|
this.axisName = axisName;
|
||
|
this.position = position;
|
||
|
this.padding = padding;
|
||
|
this.placeholder = placeholder;
|
||
|
this.axisLabel = axisLabel;
|
||
|
this.surface = surface;
|
||
|
this.width = 0;
|
||
|
this.height = 0;
|
||
|
this.elem = null;
|
||
|
}
|
||
|
|
||
|
AxisLabel.prototype.calculateSize = function() {
|
||
|
var axisId = this.axisName + 'Label',
|
||
|
layerId = axisId + 'Layer',
|
||
|
className = axisId + ' axisLabels';
|
||
|
|
||
|
var info = this.surface.getTextInfo(layerId, this.axisLabel, className);
|
||
|
this.labelWidth = info.width;
|
||
|
this.labelHeight = info.height;
|
||
|
|
||
|
if (this.position === 'left' || this.position === 'right') {
|
||
|
this.width = this.labelHeight + this.padding;
|
||
|
this.height = 0;
|
||
|
} else {
|
||
|
this.width = 0;
|
||
|
this.height = this.labelHeight + this.padding;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
AxisLabel.prototype.transforms = function(degrees, x, y, svgLayer) {
|
||
|
var transforms = [], translate, rotate;
|
||
|
if (x !== 0 || y !== 0) {
|
||
|
translate = svgLayer.createSVGTransform();
|
||
|
translate.setTranslate(x, y);
|
||
|
transforms.push(translate);
|
||
|
}
|
||
|
if (degrees !== 0) {
|
||
|
rotate = svgLayer.createSVGTransform();
|
||
|
var centerX = Math.round(this.labelWidth / 2),
|
||
|
centerY = 0;
|
||
|
rotate.setRotate(degrees, centerX, centerY);
|
||
|
transforms.push(rotate);
|
||
|
}
|
||
|
|
||
|
return transforms;
|
||
|
};
|
||
|
|
||
|
AxisLabel.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;
|
||
|
offsets.y = box.height / 2 + box.top;
|
||
|
} else if (this.position === 'right') {
|
||
|
offsets.degrees = 90;
|
||
|
offsets.x = box.left + box.width - this.labelWidth / 2;
|
||
|
offsets.y = box.height / 2 + box.top;
|
||
|
}
|
||
|
offsets.x = Math.round(offsets.x);
|
||
|
offsets.y = Math.round(offsets.y);
|
||
|
|
||
|
return offsets;
|
||
|
};
|
||
|
|
||
|
AxisLabel.prototype.cleanup = function() {
|
||
|
var axisId = this.axisName + 'Label',
|
||
|
layerId = axisId + 'Layer',
|
||
|
className = axisId + ' axisLabels';
|
||
|
this.surface.removeText(layerId, 0, 0, this.axisLabel, className);
|
||
|
};
|
||
|
|
||
|
AxisLabel.prototype.draw = function(box) {
|
||
|
var axisId = this.axisName + 'Label',
|
||
|
layerId = axisId + 'Layer',
|
||
|
className = axisId + ' axisLabels',
|
||
|
offsets = this.calculateOffsets(box),
|
||
|
style = {
|
||
|
position: 'absolute',
|
||
|
bottom: '',
|
||
|
right: '',
|
||
|
display: 'inline-block',
|
||
|
'white-space': 'nowrap'
|
||
|
};
|
||
|
|
||
|
var layer = this.surface.getSVGLayer(layerId);
|
||
|
var transforms = this.transforms(offsets.degrees, offsets.x, offsets.y, layer.parentNode);
|
||
|
|
||
|
this.surface.addText(layerId, 0, 0, this.axisLabel, className, undefined, undefined, undefined, undefined, transforms);
|
||
|
this.surface.render();
|
||
|
Object.keys(style).forEach(function(key) {
|
||
|
layer.style[key] = style[key];
|
||
|
});
|
||
|
};
|
||
|
|
||
|
function init(plot) {
|
||
|
plot.hooks.processOptions.push(function(plot, options) {
|
||
|
if (!options.axisLabels.show) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var axisLabels = {};
|
||
|
var defaultPadding = 2; // padding between axis and tick labels
|
||
|
|
||
|
plot.hooks.axisReserveSpace.push(function(plot, axis) {
|
||
|
var opts = axis.options;
|
||
|
var axisName = axis.direction + axis.n;
|
||
|
|
||
|
axis.labelHeight += axis.boxPosition.centerY;
|
||
|
axis.labelWidth += axis.boxPosition.centerX;
|
||
|
|
||
|
if (!opts || !opts.axisLabel || !axis.show) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var padding = opts.axisLabelPadding === undefined
|
||
|
? defaultPadding
|
||
|
: opts.axisLabelPadding;
|
||
|
|
||
|
var axisLabel = axisLabels[axisName];
|
||
|
if (!axisLabel) {
|
||
|
axisLabel = new AxisLabel(axisName,
|
||
|
opts.position, padding,
|
||
|
plot.getPlaceholder()[0], opts.axisLabel, plot.getSurface());
|
||
|
axisLabels[axisName] = axisLabel;
|
||
|
}
|
||
|
|
||
|
axisLabel.calculateSize();
|
||
|
|
||
|
// Incrementing the sizes of the tick labels.
|
||
|
axis.labelHeight += axisLabel.height;
|
||
|
axis.labelWidth += axisLabel.width;
|
||
|
});
|
||
|
|
||
|
// TODO - use the drawAxis hook
|
||
|
plot.hooks.draw.push(function(plot, ctx) {
|
||
|
$.each(plot.getAxes(), function(flotAxisName, axis) {
|
||
|
var opts = axis.options;
|
||
|
if (!opts || !opts.axisLabel || !axis.show) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var axisName = axis.direction + axis.n;
|
||
|
axisLabels[axisName].draw(axis.box);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
plot.hooks.shutdown.push(function(plot, eventHolder) {
|
||
|
for (var axisName in axisLabels) {
|
||
|
axisLabels[axisName].cleanup();
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
};
|
||
|
|
||
|
$.plot.plugins.push({
|
||
|
init: init,
|
||
|
options: options,
|
||
|
name: 'axisLabels',
|
||
|
version: '3.0'
|
||
|
});
|
||
|
})(jQuery);
|