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