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