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