flot-graph-rails 1.0.2 → 1.0.3

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