Skip to content

Instantly share code, notes, and snippets.

@rlr
Created January 27, 2012 19:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rlr/1690515 to your computer and use it in GitHub Desktop.
Save rlr/1690515 to your computer and use it in GitHub Desktop.
dotjs'ified bugzilla agile for hackability.
var settings = {
agile_backlog: true
};
var css = ".graph { width: 300px; height: 300px; float: left; } .graph canvas.overlay { background: none repeat scroll 0 0 rgba(0, 0, 0, 0) !important; }";
$('head').append($('<style>').text(css));
function agileBacklog() {
if(settings.agile_backlog) {
var $wb = $('td.bz_status_whiteboard_column');
if(!$wb.length) return;
var bugs = [];
var components_data = [];
var users_data = [];
var closed_statuses = ["RESO","VERI"];
var closed_points = 0;
var closed_stories = 0;
var open_points = 0;
var open_stories = 0;
var assigned_points = 0;
var assigned_stories = 0;
var total_stories = 0;
var total_points = 0;
var points_dist_data = {};
$wb.each(function(){
total_stories += 1;
var bug = {};
bug.status = $.trim($(this).siblings('.bz_bug_status_column').text());
bug.tags = $.trim($(this).text()).split(" ");
bug.elements = [];
for (var i=0, j=bug.tags.length; i<j; i++) {
var tokens = bug.tags[i].split("=");
bug.elements[tokens[0]] = tokens[1];
}
bug.points = 0;
if (bug.elements.p && typeof(parseInt(bug.elements.p,0)) == "number") {
bug.points = parseInt(bug.elements.p,0);
total_points += bug.points;
}
if ($.inArray(bug.status, closed_statuses) > -1){
closed_stories++;
closed_points += bug.points;
} else {
if ($.trim($(this).siblings('.bz_assigned_to_column').text()) == "nobody@mozilla.org") {
open_stories++;
open_points += bug.points;
} else {
assigned_stories++;
assigned_points += bug.points;
}
}
if(points_dist_data[bug.points]) {
points_dist_data[bug.points] += bug.points;
} else {
points_dist_data[bug.points] = bug.points;
}
// build components_data
if (bug.elements.c && bug.elements.c !== "") {
var component = {}, existing_component_idx = -1;
for (i=0, j=components_data.length; i<j; i++) {
if (bug.elements.c == components_data[i].label) {
existing_component_idx = i;
}
}
if (existing_component_idx > -1) {
component = components_data[existing_component_idx];
component.data += bug.points;
} else {
component.label = bug.elements.c;
component.data = bug.points;
components_data.push(component);
}
}
// build users_data
if (bug.elements.u && bug.elements.u !== "") {
var user = {}, existing_user_idx = -1;
for (i=0, j=users_data.length; i<j; i++) {
if (bug.elements.u == users_data[i].label) {
existing_user_idx = i;
}
}
if (existing_user_idx > -1) {
user = users_data[existing_user_idx];
user.data += bug.points;
} else {
user.label = bug.elements.u;
user.data = bug.points;
users_data.push(user);
}
}
bugs.push(bug);
});
$('table.bz_buglist').append('<tr><td colspan="99" align="right"><div><div id="pointsGraph" class="graph"></div><div id="componentsGraph" class="graph"></div><div id="usersGraph" class="graph"></div><div id="pointsDistGraph" class="graph"></div><div>Open Stories: ' + open_stories + '<br/>Open points: ' + open_points + '<br/><div>Assigned Stories: ' + assigned_stories + '<br/>Assigned points: ' + assigned_points + '<br/>Closed Stories: ' + closed_stories + '<br/>Closed points: ' + closed_points + '<br/>Total Stories: ' + total_stories + '<br/>Total Points: ' + total_points + '</div></div></td></tr>');
var points_data = [
{label:"Open", data:open_points, color: "rgb(237, 194, 64)"},
{label:"Closed", data:closed_points, color: "rgb(77, 167, 77)"},
{label:"Assigned", data:assigned_points, color: "rgb(175, 216, 248)"}
];
$.plot($('#pointsGraph'), points_data, {series:{pie:{show:true}},legend:{show:false}});
$.plot($('#componentsGraph'), components_data, {series:{pie:{show:true}},legend:{show:false}});
$.plot($('#usersGraph'), users_data, {series:{pie:{show:true}},legend:{show:false}});
points_data = [];
$.each(points_dist_data, function(p,v){
points_data.push({label:p + 'pts',data:v});
});
$.plot($('#pointsDistGraph'), points_data, {series:{pie:{show:true}},legend:{show:false}});
console.log("Open Stories: " + open_stories);
console.log("Open points: " + open_points);
console.log("Closed Stories: " + closed_stories);
console.log("Closed points: " + closed_points);
}
}
preReqs();
agileBacklog();
//** required libs **//
function preReqs() {
/*! Javascript plotting library for jQuery, v. 0.7.
*
* Released under the MIT license by IOLA, December 2007.
*
*/
// first an inline dependency, jquery.colorhelpers.js, we inline it here
// for convenience
/* Plugin for jQuery for working with colors.
*
* Version 1.1.
*
* Inspiration from jQuery color animation plugin by John Resig.
*
* Released under the MIT license by Ole Laursen, October 2009.
*
* Examples:
*
* $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
* var c = $.color.extract($("#mydiv"), 'background-color');
* console.log(c.r, c.g, c.b, c.a);
* $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
*
* Note that .scale() and .add() return the same modified object
* instead of making a new one.
*
* V. 1.1: Fix error handling so e.g. parsing an empty string does
* produce a color rather than just crashing.
*/
(function(B){B.color={};B.color.make=function(F,E,C,D){var G={};G.r=F||0;G.g=E||0;G.b=C||0;G.a=D!=null?D:1;G.add=function(J,I){for(var H=0;H<J.length;++H){G[J.charAt(H)]+=I}return G.normalize()};G.scale=function(J,I){for(var H=0;H<J.length;++H){G[J.charAt(H)]*=I}return G.normalize()};G.toString=function(){if(G.a>=1){return"rgb("+[G.r,G.g,G.b].join(",")+")"}else{return"rgba("+[G.r,G.g,G.b,G.a].join(",")+")"}};G.normalize=function(){function H(J,K,I){return K<J?J:(K>I?I:K)}G.r=H(0,parseInt(G.r),255);G.g=H(0,parseInt(G.g),255);G.b=H(0,parseInt(G.b),255);G.a=H(0,G.a,1);return G};G.clone=function(){return B.color.make(G.r,G.b,G.g,G.a)};return G.normalize()};B.color.extract=function(D,C){var E;do{E=D.css(C).toLowerCase();if(E!=""&&E!="transparent"){break}D=D.parent()}while(!B.nodeName(D.get(0),"body"));if(E=="rgba(0, 0, 0, 0)"){E="transparent"}return B.color.parse(E)};B.color.parse=function(F){var E,C=B.color.make;if(E=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10))}if(E=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10),parseFloat(E[4]))}if(E=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55)}if(E=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55,parseFloat(E[4]))}if(E=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(F)){return C(parseInt(E[1],16),parseInt(E[2],16),parseInt(E[3],16))}if(E=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(F)){return C(parseInt(E[1]+E[1],16),parseInt(E[2]+E[2],16),parseInt(E[3]+E[3],16))}var D=B.trim(F).toLowerCase();if(D=="transparent"){return C(255,255,255,0)}else{E=A[D]||[0,0,0];return C(E[0],E[1],E[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})($);
// the actual Flot code
(function($) {
function Plot(placeholder, data_, options_, plugins) {
// data is on the form:
// [ series1, series2 ... ]
// where series is either just the data as [ [x1, y1], [x2, y2], ... ]
// or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... }
var series = [],
options = {
// the color theme used for graphs
colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"],
legend: {
show: true,
noColumns: 1, // number of colums in legend table
labelFormatter: null, // fn: string -> string
labelBoxBorderColor: "#ccc", // border color for the little label boxes
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
backgroundColor: null, // null means auto-detect
backgroundOpacity: 0.85 // set to 0 to avoid background
},
xaxis: {
show: null, // null = auto-detect, true = always, false = never
position: "bottom", // or "top"
mode: null, // null or "time"
color: null, // base color, labels, ticks
tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)"
transform: null, // null or f: number -> number to transform axis
inverseTransform: null, // if transform is set, this should be the inverse function
min: null, // min. value to show, null means set automatically
max: null, // max. value to show, null means set automatically
autoscaleMargin: null, // margin in % to add if auto-setting min/max
ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks
tickFormatter: null, // fn: number -> string
labelWidth: null, // size of tick labels in pixels
labelHeight: null,
reserveSpace: null, // whether to reserve space even if axis isn't shown
tickLength: null, // size in pixels of ticks, or "full" for whole line
alignTicksWithAxis: null, // axis number or null for no sync
// mode specific options
tickDecimals: null, // no. of decimals, null means auto
tickSize: null, // number or [number, "unit"]
minTickSize: null, // number or [number, "unit"]
monthNames: null, // list of names of months
timeformat: null, // format string to use
twelveHourClock: false // 12 or 24 time in time mode
},
yaxis: {
autoscaleMargin: 0.02,
position: "left" // or "right"
},
xaxes: [],
yaxes: [],
series: {
points: {
show: false,
radius: 3,
lineWidth: 2, // in pixels
fill: true,
fillColor: "#ffffff",
symbol: "circle" // or callback
},
lines: {
// we don't put in show: false so we can see
// whether lines were actively disabled
lineWidth: 2, // in pixels
fill: false,
fillColor: null,
steps: false
},
bars: {
show: false,
lineWidth: 2, // in pixels
barWidth: 1, // in units of the x axis
fill: true,
fillColor: null,
align: "left", // or "center"
horizontal: false
},
shadowSize: 3
},
grid: {
show: true,
aboveData: false,
color: "#545454", // primary color used for outline and labels
backgroundColor: null, // null for transparent, else color
borderColor: null, // set if different from the grid color
tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)"
labelMargin: 5, // in pixels
axisMargin: 8, // in pixels
borderWidth: 2, // in pixels
minBorderMargin: null, // in pixels, null means taken from points radius
markings: null, // array of ranges or fn: axes -> array of ranges
markingsColor: "#f4f4f4",
markingsLineWidth: 2,
// interactive stuff
clickable: false,
hoverable: false,
autoHighlight: true, // highlight in case mouse is near
mouseActiveRadius: 10 // how far the mouse can be away to activate an item
},
hooks: {}
},
canvas = null, // the canvas for the plot itself
overlay = null, // canvas for interactive stuff on top of plot
eventHolder = null, // jQuery object that events should be bound to
ctx = null, octx = null,
xaxes = [], yaxes = [],
plotOffset = { left: 0, right: 0, top: 0, bottom: 0},
canvasWidth = 0, canvasHeight = 0,
plotWidth = 0, plotHeight = 0,
hooks = {
processOptions: [],
processRawData: [],
processDatapoints: [],
drawSeries: [],
draw: [],
bindEvents: [],
drawOverlay: [],
shutdown: []
},
plot = this;
// public functions
plot.setData = setData;
plot.setupGrid = setupGrid;
plot.draw = draw;
plot.getPlaceholder = function() { return placeholder; };
plot.getCanvas = function() { return canvas; };
plot.getPlotOffset = function() { return plotOffset; };
plot.width = function () { return plotWidth; };
plot.height = function () { return plotHeight; };
plot.offset = function () {
var o = eventHolder.offset();
o.left += plotOffset.left;
o.top += plotOffset.top;
return o;
};
plot.getData = function () { return series; };
plot.getAxes = function () {
var res = {}, i;
$.each(xaxes.concat(yaxes), function (_, axis) {
if (axis)
res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis;
});
return res;
};
plot.getXAxes = function () { return xaxes; };
plot.getYAxes = function () { return yaxes; };
plot.c2p = canvasToAxisCoords;
plot.p2c = axisToCanvasCoords;
plot.getOptions = function () { return options; };
plot.highlight = highlight;
plot.unhighlight = unhighlight;
plot.triggerRedrawOverlay = triggerRedrawOverlay;
plot.pointOffset = function(point) {
return {
left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left),
top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top)
};
};
plot.shutdown = shutdown;
plot.resize = function () {
getCanvasDimensions();
resizeCanvas(canvas);
resizeCanvas(overlay);
};
// public attributes
plot.hooks = hooks;
// initialize
initPlugins(plot);
parseOptions(options_);
setupCanvases();
setData(data_);
setupGrid();
draw();
bindEvents();
function executeHooks(hook, args) {
args = [plot].concat(args);
for (var i = 0; i < hook.length; ++i)
hook[i].apply(this, args);
}
function initPlugins() {
for (var i = 0; i < plugins.length; ++i) {
var p = plugins[i];
p.init(plot);
if (p.options)
$.extend(true, options, p.options);
}
}
function parseOptions(opts) {
var i;
$.extend(true, options, opts);
if (options.xaxis.color == null)
options.xaxis.color = options.grid.color;
if (options.yaxis.color == null)
options.yaxis.color = options.grid.color;
if (options.xaxis.tickColor == null) // backwards-compatibility
options.xaxis.tickColor = options.grid.tickColor;
if (options.yaxis.tickColor == null) // backwards-compatibility
options.yaxis.tickColor = options.grid.tickColor;
if (options.grid.borderColor == null)
options.grid.borderColor = options.grid.color;
if (options.grid.tickColor == null)
options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString();
// fill in defaults in axes, copy at least always the
// first as the rest of the code assumes it'll be there
for (i = 0; i < Math.max(1, options.xaxes.length); ++i)
options.xaxes[i] = $.extend(true, {}, options.xaxis, options.xaxes[i]);
for (i = 0; i < Math.max(1, options.yaxes.length); ++i)
options.yaxes[i] = $.extend(true, {}, options.yaxis, options.yaxes[i]);
// backwards compatibility, to be removed in future
if (options.xaxis.noTicks && options.xaxis.ticks == null)
options.xaxis.ticks = options.xaxis.noTicks;
if (options.yaxis.noTicks && options.yaxis.ticks == null)
options.yaxis.ticks = options.yaxis.noTicks;
if (options.x2axis) {
options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis);
options.xaxes[1].position = "top";
}
if (options.y2axis) {
options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis);
options.yaxes[1].position = "right";
}
if (options.grid.coloredAreas)
options.grid.markings = options.grid.coloredAreas;
if (options.grid.coloredAreasColor)
options.grid.markingsColor = options.grid.coloredAreasColor;
if (options.lines)
$.extend(true, options.series.lines, options.lines);
if (options.points)
$.extend(true, options.series.points, options.points);
if (options.bars)
$.extend(true, options.series.bars, options.bars);
if (options.shadowSize != null)
options.series.shadowSize = options.shadowSize;
// save options on axes for future reference
for (i = 0; i < options.xaxes.length; ++i)
getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i];
for (i = 0; i < options.yaxes.length; ++i)
getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i];
// add hooks from options
for (var n in hooks)
if (options.hooks[n] && options.hooks[n].length)
hooks[n] = hooks[n].concat(options.hooks[n]);
executeHooks(hooks.processOptions, [options]);
}
function setData(d) {
series = parseData(d);
fillInSeriesOptions();
processData();
}
function parseData(d) {
var res = [];
for (var i = 0; i < d.length; ++i) {
var s = $.extend(true, {}, options.series);
if (d[i].data != null) {
s.data = d[i].data; // move the data instead of deep-copy
delete d[i].data;
$.extend(true, s, d[i]);
d[i].data = s.data;
}
else
s.data = d[i];
res.push(s);
}
return res;
}
function axisNumber(obj, coord) {
var a = obj[coord + "axis"];
if (typeof a == "object") // if we got a real axis, extract number
a = a.n;
if (typeof a != "number")
a = 1; // default to first axis
return a;
}
function allAxes() {
// return flat array without annoying null entries
return $.grep(xaxes.concat(yaxes), function (a) { return a; });
}
function canvasToAxisCoords(pos) {
// return an object with x/y corresponding to all used axes
var res = {}, i, axis;
for (i = 0; i < xaxes.length; ++i) {
axis = xaxes[i];
if (axis && axis.used)
res["x" + axis.n] = axis.c2p(pos.left);
}
for (i = 0; i < yaxes.length; ++i) {
axis = yaxes[i];
if (axis && axis.used)
res["y" + axis.n] = axis.c2p(pos.top);
}
if (res.x1 !== undefined)
res.x = res.x1;
if (res.y1 !== undefined)
res.y = res.y1;
return res;
}
function axisToCanvasCoords(pos) {
// get canvas coords from the first pair of x/y found in pos
var res = {}, i, axis, key;
for (i = 0; i < xaxes.length; ++i) {
axis = xaxes[i];
if (axis && axis.used) {
key = "x" + axis.n;
if (pos[key] == null && axis.n == 1)
key = "x";
if (pos[key] != null) {
res.left = axis.p2c(pos[key]);
break;
}
}
}
for (i = 0; i < yaxes.length; ++i) {
axis = yaxes[i];
if (axis && axis.used) {
key = "y" + axis.n;
if (pos[key] == null && axis.n == 1)
key = "y";
if (pos[key] != null) {
res.top = axis.p2c(pos[key]);
break;
}
}
}
return res;
}
function getOrCreateAxis(axes, number) {
if (!axes[number - 1])
axes[number - 1] = {
n: number, // save the number for future reference
direction: axes == xaxes ? "x" : "y",
options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis)
};
return axes[number - 1];
}
function fillInSeriesOptions() {
var i;
// collect what we already got of colors
var neededColors = series.length,
usedColors = [],
assignedColors = [];
for (i = 0; i < series.length; ++i) {
var sc = series[i].color;
if (sc != null) {
--neededColors;
if (typeof sc == "number")
assignedColors.push(sc);
else
usedColors.push($.color.parse(series[i].color));
}
}
// we might need to generate more colors if higher indices
// are assigned
for (i = 0; i < assignedColors.length; ++i) {
neededColors = Math.max(neededColors, assignedColors[i] + 1);
}
// produce colors as needed
var colors = [], variation = 0;
i = 0;
while (colors.length < neededColors) {
var c;
if (options.colors.length == i) // check degenerate case
c = $.color.make(100, 100, 100);
else
c = $.color.parse(options.colors[i]);
// vary color if needed
var sign = variation % 2 == 1 ? -1 : 1;
c.scale('rgb', 1 + sign * Math.ceil(variation / 2) * 0.2)
// FIXME: if we're getting to close to something else,
// we should probably skip this one
colors.push(c);
++i;
if (i >= options.colors.length) {
i = 0;
++variation;
}
}
// fill in the options
var colori = 0, s;
for (i = 0; i < series.length; ++i) {
s = series[i];
// assign colors
if (s.color == null) {
s.color = colors[colori].toString();
++colori;
}
else if (typeof s.color == "number")
s.color = colors[s.color].toString();
// turn on lines automatically in case nothing is set
if (s.lines.show == null) {
var v, show = true;
for (v in s)
if (s[v] && s[v].show) {
show = false;
break;
}
if (show)
s.lines.show = true;
}
// setup axes
s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x"));
s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y"));
}
}
function processData() {
var topSentry = Number.POSITIVE_INFINITY,
bottomSentry = Number.NEGATIVE_INFINITY,
fakeInfinity = Number.MAX_VALUE,
i, j, k, m, length,
s, points, ps, x, y, axis, val, f, p;
function updateAxis(axis, min, max) {
if (min < axis.datamin && min != -fakeInfinity)
axis.datamin = min;
if (max > axis.datamax && max != fakeInfinity)
axis.datamax = max;
}
$.each(allAxes(), function (_, axis) {
// init axis
axis.datamin = topSentry;
axis.datamax = bottomSentry;
axis.used = false;
});
for (i = 0; i < series.length; ++i) {
s = series[i];
s.datapoints = { points: [] };
executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]);
}
// first pass: clean and copy data
for (i = 0; i < series.length; ++i) {
s = series[i];
var data = s.data, format = s.datapoints.format;
if (!format) {
format = [];
// find out how to copy
format.push({ x: true, number: true, required: true });
format.push({ y: true, number: true, required: true });
if (s.bars.show || (s.lines.show && s.lines.fill)) {
format.push({ y: true, number: true, required: false, defaultValue: 0 });
if (s.bars.horizontal) {
delete format[format.length - 1].y;
format[format.length - 1].x = true;
}
}
s.datapoints.format = format;
}
if (s.datapoints.pointsize != null)
continue; // already filled in
s.datapoints.pointsize = format.length;
ps = s.datapoints.pointsize;
points = s.datapoints.points;
insertSteps = s.lines.show && s.lines.steps;
s.xaxis.used = s.yaxis.used = true;
for (j = k = 0; j < data.length; ++j, k += ps) {
p = data[j];
var nullify = p == null;
if (!nullify) {
for (m = 0; m < ps; ++m) {
val = p[m];
f = format[m];
if (f) {
if (f.number && val != null) {
val = +val; // convert to number
if (isNaN(val))
val = null;
else if (val == Infinity)
val = fakeInfinity;
else if (val == -Infinity)
val = -fakeInfinity;
}
if (val == null) {
if (f.required)
nullify = true;
if (f.defaultValue != null)
val = f.defaultValue;
}
}
points[k + m] = val;
}
}
if (nullify) {
for (m = 0; m < ps; ++m) {
val = points[k + m];
if (val != null) {
f = format[m];
// extract min/max info
if (f.x)
updateAxis(s.xaxis, val, val);
if (f.y)
updateAxis(s.yaxis, val, val);
}
points[k + m] = null;
}
}
else {
// a little bit of line specific stuff that
// perhaps shouldn't be here, but lacking
// better means...
if (insertSteps && k > 0
&& points[k - ps] != null
&& points[k - ps] != points[k]
&& points[k - ps + 1] != points[k + 1]) {
// copy the point to make room for a middle point
for (m = 0; m < ps; ++m)
points[k + ps + m] = points[k + m];
// middle point has same y
points[k + 1] = points[k - ps + 1];
// we've added a point, better reflect that
k += ps;
}
}
}
}
// give the hooks a chance to run
for (i = 0; i < series.length; ++i) {
s = series[i];
executeHooks(hooks.processDatapoints, [ s, s.datapoints]);
}
// second pass: find datamax/datamin for auto-scaling
for (i = 0; i < series.length; ++i) {
s = series[i];
points = s.datapoints.points,
ps = s.datapoints.pointsize;
var xmin = topSentry, ymin = topSentry,
xmax = bottomSentry, ymax = bottomSentry;
for (j = 0; j < points.length; j += ps) {
if (points[j] == null)
continue;
for (m = 0; m < ps; ++m) {
val = points[j + m];
f = format[m];
if (!f || val == fakeInfinity || val == -fakeInfinity)
continue;
if (f.x) {
if (val < xmin)
xmin = val;
if (val > xmax)
xmax = val;
}
if (f.y) {
if (val < ymin)
ymin = val;
if (val > ymax)
ymax = val;
}
}
}
if (s.bars.show) {
// make sure we got room for the bar on the dancing floor
var delta = s.bars.align == "left" ? 0 : -s.bars.barWidth/2;
if (s.bars.horizontal) {
ymin += delta;
ymax += delta + s.bars.barWidth;
}
else {
xmin += delta;
xmax += delta + s.bars.barWidth;
}
}
updateAxis(s.xaxis, xmin, xmax);
updateAxis(s.yaxis, ymin, ymax);
}
$.each(allAxes(), function (_, axis) {
if (axis.datamin == topSentry)
axis.datamin = null;
if (axis.datamax == bottomSentry)
axis.datamax = null;
});
}
function makeCanvas(skipPositioning, cls) {
var c = document.createElement('canvas');
c.className = cls;
c.width = canvasWidth;
c.height = canvasHeight;
if (!skipPositioning)
$(c).css({ position: 'absolute', left: 0, top: 0 });
$(c).appendTo(placeholder);
if (!c.getContext) // excanvas hack
c = window.G_vmlCanvasManager.initElement(c);
// used for resetting in case we get replotted
c.getContext("2d").save();
return c;
}
function getCanvasDimensions() {
canvasWidth = placeholder.width();
canvasHeight = placeholder.height();
if (canvasWidth <= 0 || canvasHeight <= 0)
throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight;
}
function resizeCanvas(c) {
// resizing should reset the state (excanvas seems to be
// buggy though)
if (c.width != canvasWidth)
c.width = canvasWidth;
if (c.height != canvasHeight)
c.height = canvasHeight;
// so try to get back to the initial state (even if it's
// gone now, this should be safe according to the spec)
var cctx = c.getContext("2d");
cctx.restore();
// and save again
cctx.save();
}
function setupCanvases() {
var reused,
existingCanvas = placeholder.children("canvas.base"),
existingOverlay = placeholder.children("canvas.overlay");
if (existingCanvas.length == 0 || existingOverlay == 0) {
// init everything
placeholder.html(""); // make sure placeholder is clear
placeholder.css({ padding: 0 }); // padding messes up the positioning
if (placeholder.css("position") == 'static')
placeholder.css("position", "relative"); // for positioning labels and overlay
getCanvasDimensions();
canvas = makeCanvas(true, "base");
overlay = makeCanvas(false, "overlay"); // overlay canvas for interactive features
reused = false;
}
else {
// reuse existing elements
canvas = existingCanvas.get(0);
overlay = existingOverlay.get(0);
reused = true;
}
ctx = canvas.getContext("2d");
octx = overlay.getContext("2d");
// we include the canvas in the event holder too, because IE 7
// sometimes has trouble with the stacking order
eventHolder = $([overlay, canvas]);
if (reused) {
// run shutdown in the old plot object
placeholder.data("plot").shutdown();
// reset reused canvases
plot.resize();
// make sure overlay pixels are cleared (canvas is cleared when we redraw)
octx.clearRect(0, 0, canvasWidth, canvasHeight);
// then whack any remaining obvious garbage left
eventHolder.unbind();
placeholder.children().not([canvas, overlay]).remove();
}
// save in case we get replotted
placeholder.data("plot", plot);
}
function bindEvents() {
// bind events
if (options.grid.hoverable) {
eventHolder.mousemove(onMouseMove);
eventHolder.mouseleave(onMouseLeave);
}
if (options.grid.clickable)
eventHolder.click(onClick);
executeHooks(hooks.bindEvents, [eventHolder]);
}
function shutdown() {
if (redrawTimeout)
clearTimeout(redrawTimeout);
eventHolder.unbind("mousemove", onMouseMove);
eventHolder.unbind("mouseleave", onMouseLeave);
eventHolder.unbind("click", onClick);
executeHooks(hooks.shutdown, [eventHolder]);
}
function setTransformationHelpers(axis) {
// set helper functions on the axis, assumes plot area
// has been computed already
function identity(x) { return x; }
var s, m, t = axis.options.transform || identity,
it = axis.options.inverseTransform;
// precompute how much the axis is scaling a point
// in canvas space
if (axis.direction == "x") {
s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min));
m = Math.min(t(axis.max), t(axis.min));
}
else {
s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min));
s = -s;
m = Math.max(t(axis.max), t(axis.min));
}
// data point to canvas coordinate
if (t == identity) // slight optimization
axis.p2c = function (p) { return (p - m) * s; };
else
axis.p2c = function (p) { return (t(p) - m) * s; };
// canvas coordinate to data point
if (!it)
axis.c2p = function (c) { return m + c / s; };
else
axis.c2p = function (c) { return it(m + c / s); };
}
function measureTickLabels(axis) {
var opts = axis.options, i, ticks = axis.ticks || [], labels = [],
l, w = opts.labelWidth, h = opts.labelHeight, dummyDiv;
function makeDummyDiv(labels, width) {
return $('<div style="position:absolute;top:-10000px;' + width + 'font-size:smaller">' +
'<div class="' + axis.direction + 'Axis ' + axis.direction + axis.n + 'Axis">'
+ labels.join("") + '</div></div>')
.appendTo(placeholder);
}
if (axis.direction == "x") {
// to avoid measuring the widths of the labels (it's slow), we
// construct fixed-size boxes and put the labels inside
// them, we don't need the exact figures and the
// fixed-size box content is easy to center
if (w == null)
w = Math.floor(canvasWidth / (ticks.length > 0 ? ticks.length : 1));
// measure x label heights
if (h == null) {
labels = [];
for (i = 0; i < ticks.length; ++i) {
l = ticks[i].label;
if (l)
labels.push('<div class="tickLabel" style="float:left;width:' + w + 'px">' + l + '</div>');
}
if (labels.length > 0) {
// stick them all in the same div and measure
// collective height
labels.push('<div style="clear:left"></div>');
dummyDiv = makeDummyDiv(labels, "width:10000px;");
h = dummyDiv.height();
dummyDiv.remove();
}
}
}
else if (w == null || h == null) {
// calculate y label dimensions
for (i = 0; i < ticks.length; ++i) {
l = ticks[i].label;
if (l)
labels.push('<div class="tickLabel">' + l + '</div>');
}
if (labels.length > 0) {
dummyDiv = makeDummyDiv(labels, "");
if (w == null)
w = dummyDiv.children().width();
if (h == null)
h = dummyDiv.find("div.tickLabel").height();
dummyDiv.remove();
}
}
if (w == null)
w = 0;
if (h == null)
h = 0;
axis.labelWidth = w;
axis.labelHeight = h;
}
function allocateAxisBoxFirstPhase(axis) {
// find the bounding box of the axis by looking at label
// widths/heights and ticks, make room by diminishing the
// plotOffset
var lw = axis.labelWidth,
lh = axis.labelHeight,
pos = axis.options.position,
tickLength = axis.options.tickLength,
axismargin = options.grid.axisMargin,
padding = options.grid.labelMargin,
all = axis.direction == "x" ? xaxes : yaxes,
index;
// determine axis margin
var samePosition = $.grep(all, function (a) {
return a && a.options.position == pos && a.reserveSpace;
});
if ($.inArray(axis, samePosition) == samePosition.length - 1)
axismargin = 0; // outermost
// determine tick length - if we're innermost, we can use "full"
if (tickLength == null)
tickLength = "full";
var sameDirection = $.grep(all, function (a) {
return a && a.reserveSpace;
});
var innermost = $.inArray(axis, sameDirection) == 0;
if (!innermost && tickLength == "full")
tickLength = 5;
if (!isNaN(+tickLength))
padding += +tickLength;
// compute box
if (axis.direction == "x") {
lh += padding;
if (pos == "bottom") {
plotOffset.bottom += lh + axismargin;
axis.box = { top: canvasHeight - plotOffset.bottom, height: lh };
}
else {
axis.box = { top: plotOffset.top + axismargin, height: lh };
plotOffset.top += lh + axismargin;
}
}
else {
lw += padding;
if (pos == "left") {
axis.box = { left: plotOffset.left + axismargin, width: lw };
plotOffset.left += lw + axismargin;
}
else {
plotOffset.right += lw + axismargin;
axis.box = { left: canvasWidth - plotOffset.right, width: lw };
}
}
// save for future reference
axis.position = pos;
axis.tickLength = tickLength;
axis.box.padding = padding;
axis.innermost = innermost;
}
function allocateAxisBoxSecondPhase(axis) {
// set remaining bounding box coordinates
if (axis.direction == "x") {
axis.box.left = plotOffset.left;
axis.box.width = plotWidth;
}
else {
axis.box.top = plotOffset.top;
axis.box.height = plotHeight;
}
}
function setupGrid() {
var i, axes = allAxes();
// first calculate the plot and axis box dimensions
$.each(axes, function (_, axis) {
axis.show = axis.options.show;
if (axis.show == null)
axis.show = axis.used; // by default an axis is visible if it's got data
axis.reserveSpace = axis.show || axis.options.reserveSpace;
setRange(axis);
});
allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; });
plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0;
if (options.grid.show) {
$.each(allocatedAxes, function (_, axis) {
// make the ticks
setupTickGeneration(axis);
setTicks(axis);
snapRangeToTicks(axis, axis.ticks);
// find labelWidth/Height for axis
measureTickLabels(axis);
});
// with all dimensions in house, we can compute the
// axis boxes, start from the outside (reverse order)
for (i = allocatedAxes.length - 1; i >= 0; --i)
allocateAxisBoxFirstPhase(allocatedAxes[i]);
// make sure we've got enough space for things that
// might stick out
var minMargin = options.grid.minBorderMargin;
if (minMargin == null) {
minMargin = 0;
for (i = 0; i < series.length; ++i)
minMargin = Math.max(minMargin, series[i].points.radius + series[i].points.lineWidth/2);
}
for (var a in plotOffset) {
plotOffset[a] += options.grid.borderWidth;
plotOffset[a] = Math.max(minMargin, plotOffset[a]);
}
}
plotWidth = canvasWidth - plotOffset.left - plotOffset.right;
plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top;
// now we got the proper plotWidth/Height, we can compute the scaling
$.each(axes, function (_, axis) {
setTransformationHelpers(axis);
});
if (options.grid.show) {
$.each(allocatedAxes, function (_, axis) {
allocateAxisBoxSecondPhase(axis);
});
insertAxisLabels();
}
insertLegend();
}
function setRange(axis) {
var opts = axis.options,
min = +(opts.min != null ? opts.min : axis.datamin),
max = +(opts.max != null ? opts.max : axis.datamax),
delta = max - min;
if (delta == 0.0) {
// degenerate case
var widen = max == 0 ? 1 : 0.01;
if (opts.min == null)
min -= widen;
// always widen max if we couldn't widen min to ensure we
// don't fall into min == max which doesn't work
if (opts.max == null || opts.min != null)
max += widen;
}
else {
// consider autoscaling
var margin = opts.autoscaleMargin;
if (margin != null) {
if (opts.min == null) {
min -= delta * margin;
// make sure we don't go below zero if all values
// are positive
if (min < 0 && axis.datamin != null && axis.datamin >= 0)
min = 0;
}
if (opts.max == null) {
max += delta * margin;
if (max > 0 && axis.datamax != null && axis.datamax <= 0)
max = 0;
}
}
}
axis.min = min;
axis.max = max;
}
function setupTickGeneration(axis) {
var opts = axis.options;
// estimate number of ticks
var noTicks;
if (typeof opts.ticks == "number" && opts.ticks > 0)
noTicks = opts.ticks;
else
// heuristic based on the model a*sqrt(x) fitted to
// some data points that seemed reasonable
noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? canvasWidth : canvasHeight);
var delta = (axis.max - axis.min) / noTicks,
size, generator, unit, formatter, i, magn, norm;
if (opts.mode == "time") {
// pretty handling of time
// map of app. size of time units in milliseconds
var timeUnitSize = {
"second": 1000,
"minute": 60 * 1000,
"hour": 60 * 60 * 1000,
"day": 24 * 60 * 60 * 1000,
"month": 30 * 24 * 60 * 60 * 1000,
"year": 365.2425 * 24 * 60 * 60 * 1000
};
// the allowed tick sizes, after 1 year we use
// an integer algorithm
var spec = [
[1, "second"], [2, "second"], [5, "second"], [10, "second"],
[30, "second"],
[1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
[30, "minute"],
[1, "hour"], [2, "hour"], [4, "hour"],
[8, "hour"], [12, "hour"],
[1, "day"], [2, "day"], [3, "day"],
[0.25, "month"], [0.5, "month"], [1, "month"],
[2, "month"], [3, "month"], [6, "month"],
[1, "year"]
];
var minSize = 0;
if (opts.minTickSize != null) {
if (typeof opts.tickSize == "number")
minSize = opts.tickSize;
else
minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]];
}
for (var i = 0; i < spec.length - 1; ++i)
if (delta < (spec[i][0] * timeUnitSize[spec[i][1]]
+ spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
&& spec[i][0] * timeUnitSize[spec[i][1]] >= minSize)
break;
size = spec[i][0];
unit = spec[i][1];
// special-case the possibility of several years
if (unit == "year") {
magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10));
norm = (delta / timeUnitSize.year) / magn;
if (norm < 1.5)
size = 1;
else if (norm < 3)
size = 2;
else if (norm < 7.5)
size = 5;
else
size = 10;
size *= magn;
}
axis.tickSize = opts.tickSize || [size, unit];
generator = function(axis) {
var ticks = [],
tickSize = axis.tickSize[0], unit = axis.tickSize[1],
d = new Date(axis.min);
var step = tickSize * timeUnitSize[unit];
if (unit == "second")
d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize));
if (unit == "minute")
d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize));
if (unit == "hour")
d.setUTCHours(floorInBase(d.getUTCHours(), tickSize));
if (unit == "month")
d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize));
if (unit == "year")
d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize));
// reset smaller components
d.setUTCMilliseconds(0);
if (step >= timeUnitSize.minute)
d.setUTCSeconds(0);
if (step >= timeUnitSize.hour)
d.setUTCMinutes(0);
if (step >= timeUnitSize.day)
d.setUTCHours(0);
if (step >= timeUnitSize.day * 4)
d.setUTCDate(1);
if (step >= timeUnitSize.year)
d.setUTCMonth(0);
var carry = 0, v = Number.NaN, prev;
do {
prev = v;
v = d.getTime();
ticks.push(v);
if (unit == "month") {
if (tickSize < 1) {
// a bit complicated - we'll divide the month
// up but we need to take care of fractions
// so we don't end up in the middle of a day
d.setUTCDate(1);
var start = d.getTime();
d.setUTCMonth(d.getUTCMonth() + 1);
var end = d.getTime();
d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
carry = d.getUTCHours();
d.setUTCHours(0);
}
else
d.setUTCMonth(d.getUTCMonth() + tickSize);
}
else if (unit == "year") {
d.setUTCFullYear(d.getUTCFullYear() + tickSize);
}
else
d.setTime(v + step);
} while (v < axis.max && v != prev);
return ticks;
};
formatter = function (v, axis) {
var d = new Date(v);
// first check global format
if (opts.timeformat != null)
return $.plot.formatDate(d, opts.timeformat, opts.monthNames);
var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
var span = axis.max - axis.min;
var suffix = (opts.twelveHourClock) ? " %p" : "";
if (t < timeUnitSize.minute)
fmt = "%h:%M:%S" + suffix;
else if (t < timeUnitSize.day) {
if (span < 2 * timeUnitSize.day)
fmt = "%h:%M" + suffix;
else
fmt = "%b %d %h:%M" + suffix;
}
else if (t < timeUnitSize.month)
fmt = "%b %d";
else if (t < timeUnitSize.year) {
if (span < timeUnitSize.year)
fmt = "%b";
else
fmt = "%b %y";
}
else
fmt = "%y";
return $.plot.formatDate(d, fmt, opts.monthNames);
};
}
else {
// pretty rounding of base-10 numbers
var maxDec = opts.tickDecimals;
var dec = -Math.floor(Math.log(delta) / Math.LN10);
if (maxDec != null && dec > maxDec)
dec = maxDec;
magn = Math.pow(10, -dec);
norm = delta / magn; // norm is between 1.0 and 10.0
if (norm < 1.5)
size = 1;
else if (norm < 3) {
size = 2;
// special case for 2.5, requires an extra decimal
if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
size = 2.5;
++dec;
}
}
else if (norm < 7.5)
size = 5;
else
size = 10;
size *= magn;
if (opts.minTickSize != null && size < opts.minTickSize)
size = opts.minTickSize;
axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
axis.tickSize = opts.tickSize || size;
generator = function (axis) {
var ticks = [];
// spew out all possible ticks
var start = floorInBase(axis.min, axis.tickSize),
i = 0, v = Number.NaN, prev;
do {
prev = v;
v = start + i * axis.tickSize;
ticks.push(v);
++i;
} while (v < axis.max && v != prev);
return ticks;
};
formatter = function (v, axis) {
return v.toFixed(axis.tickDecimals);
};
}
if (opts.alignTicksWithAxis != null) {
var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1];
if (otherAxis && otherAxis.used && otherAxis != axis) {
// consider snapping min/max to outermost nice ticks
var niceTicks = generator(axis);
if (niceTicks.length > 0) {
if (opts.min == null)
axis.min = Math.min(axis.min, niceTicks[0]);
if (opts.max == null && niceTicks.length > 1)
axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]);
}
generator = function (axis) {
// copy ticks, scaled to this axis
var ticks = [], v, i;
for (i = 0; i < otherAxis.ticks.length; ++i) {
v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min);
v = axis.min + v * (axis.max - axis.min);
ticks.push(v);
}
return ticks;
};
// we might need an extra decimal since forced
// ticks don't necessarily fit naturally
if (axis.mode != "time" && opts.tickDecimals == null) {
var extraDec = Math.max(0, -Math.floor(Math.log(delta) / Math.LN10) + 1),
ts = generator(axis);
// only proceed if the tick interval rounded
// with an extra decimal doesn't give us a
// zero at end
if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec))))
axis.tickDecimals = extraDec;
}
}
}
axis.tickGenerator = generator;
if ($.isFunction(opts.tickFormatter))
axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); };
else
axis.tickFormatter = formatter;
}
function setTicks(axis) {
var oticks = axis.options.ticks, ticks = [];
if (oticks == null || (typeof oticks == "number" && oticks > 0))
ticks = axis.tickGenerator(axis);
else if (oticks) {
if ($.isFunction(oticks))
// generate the ticks
ticks = oticks({ min: axis.min, max: axis.max });
else
ticks = oticks;
}
// clean up/labelify the supplied ticks, copy them over
var i, v;
axis.ticks = [];
for (i = 0; i < ticks.length; ++i) {
var label = null;
var t = ticks[i];
if (typeof t == "object") {
v = +t[0];
if (t.length > 1)
label = t[1];
}
else
v = +t;
if (label == null)
label = axis.tickFormatter(v, axis);
if (!isNaN(v))
axis.ticks.push({ v: v, label: label });
}
}
function snapRangeToTicks(axis, ticks) {
if (axis.options.autoscaleMargin && ticks.length > 0) {
// snap to ticks
if (axis.options.min == null)
axis.min = Math.min(axis.min, ticks[0].v);
if (axis.options.max == null && ticks.length > 1)
axis.max = Math.max(axis.max, ticks[ticks.length - 1].v);
}
}
function draw() {
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
var grid = options.grid;
// draw background, if any
if (grid.show && grid.backgroundColor)
drawBackground();
if (grid.show && !grid.aboveData)
drawGrid();
for (var i = 0; i < series.length; ++i) {
executeHooks(hooks.drawSeries, [ctx, series[i]]);
drawSeries(series[i]);
}
executeHooks(hooks.draw, [ctx]);
if (grid.show && grid.aboveData)
drawGrid();
}
function extractRange(ranges, coord) {
var axis, from, to, key, axes = allAxes();
for (i = 0; i < axes.length; ++i) {
axis = axes[i];
if (axis.direction == coord) {
key = coord + axis.n + "axis";
if (!ranges[key] && axis.n == 1)
key = coord + "axis"; // support x1axis as xaxis
if (ranges[key]) {
from = ranges[key].from;
to = ranges[key].to;
break;
}
}
}
// backwards-compat stuff - to be removed in future
if (!ranges[key]) {
axis = coord == "x" ? xaxes[0] : yaxes[0];
from = ranges[coord + "1"];
to = ranges[coord + "2"];
}
// auto-reverse as an added bonus
if (from != null && to != null && from > to) {
var tmp = from;
from = to;
to = tmp;
}
return { from: from, to: to, axis: axis };
}
function drawBackground() {
ctx.save();
ctx.translate(plotOffset.left, plotOffset.top);
ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)");
ctx.fillRect(0, 0, plotWidth, plotHeight);
ctx.restore();
}
function drawGrid() {
var i;
ctx.save();
ctx.translate(plotOffset.left, plotOffset.top);
// draw markings
var markings = options.grid.markings;
if (markings) {
if ($.isFunction(markings)) {
var axes = plot.getAxes();
// xmin etc. is backwards compatibility, to be
// removed in the future
axes.xmin = axes.xaxis.min;
axes.xmax = axes.xaxis.max;
axes.ymin = axes.yaxis.min;
axes.ymax = axes.yaxis.max;
markings = markings(axes);
}
for (i = 0; i < markings.length; ++i) {
var m = markings[i],
xrange = extractRange(m, "x"),
yrange = extractRange(m, "y");
// fill in missing
if (xrange.from == null)
xrange.from = xrange.axis.min;
if (xrange.to == null)
xrange.to = xrange.axis.max;
if (yrange.from == null)
yrange.from = yrange.axis.min;
if (yrange.to == null)
yrange.to = yrange.axis.max;
// clip
if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max ||
yrange.to < yrange.axis.min || yrange.from > yrange.axis.max)
continue;
xrange.from = Math.max(xrange.from, xrange.axis.min);
xrange.to = Math.min(xrange.to, xrange.axis.max);
yrange.from = Math.max(yrange.from, yrange.axis.min);
yrange.to = Math.min(yrange.to, yrange.axis.max);
if (xrange.from == xrange.to && yrange.from == yrange.to)
continue;
// then draw
xrange.from = xrange.axis.p2c(xrange.from);
xrange.to = xrange.axis.p2c(xrange.to);
yrange.from = yrange.axis.p2c(yrange.from);
yrange.to = yrange.axis.p2c(yrange.to);
if (xrange.from == xrange.to || yrange.from == yrange.to) {
// draw line
ctx.beginPath();
ctx.strokeStyle = m.color || options.grid.markingsColor;
ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth;
ctx.moveTo(xrange.from, yrange.from);
ctx.lineTo(xrange.to, yrange.to);
ctx.stroke();
}
else {
// fill area
ctx.fillStyle = m.color || options.grid.markingsColor;
ctx.fillRect(xrange.from, yrange.to,
xrange.to - xrange.from,
yrange.from - yrange.to);
}
}
}
// draw the ticks
var axes = allAxes(), bw = options.grid.borderWidth;
for (var j = 0; j < axes.length; ++j) {
var axis = axes[j], box = axis.box,
t = axis.tickLength, x, y, xoff, yoff;
if (!axis.show || axis.ticks.length == 0)
continue
ctx.strokeStyle = axis.options.tickColor || $.color.parse(axis.options.color).scale('a', 0.22).toString();
ctx.lineWidth = 1;
// find the edges
if (axis.direction == "x") {
x = 0;
if (t == "full")
y = (axis.position == "top" ? 0 : plotHeight);
else
y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0);
}
else {
y = 0;
if (t == "full")
x = (axis.position == "left" ? 0 : plotWidth);
else
x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0);
}
// draw tick bar
if (!axis.innermost) {
ctx.beginPath();
xoff = yoff = 0;
if (axis.direction == "x")
xoff = plotWidth;
else
yoff = plotHeight;
if (ctx.lineWidth == 1) {
x = Math.floor(x) + 0.5;
y = Math.floor(y) + 0.5;
}
ctx.moveTo(x, y);
ctx.lineTo(x + xoff, y + yoff);
ctx.stroke();
}
// draw ticks
ctx.beginPath();
for (i = 0; i < axis.ticks.length; ++i) {
var v = axis.ticks[i].v;
xoff = yoff = 0;
if (v < axis.min || v > axis.max
// skip those lying on the axes if we got a border
|| (t == "full" && bw > 0
&& (v == axis.min || v == axis.max)))
continue;
if (axis.direction == "x") {
x = axis.p2c(v);
yoff = t == "full" ? -plotHeight : t;
if (axis.position == "top")
yoff = -yoff;
}
else {
y = axis.p2c(v);
xoff = t == "full" ? -plotWidth : t;
if (axis.position == "left")
xoff = -xoff;
}
if (ctx.lineWidth == 1) {
if (axis.direction == "x")
x = Math.floor(x) + 0.5;
else
y = Math.floor(y) + 0.5;
}
ctx.moveTo(x, y);
ctx.lineTo(x + xoff, y + yoff);
}
ctx.stroke();
}
// draw border
if (bw) {
ctx.lineWidth = bw;
ctx.strokeStyle = options.grid.borderColor;
ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw);
}
ctx.restore();
}
function insertAxisLabels() {
placeholder.find(".tickLabels").remove();
var html = ['<div class="tickLabels" style="font-size:smaller">'];
var axes = allAxes();
for (var j = 0; j < axes.length; ++j) {
var axis = axes[j], box = axis.box;
if (!axis.show)
continue;
//debug: html.push('<div style="position:absolute;opacity:0.10;background-color:red;left:' + box.left + 'px;top:' + box.top + 'px;width:' + box.width + 'px;height:' + box.height + 'px"></div>')
html.push('<div class="' + axis.direction + 'Axis ' + axis.direction + axis.n + 'Axis" style="color:' + axis.options.color + '">');
for (var i = 0; i < axis.ticks.length; ++i) {
var tick = axis.ticks[i];
if (!tick.label || tick.v < axis.min || tick.v > axis.max)
continue;
var pos = {}, align;
if (axis.direction == "x") {
align = "center";
pos.left = Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2);
if (axis.position == "bottom")
pos.top = box.top + box.padding;
else
pos.bottom = canvasHeight - (box.top + box.height - box.padding);
}
else {
pos.top = Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2);
if (axis.position == "left") {
pos.right = canvasWidth - (box.left + box.width - box.padding)
align = "right";
}
else {
pos.left = box.left + box.padding;
align = "left";
}
}
pos.width = axis.labelWidth;
var style = ["position:absolute", "text-align:" + align ];
for (var a in pos)
style.push(a + ":" + pos[a] + "px")
html.push('<div class="tickLabel" style="' + style.join(';') + '">' + tick.label + '</div>');
}
html.push('</div>');
}
html.push('</div>');
placeholder.append(html.join(""));
}
function drawSeries(series) {
if (series.lines.show)
drawSeriesLines(series);
if (series.bars.show)
drawSeriesBars(series);
if (series.points.show)
drawSeriesPoints(series);
}
function drawSeriesLines(series) {
function plotLine(datapoints, xoffset, yoffset, axisx, axisy) {
var points = datapoints.points,
ps = datapoints.pointsize,
prevx = null, prevy = null;
ctx.beginPath();
for (var i = ps; i < points.length; i += ps) {
var x1 = points[i - ps], y1 = points[i - ps + 1],
x2 = points[i], y2 = points[i + 1];
if (x1 == null || x2 == null)
continue;
// clip with ymin
if (y1 <= y2 && y1 < axisy.min) {
if (y2 < axisy.min)
continue; // line segment is outside
// compute new intersection point
x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
y1 = axisy.min;
}
else if (y2 <= y1 && y2 < axisy.min) {
if (y1 < axisy.min)
continue;
x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
y2 = axisy.min;
}
// clip with ymax
if (y1 >= y2 && y1 > axisy.max) {
if (y2 > axisy.max)
continue;
x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
y1 = axisy.max;
}
else if (y2 >= y1 && y2 > axisy.max) {
if (y1 > axisy.max)
continue;
x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
y2 = axisy.max;
}
// clip with xmin
if (x1 <= x2 && x1 < axisx.min) {
if (x2 < axisx.min)
continue;
y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
x1 = axisx.min;
}
else if (x2 <= x1 && x2 < axisx.min) {
if (x1 < axisx.min)
continue;
y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
x2 = axisx.min;
}
// clip with xmax
if (x1 >= x2 && x1 > axisx.max) {
if (x2 > axisx.max)
continue;
y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
x1 = axisx.max;
}
else if (x2 >= x1 && x2 > axisx.max) {
if (x1 > axisx.max)
continue;
y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
x2 = axisx.max;
}
if (x1 != prevx || y1 != prevy)
ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
prevx = x2;
prevy = y2;
ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
}
ctx.stroke();
}
function plotLineArea(datapoints, axisx, axisy) {
var points = datapoints.points,
ps = datapoints.pointsize,
bottom = Math.min(Math.max(0, axisy.min), axisy.max),
i = 0, top, areaOpen = false,
ypos = 1, segmentStart = 0, segmentEnd = 0;
// 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 (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)
continue;
// 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));
}
}
}
ctx.save();
ctx.translate(plotOffset.left, plotOffset.top);
ctx.lineJoin = "round";
var lw = series.lines.lineWidth,
sw = series.shadowSize;
// FIXME: consider another form of shadow when filling is turned on
if (lw > 0 && sw > 0) {
// draw shadow as a thick and thin line with transparency
ctx.lineWidth = sw;
ctx.strokeStyle = "rgba(0,0,0,0.1)";
// position shadow at angle from the mid of line
var angle = Math.PI/18;
plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis);
ctx.lineWidth = sw/2;
plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis);
}
ctx.lineWidth = lw;
ctx.strokeStyle = series.color;
var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight);
if (fillStyle) {
ctx.fillStyle = fillStyle;
plotLineArea(series.datapoints, series.xaxis, series.yaxis);
}
if (lw > 0)
plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis);
ctx.restore();
}
function drawSeriesPoints(series) {
function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) {
var points = datapoints.points, ps = datapoints.pointsize;
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;
ctx.beginPath();
x = axisx.p2c(x);
y = axisy.p2c(y) + offset;
if (symbol == "circle")
ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);
else
symbol(ctx, x, y, radius, shadow);
ctx.closePath();
if (fillStyle) {
ctx.fillStyle = fillStyle;
ctx.fill();
}
ctx.stroke();
}
}
ctx.save();
ctx.translate(plotOffset.left, plotOffset.top);
var lw = series.points.lineWidth,
sw = series.shadowSize,
radius = series.points.radius,
symbol = series.points.symbol;
if (lw > 0 && sw > 0) {
// draw shadow in two steps
var w = sw / 2;
ctx.lineWidth = w;
ctx.strokeStyle = "rgba(0,0,0,0.1)";
plotPoints(series.datapoints, radius, null, w + w/2, true,
series.xaxis, series.yaxis, symbol);
ctx.strokeStyle = "rgba(0,0,0,0.2)";
plotPoints(series.datapoints, radius, null, w/2, true,
series.xaxis, series.yaxis, symbol);
}
ctx.lineWidth = lw;
ctx.strokeStyle = series.color;
plotPoints(series.datapoints, radius,
getFillStyle(series.points, series.color), 0, false,
series.xaxis, series.yaxis, symbol);
ctx.restore();
}
function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) {
var left, right, bottom, top,
drawLeft, drawRight, drawTop, drawBottom,
tmp;
// 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.beginPath();
c.moveTo(left, bottom);
c.lineTo(left, top);
c.lineTo(right, top);
c.lineTo(right, bottom);
c.fillStyle = fillStyleCallback(bottom, top);
c.fill();
}
// draw outline
if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) {
c.beginPath();
// FIXME: inline moveTo is buggy with excanvas
c.moveTo(left, bottom + offset);
if (drawLeft)
c.lineTo(left, top + offset);
else
c.moveTo(left, top + offset);
if (drawTop)
c.lineTo(right, top + offset);
else
c.moveTo(right, top + offset);
if (drawRight)
c.lineTo(right, bottom + offset);
else
c.moveTo(right, bottom + offset);
if (drawBottom)
c.lineTo(left, bottom + offset);
else
c.moveTo(left, bottom + offset);
c.stroke();
}
}
function drawSeriesBars(series) {
function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) {
var points = datapoints.points, ps = datapoints.pointsize;
for (var i = 0; i < points.length; i += ps) {
if (points[i] == null)
continue;
drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth);
}
}
ctx.save();
ctx.translate(plotOffset.left, plotOffset.top);
// FIXME: figure out a way to add shadows (for instance along the right edge)
ctx.lineWidth = series.bars.lineWidth;
ctx.strokeStyle = series.color;
var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null;
plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis);
ctx.restore();
}
function getFillStyle(filloptions, seriesColor, bottom, top) {
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();
}
function insertLegend() {
placeholder.find(".legend").remove();
if (!options.legend.show)
return;
var fragments = [], rowStarted = false,
lf = options.legend.labelFormatter, s, label;
for (var i = 0; i < series.length; ++i) {
s = series[i];
label = s.label;
if (!label)
continue;
if (i % options.legend.noColumns == 0) {
if (rowStarted)
fragments.push('</tr>');
fragments.push('<tr>');
rowStarted = true;
}
if (lf)
label = lf(label, s);
fragments.push(
'<td class="legendColorBox"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:4px;height:0;border:5px solid ' + s.color + ';overflow:hidden"></div></div></td>' +
'<td class="legendLabel">' + label + '</td>');
}
if (rowStarted)
fragments.push('</tr>');
if (fragments.length == 0)
return;
var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>';
if (options.legend.container != null)
$(options.legend.container).html(table);
else {
var pos = "",
p = options.legend.position,
m = options.legend.margin;
if (m[0] == null)
m = [m, m];
if (p.charAt(0) == "n")
pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
else if (p.charAt(0) == "s")
pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
if (p.charAt(1) == "e")
pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
else if (p.charAt(1) == "w")
pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder);
if (options.legend.backgroundOpacity != 0.0) {
// put in the transparent background
// separately to avoid blended labels and
// label boxes
var c = options.legend.backgroundColor;
if (c == null) {
c = options.grid.backgroundColor;
if (c && typeof c == "string")
c = $.color.parse(c);
else
c = $.color.extract(legend, 'background-color');
c.a = 1;
c = c.toString();
}
var div = legend.children();
$('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity);
}
}
}
// interactive features
var highlights = [],
redrawTimeout = null;
// returns the data item the mouse is over, or null if none is found
function findNearbyItem(mouseX, mouseY, seriesFilter) {
var maxDistance = options.grid.mouseActiveRadius,
smallestDistance = maxDistance * maxDistance + 1,
item = null, foundPoint = false, i, j;
for (i = series.length - 1; i >= 0; --i) {
if (!seriesFilter(series[i]))
continue;
var s = series[i],
axisx = s.xaxis,
axisy = s.yaxis,
points = s.datapoints.points,
ps = s.datapoints.pointsize,
mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster
my = axisy.c2p(mouseY),
maxx = maxDistance / axisx.scale,
maxy = maxDistance / axisy.scale;
// with inverse transforms, we can't use the maxx/maxy
// optimization, sadly
if (axisx.options.inverseTransform)
maxx = Number.MAX_VALUE;
if (axisy.options.inverseTransform)
maxy = Number.MAX_VALUE;
if (s.lines.show || s.points.show) {
for (j = 0; j < points.length; j += ps) {
var x = points[j], y = points[j + 1];
if (x == null)
continue;
// For points and lines, the cursor must be within a
// certain distance to the data point
if (x - mx > maxx || x - mx < -maxx ||
y - my > maxy || y - my < -maxy)
continue;
// We have to calculate distances in pixels, not in
// data units, because the scales of the axes may be different
var dx = Math.abs(axisx.p2c(x) - mouseX),
dy = Math.abs(axisy.p2c(y) - mouseY),
dist = dx * dx + dy * dy; // we save the sqrt
// use <= to ensure last point takes precedence
// (last generally means on top of)
if (dist < smallestDistance) {
smallestDistance = dist;
item = [i, j / ps];
}
}
}
if (s.bars.show && !item) { // no other point can be nearby
var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2,
barRight = barLeft + s.bars.barWidth;
for (j = 0; j < points.length; j += ps) {
var x = points[j], y = points[j + 1], b = points[j + 2];
if (x == null)
continue;
// for a bar graph, the cursor must be inside the bar
if (series[i].bars.horizontal ?
(mx <= Math.max(b, x) && mx >= Math.min(b, x) &&
my >= y + barLeft && my <= y + barRight) :
(mx >= x + barLeft && mx <= x + barRight &&
my >= Math.min(b, y) && my <= Math.max(b, y)))
item = [i, j / ps];
}
}
}
if (item) {
i = item[0];
j = item[1];
ps = series[i].datapoints.pointsize;
return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),
dataIndex: j,
series: series[i],
seriesIndex: i };
}
return null;
}
function onMouseMove(e) {
if (options.grid.hoverable)
triggerClickHoverEvent("plothover", e,
function (s) { return s["hoverable"] != false; });
}
function onMouseLeave(e) {
if (options.grid.hoverable)
triggerClickHoverEvent("plothover", e,
function (s) { return false; });
}
function onClick(e) {
triggerClickHoverEvent("plotclick", e,
function (s) { return s["clickable"] != false; });
}
// trigger click or hover event (they send the same parameters
// so we share their code)
function triggerClickHoverEvent(eventname, event, seriesFilter) {
var offset = eventHolder.offset(),
canvasX = event.pageX - offset.left - plotOffset.left,
canvasY = event.pageY - offset.top - plotOffset.top,
pos = canvasToAxisCoords({ left: canvasX, top: canvasY });
pos.pageX = event.pageX;
pos.pageY = event.pageY;
var item = findNearbyItem(canvasX, canvasY, seriesFilter);
if (item) {
// fill in mouse pos for any listeners out there
item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left);
item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top);
}
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]))
unhighlight(h.series, h.point);
}
if (item)
highlight(item.series, item.datapoint, eventname);
}
placeholder.trigger(eventname, [ pos, item ]);
}
function triggerRedrawOverlay() {
if (!redrawTimeout)
redrawTimeout = setTimeout(drawOverlay, 30);
}
function drawOverlay() {
redrawTimeout = null;
// draw highlights
octx.save();
octx.clearRect(0, 0, canvasWidth, canvasHeight);
octx.translate(plotOffset.left, plotOffset.top);
var i, hi;
for (i = 0; i < highlights.length; ++i) {
hi = highlights[i];
if (hi.series.bars.show)
drawBarHighlight(hi.series, hi.point);
else
drawPointHighlight(hi.series, hi.point);
}
octx.restore();
executeHooks(hooks.drawOverlay, [octx]);
}
function highlight(s, point, auto) {
if (typeof s == "number")
s = series[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 });
triggerRedrawOverlay();
}
else if (!auto)
highlights[i].auto = false;
}
function unhighlight(s, point) {
if (s == null && point == null) {
highlights = [];
triggerRedrawOverlay();
}
if (typeof s == "number")
s = series[s];
if (typeof point == "number")
point = s.data[point];
var i = indexOfHighlight(s, point);
if (i != -1) {
highlights.splice(i, 1);
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 drawPointHighlight(series, point) {
var x = point[0], y = point[1],
axisx = series.xaxis, axisy = series.yaxis;
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 = $.color.parse(series.color).scale('a', 0.5).toString();
var radius = 1.5 * pointRadius,
x = axisx.p2c(x),
y = axisy.p2c(y);
octx.beginPath();
if (series.points.symbol == "circle")
octx.arc(x, y, radius, 0, 2 * Math.PI, false);
else
series.points.symbol(octx, x, y, radius, false);
octx.closePath();
octx.stroke();
}
function drawBarHighlight(series, point) {
octx.lineWidth = series.bars.lineWidth;
octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
var fillStyle = $.color.parse(series.color).scale('a', 0.5).toString();
var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth,
0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth);
}
function getColorOrGradient(spec, bottom, top, defaultColor) {
if (typeof spec == "string")
return spec;
else {
// assume this is a gradient spec; IE currently only
// supports a simple vertical gradient properly, so that's
// what we support too
var gradient = ctx.createLinearGradient(0, top, 0, bottom);
for (var i = 0, l = spec.colors.length; i < l; ++i) {
var c = spec.colors[i];
if (typeof c != "string") {
var co = $.color.parse(defaultColor);
if (c.brightness != null)
co = co.scale('rgb', c.brightness)
if (c.opacity != null)
co.a *= c.opacity;
c = co.toString();
}
gradient.addColorStop(i / (l - 1), c);
}
return gradient;
}
}
}
$.plot = function(placeholder, data, options) {
//var t0 = new Date();
var plot = new Plot($(placeholder), data, options, $.plot.plugins);
//(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime()));
return plot;
};
$.plot.version = "0.7";
$.plot.plugins = [];
// returns a string with the date d formatted according to fmt
$.plot.formatDate = function(d, fmt, monthNames) {
var leftPad = function(n) {
n = "" + n;
return n.length == 1 ? "0" + n : n;
};
var r = [];
var escape = false, padNext = false;
var hours = d.getUTCHours();
var isAM = hours < 12;
if (monthNames == null)
monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
if (fmt.search(/%p|%P/) != -1) {
if (hours > 12) {
hours = hours - 12;
} else if (hours == 0) {
hours = 12;
}
}
for (var i = 0; i < fmt.length; ++i) {
var c = fmt.charAt(i);
if (escape) {
switch (c) {
case 'h': c = "" + hours; break;
case 'H': c = leftPad(hours); break;
case 'M': c = leftPad(d.getUTCMinutes()); break;
case 'S': c = leftPad(d.getUTCSeconds()); break;
case 'd': c = "" + d.getUTCDate(); break;
case 'm': c = "" + (d.getUTCMonth() + 1); break;
case 'y': c = "" + d.getUTCFullYear(); break;
case 'b': c = "" + monthNames[d.getUTCMonth()]; break;
case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
case '0': c = ""; padNext = true; break;
}
if (c && padNext) {
c = leftPad(c);
padNext = false;
}
r.push(c);
if (!padNext)
escape = false;
}
else {
if (c == "%")
escape = true;
else
r.push(c);
}
}
return r.join("");
};
// round to nearby lower multiple of base
function floorInBase(n, base) {
return base * Math.floor(n / base);
}
})($);
/*
Flot plugin for rendering pie charts. The plugin assumes the data is
coming is as a single data value for each series, and each of those
values is a positive value or zero (negative numbers don't make
any sense and will cause strange effects). The data values do
NOT need to be passed in as percentage values because it
internally calculates the total and percentages.
* Created by Brian Medendorp, June 2009
* Updated November 2009 with contributions from: btburnett3, Anthony Aragues and Xavi Ivars
* Changes:
2009-10-22: lineJoin set to round
2009-10-23: IE full circle fix, donut
2009-11-11: Added basic hover from btburnett3 - does not work in IE, and center is off in Chrome and Opera
2009-11-17: Added IE hover capability submitted by Anthony Aragues
2009-11-18: Added bug fix submitted by Xavi Ivars (issues with arrays when other JS libraries are included as well)
Available options are:
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 ($)
{
function init(plot) // this is the "body" of the plugin
{
var canvas = null;
var target = null;
var maxRadius = null;
var centerLeft = null;
var centerTop = null;
var total = 0;
var redraw = true;
var redrawAttempts = 10;
var shrink = 0.95;
var legendWidth = 0;
var processed = false;
var raw = false;
// interactive variables
var highlights = [];
// add hook to determine if pie plugin in enabled, and then perform necessary operations
plot.hooks.processOptions.push(checkPieEnabled);
plot.hooks.bindEvents.push(bindEvents);
// check to see if the pie plugin is enabled
function checkPieEnabled(plot, options)
{
if (options.series.pie.show)
{
//disable grid
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;
if (options.series.pie.tilt<0)
options.series.pie.tilt=0;
// add processData hook to do transformations on the data
plot.hooks.processDatapoints.push(processDatapoints);
plot.hooks.drawOverlay.push(drawOverlay);
// add draw hook
plot.hooks.draw.push(draw);
}
}
// bind hoverable events
function bindEvents(plot, eventHolder)
{
var options = plot.getOptions();
if (options.series.pie.show && options.grid.hoverable)
eventHolder.unbind('mousemove').mousemove(onMouseMove);
if (options.series.pie.show && options.grid.clickable)
eventHolder.unbind('click').click(onClick);
}
// debugging function that prints out an object
function alertObject(obj)
{
var msg = '';
function traverse(obj, depth)
{
if (!depth)
depth = 0;
for (var i = 0; i < obj.length; ++i)
{
for (var j=0; j<depth; j++)
msg += '\t';
if( typeof obj[i] == "object")
{ // its an object
msg += ''+i+':\n';
traverse(obj[i], depth+1);
}
else
{ // its a value
msg += ''+i+': '+obj[i]+'\n';
}
}
}
traverse(obj);
alert(msg);
}
function calcTotal(data)
{
for (var i = 0; i < data.length; ++i)
{
var item = parseFloat(data[i].data[0][1]);
if (item)
total += item;
}
}
function processDatapoints(plot, series, data, datapoints)
{
if (!processed)
{
processed = true;
canvas = plot.getCanvas();
target = $(canvas).parent();
options = plot.getOptions();
plot.setData(combine(plot.getData()));
}
}
function setupPie()
{
legendWidth = target.children().filter('.legend').children().width();
// calculate maximum radius and center point
maxRadius = Math.min(canvas.width,(canvas.height/options.series.pie.tilt))/2;
centerTop = (canvas.height/2)+options.series.pie.offset.top;
centerLeft = (canvas.width/2);
if (options.series.pie.offset.left=='auto')
if (options.legend.position.match('w'))
centerLeft += legendWidth/2;
else
centerLeft -= legendWidth/2;
else
centerLeft += options.series.pie.offset.left;
if (centerLeft<maxRadius)
centerLeft = maxRadius;
else if (centerLeft>canvas.width-maxRadius)
centerLeft = canvas.width-maxRadius;
}
function fixData(data)
{
for (var i = 0; i < data.length; ++i)
{
if (typeof(data[i].data)=='number')
data[i].data = [[1,data[i].data]];
else if (typeof(data[i].data)=='undefined' || typeof(data[i].data[0])=='undefined')
{
if (typeof(data[i].data)!='undefined' && typeof(data[i].data.label)!='undefined')
data[i].label = data[i].data.label; // fix weirdness coming from flot
data[i].data = [[1,0]];
}
}
return data;
}
function combine(data)
{
data = fixData(data);
calcTotal(data);
var combined = 0;
var numCombined = 0;
var color = options.series.pie.combine.color;
var newdata = [];
for (var i = 0; i < data.length; ++i)
{
// make sure its a number
data[i].data[0][1] = parseFloat(data[i].data[0][1]);
if (!data[i].data[0][1])
data[i].data[0][1] = 0;
if (data[i].data[0][1]/total<=options.series.pie.combine.threshold)
{
combined += data[i].data[0][1];
numCombined++;
if (!color)
color = data[i].color;
}
else
{
newdata.push({
data: [[1,data[i].data[0][1]]],
color: data[i].color,
label: data[i].label,
angle: (data[i].data[0][1]*(Math.PI*2))/total,
percent: (data[i].data[0][1]/total*100)
});
}
}
if (numCombined>0)
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
ctx = newCtx;
setupPie();
var slices = plot.getData();
var attempts = 0;
while (redraw && attempts<redrawAttempts)
{
redraw = false;
if (attempts>0)
maxRadius *= shrink;
attempts += 1;
clear();
if (options.series.pie.tilt<=0.8)
drawShadow();
drawPie();
}
if (attempts >= redrawAttempts) {
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,canvas.width,canvas.height);
target.children().filter('.pieLabel, .pieLabelBackground').remove();
}
function drawShadow()
{
var shadowLeft = 5;
var shadowTop = 15;
var edge = 10;
var alpha = 0.02;
// set radius
if (options.series.pie.radius>1)
var radius = options.series.pie.radius;
else
var radius = maxRadius * options.series.pie.radius;
if (radius>=(canvas.width/2)-shadowLeft || radius*options.series.pie.tilt>=(canvas.height/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()
{
startAngle = Math.PI*options.series.pie.startAngle;
// set radius
if (options.series.pie.radius>1)
var radius = options.series.pie.radius;
else
var radius = maxRadius * options.series.pie.radius;
// 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 (var i = 0; i < slices.length; ++i)
{
slices[i].startAngle = currentAngle;
drawSlice(slices[i].angle, slices[i].color, true);
}
ctx.restore();
// draw slice outlines
ctx.save();
ctx.lineWidth = options.series.pie.stroke.width;
currentAngle = startAngle;
for (var i = 0; i < slices.length; ++i)
drawSlice(slices[i].angle, options.series.pie.stroke.color, false);
ctx.restore();
// draw donut hole
drawDonutHole(ctx);
// draw labels
if (options.series.pie.label.show)
drawLabels();
// restore to original state
ctx.restore();
function drawSlice(angle, color, fill)
{
if (angle<=0)
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
else if ($.browser.msie)
angle -= 0.0001;
//ctx.arc(0,0,radius,0,angle,false); // This doesn't work properly in Opera
ctx.arc(0,0,radius,currentAngle,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;
// set radius
if (options.series.pie.label.radius>1)
var radius = options.series.pie.label.radius;
else
var 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)
drawLabel(slices[i], currentAngle, i);
currentAngle += slices[i].angle;
}
function drawLabel(slice, startAngle, index)
{
if (slice.data[0][1]==0)
return;
// 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 || canvas.height-(labelTop+label.height())<0 || canvas.width-(labelLeft+label.width())<0)
redraw = true;
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>').insertBefore(label).css('opacity', options.series.pie.label.background.opacity);
}
} // 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)
{
// draw donut hole
if(options.series.pie.innerRadius > 0)
{
// subtract the center
layer.save();
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;
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,false);
ctx.closePath();
x = mouseX-centerLeft;
y = mouseY-centerTop;
if(ctx.isPointInPath)
{
if (ctx.isPointInPath(mouseX-centerLeft, mouseY-centerTop))
{
//alert('found slice!');
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.
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(),
canvasX = parseInt(e.pageX - offset.left),
canvasY = parseInt(e.pageY - offset.top),
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)
{
//alert(options.series.pie.radius);
var options = plot.getOptions();
//alert(options.series.pie.radius);
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 (i = 0; i < highlights.length; ++i)
drawHighlight(highlights[i].series);
drawDonutHole(octx);
octx.restore();
function drawHighlight(series)
{
if (series.angle < 0) 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,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,
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.0"
});
})($);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment