PluginProbe
Property Hive / trunk
Property Hive vtrunk
2.4.0 2.3.1 2.3.0 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 1.4.46 1.4.47 1.4.48 1.4.49 1.4.5 1.4.50 1.4.51 1.4.52 1.4.53 1.4.54 1.4.55 1.4.56 1.4.57 1.4.58 1.4.59 1.4.6 1.4.60 All 262 releases
propertyhive / assets / js / jquery-flot / jquery.flot.js

jquery.flot.js in Property Hive trunk, at assets/js/jquery-flot/jquery.flot.js

3,062 lines 116.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /* Javascript plotting library for jQuery, version 0.8.1.
2
3 Copyright (c) 2007-2013 IOLA and Ole Laursen.
4 Licensed under the MIT license.
5
6 */
7
8 // first an inline dependency, jquery.colorhelpers.js, we inline it here
9 // for convenience
10
11 /* Plugin for jQuery for working with colors.
12 *
13 * Version 1.1.
14 *
15 * Inspiration from jQuery color animation plugin by John Resig.
16 *
17 * Released under the MIT license by Ole Laursen, October 2009.
18 *
19 * Examples:
20 *
21 * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
22 * var c = $.color.extract($("#mydiv"), 'background-color');
23 * console.log(c.r, c.g, c.b, c.a);
24 * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
25 *
26 * Note that .scale() and .add() return the same modified object
27 * instead of making a new one.
28 *
29 * V. 1.1: Fix error handling so e.g. parsing an empty string does
30 * produce a color rather than just crashing.
31 */
32 (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);
33
34 // the actual Flot code
35 (function($) {
36
37 // Cache the prototype hasOwnProperty for faster access
38
39 var hasOwnProperty = Object.prototype.hasOwnProperty;
40
41 ///////////////////////////////////////////////////////////////////////////
42 // The Canvas object is a wrapper around an HTML5 <canvas> tag.
43 //
44 // @constructor
45 // @param {string} cls List of classes to apply to the canvas.
46 // @param {element} container Element onto which to append the canvas.
47 //
48 // Requiring a container is a little iffy, but unfortunately canvas
49 // operations don't work unless the canvas is attached to the DOM.
50
51 function Canvas(cls, container) {
52
53 var element = container.children("." + cls)[0];
54
55 if (element == null) {
56
57 element = document.createElement("canvas");
58 element.className = cls;
59
60 $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 })
61 .appendTo(container);
62
63 // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas
64
65 if (!element.getContext) {
66 if (window.G_vmlCanvasManager) {
67 element = window.G_vmlCanvasManager.initElement(element);
68 } else {
69 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.");
70 }
71 }
72 }
73
74 this.element = element;
75
76 var context = this.context = element.getContext("2d");
77
78 // Determine the screen's ratio of physical to device-independent
79 // pixels. This is the ratio between the canvas width that the browser
80 // advertises and the number of pixels actually present in that space.
81
82 // The iPhone 4, for example, has a device-independent width of 320px,
83 // but its screen is actually 640px wide. It therefore has a pixel
84 // ratio of 2, while most normal devices have a ratio of 1.
85
86 var devicePixelRatio = window.devicePixelRatio || 1,
87 backingStoreRatio =
88 context.webkitBackingStorePixelRatio ||
89 context.mozBackingStorePixelRatio ||
90 context.msBackingStorePixelRatio ||
91 context.oBackingStorePixelRatio ||
92 context.backingStorePixelRatio || 1;
93
94 this.pixelRatio = devicePixelRatio / backingStoreRatio;
95
96 // Size the canvas to match the internal dimensions of its container
97
98 this.resize(container.width(), container.height());
99
100 // Collection of HTML div layers for text overlaid onto the canvas
101
102 this.textContainer = null;
103 this.text = {};
104
105 // Cache of text fragments and metrics, so we can avoid expensively
106 // re-calculating them when the plot is re-rendered in a loop.
107
108 this._textCache = {};
109 }
110
111 // Resizes the canvas to the given dimensions.
112 //
113 // @param {number} width New width of the canvas, in pixels.
114 // @param {number} width New height of the canvas, in pixels.
115
116 Canvas.prototype.resize = function(width, height) {
117
118 if (width <= 0 || height <= 0) {
119 throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height);
120 }
121
122 var element = this.element,
123 context = this.context,
124 pixelRatio = this.pixelRatio;
125
126 // Resize the canvas, increasing its density based on the display's
127 // pixel ratio; basically giving it more pixels without increasing the
128 // size of its element, to take advantage of the fact that retina
129 // displays have that many more pixels in the same advertised space.
130
131 // Resizing should reset the state (excanvas seems to be buggy though)
132
133 if (this.width != width) {
134 element.width = width * pixelRatio;
135 element.style.width = width + "px";
136 this.width = width;
137 }
138
139 if (this.height != height) {
140 element.height = height * pixelRatio;
141 element.style.height = height + "px";
142 this.height = height;
143 }
144
145 // Save the context, so we can reset in case we get replotted. The
146 // restore ensure that we're really back at the initial state, and
147 // should be safe even if we haven't saved the initial state yet.
148
149 context.restore();
150 context.save();
151
152 // Scale the coordinate space to match the display density; so even though we
153 // may have twice as many pixels, we still want lines and other drawing to
154 // appear at the same size; the extra pixels will just make them crisper.
155
156 context.scale(pixelRatio, pixelRatio);
157 };
158
159 // Clears the entire canvas area, not including any overlaid HTML text
160
161 Canvas.prototype.clear = function() {
162 this.context.clearRect(0, 0, this.width, this.height);
163 };
164
165 // Finishes rendering the canvas, including managing the text overlay.
166
167 Canvas.prototype.render = function() {
168
169 var cache = this._textCache;
170
171 // For each text layer, add elements marked as active that haven't
172 // already been rendered, and remove those that are no longer active.
173
174 for (var layerKey in cache) {
175 if (hasOwnProperty.call(cache, layerKey)) {
176
177 var layer = this.getTextLayer(layerKey),
178 layerCache = cache[layerKey];
179
180 layer.hide();
181
182 for (var styleKey in layerCache) {
183 if (hasOwnProperty.call(layerCache, styleKey)) {
184 var styleCache = layerCache[styleKey];
185 for (var key in styleCache) {
186 if (hasOwnProperty.call(styleCache, key)) {
187
188 var positions = styleCache[key].positions;
189
190 for (var i = 0, position; position = positions[i]; i++) {
191 if (position.active) {
192 if (!position.rendered) {
193 layer.append(position.element);
194 position.rendered = true;
195 }
196 } else {
197 positions.splice(i--, 1);
198 if (position.rendered) {
199 position.element.detach();
200 }
201 }
202 }
203
204 if (positions.length == 0) {
205 delete styleCache[key];
206 }
207 }
208 }
209 }
210 }
211
212 layer.show();
213 }
214 }
215 };
216
217 // Creates (if necessary) and returns the text overlay container.
218 //
219 // @param {string} classes String of space-separated CSS classes used to
220 // uniquely identify the text layer.
221 // @return {object} The jQuery-wrapped text-layer div.
222
223 Canvas.prototype.getTextLayer = function(classes) {
224
225 var layer = this.text[classes];
226
227 // Create the text layer if it doesn't exist
228
229 if (layer == null) {
230
231 // Create the text layer container, if it doesn't exist
232
233 if (this.textContainer == null) {
234 this.textContainer = $("<div class='flot-text'></div>")
235 .css({
236 position: "absolute",
237 top: 0,
238 left: 0,
239 bottom: 0,
240 right: 0,
241 'font-size': "smaller",
242 color: "#545454"
243 })
244 .insertAfter(this.element);
245 }
246
247 layer = this.text[classes] = $("<div></div>")
248 .addClass(classes)
249 .css({
250 position: "absolute",
251 top: 0,
252 left: 0,
253 bottom: 0,
254 right: 0
255 })
256 .appendTo(this.textContainer);
257 }
258
259 return layer;
260 };
261
262 // Creates (if necessary) and returns a text info object.
263 //
264 // The object looks like this:
265 //
266 // {
267 // width: Width of the text's wrapper div.
268 // height: Height of the text's wrapper div.
269 // element: The jQuery-wrapped HTML div containing the text.
270 // positions: Array of positions at which this text is drawn.
271 // }
272 //
273 // The positions array contains objects that look like this:
274 //
275 // {
276 // active: Flag indicating whether the text should be visible.
277 // rendered: Flag indicating whether the text is currently visible.
278 // element: The jQuery-wrapped HTML div containing the text.
279 // x: X coordinate at which to draw the text.
280 // y: Y coordinate at which to draw the text.
281 // }
282 //
283 // Each position after the first receives a clone of the original element.
284 //
285 // The idea is that that the width, height, and general 'identity' of the
286 // text is constant no matter where it is placed; the placements are a
287 // secondary property.
288 //
289 // Canvas maintains a cache of recently-used text info objects; getTextInfo
290 // either returns the cached element or creates a new entry.
291 //
292 // @param {string} layer A string of space-separated CSS classes uniquely
293 // identifying the layer containing this text.
294 // @param {string} text Text string to retrieve info for.
295 // @param {(string|object)=} font Either a string of space-separated CSS
296 // classes or a font-spec object, defining the text's font and style.
297 // @param {number=} angle Angle at which to rotate the text, in degrees.
298 // Angle is currently unused, it will be implemented in the future.
299 // @param {number=} width Maximum width of the text before it wraps.
300 // @return {object} a text info object.
301
302 Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) {
303
304 var textStyle, layerCache, styleCache, info;
305
306 // Cast the value to a string, in case we were given a number or such
307
308 text = "" + text;
309
310 // If the font is a font-spec object, generate a CSS font definition
311
312 if (typeof font === "object") {
313 textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family;
314 } else {
315 textStyle = font;
316 }
317
318 // Retrieve (or create) the cache for the text's layer and styles
319
320 layerCache = this._textCache[layer];
321
322 if (layerCache == null) {
323 layerCache = this._textCache[layer] = {};
324 }
325
326 styleCache = layerCache[textStyle];
327
328 if (styleCache == null) {
329 styleCache = layerCache[textStyle] = {};
330 }
331
332 info = styleCache[text];
333
334 // If we can't find a matching element in our cache, create a new one
335
336 if (info == null) {
337
338 var element = $("<div></div>").html(text)
339 .css({
340 position: "absolute",
341 'max-width': width,
342 top: -9999
343 })
344 .appendTo(this.getTextLayer(layer));
345
346 if (typeof font === "object") {
347 element.css({
348 font: textStyle,
349 color: font.color
350 });
351 } else if (typeof font === "string") {
352 element.addClass(font);
353 }
354
355 info = styleCache[text] = {
356 width: element.outerWidth(true),
357 height: element.outerHeight(true),
358 element: element,
359 positions: []
360 };
361
362 element.detach();
363 }
364
365 return info;
366 };
367
368 // Adds a text string to the canvas text overlay.
369 //
370 // The text isn't drawn immediately; it is marked as rendering, which will
371 // result in its addition to the canvas on the next render pass.
372 //
373 // @param {string} layer A string of space-separated CSS classes uniquely
374 // identifying the layer containing this text.
375 // @param {number} x X coordinate at which to draw the text.
376 // @param {number} y Y coordinate at which to draw the text.
377 // @param {string} text Text string to draw.
378 // @param {(string|object)=} font Either a string of space-separated CSS
379 // classes or a font-spec object, defining the text's font and style.
380 // @param {number=} angle Angle at which to rotate the text, in degrees.
381 // Angle is currently unused, it will be implemented in the future.
382 // @param {number=} width Maximum width of the text before it wraps.
383 // @param {string=} halign Horizontal alignment of the text; either "left",
384 // "center" or "right".
385 // @param {string=} valign Vertical alignment of the text; either "top",
386 // "middle" or "bottom".
387
388 Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) {
389
390 var info = this.getTextInfo(layer, text, font, angle, width),
391 positions = info.positions;
392
393 // Tweak the div's position to match the text's alignment
394
395 if (halign == "center") {
396 x -= info.width / 2;
397 } else if (halign == "right") {
398 x -= info.width;
399 }
400
401 if (valign == "middle") {
402 y -= info.height / 2;
403 } else if (valign == "bottom") {
404 y -= info.height;
405 }
406
407 // Determine whether this text already exists at this position.
408 // If so, mark it for inclusion in the next render pass.
409
410 for (var i = 0, position; position = positions[i]; i++) {
411 if (position.x == x && position.y == y) {
412 position.active = true;
413 return;
414 }
415 }
416
417 // If the text doesn't exist at this position, create a new entry
418
419 // For the very first position we'll re-use the original element,
420 // while for subsequent ones we'll clone it.
421
422 position = {
423 active: true,
424 rendered: false,
425 element: positions.length ? info.element.clone() : info.element,
426 x: x,
427 y: y
428 }
429
430 positions.push(position);
431
432 // Move the element to its final position within the container
433
434 position.element.css({
435 top: Math.round(y),
436 left: Math.round(x),
437 'text-align': halign // In case the text wraps
438 });
439 };
440
441 // Removes one or more text strings from the canvas text overlay.
442 //
443 // If no parameters are given, all text within the layer is removed.
444 //
445 // Note that the text is not immediately removed; it is simply marked as
446 // inactive, which will result in its removal on the next render pass.
447 // This avoids the performance penalty for 'clear and redraw' behavior,
448 // where we potentially get rid of all text on a layer, but will likely
449 // add back most or all of it later, as when redrawing axes, for example.
450 //
451 // @param {string} layer A string of space-separated CSS classes uniquely
452 // identifying the layer containing this text.
453 // @param {number=} x X coordinate of the text.
454 // @param {number=} y Y coordinate of the text.
455 // @param {string=} text Text string to remove.
456 // @param {(string|object)=} font Either a string of space-separated CSS
457 // classes or a font-spec object, defining the text's font and style.
458 // @param {number=} angle Angle at which the text is rotated, in degrees.
459 // Angle is currently unused, it will be implemented in the future.
460
461 Canvas.prototype.removeText = function(layer, x, y, text, font, angle) {
462 if (text == null) {
463 var layerCache = this._textCache[layer];
464 if (layerCache != null) {
465 for (var styleKey in layerCache) {
466 if (hasOwnProperty.call(layerCache, styleKey)) {
467 var styleCache = layerCache[styleKey];
468 for (var key in styleCache) {
469 if (hasOwnProperty.call(styleCache, key)) {
470 var positions = styleCache[key].positions;
471 for (var i = 0, position; position = positions[i]; i++) {
472 position.active = false;
473 }
474 }
475 }
476 }
477 }
478 }
479 } else {
480 var positions = this.getTextInfo(layer, text, font, angle).positions;
481 for (var i = 0, position; position = positions[i]; i++) {
482 if (position.x == x && position.y == y) {
483 position.active = false;
484 }
485 }
486 }
487 };
488
489 ///////////////////////////////////////////////////////////////////////////
490 // The top-level container for the entire plot.
491
492 function Plot(placeholder, data_, options_, plugins) {
493 // data is on the form:
494 // [ series1, series2 ... ]
495 // where series is either just the data as [ [x1, y1], [x2, y2], ... ]
496 // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... }
497
498 var series = [],
499 options = {
500 // the color theme used for graphs
501 colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"],
502 legend: {
503 show: true,
504 noColumns: 1, // number of colums in legend table
505 labelFormatter: null, // fn: string -> string
506 labelBoxBorderColor: "#ccc", // border color for the little label boxes
507 container: null, // container (as jQuery object) to put legend in, null means default on top of graph
508 position: "ne", // position of default legend container within plot
509 margin: 5, // distance from grid edge to default legend container within plot
510 backgroundColor: null, // null means auto-detect
511 backgroundOpacity: 0.85, // set to 0 to avoid background
512 sorted: null // default to no legend sorting
513 },
514 xaxis: {
515 show: null, // null = auto-detect, true = always, false = never
516 position: "bottom", // or "top"
517 mode: null, // null or "time"
518 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" }
519 color: null, // base color, labels, ticks
520 tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)"
521 transform: null, // null or f: number -> number to transform axis
522 inverseTransform: null, // if transform is set, this should be the inverse function
523 min: null, // min. value to show, null means set automatically
524 max: null, // max. value to show, null means set automatically
525 autoscaleMargin: null, // margin in % to add if auto-setting min/max
526 ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks
527 tickFormatter: null, // fn: number -> string
528 labelWidth: null, // size of tick labels in pixels
529 labelHeight: null,
530 reserveSpace: null, // whether to reserve space even if axis isn't shown
531 tickLength: null, // size in pixels of ticks, or "full" for whole line
532 alignTicksWithAxis: null, // axis number or null for no sync
533 tickDecimals: null, // no. of decimals, null means auto
534 tickSize: null, // number or [number, "unit"]
535 minTickSize: null // number or [number, "unit"]
536 },
537 yaxis: {
538 autoscaleMargin: 0.02,
539 position: "left" // or "right"
540 },
541 xaxes: [],
542 yaxes: [],
543 series: {
544 points: {
545 show: false,
546 radius: 3,
547 lineWidth: 2, // in pixels
548 fill: true,
549 fillColor: "#ffffff",
550 symbol: "circle" // or callback
551 },
552 lines: {
553 // we don't put in show: false so we can see
554 // whether lines were actively disabled
555 lineWidth: 2, // in pixels
556 fill: false,
557 fillColor: null,
558 steps: false
559 // Omit 'zero', so we can later default its value to
560 // match that of the 'fill' option.
561 },
562 bars: {
563 show: false,
564 lineWidth: 2, // in pixels
565 barWidth: 1, // in units of the x axis
566 fill: true,
567 fillColor: null,
568 align: "left", // "left", "right", or "center"
569 horizontal: false,
570 zero: true
571 },
572 shadowSize: 3,
573 highlightColor: null
574 },
575 grid: {
576 show: true,
577 aboveData: false,
578 color: "#545454", // primary color used for outline and labels
579 backgroundColor: null, // null for transparent, else color
580 borderColor: null, // set if different from the grid color
581 tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)"
582 margin: 0, // distance from the canvas edge to the grid
583 labelMargin: 5, // in pixels
584 axisMargin: 8, // in pixels
585 borderWidth: 2, // in pixels
586 minBorderMargin: null, // in pixels, null means taken from points radius
587 markings: null, // array of ranges or fn: axes -> array of ranges
588 markingsColor: "#f4f4f4",
589 markingsLineWidth: 2,
590 // interactive stuff
591 clickable: false,
592 hoverable: false,
593 autoHighlight: true, // highlight in case mouse is near
594 mouseActiveRadius: 10 // how far the mouse can be away to activate an item
595 },
596 interaction: {
597 redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow
598 },
599 hooks: {}
600 },
601 surface = null, // the canvas for the plot itself
602 overlay = null, // canvas for interactive stuff on top of plot
603 eventHolder = null, // jQuery object that events should be bound to
604 ctx = null, octx = null,
605 xaxes = [], yaxes = [],
606 plotOffset = { left: 0, right: 0, top: 0, bottom: 0},
607 plotWidth = 0, plotHeight = 0,
608 hooks = {
609 processOptions: [],
610 processRawData: [],
611 processDatapoints: [],
612 processOffset: [],
613 drawBackground: [],
614 drawSeries: [],
615 draw: [],
616 bindEvents: [],
617 drawOverlay: [],
618 shutdown: []
619 },
620 plot = this;
621
622 // public functions
623 plot.setData = setData;
624 plot.setupGrid = setupGrid;
625 plot.draw = draw;
626 plot.getPlaceholder = function() { return placeholder; };
627 plot.getCanvas = function() { return surface.element; };
628 plot.getPlotOffset = function() { return plotOffset; };
629 plot.width = function () { return plotWidth; };
630 plot.height = function () { return plotHeight; };
631 plot.offset = function () {
632 var o = eventHolder.offset();
633 o.left += plotOffset.left;
634 o.top += plotOffset.top;
635 return o;
636 };
637 plot.getData = function () { return series; };
638 plot.getAxes = function () {
639 var res = {}, i;
640 $.each(xaxes.concat(yaxes), function (_, axis) {
641 if (axis)
642 res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis;
643 });
644 return res;
645 };
646 plot.getXAxes = function () { return xaxes; };
647 plot.getYAxes = function () { return yaxes; };
648 plot.c2p = canvasToAxisCoords;
649 plot.p2c = axisToCanvasCoords;
650 plot.getOptions = function () { return options; };
651 plot.highlight = highlight;
652 plot.unhighlight = unhighlight;
653 plot.triggerRedrawOverlay = triggerRedrawOverlay;
654 plot.pointOffset = function(point) {
655 return {
656 left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10),
657 top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10)
658 };
659 };
660 plot.shutdown = shutdown;
661 plot.resize = function () {
662 var width = placeholder.width(),
663 height = placeholder.height();
664 surface.resize(width, height);
665 overlay.resize(width, height);
666 };
667
668 // public attributes
669 plot.hooks = hooks;
670
671 // initialize
672 initPlugins(plot);
673 parseOptions(options_);
674 setupCanvases();
675 setData(data_);
676 setupGrid();
677 draw();
678 bindEvents();
679
680
681 function executeHooks(hook, args) {
682 args = [plot].concat(args);
683 for (var i = 0; i < hook.length; ++i)
684 hook[i].apply(this, args);
685 }
686
687 function initPlugins() {
688
689 // References to key classes, allowing plugins to modify them
690
691 var classes = {
692 Canvas: Canvas
693 };
694
695 for (var i = 0; i < plugins.length; ++i) {
696 var p = plugins[i];
697 p.init(plot, classes);
698 if (p.options)
699 $.extend(true, options, p.options);
700 }
701 }
702
703 function parseOptions(opts) {
704
705 $.extend(true, options, opts);
706
707 // $.extend merges arrays, rather than replacing them. When less
708 // colors are provided than the size of the default palette, we
709 // end up with those colors plus the remaining defaults, which is
710 // not expected behavior; avoid it by replacing them here.
711
712 if (opts && opts.colors) {
713 options.colors = opts.colors;
714 }
715
716 if (options.xaxis.color == null)
717 options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString();
718 if (options.yaxis.color == null)
719 options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString();
720
721 if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility
722 options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color;
723 if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility
724 options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color;
725
726 if (options.grid.borderColor == null)
727 options.grid.borderColor = options.grid.color;
728 if (options.grid.tickColor == null)
729 options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString();
730
731 // Fill in defaults for axis options, including any unspecified
732 // font-spec fields, if a font-spec was provided.
733
734 // If no x/y axis options were provided, create one of each anyway,
735 // since the rest of the code assumes that they exist.
736
737 var i, axisOptions, axisCount,
738 fontDefaults = {
739 style: placeholder.css("font-style"),
740 size: Math.round(0.8 * (+placeholder.css("font-size").replace("px", "") || 13)),
741 variant: placeholder.css("font-variant"),
742 weight: placeholder.css("font-weight"),
743 family: placeholder.css("font-family")
744 };
745
746 fontDefaults.lineHeight = fontDefaults.size * 1.15;
747
748 axisCount = options.xaxes.length || 1;
749 for (i = 0; i < axisCount; ++i) {
750
751 axisOptions = options.xaxes[i];
752 if (axisOptions && !axisOptions.tickColor) {
753 axisOptions.tickColor = axisOptions.color;
754 }
755
756 axisOptions = $.extend(true, {}, options.xaxis, axisOptions);
757 options.xaxes[i] = axisOptions;
758
759 if (axisOptions.font) {
760 axisOptions.font = $.extend({}, fontDefaults, axisOptions.font);
761 if (!axisOptions.font.color) {
762 axisOptions.font.color = axisOptions.color;
763 }
764 }
765 }
766
767 axisCount = options.yaxes.length || 1;
768 for (i = 0; i < axisCount; ++i) {
769
770 axisOptions = options.yaxes[i];
771 if (axisOptions && !axisOptions.tickColor) {
772 axisOptions.tickColor = axisOptions.color;
773 }
774
775 axisOptions = $.extend(true, {}, options.yaxis, axisOptions);
776 options.yaxes[i] = axisOptions;
777
778 if (axisOptions.font) {
779 axisOptions.font = $.extend({}, fontDefaults, axisOptions.font);
780 if (!axisOptions.font.color) {
781 axisOptions.font.color = axisOptions.color;
782 }
783 }
784 }
785
786 // backwards compatibility, to be removed in future
787 if (options.xaxis.noTicks && options.xaxis.ticks == null)
788 options.xaxis.ticks = options.xaxis.noTicks;
789 if (options.yaxis.noTicks && options.yaxis.ticks == null)
790 options.yaxis.ticks = options.yaxis.noTicks;
791 if (options.x2axis) {
792 options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis);
793 options.xaxes[1].position = "top";
794 }
795 if (options.y2axis) {
796 options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis);
797 options.yaxes[1].position = "right";
798 }
799 if (options.grid.coloredAreas)
800 options.grid.markings = options.grid.coloredAreas;
801 if (options.grid.coloredAreasColor)
802 options.grid.markingsColor = options.grid.coloredAreasColor;
803 if (options.lines)
804 $.extend(true, options.series.lines, options.lines);
805 if (options.points)
806 $.extend(true, options.series.points, options.points);
807 if (options.bars)
808 $.extend(true, options.series.bars, options.bars);
809 if (options.shadowSize != null)
810 options.series.shadowSize = options.shadowSize;
811 if (options.highlightColor != null)
812 options.series.highlightColor = options.highlightColor;
813
814 // save options on axes for future reference
815 for (i = 0; i < options.xaxes.length; ++i)
816 getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i];
817 for (i = 0; i < options.yaxes.length; ++i)
818 getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i];
819
820 // add hooks from options
821 for (var n in hooks)
822 if (options.hooks[n] && options.hooks[n].length)
823 hooks[n] = hooks[n].concat(options.hooks[n]);
824
825 executeHooks(hooks.processOptions, [options]);
826 }
827
828 function setData(d) {
829 series = parseData(d);
830 fillInSeriesOptions();
831 processData();
832 }
833
834 function parseData(d) {
835 var res = [];
836 for (var i = 0; i < d.length; ++i) {
837 var s = $.extend(true, {}, options.series);
838
839 if (d[i].data != null) {
840 s.data = d[i].data; // move the data instead of deep-copy
841 delete d[i].data;
842
843 $.extend(true, s, d[i]);
844
845 d[i].data = s.data;
846 }
847 else
848 s.data = d[i];
849 res.push(s);
850 }
851
852 return res;
853 }
854
855 function axisNumber(obj, coord) {
856 var a = obj[coord + "axis"];
857 if (typeof a == "object") // if we got a real axis, extract number
858 a = a.n;
859 if (typeof a != "number")
860 a = 1; // default to first axis
861 return a;
862 }
863
864 function allAxes() {
865 // return flat array without annoying null entries
866 return $.grep(xaxes.concat(yaxes), function (a) { return a; });
867 }
868
869 function canvasToAxisCoords(pos) {
870 // return an object with x/y corresponding to all used axes
871 var res = {}, i, axis;
872 for (i = 0; i < xaxes.length; ++i) {
873 axis = xaxes[i];
874 if (axis && axis.used)
875 res["x" + axis.n] = axis.c2p(pos.left);
876 }
877
878 for (i = 0; i < yaxes.length; ++i) {
879 axis = yaxes[i];
880 if (axis && axis.used)
881 res["y" + axis.n] = axis.c2p(pos.top);
882 }
883
884 if (res.x1 !== undefined)
885 res.x = res.x1;
886 if (res.y1 !== undefined)
887 res.y = res.y1;
888
889 return res;
890 }
891
892 function axisToCanvasCoords(pos) {
893 // get canvas coords from the first pair of x/y found in pos
894 var res = {}, i, axis, key;
895
896 for (i = 0; i < xaxes.length; ++i) {
897 axis = xaxes[i];
898 if (axis && axis.used) {
899 key = "x" + axis.n;
900 if (pos[key] == null && axis.n == 1)
901 key = "x";
902
903 if (pos[key] != null) {
904 res.left = axis.p2c(pos[key]);
905 break;
906 }
907 }
908 }
909
910 for (i = 0; i < yaxes.length; ++i) {
911 axis = yaxes[i];
912 if (axis && axis.used) {
913 key = "y" + axis.n;
914 if (pos[key] == null && axis.n == 1)
915 key = "y";
916
917 if (pos[key] != null) {
918 res.top = axis.p2c(pos[key]);
919 break;
920 }
921 }
922 }
923
924 return res;
925 }
926
927 function getOrCreateAxis(axes, number) {
928 if (!axes[number - 1])
929 axes[number - 1] = {
930 n: number, // save the number for future reference
931 direction: axes == xaxes ? "x" : "y",
932 options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis)
933 };
934
935 return axes[number - 1];
936 }
937
938 function fillInSeriesOptions() {
939
940 var neededColors = series.length, maxIndex = -1, i;
941
942 // Subtract the number of series that already have fixed colors or
943 // color indexes from the number that we still need to generate.
944
945 for (i = 0; i < series.length; ++i) {
946 var sc = series[i].color;
947 if (sc != null) {
948 neededColors--;
949 if (typeof sc == "number" && sc > maxIndex) {
950 maxIndex = sc;
951 }
952 }
953 }
954
955 // If any of the series have fixed color indexes, then we need to
956 // generate at least as many colors as the highest index.
957
958 if (neededColors <= maxIndex) {
959 neededColors = maxIndex + 1;
960 }
961
962 // Generate all the colors, using first the option colors and then
963 // variations on those colors once they're exhausted.
964
965 var c, colors = [], colorPool = options.colors,
966 colorPoolSize = colorPool.length, variation = 0;
967
968 for (i = 0; i < neededColors; i++) {
969
970 c = $.color.parse(colorPool[i % colorPoolSize] || "#666");
971
972 // Each time we exhaust the colors in the pool we adjust
973 // a scaling factor used to produce more variations on
974 // those colors. The factor alternates negative/positive
975 // to produce lighter/darker colors.
976
977 // Reset the variation after every few cycles, or else
978 // it will end up producing only white or black colors.
979
980 if (i % colorPoolSize == 0 && i) {
981 if (variation >= 0) {
982 if (variation < 0.5) {
983 variation = -variation - 0.2;
984 } else variation = 0;
985 } else variation = -variation;
986 }
987
988 colors[i] = c.scale('rgb', 1 + variation);
989 }
990
991 // Finalize the series options, filling in their colors
992
993 var colori = 0, s;
994 for (i = 0; i < series.length; ++i) {
995 s = series[i];
996
997 // assign colors
998 if (s.color == null) {
999 s.color = colors[colori].toString();
1000 ++colori;
1001 }
1002 else if (typeof s.color == "number")
1003 s.color = colors[s.color].toString();
1004
1005 // turn on lines automatically in case nothing is set
1006 if (s.lines.show == null) {
1007 var v, show = true;
1008 for (v in s)
1009 if (s[v] && s[v].show) {
1010 show = false;
1011 break;
1012 }
1013 if (show)
1014 s.lines.show = true;
1015 }
1016
1017 // If nothing was provided for lines.zero, default it to match
1018 // lines.fill, since areas by default should extend to zero.
1019
1020 if (s.lines.zero == null) {
1021 s.lines.zero = !!s.lines.fill;
1022 }
1023
1024 // setup axes
1025 s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x"));
1026 s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y"));
1027 }
1028 }
1029
1030 function processData() {
1031 var topSentry = Number.POSITIVE_INFINITY,
1032 bottomSentry = Number.NEGATIVE_INFINITY,
1033 fakeInfinity = Number.MAX_VALUE,
1034 i, j, k, m, length,
1035 s, points, ps, x, y, axis, val, f, p,
1036 data, format;
1037
1038 function updateAxis(axis, min, max) {
1039 if (min < axis.datamin && min != -fakeInfinity)
1040 axis.datamin = min;
1041 if (max > axis.datamax && max != fakeInfinity)
1042 axis.datamax = max;
1043 }
1044
1045 $.each(allAxes(), function (_, axis) {
1046 // init axis
1047 axis.datamin = topSentry;
1048 axis.datamax = bottomSentry;
1049 axis.used = false;
1050 });
1051
1052 for (i = 0; i < series.length; ++i) {
1053 s = series[i];
1054 s.datapoints = { points: [] };
1055
1056 executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]);
1057 }
1058
1059 // first pass: clean and copy data
1060 for (i = 0; i < series.length; ++i) {
1061 s = series[i];
1062
1063 data = s.data;
1064 format = s.datapoints.format;
1065
1066 if (!format) {
1067 format = [];
1068 // find out how to copy
1069 format.push({ x: true, number: true, required: true });
1070 format.push({ y: true, number: true, required: true });
1071
1072 if (s.bars.show || (s.lines.show && s.lines.fill)) {
1073 var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero));
1074 format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale });
1075 if (s.bars.horizontal) {
1076 delete format[format.length - 1].y;
1077 format[format.length - 1].x = true;
1078 }
1079 }
1080
1081 s.datapoints.format = format;
1082 }
1083
1084 if (s.datapoints.pointsize != null)
1085 continue; // already filled in
1086
1087 s.datapoints.pointsize = format.length;
1088
1089 ps = s.datapoints.pointsize;
1090 points = s.datapoints.points;
1091
1092 var insertSteps = s.lines.show && s.lines.steps;
1093 s.xaxis.used = s.yaxis.used = true;
1094
1095 for (j = k = 0; j < data.length; ++j, k += ps) {
1096 p = data[j];
1097
1098 var nullify = p == null;
1099 if (!nullify) {
1100 for (m = 0; m < ps; ++m) {
1101 val = p[m];
1102 f = format[m];
1103
1104 if (f) {
1105 if (f.number && val != null) {
1106 val = +val; // convert to number
1107 if (isNaN(val))
1108 val = null;
1109 else if (val == Infinity)
1110 val = fakeInfinity;
1111 else if (val == -Infinity)
1112 val = -fakeInfinity;
1113 }
1114
1115 if (val == null) {
1116 if (f.required)
1117 nullify = true;
1118
1119 if (f.defaultValue != null)
1120 val = f.defaultValue;
1121 }
1122 }
1123
1124 points[k + m] = val;
1125 }
1126 }
1127
1128 if (nullify) {
1129 for (m = 0; m < ps; ++m) {
1130 val = points[k + m];
1131 if (val != null) {
1132 f = format[m];
1133 // extract min/max info
1134 if (f.autoscale) {
1135 if (f.x) {
1136 updateAxis(s.xaxis, val, val);
1137 }
1138 if (f.y) {
1139 updateAxis(s.yaxis, val, val);
1140 }
1141 }
1142 }
1143 points[k + m] = null;
1144 }
1145 }
1146 else {
1147 // a little bit of line specific stuff that
1148 // perhaps shouldn't be here, but lacking
1149 // better means...
1150 if (insertSteps && k > 0
1151 && points[k - ps] != null
1152 && points[k - ps] != points[k]
1153 && points[k - ps + 1] != points[k + 1]) {
1154 // copy the point to make room for a middle point
1155 for (m = 0; m < ps; ++m)
1156 points[k + ps + m] = points[k + m];
1157
1158 // middle point has same y
1159 points[k + 1] = points[k - ps + 1];
1160
1161 // we've added a point, better reflect that
1162 k += ps;
1163 }
1164 }
1165 }
1166 }
1167
1168 // give the hooks a chance to run
1169 for (i = 0; i < series.length; ++i) {
1170 s = series[i];
1171
1172 executeHooks(hooks.processDatapoints, [ s, s.datapoints]);
1173 }
1174
1175 // second pass: find datamax/datamin for auto-scaling
1176 for (i = 0; i < series.length; ++i) {
1177 s = series[i];
1178 points = s.datapoints.points;
1179 ps = s.datapoints.pointsize;
1180 format = s.datapoints.format;
1181
1182 var xmin = topSentry, ymin = topSentry,
1183 xmax = bottomSentry, ymax = bottomSentry;
1184
1185 for (j = 0; j < points.length; j += ps) {
1186 if (points[j] == null)
1187 continue;
1188
1189 for (m = 0; m < ps; ++m) {
1190 val = points[j + m];
1191 f = format[m];
1192 if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity)
1193 continue;
1194
1195 if (f.x) {
1196 if (val < xmin)
1197 xmin = val;
1198 if (val > xmax)
1199 xmax = val;
1200 }
1201 if (f.y) {
1202 if (val < ymin)
1203 ymin = val;
1204 if (val > ymax)
1205 ymax = val;
1206 }
1207 }
1208 }
1209
1210 if (s.bars.show) {
1211 // make sure we got room for the bar on the dancing floor
1212 var delta;
1213
1214 switch (s.bars.align) {
1215 case "left":
1216 delta = 0;
1217 break;
1218 case "right":
1219 delta = -s.bars.barWidth;
1220 break;
1221 case "center":
1222 delta = -s.bars.barWidth / 2;
1223 break;
1224 default:
1225 throw new Error("Invalid bar alignment: " + s.bars.align);
1226 }
1227
1228 if (s.bars.horizontal) {
1229 ymin += delta;
1230 ymax += delta + s.bars.barWidth;
1231 }
1232 else {
1233 xmin += delta;
1234 xmax += delta + s.bars.barWidth;
1235 }
1236 }
1237
1238 updateAxis(s.xaxis, xmin, xmax);
1239 updateAxis(s.yaxis, ymin, ymax);
1240 }
1241
1242 $.each(allAxes(), function (_, axis) {
1243 if (axis.datamin == topSentry)
1244 axis.datamin = null;
1245 if (axis.datamax == bottomSentry)
1246 axis.datamax = null;
1247 });
1248 }
1249
1250 function setupCanvases() {
1251
1252 // Make sure the placeholder is clear of everything except canvases
1253 // from a previous plot in this container that we'll try to re-use.
1254
1255 placeholder.css("padding", 0) // padding messes up the positioning
1256 .children(":not(.flot-base,.flot-overlay)").remove();
1257
1258 if (placeholder.css("position") == 'static')
1259 placeholder.css("position", "relative"); // for positioning labels and overlay
1260
1261 surface = new Canvas("flot-base", placeholder);
1262 overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features
1263
1264 ctx = surface.context;
1265 octx = overlay.context;
1266
1267 // define which element we're listening for events on
1268 eventHolder = $(overlay.element).unbind();
1269
1270 // If we're re-using a plot object, shut down the old one
1271
1272 var existing = placeholder.data("plot");
1273
1274 if (existing) {
1275 existing.shutdown();
1276 overlay.clear();
1277 }
1278
1279 // save in case we get replotted
1280 placeholder.data("plot", plot);
1281 }
1282
1283 function bindEvents() {
1284 // bind events
1285 if (options.grid.hoverable) {
1286 eventHolder.mousemove(onMouseMove);
1287
1288 // Use bind, rather than .mouseleave, because we officially
1289 // still support jQuery 1.2.6, which doesn't define a shortcut
1290 // for mouseenter or mouseleave. This was a bug/oversight that
1291 // was fixed somewhere around 1.3.x. We can return to using
1292 // .mouseleave when we drop support for 1.2.6.
1293
1294 eventHolder.bind("mouseleave", onMouseLeave);
1295 }
1296
1297 if (options.grid.clickable)
1298 eventHolder.click(onClick);
1299
1300 executeHooks(hooks.bindEvents, [eventHolder]);
1301 }
1302
1303 function shutdown() {
1304 if (redrawTimeout)
1305 clearTimeout(redrawTimeout);
1306
1307 eventHolder.unbind("mousemove", onMouseMove);
1308 eventHolder.unbind("mouseleave", onMouseLeave);
1309 eventHolder.unbind("click", onClick);
1310
1311 executeHooks(hooks.shutdown, [eventHolder]);
1312 }
1313
1314 function setTransformationHelpers(axis) {
1315 // set helper functions on the axis, assumes plot area
1316 // has been computed already
1317
1318 function identity(x) { return x; }
1319
1320 var s, m, t = axis.options.transform || identity,
1321 it = axis.options.inverseTransform;
1322
1323 // precompute how much the axis is scaling a point
1324 // in canvas space
1325 if (axis.direction == "x") {
1326 s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min));
1327 m = Math.min(t(axis.max), t(axis.min));
1328 }
1329 else {
1330 s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min));
1331 s = -s;
1332 m = Math.max(t(axis.max), t(axis.min));
1333 }
1334
1335 // data point to canvas coordinate
1336 if (t == identity) // slight optimization
1337 axis.p2c = function (p) { return (p - m) * s; };
1338 else
1339 axis.p2c = function (p) { return (t(p) - m) * s; };
1340 // canvas coordinate to data point
1341 if (!it)
1342 axis.c2p = function (c) { return m + c / s; };
1343 else
1344 axis.c2p = function (c) { return it(m + c / s); };
1345 }
1346
1347 function measureTickLabels(axis) {
1348
1349 var opts = axis.options,
1350 ticks = axis.ticks || [],
1351 labelWidth = opts.labelWidth || 0,
1352 labelHeight = opts.labelHeight || 0,
1353 maxWidth = labelWidth || axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null;
1354 legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis",
1355 layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles,
1356 font = opts.font || "flot-tick-label tickLabel";
1357
1358 for (var i = 0; i < ticks.length; ++i) {
1359
1360 var t = ticks[i];
1361
1362 if (!t.label)
1363 continue;
1364
1365 var info = surface.getTextInfo(layer, t.label, font, null, maxWidth);
1366
1367 labelWidth = Math.max(labelWidth, info.width);
1368 labelHeight = Math.max(labelHeight, info.height);
1369 }
1370
1371 axis.labelWidth = opts.labelWidth || labelWidth;
1372 axis.labelHeight = opts.labelHeight || labelHeight;
1373 }
1374
1375 function allocateAxisBoxFirstPhase(axis) {
1376 // find the bounding box of the axis by looking at label
1377 // widths/heights and ticks, make room by diminishing the
1378 // plotOffset; this first phase only looks at one
1379 // dimension per axis, the other dimension depends on the
1380 // other axes so will have to wait
1381
1382 var lw = axis.labelWidth,
1383 lh = axis.labelHeight,
1384 pos = axis.options.position,
1385 tickLength = axis.options.tickLength,
1386 axisMargin = options.grid.axisMargin,
1387 padding = options.grid.labelMargin,
1388 all = axis.direction == "x" ? xaxes : yaxes,
1389 index, innermost;
1390
1391 // determine axis margin
1392 var samePosition = $.grep(all, function (a) {
1393 return a && a.options.position == pos && a.reserveSpace;
1394 });
1395 if ($.inArray(axis, samePosition) == samePosition.length - 1)
1396 axisMargin = 0; // outermost
1397
1398 // determine tick length - if we're innermost, we can use "full"
1399 if (tickLength == null) {
1400 var sameDirection = $.grep(all, function (a) {
1401 return a && a.reserveSpace;
1402 });
1403
1404 innermost = $.inArray(axis, sameDirection) == 0;
1405 if (innermost)
1406 tickLength = "full";
1407 else
1408 tickLength = 5;
1409 }
1410
1411 if (!isNaN(+tickLength))
1412 padding += +tickLength;
1413
1414 // compute box
1415 if (axis.direction == "x") {
1416 lh += padding;
1417
1418 if (pos == "bottom") {
1419 plotOffset.bottom += lh + axisMargin;
1420 axis.box = { top: surface.height - plotOffset.bottom, height: lh };
1421 }
1422 else {
1423 axis.box = { top: plotOffset.top + axisMargin, height: lh };
1424 plotOffset.top += lh + axisMargin;
1425 }
1426 }
1427 else {
1428 lw += padding;
1429
1430 if (pos == "left") {
1431 axis.box = { left: plotOffset.left + axisMargin, width: lw };
1432 plotOffset.left += lw + axisMargin;
1433 }
1434 else {
1435 plotOffset.right += lw + axisMargin;
1436 axis.box = { left: surface.width - plotOffset.right, width: lw };
1437 }
1438 }
1439
1440 // save for future reference
1441 axis.position = pos;
1442 axis.tickLength = tickLength;
1443 axis.box.padding = padding;
1444 axis.innermost = innermost;
1445 }
1446
1447 function allocateAxisBoxSecondPhase(axis) {
1448 // now that all axis boxes have been placed in one
1449 // dimension, we can set the remaining dimension coordinates
1450 if (axis.direction == "x") {
1451 axis.box.left = plotOffset.left - axis.labelWidth / 2;
1452 axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth;
1453 }
1454 else {
1455 axis.box.top = plotOffset.top - axis.labelHeight / 2;
1456 axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight;
1457 }
1458 }
1459
1460 function adjustLayoutForThingsStickingOut() {
1461 // possibly adjust plot offset to ensure everything stays
1462 // inside the canvas and isn't clipped off
1463
1464 var minMargin = options.grid.minBorderMargin,
1465 margins = { x: 0, y: 0 }, i, axis;
1466
1467 // check stuff from the plot (FIXME: this should just read
1468 // a value from the series, otherwise it's impossible to
1469 // customize)
1470 if (minMargin == null) {
1471 minMargin = 0;
1472 for (i = 0; i < series.length; ++i)
1473 minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2));
1474 }
1475
1476 margins.x = margins.y = Math.ceil(minMargin);
1477
1478 // check axis labels, note we don't check the actual
1479 // labels but instead use the overall width/height to not
1480 // jump as much around with replots
1481 $.each(allAxes(), function (_, axis) {
1482 var dir = axis.direction;
1483 if (axis.reserveSpace)
1484 margins[dir] = Math.ceil(Math.max(margins[dir], (dir == "x" ? axis.labelWidth : axis.labelHeight) / 2));
1485 });
1486
1487 plotOffset.left = Math.max(margins.x, plotOffset.left);
1488 plotOffset.right = Math.max(margins.x, plotOffset.right);
1489 plotOffset.top = Math.max(margins.y, plotOffset.top);
1490 plotOffset.bottom = Math.max(margins.y, plotOffset.bottom);
1491 }
1492
1493 function setupGrid() {
1494 var i, axes = allAxes(), showGrid = options.grid.show;
1495
1496 // Initialize the plot's offset from the edge of the canvas
1497
1498 for (var a in plotOffset) {
1499 var margin = options.grid.margin || 0;
1500 plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0;
1501 }
1502
1503 executeHooks(hooks.processOffset, [plotOffset]);
1504
1505 // If the grid is visible, add its border width to the offset
1506
1507 for (var a in plotOffset) {
1508 if(typeof(options.grid.borderWidth) == "object") {
1509 plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0;
1510 }
1511 else {
1512 plotOffset[a] += showGrid ? options.grid.borderWidth : 0;
1513 }
1514 }
1515
1516 // init axes
1517 $.each(axes, function (_, axis) {
1518 axis.show = axis.options.show;
1519 if (axis.show == null)
1520 axis.show = axis.used; // by default an axis is visible if it's got data
1521
1522 axis.reserveSpace = axis.show || axis.options.reserveSpace;
1523
1524 setRange(axis);
1525 });
1526
1527 if (showGrid) {
1528
1529 var allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; });
1530
1531 $.each(allocatedAxes, function (_, axis) {
1532 // make the ticks
1533 setupTickGeneration(axis);
1534 setTicks(axis);
1535 snapRangeToTicks(axis, axis.ticks);
1536 // find labelWidth/Height for axis
1537 measureTickLabels(axis);
1538 });
1539
1540 // with all dimensions calculated, we can compute the
1541 // axis bounding boxes, start from the outside
1542 // (reverse order)
1543 for (i = allocatedAxes.length - 1; i >= 0; --i)
1544 allocateAxisBoxFirstPhase(allocatedAxes[i]);
1545
1546 // make sure we've got enough space for things that
1547 // might stick out
1548 adjustLayoutForThingsStickingOut();
1549
1550 $.each(allocatedAxes, function (_, axis) {
1551 allocateAxisBoxSecondPhase(axis);
1552 });
1553 }
1554
1555 plotWidth = surface.width - plotOffset.left - plotOffset.right;
1556 plotHeight = surface.height - plotOffset.bottom - plotOffset.top;
1557
1558 // now we got the proper plot dimensions, we can compute the scaling
1559 $.each(axes, function (_, axis) {
1560 setTransformationHelpers(axis);
1561 });
1562
1563 if (showGrid) {
1564 drawAxisLabels();
1565 }
1566
1567 insertLegend();
1568 }
1569
1570 function setRange(axis) {
1571 var opts = axis.options,
1572 min = +(opts.min != null ? opts.min : axis.datamin),
1573 max = +(opts.max != null ? opts.max : axis.datamax),
1574 delta = max - min;
1575
1576 if (delta == 0.0) {
1577 // degenerate case
1578 var widen = max == 0 ? 1 : 0.01;
1579
1580 if (opts.min == null)
1581 min -= widen;
1582 // always widen max if we couldn't widen min to ensure we
1583 // don't fall into min == max which doesn't work
1584 if (opts.max == null || opts.min != null)
1585 max += widen;
1586 }
1587 else {
1588 // consider autoscaling
1589 var margin = opts.autoscaleMargin;
1590 if (margin != null) {
1591 if (opts.min == null) {
1592 min -= delta * margin;
1593 // make sure we don't go below zero if all values
1594 // are positive
1595 if (min < 0 && axis.datamin != null && axis.datamin >= 0)
1596 min = 0;
1597 }
1598 if (opts.max == null) {
1599 max += delta * margin;
1600 if (max > 0 && axis.datamax != null && axis.datamax <= 0)
1601 max = 0;
1602 }
1603 }
1604 }
1605 axis.min = min;
1606 axis.max = max;
1607 }
1608
1609 function setupTickGeneration(axis) {
1610 var opts = axis.options;
1611
1612 // estimate number of ticks
1613 var noTicks;
1614 if (typeof opts.ticks == "number" && opts.ticks > 0)
1615 noTicks = opts.ticks;
1616 else
1617 // heuristic based on the model a*sqrt(x) fitted to
1618 // some data points that seemed reasonable
1619 noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height);
1620
1621 var delta = (axis.max - axis.min) / noTicks,
1622 dec = -Math.floor(Math.log(delta) / Math.LN10),
1623 maxDec = opts.tickDecimals;
1624
1625 if (maxDec != null && dec > maxDec) {
1626 dec = maxDec;
1627 }
1628
1629 var magn = Math.pow(10, -dec),
1630 norm = delta / magn, // norm is between 1.0 and 10.0
1631 size;
1632
1633 if (norm < 1.5) {
1634 size = 1;
1635 } else if (norm < 3) {
1636 size = 2;
1637 // special case for 2.5, requires an extra decimal
1638 if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
1639 size = 2.5;
1640 ++dec;
1641 }
1642 } else if (norm < 7.5) {
1643 size = 5;
1644 } else {
1645 size = 10;
1646 }
1647
1648 size *= magn;
1649
1650 if (opts.minTickSize != null && size < opts.minTickSize) {
1651 size = opts.minTickSize;
1652 }
1653
1654 axis.delta = delta;
1655 axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
1656 axis.tickSize = opts.tickSize || size;
1657
1658 // Time mode was moved to a plug-in in 0.8, but since so many people use this
1659 // we'll add an especially friendly make sure they remembered to include it.
1660
1661 if (opts.mode == "time" && !axis.tickGenerator) {
1662 throw new Error("Time mode requires the flot.time plugin.");
1663 }
1664
1665 // Flot supports base-10 axes; any other mode else is handled by a plug-in,
1666 // like flot.time.js.
1667
1668 if (!axis.tickGenerator) {
1669
1670 axis.tickGenerator = function (axis) {
1671
1672 var ticks = [],
1673 start = floorInBase(axis.min, axis.tickSize),
1674 i = 0,
1675 v = Number.NaN,
1676 prev;
1677
1678 do {
1679 prev = v;
1680 v = start + i * axis.tickSize;
1681 ticks.push(v);
1682 ++i;
1683 } while (v < axis.max && v != prev);
1684 return ticks;
1685 };
1686
1687 axis.tickFormatter = function (value, axis) {
1688
1689 var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1;
1690 var formatted = "" + Math.round(value * factor) / factor;
1691
1692 // If tickDecimals was specified, ensure that we have exactly that
1693 // much precision; otherwise default to the value's own precision.
1694
1695 if (axis.tickDecimals != null) {
1696 var decimal = formatted.indexOf(".");
1697 var precision = decimal == -1 ? 0 : formatted.length - decimal - 1;
1698 if (precision < axis.tickDecimals) {
1699 return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision);
1700 }
1701 }
1702
1703 return formatted;
1704 };
1705 }
1706
1707 if ($.isFunction(opts.tickFormatter))
1708 axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); };
1709
1710 if (opts.alignTicksWithAxis != null) {
1711 var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1];
1712 if (otherAxis && otherAxis.used && otherAxis != axis) {
1713 // consider snapping min/max to outermost nice ticks
1714 var niceTicks = axis.tickGenerator(axis);
1715 if (niceTicks.length > 0) {
1716 if (opts.min == null)
1717 axis.min = Math.min(axis.min, niceTicks[0]);
1718 if (opts.max == null && niceTicks.length > 1)
1719 axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]);
1720 }
1721
1722 axis.tickGenerator = function (axis) {
1723 // copy ticks, scaled to this axis
1724 var ticks = [], v, i;
1725 for (i = 0; i < otherAxis.ticks.length; ++i) {
1726 v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min);
1727 v = axis.min + v * (axis.max - axis.min);
1728 ticks.push(v);
1729 }
1730 return ticks;
1731 };
1732
1733 // we might need an extra decimal since forced
1734 // ticks don't necessarily fit naturally
1735 if (!axis.mode && opts.tickDecimals == null) {
1736 var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1),
1737 ts = axis.tickGenerator(axis);
1738
1739 // only proceed if the tick interval rounded
1740 // with an extra decimal doesn't give us a
1741 // zero at end
1742 if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec))))
1743 axis.tickDecimals = extraDec;
1744 }
1745 }
1746 }
1747 }
1748
1749 function setTicks(axis) {
1750 var oticks = axis.options.ticks, ticks = [];
1751 if (oticks == null || (typeof oticks == "number" && oticks > 0))
1752 ticks = axis.tickGenerator(axis);
1753 else if (oticks) {
1754 if ($.isFunction(oticks))
1755 // generate the ticks
1756 ticks = oticks(axis);
1757 else
1758 ticks = oticks;
1759 }
1760
1761 // clean up/labelify the supplied ticks, copy them over
1762 var i, v;
1763 axis.ticks = [];
1764 for (i = 0; i < ticks.length; ++i) {
1765 var label = null;
1766 var t = ticks[i];
1767 if (typeof t == "object") {
1768 v = +t[0];
1769 if (t.length > 1)
1770 label = t[1];
1771 }
1772 else
1773 v = +t;
1774 if (label == null)
1775 label = axis.tickFormatter(v, axis);
1776 if (!isNaN(v))
1777 axis.ticks.push({ v: v, label: label });
1778 }
1779 }
1780
1781 function snapRangeToTicks(axis, ticks) {
1782 if (axis.options.autoscaleMargin && ticks.length > 0) {
1783 // snap to ticks
1784 if (axis.options.min == null)
1785 axis.min = Math.min(axis.min, ticks[0].v);
1786 if (axis.options.max == null && ticks.length > 1)
1787 axis.max = Math.max(axis.max, ticks[ticks.length - 1].v);
1788 }
1789 }
1790
1791 function draw() {
1792
1793 surface.clear();
1794
1795 executeHooks(hooks.drawBackground, [ctx]);
1796
1797 var grid = options.grid;
1798
1799 // draw background, if any
1800 if (grid.show && grid.backgroundColor)
1801 drawBackground();
1802
1803 if (grid.show && !grid.aboveData) {
1804 drawGrid();
1805 }
1806
1807 for (var i = 0; i < series.length; ++i) {
1808 executeHooks(hooks.drawSeries, [ctx, series[i]]);
1809 drawSeries(series[i]);
1810 }
1811
1812 executeHooks(hooks.draw, [ctx]);
1813
1814 if (grid.show && grid.aboveData) {
1815 drawGrid();
1816 }
1817
1818 surface.render();
1819
1820 // A draw implies that either the axes or data have changed, so we
1821 // should probably update the overlay highlights as well.
1822
1823 triggerRedrawOverlay();
1824 }
1825
1826 function extractRange(ranges, coord) {
1827 var axis, from, to, key, axes = allAxes();
1828
1829 for (var i = 0; i < axes.length; ++i) {
1830 axis = axes[i];
1831 if (axis.direction == coord) {
1832 key = coord + axis.n + "axis";
1833 if (!ranges[key] && axis.n == 1)
1834 key = coord + "axis"; // support x1axis as xaxis
1835 if (ranges[key]) {
1836 from = ranges[key].from;
1837 to = ranges[key].to;
1838 break;
1839 }
1840 }
1841 }
1842
1843 // backwards-compat stuff - to be removed in future
1844 if (!ranges[key]) {
1845 axis = coord == "x" ? xaxes[0] : yaxes[0];
1846 from = ranges[coord + "1"];
1847 to = ranges[coord + "2"];
1848 }
1849
1850 // auto-reverse as an added bonus
1851 if (from != null && to != null && from > to) {
1852 var tmp = from;
1853 from = to;
1854 to = tmp;
1855 }
1856
1857 return { from: from, to: to, axis: axis };
1858 }
1859
1860 function drawBackground() {
1861 ctx.save();
1862 ctx.translate(plotOffset.left, plotOffset.top);
1863
1864 ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)");
1865 ctx.fillRect(0, 0, plotWidth, plotHeight);
1866 ctx.restore();
1867 }
1868
1869 function drawGrid() {
1870 var i, axes, bw, bc;
1871
1872 ctx.save();
1873 ctx.translate(plotOffset.left, plotOffset.top);
1874
1875 // draw markings
1876 var markings = options.grid.markings;
1877 if (markings) {
1878 if ($.isFunction(markings)) {
1879 axes = plot.getAxes();
1880 // xmin etc. is backwards compatibility, to be
1881 // removed in the future
1882 axes.xmin = axes.xaxis.min;
1883 axes.xmax = axes.xaxis.max;
1884 axes.ymin = axes.yaxis.min;
1885 axes.ymax = axes.yaxis.max;
1886
1887 markings = markings(axes);
1888 }
1889
1890 for (i = 0; i < markings.length; ++i) {
1891 var m = markings[i],
1892 xrange = extractRange(m, "x"),
1893 yrange = extractRange(m, "y");
1894
1895 // fill in missing
1896 if (xrange.from == null)
1897 xrange.from = xrange.axis.min;
1898 if (xrange.to == null)
1899 xrange.to = xrange.axis.max;
1900 if (yrange.from == null)
1901 yrange.from = yrange.axis.min;
1902 if (yrange.to == null)
1903 yrange.to = yrange.axis.max;
1904
1905 // clip
1906 if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max ||
1907 yrange.to < yrange.axis.min || yrange.from > yrange.axis.max)
1908 continue;
1909
1910 xrange.from = Math.max(xrange.from, xrange.axis.min);
1911 xrange.to = Math.min(xrange.to, xrange.axis.max);
1912 yrange.from = Math.max(yrange.from, yrange.axis.min);
1913 yrange.to = Math.min(yrange.to, yrange.axis.max);
1914
1915 if (xrange.from == xrange.to && yrange.from == yrange.to)
1916 continue;
1917
1918 // then draw
1919 xrange.from = xrange.axis.p2c(xrange.from);
1920 xrange.to = xrange.axis.p2c(xrange.to);
1921 yrange.from = yrange.axis.p2c(yrange.from);
1922 yrange.to = yrange.axis.p2c(yrange.to);
1923
1924 if (xrange.from == xrange.to || yrange.from == yrange.to) {
1925 // draw line
1926 ctx.beginPath();
1927 ctx.strokeStyle = m.color || options.grid.markingsColor;
1928 ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth;
1929 ctx.moveTo(xrange.from, yrange.from);
1930 ctx.lineTo(xrange.to, yrange.to);
1931 ctx.stroke();
1932 }
1933 else {
1934 // fill area
1935 ctx.fillStyle = m.color || options.grid.markingsColor;
1936 ctx.fillRect(xrange.from, yrange.to,
1937 xrange.to - xrange.from,
1938 yrange.from - yrange.to);
1939 }
1940 }
1941 }
1942
1943 // draw the ticks
1944 axes = allAxes();
1945 bw = options.grid.borderWidth;
1946
1947 for (var j = 0; j < axes.length; ++j) {
1948 var axis = axes[j], box = axis.box,
1949 t = axis.tickLength, x, y, xoff, yoff;
1950 if (!axis.show || axis.ticks.length == 0)
1951 continue;
1952
1953 ctx.lineWidth = 1;
1954
1955 // find the edges
1956 if (axis.direction == "x") {
1957 x = 0;
1958 if (t == "full")
1959 y = (axis.position == "top" ? 0 : plotHeight);
1960 else
1961 y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0);
1962 }
1963 else {
1964 y = 0;
1965 if (t == "full")
1966 x = (axis.position == "left" ? 0 : plotWidth);
1967 else
1968 x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0);
1969 }
1970
1971 // draw tick bar
1972 if (!axis.innermost) {
1973 ctx.strokeStyle = axis.options.color;
1974 ctx.beginPath();
1975 xoff = yoff = 0;
1976 if (axis.direction == "x")
1977 xoff = plotWidth + 1;
1978 else
1979 yoff = plotHeight + 1;
1980
1981 if (ctx.lineWidth == 1) {
1982 if (axis.direction == "x") {
1983 y = Math.floor(y) + 0.5;
1984 } else {
1985 x = Math.floor(x) + 0.5;
1986 }
1987 }
1988
1989 ctx.moveTo(x, y);
1990 ctx.lineTo(x + xoff, y + yoff);
1991 ctx.stroke();
1992 }
1993
1994 // draw ticks
1995
1996 ctx.strokeStyle = axis.options.tickColor;
1997
1998 ctx.beginPath();
1999 for (i = 0; i < axis.ticks.length; ++i) {
2000 var v = axis.ticks[i].v;
2001
2002 xoff = yoff = 0;
2003
2004 if (isNaN(v) || v < axis.min || v > axis.max
2005 // skip those lying on the axes if we got a border
2006 || (t == "full"
2007 && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0)
2008 && (v == axis.min || v == axis.max)))
2009 continue;
2010
2011 if (axis.direction == "x") {
2012 x = axis.p2c(v);
2013 yoff = t == "full" ? -plotHeight : t;
2014
2015 if (axis.position == "top")
2016 yoff = -yoff;
2017 }
2018 else {
2019 y = axis.p2c(v);
2020 xoff = t == "full" ? -plotWidth : t;
2021
2022 if (axis.position == "left")
2023 xoff = -xoff;
2024 }
2025
2026 if (ctx.lineWidth == 1) {
2027 if (axis.direction == "x")
2028 x = Math.floor(x) + 0.5;
2029 else
2030 y = Math.floor(y) + 0.5;
2031 }
2032
2033 ctx.moveTo(x, y);
2034 ctx.lineTo(x + xoff, y + yoff);
2035 }
2036
2037 ctx.stroke();
2038 }
2039
2040
2041 // draw border
2042 if (bw) {
2043 // If either borderWidth or borderColor is an object, then draw the border
2044 // line by line instead of as one rectangle
2045 bc = options.grid.borderColor;
2046 if(typeof bw == "object" || typeof bc == "object") {
2047 if (typeof bw !== "object") {
2048 bw = {top: bw, right: bw, bottom: bw, left: bw};
2049 }
2050 if (typeof bc !== "object") {
2051 bc = {top: bc, right: bc, bottom: bc, left: bc};
2052 }
2053
2054 if (bw.top > 0) {
2055 ctx.strokeStyle = bc.top;
2056 ctx.lineWidth = bw.top;
2057 ctx.beginPath();
2058 ctx.moveTo(0 - bw.left, 0 - bw.top/2);
2059 ctx.lineTo(plotWidth, 0 - bw.top/2);
2060 ctx.stroke();
2061 }
2062
2063 if (bw.right > 0) {
2064 ctx.strokeStyle = bc.right;
2065 ctx.lineWidth = bw.right;
2066 ctx.beginPath();
2067 ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top);
2068 ctx.lineTo(plotWidth + bw.right / 2, plotHeight);
2069 ctx.stroke();
2070 }
2071
2072 if (bw.bottom > 0) {
2073 ctx.strokeStyle = bc.bottom;
2074 ctx.lineWidth = bw.bottom;
2075 ctx.beginPath();
2076 ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2);
2077 ctx.lineTo(0, plotHeight + bw.bottom / 2);
2078 ctx.stroke();
2079 }
2080
2081 if (bw.left > 0) {
2082 ctx.strokeStyle = bc.left;
2083 ctx.lineWidth = bw.left;
2084 ctx.beginPath();
2085 ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom);
2086 ctx.lineTo(0- bw.left/2, 0);
2087 ctx.stroke();
2088 }
2089 }
2090 else {
2091 ctx.lineWidth = bw;
2092 ctx.strokeStyle = options.grid.borderColor;
2093 ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw);
2094 }
2095 }
2096
2097 ctx.restore();
2098 }
2099
2100 function drawAxisLabels() {
2101
2102 $.each(allAxes(), function (_, axis) {
2103 if (!axis.show || axis.ticks.length == 0)
2104 return;
2105
2106 var box = axis.box,
2107 legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis",
2108 layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles,
2109 font = axis.options.font || "flot-tick-label tickLabel",
2110 tick, x, y, halign, valign;
2111
2112 surface.removeText(layer);
2113
2114 for (var i = 0; i < axis.ticks.length; ++i) {
2115
2116 tick = axis.ticks[i];
2117 if (!tick.label || tick.v < axis.min || tick.v > axis.max)
2118 continue;
2119
2120 if (axis.direction == "x") {
2121 halign = "center";
2122 x = plotOffset.left + axis.p2c(tick.v);
2123 if (axis.position == "bottom") {
2124 y = box.top + box.padding;
2125 } else {
2126 y = box.top + box.height - box.padding;
2127 valign = "bottom";
2128 }
2129 } else {
2130 valign = "middle";
2131 y = plotOffset.top + axis.p2c(tick.v);
2132 if (axis.position == "left") {
2133 x = box.left + box.width - box.padding;
2134 halign = "right";
2135 } else {
2136 x = box.left + box.padding;
2137 }
2138 }
2139
2140 surface.addText(layer, x, y, tick.label, font, null, null, halign, valign);
2141 }
2142 });
2143 }
2144
2145 function drawSeries(series) {
2146 if (series.lines.show)
2147 drawSeriesLines(series);
2148 if (series.bars.show)
2149 drawSeriesBars(series);
2150 if (series.points.show)
2151 drawSeriesPoints(series);
2152 }
2153
2154 function drawSeriesLines(series) {
2155 function plotLine(datapoints, xoffset, yoffset, axisx, axisy) {
2156 var points = datapoints.points,
2157 ps = datapoints.pointsize,
2158 prevx = null, prevy = null;
2159
2160 ctx.beginPath();
2161 for (var i = ps; i < points.length; i += ps) {
2162 var x1 = points[i - ps], y1 = points[i - ps + 1],
2163 x2 = points[i], y2 = points[i + 1];
2164
2165 if (x1 == null || x2 == null)
2166 continue;
2167
2168 // clip with ymin
2169 if (y1 <= y2 && y1 < axisy.min) {
2170 if (y2 < axisy.min)
2171 continue; // line segment is outside
2172 // compute new intersection point
2173 x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
2174 y1 = axisy.min;
2175 }
2176 else if (y2 <= y1 && y2 < axisy.min) {
2177 if (y1 < axisy.min)
2178 continue;
2179 x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
2180 y2 = axisy.min;
2181 }
2182
2183 // clip with ymax
2184 if (y1 >= y2 && y1 > axisy.max) {
2185 if (y2 > axisy.max)
2186 continue;
2187 x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
2188 y1 = axisy.max;
2189 }
2190 else if (y2 >= y1 && y2 > axisy.max) {
2191 if (y1 > axisy.max)
2192 continue;
2193 x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
2194 y2 = axisy.max;
2195 }
2196
2197 // clip with xmin
2198 if (x1 <= x2 && x1 < axisx.min) {
2199 if (x2 < axisx.min)
2200 continue;
2201 y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
2202 x1 = axisx.min;
2203 }
2204 else if (x2 <= x1 && x2 < axisx.min) {
2205 if (x1 < axisx.min)
2206 continue;
2207 y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
2208 x2 = axisx.min;
2209 }
2210
2211 // clip with xmax
2212 if (x1 >= x2 && x1 > axisx.max) {
2213 if (x2 > axisx.max)
2214 continue;
2215 y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
2216 x1 = axisx.max;
2217 }
2218 else if (x2 >= x1 && x2 > axisx.max) {
2219 if (x1 > axisx.max)
2220 continue;
2221 y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
2222 x2 = axisx.max;
2223 }
2224
2225 if (x1 != prevx || y1 != prevy)
2226 ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
2227
2228 prevx = x2;
2229 prevy = y2;
2230 ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
2231 }
2232 ctx.stroke();
2233 }
2234
2235 function plotLineArea(datapoints, axisx, axisy) {
2236 var points = datapoints.points,
2237 ps = datapoints.pointsize,
2238 bottom = Math.min(Math.max(0, axisy.min), axisy.max),
2239 i = 0, top, areaOpen = false,
2240 ypos = 1, segmentStart = 0, segmentEnd = 0;
2241
2242 // we process each segment in two turns, first forward
2243 // direction to sketch out top, then once we hit the
2244 // end we go backwards to sketch the bottom
2245 while (true) {
2246 if (ps > 0 && i > points.length + ps)
2247 break;
2248
2249 i += ps; // ps is negative if going backwards
2250
2251 var x1 = points[i - ps],
2252 y1 = points[i - ps + ypos],
2253 x2 = points[i], y2 = points[i + ypos];
2254
2255 if (areaOpen) {
2256 if (ps > 0 && x1 != null && x2 == null) {
2257 // at turning point
2258 segmentEnd = i;
2259 ps = -ps;
2260 ypos = 2;
2261 continue;
2262 }
2263
2264 if (ps < 0 && i == segmentStart + ps) {
2265 // done with the reverse sweep
2266 ctx.fill();
2267 areaOpen = false;
2268 ps = -ps;
2269 ypos = 1;
2270 i = segmentStart = segmentEnd + ps;
2271 continue;
2272 }
2273 }
2274
2275 if (x1 == null || x2 == null)
2276 continue;
2277
2278 // clip x values
2279
2280 // clip with xmin
2281 if (x1 <= x2 && x1 < axisx.min) {
2282 if (x2 < axisx.min)
2283 continue;
2284 y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
2285 x1 = axisx.min;
2286 }
2287 else if (x2 <= x1 && x2 < axisx.min) {
2288 if (x1 < axisx.min)
2289 continue;
2290 y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
2291 x2 = axisx.min;
2292 }
2293
2294 // clip with xmax
2295 if (x1 >= x2 && x1 > axisx.max) {
2296 if (x2 > axisx.max)
2297 continue;
2298 y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
2299 x1 = axisx.max;
2300 }
2301 else if (x2 >= x1 && x2 > axisx.max) {
2302 if (x1 > axisx.max)
2303 continue;
2304 y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
2305 x2 = axisx.max;
2306 }
2307
2308 if (!areaOpen) {
2309 // open area
2310 ctx.beginPath();
2311 ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
2312 areaOpen = true;
2313 }
2314
2315 // now first check the case where both is outside
2316 if (y1 >= axisy.max && y2 >= axisy.max) {
2317 ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));
2318 ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));
2319 continue;
2320 }
2321 else if (y1 <= axisy.min && y2 <= axisy.min) {
2322 ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
2323 ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
2324 continue;
2325 }
2326
2327 // else it's a bit more complicated, there might
2328 // be a flat maxed out rectangle first, then a
2329 // triangular cutout or reverse; to find these
2330 // keep track of the current x values
2331 var x1old = x1, x2old = x2;
2332
2333 // clip the y values, without shortcutting, we
2334 // go through all cases in turn
2335
2336 // clip with ymin
2337 if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {
2338 x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
2339 y1 = axisy.min;
2340 }
2341 else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {
2342 x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
2343 y2 = axisy.min;
2344 }
2345
2346 // clip with ymax
2347 if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {
2348 x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
2349 y1 = axisy.max;
2350 }
2351 else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {
2352 x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
2353 y2 = axisy.max;
2354 }
2355
2356 // if the x value was changed we got a rectangle
2357 // to fill
2358 if (x1 != x1old) {
2359 ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1));
2360 // it goes to (x1, y1), but we fill that below
2361 }
2362
2363 // fill triangular section, this sometimes result
2364 // in redundant points if (x1, y1) hasn't changed
2365 // from previous line to, but we just ignore that
2366 ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));
2367 ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
2368
2369 // fill the other rectangle if it's there
2370 if (x2 != x2old) {
2371 ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
2372 ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2));
2373 }
2374 }
2375 }
2376
2377 ctx.save();
2378 ctx.translate(plotOffset.left, plotOffset.top);
2379 ctx.lineJoin = "round";
2380
2381 var lw = series.lines.lineWidth,
2382 sw = series.shadowSize;
2383 // FIXME: consider another form of shadow when filling is turned on
2384 if (lw > 0 && sw > 0) {
2385 // draw shadow as a thick and thin line with transparency
2386 ctx.lineWidth = sw;
2387 ctx.strokeStyle = "rgba(0,0,0,0.1)";
2388 // position shadow at angle from the mid of line
2389 var angle = Math.PI/18;
2390 plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis);
2391 ctx.lineWidth = sw/2;
2392 plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis);
2393 }
2394
2395 ctx.lineWidth = lw;
2396 ctx.strokeStyle = series.color;
2397 var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight);
2398 if (fillStyle) {
2399 ctx.fillStyle = fillStyle;
2400 plotLineArea(series.datapoints, series.xaxis, series.yaxis);
2401 }
2402
2403 if (lw > 0)
2404 plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis);
2405 ctx.restore();
2406 }
2407
2408 function drawSeriesPoints(series) {
2409 function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) {
2410 var points = datapoints.points, ps = datapoints.pointsize;
2411
2412 for (var i = 0; i < points.length; i += ps) {
2413 var x = points[i], y = points[i + 1];
2414 if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
2415 continue;
2416
2417 ctx.beginPath();
2418 x = axisx.p2c(x);
2419 y = axisy.p2c(y) + offset;
2420 if (symbol == "circle")
2421 ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);
2422 else
2423 symbol(ctx, x, y, radius, shadow);
2424 ctx.closePath();
2425
2426 if (fillStyle) {
2427 ctx.fillStyle = fillStyle;
2428 ctx.fill();
2429 }
2430 ctx.stroke();
2431 }
2432 }
2433
2434 ctx.save();
2435 ctx.translate(plotOffset.left, plotOffset.top);
2436
2437 var lw = series.points.lineWidth,
2438 sw = series.shadowSize,
2439 radius = series.points.radius,
2440 symbol = series.points.symbol;
2441
2442 // If the user sets the line width to 0, we change it to a very
2443 // small value. A line width of 0 seems to force the default of 1.
2444 // Doing the conditional here allows the shadow setting to still be
2445 // optional even with a lineWidth of 0.
2446
2447 if( lw == 0 )
2448 lw = 0.0001;
2449
2450 if (lw > 0 && sw > 0) {
2451 // draw shadow in two steps
2452 var w = sw / 2;
2453 ctx.lineWidth = w;
2454 ctx.strokeStyle = "rgba(0,0,0,0.1)";
2455 plotPoints(series.datapoints, radius, null, w + w/2, true,
2456 series.xaxis, series.yaxis, symbol);
2457
2458 ctx.strokeStyle = "rgba(0,0,0,0.2)";
2459 plotPoints(series.datapoints, radius, null, w/2, true,
2460 series.xaxis, series.yaxis, symbol);
2461 }
2462
2463 ctx.lineWidth = lw;
2464 ctx.strokeStyle = series.color;
2465 plotPoints(series.datapoints, radius,
2466 getFillStyle(series.points, series.color), 0, false,
2467 series.xaxis, series.yaxis, symbol);
2468 ctx.restore();
2469 }
2470
2471 function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) {
2472 var left, right, bottom, top,
2473 drawLeft, drawRight, drawTop, drawBottom,
2474 tmp;
2475
2476 // in horizontal mode, we start the bar from the left
2477 // instead of from the bottom so it appears to be
2478 // horizontal rather than vertical
2479 if (horizontal) {
2480 drawBottom = drawRight = drawTop = true;
2481 drawLeft = false;
2482 left = b;
2483 right = x;
2484 top = y + barLeft;
2485 bottom = y + barRight;
2486
2487 // account for negative bars
2488 if (right < left) {
2489 tmp = right;
2490 right = left;
2491 left = tmp;
2492 drawLeft = true;
2493 drawRight = false;
2494 }
2495 }
2496 else {
2497 drawLeft = drawRight = drawTop = true;
2498 drawBottom = false;
2499 left = x + barLeft;
2500 right = x + barRight;
2501 bottom = b;
2502 top = y;
2503
2504 // account for negative bars
2505 if (top < bottom) {
2506 tmp = top;
2507 top = bottom;
2508 bottom = tmp;
2509 drawBottom = true;
2510 drawTop = false;
2511 }
2512 }
2513
2514 // clip
2515 if (right < axisx.min || left > axisx.max ||
2516 top < axisy.min || bottom > axisy.max)
2517 return;
2518
2519 if (left < axisx.min) {
2520 left = axisx.min;
2521 drawLeft = false;
2522 }
2523
2524 if (right > axisx.max) {
2525 right = axisx.max;
2526 drawRight = false;
2527 }
2528
2529 if (bottom < axisy.min) {
2530 bottom = axisy.min;
2531 drawBottom = false;
2532 }
2533
2534 if (top > axisy.max) {
2535 top = axisy.max;
2536 drawTop = false;
2537 }
2538
2539 left = axisx.p2c(left);
2540 bottom = axisy.p2c(bottom);
2541 right = axisx.p2c(right);
2542 top = axisy.p2c(top);
2543
2544 // fill the bar
2545 if (fillStyleCallback) {
2546 c.beginPath();
2547 c.moveTo(left, bottom);
2548 c.lineTo(left, top);
2549 c.lineTo(right, top);
2550 c.lineTo(right, bottom);
2551 c.fillStyle = fillStyleCallback(bottom, top);
2552 c.fill();
2553 }
2554
2555 // draw outline
2556 if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) {
2557 c.beginPath();
2558
2559 // FIXME: inline moveTo is buggy with excanvas
2560 c.moveTo(left, bottom + offset);
2561 if (drawLeft)
2562 c.lineTo(left, top + offset);
2563 else
2564 c.moveTo(left, top + offset);
2565 if (drawTop)
2566 c.lineTo(right, top + offset);
2567 else
2568 c.moveTo(right, top + offset);
2569 if (drawRight)
2570 c.lineTo(right, bottom + offset);
2571 else
2572 c.moveTo(right, bottom + offset);
2573 if (drawBottom)
2574 c.lineTo(left, bottom + offset);
2575 else
2576 c.moveTo(left, bottom + offset);
2577 c.stroke();
2578 }
2579 }
2580
2581 function drawSeriesBars(series) {
2582 function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) {
2583 var points = datapoints.points, ps = datapoints.pointsize;
2584
2585 for (var i = 0; i < points.length; i += ps) {
2586 if (points[i] == null)
2587 continue;
2588 drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth);
2589 }
2590 }
2591
2592 ctx.save();
2593 ctx.translate(plotOffset.left, plotOffset.top);
2594
2595 // FIXME: figure out a way to add shadows (for instance along the right edge)
2596 ctx.lineWidth = series.bars.lineWidth;
2597 ctx.strokeStyle = series.color;
2598
2599 var barLeft;
2600
2601 switch (series.bars.align) {
2602 case "left":
2603 barLeft = 0;
2604 break;
2605 case "right":
2606 barLeft = -series.bars.barWidth;
2607 break;
2608 case "center":
2609 barLeft = -series.bars.barWidth / 2;
2610 break;
2611 default:
2612 throw new Error("Invalid bar alignment: " + series.bars.align);
2613 }
2614
2615 var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null;
2616 plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis);
2617 ctx.restore();
2618 }
2619
2620 function getFillStyle(filloptions, seriesColor, bottom, top) {
2621 var fill = filloptions.fill;
2622 if (!fill)
2623 return null;
2624
2625 if (filloptions.fillColor)
2626 return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
2627
2628 var c = $.color.parse(seriesColor);
2629 c.a = typeof fill == "number" ? fill : 0.4;
2630 c.normalize();
2631 return c.toString();
2632 }
2633
2634 function insertLegend() {
2635
2636 placeholder.find(".legend").remove();
2637
2638 if (!options.legend.show)
2639 return;
2640
2641 var fragments = [], entries = [], rowStarted = false,
2642 lf = options.legend.labelFormatter, s, label;
2643
2644 // Build a list of legend entries, with each having a label and a color
2645
2646 for (var i = 0; i < series.length; ++i) {
2647 s = series[i];
2648 if (s.label) {
2649 label = lf ? lf(s.label, s) : s.label;
2650 if (label) {
2651 entries.push({
2652 label: label,
2653 color: s.color
2654 });
2655 }
2656 }
2657 }
2658
2659 // Sort the legend using either the default or a custom comparator
2660
2661 if (options.legend.sorted) {
2662 if ($.isFunction(options.legend.sorted)) {
2663 entries.sort(options.legend.sorted);
2664 } else if (options.legend.sorted == "reverse") {
2665 entries.reverse();
2666 } else {
2667 var ascending = options.legend.sorted != "descending";
2668 entries.sort(function(a, b) {
2669 return a.label == b.label ? 0 : (
2670 (a.label < b.label) != ascending ? 1 : -1 // Logical XOR
2671 );
2672 });
2673 }
2674 }
2675
2676 // Generate markup for the list of entries, in their final order
2677
2678 for (var i = 0; i < entries.length; ++i) {
2679
2680 var entry = entries[i];
2681
2682 if (i % options.legend.noColumns == 0) {
2683 if (rowStarted)
2684 fragments.push('</tr>');
2685 fragments.push('<tr>');
2686 rowStarted = true;
2687 }
2688
2689 fragments.push(
2690 '<td class="legendColorBox"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:4px;height:0;border:5px solid ' + entry.color + ';overflow:hidden"></div></div></td>' +
2691 '<td class="legendLabel">' + entry.label + '</td>'
2692 );
2693 }
2694
2695 if (rowStarted)
2696 fragments.push('</tr>');
2697
2698 if (fragments.length == 0)
2699 return;
2700
2701 var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>';
2702 if (options.legend.container != null)
2703 $(options.legend.container).html(table);
2704 else {
2705 var pos = "",
2706 p = options.legend.position,
2707 m = options.legend.margin;
2708 if (m[0] == null)
2709 m = [m, m];
2710 if (p.charAt(0) == "n")
2711 pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
2712 else if (p.charAt(0) == "s")
2713 pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
2714 if (p.charAt(1) == "e")
2715 pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
2716 else if (p.charAt(1) == "w")
2717 pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
2718 var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder);
2719 if (options.legend.backgroundOpacity != 0.0) {
2720 // put in the transparent background
2721 // separately to avoid blended labels and
2722 // label boxes
2723 var c = options.legend.backgroundColor;
2724 if (c == null) {
2725 c = options.grid.backgroundColor;
2726 if (c && typeof c == "string")
2727 c = $.color.parse(c);
2728 else
2729 c = $.color.extract(legend, 'background-color');
2730 c.a = 1;
2731 c = c.toString();
2732 }
2733 var div = legend.children();
2734 $('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity);
2735 }
2736 }
2737 }
2738
2739
2740 // interactive features
2741
2742 var highlights = [],
2743 redrawTimeout = null;
2744
2745 // returns the data item the mouse is over, or null if none is found
2746 function findNearbyItem(mouseX, mouseY, seriesFilter) {
2747 var maxDistance = options.grid.mouseActiveRadius,
2748 smallestDistance = maxDistance * maxDistance + 1,
2749 item = null, foundPoint = false, i, j, ps;
2750
2751 for (i = series.length - 1; i >= 0; --i) {
2752 if (!seriesFilter(series[i]))
2753 continue;
2754
2755 var s = series[i],
2756 axisx = s.xaxis,
2757 axisy = s.yaxis,
2758 points = s.datapoints.points,
2759 mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster
2760 my = axisy.c2p(mouseY),
2761 maxx = maxDistance / axisx.scale,
2762 maxy = maxDistance / axisy.scale;
2763
2764 ps = s.datapoints.pointsize;
2765 // with inverse transforms, we can't use the maxx/maxy
2766 // optimization, sadly
2767 if (axisx.options.inverseTransform)
2768 maxx = Number.MAX_VALUE;
2769 if (axisy.options.inverseTransform)
2770 maxy = Number.MAX_VALUE;
2771
2772 if (s.lines.show || s.points.show) {
2773 for (j = 0; j < points.length; j += ps) {
2774 var x = points[j], y = points[j + 1];
2775 if (x == null)
2776 continue;
2777
2778 // For points and lines, the cursor must be within a
2779 // certain distance to the data point
2780 if (x - mx > maxx || x - mx < -maxx ||
2781 y - my > maxy || y - my < -maxy)
2782 continue;
2783
2784 // We have to calculate distances in pixels, not in
2785 // data units, because the scales of the axes may be different
2786 var dx = Math.abs(axisx.p2c(x) - mouseX),
2787 dy = Math.abs(axisy.p2c(y) - mouseY),
2788 dist = dx * dx + dy * dy; // we save the sqrt
2789
2790 // use <= to ensure last point takes precedence
2791 // (last generally means on top of)
2792 if (dist < smallestDistance) {
2793 smallestDistance = dist;
2794 item = [i, j / ps];
2795 }
2796 }
2797 }
2798
2799 if (s.bars.show && !item) { // no other point can be nearby
2800 var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2,
2801 barRight = barLeft + s.bars.barWidth;
2802
2803 for (j = 0; j < points.length; j += ps) {
2804 var x = points[j], y = points[j + 1], b = points[j + 2];
2805 if (x == null)
2806 continue;
2807
2808 // for a bar graph, the cursor must be inside the bar
2809 if (series[i].bars.horizontal ?
2810 (mx <= Math.max(b, x) && mx >= Math.min(b, x) &&
2811 my >= y + barLeft && my <= y + barRight) :
2812 (mx >= x + barLeft && mx <= x + barRight &&
2813 my >= Math.min(b, y) && my <= Math.max(b, y)))
2814 item = [i, j / ps];
2815 }
2816 }
2817 }
2818
2819 if (item) {
2820 i = item[0];
2821 j = item[1];
2822 ps = series[i].datapoints.pointsize;
2823
2824 return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),
2825 dataIndex: j,
2826 series: series[i],
2827 seriesIndex: i };
2828 }
2829
2830 return null;
2831 }
2832
2833 function onMouseMove(e) {
2834 if (options.grid.hoverable)
2835 triggerClickHoverEvent("plothover", e,
2836 function (s) { return s["hoverable"] != false; });
2837 }
2838
2839 function onMouseLeave(e) {
2840 if (options.grid.hoverable)
2841 triggerClickHoverEvent("plothover", e,
2842 function (s) { return false; });
2843 }
2844
2845 function onClick(e) {
2846 triggerClickHoverEvent("plotclick", e,
2847 function (s) { return s["clickable"] != false; });
2848 }
2849
2850 // trigger click or hover event (they send the same parameters
2851 // so we share their code)
2852 function triggerClickHoverEvent(eventname, event, seriesFilter) {
2853 var offset = eventHolder.offset(),
2854 canvasX = event.pageX - offset.left - plotOffset.left,
2855 canvasY = event.pageY - offset.top - plotOffset.top,
2856 pos = canvasToAxisCoords({ left: canvasX, top: canvasY });
2857
2858 pos.pageX = event.pageX;
2859 pos.pageY = event.pageY;
2860
2861 var item = findNearbyItem(canvasX, canvasY, seriesFilter);
2862
2863 if (item) {
2864 // fill in mouse pos for any listeners out there
2865 item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10);
2866 item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10);
2867 }
2868
2869 if (options.grid.autoHighlight) {
2870 // clear auto-highlights
2871 for (var i = 0; i < highlights.length; ++i) {
2872 var h = highlights[i];
2873 if (h.auto == eventname &&
2874 !(item && h.series == item.series &&
2875 h.point[0] == item.datapoint[0] &&
2876 h.point[1] == item.datapoint[1]))
2877 unhighlight(h.series, h.point);
2878 }
2879
2880 if (item)
2881 highlight(item.series, item.datapoint, eventname);
2882 }
2883
2884 placeholder.trigger(eventname, [ pos, item ]);
2885 }
2886
2887 function triggerRedrawOverlay() {
2888 var t = options.interaction.redrawOverlayInterval;
2889 if (t == -1) { // skip event queue
2890 drawOverlay();
2891 return;
2892 }
2893
2894 if (!redrawTimeout)
2895 redrawTimeout = setTimeout(drawOverlay, t);
2896 }
2897
2898 function drawOverlay() {
2899 redrawTimeout = null;
2900
2901 // draw highlights
2902 octx.save();
2903 overlay.clear();
2904 octx.translate(plotOffset.left, plotOffset.top);
2905
2906 var i, hi;
2907 for (i = 0; i < highlights.length; ++i) {
2908 hi = highlights[i];
2909
2910 if (hi.series.bars.show)
2911 drawBarHighlight(hi.series, hi.point);
2912 else
2913 drawPointHighlight(hi.series, hi.point);
2914 }
2915 octx.restore();
2916
2917 executeHooks(hooks.drawOverlay, [octx]);
2918 }
2919
2920 function highlight(s, point, auto) {
2921 if (typeof s == "number")
2922 s = series[s];
2923
2924 if (typeof point == "number") {
2925 var ps = s.datapoints.pointsize;
2926 point = s.datapoints.points.slice(ps * point, ps * (point + 1));
2927 }
2928
2929 var i = indexOfHighlight(s, point);
2930 if (i == -1) {
2931 highlights.push({ series: s, point: point, auto: auto });
2932
2933 triggerRedrawOverlay();
2934 }
2935 else if (!auto)
2936 highlights[i].auto = false;
2937 }
2938
2939 function unhighlight(s, point) {
2940 if (s == null && point == null) {
2941 highlights = [];
2942 triggerRedrawOverlay();
2943 return;
2944 }
2945
2946 if (typeof s == "number")
2947 s = series[s];
2948
2949 if (typeof point == "number") {
2950 var ps = s.datapoints.pointsize;
2951 point = s.datapoints.points.slice(ps * point, ps * (point + 1));
2952 }
2953
2954 var i = indexOfHighlight(s, point);
2955 if (i != -1) {
2956 highlights.splice(i, 1);
2957
2958 triggerRedrawOverlay();
2959 }
2960 }
2961
2962 function indexOfHighlight(s, p) {
2963 for (var i = 0; i < highlights.length; ++i) {
2964 var h = highlights[i];
2965 if (h.series == s && h.point[0] == p[0]
2966 && h.point[1] == p[1])
2967 return i;
2968 }
2969 return -1;
2970 }
2971
2972 function drawPointHighlight(series, point) {
2973 var x = point[0], y = point[1],
2974 axisx = series.xaxis, axisy = series.yaxis,
2975 highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString();
2976
2977 if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
2978 return;
2979
2980 var pointRadius = series.points.radius + series.points.lineWidth / 2;
2981 octx.lineWidth = pointRadius;
2982 octx.strokeStyle = highlightColor;
2983 var radius = 1.5 * pointRadius;
2984 x = axisx.p2c(x);
2985 y = axisy.p2c(y);
2986
2987 octx.beginPath();
2988 if (series.points.symbol == "circle")
2989 octx.arc(x, y, radius, 0, 2 * Math.PI, false);
2990 else
2991 series.points.symbol(octx, x, y, radius, false);
2992 octx.closePath();
2993 octx.stroke();
2994 }
2995
2996 function drawBarHighlight(series, point) {
2997 var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(),
2998 fillStyle = highlightColor,
2999 barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
3000
3001 octx.lineWidth = series.bars.lineWidth;
3002 octx.strokeStyle = highlightColor;
3003
3004 drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth,
3005 0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth);
3006 }
3007
3008 function getColorOrGradient(spec, bottom, top, defaultColor) {
3009 if (typeof spec == "string")
3010 return spec;
3011 else {
3012 // assume this is a gradient spec; IE currently only
3013 // supports a simple vertical gradient properly, so that's
3014 // what we support too
3015 var gradient = ctx.createLinearGradient(0, top, 0, bottom);
3016
3017 for (var i = 0, l = spec.colors.length; i < l; ++i) {
3018 var c = spec.colors[i];
3019 if (typeof c != "string") {
3020 var co = $.color.parse(defaultColor);
3021 if (c.brightness != null)
3022 co = co.scale('rgb', c.brightness);
3023 if (c.opacity != null)
3024 co.a *= c.opacity;
3025 c = co.toString();
3026 }
3027 gradient.addColorStop(i / (l - 1), c);
3028 }
3029
3030 return gradient;
3031 }
3032 }
3033 }
3034
3035 // Add the plot function to the top level of the jQuery object
3036
3037 $.plot = function(placeholder, data, options) {
3038 //var t0 = new Date();
3039 var plot = new Plot($(placeholder), data, options, $.plot.plugins);
3040 //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime()));
3041 return plot;
3042 };
3043
3044 $.plot.version = "0.8.1";
3045
3046 $.plot.plugins = [];
3047
3048 // Also add the plot function as a chainable property
3049
3050 $.fn.plot = function(data, options) {
3051 return this.each(function() {
3052 $.plot(this, data, options);
3053 });
3054 };
3055
3056 // round to nearby lower multiple of base
3057 function floorInBase(n, base) {
3058 return base * Math.floor(n / base);
3059 }
3060
3061 })(jQuery);
3062