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