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