|
/* Javascript plotting library for jQuery, v. 0.6. |
|
* |
|
* 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.0. |
|
* |
|
* 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() work in-place instead of returning |
|
* new objects. |
|
*/ |
|
(function(){jQuery.color={};jQuery.color.make=function(E,D,B,C){var F={};F.r=E||0;F.g=D||0;F.b=B||0;F.a=C!=null?C:1;F.add=function(I,H){for(var G=0;G<I.length;++G){F[I.charAt(G)]+=H}return F.normalize()};F.scale=function(I,H){for(var G=0;G<I.length;++G){F[I.charAt(G)]*=H}return F.normalize()};F.toString=function(){if(F.a>=1){return"rgb("+[F.r,F.g,F.b].join(",")+")"}else{return"rgba("+[F.r,F.g,F.b,F.a].join(",")+")"}};F.normalize=function(){function G(I,J,H){return J<I?I:(J>H?H:J)}F.r=G(0,parseInt(F.r),255);F.g=G(0,parseInt(F.g),255);F.b=G(0,parseInt(F.b),255);F.a=G(0,F.a,1);return F};F.clone=function(){return jQuery.color.make(F.r,F.b,F.g,F.a)};return F.normalize()};jQuery.color.extract=function(C,B){var D;do{D=C.css(B).toLowerCase();if(D!=""&&D!="transparent"){break}C=C.parent()}while(!jQuery.nodeName(C.get(0),"body"));if(D=="rgba(0, 0, 0, 0)"){D="transparent"}return jQuery.color.parse(D)};jQuery.color.parse=function(E){var D,B=jQuery.color.make;if(D=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10))}if(D=/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(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10),parseFloat(D[4]))}if(D=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55)}if(D=/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(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55,parseFloat(D[4]))}if(D=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(E)){return B(parseInt(D[1],16),parseInt(D[2],16),parseInt(D[3],16))}if(D=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(E)){return B(parseInt(D[1]+D[1],16),parseInt(D[2]+D[2],16),parseInt(D[3]+D[3],16))}var C=jQuery.trim(E).toLowerCase();if(C=="transparent"){return B(255,255,255,0)}else{D=A[C];return B(D[0],D[1],D[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: { |
|
mode: null, // null or "time" |
|
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, |
|
|
|
// 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 |
|
}, |
|
x2axis: { |
|
autoscaleMargin: null |
|
}, |
|
y2axis: { |
|
autoscaleMargin: 0.02 |
|
}, |
|
series: { |
|
points: { |
|
show: false, |
|
radius: 3, |
|
lineWidth: 2, // in pixels |
|
fill: true, |
|
fillColor: "#ffffff" |
|
}, |
|
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 // when horizontal, left is now top |
|
}, |
|
shadowSize: 3 |
|
}, |
|
grid: { |
|
show: true, |
|
aboveData: false, |
|
color: "#545454", // primary color used for outline and labels |
|
backgroundColor: null, // null for transparent, else color |
|
tickColor: "rgba(0,0,0,0.15)", // color used for the ticks |
|
labelMargin: 5, // in pixels |
|
borderWidth: 2, // in pixels |
|
borderColor: null, // set if different from the grid color |
|
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, |
|
axes = { xaxis: {}, yaxis: {}, x2axis: {}, y2axis: {} }, |
|
plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, |
|
canvasWidth = 0, canvasHeight = 0, |
|
plotWidth = 0, plotHeight = 0, |
|
hooks = { |
|
processOptions: [], |
|
processRawData: [], |
|
processDatapoints: [], |
|
draw: [], |
|
bindEvents: [], |
|
drawOverlay: [] |
|
}, |
|
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() { return axes; }; |
|
plot.getOptions = function() { return options; }; |
|
plot.highlight = highlight; |
|
plot.unhighlight = unhighlight; |
|
plot.triggerRedrawOverlay = triggerRedrawOverlay; |
|
plot.pointOffset = function(point) { |
|
return { left: parseInt(axisSpecToRealAxis(point, "xaxis").p2c(+point.x) + plotOffset.left), |
|
top: parseInt(axisSpecToRealAxis(point, "yaxis").p2c(+point.y) + plotOffset.top) }; |
|
}; |
|
|
|
|
|
// public attributes |
|
plot.hooks = hooks; |
|
|
|
// initialize |
|
initPlugins(plot); |
|
parseOptions(options_); |
|
constructCanvas(); |
|
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) { |
|
$.extend(true, options, opts); |
|
if (options.grid.borderColor == null) |
|
options.grid.borderColor = options.grid.color; |
|
// 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.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) |
|
options.series.shadowSize = options.shadowSize; |
|
|
|
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) { |
|
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 axisSpecToRealAxis(obj, attr) { |
|
var a = obj[attr]; |
|
if (!a || a == 1) |
|
return axes[attr]; |
|
if (typeof a == "number") |
|
return axes[attr.charAt(0) + a + attr.slice(1)]; |
|
return a; // assume it's OK |
|
} |
|
|
|
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].show) { |
|
show = false; |
|
break; |
|
|