PluginProbe
Contact Forms by Cimatti / 1.3.3
Contact Forms by Cimatti v1.3.3
2.3.6 2.3.5 2.3.0 2.2.32 2.2.4 2.2.0 2.1.2 2.1.1 trunk 1.0 1.1 1.2 1.2.1 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 All 62 releases
contact-forms / flot / jquery.flot.js

jquery.flot.js in Contact Forms by Cimatti 1.3.3, at flot/jquery.flot.js

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