scriptbox
Version:
Script box is a full VAS application
1,288 lines (1,128 loc) • 119 kB
JavaScript
/* Javascript plotting library for jQuery, version 0.9.0-alpha.
Copyright (c) 2007-2013 IOLA and Ole Laursen.
Licensed under the MIT license.
*/
// 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]
}
})(jQuery);
// the actual Flot code
(function ($) {
// Cache the prototype hasOwnProperty for faster access
var hasOwnProperty = Object.prototype.hasOwnProperty;
///////////////////////////////////////////////////////////////////////////
// The Canvas object is a wrapper around an HTML5 <canvas> tag.
//
// @constructor
// @param {string} cls List of classes to apply to the canvas.
// @param {element} container Element onto which to append the canvas.
//
// Requiring a container is a little iffy, but unfortunately canvas
// operations don't work unless the canvas is attached to the DOM.
function Canvas(cls, container) {
var element = container.children("." + cls)[0];
if (element == null) {
element = document.createElement("canvas");
element.className = cls;
$(element).css({
direction: "ltr",
position: "absolute",
left: 0,
top: 0
}).appendTo(container);
// If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas
if (!element.getContext) {
if (window.G_vmlCanvasManager) {
element = window.G_vmlCanvasManager.initElement(element);
} else {
throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode.");
}
}
}
this.element = element;
var context = this.context = element.getContext("2d");
// Determine the screen's ratio of physical to device-independent
// pixels. This is the ratio between the canvas width that the browser
// advertises and the number of pixels actually present in that space.
// The iPhone 4, for example, has a device-independent width of 320px,
// but its screen is actually 640px wide. It therefore has a pixel
// ratio of 2, while most normal devices have a ratio of 1.
var devicePixelRatio = window.devicePixelRatio || 1,
backingStoreRatio = context.webkitBackingStorePixelRatio || context.mozBackingStorePixelRatio || context.msBackingStorePixelRatio || context.oBackingStorePixelRatio || context.backingStorePixelRatio || 1;
this.pixelRatio = devicePixelRatio / backingStoreRatio;
// Size the canvas to match the internal dimensions of its container
this.resize(container.width(), container.height());
// Collection of HTML div layers for text overlaid onto the canvas
this.textContainer = null;
this.text = {};
// Cache of text fragments and metrics, so we can avoid expensively
// re-calculating them when the plot is re-rendered in a loop.
this._textCache = {};
}
// Resizes the canvas to the given dimensions.
//
// @param {number} width New width of the canvas, in pixels.
// @param {number} width New height of the canvas, in pixels.
Canvas.prototype.resize = function (width, height) {
if (width <= 0 || height <= 0) {
throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height);
}
var element = this.element,
context = this.context,
pixelRatio = this.pixelRatio;
// Resize the canvas, increasing its density based on the display's
// pixel ratio; basically giving it more pixels without increasing the
// size of its element, to take advantage of the fact that retina
// displays have that many more pixels in the same advertised space.
// Resizing should reset the state (excanvas seems to be buggy though)
if (this.width != width) {
element.width = width * pixelRatio;
element.style.width = width + "px";
this.width = width;
}
if (this.height != height) {
element.height = height * pixelRatio;
element.style.height = height + "px";
this.height = height;
}
// Save the context, so we can reset in case we get replotted. The
// restore ensure that we're really back at the initial state, and
// should be safe even if we haven't saved the initial state yet.
context.restore();
context.save();
// Scale the coordinate space to match the display density; so even though we
// may have twice as many pixels, we still want lines and other drawing to
// appear at the same size; the extra pixels will just make them crisper.
context.scale(pixelRatio, pixelRatio);
};
// Clears the entire canvas area, not including any overlaid HTML text
Canvas.prototype.clear = function () {
this.context.clearRect(0, 0, this.width, this.height);
};
// Finishes rendering the canvas, including managing the text overlay.
Canvas.prototype.render = function () {
var cache = this._textCache;
// For each text layer, add elements marked as active that haven't
// already been rendered, and remove those that are no longer active.
for (var layerKey in cache) {
if (hasOwnProperty.call(cache, layerKey)) {
var layer = this.getTextLayer(layerKey),
layerCache = cache[layerKey];
layer.hide();
for (var styleKey in layerCache) {
if (hasOwnProperty.call(layerCache, styleKey)) {
var styleCache = layerCache[styleKey];
for (var key in styleCache) {
if (hasOwnProperty.call(styleCache, key)) {
var info = styleCache[key];
if (info.active) {
if (!info.rendered) {
layer.append(info.element);
info.rendered = true;
}
} else {
delete styleCache[key];
if (info.rendered) {
info.element.detach();
}
}
}
}
}
}
layer.show();
}
}
};
// Creates (if necessary) and returns the text overlay container.
//
// @param {string} classes String of space-separated CSS classes used to
// uniquely identify the text layer.
// @return {object} The jQuery-wrapped text-layer div.
Canvas.prototype.getTextLayer = function (classes) {
var layer = this.text[classes];
// Create the text layer if it doesn't exist
if (layer == null) {
// Create the text layer container, if it doesn't exist
if (this.textContainer == null) {
this.textContainer = $("<div class='flot-text'></div>").css({
position: "absolute",
top: 0,
left: 0,
bottom: 0,
right: 0,
'font-size': "smaller",
color: "#545454"
}).insertAfter(this.element);
}
layer = this.text[classes] = $("<div></div>").addClass(classes).css({
position: "absolute",
top: 0,
left: 0,
bottom: 0,
right: 0
}).appendTo(this.textContainer);
}
return layer;
};
// Creates (if necessary) and returns a text info object.
//
// The object looks like this:
//
// {
// width: Width of the text's wrapper div.
// height: Height of the text's wrapper div.
// active: Flag indicating whether the text should be visible.
// rendered: Flag indicating whether the text is currently visible.
// element: The jQuery-wrapped HTML div containing the text.
// }
//
// Canvas maintains a cache of recently-used text info objects; getTextInfo
// either returns the cached element or creates a new entry.
//
// @param {string} layer A string of space-separated CSS classes uniquely
// identifying the layer containing this text.
// @param {string} text Text string to retrieve info for.
// @param {(string|object)=} font Either a string of space-separated CSS
// classes or a font-spec object, defining the text's font and style.
// @param {number=} angle Angle at which to rotate the text, in degrees.
// Angle is currently unused, it will be implemented in the future.
// @return {object} a text info object.
Canvas.prototype.getTextInfo = function (layer, text, font, angle) {
var textStyle, layerCache, styleCache, info;
// Cast the value to a string, in case we were given a number or such
text = "" + text;
// If the font is a font-spec object, generate a CSS font definition
if (typeof font === "object") {
textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family;
} else {
textStyle = font;
}
// Retrieve (or create) the cache for the text's layer and styles
layerCache = this._textCache[layer];
if (layerCache == null) {
layerCache = this._textCache[layer] = {};
}
styleCache = layerCache[textStyle];
if (styleCache == null) {
styleCache = layerCache[textStyle] = {};
}
info = styleCache[text];
// If we can't find a matching element in our cache, create a new one
if (info == null) {
var element = $("<div></div>").html(text).css({
position: "absolute",
top: -9999
}).appendTo(this.getTextLayer(layer));
if (typeof font === "object") {
element.css({
font: textStyle,
color: font.color
});
} else if (typeof font === "string") {
element.addClass(font);
}
info = styleCache[text] = {
active: false,
rendered: false,
element: element,
width: element.outerWidth(true),
height: element.outerHeight(true)
};
element.detach();
}
return info;
};
// Adds a text string to the canvas text overlay.
//
// The text isn't drawn immediately; it is marked as rendering, which will
// result in its addition to the canvas on the next render pass.
//
// @param {string} layer A string of space-separated CSS classes uniquely
// identifying the layer containing this text.
// @param {number} x X coordinate at which to draw the text.
// @param {number} y Y coordinate at which to draw the text.
// @param {string} text Text string to draw.
// @param {(string|object)=} font Either a string of space-separated CSS
// classes or a font-spec object, defining the text's font and style.
// @param {number=} angle Angle at which to rotate the text, in degrees.
// Angle is currently unused, it will be implemented in the future.
// @param {string=} halign Horizontal alignment of the text; either "left",
// "center" or "right".
// @param {string=} valign Vertical alignment of the text; either "top",
// "middle" or "bottom".
Canvas.prototype.addText = function (layer, x, y, text, font, angle, halign, valign) {
var info = this.getTextInfo(layer, text, font, angle);
// Mark the div for inclusion in the next render pass
info.active = true;
// Tweak the div's position to match the text's alignment
if (halign == "center") {
x -= info.width / 2;
} else if (halign == "right") {
x -= info.width;
}
if (valign == "middle") {
y -= info.height / 2;
} else if (valign == "bottom") {
y -= info.height;
}
// Move the element to its final position within the container
info.element.css({
top: Math.round(y),
left: Math.round(x)
});
};
// Removes one or more text strings from the canvas text overlay.
//
// If no parameters are given, all text within the layer is removed.
// The text is not actually removed; it is simply marked as inactive, which
// will result in its removal on the next render pass.
//
// @param {string} layer A string of space-separated CSS classes uniquely
// identifying the layer containing this text.
// @param {string} text Text string to remove.
// @param {(string|object)=} font Either a string of space-separated CSS
// classes or a font-spec object, defining the text's font and style.
// @param {number=} angle Angle at which the text is rotated, in degrees.
// Angle is currently unused, it will be implemented in the future.
Canvas.prototype.removeText = function (layer, text, font, angle) {
if (text == null) {
var layerCache = this._textCache[layer];
if (layerCache != null) {
for (var styleKey in layerCache) {
if (hasOwnProperty.call(layerCache, styleKey)) {
var styleCache = layerCache[styleKey]
for (var key in styleCache) {
if (hasOwnProperty.call(styleCache, key)) {
styleCache[key].active = false;
}
}
}
}
}
} else {
this.getTextInfo(layer, text, font, angle).active = false;
}
};
///////////////////////////////////////////////////////////////////////////
// The top-level container for the entire plot.
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
sorted: null // default to no legend sorting
},
xaxis: {
show: null,
// null = auto-detect, true = always, false = never
position: "bottom",
// or "top"
mode: null,
// null or "time"
font: null,
// null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" }
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
tickDecimals: null,
// no. of decimals, null means auto
tickSize: null,
// number or [number, "unit"]
minTickSize: null // number or [number, "unit"]
},
yaxis: {
autoscaleMargin: 0.02,
position: "left" // or "right"
},
xaxes: [],
yaxes: [],
series: {
points: {
show: false,
radius: 3,
lineWidth: 2,
// in pixels
fill: true,
fillColor: "#ffffff",
strokeColor: null,
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
// Omit 'zero', so we can later default its value to
// match that of the 'fill' option.
},
bars: {
show: false,
lineWidth: 2,
// in pixels
barWidth: 1,
// in units of the x axis
fill: true,
fillColor: null,
align: "left",
// "left", "right", or "center"
horizontal: false,
zero: true
},
shadowSize: 3,
highlightColor: null
},
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)"
margin: 0,
// distance from the canvas edge to the grid
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
},
interaction: {
redrawOverlayInterval: 1000 / 60 // time between updates, -1 means in same flow
},
hooks: {}
},
surface = 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
},
plotWidth = 0,
plotHeight = 0,
hooks = {
processOptions: [],
processRawData: [],
processDatapoints: [],
processOffset: [],
drawBackground: [],
drawSeries: [],
draw: [],
bindEvents: [],
drawOverlay: [],
shutdown: []
},
plot = this;
// public functions
plot.setData = setData;
plot.setupGrid = setupGrid;
plot.draw = draw;
plot.findNearbyItem = findNearbyItem;
plot.getPlaceholder = function () {
return placeholder;
};
plot.getCanvas = function () {
return surface.element;
};
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, 10),
top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10)
};
};
plot.shutdown = shutdown;
plot.resize = function () {
var width = placeholder.width(),
height = placeholder.height();
surface.resize(width, height);
overlay.resize(width, height);
};
// 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() {
// References to key classes, allowing plugins to modify them
var classes = {
Canvas: Canvas
};
for (var i = 0; i < plugins.length; ++i) {
var p = plugins[i];
p.init(plot, classes);
if (p.options) $.extend(true, options, p.options);
}
}
function parseOptions(opts) {
$.extend(true, options, opts);
if (options.xaxis.color == null) options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString();
if (options.yaxis.color == null) options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString();
if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility
options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color;
if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility
options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color;
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 for axis options, including any unspecified
// font-spec fields, if a font-spec was provided.
// If no x/y axis options were provided, create one of each anyway,
// since the rest of the code assumes that they exist.
var i, axisOptions, axisCount, fontDefaults = {
style: placeholder.css("font-style"),
size: Math.round(0.8 * (+placeholder.css("font-size").replace("px", "") || 13)),
variant: placeholder.css("font-variant"),
weight: placeholder.css("font-weight"),
family: placeholder.css("font-family")
};
fontDefaults.lineHeight = fontDefaults.size * 1.15;
axisCount = options.xaxes.length || 1;
for (i = 0; i < axisCount; ++i) {
axisOptions = options.xaxes[i];
if (axisOptions && !axisOptions.tickColor) {
axisOptions.tickColor = axisOptions.color;
}
axisOptions = $.extend(true, {}, options.xaxis, axisOptions);
options.xaxes[i] = axisOptions;
if (axisOptions.font) {
axisOptions.font = $.extend({}, fontDefaults, axisOptions.font);
if (!axisOptions.font.color) {
axisOptions.font.color = axisOptions.color;
}
}
}
axisCount = options.yaxes.length || 1;
for (i = 0; i < axisCount; ++i) {
axisOptions = options.yaxes[i];
if (axisOptions && !axisOptions.tickColor) {
axisOptions.tickColor = axisOptions.color;
}
axisOptions = $.extend(true, {}, options.yaxis, axisOptions);
options.yaxes[i] = axisOptions;
if (axisOptions.font) {
axisOptions.font = $.extend({}, fontDefaults, axisOptions.font);
if (!axisOptions.font.color) {
axisOptions.font.color = axisOptions.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.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;
if (options.highlightColor != null) options.series.highlightColor = options.highlightColor;
// 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 neededColors = series.length,
maxIndex = -1,
i;
// Subtract the number of series that already have fixed colors or
// color indexes from the number that we still need to generate.
for (i = 0; i < series.length; ++i) {
var sc = series[i].color;
if (sc != null) {
neededColors--;
if (typeof sc == "number" && sc > maxIndex) {
maxIndex = sc;
}
}
}
// If any of the series have fixed color indexes, then we need to
// generate at least as many colors as the highest index.
if (neededColors <= maxIndex) {
neededColors = maxIndex + 1;
}
// Generate all the colors, using first the option colors and then
// variations on those colors once they're exhausted.
var c, colors = [],
colorPool = options.colors,
colorPoolSize = colorPool.length,
variation = 0;
for (i = 0; i < neededColors; i++) {
c = $.color.parse(colorPool[i % colorPoolSize] || "#666");
// Each time we exhaust the colors in the pool we adjust
// a scaling factor used to produce more variations on
// those colors. The factor alternates negative/positive
// to produce lighter/darker colors.
// Reset the variation after every few cycles, or else
// it will end up producing only white or black colors.
if (i % colorPoolSize == 0 && i) {
if (variation >= 0) {
if (variation < 0.5) {
variation = -variation - 0.2;
} else variation = 0;
} else variation = -variation;
}
colors[i] = c.scale('rgb', 1 + variation);
}
// Finalize the series options, filling in their colors
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;
}
// If nothing was provided for lines.zero, default it to match
// lines.fill, since areas by default should extend to zero.
if (s.lines.zero == null) {
s.lines.zero = !!s.lines.fill;
}
// 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, data, format;
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];
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)) {
var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero));
format.push({
y: true,
number: true,
required: false,
defaultValue: 0,
autoscale: autoscale
});
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;
var 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;
format = s.datapoints.format;
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 || f.autoscale === false || 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;
switch (s.bars.align) {
case "left":
delta = 0;
break;
case "right":
delta = -s.bars.barWidth;
break;
case "center":
delta = -s.bars.barWidth / 2;
break;
default:
throw new Error("Invalid bar alignment: " + s.bars.align);
}
if (s.bars.horizontal) {
ymin += delta;
ymax += de