Mercurial > ~darius > hgwebdir.cgi > iwws
comparison static/jquery.flot.js @ 0:2d9ee2b3ae82
Initial commit of iWWS.
author | Daniel O'Connor <darius@dons.net.au> |
---|---|
date | Mon, 15 Aug 2011 17:44:56 +0930 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:2d9ee2b3ae82 |
---|---|
1 /* Javascript plotting library for jQuery, v. 0.6. | |
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.0. | |
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() work in-place instead of returning | |
26 * new objects. | |
27 */ | |
28 (function(){jQuery.color={};jQuery.color.make=function(E,D,B,C){var F={};F.r=E||0;F.g=D||0;F.b=B||0;F.a=C!=null?C:1;F.add=function(I,H){for(var G=0;G<I.length;++G){F[I.charAt(G)]+=H}return F.normalize()};F.scale=function(I,H){for(var G=0;G<I.length;++G){F[I.charAt(G)]*=H}return F.normalize()};F.toString=function(){if(F.a>=1){return"rgb("+[F.r,F.g,F.b].join(",")+")"}else{return"rgba("+[F.r,F.g,F.b,F.a].join(",")+")"}};F.normalize=function(){function G(I,J,H){return J<I?I:(J>H?H:J)}F.r=G(0,parseInt(F.r),255);F.g=G(0,parseInt(F.g),255);F.b=G(0,parseInt(F.b),255);F.a=G(0,F.a,1);return F};F.clone=function(){return jQuery.color.make(F.r,F.b,F.g,F.a)};return F.normalize()};jQuery.color.extract=function(C,B){var D;do{D=C.css(B).toLowerCase();if(D!=""&&D!="transparent"){break}C=C.parent()}while(!jQuery.nodeName(C.get(0),"body"));if(D=="rgba(0, 0, 0, 0)"){D="transparent"}return jQuery.color.parse(D)};jQuery.color.parse=function(E){var D,B=jQuery.color.make;if(D=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10))}if(D=/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(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10),parseFloat(D[4]))}if(D=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55)}if(D=/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(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55,parseFloat(D[4]))}if(D=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(E)){return B(parseInt(D[1],16),parseInt(D[2],16),parseInt(D[3],16))}if(D=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(E)){return B(parseInt(D[1]+D[1],16),parseInt(D[2]+D[2],16),parseInt(D[3]+D[3],16))}var C=jQuery.trim(E).toLowerCase();if(C=="transparent"){return B(255,255,255,0)}else{D=A[C];return B(D[0],D[1],D[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]}})(); | |
29 | |
30 // the actual Flot code | |
31 (function($) { | |
32 function Plot(placeholder, data_, options_, plugins) { | |
33 // data is on the form: | |
34 // [ series1, series2 ... ] | |
35 // where series is either just the data as [ [x1, y1], [x2, y2], ... ] | |
36 // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } | |
37 | |
38 var series = [], | |
39 options = { | |
40 // the color theme used for graphs | |
41 colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], | |
42 legend: { | |
43 show: true, | |
44 noColumns: 1, // number of colums in legend table | |
45 labelFormatter: null, // fn: string -> string | |
46 labelBoxBorderColor: "#ccc", // border color for the little label boxes | |
47 container: null, // container (as jQuery object) to put legend in, null means default on top of graph | |
48 position: "ne", // position of default legend container within plot | |
49 margin: 5, // distance from grid edge to default legend container within plot | |
50 backgroundColor: null, // null means auto-detect | |
51 backgroundOpacity: 0.85 // set to 0 to avoid background | |
52 }, | |
53 xaxis: { | |
54 mode: null, // null or "time" | |
55 transform: null, // null or f: number -> number to transform axis | |
56 inverseTransform: null, // if transform is set, this should be the inverse function | |
57 min: null, // min. value to show, null means set automatically | |
58 max: null, // max. value to show, null means set automatically | |
59 autoscaleMargin: null, // margin in % to add if auto-setting min/max | |
60 ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks | |
61 tickFormatter: null, // fn: number -> string | |
62 labelWidth: null, // size of tick labels in pixels | |
63 labelHeight: null, | |
64 | |
65 // mode specific options | |
66 tickDecimals: null, // no. of decimals, null means auto | |
67 tickSize: null, // number or [number, "unit"] | |
68 minTickSize: null, // number or [number, "unit"] | |
69 monthNames: null, // list of names of months | |
70 timeformat: null, // format string to use | |
71 twelveHourClock: false // 12 or 24 time in time mode | |
72 }, | |
73 yaxis: { | |
74 autoscaleMargin: 0.02 | |
75 }, | |
76 x2axis: { | |
77 autoscaleMargin: null | |
78 }, | |
79 y2axis: { | |
80 autoscaleMargin: 0.02 | |
81 }, | |
82 series: { | |
83 points: { | |
84 show: false, | |
85 radius: 3, | |
86 lineWidth: 2, // in pixels | |
87 fill: true, | |
88 fillColor: "#ffffff" | |
89 }, | |
90 lines: { | |
91 // we don't put in show: false so we can see | |
92 // whether lines were actively disabled | |
93 lineWidth: 2, // in pixels | |
94 fill: false, | |
95 fillColor: null, | |
96 steps: false | |
97 }, | |
98 bars: { | |
99 show: false, | |
100 lineWidth: 2, // in pixels | |
101 barWidth: 1, // in units of the x axis | |
102 fill: true, | |
103 fillColor: null, | |
104 align: "left", // or "center" | |
105 horizontal: false // when horizontal, left is now top | |
106 }, | |
107 shadowSize: 3 | |
108 }, | |
109 grid: { | |
110 show: true, | |
111 aboveData: false, | |
112 color: "#545454", // primary color used for outline and labels | |
113 backgroundColor: null, // null for transparent, else color | |
114 tickColor: "rgba(0,0,0,0.15)", // color used for the ticks | |
115 labelMargin: 5, // in pixels | |
116 borderWidth: 2, // in pixels | |
117 borderColor: null, // set if different from the grid color | |
118 markings: null, // array of ranges or fn: axes -> array of ranges | |
119 markingsColor: "#f4f4f4", | |
120 markingsLineWidth: 2, | |
121 // interactive stuff | |
122 clickable: false, | |
123 hoverable: false, | |
124 autoHighlight: true, // highlight in case mouse is near | |
125 mouseActiveRadius: 10 // how far the mouse can be away to activate an item | |
126 }, | |
127 hooks: {} | |
128 }, | |
129 canvas = null, // the canvas for the plot itself | |
130 overlay = null, // canvas for interactive stuff on top of plot | |
131 eventHolder = null, // jQuery object that events should be bound to | |
132 ctx = null, octx = null, | |
133 axes = { xaxis: {}, yaxis: {}, x2axis: {}, y2axis: {} }, | |
134 plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, | |
135 canvasWidth = 0, canvasHeight = 0, | |
136 plotWidth = 0, plotHeight = 0, | |
137 hooks = { | |
138 processOptions: [], | |
139 processRawData: [], | |
140 processDatapoints: [], | |
141 draw: [], | |
142 bindEvents: [], | |
143 drawOverlay: [] | |
144 }, | |
145 plot = this; | |
146 | |
147 // public functions | |
148 plot.setData = setData; | |
149 plot.setupGrid = setupGrid; | |
150 plot.draw = draw; | |
151 plot.getPlaceholder = function() { return placeholder; }; | |
152 plot.getCanvas = function() { return canvas; }; | |
153 plot.getPlotOffset = function() { return plotOffset; }; | |
154 plot.width = function () { return plotWidth; }; | |
155 plot.height = function () { return plotHeight; }; | |
156 plot.offset = function () { | |
157 var o = eventHolder.offset(); | |
158 o.left += plotOffset.left; | |
159 o.top += plotOffset.top; | |
160 return o; | |
161 }; | |
162 plot.getData = function() { return series; }; | |
163 plot.getAxes = function() { return axes; }; | |
164 plot.getOptions = function() { return options; }; | |
165 plot.highlight = highlight; | |
166 plot.unhighlight = unhighlight; | |
167 plot.triggerRedrawOverlay = triggerRedrawOverlay; | |
168 plot.pointOffset = function(point) { | |
169 return { left: parseInt(axisSpecToRealAxis(point, "xaxis").p2c(+point.x) + plotOffset.left), | |
170 top: parseInt(axisSpecToRealAxis(point, "yaxis").p2c(+point.y) + plotOffset.top) }; | |
171 }; | |
172 | |
173 | |
174 // public attributes | |
175 plot.hooks = hooks; | |
176 | |
177 // initialize | |
178 initPlugins(plot); | |
179 parseOptions(options_); | |
180 constructCanvas(); | |
181 setData(data_); | |
182 setupGrid(); | |
183 draw(); | |
184 bindEvents(); | |
185 | |
186 | |
187 function executeHooks(hook, args) { | |
188 args = [plot].concat(args); | |
189 for (var i = 0; i < hook.length; ++i) | |
190 hook[i].apply(this, args); | |
191 } | |
192 | |
193 function initPlugins() { | |
194 for (var i = 0; i < plugins.length; ++i) { | |
195 var p = plugins[i]; | |
196 p.init(plot); | |
197 if (p.options) | |
198 $.extend(true, options, p.options); | |
199 } | |
200 } | |
201 | |
202 function parseOptions(opts) { | |
203 $.extend(true, options, opts); | |
204 if (options.grid.borderColor == null) | |
205 options.grid.borderColor = options.grid.color; | |
206 // backwards compatibility, to be removed in future | |
207 if (options.xaxis.noTicks && options.xaxis.ticks == null) | |
208 options.xaxis.ticks = options.xaxis.noTicks; | |
209 if (options.yaxis.noTicks && options.yaxis.ticks == null) | |
210 options.yaxis.ticks = options.yaxis.noTicks; | |
211 if (options.grid.coloredAreas) | |
212 options.grid.markings = options.grid.coloredAreas; | |
213 if (options.grid.coloredAreasColor) | |
214 options.grid.markingsColor = options.grid.coloredAreasColor; | |
215 if (options.lines) | |
216 $.extend(true, options.series.lines, options.lines); | |
217 if (options.points) | |
218 $.extend(true, options.series.points, options.points); | |
219 if (options.bars) | |
220 $.extend(true, options.series.bars, options.bars); | |
221 if (options.shadowSize) | |
222 options.series.shadowSize = options.shadowSize; | |
223 | |
224 for (var n in hooks) | |
225 if (options.hooks[n] && options.hooks[n].length) | |
226 hooks[n] = hooks[n].concat(options.hooks[n]); | |
227 | |
228 executeHooks(hooks.processOptions, [options]); | |
229 } | |
230 | |
231 function setData(d) { | |
232 series = parseData(d); | |
233 fillInSeriesOptions(); | |
234 processData(); | |
235 } | |
236 | |
237 function parseData(d) { | |
238 var res = []; | |
239 for (var i = 0; i < d.length; ++i) { | |
240 var s = $.extend(true, {}, options.series); | |
241 | |
242 if (d[i].data) { | |
243 s.data = d[i].data; // move the data instead of deep-copy | |
244 delete d[i].data; | |
245 | |
246 $.extend(true, s, d[i]); | |
247 | |
248 d[i].data = s.data; | |
249 } | |
250 else | |
251 s.data = d[i]; | |
252 res.push(s); | |
253 } | |
254 | |
255 return res; | |
256 } | |
257 | |
258 function axisSpecToRealAxis(obj, attr) { | |
259 var a = obj[attr]; | |
260 if (!a || a == 1) | |
261 return axes[attr]; | |
262 if (typeof a == "number") | |
263 return axes[attr.charAt(0) + a + attr.slice(1)]; | |
264 return a; // assume it's OK | |
265 } | |
266 | |
267 function fillInSeriesOptions() { | |
268 var i; | |
269 | |
270 // collect what we already got of colors | |
271 var neededColors = series.length, | |
272 usedColors = [], | |
273 assignedColors = []; | |
274 for (i = 0; i < series.length; ++i) { | |
275 var sc = series[i].color; | |
276 if (sc != null) { | |
277 --neededColors; | |
278 if (typeof sc == "number") | |
279 assignedColors.push(sc); | |
280 else | |
281 usedColors.push($.color.parse(series[i].color)); | |
282 } | |
283 } | |
284 | |
285 // we might need to generate more colors if higher indices | |
286 // are assigned | |
287 for (i = 0; i < assignedColors.length; ++i) { | |
288 neededColors = Math.max(neededColors, assignedColors[i] + 1); | |
289 } | |
290 | |
291 // produce colors as needed | |
292 var colors = [], variation = 0; | |
293 i = 0; | |
294 while (colors.length < neededColors) { | |
295 var c; | |
296 if (options.colors.length == i) // check degenerate case | |
297 c = $.color.make(100, 100, 100); | |
298 else | |
299 c = $.color.parse(options.colors[i]); | |
300 | |
301 // vary color if needed | |
302 var sign = variation % 2 == 1 ? -1 : 1; | |
303 c.scale('rgb', 1 + sign * Math.ceil(variation / 2) * 0.2) | |
304 | |
305 // FIXME: if we're getting to close to something else, | |
306 // we should probably skip this one | |
307 colors.push(c); | |
308 | |
309 ++i; | |
310 if (i >= options.colors.length) { | |
311 i = 0; | |
312 ++variation; | |
313 } | |
314 } | |
315 | |
316 // fill in the options | |
317 var colori = 0, s; | |
318 for (i = 0; i < series.length; ++i) { | |
319 s = series[i]; | |
320 | |
321 // assign colors | |
322 if (s.color == null) { | |
323 s.color = colors[colori].toString(); | |
324 ++colori; | |
325 } | |
326 else if (typeof s.color == "number") | |
327 s.color = colors[s.color].toString(); | |
328 | |
329 // turn on lines automatically in case nothing is set | |
330 if (s.lines.show == null) { | |
331 var v, show = true; | |
332 for (v in s) | |
333 if (s[v].show) { | |
334 show = false; | |
335 break; | |
336 } | |
337 if (show) | |
338 s.lines.show = true; | |
339 } | |
340 | |
341 // setup axes | |
342 s.xaxis = axisSpecToRealAxis(s, "xaxis"); | |
343 s.yaxis = axisSpecToRealAxis(s, "yaxis"); | |
344 } | |
345 } | |
346 | |
347 function processData() { | |
348 var topSentry = Number.POSITIVE_INFINITY, | |
349 bottomSentry = Number.NEGATIVE_INFINITY, | |
350 i, j, k, m, length, | |
351 s, points, ps, x, y, axis, val, f, p; | |
352 | |
353 for (axis in axes) { | |
354 axes[axis].datamin = topSentry; | |
355 axes[axis].datamax = bottomSentry; | |
356 axes[axis].used = false; | |
357 } | |
358 | |
359 function updateAxis(axis, min, max) { | |
360 if (min < axis.datamin) | |
361 axis.datamin = min; | |
362 if (max > axis.datamax) | |
363 axis.datamax = max; | |
364 } | |
365 | |
366 for (i = 0; i < series.length; ++i) { | |
367 s = series[i]; | |
368 s.datapoints = { points: [] }; | |
369 | |
370 executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); | |
371 } | |
372 | |
373 // first pass: clean and copy data | |
374 for (i = 0; i < series.length; ++i) { | |
375 s = series[i]; | |
376 | |
377 var data = s.data, format = s.datapoints.format; | |
378 | |
379 if (!format) { | |
380 format = []; | |
381 // find out how to copy | |
382 format.push({ x: true, number: true, required: true }); | |
383 format.push({ y: true, number: true, required: true }); | |
384 | |
385 if (s.bars.show) | |
386 format.push({ y: true, number: true, required: false, defaultValue: 0 }); | |
387 | |
388 s.datapoints.format = format; | |
389 } | |
390 | |
391 if (s.datapoints.pointsize != null) | |
392 continue; // already filled in | |
393 | |
394 if (s.datapoints.pointsize == null) | |
395 s.datapoints.pointsize = format.length; | |
396 | |
397 ps = s.datapoints.pointsize; | |
398 points = s.datapoints.points; | |
399 | |
400 insertSteps = s.lines.show && s.lines.steps; | |
401 s.xaxis.used = s.yaxis.used = true; | |
402 | |
403 for (j = k = 0; j < data.length; ++j, k += ps) { | |
404 p = data[j]; | |
405 | |
406 var nullify = p == null; | |
407 if (!nullify) { | |
408 for (m = 0; m < ps; ++m) { | |
409 val = p[m]; | |
410 f = format[m]; | |
411 | |
412 if (f) { | |
413 if (f.number && val != null) { | |
414 val = +val; // convert to number | |
415 if (isNaN(val)) | |
416 val = null; | |
417 } | |
418 | |
419 if (val == null) { | |
420 if (f.required) | |
421 nullify = true; | |
422 | |
423 if (f.defaultValue != null) | |
424 val = f.defaultValue; | |
425 } | |
426 } | |
427 | |
428 points[k + m] = val; | |
429 } | |
430 } | |
431 | |
432 if (nullify) { | |
433 for (m = 0; m < ps; ++m) { | |
434 val = points[k + m]; | |
435 if (val != null) { | |
436 f = format[m]; | |
437 // extract min/max info | |
438 if (f.x) | |
439 updateAxis(s.xaxis, val, val); | |
440 if (f.y) | |
441 updateAxis(s.yaxis, val, val); | |
442 } | |
443 points[k + m] = null; | |
444 } | |
445 } | |
446 else { | |
447 // a little bit of line specific stuff that | |
448 // perhaps shouldn't be here, but lacking | |
449 // better means... | |
450 if (insertSteps && k > 0 | |
451 && points[k - ps] != null | |
452 && points[k - ps] != points[k] | |
453 && points[k - ps + 1] != points[k + 1]) { | |
454 // copy the point to make room for a middle point | |
455 for (m = 0; m < ps; ++m) | |
456 points[k + ps + m] = points[k + m]; | |
457 | |
458 // middle point has same y | |
459 points[k + 1] = points[k - ps + 1]; | |
460 | |
461 // we've added a point, better reflect that | |
462 k += ps; | |
463 } | |
464 } | |
465 } | |
466 } | |
467 | |
468 // give the hooks a chance to run | |
469 for (i = 0; i < series.length; ++i) { | |
470 s = series[i]; | |
471 | |
472 executeHooks(hooks.processDatapoints, [ s, s.datapoints]); | |
473 } | |
474 | |
475 // second pass: find datamax/datamin for auto-scaling | |
476 for (i = 0; i < series.length; ++i) { | |
477 s = series[i]; | |
478 points = s.datapoints.points, | |
479 ps = s.datapoints.pointsize; | |
480 | |
481 var xmin = topSentry, ymin = topSentry, | |
482 xmax = bottomSentry, ymax = bottomSentry; | |
483 | |
484 for (j = 0; j < points.length; j += ps) { | |
485 if (points[j] == null) | |
486 continue; | |
487 | |
488 for (m = 0; m < ps; ++m) { | |
489 val = points[j + m]; | |
490 f = format[m]; | |
491 if (!f) | |
492 continue; | |
493 | |
494 if (f.x) { | |
495 if (val < xmin) | |
496 xmin = val; | |
497 if (val > xmax) | |
498 xmax = val; | |
499 } | |
500 if (f.y) { | |
501 if (val < ymin) | |
502 ymin = val; | |
503 if (val > ymax) | |
504 ymax = val; | |
505 } | |
506 } | |
507 } | |
508 | |
509 if (s.bars.show) { | |
510 // make sure we got room for the bar on the dancing floor | |
511 var delta = s.bars.align == "left" ? 0 : -s.bars.barWidth/2; | |
512 if (s.bars.horizontal) { | |
513 ymin += delta; | |
514 ymax += delta + s.bars.barWidth; | |
515 } | |
516 else { | |
517 xmin += delta; | |
518 xmax += delta + s.bars.barWidth; | |
519 } | |
520 } | |
521 | |
522 updateAxis(s.xaxis, xmin, xmax); | |
523 updateAxis(s.yaxis, ymin, ymax); | |
524 } | |
525 | |
526 for (axis in axes) { | |
527 if (axes[axis].datamin == topSentry) | |
528 axes[axis].datamin = null; | |
529 if (axes[axis].datamax == bottomSentry) | |
530 axes[axis].datamax = null; | |
531 } | |
532 } | |
533 | |
534 function constructCanvas() { | |
535 function makeCanvas(width, height) { | |
536 var c = document.createElement('canvas'); | |
537 c.width = width; | |
538 c.height = height; | |
539 if ($.browser.msie) // excanvas hack | |
540 c = window.G_vmlCanvasManager.initElement(c); | |
541 return c; | |
542 } | |
543 | |
544 canvasWidth = placeholder.width(); | |
545 canvasHeight = placeholder.height(); | |
546 placeholder.html(""); // clear placeholder | |
547 if (placeholder.css("position") == 'static') | |
548 placeholder.css("position", "relative"); // for positioning labels and overlay | |
549 | |
550 if (canvasWidth <= 0 || canvasHeight <= 0) | |
551 throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight; | |
552 | |
553 if ($.browser.msie) // excanvas hack | |
554 window.G_vmlCanvasManager.init_(document); // make sure everything is setup | |
555 | |
556 // the canvas | |
557 canvas = $(makeCanvas(canvasWidth, canvasHeight)).appendTo(placeholder).get(0); | |
558 ctx = canvas.getContext("2d"); | |
559 | |
560 // overlay canvas for interactive features | |
561 overlay = $(makeCanvas(canvasWidth, canvasHeight)).css({ position: 'absolute', left: 0, top: 0 }).appendTo(placeholder).get(0); | |
562 octx = overlay.getContext("2d"); | |
563 octx.stroke(); | |
564 } | |
565 | |
566 function bindEvents() { | |
567 // we include the canvas in the event holder too, because IE 7 | |
568 // sometimes has trouble with the stacking order | |
569 eventHolder = $([overlay, canvas]); | |
570 | |
571 // bind events | |
572 if (options.grid.hoverable) | |
573 eventHolder.mousemove(onMouseMove); | |
574 | |
575 if (options.grid.clickable) | |
576 eventHolder.click(onClick); | |
577 | |
578 executeHooks(hooks.bindEvents, [eventHolder]); | |
579 } | |
580 | |
581 function setupGrid() { | |
582 function setTransformationHelpers(axis, o) { | |
583 function identity(x) { return x; } | |
584 | |
585 var s, m, t = o.transform || identity, | |
586 it = o.inverseTransform; | |
587 | |
588 // add transformation helpers | |
589 if (axis == axes.xaxis || axis == axes.x2axis) { | |
590 // precompute how much the axis is scaling a point | |
591 // in canvas space | |
592 s = axis.scale = plotWidth / (t(axis.max) - t(axis.min)); | |
593 m = t(axis.min); | |
594 | |
595 // data point to canvas coordinate | |
596 if (t == identity) // slight optimization | |
597 axis.p2c = function (p) { return (p - m) * s; }; | |
598 else | |
599 axis.p2c = function (p) { return (t(p) - m) * s; }; | |
600 // canvas coordinate to data point | |
601 if (!it) | |
602 axis.c2p = function (c) { return m + c / s; }; | |
603 else | |
604 axis.c2p = function (c) { return it(m + c / s); }; | |
605 } | |
606 else { | |
607 s = axis.scale = plotHeight / (t(axis.max) - t(axis.min)); | |
608 m = t(axis.max); | |
609 | |
610 if (t == identity) | |
611 axis.p2c = function (p) { return (m - p) * s; }; | |
612 else | |
613 axis.p2c = function (p) { return (m - t(p)) * s; }; | |
614 if (!it) | |
615 axis.c2p = function (c) { return m - c / s; }; | |
616 else | |
617 axis.c2p = function (c) { return it(m - c / s); }; | |
618 } | |
619 } | |
620 | |
621 function measureLabels(axis, axisOptions) { | |
622 var i, labels = [], l; | |
623 | |
624 axis.labelWidth = axisOptions.labelWidth; | |
625 axis.labelHeight = axisOptions.labelHeight; | |
626 | |
627 if (axis == axes.xaxis || axis == axes.x2axis) { | |
628 // to avoid measuring the widths of the labels, we | |
629 // construct fixed-size boxes and put the labels inside | |
630 // them, we don't need the exact figures and the | |
631 // fixed-size box content is easy to center | |
632 if (axis.labelWidth == null) | |
633 axis.labelWidth = canvasWidth / (axis.ticks.length > 0 ? axis.ticks.length : 1); | |
634 | |
635 // measure x label heights | |
636 if (axis.labelHeight == null) { | |
637 labels = []; | |
638 for (i = 0; i < axis.ticks.length; ++i) { | |
639 l = axis.ticks[i].label; | |
640 if (l) | |
641 labels.push('<div class="tickLabel" style="float:left;width:' + axis.labelWidth + 'px">' + l + '</div>'); | |
642 } | |
643 | |
644 if (labels.length > 0) { | |
645 var dummyDiv = $('<div style="position:absolute;top:-10000px;width:10000px;font-size:smaller">' | |
646 + labels.join("") + '<div style="clear:left"></div></div>').appendTo(placeholder); | |
647 axis.labelHeight = dummyDiv.height(); | |
648 dummyDiv.remove(); | |
649 } | |
650 } | |
651 } | |
652 else if (axis.labelWidth == null || axis.labelHeight == null) { | |
653 // calculate y label dimensions | |
654 for (i = 0; i < axis.ticks.length; ++i) { | |
655 l = axis.ticks[i].label; | |
656 if (l) | |
657 labels.push('<div class="tickLabel">' + l + '</div>'); | |
658 } | |
659 | |
660 if (labels.length > 0) { | |
661 var dummyDiv = $('<div style="position:absolute;top:-10000px;font-size:smaller">' | |
662 + labels.join("") + '</div>').appendTo(placeholder); | |
663 if (axis.labelWidth == null) | |
664 axis.labelWidth = dummyDiv.width(); | |
665 if (axis.labelHeight == null) | |
666 axis.labelHeight = dummyDiv.find("div").height(); | |
667 dummyDiv.remove(); | |
668 } | |
669 | |
670 } | |
671 | |
672 if (axis.labelWidth == null) | |
673 axis.labelWidth = 0; | |
674 if (axis.labelHeight == null) | |
675 axis.labelHeight = 0; | |
676 } | |
677 | |
678 function setGridSpacing() { | |
679 // get the most space needed around the grid for things | |
680 // that may stick out | |
681 var maxOutset = options.grid.borderWidth; | |
682 for (i = 0; i < series.length; ++i) | |
683 maxOutset = Math.max(maxOutset, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); | |
684 | |
685 plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = maxOutset; | |
686 | |
687 var margin = options.grid.labelMargin + options.grid.borderWidth; | |
688 | |
689 if (axes.xaxis.labelHeight > 0) | |
690 plotOffset.bottom = Math.max(maxOutset, axes.xaxis.labelHeight + margin); | |
691 if (axes.yaxis.labelWidth > 0) | |
692 plotOffset.left = Math.max(maxOutset, axes.yaxis.labelWidth + margin); | |
693 if (axes.x2axis.labelHeight > 0) | |
694 plotOffset.top = Math.max(maxOutset, axes.x2axis.labelHeight + margin); | |
695 if (axes.y2axis.labelWidth > 0) | |
696 plotOffset.right = Math.max(maxOutset, axes.y2axis.labelWidth + margin); | |
697 | |
698 plotWidth = canvasWidth - plotOffset.left - plotOffset.right; | |
699 plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top; | |
700 } | |
701 | |
702 var axis; | |
703 for (axis in axes) | |
704 setRange(axes[axis], options[axis]); | |
705 | |
706 if (options.grid.show) { | |
707 for (axis in axes) { | |
708 prepareTickGeneration(axes[axis], options[axis]); | |
709 setTicks(axes[axis], options[axis]); | |
710 measureLabels(axes[axis], options[axis]); | |
711 } | |
712 | |
713 setGridSpacing(); | |
714 } | |
715 else { | |
716 plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0; | |
717 plotWidth = canvasWidth; | |
718 plotHeight = canvasHeight; | |
719 } | |
720 | |
721 for (axis in axes) | |
722 setTransformationHelpers(axes[axis], options[axis]); | |
723 | |
724 if (options.grid.show) | |
725 insertLabels(); | |
726 | |
727 insertLegend(); | |
728 } | |
729 | |
730 function setRange(axis, axisOptions) { | |
731 var min = +(axisOptions.min != null ? axisOptions.min : axis.datamin), | |
732 max = +(axisOptions.max != null ? axisOptions.max : axis.datamax), | |
733 delta = max - min; | |
734 | |
735 if (delta == 0.0) { | |
736 // degenerate case | |
737 var widen = max == 0 ? 1 : 0.01; | |
738 | |
739 if (axisOptions.min == null) | |
740 min -= widen; | |
741 // alway widen max if we couldn't widen min to ensure we | |
742 // don't fall into min == max which doesn't work | |
743 if (axisOptions.max == null || axisOptions.min != null) | |
744 max += widen; | |
745 } | |
746 else { | |
747 // consider autoscaling | |
748 var margin = axisOptions.autoscaleMargin; | |
749 if (margin != null) { | |
750 if (axisOptions.min == null) { | |
751 min -= delta * margin; | |
752 // make sure we don't go below zero if all values | |
753 // are positive | |
754 if (min < 0 && axis.datamin != null && axis.datamin >= 0) | |
755 min = 0; | |
756 } | |
757 if (axisOptions.max == null) { | |
758 max += delta * margin; | |
759 if (max > 0 && axis.datamax != null && axis.datamax <= 0) | |
760 max = 0; | |
761 } | |
762 } | |
763 } | |
764 axis.min = min; | |
765 axis.max = max; | |
766 } | |
767 | |
768 function prepareTickGeneration(axis, axisOptions) { | |
769 // estimate number of ticks | |
770 var noTicks; | |
771 if (typeof axisOptions.ticks == "number" && axisOptions.ticks > 0) | |
772 noTicks = axisOptions.ticks; | |
773 else if (axis == axes.xaxis || axis == axes.x2axis) | |
774 // heuristic based on the model a*sqrt(x) fitted to | |
775 // some reasonable data points | |
776 noTicks = 0.3 * Math.sqrt(canvasWidth); | |
777 else | |
778 noTicks = 0.3 * Math.sqrt(canvasHeight); | |
779 | |
780 var delta = (axis.max - axis.min) / noTicks, | |
781 size, generator, unit, formatter, i, magn, norm; | |
782 | |
783 if (axisOptions.mode == "time") { | |
784 // pretty handling of time | |
785 | |
786 // map of app. size of time units in milliseconds | |
787 var timeUnitSize = { | |
788 "second": 1000, | |
789 "minute": 60 * 1000, | |
790 "hour": 60 * 60 * 1000, | |
791 "day": 24 * 60 * 60 * 1000, | |
792 "month": 30 * 24 * 60 * 60 * 1000, | |
793 "year": 365.2425 * 24 * 60 * 60 * 1000 | |
794 }; | |
795 | |
796 | |
797 // the allowed tick sizes, after 1 year we use | |
798 // an integer algorithm | |
799 var spec = [ | |
800 [1, "second"], [2, "second"], [5, "second"], [10, "second"], | |
801 [30, "second"], | |
802 [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], | |
803 [30, "minute"], | |
804 [1, "hour"], [2, "hour"], [4, "hour"], | |
805 [8, "hour"], [12, "hour"], | |
806 [1, "day"], [2, "day"], [3, "day"], | |
807 [0.25, "month"], [0.5, "month"], [1, "month"], | |
808 [2, "month"], [3, "month"], [6, "month"], | |
809 [1, "year"] | |
810 ]; | |
811 | |
812 var minSize = 0; | |
813 if (axisOptions.minTickSize != null) { | |
814 if (typeof axisOptions.tickSize == "number") | |
815 minSize = axisOptions.tickSize; | |
816 else | |
817 minSize = axisOptions.minTickSize[0] * timeUnitSize[axisOptions.minTickSize[1]]; | |
818 } | |
819 | |
820 for (i = 0; i < spec.length - 1; ++i) | |
821 if (delta < (spec[i][0] * timeUnitSize[spec[i][1]] | |
822 + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 | |
823 && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) | |
824 break; | |
825 size = spec[i][0]; | |
826 unit = spec[i][1]; | |
827 | |
828 // special-case the possibility of several years | |
829 if (unit == "year") { | |
830 magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10)); | |
831 norm = (delta / timeUnitSize.year) / magn; | |
832 if (norm < 1.5) | |
833 size = 1; | |
834 else if (norm < 3) | |
835 size = 2; | |
836 else if (norm < 7.5) | |
837 size = 5; | |
838 else | |
839 size = 10; | |
840 | |
841 size *= magn; | |
842 } | |
843 | |
844 if (axisOptions.tickSize) { | |
845 size = axisOptions.tickSize[0]; | |
846 unit = axisOptions.tickSize[1]; | |
847 } | |
848 | |
849 generator = function(axis) { | |
850 var ticks = [], | |
851 tickSize = axis.tickSize[0], unit = axis.tickSize[1], | |
852 d = new Date(axis.min); | |
853 | |
854 var step = tickSize * timeUnitSize[unit]; | |
855 | |
856 if (unit == "second") | |
857 d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize)); | |
858 if (unit == "minute") | |
859 d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize)); | |
860 if (unit == "hour") | |
861 d.setUTCHours(floorInBase(d.getUTCHours(), tickSize)); | |
862 if (unit == "month") | |
863 d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize)); | |
864 if (unit == "year") | |
865 d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize)); | |
866 | |
867 // reset smaller components | |
868 d.setUTCMilliseconds(0); | |
869 if (step >= timeUnitSize.minute) | |
870 d.setUTCSeconds(0); | |
871 if (step >= timeUnitSize.hour) | |
872 d.setUTCMinutes(0); | |
873 if (step >= timeUnitSize.day) | |
874 d.setUTCHours(0); | |
875 if (step >= timeUnitSize.day * 4) | |
876 d.setUTCDate(1); | |
877 if (step >= timeUnitSize.year) | |
878 d.setUTCMonth(0); | |
879 | |
880 | |
881 var carry = 0, v = Number.NaN, prev; | |
882 do { | |
883 prev = v; | |
884 v = d.getTime(); | |
885 ticks.push({ v: v, label: axis.tickFormatter(v, axis) }); | |
886 if (unit == "month") { | |
887 if (tickSize < 1) { | |
888 // a bit complicated - we'll divide the month | |
889 // up but we need to take care of fractions | |
890 // so we don't end up in the middle of a day | |
891 d.setUTCDate(1); | |
892 var start = d.getTime(); | |
893 d.setUTCMonth(d.getUTCMonth() + 1); | |
894 var end = d.getTime(); | |
895 d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); | |
896 carry = d.getUTCHours(); | |
897 d.setUTCHours(0); | |
898 } | |
899 else | |
900 d.setUTCMonth(d.getUTCMonth() + tickSize); | |
901 } | |
902 else if (unit == "year") { | |
903 d.setUTCFullYear(d.getUTCFullYear() + tickSize); | |
904 } | |
905 else | |
906 d.setTime(v + step); | |
907 } while (v < axis.max && v != prev); | |
908 | |
909 return ticks; | |
910 }; | |
911 | |
912 formatter = function (v, axis) { | |
913 var d = new Date(v); | |
914 | |
915 // first check global format | |
916 if (axisOptions.timeformat != null) | |
917 return $.plot.formatDate(d, axisOptions.timeformat, axisOptions.monthNames); | |
918 | |
919 var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; | |
920 var span = axis.max - axis.min; | |
921 var suffix = (axisOptions.twelveHourClock) ? " %p" : ""; | |
922 | |
923 if (t < timeUnitSize.minute) | |
924 fmt = "%h:%M:%S" + suffix; | |
925 else if (t < timeUnitSize.day) { | |
926 if (span < 2 * timeUnitSize.day) | |
927 fmt = "%h:%M" + suffix; | |
928 else | |
929 fmt = "%b %d %h:%M" + suffix; | |
930 } | |
931 else if (t < timeUnitSize.month) | |
932 fmt = "%b %d"; | |
933 else if (t < timeUnitSize.year) { | |
934 if (span < timeUnitSize.year) | |
935 fmt = "%b"; | |
936 else | |
937 fmt = "%b %y"; | |
938 } | |
939 else | |
940 fmt = "%y"; | |
941 | |
942 return $.plot.formatDate(d, fmt, axisOptions.monthNames); | |
943 }; | |
944 } | |
945 else { | |
946 // pretty rounding of base-10 numbers | |
947 var maxDec = axisOptions.tickDecimals; | |
948 var dec = -Math.floor(Math.log(delta) / Math.LN10); | |
949 if (maxDec != null && dec > maxDec) | |
950 dec = maxDec; | |
951 | |
952 magn = Math.pow(10, -dec); | |
953 norm = delta / magn; // norm is between 1.0 and 10.0 | |
954 | |
955 if (norm < 1.5) | |
956 size = 1; | |
957 else if (norm < 3) { | |
958 size = 2; | |
959 // special case for 2.5, requires an extra decimal | |
960 if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { | |
961 size = 2.5; | |
962 ++dec; | |
963 } | |
964 } | |
965 else if (norm < 7.5) | |
966 size = 5; | |
967 else | |
968 size = 10; | |
969 | |
970 size *= magn; | |
971 | |
972 if (axisOptions.minTickSize != null && size < axisOptions.minTickSize) | |
973 size = axisOptions.minTickSize; | |
974 | |
975 if (axisOptions.tickSize != null) | |
976 size = axisOptions.tickSize; | |
977 | |
978 axis.tickDecimals = Math.max(0, (maxDec != null) ? maxDec : dec); | |
979 | |
980 generator = function (axis) { | |
981 var ticks = []; | |
982 | |
983 // spew out all possible ticks | |
984 var start = floorInBase(axis.min, axis.tickSize), | |
985 i = 0, v = Number.NaN, prev; | |
986 do { | |
987 prev = v; | |
988 v = start + i * axis.tickSize; | |
989 ticks.push({ v: v, label: axis.tickFormatter(v, axis) }); | |
990 ++i; | |
991 } while (v < axis.max && v != prev); | |
992 return ticks; | |
993 }; | |
994 | |
995 formatter = function (v, axis) { | |
996 return v.toFixed(axis.tickDecimals); | |
997 }; | |
998 } | |
999 | |
1000 axis.tickSize = unit ? [size, unit] : size; | |
1001 axis.tickGenerator = generator; | |
1002 if ($.isFunction(axisOptions.tickFormatter)) | |
1003 axis.tickFormatter = function (v, axis) { return "" + axisOptions.tickFormatter(v, axis); }; | |
1004 else | |
1005 axis.tickFormatter = formatter; | |
1006 } | |
1007 | |
1008 function setTicks(axis, axisOptions) { | |
1009 axis.ticks = []; | |
1010 | |
1011 if (!axis.used) | |
1012 return; | |
1013 | |
1014 if (axisOptions.ticks == null) | |
1015 axis.ticks = axis.tickGenerator(axis); | |
1016 else if (typeof axisOptions.ticks == "number") { | |
1017 if (axisOptions.ticks > 0) | |
1018 axis.ticks = axis.tickGenerator(axis); | |
1019 } | |
1020 else if (axisOptions.ticks) { | |
1021 var ticks = axisOptions.ticks; | |
1022 | |
1023 if ($.isFunction(ticks)) | |
1024 // generate the ticks | |
1025 ticks = ticks({ min: axis.min, max: axis.max }); | |
1026 | |
1027 // clean up the user-supplied ticks, copy them over | |
1028 var i, v; | |
1029 for (i = 0; i < ticks.length; ++i) { | |
1030 var label = null; | |
1031 var t = ticks[i]; | |
1032 if (typeof t == "object") { | |
1033 v = t[0]; | |
1034 if (t.length > 1) | |
1035 label = t[1]; | |
1036 } | |
1037 else | |
1038 v = t; | |
1039 if (label == null) | |
1040 label = axis.tickFormatter(v, axis); | |
1041 axis.ticks[i] = { v: v, label: label }; | |
1042 } | |
1043 } | |
1044 | |
1045 if (axisOptions.autoscaleMargin != null && axis.ticks.length > 0) { | |
1046 // snap to ticks | |
1047 if (axisOptions.min == null) | |
1048 axis.min = Math.min(axis.min, axis.ticks[0].v); | |
1049 if (axisOptions.max == null && axis.ticks.length > 1) | |
1050 axis.max = Math.max(axis.max, axis.ticks[axis.ticks.length - 1].v); | |
1051 } | |
1052 } | |
1053 | |
1054 function draw() { | |
1055 ctx.clearRect(0, 0, canvasWidth, canvasHeight); | |
1056 | |
1057 var grid = options.grid; | |
1058 | |
1059 if (grid.show && !grid.aboveData) | |
1060 drawGrid(); | |
1061 | |
1062 for (var i = 0; i < series.length; ++i) | |
1063 drawSeries(series[i]); | |
1064 | |
1065 executeHooks(hooks.draw, [ctx]); | |
1066 | |
1067 if (grid.show && grid.aboveData) | |
1068 drawGrid(); | |
1069 } | |
1070 | |
1071 function extractRange(ranges, coord) { | |
1072 var firstAxis = coord + "axis", | |
1073 secondaryAxis = coord + "2axis", | |
1074 axis, from, to, reverse; | |
1075 | |
1076 if (ranges[firstAxis]) { | |
1077 axis = axes[firstAxis]; | |
1078 from = ranges[firstAxis].from; | |
1079 to = ranges[firstAxis].to; | |
1080 } | |
1081 else if (ranges[secondaryAxis]) { | |
1082 axis = axes[secondaryAxis]; | |
1083 from = ranges[secondaryAxis].from; | |
1084 to = ranges[secondaryAxis].to; | |
1085 } | |
1086 else { | |
1087 // backwards-compat stuff - to be removed in future | |
1088 axis = axes[firstAxis]; | |
1089 from = ranges[coord + "1"]; | |
1090 to = ranges[coord + "2"]; | |
1091 } | |
1092 | |
1093 // auto-reverse as an added bonus | |
1094 if (from != null && to != null && from > to) | |
1095 return { from: to, to: from, axis: axis }; | |
1096 | |
1097 return { from: from, to: to, axis: axis }; | |
1098 } | |
1099 | |
1100 function drawGrid() { | |
1101 var i; | |
1102 | |
1103 ctx.save(); | |
1104 ctx.translate(plotOffset.left, plotOffset.top); | |
1105 | |
1106 // draw background, if any | |
1107 if (options.grid.backgroundColor) { | |
1108 ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); | |
1109 ctx.fillRect(0, 0, plotWidth, plotHeight); | |
1110 } | |
1111 | |
1112 // draw markings | |
1113 var markings = options.grid.markings; | |
1114 if (markings) { | |
1115 if ($.isFunction(markings)) | |
1116 // xmin etc. are backwards-compatible, to be removed in future | |
1117 markings = markings({ xmin: axes.xaxis.min, xmax: axes.xaxis.max, ymin: axes.yaxis.min, ymax: axes.yaxis.max, xaxis: axes.xaxis, yaxis: axes.yaxis, x2axis: axes.x2axis, y2axis: axes.y2axis }); | |
1118 | |
1119 for (i = 0; i < markings.length; ++i) { | |
1120 var m = markings[i], | |
1121 xrange = extractRange(m, "x"), | |
1122 yrange = extractRange(m, "y"); | |
1123 | |
1124 // fill in missing | |
1125 if (xrange.from == null) | |
1126 xrange.from = xrange.axis.min; | |
1127 if (xrange.to == null) | |
1128 xrange.to = xrange.axis.max; | |
1129 if (yrange.from == null) | |
1130 yrange.from = yrange.axis.min; | |
1131 if (yrange.to == null) | |
1132 yrange.to = yrange.axis.max; | |
1133 | |
1134 // clip | |
1135 if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || | |
1136 yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) | |
1137 continue; | |
1138 | |
1139 xrange.from = Math.max(xrange.from, xrange.axis.min); | |
1140 xrange.to = Math.min(xrange.to, xrange.axis.max); | |
1141 yrange.from = Math.max(yrange.from, yrange.axis.min); | |
1142 yrange.to = Math.min(yrange.to, yrange.axis.max); | |
1143 | |
1144 if (xrange.from == xrange.to && yrange.from == yrange.to) | |
1145 continue; | |
1146 | |
1147 // then draw | |
1148 xrange.from = xrange.axis.p2c(xrange.from); | |
1149 xrange.to = xrange.axis.p2c(xrange.to); | |
1150 yrange.from = yrange.axis.p2c(yrange.from); | |
1151 yrange.to = yrange.axis.p2c(yrange.to); | |
1152 | |
1153 if (xrange.from == xrange.to || yrange.from == yrange.to) { | |
1154 // draw line | |
1155 ctx.beginPath(); | |
1156 ctx.strokeStyle = m.color || options.grid.markingsColor; | |
1157 ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth; | |
1158 //ctx.moveTo(Math.floor(xrange.from), yrange.from); | |
1159 //ctx.lineTo(Math.floor(xrange.to), yrange.to); | |
1160 ctx.moveTo(xrange.from, yrange.from); | |
1161 ctx.lineTo(xrange.to, yrange.to); | |
1162 ctx.stroke(); | |
1163 } | |
1164 else { | |
1165 // fill area | |
1166 ctx.fillStyle = m.color || options.grid.markingsColor; | |
1167 ctx.fillRect(xrange.from, yrange.to, | |
1168 xrange.to - xrange.from, | |
1169 yrange.from - yrange.to); | |
1170 } | |
1171 } | |
1172 } | |
1173 | |
1174 // draw the inner grid | |
1175 ctx.lineWidth = 1; | |
1176 ctx.strokeStyle = options.grid.tickColor; | |
1177 ctx.beginPath(); | |
1178 var v, axis = axes.xaxis; | |
1179 for (i = 0; i < axis.ticks.length; ++i) { | |
1180 v = axis.ticks[i].v; | |
1181 if (v <= axis.min || v >= axes.xaxis.max) | |
1182 continue; // skip those lying on the axes | |
1183 | |
1184 ctx.moveTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, 0); | |
1185 ctx.lineTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, plotHeight); | |
1186 } | |
1187 | |
1188 axis = axes.yaxis; | |
1189 for (i = 0; i < axis.ticks.length; ++i) { | |
1190 v = axis.ticks[i].v; | |
1191 if (v <= axis.min || v >= axis.max) | |
1192 continue; | |
1193 | |
1194 ctx.moveTo(0, Math.floor(axis.p2c(v)) + ctx.lineWidth/2); | |
1195 ctx.lineTo(plotWidth, Math.floor(axis.p2c(v)) + ctx.lineWidth/2); | |
1196 } | |
1197 | |
1198 axis = axes.x2axis; | |
1199 for (i = 0; i < axis.ticks.length; ++i) { | |
1200 v = axis.ticks[i].v; | |
1201 if (v <= axis.min || v >= axis.max) | |
1202 continue; | |
1203 | |
1204 ctx.moveTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, -5); | |
1205 ctx.lineTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, 5); | |
1206 } | |
1207 | |
1208 axis = axes.y2axis; | |
1209 for (i = 0; i < axis.ticks.length; ++i) { | |
1210 v = axis.ticks[i].v; | |
1211 if (v <= axis.min || v >= axis.max) | |
1212 continue; | |
1213 | |
1214 ctx.moveTo(plotWidth-5, Math.floor(axis.p2c(v)) + ctx.lineWidth/2); | |
1215 ctx.lineTo(plotWidth+5, Math.floor(axis.p2c(v)) + ctx.lineWidth/2); | |
1216 } | |
1217 | |
1218 ctx.stroke(); | |
1219 | |
1220 if (options.grid.borderWidth) { | |
1221 // draw border | |
1222 var bw = options.grid.borderWidth; | |
1223 ctx.lineWidth = bw; | |
1224 ctx.strokeStyle = options.grid.borderColor; | |
1225 ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); | |
1226 } | |
1227 | |
1228 ctx.restore(); | |
1229 } | |
1230 | |
1231 function insertLabels() { | |
1232 placeholder.find(".tickLabels").remove(); | |
1233 | |
1234 var html = ['<div class="tickLabels" style="font-size:smaller;color:' + options.grid.color + '">']; | |
1235 | |
1236 function addLabels(axis, labelGenerator) { | |
1237 for (var i = 0; i < axis.ticks.length; ++i) { | |
1238 var tick = axis.ticks[i]; | |
1239 if (!tick.label || tick.v < axis.min || tick.v > axis.max) | |
1240 continue; | |
1241 html.push(labelGenerator(tick, axis)); | |
1242 } | |
1243 } | |
1244 | |
1245 var margin = options.grid.labelMargin + options.grid.borderWidth; | |
1246 | |
1247 addLabels(axes.xaxis, function (tick, axis) { | |
1248 return '<div style="position:absolute;top:' + (plotOffset.top + plotHeight + margin) + 'px;left:' + Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2) + 'px;width:' + axis.labelWidth + 'px;text-align:center" class="tickLabel">' + tick.label + "</div>"; | |
1249 }); | |
1250 | |
1251 | |
1252 addLabels(axes.yaxis, function (tick, axis) { | |
1253 return '<div style="position:absolute;top:' + Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2) + 'px;right:' + (plotOffset.right + plotWidth + margin) + 'px;width:' + axis.labelWidth + 'px;text-align:right" class="tickLabel">' + tick.label + "</div>"; | |
1254 }); | |
1255 | |
1256 addLabels(axes.x2axis, function (tick, axis) { | |
1257 return '<div style="position:absolute;bottom:' + (plotOffset.bottom + plotHeight + margin) + 'px;left:' + Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2) + 'px;width:' + axis.labelWidth + 'px;text-align:center" class="tickLabel">' + tick.label + "</div>"; | |
1258 }); | |
1259 | |
1260 addLabels(axes.y2axis, function (tick, axis) { | |
1261 return '<div style="position:absolute;top:' + Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2) + 'px;left:' + (plotOffset.left + plotWidth + margin) +'px;width:' + axis.labelWidth + 'px;text-align:left" class="tickLabel">' + tick.label + "</div>"; | |
1262 }); | |
1263 | |
1264 html.push('</div>'); | |
1265 | |
1266 placeholder.append(html.join("")); | |
1267 } | |
1268 | |
1269 function drawSeries(series) { | |
1270 if (series.lines.show) | |
1271 drawSeriesLines(series); | |
1272 if (series.bars.show) | |
1273 drawSeriesBars(series); | |
1274 if (series.points.show) | |
1275 drawSeriesPoints(series); | |
1276 } | |
1277 | |
1278 function drawSeriesLines(series) { | |
1279 function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { | |
1280 var points = datapoints.points, | |
1281 ps = datapoints.pointsize, | |
1282 prevx = null, prevy = null; | |
1283 | |
1284 ctx.beginPath(); | |
1285 for (var i = ps; i < points.length; i += ps) { | |
1286 var x1 = points[i - ps], y1 = points[i - ps + 1], | |
1287 x2 = points[i], y2 = points[i + 1]; | |
1288 | |
1289 if (x1 == null || x2 == null) | |
1290 continue; | |
1291 | |
1292 // clip with ymin | |
1293 if (y1 <= y2 && y1 < axisy.min) { | |
1294 if (y2 < axisy.min) | |
1295 continue; // line segment is outside | |
1296 // compute new intersection point | |
1297 x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; | |
1298 y1 = axisy.min; | |
1299 } | |
1300 else if (y2 <= y1 && y2 < axisy.min) { | |
1301 if (y1 < axisy.min) | |
1302 continue; | |
1303 x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; | |
1304 y2 = axisy.min; | |
1305 } | |
1306 | |
1307 // clip with ymax | |
1308 if (y1 >= y2 && y1 > axisy.max) { | |
1309 if (y2 > axisy.max) | |
1310 continue; | |
1311 x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; | |
1312 y1 = axisy.max; | |
1313 } | |
1314 else if (y2 >= y1 && y2 > axisy.max) { | |
1315 if (y1 > axisy.max) | |
1316 continue; | |
1317 x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; | |
1318 y2 = axisy.max; | |
1319 } | |
1320 | |
1321 // clip with xmin | |
1322 if (x1 <= x2 && x1 < axisx.min) { | |
1323 if (x2 < axisx.min) | |
1324 continue; | |
1325 y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; | |
1326 x1 = axisx.min; | |
1327 } | |
1328 else if (x2 <= x1 && x2 < axisx.min) { | |
1329 if (x1 < axisx.min) | |
1330 continue; | |
1331 y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; | |
1332 x2 = axisx.min; | |
1333 } | |
1334 | |
1335 // clip with xmax | |
1336 if (x1 >= x2 && x1 > axisx.max) { | |
1337 if (x2 > axisx.max) | |
1338 continue; | |
1339 y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; | |
1340 x1 = axisx.max; | |
1341 } | |
1342 else if (x2 >= x1 && x2 > axisx.max) { | |
1343 if (x1 > axisx.max) | |
1344 continue; | |
1345 y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; | |
1346 x2 = axisx.max; | |
1347 } | |
1348 | |
1349 if (x1 != prevx || y1 != prevy) | |
1350 ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); | |
1351 | |
1352 prevx = x2; | |
1353 prevy = y2; | |
1354 ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); | |
1355 } | |
1356 ctx.stroke(); | |
1357 } | |
1358 | |
1359 function plotLineArea(datapoints, axisx, axisy) { | |
1360 var points = datapoints.points, | |
1361 ps = datapoints.pointsize, | |
1362 bottom = Math.min(Math.max(0, axisy.min), axisy.max), | |
1363 top, lastX = 0, areaOpen = false; | |
1364 | |
1365 for (var i = ps; i < points.length; i += ps) { | |
1366 var x1 = points[i - ps], y1 = points[i - ps + 1], | |
1367 x2 = points[i], y2 = points[i + 1]; | |
1368 | |
1369 if (areaOpen && x1 != null && x2 == null) { | |
1370 // close area | |
1371 ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom)); | |
1372 ctx.fill(); | |
1373 areaOpen = false; | |
1374 continue; | |
1375 } | |
1376 | |
1377 if (x1 == null || x2 == null) | |
1378 continue; | |
1379 | |
1380 // clip x values | |
1381 | |
1382 // clip with xmin | |
1383 if (x1 <= x2 && x1 < axisx.min) { | |
1384 if (x2 < axisx.min) | |
1385 continue; | |
1386 y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; | |
1387 x1 = axisx.min; | |
1388 } | |
1389 else if (x2 <= x1 && x2 < axisx.min) { | |
1390 if (x1 < axisx.min) | |
1391 continue; | |
1392 y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; | |
1393 x2 = axisx.min; | |
1394 } | |
1395 | |
1396 // clip with xmax | |
1397 if (x1 >= x2 && x1 > axisx.max) { | |
1398 if (x2 > axisx.max) | |
1399 continue; | |
1400 y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; | |
1401 x1 = axisx.max; | |
1402 } | |
1403 else if (x2 >= x1 && x2 > axisx.max) { | |
1404 if (x1 > axisx.max) | |
1405 continue; | |
1406 y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; | |
1407 x2 = axisx.max; | |
1408 } | |
1409 | |
1410 if (!areaOpen) { | |
1411 // open area | |
1412 ctx.beginPath(); | |
1413 ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); | |
1414 areaOpen = true; | |
1415 } | |
1416 | |
1417 // now first check the case where both is outside | |
1418 if (y1 >= axisy.max && y2 >= axisy.max) { | |
1419 ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); | |
1420 ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); | |
1421 lastX = x2; | |
1422 continue; | |
1423 } | |
1424 else if (y1 <= axisy.min && y2 <= axisy.min) { | |
1425 ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); | |
1426 ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); | |
1427 lastX = x2; | |
1428 continue; | |
1429 } | |
1430 | |
1431 // else it's a bit more complicated, there might | |
1432 // be two rectangles and two triangles we need to fill | |
1433 // in; to find these keep track of the current x values | |
1434 var x1old = x1, x2old = x2; | |
1435 | |
1436 // and clip the y values, without shortcutting | |
1437 | |
1438 // clip with ymin | |
1439 if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { | |
1440 x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; | |
1441 y1 = axisy.min; | |
1442 } | |
1443 else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { | |
1444 x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; | |
1445 y2 = axisy.min; | |
1446 } | |
1447 | |
1448 // clip with ymax | |
1449 if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { | |
1450 x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; | |
1451 y1 = axisy.max; | |
1452 } | |
1453 else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { | |
1454 x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; | |
1455 y2 = axisy.max; | |
1456 } | |
1457 | |
1458 | |
1459 // if the x value was changed we got a rectangle | |
1460 // to fill | |
1461 if (x1 != x1old) { | |
1462 if (y1 <= axisy.min) | |
1463 top = axisy.min; | |
1464 else | |
1465 top = axisy.max; | |
1466 | |
1467 ctx.lineTo(axisx.p2c(x1old), axisy.p2c(top)); | |
1468 ctx.lineTo(axisx.p2c(x1), axisy.p2c(top)); | |
1469 } | |
1470 | |
1471 // fill the triangles | |
1472 ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); | |
1473 ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); | |
1474 | |
1475 // fill the other rectangle if it's there | |
1476 if (x2 != x2old) { | |
1477 if (y2 <= axisy.min) | |
1478 top = axisy.min; | |
1479 else | |
1480 top = axisy.max; | |
1481 | |
1482 ctx.lineTo(axisx.p2c(x2), axisy.p2c(top)); | |
1483 ctx.lineTo(axisx.p2c(x2old), axisy.p2c(top)); | |
1484 } | |
1485 | |
1486 lastX = Math.max(x2, x2old); | |
1487 } | |
1488 | |
1489 if (areaOpen) { | |
1490 ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom)); | |
1491 ctx.fill(); | |
1492 } | |
1493 } | |
1494 | |
1495 ctx.save(); | |
1496 ctx.translate(plotOffset.left, plotOffset.top); | |
1497 ctx.lineJoin = "round"; | |
1498 | |
1499 var lw = series.lines.lineWidth, | |
1500 sw = series.shadowSize; | |
1501 // FIXME: consider another form of shadow when filling is turned on | |
1502 if (lw > 0 && sw > 0) { | |
1503 // draw shadow as a thick and thin line with transparency | |
1504 ctx.lineWidth = sw; | |
1505 ctx.strokeStyle = "rgba(0,0,0,0.1)"; | |
1506 // position shadow at angle from the mid of line | |
1507 var angle = Math.PI/18; | |
1508 plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); | |
1509 ctx.lineWidth = sw/2; | |
1510 plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); | |
1511 } | |
1512 | |
1513 ctx.lineWidth = lw; | |
1514 ctx.strokeStyle = series.color; | |
1515 var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); | |
1516 if (fillStyle) { | |
1517 ctx.fillStyle = fillStyle; | |
1518 plotLineArea(series.datapoints, series.xaxis, series.yaxis); | |
1519 } | |
1520 | |
1521 if (lw > 0) | |
1522 plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); | |
1523 ctx.restore(); | |
1524 } | |
1525 | |
1526 function drawSeriesPoints(series) { | |
1527 function plotPoints(datapoints, radius, fillStyle, offset, circumference, axisx, axisy) { | |
1528 var points = datapoints.points, ps = datapoints.pointsize; | |
1529 | |
1530 for (var i = 0; i < points.length; i += ps) { | |
1531 var x = points[i], y = points[i + 1]; | |
1532 if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) | |
1533 continue; | |
1534 | |
1535 ctx.beginPath(); | |
1536 ctx.arc(axisx.p2c(x), axisy.p2c(y) + offset, radius, 0, circumference, false); | |
1537 if (fillStyle) { | |
1538 ctx.fillStyle = fillStyle; | |
1539 ctx.fill(); | |
1540 } | |
1541 ctx.stroke(); | |
1542 } | |
1543 } | |
1544 | |
1545 ctx.save(); | |
1546 ctx.translate(plotOffset.left, plotOffset.top); | |
1547 | |
1548 var lw = series.lines.lineWidth, | |
1549 sw = series.shadowSize, | |
1550 radius = series.points.radius; | |
1551 if (lw > 0 && sw > 0) { | |
1552 // draw shadow in two steps | |
1553 var w = sw / 2; | |
1554 ctx.lineWidth = w; | |
1555 ctx.strokeStyle = "rgba(0,0,0,0.1)"; | |
1556 plotPoints(series.datapoints, radius, null, w + w/2, Math.PI, | |
1557 series.xaxis, series.yaxis); | |
1558 | |
1559 ctx.strokeStyle = "rgba(0,0,0,0.2)"; | |
1560 plotPoints(series.datapoints, radius, null, w/2, Math.PI, | |
1561 series.xaxis, series.yaxis); | |
1562 } | |
1563 | |
1564 ctx.lineWidth = lw; | |
1565 ctx.strokeStyle = series.color; | |
1566 plotPoints(series.datapoints, radius, | |
1567 getFillStyle(series.points, series.color), 0, 2 * Math.PI, | |
1568 series.xaxis, series.yaxis); | |
1569 ctx.restore(); | |
1570 } | |
1571 | |
1572 function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal) { | |
1573 var left, right, bottom, top, | |
1574 drawLeft, drawRight, drawTop, drawBottom, | |
1575 tmp; | |
1576 | |
1577 if (horizontal) { | |
1578 drawBottom = drawRight = drawTop = true; | |
1579 drawLeft = false; | |
1580 left = b; | |
1581 right = x; | |
1582 top = y + barLeft; | |
1583 bottom = y + barRight; | |
1584 | |
1585 // account for negative bars | |
1586 if (right < left) { | |
1587 tmp = right; | |
1588 right = left; | |
1589 left = tmp; | |
1590 drawLeft = true; | |
1591 drawRight = false; | |
1592 } | |
1593 } | |
1594 else { | |
1595 drawLeft = drawRight = drawTop = true; | |
1596 drawBottom = false; | |
1597 left = x + barLeft; | |
1598 right = x + barRight; | |
1599 bottom = b; | |
1600 top = y; | |
1601 | |
1602 // account for negative bars | |
1603 if (top < bottom) { | |
1604 tmp = top; | |
1605 top = bottom; | |
1606 bottom = tmp; | |
1607 drawBottom = true; | |
1608 drawTop = false; | |
1609 } | |
1610 } | |
1611 | |
1612 // clip | |
1613 if (right < axisx.min || left > axisx.max || | |
1614 top < axisy.min || bottom > axisy.max) | |
1615 return; | |
1616 | |
1617 if (left < axisx.min) { | |
1618 left = axisx.min; | |
1619 drawLeft = false; | |
1620 } | |
1621 | |
1622 if (right > axisx.max) { | |
1623 right = axisx.max; | |
1624 drawRight = false; | |
1625 } | |
1626 | |
1627 if (bottom < axisy.min) { | |
1628 bottom = axisy.min; | |
1629 drawBottom = false; | |
1630 } | |
1631 | |
1632 if (top > axisy.max) { | |
1633 top = axisy.max; | |
1634 drawTop = false; | |
1635 } | |
1636 | |
1637 left = axisx.p2c(left); | |
1638 bottom = axisy.p2c(bottom); | |
1639 right = axisx.p2c(right); | |
1640 top = axisy.p2c(top); | |
1641 | |
1642 // fill the bar | |
1643 if (fillStyleCallback) { | |
1644 c.beginPath(); | |
1645 c.moveTo(left, bottom); | |
1646 c.lineTo(left, top); | |
1647 c.lineTo(right, top); | |
1648 c.lineTo(right, bottom); | |
1649 c.fillStyle = fillStyleCallback(bottom, top); | |
1650 c.fill(); | |
1651 } | |
1652 | |
1653 // draw outline | |
1654 if (drawLeft || drawRight || drawTop || drawBottom) { | |
1655 c.beginPath(); | |
1656 | |
1657 // FIXME: inline moveTo is buggy with excanvas | |
1658 c.moveTo(left, bottom + offset); | |
1659 if (drawLeft) | |
1660 c.lineTo(left, top + offset); | |
1661 else | |
1662 c.moveTo(left, top + offset); | |
1663 if (drawTop) | |
1664 c.lineTo(right, top + offset); | |
1665 else | |
1666 c.moveTo(right, top + offset); | |
1667 if (drawRight) | |
1668 c.lineTo(right, bottom + offset); | |
1669 else | |
1670 c.moveTo(right, bottom + offset); | |
1671 if (drawBottom) | |
1672 c.lineTo(left, bottom + offset); | |
1673 else | |
1674 c.moveTo(left, bottom + offset); | |
1675 c.stroke(); | |
1676 } | |
1677 } | |
1678 | |
1679 function drawSeriesBars(series) { | |
1680 function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) { | |
1681 var points = datapoints.points, ps = datapoints.pointsize; | |
1682 | |
1683 for (var i = 0; i < points.length; i += ps) { | |
1684 if (points[i] == null) | |
1685 continue; | |
1686 drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal); | |
1687 } | |
1688 } | |
1689 | |
1690 ctx.save(); | |
1691 ctx.translate(plotOffset.left, plotOffset.top); | |
1692 | |
1693 // FIXME: figure out a way to add shadows (for instance along the right edge) | |
1694 ctx.lineWidth = series.bars.lineWidth; | |
1695 ctx.strokeStyle = series.color; | |
1696 var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2; | |
1697 var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; | |
1698 plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis); | |
1699 ctx.restore(); | |
1700 } | |
1701 | |
1702 function getFillStyle(filloptions, seriesColor, bottom, top) { | |
1703 var fill = filloptions.fill; | |
1704 if (!fill) | |
1705 return null; | |
1706 | |
1707 if (filloptions.fillColor) | |
1708 return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); | |
1709 | |
1710 var c = $.color.parse(seriesColor); | |
1711 c.a = typeof fill == "number" ? fill : 0.4; | |
1712 c.normalize(); | |
1713 return c.toString(); | |
1714 } | |
1715 | |
1716 function insertLegend() { | |
1717 placeholder.find(".legend").remove(); | |
1718 | |
1719 if (!options.legend.show) | |
1720 return; | |
1721 | |
1722 var fragments = [], rowStarted = false, | |
1723 lf = options.legend.labelFormatter, s, label; | |
1724 for (i = 0; i < series.length; ++i) { | |
1725 s = series[i]; | |
1726 label = s.label; | |
1727 if (!label) | |
1728 continue; | |
1729 | |
1730 if (i % options.legend.noColumns == 0) { | |
1731 if (rowStarted) | |
1732 fragments.push('</tr>'); | |
1733 fragments.push('<tr>'); | |
1734 rowStarted = true; | |
1735 } | |
1736 | |
1737 if (lf) | |
1738 label = lf(label, s); | |
1739 | |
1740 fragments.push( | |
1741 '<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>' + | |
1742 '<td class="legendLabel">' + label + '</td>'); | |
1743 } | |
1744 if (rowStarted) | |
1745 fragments.push('</tr>'); | |
1746 | |
1747 if (fragments.length == 0) | |
1748 return; | |
1749 | |
1750 var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>'; | |
1751 if (options.legend.container != null) | |
1752 $(options.legend.container).html(table); | |
1753 else { | |
1754 var pos = "", | |
1755 p = options.legend.position, | |
1756 m = options.legend.margin; | |
1757 if (m[0] == null) | |
1758 m = [m, m]; | |
1759 if (p.charAt(0) == "n") | |
1760 pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; | |
1761 else if (p.charAt(0) == "s") | |
1762 pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; | |
1763 if (p.charAt(1) == "e") | |
1764 pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; | |
1765 else if (p.charAt(1) == "w") | |
1766 pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; | |
1767 var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder); | |
1768 if (options.legend.backgroundOpacity != 0.0) { | |
1769 // put in the transparent background | |
1770 // separately to avoid blended labels and | |
1771 // label boxes | |
1772 var c = options.legend.backgroundColor; | |
1773 if (c == null) { | |
1774 c = options.grid.backgroundColor; | |
1775 if (c && typeof c == "string") | |
1776 c = $.color.parse(c); | |
1777 else | |
1778 c = $.color.extract(legend, 'background-color'); | |
1779 c.a = 1; | |
1780 c = c.toString(); | |
1781 } | |
1782 var div = legend.children(); | |
1783 $('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity); | |
1784 } | |
1785 } | |
1786 } | |
1787 | |
1788 | |
1789 // interactive features | |
1790 | |
1791 var highlights = [], | |
1792 redrawTimeout = null; | |
1793 | |
1794 // returns the data item the mouse is over, or null if none is found | |
1795 function findNearbyItem(mouseX, mouseY, seriesFilter) { | |
1796 var maxDistance = options.grid.mouseActiveRadius, | |
1797 smallestDistance = maxDistance * maxDistance + 1, | |
1798 item = null, foundPoint = false, i, j; | |
1799 | |
1800 for (i = 0; i < series.length; ++i) { | |
1801 if (!seriesFilter(series[i])) | |
1802 continue; | |
1803 | |
1804 var s = series[i], | |
1805 axisx = s.xaxis, | |
1806 axisy = s.yaxis, | |
1807 points = s.datapoints.points, | |
1808 ps = s.datapoints.pointsize, | |
1809 mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster | |
1810 my = axisy.c2p(mouseY), | |
1811 maxx = maxDistance / axisx.scale, | |
1812 maxy = maxDistance / axisy.scale; | |
1813 | |
1814 if (s.lines.show || s.points.show) { | |
1815 for (j = 0; j < points.length; j += ps) { | |
1816 var x = points[j], y = points[j + 1]; | |
1817 if (x == null) | |
1818 continue; | |
1819 | |
1820 // For points and lines, the cursor must be within a | |
1821 // certain distance to the data point | |
1822 if (x - mx > maxx || x - mx < -maxx || | |
1823 y - my > maxy || y - my < -maxy) | |
1824 continue; | |
1825 | |
1826 // We have to calculate distances in pixels, not in | |
1827 // data units, because the scales of the axes may be different | |
1828 var dx = Math.abs(axisx.p2c(x) - mouseX), | |
1829 dy = Math.abs(axisy.p2c(y) - mouseY), | |
1830 dist = dx * dx + dy * dy; // we save the sqrt | |
1831 | |
1832 // use <= to ensure last point takes precedence | |
1833 // (last generally means on top of) | |
1834 if (dist <= smallestDistance) { | |
1835 smallestDistance = dist; | |
1836 item = [i, j / ps]; | |
1837 } | |
1838 } | |
1839 } | |
1840 | |
1841 if (s.bars.show && !item) { // no other point can be nearby | |
1842 var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2, | |
1843 barRight = barLeft + s.bars.barWidth; | |
1844 | |
1845 for (j = 0; j < points.length; j += ps) { | |
1846 var x = points[j], y = points[j + 1], b = points[j + 2]; | |
1847 if (x == null) | |
1848 continue; | |
1849 | |
1850 // for a bar graph, the cursor must be inside the bar | |
1851 if (series[i].bars.horizontal ? | |
1852 (mx <= Math.max(b, x) && mx >= Math.min(b, x) && | |
1853 my >= y + barLeft && my <= y + barRight) : | |
1854 (mx >= x + barLeft && mx <= x + barRight && | |
1855 my >= Math.min(b, y) && my <= Math.max(b, y))) | |
1856 item = [i, j / ps]; | |
1857 } | |
1858 } | |
1859 } | |
1860 | |
1861 if (item) { | |
1862 i = item[0]; | |
1863 j = item[1]; | |
1864 ps = series[i].datapoints.pointsize; | |
1865 | |
1866 return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), | |
1867 dataIndex: j, | |
1868 series: series[i], | |
1869 seriesIndex: i }; | |
1870 } | |
1871 | |
1872 return null; | |
1873 } | |
1874 | |
1875 function onMouseMove(e) { | |
1876 if (options.grid.hoverable) | |
1877 triggerClickHoverEvent("plothover", e, | |
1878 function (s) { return s["hoverable"] != false; }); | |
1879 } | |
1880 | |
1881 function onClick(e) { | |
1882 triggerClickHoverEvent("plotclick", e, | |
1883 function (s) { return s["clickable"] != false; }); | |
1884 } | |
1885 | |
1886 // trigger click or hover event (they send the same parameters | |
1887 // so we share their code) | |
1888 function triggerClickHoverEvent(eventname, event, seriesFilter) { | |
1889 var offset = eventHolder.offset(), | |
1890 pos = { pageX: event.pageX, pageY: event.pageY }, | |
1891 canvasX = event.pageX - offset.left - plotOffset.left, | |
1892 canvasY = event.pageY - offset.top - plotOffset.top; | |
1893 | |
1894 if (axes.xaxis.used) | |
1895 pos.x = axes.xaxis.c2p(canvasX); | |
1896 if (axes.yaxis.used) | |
1897 pos.y = axes.yaxis.c2p(canvasY); | |
1898 if (axes.x2axis.used) | |
1899 pos.x2 = axes.x2axis.c2p(canvasX); | |
1900 if (axes.y2axis.used) | |
1901 pos.y2 = axes.y2axis.c2p(canvasY); | |
1902 | |
1903 var item = findNearbyItem(canvasX, canvasY, seriesFilter); | |
1904 | |
1905 if (item) { | |
1906 // fill in mouse pos for any listeners out there | |
1907 item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left); | |
1908 item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top); | |
1909 } | |
1910 | |
1911 if (options.grid.autoHighlight) { | |
1912 // clear auto-highlights | |
1913 for (var i = 0; i < highlights.length; ++i) { | |
1914 var h = highlights[i]; | |
1915 if (h.auto == eventname && | |
1916 !(item && h.series == item.series && h.point == item.datapoint)) | |
1917 unhighlight(h.series, h.point); | |
1918 } | |
1919 | |
1920 if (item) | |
1921 highlight(item.series, item.datapoint, eventname); | |
1922 } | |
1923 | |
1924 placeholder.trigger(eventname, [ pos, item ]); | |
1925 } | |
1926 | |
1927 function triggerRedrawOverlay() { | |
1928 if (!redrawTimeout) | |
1929 redrawTimeout = setTimeout(drawOverlay, 30); | |
1930 } | |
1931 | |
1932 function drawOverlay() { | |
1933 redrawTimeout = null; | |
1934 | |
1935 // draw highlights | |
1936 octx.save(); | |
1937 octx.clearRect(0, 0, canvasWidth, canvasHeight); | |
1938 octx.translate(plotOffset.left, plotOffset.top); | |
1939 | |
1940 var i, hi; | |
1941 for (i = 0; i < highlights.length; ++i) { | |
1942 hi = highlights[i]; | |
1943 | |
1944 if (hi.series.bars.show) | |
1945 drawBarHighlight(hi.series, hi.point); | |
1946 else | |
1947 drawPointHighlight(hi.series, hi.point); | |
1948 } | |
1949 octx.restore(); | |
1950 | |
1951 executeHooks(hooks.drawOverlay, [octx]); | |
1952 } | |
1953 | |
1954 function highlight(s, point, auto) { | |
1955 if (typeof s == "number") | |
1956 s = series[s]; | |
1957 | |
1958 if (typeof point == "number") | |
1959 point = s.data[point]; | |
1960 | |
1961 var i = indexOfHighlight(s, point); | |
1962 if (i == -1) { | |
1963 highlights.push({ series: s, point: point, auto: auto }); | |
1964 | |
1965 triggerRedrawOverlay(); | |
1966 } | |
1967 else if (!auto) | |
1968 highlights[i].auto = false; | |
1969 } | |
1970 | |
1971 function unhighlight(s, point) { | |
1972 if (s == null && point == null) { | |
1973 highlights = []; | |
1974 triggerRedrawOverlay(); | |
1975 } | |
1976 | |
1977 if (typeof s == "number") | |
1978 s = series[s]; | |
1979 | |
1980 if (typeof point == "number") | |
1981 point = s.data[point]; | |
1982 | |
1983 var i = indexOfHighlight(s, point); | |
1984 if (i != -1) { | |
1985 highlights.splice(i, 1); | |
1986 | |
1987 triggerRedrawOverlay(); | |
1988 } | |
1989 } | |
1990 | |
1991 function indexOfHighlight(s, p) { | |
1992 for (var i = 0; i < highlights.length; ++i) { | |
1993 var h = highlights[i]; | |
1994 if (h.series == s && h.point[0] == p[0] | |
1995 && h.point[1] == p[1]) | |
1996 return i; | |
1997 } | |
1998 return -1; | |
1999 } | |
2000 | |
2001 function drawPointHighlight(series, point) { | |
2002 var x = point[0], y = point[1], | |
2003 axisx = series.xaxis, axisy = series.yaxis; | |
2004 | |
2005 if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) | |
2006 return; | |
2007 | |
2008 var pointRadius = series.points.radius + series.points.lineWidth / 2; | |
2009 octx.lineWidth = pointRadius; | |
2010 octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString(); | |
2011 var radius = 1.5 * pointRadius; | |
2012 octx.beginPath(); | |
2013 octx.arc(axisx.p2c(x), axisy.p2c(y), radius, 0, 2 * Math.PI, false); | |
2014 octx.stroke(); | |
2015 } | |
2016 | |
2017 function drawBarHighlight(series, point) { | |
2018 octx.lineWidth = series.bars.lineWidth; | |
2019 octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString(); | |
2020 var fillStyle = $.color.parse(series.color).scale('a', 0.5).toString(); | |
2021 var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2; | |
2022 drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, | |
2023 0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal); | |
2024 } | |
2025 | |
2026 function getColorOrGradient(spec, bottom, top, defaultColor) { | |
2027 if (typeof spec == "string") | |
2028 return spec; | |
2029 else { | |
2030 // assume this is a gradient spec; IE currently only | |
2031 // supports a simple vertical gradient properly, so that's | |
2032 // what we support too | |
2033 var gradient = ctx.createLinearGradient(0, top, 0, bottom); | |
2034 | |
2035 for (var i = 0, l = spec.colors.length; i < l; ++i) { | |
2036 var c = spec.colors[i]; | |
2037 if (typeof c != "string") { | |
2038 c = $.color.parse(defaultColor).scale('rgb', c.brightness); | |
2039 c.a *= c.opacity; | |
2040 c = c.toString(); | |
2041 } | |
2042 gradient.addColorStop(i / (l - 1), c); | |
2043 } | |
2044 | |
2045 return gradient; | |
2046 } | |
2047 } | |
2048 } | |
2049 | |
2050 $.plot = function(placeholder, data, options) { | |
2051 var plot = new Plot($(placeholder), data, options, $.plot.plugins); | |
2052 /*var t0 = new Date(); | |
2053 var t1 = new Date(); | |
2054 var tstr = "time used (msecs): " + (t1.getTime() - t0.getTime()) | |
2055 if (window.console) | |
2056 console.log(tstr); | |
2057 else | |
2058 alert(tstr);*/ | |
2059 return plot; | |
2060 }; | |
2061 | |
2062 $.plot.plugins = []; | |
2063 | |
2064 // returns a string with the date d formatted according to fmt | |
2065 $.plot.formatDate = function(d, fmt, monthNames) { | |
2066 var leftPad = function(n) { | |
2067 n = "" + n; | |
2068 return n.length == 1 ? "0" + n : n; | |
2069 }; | |
2070 | |
2071 var r = []; | |
2072 var escape = false; | |
2073 var hours = d.getUTCHours(); | |
2074 var isAM = hours < 12; | |
2075 if (monthNames == null) | |
2076 monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; | |
2077 | |
2078 if (fmt.search(/%p|%P/) != -1) { | |
2079 if (hours > 12) { | |
2080 hours = hours - 12; | |
2081 } else if (hours == 0) { | |
2082 hours = 12; | |
2083 } | |
2084 } | |
2085 for (var i = 0; i < fmt.length; ++i) { | |
2086 var c = fmt.charAt(i); | |
2087 | |
2088 if (escape) { | |
2089 switch (c) { | |
2090 case 'h': c = "" + hours; break; | |
2091 case 'H': c = leftPad(hours); break; | |
2092 case 'M': c = leftPad(d.getUTCMinutes()); break; | |
2093 case 'S': c = leftPad(d.getUTCSeconds()); break; | |
2094 case 'd': c = "" + d.getUTCDate(); break; | |
2095 case 'm': c = "" + (d.getUTCMonth() + 1); break; | |
2096 case 'y': c = "" + d.getUTCFullYear(); break; | |
2097 case 'b': c = "" + monthNames[d.getUTCMonth()]; break; | |
2098 case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; | |
2099 case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; | |
2100 } | |
2101 r.push(c); | |
2102 escape = false; | |
2103 } | |
2104 else { | |
2105 if (c == "%") | |
2106 escape = true; | |
2107 else | |
2108 r.push(c); | |
2109 } | |
2110 } | |
2111 return r.join(""); | |
2112 }; | |
2113 | |
2114 // round to nearby lower multiple of base | |
2115 function floorInBase(n, base) { | |
2116 return base * Math.floor(n / base); | |
2117 } | |
2118 | |
2119 })(jQuery); |