exception_notification_server 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.gitignore +51 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile +23 -0
  7. data/Gemfile.lock +162 -0
  8. data/LICENSE.txt +20 -0
  9. data/README.rdoc +19 -0
  10. data/Rakefile +59 -0
  11. data/VERSION +1 -0
  12. data/app/assets/images/exception_notification_server/.keep +0 -0
  13. data/app/assets/javascripts/exception_notification_server/application.js.coffee +8 -0
  14. data/app/assets/javascripts/exception_notification_server/flot/excanvas.js +1428 -0
  15. data/app/assets/javascripts/exception_notification_server/flot/jquery.flot.js +3168 -0
  16. data/app/assets/javascripts/exception_notification_server/flot/jquery.flot.resize.js +59 -0
  17. data/app/assets/javascripts/exception_notification_server/flot/jquery.flot.time.js +432 -0
  18. data/app/assets/javascripts/exception_notification_server/jquery.sparkline.js +3054 -0
  19. data/app/assets/javascripts/exception_notification_server/main.js.coffee +6 -0
  20. data/app/assets/javascripts/exception_notification_server/pages/notifications.js.coffee +15 -0
  21. data/app/assets/stylesheets/exception_notification_server/application.css.sass +4 -0
  22. data/app/assets/stylesheets/exception_notification_server/layout.css.sass +97 -0
  23. data/app/assets/stylesheets/exception_notification_server/notifications.css.sass +19 -0
  24. data/app/controllers/exception_notification_server/application_controller.rb +20 -0
  25. data/app/controllers/exception_notification_server/notifications_controller.rb +97 -0
  26. data/app/helpers/exception_notification_server/application_helper.rb +46 -0
  27. data/app/models/exception_notification_server/notification.rb +69 -0
  28. data/app/views/exception_notification_server/notifications/_notifications.html.haml +37 -0
  29. data/app/views/exception_notification_server/notifications/index.html.haml +7 -0
  30. data/app/views/exception_notification_server/notifications/index.js.haml +2 -0
  31. data/app/views/exception_notification_server/notifications/show.html.haml +76 -0
  32. data/app/views/layouts/exception_notification_server/application.html.haml +17 -0
  33. data/config/routes.rb +10 -0
  34. data/exception_notification_server.gemspec +162 -0
  35. data/lib/exception_notification_server/engine.rb +10 -0
  36. data/lib/exception_notification_server/version.rb +3 -0
  37. data/lib/exception_notification_server.rb +21 -0
  38. data/lib/generators/exception_notification_server/install_generator.rb +33 -0
  39. data/lib/generators/exception_notification_server/templates/exception_notification_server.rb +5 -0
  40. data/lib/generators/exception_notification_server/templates/migration.rb +26 -0
  41. data/lib/tasks/exception_notification_server_tasks.rake +4 -0
  42. data/readme.md +5 -0
  43. data/test/dummy/README.rdoc +28 -0
  44. data/test/dummy/Rakefile +6 -0
  45. data/test/dummy/app/assets/images/.keep +0 -0
  46. data/test/dummy/app/assets/javascripts/application.js +13 -0
  47. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  48. data/test/dummy/app/controllers/application_controller.rb +5 -0
  49. data/test/dummy/app/controllers/concerns/.keep +0 -0
  50. data/test/dummy/app/helpers/application_helper.rb +2 -0
  51. data/test/dummy/app/mailers/.keep +0 -0
  52. data/test/dummy/app/models/.keep +0 -0
  53. data/test/dummy/app/models/concerns/.keep +0 -0
  54. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  55. data/test/dummy/bin/bundle +3 -0
  56. data/test/dummy/bin/rails +4 -0
  57. data/test/dummy/bin/rake +4 -0
  58. data/test/dummy/config/application.rb +22 -0
  59. data/test/dummy/config/boot.rb +5 -0
  60. data/test/dummy/config/database.yml +25 -0
  61. data/test/dummy/config/environment.rb +5 -0
  62. data/test/dummy/config/environments/development.rb +37 -0
  63. data/test/dummy/config/environments/production.rb +82 -0
  64. data/test/dummy/config/environments/test.rb +39 -0
  65. data/test/dummy/config/initializers/assets.rb +8 -0
  66. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  67. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  68. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  69. data/test/dummy/config/initializers/inflections.rb +16 -0
  70. data/test/dummy/config/initializers/mime_types.rb +4 -0
  71. data/test/dummy/config/initializers/session_store.rb +3 -0
  72. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  73. data/test/dummy/config/locales/en.yml +23 -0
  74. data/test/dummy/config/routes.rb +3 -0
  75. data/test/dummy/config/secrets.yml +22 -0
  76. data/test/dummy/config.ru +4 -0
  77. data/test/dummy/db/test.sqlite3 +0 -0
  78. data/test/dummy/lib/assets/.keep +0 -0
  79. data/test/dummy/log/.keep +0 -0
  80. data/test/dummy/public/404.html +67 -0
  81. data/test/dummy/public/422.html +67 -0
  82. data/test/dummy/public/500.html +66 -0
  83. data/test/dummy/public/favicon.ico +0 -0
  84. data/test/exception_notification_server_test.rb +7 -0
  85. data/test/integration/navigation_test.rb +9 -0
  86. data/test/test_helper.rb +15 -0
  87. metadata +357 -0
@@ -0,0 +1,3168 @@
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);