redmon 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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);