Mercurial > kallithea
comparison pylons_app/public/js/yui.flot.js @ 362:558eb7c5028f rhodecode-0.0.0.8.0
version bump to 0.8
hg app 0.8 new template.
Add yui flot and graph into summary page.
+ various tweeks and patches into look of application
author | Marcin Kuzminski <marcin@python-works.com> |
---|---|
date | Sat, 24 Jul 2010 02:17:48 +0200 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
361:3581656180b7 | 362:558eb7c5028f |
---|---|
1 /** | |
2 \file yui.flot.js | |
3 \brief Javascript plotting library for YUI based on Flot v. 0.5. | |
4 \details | |
5 This file contains a port of Flot for YUI | |
6 | |
7 Copyright (c) 2009 Yahoo! Inc. All rights reserved. The copyrights embodied | |
8 in the content of this file are licenced by Yahoo! Inc. under the BSD (revised) | |
9 open source license. | |
10 | |
11 Requires yahoo-dom-event and datasource which you can get here: | |
12 <script type="text/javascript" src="http://yui.yahooapis.com/combo?2.7.0/build/yahoo-dom-event/yahoo-dom-event.js&2.7.0/build/datasource/datasource-min.js"></script> | |
13 | |
14 Datasource is optional, you only need it if one of your axes has its mode set to "time" | |
15 */ | |
16 | |
17 (function() { | |
18 var L = YAHOO.lang; | |
19 var UA = YAHOO.env.ua; | |
20 var DOM = YAHOO.util.Dom; | |
21 var E = YAHOO.util.Event; | |
22 | |
23 if(!DOM.createElementFromMarkup) { | |
24 DOM.createElementFromMarkup = function(markup) { | |
25 var p=document.createElement('div'); | |
26 p.innerHTML = markup; | |
27 var e = p.firstChild; | |
28 return p.removeChild(e); | |
29 }; | |
30 } | |
31 | |
32 if(!DOM.removeElement) { | |
33 DOM.removeElement = function(el) { | |
34 return el.parentNode.removeChild(el); | |
35 }; | |
36 } | |
37 | |
38 function Plot(target_, data_, options_) { | |
39 // data is on the form: | |
40 // [ series1, series2 ... ] | |
41 // where series is either just the data as [ [x1, y1], [x2, y2], ... ] | |
42 // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label" } | |
43 | |
44 var series = [], | |
45 options = { | |
46 // the color theme used for graphs | |
47 colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], | |
48 locale: "en", | |
49 legend: { | |
50 show: true, | |
51 noColumns: 1, // number of colums in legend table | |
52 labelFormatter: null, // fn: string -> string | |
53 labelBoxBorderColor: "#ccc", // border color for the little label boxes | |
54 container: null, // container (as jQuery object) to put legend in, null means default on top of graph | |
55 position: "ne", // position of default legend container within plot | |
56 margin: 5, // distance from grid edge to default legend container within plot | |
57 backgroundColor: null, // null means auto-detect | |
58 backgroundOpacity: 0.85 // set to 0 to avoid background | |
59 }, | |
60 xaxis: { | |
61 mode: null, // null or "time" | |
62 min: null, // min. value to show, null means set automatically | |
63 max: null, // max. value to show, null means set automatically | |
64 autoscaleMargin: null, // margin in % to add if auto-setting min/max | |
65 ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks | |
66 tickFormatter: null, // fn: number -> string | |
67 label: null, | |
68 labelWidth: null, // size of tick labels in pixels | |
69 labelHeight: null, | |
70 | |
71 scaleType: 'linear', // may be 'linear' or 'log' | |
72 | |
73 // mode specific options | |
74 tickDecimals: null, // no. of decimals, null means auto | |
75 tickSize: null, // number or [number, "unit"] | |
76 minTickSize: null, // number or [number, "unit"] | |
77 timeformat: null // format string to use | |
78 }, | |
79 yaxis: { | |
80 label: null, | |
81 autoscaleMargin: 0.02 | |
82 }, | |
83 x2axis: { | |
84 label: null, | |
85 autoscaleMargin: null | |
86 }, | |
87 y2axis: { | |
88 label: null, | |
89 autoscaleMargin: 0.02 | |
90 }, | |
91 points: { | |
92 show: false, | |
93 radius: 3, | |
94 lineWidth: 2, // in pixels | |
95 fill: true, | |
96 fillColor: "#ffffff" | |
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 }, | |
105 bars: { | |
106 show: false, | |
107 lineWidth: 2, // in pixels | |
108 barWidth: 1, // in units of the x axis | |
109 fill: true, | |
110 fillColor: null, | |
111 align: "left" // or "center" | |
112 }, | |
113 grid: { | |
114 show: true, | |
115 showLines: true, | |
116 color: "#545454", // primary color used for outline and labels | |
117 backgroundColor: null, // null for transparent, else color | |
118 tickColor: "#dddddd", // color used for the ticks | |
119 labelMargin: 5, // in pixels | |
120 labelFontSize: 16, | |
121 borderWidth: 2, // in pixels | |
122 borderColor: null, // set if different from the grid color | |
123 markings: null, // array of ranges or fn: axes -> array of ranges | |
124 markingsColor: "#f4f4f4", | |
125 markingsLineWidth: 2, | |
126 // interactive stuff | |
127 clickable: false, | |
128 hoverable: false, | |
129 autoHighlight: true, // highlight in case mouse is near | |
130 mouseActiveRadius: 10 // how far the mouse can be away to activate an item | |
131 }, | |
132 selection: { | |
133 mode: null, // one of null, "x", "y" or "xy" | |
134 color: "#e8cfac" | |
135 }, | |
136 crosshair: { | |
137 mode: null, // one of null, "x", "y" or "xy", | |
138 color: "#aa0000" | |
139 }, | |
140 shadowSize: 3 | |
141 }, | |
142 canvas = null, // the canvas for the plot itself | |
143 overlay = null, // canvas for interactive stuff on top of plot | |
144 eventHolder = null, // jQuery object that events should be bound to | |
145 ctx = null, octx = null, | |
146 target = DOM.get(target_), | |
147 axes = { xaxis: {}, yaxis: {}, x2axis: {}, y2axis: {} }, | |
148 plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, | |
149 canvasWidth = 0, canvasHeight = 0, | |
150 plotWidth = 0, plotHeight = 0, | |
151 // dedicated to storing data for buggy standard compliance cases | |
152 workarounds = {}; | |
153 | |
154 this.setData = setData; | |
155 this.setupGrid = setupGrid; | |
156 this.draw = draw; | |
157 this.clearSelection = clearSelection; | |
158 this.setSelection = setSelection; | |
159 this.getCanvas = function() { return canvas; }; | |
160 this.getPlotOffset = function() { return plotOffset; }; | |
161 this.getData = function() { return series; }; | |
162 this.getAxes = function() { return axes; }; | |
163 this.setCrosshair = setCrosshair; | |
164 this.clearCrosshair = function () { setCrosshair(null); }; | |
165 this.highlight = highlight; | |
166 this.unhighlight = unhighlight; | |
167 | |
168 // initialize | |
169 parseOptions(options_); | |
170 setData(data_); | |
171 constructCanvas(); | |
172 setupGrid(); | |
173 draw(); | |
174 | |
175 var plot = this; | |
176 | |
177 plot.createEvent('plotclick'); | |
178 plot.createEvent('plothover'); | |
179 plot.createEvent('plotselected'); | |
180 plot.createEvent('plotunselected'); | |
181 | |
182 | |
183 | |
184 function setData(d) { | |
185 series = parseData(d); | |
186 | |
187 fillInSeriesOptions(); | |
188 processData(); | |
189 } | |
190 | |
191 function normalizeData(d) { | |
192 var possible_controls = ['x', 'time', 'date']; | |
193 | |
194 if (L.isArray(d)) { | |
195 d = { data: d }; | |
196 } else { | |
197 d = L.merge(d); | |
198 } | |
199 | |
200 if(d.disabled) { | |
201 return undefined; | |
202 } | |
203 | |
204 if (d.data.length === 0) { | |
205 return undefined; | |
206 } | |
207 | |
208 var j, k; | |
209 | |
210 // Make a copy so we don't obliterate the caller's data | |
211 var _data = []; | |
212 | |
213 if (L.isArray(d.data[0])) { | |
214 for(j=0; j<d.data.length; j++) { | |
215 if(d.data[j]) { | |
216 var x = d.data[j][0]; | |
217 var y = d.data[j][1]; | |
218 | |
219 if(L.isObject(x) && x.getTime) x = x.getTime()/1000; | |
220 else x = parseFloat(x); | |
221 | |
222 if(L.isObject(y) && y.getTime) y = y.getTime()/1000; | |
223 else y = parseFloat(y); | |
224 | |
225 _data.push({ x: x, y: y}); | |
226 } else { | |
227 _data.push(d.data[j]); | |
228 } | |
229 } | |
230 d.control='x'; | |
231 d.schema='y'; | |
232 } else { | |
233 for(j=0; j<d.data.length; j++) { | |
234 _data.push({}); | |
235 for(k in d.data[j]) { | |
236 if(L.isObject(d.data[j][k]) && d.data[j][k].getTime) | |
237 _data[j][k] = d.data[j][k].getTime()/1000; | |
238 else | |
239 _data[j][k] = parseFloat(d.data[j][k]); | |
240 } | |
241 } | |
242 } | |
243 | |
244 d.data = _data; | |
245 | |
246 if (!d.control) { | |
247 // try to guess the control field | |
248 for (j=0; j<possible_controls.length; j++) { | |
249 if(possible_controls[j] in d.data[0]) { | |
250 d.control = possible_controls[j]; | |
251 break; | |
252 } | |
253 } | |
254 } | |
255 | |
256 if (!d.schema) { | |
257 d.schema = []; | |
258 for(k in d.data[0]) { | |
259 if(!d.control) { | |
260 d.control = k; | |
261 } | |
262 if(k !== d.control) { | |
263 d.schema.push(k); | |
264 } | |
265 } | |
266 } | |
267 | |
268 return L.merge(d, {dropped: []}); | |
269 } | |
270 | |
271 function markDroppedPoints(s) { | |
272 var l=s.data.length; | |
273 | |
274 if(l <= canvasWidth/10 || options.dontDropPoints) { // at least 10px per point | |
275 return s; | |
276 } | |
277 | |
278 var dropperiod = 1-canvasWidth/10/l; | |
279 var drops = 0; | |
280 var points = l; | |
281 | |
282 for(var j=0; j<l; j++) { | |
283 var x = s.data[j].x; | |
284 var y = s.data[j].y; | |
285 | |
286 s.dropped[j] = (drops > 1); | |
287 if(s.dropped[j]) { | |
288 drops-=1; | |
289 } | |
290 | |
291 if(!isNaN(x) && !isNaN(x)) | |
292 drops+=dropperiod; | |
293 else { | |
294 drops=0; // bonus for a null point | |
295 points--; | |
296 dropperiod=1-canvasWidth/10/points; | |
297 } | |
298 } | |
299 | |
300 return s; | |
301 } | |
302 | |
303 function splitSeries(s) { | |
304 var res = []; | |
305 | |
306 for(var k=0; k<s.schema.length; k++) { | |
307 res[k] = L.merge(s, {data: []}); | |
308 if(s.label && L.isObject(s.label) && s.label[s.schema[k]]) { | |
309 res[k].label = s.label[s.schema[k]]; | |
310 } | |
311 if(s.color && L.isObject(s.color) && s.color[s.schema[k]]) { | |
312 res[k].color = s.color[s.schema[k]]; | |
313 } | |
314 } | |
315 | |
316 for(var i=0; i<s.data.length; i++) { | |
317 var d = s.data[i]; | |
318 for(k=0; k<s.schema.length; k++) { | |
319 var tuple = { x: d[s.control], y: d[s.schema[k]] }; | |
320 res[k].data.push(tuple); | |
321 res[k].control='x'; | |
322 res[k].schema='y'; | |
323 } | |
324 } | |
325 | |
326 return res; | |
327 } | |
328 | |
329 function parseData(d) { | |
330 if(d.length === 0) { | |
331 return null; | |
332 } | |
333 | |
334 // get the canvas width so we know if we have to drop points | |
335 canvasWidth = parseInt(DOM.getStyle(target, 'width'), 10); | |
336 | |
337 // First we normalise the data into a standard format | |
338 var s, res = []; | |
339 for (var i = 0; i < d.length; ++i) { | |
340 s = normalizeData(d[i]); | |
341 if(typeof s === 'undefined') | |
342 continue; | |
343 | |
344 if(L.isArray(s.schema)) { | |
345 s = splitSeries(s); | |
346 } | |
347 else { | |
348 s = [s]; | |
349 } | |
350 | |
351 for(var k=0; k<s.length; k++) { | |
352 s[k] = markDroppedPoints(s[k]); | |
353 res.push(s[k]); | |
354 } | |
355 } | |
356 | |
357 return res; | |
358 } | |
359 | |
360 function parseOptions(o) { | |
361 if (options.grid.borderColor == null) | |
362 options.grid.borderColor = options.grid.color; | |
363 | |
364 if(typeof o === 'undefined') { | |
365 return; | |
366 } | |
367 o = YAHOO.lang.merge(o); | |
368 for(var k in o) { | |
369 if(L.isObject(o[k]) && L.isObject(options[k])) { | |
370 L.augmentObject(options[k], o[k], true); | |
371 delete o[k]; | |
372 } | |
373 } | |
374 L.augmentObject(options, o, true); | |
375 } | |
376 | |
377 function fillInSeriesOptions() { | |
378 var i; | |
379 | |
380 // collect what we already got of colors | |
381 var neededColors = series.length, | |
382 usedColors = [], | |
383 assignedColors = []; | |
384 for (i = 0; i < series.length; ++i) { | |
385 var sc = series[i].color; | |
386 if (sc != null) { | |
387 --neededColors; | |
388 if (typeof sc == "number") | |
389 assignedColors.push(sc); | |
390 else | |
391 usedColors.push(parseColor(series[i].color)); | |
392 } | |
393 } | |
394 | |
395 // we might need to generate more colors if higher indices | |
396 // are assigned | |
397 for (i = 0; i < assignedColors.length; ++i) { | |
398 neededColors = Math.max(neededColors, assignedColors[i] + 1); | |
399 } | |
400 | |
401 // produce colors as needed | |
402 var colors = [], variation = 0; | |
403 i = 0; | |
404 while (colors.length < neededColors) { | |
405 var c; | |
406 if (options.colors.length == i) // check degenerate case | |
407 c = new Color(100, 100, 100); | |
408 else | |
409 c = parseColor(options.colors[i]); | |
410 | |
411 // vary color if needed | |
412 var sign = variation % 2 == 1 ? -1 : 1; | |
413 var factor = 1 + sign * Math.ceil(variation / 2) * 0.2; | |
414 c.scale(factor, factor, factor); | |
415 | |
416 // FIXME: if we're getting too close to something else, | |
417 // we should probably skip this one | |
418 colors.push(c); | |
419 | |
420 ++i; | |
421 if (i >= options.colors.length) { | |
422 i = 0; | |
423 ++variation; | |
424 } | |
425 } | |
426 | |
427 // fill in the options | |
428 var colori = 0, s; | |
429 for (i = 0; i < series.length; ++i) { | |
430 s = series[i]; | |
431 | |
432 // assign colors | |
433 if (s.color == null) { | |
434 s.color = colors[colori].toString(); | |
435 ++colori; | |
436 } | |
437 else if (typeof s.color == "number") | |
438 s.color = colors[s.color].toString(); | |
439 | |
440 // copy the rest | |
441 s.lines = L.merge(options.lines, s.lines || {}); | |
442 s.points = L.merge(options.points, s.points || {}); | |
443 s.bars = L.merge(options.bars, s.bars || {}); | |
444 | |
445 // turn on lines automatically in case nothing is set | |
446 if (s.lines.show == null && !s.bars.show && !s.points.show) | |
447 s.lines.show = true; | |
448 | |
449 if (s.shadowSize == null) | |
450 s.shadowSize = options.shadowSize; | |
451 | |
452 if (s.xaxis && s.xaxis == 2) | |
453 s.xaxis = axes.x2axis; | |
454 else | |
455 s.xaxis = axes.xaxis; | |
456 if (s.yaxis && s.yaxis >= 2) { | |
457 if(!axes['y' + s.yaxis + 'axis']) | |
458 axes['y' + s.yaxis + 'axis'] = {}; | |
459 if(!options['y' + s.yaxis + 'axis']) | |
460 options['y' + s.yaxis + 'axis'] = { autoscaleMargin: 0.02 }; | |
461 s.yaxis = axes['y' + s.yaxis + 'axis']; | |
462 } | |
463 else | |
464 s.yaxis = axes.yaxis; | |
465 } | |
466 } | |
467 | |
468 function processData() { | |
469 var topSentry = Number.POSITIVE_INFINITY, | |
470 bottomSentry = Number.NEGATIVE_INFINITY, | |
471 axis; | |
472 | |
473 for (axis in axes) { | |
474 axes[axis].datamin = topSentry; | |
475 axes[axis].datamax = bottomSentry; | |
476 axes[axis].min = options[axis].min; | |
477 axes[axis].max = options[axis].max; | |
478 axes[axis].used = false; | |
479 } | |
480 | |
481 for (var i = 0; i < series.length; ++i) { | |
482 var s = series[i]; | |
483 var data = s.data, | |
484 axisx = s.xaxis, axisy = s.yaxis, | |
485 xmin = topSentry, xmax = bottomSentry, | |
486 ymin = topSentry, ymax = bottomSentry, | |
487 x, y, p; | |
488 | |
489 axisx.used = axisy.used = true; | |
490 | |
491 if (s.bars.show) { | |
492 // make sure we got room for the bar | |
493 var delta = s.bars.align == "left" ? 0 : -s.bars.barWidth/2; | |
494 xmin += delta; | |
495 xmax += delta + s.bars.barWidth; | |
496 } | |
497 | |
498 for (var j = 0; j < data.length; ++j) { | |
499 p = data[j]; | |
500 | |
501 if(data[j] === null) | |
502 continue; | |
503 | |
504 x = p.x; | |
505 y = p.y; | |
506 | |
507 if(L.isObject(x) && x.getTime) { // this is a Date object | |
508 x = x.getTime()/1000; | |
509 } | |
510 | |
511 if(L.isObject(y) && y.getTime) { // this is a Date object | |
512 y = y.getTime()/1000; | |
513 } | |
514 | |
515 // convert to number | |
516 if (x != null && !isNaN(x = +x)) { | |
517 if (x < xmin) | |
518 xmin = x; | |
519 if (x > xmax) | |
520 xmax = x; | |
521 } | |
522 else | |
523 x = null; | |
524 | |
525 if (y != null && !isNaN(y = +y)) { | |
526 if (y < ymin) | |
527 ymin = y; | |
528 if (y > ymax) | |
529 ymax = y; | |
530 } | |
531 else | |
532 y = null; | |
533 | |
534 if (x == null || y == null) | |
535 data[j] = x = y = null; // mark this point invalid | |
536 } | |
537 | |
538 axisx.datamin = Math.min(axisx.datamin, xmin); | |
539 axisx.datamax = Math.max(axisx.datamax, xmax); | |
540 axisy.datamin = Math.min(axisy.datamin, ymin); | |
541 axisy.datamax = Math.max(axisy.datamax, ymax); | |
542 } | |
543 } | |
544 | |
545 function constructCanvas() { | |
546 function makeCanvas(width, height, container, style) { | |
547 var c = document.createElement('canvas'); | |
548 c.width = width; | |
549 c.height = height; | |
550 if (typeof G_vmlCanvasManager !== 'undefined') // excanvas hack | |
551 c = G_vmlCanvasManager.initElement(c); | |
552 | |
553 if(style) { | |
554 for(var k in style) { | |
555 c.style[k] = style[k]; | |
556 } | |
557 } | |
558 container.appendChild(c); | |
559 | |
560 return c; | |
561 } | |
562 | |
563 canvasWidth = parseInt(DOM.getStyle(target, 'width'), 10); | |
564 canvasHeight = parseInt(DOM.getStyle(target, 'height'), 10); | |
565 target.innerHTML = ""; // clear target | |
566 target.style.position = "relative"; // for positioning labels and overlay | |
567 | |
568 if (canvasWidth <= 0 || canvasHeight <= 0) | |
569 throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight; | |
570 | |
571 if (YAHOO.env.ua.ie) { | |
572 G_vmlCanvasManager.init_(document); | |
573 } | |
574 | |
575 // the canvas | |
576 canvas = makeCanvas(canvasWidth, canvasHeight, target); | |
577 ctx = canvas.getContext("2d"); | |
578 | |
579 // overlay canvas for interactive features | |
580 overlay = makeCanvas(canvasWidth, canvasHeight, target, { position: 'absolute', left: '0px', top: '0px' }); | |
581 octx = overlay.getContext("2d"); | |
582 | |
583 // we include the canvas in the event holder too, because IE 7 | |
584 // sometimes has trouble with the stacking order | |
585 eventHolder = [overlay, canvas]; | |
586 | |
587 // bind events | |
588 if (options.selection.mode != null || options.crosshair.mode != null || options.grid.hoverable) { | |
589 E.on(eventHolder, 'mousemove', onMouseMove); | |
590 | |
591 if (options.selection.mode != null) | |
592 E.on(eventHolder, "mousedown", onMouseDown); | |
593 } | |
594 | |
595 if (options.crosshair.mode != null) | |
596 E.on(eventHolder, "mouseout", onMouseOut); | |
597 | |
598 if (options.grid.clickable) | |
599 E.on(eventHolder, "click", onClick); | |
600 } | |
601 | |
602 function setupGrid() { | |
603 function setupAxis(axis, options, type) { | |
604 setRange(axis, options); | |
605 prepareTickGeneration(axis, options); | |
606 setTicks(axis, options); | |
607 // add transformation helpers | |
608 if (type == 'x') { | |
609 // data point to canvas coordinate | |
610 axis.p2c = function (p) { return (p - axis.min) * axis.scale; }; | |
611 // canvas coordinate to data point | |
612 axis.c2p = function (c) { return axis.min + c / axis.scale; }; | |
613 } | |
614 else { | |
615 axis.p2c = function (p) { return (axis.max - p) * axis.scale; }; | |
616 axis.c2p = function (c) { return axis.max - c / axis.scale; }; | |
617 } | |
618 } | |
619 | |
620 for (var axis in axes) | |
621 setupAxis(axes[axis], options[axis], axis.charAt(0)); | |
622 | |
623 setSpacing(); | |
624 if(options.grid.show) | |
625 insertLabels(); | |
626 insertLegend(); | |
627 insertAxisLabels(); | |
628 } | |
629 | |
630 function setRange(axis, axisOptions) { | |
631 var min = axisOptions.min != null ? (axisOptions.scaleType == 'log' ? Math.log(axisOptions.min<=0?1:axisOptions.min) * Math.LOG10E : axisOptions.min) : axis.datamin; | |
632 var max = axisOptions.max != null ? (axisOptions.scaleType == 'log' ? Math.log(axisOptions.max) * Math.LOG10E : axisOptions.max) : axis.datamax; | |
633 | |
634 if(axisOptions.mode === 'time') { | |
635 if(L.isObject(min) && min.getTime) min = min.getTime()/1000; | |
636 if(L.isObject(max) && max.getTime) max = max.getTime()/1000; | |
637 } | |
638 | |
639 // degenerate case | |
640 if (min == Number.POSITIVE_INFINITY) | |
641 min = 0; | |
642 if (max == Number.NEGATIVE_INFINITY) | |
643 max = 1; | |
644 | |
645 if (max - min == 0.0) { | |
646 // degenerate case | |
647 var widen = max == 0 ? 1 : 0.01; | |
648 | |
649 if (axisOptions.min == null) | |
650 min -= widen; | |
651 // alway widen max if we couldn't widen min to ensure we | |
652 // don't fall into min == max which doesn't work | |
653 if (axisOptions.max == null || axisOptions.min != null) | |
654 max += widen; | |
655 } | |
656 else { | |
657 // consider autoscaling | |
658 var margin = axisOptions.autoscaleMargin; | |
659 if (margin != null) { | |
660 if (axisOptions.min == null) { | |
661 min -= (max - min) * margin; | |
662 // make sure we don't go below zero if all values | |
663 // are positive | |
664 if (min < 0 && axis.datamin >= 0) | |
665 min = 0; | |
666 } | |
667 if (axisOptions.max == null) { | |
668 max += (max - min) * margin; | |
669 if (max > 0 && axis.datamax <= 0) | |
670 max = 0; | |
671 } | |
672 } | |
673 } | |
674 axis.min = min; | |
675 axis.max = max; | |
676 } | |
677 | |
678 function prepareTickGeneration(axis, axisOptions) { | |
679 // estimate number of ticks | |
680 var noTicks; | |
681 if (typeof axisOptions.ticks == "number" && axisOptions.ticks > 0) | |
682 noTicks = axisOptions.ticks; | |
683 else if (axis == axes.xaxis || axis == axes.x2axis) | |
684 noTicks = canvasWidth / 100; | |
685 else | |
686 noTicks = canvasHeight / 60; | |
687 | |
688 var delta = (axis.max - axis.min) / noTicks; | |
689 var size, generator, unit, formatter, magn, norm; | |
690 | |
691 if (axisOptions.mode == "time") { | |
692 // pretty handling of time | |
693 | |
694 delta*=1000; | |
695 | |
696 // map of app. size of time units in milliseconds | |
697 var timeUnitSize = { | |
698 "second": 1000, | |
699 "minute": 60 * 1000, | |
700 "hour": 60 * 60 * 1000, | |
701 "day": 24 * 60 * 60 * 1000, | |
702 "month": 30 * 24 * 60 * 60 * 1000, | |
703 "year": 365.2425 * 24 * 60 * 60 * 1000 | |
704 }; | |
705 | |
706 | |
707 // the allowed tick sizes, after 1 year we use | |
708 // an integer algorithm | |
709 var spec = [ | |
710 [1, "second"], [2, "second"], [5, "second"], [10, "second"], | |
711 [30, "second"], | |
712 [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], | |
713 [30, "minute"], | |
714 [1, "hour"], [2, "hour"], [4, "hour"], | |
715 [8, "hour"], [12, "hour"], | |
716 [1, "day"], [2, "day"], [3, "day"], | |
717 [0.25, "month"], [0.5, "month"], [1, "month"], | |
718 [2, "month"], [3, "month"], [6, "month"], | |
719 [1, "year"] | |
720 ]; | |
721 | |
722 var minSize = 0; | |
723 if (axisOptions.minTickSize != null) { | |
724 if (typeof axisOptions.tickSize == "number") | |
725 minSize = axisOptions.tickSize; | |
726 else | |
727 minSize = axisOptions.minTickSize[0] * timeUnitSize[axisOptions.minTickSize[1]]; | |
728 } | |
729 | |
730 for (var i = 0; i < spec.length - 1; ++i) | |
731 if (delta < (spec[i][0] * timeUnitSize[spec[i][1]] | |
732 + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 | |
733 && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) | |
734 break; | |
735 size = spec[i][0]; | |
736 unit = spec[i][1]; | |
737 | |
738 // special-case the possibility of several years | |
739 if (unit == "year") { | |
740 magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10)); | |
741 norm = (delta / timeUnitSize.year) / magn; | |
742 if (norm < 1.5) | |
743 size = 1; | |
744 else if (norm < 3) | |
745 size = 2; | |
746 else if (norm < 7.5) | |
747 size = 5; | |
748 else | |
749 size = 10; | |
750 | |
751 size *= magn; | |
752 } | |
753 | |
754 if (axisOptions.tickSize) { | |
755 size = axisOptions.tickSize[0]; | |
756 unit = axisOptions.tickSize[1]; | |
757 } | |
758 | |
759 generator = function(axis) { | |
760 var ticks = [], | |
761 tickSize = axis.tickSize[0], unit = axis.tickSize[1], | |
762 d = new Date(axis.min*1000); | |
763 | |
764 var step = tickSize * timeUnitSize[unit]; | |
765 | |
766 if (unit == "second") | |
767 d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize)); | |
768 if (unit == "minute") | |
769 d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize)); | |
770 if (unit == "hour") | |
771 d.setUTCHours(floorInBase(d.getUTCHours(), tickSize)); | |
772 if (unit == "month") | |
773 d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize)); | |
774 if (unit == "year") | |
775 d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize)); | |
776 | |
777 // reset smaller components | |
778 d.setUTCMilliseconds(0); | |
779 if (step >= timeUnitSize.minute) | |
780 d.setUTCSeconds(0); | |
781 if (step >= timeUnitSize.hour) | |
782 d.setUTCMinutes(0); | |
783 if (step >= timeUnitSize.day) | |
784 d.setUTCHours(0); | |
785 if (step >= timeUnitSize.day * 4) | |
786 d.setUTCDate(1); | |
787 if (step >= timeUnitSize.year) | |
788 d.setUTCMonth(0); | |
789 | |
790 | |
791 var carry = 0, v = Number.NaN, prev; | |
792 do { | |
793 prev = v; | |
794 v = d.getTime(); | |
795 ticks.push({ v: v/1000, label: axis.tickFormatter(v, axis) }); | |
796 if (unit == "month") { | |
797 if (tickSize < 1) { | |
798 // a bit complicated - we'll divide the month | |
799 // up but we need to take care of fractions | |
800 // so we don't end up in the middle of a day | |
801 d.setUTCDate(1); | |
802 var start = d.getTime(); | |
803 d.setUTCMonth(d.getUTCMonth() + 1); | |
804 var end = d.getTime(); | |
805 d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); | |
806 carry = d.getUTCHours(); | |
807 d.setUTCHours(0); | |
808 } | |
809 else | |
810 d.setUTCMonth(d.getUTCMonth() + tickSize); | |
811 } | |
812 else if (unit == "year") { | |
813 d.setUTCFullYear(d.getUTCFullYear() + tickSize); | |
814 } | |
815 else | |
816 d.setTime(v + step); | |
817 } while (v < axis.max*1000 && v != prev); | |
818 | |
819 return ticks; | |
820 }; | |
821 | |
822 formatter = function (v, axis) { | |
823 var d = new Date(v); | |
824 | |
825 // first check global format | |
826 if (axisOptions.timeformat != null) | |
827 return YAHOO.util.Date.format(d, {format: axisOptions.timeformat}, options.locale); | |
828 | |
829 var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; | |
830 var span = axis.max - axis.min; | |
831 span*=1000; | |
832 | |
833 if (t < timeUnitSize.minute) | |
834 var fmt = "%k:%M:%S"; | |
835 else if (t < timeUnitSize.day) { | |
836 if (span < 2 * timeUnitSize.day) | |
837 fmt = "%k:%M"; | |
838 else | |
839 fmt = "%b %d %k:%M"; | |
840 } | |
841 else if (t < timeUnitSize.month) | |
842 fmt = "%b %d"; | |
843 else if (t < timeUnitSize.year) { | |
844 if (span < timeUnitSize.year/2) | |
845 fmt = "%b"; | |
846 else | |
847 fmt = "%b %Y"; | |
848 } | |
849 else | |
850 fmt = "%Y"; | |
851 | |
852 return YAHOO.util.Date.format(d, {format: fmt}, axisOptions.timelang); | |
853 }; | |
854 } | |
855 else { | |
856 // pretty rounding of base-10 numbers | |
857 var maxDec = axisOptions.tickDecimals; | |
858 var dec = -Math.floor(Math.log(delta) / Math.LN10); | |
859 if (maxDec != null && dec > maxDec) | |
860 dec = maxDec; | |
861 | |
862 magn = Math.pow(10, -dec); | |
863 norm = delta / magn; // norm is between 1.0 and 10.0 | |
864 | |
865 if (norm < 1.5) | |
866 size = 1; | |
867 else if (norm < 3) { | |
868 size = 2; | |
869 // special case for 2.5, requires an extra decimal | |
870 if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { | |
871 size = 2.5; | |
872 ++dec; | |
873 } | |
874 } | |
875 else if (norm < 7.5) | |
876 size = 5; | |
877 else | |
878 size = 10; | |
879 | |
880 size *= magn; | |
881 | |
882 if (axisOptions.minTickSize != null && size < axisOptions.minTickSize) | |
883 size = axisOptions.minTickSize; | |
884 | |
885 if (axisOptions.tickSize != null) | |
886 size = axisOptions.tickSize; | |
887 | |
888 axis.tickDecimals = Math.max(0, (maxDec != null) ? maxDec : dec); | |
889 | |
890 generator = function (axis) { | |
891 var ticks = []; | |
892 | |
893 // spew out all possible ticks | |
894 var start = floorInBase(axis.min, axis.tickSize), | |
895 i = 0, v = Number.NaN, prev; | |
896 do { | |
897 prev = v; | |
898 v = start + i * axis.tickSize; | |
899 var t=v; | |
900 if(axis.scaleType == 'log') { | |
901 t = Math.exp(t / Math.LOG10E); | |
902 } | |
903 ticks.push({ v: v, label: axis.tickFormatter(t, axis) }); | |
904 ++i; | |
905 } while (v < axis.max && v != prev); | |
906 return ticks; | |
907 }; | |
908 | |
909 formatter = function (v, axis) { | |
910 return v.toFixed(axis.tickDecimals); | |
911 }; | |
912 } | |
913 | |
914 axis.scaleType = axisOptions.scaleType; | |
915 axis.tickSize = unit ? [size, unit] : size; | |
916 axis.tickGenerator = generator; | |
917 if (L.isFunction(axisOptions.tickFormatter)) | |
918 axis.tickFormatter = function (v, axis) { return "" + axisOptions.tickFormatter(v, axis); }; | |
919 else | |
920 axis.tickFormatter = formatter; | |
921 if (axisOptions.labelWidth != null) | |
922 axis.labelWidth = axisOptions.labelWidth; | |
923 if (axisOptions.labelHeight != null) | |
924 axis.labelHeight = axisOptions.labelHeight; | |
925 } | |
926 | |
927 function setTicks(axis, axisOptions) { | |
928 axis.ticks = []; | |
929 | |
930 if (!axis.used) | |
931 return; | |
932 | |
933 if (axisOptions.ticks == null) | |
934 axis.ticks = axis.tickGenerator(axis); | |
935 else if (typeof axisOptions.ticks == "number") { | |
936 if (axisOptions.ticks > 0) | |
937 axis.ticks = axis.tickGenerator(axis); | |
938 } | |
939 else if (axisOptions.ticks) { | |
940 var ticks = axisOptions.ticks; | |
941 | |
942 if (L.isFunction(ticks)) | |
943 // generate the ticks | |
944 ticks = ticks({ min: axis.min, max: axis.max }); | |
945 | |
946 // clean up the user-supplied ticks, copy them over | |
947 var v; | |
948 for (var i = 0; i < ticks.length; ++i) { | |
949 var label = null; | |
950 var t = ticks[i]; | |
951 if (typeof t == "object") { | |
952 v = t[0]; | |
953 if (t.length > 1) | |
954 label = t[1]; | |
955 } | |
956 else | |
957 v = t; | |
958 if (axisOptions.scaleType == 'log') { | |
959 if (label == null) | |
960 label = v; | |
961 v = Math.log(v) * Math.LOG10E; | |
962 } | |
963 | |
964 if (label == null) | |
965 label = axis.tickFormatter(v, axis); | |
966 axis.ticks[i] = { v: v, label: label }; | |
967 } | |
968 } | |
969 | |
970 if (axisOptions.autoscaleMargin != null && axis.ticks.length > 0) { | |
971 // snap to ticks | |
972 if (axisOptions.min == null) | |
973 axis.min = Math.min(axis.min, axis.ticks[0].v); | |
974 if (axisOptions.max == null && axis.ticks.length > 1) | |
975 axis.max = Math.min(axis.max, axis.ticks[axis.ticks.length - 1].v); | |
976 } | |
977 } | |
978 | |
979 function setSpacing() { | |
980 function measureXLabels(axis) { | |
981 if(options.grid.show){ | |
982 // to avoid measuring the widths of the labels, we | |
983 // construct fixed-size boxes and put the labels inside | |
984 // them, we don't need the exact figures and the | |
985 // fixed-size box content is easy to center | |
986 if (axis.labelWidth == null) | |
987 axis.labelWidth = canvasWidth / 6; | |
988 | |
989 // measure x label heights | |
990 if (axis.labelHeight == null) { | |
991 var labels = []; | |
992 for (var i = 0; i < axis.ticks.length; ++i) { | |
993 var l = axis.ticks[i].label; | |
994 if (l) | |
995 labels.push('<div class="tickLabel" style="float:left;width:' + axis.labelWidth + 'px">' + l + '</div>'); | |
996 } | |
997 | |
998 axis.labelHeight = 0; | |
999 if (labels.length > 0) { | |
1000 var dummyDiv = target.appendChild(DOM.createElementFromMarkup('<div style="position:absolute;top:-10000px;width:10000px;font-size:smaller">' | |
1001 + labels.join("") + '<div style="clear:left"></div></div>')); | |
1002 axis.labelHeight = dummyDiv.offsetHeight; | |
1003 target.removeChild(dummyDiv); | |
1004 } | |
1005 } | |
1006 } | |
1007 else{ | |
1008 axis.labelHeight = 0; | |
1009 axis.labelWidth = 0; | |
1010 } | |
1011 } | |
1012 | |
1013 function measureYLabels(axis) { | |
1014 if(options.grid.show){ | |
1015 if (axis.labelWidth == null || axis.labelHeight == null) { | |
1016 var labels = [], l; | |
1017 // calculate y label dimensions | |
1018 for (var i = 0; i < axis.ticks.length; ++i) { | |
1019 l = axis.ticks[i].label; | |
1020 if (l) | |
1021 labels.push('<div class="tickLabel">' + l + '</div>'); | |
1022 } | |
1023 | |
1024 if (labels.length > 0) { | |
1025 var dummyDiv = target.appendChild(DOM.createElementFromMarkup('<div style="position:absolute;top:-10000px;font-size:smaller">' | |
1026 + labels.join("") + '</div>')); | |
1027 if (axis.labelWidth == null) | |
1028 axis.labelWidth = dummyDiv.offsetWidth; | |
1029 if (axis.labelHeight == null) | |
1030 axis.labelHeight = dummyDiv.firstChild.offsetHeight; | |
1031 target.removeChild(dummyDiv); | |
1032 } | |
1033 | |
1034 if (axis.labelWidth == null) | |
1035 axis.labelWidth = 0; | |
1036 if (axis.labelHeight == null) | |
1037 axis.labelHeight = 0; | |
1038 } | |
1039 } | |
1040 else{ | |
1041 axis.labelHeight = 0; | |
1042 axis.labelWidth = 0; | |
1043 } | |
1044 } | |
1045 | |
1046 measureXLabels(axes.xaxis); | |
1047 measureYLabels(axes.yaxis); | |
1048 measureXLabels(axes.x2axis); | |
1049 measureYLabels(axes.y2axis); | |
1050 // get the most space needed around the grid for things | |
1051 // that may stick out | |
1052 var maxOutset = (options.grid.show) ? options.grid.borderWidth : 0; | |
1053 for (var i = 0; i < series.length; ++i) | |
1054 maxOutset = (Math.max(maxOutset, 2 * (((series[i].points.show) ? series[i].points.radius : 0 ) + series[i].points.lineWidth/2))); | |
1055 | |
1056 plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = maxOutset; | |
1057 | |
1058 var margin = options.grid.labelMargin + options.grid.borderWidth; | |
1059 | |
1060 if (axes.xaxis.labelHeight > 0) | |
1061 plotOffset.bottom = Math.max(maxOutset, axes.xaxis.labelHeight + margin); | |
1062 if (axes.yaxis.labelWidth > 0) | |
1063 plotOffset.left = Math.max(maxOutset, axes.yaxis.labelWidth + margin); | |
1064 | |
1065 if (axes.x2axis.labelHeight > 0) | |
1066 plotOffset.top = Math.max(maxOutset, axes.x2axis.labelHeight + margin); | |
1067 | |
1068 if (axes.y2axis.labelWidth > 0) | |
1069 plotOffset.right = Math.max(maxOutset, axes.y2axis.labelWidth + margin); | |
1070 | |
1071 plotWidth = canvasWidth - plotOffset.left - plotOffset.right; | |
1072 plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top; | |
1073 | |
1074 // precompute how much the axis is scaling a point in canvas space | |
1075 for(var axis in axes) { | |
1076 axes[axis].scale = (axis.charAt(0) == 'x' ? plotWidth : plotHeight) / (axes[axis].max - axes[axis].min); | |
1077 } | |
1078 } | |
1079 | |
1080 function draw() { | |
1081 drawGrid(); | |
1082 for (var i = 0; i < series.length; i++) { | |
1083 drawSeries(series[i]); | |
1084 } | |
1085 } | |
1086 | |
1087 function extractRange(ranges, coord) { | |
1088 var firstAxis = coord + "axis", | |
1089 secondaryAxis = coord + "2axis", | |
1090 axis, from, to, reverse; | |
1091 | |
1092 if (ranges[firstAxis]) { | |
1093 axis = firstAxis; | |
1094 } | |
1095 else if (ranges[secondaryAxis]) { | |
1096 axis = secondaryAxis; | |
1097 } | |
1098 else { | |
1099 return { from: null, to: null, axis: axes[firstAxis] }; | |
1100 } | |
1101 | |
1102 from = ranges[axis].from; | |
1103 to = ranges[axis].to; | |
1104 | |
1105 if (options[axis].scaleType == 'log') { | |
1106 if (from != null) | |
1107 from = Math.log(from) * Math.LOG10E; | |
1108 if (to != null) | |
1109 to = Math.log(to) * Math.LOG10E; | |
1110 } | |
1111 | |
1112 axis = axes[axis]; | |
1113 | |
1114 // auto-reverse as an added bonus | |
1115 if (from != null && to != null && from > to) | |
1116 return { from: to, to: from, axis: axis }; | |
1117 | |
1118 return { from: from, to: to, axis: axis }; | |
1119 } | |
1120 | |
1121 function drawGrid() { | |
1122 var i; | |
1123 | |
1124 ctx.save(); | |
1125 ctx.clearRect(0, 0, canvasWidth, canvasHeight); | |
1126 ctx.translate(plotOffset.left, plotOffset.top); | |
1127 | |
1128 // draw background, if any | |
1129 if (options.grid.backgroundColor) { | |
1130 ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); | |
1131 ctx.fillRect(0, 0, plotWidth, plotHeight); | |
1132 } | |
1133 | |
1134 // draw markings | |
1135 var markings = options.grid.markings; | |
1136 if (markings) { | |
1137 if (L.isFunction(markings)) | |
1138 markings = markings({ xaxis: axes.xaxis, yaxis: axes.yaxis, x2axis: axes.x2axis, y2axis: axes.y2axis }); | |
1139 | |
1140 for (i = 0; i < markings.length; ++i) { | |
1141 var m = markings[i], | |
1142 xrange = extractRange(m, "x"), | |
1143 yrange = extractRange(m, "y"); | |
1144 | |
1145 // fill in missing | |
1146 if (xrange.from == null) | |
1147 xrange.from = xrange.axis.min; | |
1148 if (xrange.to == null) | |
1149 xrange.to = xrange.axis.max; | |
1150 if (yrange.from == null) | |
1151 yrange.from = yrange.axis.min; | |
1152 if (yrange.to == null) | |
1153 yrange.to = yrange.axis.max; | |
1154 | |
1155 // clip | |
1156 if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || | |
1157 yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) | |
1158 continue; | |
1159 | |
1160 xrange.from = Math.max(xrange.from, xrange.axis.min); | |
1161 xrange.to = Math.min(xrange.to, xrange.axis.max); | |
1162 yrange.from = Math.max(yrange.from, yrange.axis.min); | |
1163 yrange.to = Math.min(yrange.to, yrange.axis.max); | |
1164 | |
1165 if (xrange.from == xrange.to && yrange.from == yrange.to) | |
1166 continue; | |
1167 | |
1168 // then draw | |
1169 xrange.from = xrange.axis.p2c(xrange.from); | |
1170 xrange.to = xrange.axis.p2c(xrange.to); | |
1171 yrange.from = yrange.axis.p2c(yrange.from); | |
1172 yrange.to = yrange.axis.p2c(yrange.to); | |
1173 | |
1174 if (xrange.from == xrange.to || yrange.from == yrange.to) { | |
1175 // draw line | |
1176 ctx.strokeStyle = m.color || options.grid.markingsColor; | |
1177 ctx.beginPath(); | |
1178 ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth; | |
1179 ctx.moveTo(xrange.from, yrange.from); | |
1180 ctx.lineTo(xrange.to, yrange.to); | |
1181 ctx.stroke(); | |
1182 } | |
1183 else { | |
1184 // fill area | |
1185 ctx.fillStyle = m.color || options.grid.markingsColor; | |
1186 ctx.fillRect(xrange.from, yrange.to, | |
1187 xrange.to - xrange.from, | |
1188 yrange.from - yrange.to); | |
1189 } | |
1190 } | |
1191 } | |
1192 | |
1193 if(options.grid.show && options.grid.showLines) { | |
1194 // draw the inner grid | |
1195 ctx.lineWidth = 1; | |
1196 ctx.strokeStyle = options.grid.tickColor; | |
1197 ctx.beginPath(); | |
1198 var v, axis = axes.xaxis; | |
1199 for (i = 0; i < axis.ticks.length; ++i) { | |
1200 v = axis.ticks[i].v; | |
1201 if (v <= axis.min || v >= axes.xaxis.max) | |
1202 continue; // skip those lying on the axes | |
1203 | |
1204 ctx.moveTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, 0); | |
1205 ctx.lineTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, plotHeight); | |
1206 } | |
1207 | |
1208 axis = axes.yaxis; | |
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(0, Math.floor(axis.p2c(v)) + ctx.lineWidth/2); | |
1215 ctx.lineTo(plotWidth, Math.floor(axis.p2c(v)) + ctx.lineWidth/2); | |
1216 } | |
1217 | |
1218 axis = axes.x2axis; | |
1219 for (i = 0; i < axis.ticks.length; ++i) { | |
1220 v = axis.ticks[i].v; | |
1221 if (v <= axis.min || v >= axis.max) | |
1222 continue; | |
1223 | |
1224 ctx.moveTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, -5); | |
1225 ctx.lineTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, 5); | |
1226 } | |
1227 | |
1228 axis = axes.y2axis; | |
1229 for (i = 0; i < axis.ticks.length; ++i) { | |
1230 v = axis.ticks[i].v; | |
1231 if (v <= axis.min || v >= axis.max) | |
1232 continue; | |
1233 | |
1234 ctx.moveTo(plotWidth-5, Math.floor(axis.p2c(v)) + ctx.lineWidth/2); | |
1235 ctx.lineTo(plotWidth+5, Math.floor(axis.p2c(v)) + ctx.lineWidth/2); | |
1236 } | |
1237 | |
1238 ctx.stroke(); | |
1239 } | |
1240 | |
1241 if (options.grid.show && options.grid.borderWidth) { | |
1242 // draw border | |
1243 var bw = options.grid.borderWidth; | |
1244 ctx.lineWidth = bw; | |
1245 ctx.strokeStyle = options.grid.borderColor; | |
1246 ctx.lineJoin = "round"; | |
1247 ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); | |
1248 } | |
1249 | |
1250 ctx.restore(); | |
1251 } | |
1252 | |
1253 function insertLabels() { | |
1254 DOM.getElementsByClassName("tickLabels", "div", target, DOM.removeElement); | |
1255 | |
1256 var html = ['<div class="tickLabels" style="font-size:smaller;color:' + options.grid.color + '">']; | |
1257 | |
1258 function addLabels(axis, labelGenerator) { | |
1259 for (var i = 0; i < axis.ticks.length; ++i) { | |
1260 var tick = axis.ticks[i]; | |
1261 if (!tick.label || tick.v < axis.min || tick.v > axis.max) | |
1262 continue; | |
1263 html.push(labelGenerator(tick, axis)); | |
1264 } | |
1265 } | |
1266 | |
1267 var margin = options.grid.labelMargin + options.grid.borderWidth; | |
1268 | |
1269 addLabels(axes.xaxis, function (tick, axis) { | |
1270 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>"; | |
1271 }); | |
1272 | |
1273 | |
1274 addLabels(axes.yaxis, function (tick, axis) { | |
1275 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>"; | |
1276 }); | |
1277 | |
1278 addLabels(axes.x2axis, function (tick, axis) { | |
1279 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>"; | |
1280 }); | |
1281 | |
1282 addLabels(axes.y2axis, function (tick, axis) { | |
1283 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>"; | |
1284 }); | |
1285 | |
1286 html.push('</div>'); | |
1287 | |
1288 target.appendChild(DOM.createElementFromMarkup(html.join(""))); | |
1289 } | |
1290 | |
1291 function insertAxisLabels() { | |
1292 var xLocation, yLocation; | |
1293 if( options.xaxis.label ) { | |
1294 yLocation = plotOffset.top + plotHeight + ( axes.xaxis.labelHeight * 1.5 ); | |
1295 xLocation = plotOffset.left; | |
1296 DOM.getElementsByClassName("xaxislabel", "div", target, DOM.removeElement); | |
1297 target.appendChild( | |
1298 DOM.createElementFromMarkup( | |
1299 "<div class='xaxislabel' style='color:" + options.grid.color + ";width:" + plotWidth + "px;" | |
1300 + "text-align:center;position:absolute;top:" + yLocation + "px;left:" + xLocation + "px;'>" | |
1301 + options.xaxis.label + "</div>" | |
1302 ) | |
1303 ); | |
1304 } | |
1305 if( options.yaxis.label ) { | |
1306 xLocation = plotOffset.left - ( axes.yaxis.labelWidth * 2 ) - options.grid.labelFontSize; | |
1307 yLocation = plotOffset.top + plotHeight/2; | |
1308 DOM.getElementsByClassName("yaxislabel", "div", target, DOM.removeElement); | |
1309 | |
1310 target.appendChild( | |
1311 DOM.createElementFromMarkup( | |
1312 "<div class='yaxislabel' style='-moz-transform:rotate(270deg);-webkit-transform:rotate(270deg);writing-mode:tb-rl;filter:flipV flipH;color:" + options.grid.color + ";" | |
1313 + "text-align:center;position:absolute;top:" + yLocation + "px;left:" + xLocation + "px;'>" | |
1314 + options.yaxis.label + "</div>") | |
1315 ); | |
1316 } | |
1317 } | |
1318 | |
1319 function drawSeries(series) { | |
1320 if (series.lines.show) | |
1321 drawSeriesLines(series); | |
1322 if (series.bars.show) | |
1323 drawSeriesBars(series); | |
1324 if (series.points.show) | |
1325 drawSeriesPoints(series); | |
1326 } | |
1327 | |
1328 function drawSeriesLines(series) { | |
1329 function plotLine(data, xoffset, yoffset, axisx, axisy) { | |
1330 var prev = null, cur=null, drawx = null, drawy = null; | |
1331 | |
1332 ctx.beginPath(); | |
1333 for (var i = 0; i < data.length; i++) { | |
1334 prev = cur; | |
1335 cur = data[i]; | |
1336 | |
1337 if(prev == null || cur == null) | |
1338 continue; | |
1339 | |
1340 var x1 = prev.x, y1 = prev.y, | |
1341 x2 = cur.x, y2 = cur.y; | |
1342 | |
1343 // clip with ymin | |
1344 if (y1 <= y2 && y1 < axisy.min) { | |
1345 if (y2 < axisy.min) | |
1346 continue; // line segment is outside | |
1347 // compute new intersection point | |
1348 x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; | |
1349 y1 = axisy.min; | |
1350 } | |
1351 else if (y2 <= y1 && y2 < axisy.min) { | |
1352 if (y1 < axisy.min) | |
1353 continue; | |
1354 x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; | |
1355 y2 = axisy.min; | |
1356 } | |
1357 | |
1358 // clip with ymax | |
1359 if (y1 >= y2 && y1 > axisy.max) { | |
1360 if (y2 > axisy.max) | |
1361 continue; | |
1362 x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; | |
1363 y1 = axisy.max; | |
1364 } | |
1365 else if (y2 >= y1 && y2 > axisy.max) { | |
1366 if (y1 > axisy.max) | |
1367 continue; | |
1368 x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; | |
1369 y2 = axisy.max; | |
1370 } | |
1371 | |
1372 // clip with xmin | |
1373 if (x1 <= x2 && x1 < axisx.min) { | |
1374 if (x2 < axisx.min) | |
1375 continue; | |
1376 y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; | |
1377 x1 = axisx.min; | |
1378 } | |
1379 else if (x2 <= x1 && x2 < axisx.min) { | |
1380 if (x1 < axisx.min) | |
1381 continue; | |
1382 y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; | |
1383 x2 = axisx.min; | |
1384 } | |
1385 | |
1386 // clip with xmax | |
1387 if (x1 >= x2 && x1 > axisx.max) { | |
1388 if (x2 > axisx.max) | |
1389 continue; | |
1390 y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; | |
1391 x1 = axisx.max; | |
1392 } | |
1393 else if (x2 >= x1 && x2 > axisx.max) { | |
1394 if (x1 > axisx.max) | |
1395 continue; | |
1396 y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; | |
1397 x2 = axisx.max; | |
1398 } | |
1399 | |
1400 if (drawx != axisx.p2c(x1) + xoffset || drawy != axisy.p2c(y1) + yoffset) | |
1401 ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); | |
1402 | |
1403 drawx = axisx.p2c(x2) + xoffset; | |
1404 drawy = axisy.p2c(y2) + yoffset; | |
1405 ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); | |
1406 } | |
1407 ctx.stroke(); | |
1408 } | |
1409 | |
1410 function plotLineArea(data, axisx, axisy) { | |
1411 var prev, cur = null, | |
1412 bottom = Math.min(Math.max(0, axisy.min), axisy.max), | |
1413 top, lastX = 0, areaOpen = false; | |
1414 | |
1415 for (var i = 0; i < data.length; i++) { | |
1416 prev = cur; | |
1417 cur = data[i]; | |
1418 | |
1419 if (areaOpen && x1 != null && x2 == null) { | |
1420 // close area | |
1421 ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom)); | |
1422 ctx.fill(); | |
1423 areaOpen = false; | |
1424 continue; | |
1425 } | |
1426 | |
1427 if (prev == null || cur == null) { | |
1428 if(areaOpen) { | |
1429 ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom)); | |
1430 ctx.fill(); | |
1431 } | |
1432 areaOpen = false; | |
1433 continue; | |
1434 } | |
1435 | |
1436 var x1 = prev.x, y1 = prev.y, | |
1437 x2 = cur.x, y2 = cur.y; | |
1438 | |
1439 // clip x values | |
1440 | |
1441 // clip with xmin | |
1442 if (x1 <= x2 && x1 < axisx.min) { | |
1443 if (x2 < axisx.min) | |
1444 continue; | |
1445 y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; | |
1446 x1 = axisx.min; | |
1447 } | |
1448 else if (x2 <= x1 && x2 < axisx.min) { | |
1449 if (x1 < axisx.min) | |
1450 continue; | |
1451 y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; | |
1452 x2 = axisx.min; | |
1453 } | |
1454 | |
1455 // clip with xmax | |
1456 if (x1 >= x2 && x1 > axisx.max) { | |
1457 if (x2 > axisx.max) | |
1458 continue; | |
1459 y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; | |
1460 x1 = axisx.max; | |
1461 } | |
1462 else if (x2 >= x1 && x2 > axisx.max) { | |
1463 if (x1 > axisx.max) | |
1464 continue; | |
1465 y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; | |
1466 x2 = axisx.max; | |
1467 } | |
1468 | |
1469 if (!areaOpen) { | |
1470 // open area | |
1471 ctx.beginPath(); | |
1472 ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); | |
1473 areaOpen = true; | |
1474 } | |
1475 | |
1476 // now first check the case where both is outside | |
1477 if (y1 >= axisy.max && y2 >= axisy.max) { | |
1478 ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); | |
1479 ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); | |
1480 lastX = x2; | |
1481 continue; | |
1482 } | |
1483 else if (y1 <= axisy.min && y2 <= axisy.min) { | |
1484 ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); | |
1485 ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); | |
1486 lastX = x2; | |
1487 continue; | |
1488 } | |
1489 | |
1490 // else it's a bit more complicated, there might | |
1491 // be two rectangles and two triangles we need to fill | |
1492 // in; to find these keep track of the current x values | |
1493 var x1old = x1, x2old = x2; | |
1494 | |
1495 // and clip the y values, without shortcutting | |
1496 | |
1497 // clip with ymin | |
1498 if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { | |
1499 x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; | |
1500 y1 = axisy.min; | |
1501 } | |
1502 else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { | |
1503 x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; | |
1504 y2 = axisy.min; | |
1505 } | |
1506 | |
1507 // clip with ymax | |
1508 if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { | |
1509 x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; | |
1510 y1 = axisy.max; | |
1511 } | |
1512 else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { | |
1513 x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; | |
1514 y2 = axisy.max; | |
1515 } | |
1516 | |
1517 | |
1518 // if the x value was changed we got a rectangle | |
1519 // to fill | |
1520 if (x1 != x1old) { | |
1521 if (y1 <= axisy.min) | |
1522 top = axisy.min; | |
1523 else | |
1524 top = axisy.max; | |
1525 | |
1526 ctx.lineTo(axisx.p2c(x1old), axisy.p2c(top)); | |
1527 ctx.lineTo(axisx.p2c(x1), axisy.p2c(top)); | |
1528 } | |
1529 | |
1530 // fill the triangles | |
1531 ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); | |
1532 ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); | |
1533 | |
1534 // fill the other rectangle if it's there | |
1535 if (x2 != x2old) { | |
1536 if (y2 <= axisy.min) | |
1537 top = axisy.min; | |
1538 else | |
1539 top = axisy.max; | |
1540 | |
1541 ctx.lineTo(axisx.p2c(x2), axisy.p2c(top)); | |
1542 ctx.lineTo(axisx.p2c(x2old), axisy.p2c(top)); | |
1543 } | |
1544 | |
1545 lastX = Math.max(x2, x2old); | |
1546 } | |
1547 | |
1548 if (areaOpen) { | |
1549 ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom)); | |
1550 ctx.fill(); | |
1551 } | |
1552 } | |
1553 | |
1554 ctx.save(); | |
1555 ctx.translate(plotOffset.left, plotOffset.top); | |
1556 ctx.lineJoin = "round"; | |
1557 | |
1558 var lw = series.lines.lineWidth, | |
1559 sw = series.shadowSize; | |
1560 // FIXME: consider another form of shadow when filling is turned on | |
1561 if (lw > 0 && sw > 0) { | |
1562 // draw shadow as a thick and thin line with transparency | |
1563 ctx.lineWidth = sw; | |
1564 ctx.strokeStyle = "rgba(0,0,0,0.1)"; | |
1565 var xoffset = 1; | |
1566 plotLine(series.data, xoffset, Math.sqrt((lw/2 + sw/2)*(lw/2 + sw/2) - xoffset*xoffset), series.xaxis, series.yaxis); | |
1567 ctx.lineWidth = sw/2; | |
1568 plotLine(series.data, xoffset, Math.sqrt((lw/2 + sw/4)*(lw/2 + sw/4) - xoffset*xoffset), series.xaxis, series.yaxis); | |
1569 } | |
1570 | |
1571 ctx.lineWidth = lw; | |
1572 ctx.strokeStyle = series.color; | |
1573 var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); | |
1574 if (fillStyle) { | |
1575 ctx.fillStyle = fillStyle; | |
1576 plotLineArea(series.data, series.xaxis, series.yaxis); | |
1577 } | |
1578 | |
1579 if (lw > 0) | |
1580 plotLine(series.data, 0, 0, series.xaxis, series.yaxis); | |
1581 ctx.restore(); | |
1582 } | |
1583 | |
1584 function drawSeriesPoints(series) { | |
1585 function plotPoints(data, radius, fillStyle, offset, circumference, axisx, axisy) { | |
1586 for (var i = 0; i < data.length; i++) { | |
1587 if (data[i] == null || series.dropped[i]) | |
1588 continue; | |
1589 | |
1590 var x = data[i].x, y = data[i].y; | |
1591 if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) | |
1592 continue; | |
1593 | |
1594 ctx.beginPath(); | |
1595 ctx.arc(axisx.p2c(x), axisy.p2c(y) + offset, radius, 0, circumference, true); | |
1596 if (fillStyle) { | |
1597 ctx.fillStyle = fillStyle; | |
1598 ctx.fill(); | |
1599 } | |
1600 ctx.stroke(); | |
1601 } | |
1602 } | |
1603 | |
1604 ctx.save(); | |
1605 ctx.translate(plotOffset.left, plotOffset.top); | |
1606 | |
1607 var lw = series.lines.lineWidth, | |
1608 sw = series.shadowSize, | |
1609 radius = series.points.radius; | |
1610 if (lw > 0 && sw > 0) { | |
1611 // draw shadow in two steps | |
1612 var w = sw / 2; | |
1613 ctx.lineWidth = w; | |
1614 ctx.strokeStyle = "rgba(0,0,0,0.1)"; | |
1615 plotPoints(series.data, radius, null, w + w/2, 2 * Math.PI, | |
1616 series.xaxis, series.yaxis); | |
1617 | |
1618 ctx.strokeStyle = "rgba(0,0,0,0.2)"; | |
1619 plotPoints(series.data, radius, null, w/2, 2 * Math.PI, | |
1620 series.xaxis, series.yaxis); | |
1621 } | |
1622 | |
1623 ctx.lineWidth = lw; | |
1624 ctx.strokeStyle = series.color; | |
1625 plotPoints(series.data, radius, | |
1626 getFillStyle(series.points, series.color), 0, 2 * Math.PI, | |
1627 series.xaxis, series.yaxis); | |
1628 ctx.restore(); | |
1629 } | |
1630 | |
1631 function drawBar(x, y, barLeft, barRight, offset, fill, axisx, axisy, c) { | |
1632 var drawLeft = true, drawRight = true, | |
1633 drawTop = true, drawBottom = false, | |
1634 left = x + barLeft, right = x + barRight, | |
1635 bottom = 0, top = y; | |
1636 | |
1637 // account for negative bars | |
1638 if (top < bottom) { | |
1639 top = 0; | |
1640 bottom = y; | |
1641 drawBottom = true; | |
1642 drawTop = false; | |
1643 } | |
1644 | |
1645 // clip | |
1646 if (right < axisx.min || left > axisx.max || | |
1647 top < axisy.min || bottom > axisy.max) | |
1648 return; | |
1649 | |
1650 if (left < axisx.min) { | |
1651 left = axisx.min; | |
1652 drawLeft = false; | |
1653 } | |
1654 | |
1655 if (right > axisx.max) { | |
1656 right = axisx.max; | |
1657 drawRight = false; | |
1658 } | |
1659 | |
1660 if (bottom < axisy.min) { | |
1661 bottom = axisy.min; | |
1662 drawBottom = false; | |
1663 } | |
1664 | |
1665 if (top > axisy.max) { | |
1666 top = axisy.max; | |
1667 drawTop = false; | |
1668 } | |
1669 | |
1670 left = axisx.p2c(left); | |
1671 bottom = axisy.p2c(bottom); | |
1672 right = axisx.p2c(right); | |
1673 top = axisy.p2c(top); | |
1674 | |
1675 // fill the bar | |
1676 if (fill) { | |
1677 c.beginPath(); | |
1678 c.moveTo(left, bottom); | |
1679 c.lineTo(left, top); | |
1680 c.lineTo(right, top); | |
1681 c.lineTo(right, bottom); | |
1682 if(typeof fill === 'function') { | |
1683 c.fillStyle = fill(bottom, top); | |
1684 } else if(typeof fill === 'string') { | |
1685 c.fillStyle = fill; | |
1686 } | |
1687 c.fill(); | |
1688 } | |
1689 | |
1690 // draw outline | |
1691 if (drawLeft || drawRight || drawTop || drawBottom) { | |
1692 c.beginPath(); | |
1693 | |
1694 // FIXME: inline moveTo is buggy with excanvas | |
1695 c.moveTo(left, bottom + offset); | |
1696 if (drawLeft) | |
1697 c.lineTo(left, top + offset); | |
1698 else | |
1699 c.moveTo(left, top + offset); | |
1700 if (drawTop) | |
1701 c.lineTo(right, top + offset); | |
1702 else | |
1703 c.moveTo(right, top + offset); | |
1704 if (drawRight) | |
1705 c.lineTo(right, bottom + offset); | |
1706 else | |
1707 c.moveTo(right, bottom + offset); | |
1708 if (drawBottom) | |
1709 c.lineTo(left, bottom + offset); | |
1710 else | |
1711 c.moveTo(left, bottom + offset); | |
1712 c.stroke(); | |
1713 } | |
1714 } | |
1715 | |
1716 function drawSeriesBars(series) { | |
1717 function plotBars(data, barLeft, barRight, offset, fill, axisx, axisy) { | |
1718 | |
1719 for (var i = 0; i < data.length; i++) { | |
1720 if (data[i] == null) | |
1721 continue; | |
1722 drawBar(data[i].x, data[i].y, barLeft, barRight, offset, fill, axisx, axisy, ctx); | |
1723 } | |
1724 } | |
1725 | |
1726 ctx.save(); | |
1727 ctx.translate(plotOffset.left, plotOffset.top); | |
1728 | |
1729 // FIXME: figure out a way to add shadows (for instance along the right edge) | |
1730 ctx.lineWidth = series.bars.lineWidth; | |
1731 ctx.strokeStyle = series.color; | |
1732 var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2; | |
1733 var fill = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; | |
1734 plotBars(series.data, barLeft, barLeft + series.bars.barWidth, 0, fill, series.xaxis, series.yaxis); | |
1735 ctx.restore(); | |
1736 } | |
1737 | |
1738 function getFillStyle(filloptions, seriesColor, bottom, top) { | |
1739 var fill = filloptions.fill; | |
1740 if (!fill) | |
1741 return null; | |
1742 | |
1743 if (filloptions.fillColor) | |
1744 return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); | |
1745 | |
1746 var c = parseColor(seriesColor); | |
1747 c.a = typeof fill == "number" ? fill : 0.4; | |
1748 c.normalize(); | |
1749 return c.toString(); | |
1750 } | |
1751 | |
1752 function insertLegend() { | |
1753 DOM.getElementsByClassName("legend", "div", target, DOM.removeElement); | |
1754 | |
1755 if (!options.legend.show) | |
1756 return; | |
1757 | |
1758 var fragments = [], rowStarted = false, | |
1759 lf = options.legend.labelFormatter, s, label; | |
1760 for (var i = 0; i < series.length; ++i) { | |
1761 s = series[i]; | |
1762 label = s.label; | |
1763 if (!label) | |
1764 continue; | |
1765 | |
1766 if (i % options.legend.noColumns == 0) { | |
1767 if (rowStarted) | |
1768 fragments.push('</tr>'); | |
1769 fragments.push('<tr>'); | |
1770 rowStarted = true; | |
1771 } | |
1772 | |
1773 if (lf) | |
1774 label = lf(label, s); | |
1775 | |
1776 fragments.push( | |
1777 '<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>' + | |
1778 '<td class="legendLabel">' + label + '</td>'); | |
1779 } | |
1780 if (rowStarted) | |
1781 fragments.push('</tr>'); | |
1782 | |
1783 if (fragments.length == 0) | |
1784 return; | |
1785 | |
1786 var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>'; | |
1787 if (options.legend.container != null) | |
1788 DOM.get(options.legend.container).innerHTML = table; | |
1789 else { | |
1790 var pos = "", | |
1791 p = options.legend.position, | |
1792 m = options.legend.margin; | |
1793 if (m[0] == null) | |
1794 m = [m, m]; | |
1795 if (p.charAt(0) == "n") | |
1796 pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; | |
1797 else if (p.charAt(0) == "s") | |
1798 pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; | |
1799 if (p.charAt(1) == "e") | |
1800 pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; | |
1801 else if (p.charAt(1) == "w") | |
1802 pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; | |
1803 var legend = target.appendChild(DOM.createElementFromMarkup('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>')); | |
1804 if (options.legend.backgroundOpacity != 0.0) { | |
1805 // put in the transparent background | |
1806 // separately to avoid blended labels and | |
1807 // label boxes | |
1808 var c = options.legend.backgroundColor; | |
1809 if (c == null) { | |
1810 var tmp; | |
1811 if (options.grid.backgroundColor && typeof options.grid.backgroundColor == "string") | |
1812 tmp = options.grid.backgroundColor; | |
1813 else | |
1814 tmp = extractColor(legend); | |
1815 c = parseColor(tmp).adjust(null, null, null, 1).toString(); | |
1816 } | |
1817 var div = legend.firstChild; | |
1818 var _el = DOM.insertBefore( | |
1819 DOM.createElementFromMarkup('<div style="position:absolute;width:' + parseInt(DOM.getStyle(div, 'width'), 10) | |
1820 + 'px;height:' + parseInt(DOM.getStyle(div, 'height'), 10) + 'px;' | |
1821 + pos +'background-color:' + c + ';"> </div>'), | |
1822 legend | |
1823 ); | |
1824 DOM.setStyle(_el, 'opacity', options.legend.backgroundOpacity); | |
1825 } | |
1826 } | |
1827 } | |
1828 | |
1829 | |
1830 // interactive features | |
1831 | |
1832 var lastMousePos = { pageX: null, pageY: null }, | |
1833 selection = { | |
1834 first: { x: -1, y: -1}, second: { x: -1, y: -1}, | |
1835 show: false, active: false }, | |
1836 crosshair = { pos: { x: -1, y: -1 } }, | |
1837 highlights = [], | |
1838 clickIsMouseUp = false, | |
1839 redrawTimeout = null, | |
1840 hoverTimeout = null; | |
1841 | |
1842 // Returns the data item the mouse is over, or null if none is found | |
1843 function findNearbyItem(mouseX, mouseY) { | |
1844 var maxDistance = options.grid.mouseActiveRadius, | |
1845 lowestDistance = maxDistance * maxDistance + 1, | |
1846 item = null, foundPoint = false, j, x, y; | |
1847 | |
1848 function result(i, j) { | |
1849 return { | |
1850 datapoint: series[i].data[j], | |
1851 dataIndex: j, | |
1852 series: series[i], | |
1853 seriesIndex: i | |
1854 }; | |
1855 } | |
1856 | |
1857 for (var i = 0; i < series.length; ++i) { | |
1858 var s = series[i], | |
1859 axisx = s.xaxis, | |
1860 axisy = s.yaxis, | |
1861 mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster | |
1862 my = axisy.c2p(mouseY), | |
1863 maxx = maxDistance / axisx.scale, | |
1864 maxy = maxDistance / axisy.scale; | |
1865 | |
1866 var data = s.data; | |
1867 | |
1868 if (s.lines.show || s.points.show) { | |
1869 for (j = 0; j < data.length; j++ ) { | |
1870 if (data[j] == null) | |
1871 continue; | |
1872 | |
1873 x = data[j].x; | |
1874 y = data[j].y; | |
1875 | |
1876 // For points and lines, the cursor must be within a | |
1877 // certain distance to the data point | |
1878 if (x - mx > maxx || x - mx < -maxx || | |
1879 y - my > maxy || y - my < -maxy) | |
1880 continue; | |
1881 | |
1882 // We have to calculate distances in pixels, not in | |
1883 // data units, because the scales of the axes may be different | |
1884 var dx = Math.abs(axisx.p2c(x) - mouseX), | |
1885 dy = Math.abs(axisy.p2c(y) - mouseY), | |
1886 dist = dx * dx + dy * dy; // no idea in taking sqrt | |
1887 if (dist < lowestDistance) { | |
1888 lowestDistance = dist; | |
1889 item = result(i, j); | |
1890 } | |
1891 } | |
1892 } | |
1893 | |
1894 if (s.bars.show && !item) { // no other point can be nearby | |
1895 var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2, | |
1896 barRight = barLeft + s.bars.barWidth; | |
1897 | |
1898 for (j = 0; j < data.length; j++) { | |
1899 x = data[j].x; | |
1900 y = data[j].y; | |
1901 if (x == null) | |
1902 continue; | |
1903 | |
1904 // for a bar graph, the cursor must be inside the bar | |
1905 if ((mx >= x + barLeft && mx <= x + barRight && | |
1906 my >= Math.min(0, y) && my <= Math.max(0, y))) | |
1907 item = result(i, j); | |
1908 } | |
1909 } | |
1910 } | |
1911 | |
1912 return item; | |
1913 } | |
1914 | |
1915 function onMouseMove(e) { | |
1916 lastMousePos.pageX = E.getPageX(e); | |
1917 lastMousePos.pageY = E.getPageY(e); | |
1918 | |
1919 if (options.grid.hoverable) | |
1920 triggerClickHoverEvent("plothover", lastMousePos); | |
1921 | |
1922 if (options.crosshair.mode != null) { | |
1923 if (!selection.active) { | |
1924 setPositionFromEvent(crosshair.pos, lastMousePos); | |
1925 triggerRedrawOverlay(); | |
1926 } | |
1927 else | |
1928 crosshair.pos.x = -1; // hide the crosshair while selecting | |
1929 } | |
1930 | |
1931 if (selection.active) { | |
1932 updateSelection(lastMousePos); | |
1933 } | |
1934 } | |
1935 | |
1936 function onMouseDown(e) { | |
1937 var button = e.which || e.button; | |
1938 if (button != 1) // only accept left-click | |
1939 return; | |
1940 | |
1941 // cancel out any text selections | |
1942 document.body.focus(); | |
1943 | |
1944 // prevent text selection and drag in old-school browsers | |
1945 if (document.onselectstart !== undefined && workarounds.onselectstart == null) { | |
1946 workarounds.onselectstart = document.onselectstart; | |
1947 document.onselectstart = function () { return false; }; | |
1948 } | |
1949 if (document.ondrag !== undefined && workarounds.ondrag == null) { | |
1950 workarounds.ondrag = document.ondrag; | |
1951 document.ondrag = function () { return false; }; | |
1952 } | |
1953 | |
1954 var mousePos = {pageX: E.getPageX(e), pageY: E.getPageY(e)}; | |
1955 setSelectionPos(selection.first, mousePos); | |
1956 | |
1957 lastMousePos.pageX = null; | |
1958 selection.active = true; | |
1959 E.on(document, "mouseup", onSelectionMouseUp); | |
1960 } | |
1961 | |
1962 function onMouseOut(e) { | |
1963 if (options.crosshair.mode != null && crosshair.pos.x != -1) { | |
1964 crosshair.pos.x = -1; | |
1965 triggerRedrawOverlay(); | |
1966 } | |
1967 } | |
1968 | |
1969 function onClick(e) { | |
1970 if (clickIsMouseUp) { | |
1971 clickIsMouseUp = false; | |
1972 return; | |
1973 } | |
1974 | |
1975 var mousePos = {pageX: E.getPageX(e), pageY: E.getPageY(e)}; | |
1976 triggerClickHoverEvent("plotclick", mousePos); | |
1977 } | |
1978 | |
1979 // trigger click or hover event (they send the same parameters | |
1980 // so we share their code) | |
1981 function triggerClickHoverEvent(eventname, event) { | |
1982 var offset = DOM.getXY(eventHolder[0]), | |
1983 pos = { pageX: event.pageX, pageY: event.pageY }, | |
1984 canvasX = event.pageX - offset[0] - plotOffset.left, | |
1985 canvasY = event.pageY - offset[1] - plotOffset.top; | |
1986 | |
1987 for(var axis in axes) | |
1988 if(axes[axis].used) | |
1989 pos[axis.replace(/axis$/, '')] = axes[axis].c2p(axis.charAt(0) == 'x' ? canvasX : canvasY); | |
1990 | |
1991 var item = findNearbyItem(canvasX, canvasY); | |
1992 | |
1993 if (item) { | |
1994 // fill in mouse pos for any listeners out there | |
1995 item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint.x) + offset[0] + plotOffset.left, 10); | |
1996 item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint.y) + offset[1] + plotOffset.top, 10); | |
1997 } | |
1998 | |
1999 if (options.grid.autoHighlight) { | |
2000 // clear auto-highlights | |
2001 for (var i = 0; i < highlights.length; ++i) { | |
2002 var h = highlights[i]; | |
2003 if (h.auto == eventname && | |
2004 !(item && h.series == item.series && h.point == item.datapoint)) | |
2005 unhighlight(h.series, h.point); | |
2006 } | |
2007 | |
2008 if (item) | |
2009 highlight(item.series, item.datapoint, eventname); | |
2010 } | |
2011 | |
2012 plot.fireEvent(eventname, {pos: pos, item: item }); | |
2013 } | |
2014 | |
2015 function triggerRedrawOverlay() { | |
2016 if (!redrawTimeout) | |
2017 redrawTimeout = setTimeout(redrawOverlay, 30); | |
2018 } | |
2019 | |
2020 function redrawOverlay() { | |
2021 redrawTimeout = null; | |
2022 | |
2023 // redraw highlights | |
2024 octx.save(); | |
2025 octx.clearRect(0, 0, canvasWidth, canvasHeight); | |
2026 octx.translate(plotOffset.left, plotOffset.top); | |
2027 | |
2028 var hi; | |
2029 for (var i = 0; i < highlights.length; ++i) { | |
2030 hi = highlights[i]; | |
2031 | |
2032 if (hi.series.bars.show) | |
2033 drawBarHighlight(hi.series, hi.point); | |
2034 else | |
2035 drawPointHighlight(hi.series, hi.point); | |
2036 } | |
2037 | |
2038 // redraw selection | |
2039 if (selection.show && selectionIsSane()) { | |
2040 octx.strokeStyle = parseColor(options.selection.color).scale(null, null, null, 0.8).toString(); | |
2041 octx.lineWidth = 1; | |
2042 ctx.lineJoin = "round"; | |
2043 octx.fillStyle = parseColor(options.selection.color).scale(null, null, null, 0.4).toString(); | |
2044 | |
2045 var x = Math.min(selection.first.x, selection.second.x), | |
2046 y = Math.min(selection.first.y, selection.second.y), | |
2047 w = Math.abs(selection.second.x - selection.first.x), | |
2048 h = Math.abs(selection.second.y - selection.first.y); | |
2049 | |
2050 octx.fillRect(x, y, w, h); | |
2051 octx.strokeRect(x, y, w, h); | |
2052 } | |
2053 | |
2054 // redraw crosshair | |
2055 var pos = crosshair.pos, mode = options.crosshair.mode; | |
2056 if (mode != null && pos.x != -1) { | |
2057 octx.strokeStyle = parseColor(options.crosshair.color).scale(null, null, null, 0.8).toString(); | |
2058 octx.lineWidth = 1; | |
2059 ctx.lineJoin = "round"; | |
2060 | |
2061 octx.beginPath(); | |
2062 if (mode.indexOf("x") != -1) { | |
2063 octx.moveTo(pos.x, 0); | |
2064 octx.lineTo(pos.x, plotHeight); | |
2065 } | |
2066 if (mode.indexOf("y") != -1) { | |
2067 octx.moveTo(0, pos.y); | |
2068 octx.lineTo(plotWidth, pos.y); | |
2069 } | |
2070 octx.stroke(); | |
2071 | |
2072 } | |
2073 octx.restore(); | |
2074 } | |
2075 | |
2076 function highlight(s, point, auto) { | |
2077 if (typeof s == "number") | |
2078 s = series[s]; | |
2079 | |
2080 if (typeof point == "number") | |
2081 point = s.data[point]; | |
2082 | |
2083 var i = indexOfHighlight(s, point); | |
2084 if (i == -1) { | |
2085 highlights.push({ series: s, point: point, auto: auto }); | |
2086 | |
2087 triggerRedrawOverlay(); | |
2088 } | |
2089 else if (!auto) | |
2090 highlights[i].auto = false; | |
2091 } | |
2092 | |
2093 function unhighlight(s, point) { | |
2094 if (typeof s == "number") | |
2095 s = series[s]; | |
2096 | |
2097 if (typeof point == "number") | |
2098 point = s.data[point]; | |
2099 | |
2100 var i = indexOfHighlight(s, point); | |
2101 if (i != -1) { | |
2102 highlights.splice(i, 1); | |
2103 | |
2104 triggerRedrawOverlay(); | |
2105 } | |
2106 } | |
2107 | |
2108 function indexOfHighlight(s, p) { | |
2109 for (var i = 0; i < highlights.length; ++i) { | |
2110 var h = highlights[i]; | |
2111 if (h.series == s && h.point[0] == p[0] | |
2112 && h.point[1] == p[1]) | |
2113 return i; | |
2114 } | |
2115 return -1; | |
2116 } | |
2117 | |
2118 function drawPointHighlight(series, point) { | |
2119 var x = point.x, y = point.y, | |
2120 axisx = series.xaxis, axisy = series.yaxis; | |
2121 | |
2122 if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) | |
2123 return; | |
2124 | |
2125 var pointRadius = series.points.radius + series.points.lineWidth / 2; | |
2126 octx.lineWidth = pointRadius; | |
2127 octx.strokeStyle = parseColor(series.color).scale(1, 1, 1, 0.5).toString(); | |
2128 var radius = 1.5 * pointRadius; | |
2129 octx.beginPath(); | |
2130 octx.arc(axisx.p2c(x), axisy.p2c(y), radius, 0, 2 * Math.PI, true); | |
2131 octx.stroke(); | |
2132 } | |
2133 | |
2134 function drawBarHighlight(series, point) { | |
2135 octx.lineJoin = "round"; | |
2136 octx.lineWidth = series.bars.lineWidth; | |
2137 octx.strokeStyle = parseColor(series.color).scale(1, 1, 1, 0.5).toString(); | |
2138 var fillStyle = parseColor(series.color).scale(1, 1, 1, 0.5).toString(); | |
2139 var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2; | |
2140 drawBar(point.x, point.y, barLeft, barLeft + series.bars.barWidth, | |
2141 0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx); | |
2142 } | |
2143 | |
2144 function setPositionFromEvent(pos, e) { | |
2145 var offset = DOM.getXY(eventHolder[0]); | |
2146 pos.x = clamp(0, e.pageX - offset[0] - plotOffset.left, plotWidth); | |
2147 pos.y = clamp(0, e.pageY - offset[1] - plotOffset.top, plotHeight); | |
2148 } | |
2149 | |
2150 function setCrosshair(pos) { | |
2151 if (pos == null) | |
2152 crosshair.pos.x = -1; | |
2153 else { | |
2154 crosshair.pos.x = clamp(0, pos.x != null ? axes.xaxis.p2c(pos.x) : axes.x2axis.p2c(pos.x2), plotWidth); | |
2155 crosshair.pos.y = clamp(0, pos.y != null ? axes.yaxis.p2c(pos.y) : axes.y2axis.p2c(pos.y2), plotHeight); | |
2156 } | |
2157 triggerRedrawOverlay(); | |
2158 } | |
2159 | |
2160 function getSelectionForEvent() { | |
2161 var x1 = Math.min(selection.first.x, selection.second.x), | |
2162 x2 = Math.max(selection.first.x, selection.second.x), | |
2163 y1 = Math.max(selection.first.y, selection.second.y), | |
2164 y2 = Math.min(selection.first.y, selection.second.y); | |
2165 | |
2166 var r = {}; | |
2167 if (axes.xaxis.used) | |
2168 r.xaxis = { from: axes.xaxis.c2p(x1), to: axes.xaxis.c2p(x2) }; | |
2169 if (axes.x2axis.used) | |
2170 r.x2axis = { from: axes.x2axis.c2p(x1), to: axes.x2axis.c2p(x2) }; | |
2171 if (axes.yaxis.used) | |
2172 r.yaxis = { from: axes.yaxis.c2p(y1), to: axes.yaxis.c2p(y2) }; | |
2173 if (axes.y2axis.used) | |
2174 r.y2axis = { from: axes.y2axis.c2p(y1), to: axes.y2axis.c2p(y2) }; | |
2175 return r; | |
2176 } | |
2177 | |
2178 function triggerSelectedEvent() { | |
2179 var r = getSelectionForEvent(); | |
2180 | |
2181 plot.fireEvent("plotselected", r); | |
2182 } | |
2183 | |
2184 function onSelectionMouseUp(e) { | |
2185 // revert drag stuff for old-school browsers | |
2186 if (document.onselectstart !== undefined) | |
2187 document.onselectstart = workarounds.onselectstart; | |
2188 if (document.ondrag !== undefined) | |
2189 document.ondrag = workarounds.ondrag; | |
2190 | |
2191 // no more draggy-dee-drag | |
2192 selection.active = false; | |
2193 var mousePos = {pageX: E.getPageX(e), pageY: E.getPageY(e)}; | |
2194 updateSelection(mousePos); | |
2195 | |
2196 if (selectionIsSane()) { | |
2197 triggerSelectedEvent(); | |
2198 clickIsMouseUp = true; | |
2199 } | |
2200 else { | |
2201 // this counts as a clear | |
2202 plot.fireEvent("plotunselected", {}); | |
2203 } | |
2204 | |
2205 E.removeListener(document, "mouseup", onSelectionMouseUp); | |
2206 return false; | |
2207 } | |
2208 | |
2209 function setSelectionPos(pos, e) { | |
2210 setPositionFromEvent(pos, e); | |
2211 | |
2212 if (options.selection.mode == "y") { | |
2213 if (pos == selection.first) | |
2214 pos.x = 0; | |
2215 else | |
2216 pos.x = plotWidth; | |
2217 } | |
2218 | |
2219 if (options.selection.mode == "x") { | |
2220 if (pos == selection.first) | |
2221 pos.y = 0; | |
2222 else | |
2223 pos.y = plotHeight; | |
2224 } | |
2225 } | |
2226 | |
2227 function updateSelection(pos) { | |
2228 if (pos.pageX == null) | |
2229 return; | |
2230 | |
2231 setSelectionPos(selection.second, pos); | |
2232 if (selectionIsSane()) { | |
2233 selection.show = true; | |
2234 triggerRedrawOverlay(); | |
2235 } | |
2236 else | |
2237 clearSelection(true); | |
2238 } | |
2239 | |
2240 function clearSelection(preventEvent) { | |
2241 if (selection.show) { | |
2242 selection.show = false; | |
2243 triggerRedrawOverlay(); | |
2244 if (!preventEvent) | |
2245 plot.fireEvent("plotunselected", {}); | |
2246 } | |
2247 } | |
2248 | |
2249 function setSelection(ranges, preventEvent) { | |
2250 var range; | |
2251 | |
2252 if (options.selection.mode == "y") { | |
2253 selection.first.x = 0; | |
2254 selection.second.x = plotWidth; | |
2255 } | |
2256 else { | |
2257 range = extractRange(ranges, "x"); | |
2258 | |
2259 selection.first.x = range.axis.p2c(range.from); | |
2260 selection.second.x = range.axis.p2c(range.to); | |
2261 } | |
2262 | |
2263 if (options.selection.mode == "x") { | |
2264 selection.first.y = 0; | |
2265 selection.second.y = plotHeight; | |
2266 } | |
2267 else { | |
2268 range = extractRange(ranges, "y"); | |
2269 | |
2270 selection.first.y = range.axis.p2c(range.from); | |
2271 selection.second.y = range.axis.p2c(range.to); | |
2272 } | |
2273 | |
2274 selection.show = true; | |
2275 triggerRedrawOverlay(); | |
2276 if (!preventEvent) | |
2277 triggerSelectedEvent(); | |
2278 } | |
2279 | |
2280 function selectionIsSane() { | |
2281 var minSize = 5; | |
2282 return Math.abs(selection.second.x - selection.first.x) >= minSize && | |
2283 Math.abs(selection.second.y - selection.first.y) >= minSize; | |
2284 } | |
2285 | |
2286 function getColorOrGradient(spec, bottom, top, defaultColor) { | |
2287 if (typeof spec == "string") | |
2288 return spec; | |
2289 else { | |
2290 // assume this is a gradient spec; IE currently only | |
2291 // supports a simple vertical gradient properly, so that's | |
2292 // what we support too | |
2293 var gradient = ctx.createLinearGradient(0, top, 0, bottom); | |
2294 | |
2295 for (var i = 0, l = spec.colors.length; i < l; ++i) { | |
2296 var c = spec.colors[i]; | |
2297 gradient.addColorStop(i / (l - 1), typeof c == "string" ? c : parseColor(defaultColor).scale(c.brightness, c.brightness, c.brightness, c.opacity)); | |
2298 } | |
2299 | |
2300 return gradient; | |
2301 } | |
2302 } | |
2303 } | |
2304 | |
2305 L.augment(Plot, YAHOO.util.EventProvider); | |
2306 | |
2307 YAHOO.widget.Flot = function(target, data, options) { | |
2308 return new Plot(target, data, options); | |
2309 }; | |
2310 | |
2311 // round to nearby lower multiple of base | |
2312 function floorInBase(n, base) { | |
2313 return base * Math.floor(n / base); | |
2314 } | |
2315 | |
2316 function clamp(min, value, max) { | |
2317 if (value < min) | |
2318 return min; | |
2319 else if (value > max) | |
2320 return max; | |
2321 else | |
2322 return value; | |
2323 } | |
2324 | |
2325 // color helpers, inspiration from the jquery color animation | |
2326 // plugin by John Resig | |
2327 function Color (r, g, b, a) { | |
2328 | |
2329 var rgba = ['r','g','b','a']; | |
2330 var x = 4; //rgba.length | |
2331 | |
2332 while (-1<--x) { | |
2333 this[rgba[x]] = arguments[x] || ((x==3) ? 1.0 : 0); | |
2334 } | |
2335 | |
2336 this.toString = function() { | |
2337 if (this.a >= 1.0) { | |
2338 return "rgb("+[this.r,this.g,this.b].join(",")+")"; | |
2339 } else { | |
2340 return "rgba("+[this.r,this.g,this.b,this.a].join(",")+")"; | |
2341 } | |
2342 }; | |
2343 | |
2344 this.scale = function(rf, gf, bf, af) { | |
2345 x = 4; //rgba.length | |
2346 while (-1<--x) { | |
2347 if (arguments[x] != null) | |
2348 this[rgba[x]] *= arguments[x]; | |
2349 } | |
2350 return this.normalize(); | |
2351 }; | |
2352 | |
2353 this.adjust = function(rd, gd, bd, ad) { | |
2354 x = 4; //rgba.length | |
2355 while (-1<--x) { | |
2356 if (arguments[x] != null) | |
2357 this[rgba[x]] += arguments[x]; | |
2358 } | |
2359 return this.normalize(); | |
2360 }; | |
2361 | |
2362 this.clone = function() { | |
2363 return new Color(this.r, this.b, this.g, this.a); | |
2364 }; | |
2365 | |
2366 var limit = function(val,minVal,maxVal) { | |
2367 return Math.max(Math.min(val, maxVal), minVal); | |
2368 }; | |
2369 | |
2370 this.normalize = function() { | |
2371 this.r = clamp(0, parseInt(this.r, 10), 255); | |
2372 this.g = clamp(0, parseInt(this.g, 10), 255); | |
2373 this.b = clamp(0, parseInt(this.b, 10), 255); | |
2374 this.a = clamp(0, this.a, 1); | |
2375 return this; | |
2376 }; | |
2377 | |
2378 this.normalize(); | |
2379 } | |
2380 | |
2381 var lookupColors = { | |
2382 aqua:[0,255,255], | |
2383 azure:[240,255,255], | |
2384 beige:[245,245,220], | |
2385 black:[0,0,0], | |
2386 blue:[0,0,255], | |
2387 brown:[165,42,42], | |
2388 cyan:[0,255,255], | |
2389 darkblue:[0,0,139], | |
2390 darkcyan:[0,139,139], | |
2391 darkgrey:[169,169,169], | |
2392 darkgreen:[0,100,0], | |
2393 darkkhaki:[189,183,107], | |
2394 darkmagenta:[139,0,139], | |
2395 darkolivegreen:[85,107,47], | |
2396 darkorange:[255,140,0], | |
2397 darkorchid:[153,50,204], | |
2398 darkred:[139,0,0], | |
2399 darksalmon:[233,150,122], | |
2400 darkviolet:[148,0,211], | |
2401 fuchsia:[255,0,255], | |
2402 gold:[255,215,0], | |
2403 green:[0,128,0], | |
2404 indigo:[75,0,130], | |
2405 khaki:[240,230,140], | |
2406 lightblue:[173,216,230], | |
2407 lightcyan:[224,255,255], | |
2408 lightgreen:[144,238,144], | |
2409 lightgrey:[211,211,211], | |
2410 lightpink:[255,182,193], | |
2411 lightyellow:[255,255,224], | |
2412 lime:[0,255,0], | |
2413 magenta:[255,0,255], | |
2414 maroon:[128,0,0], | |
2415 navy:[0,0,128], | |
2416 olive:[128,128,0], | |
2417 orange:[255,165,0], | |
2418 pink:[255,192,203], | |
2419 purple:[128,0,128], | |
2420 violet:[128,0,128], | |
2421 red:[255,0,0], | |
2422 silver:[192,192,192], | |
2423 white:[255,255,255], | |
2424 yellow:[255,255,0] | |
2425 }; | |
2426 | |
2427 function extractColor(element) { | |
2428 var color, elem = element; | |
2429 do { | |
2430 color = DOM.getStyle(elem, 'backgroundColor').toLowerCase(); | |
2431 // keep going until we find an element that has color, or | |
2432 // we hit the body | |
2433 if (color != '' && color != 'transparent') | |
2434 break; | |
2435 elem = elem.parentNode; | |
2436 } while (!elem.nodeName == "body"); | |
2437 | |
2438 // catch Safari's way of signalling transparent | |
2439 if (color == "rgba(0, 0, 0, 0)") | |
2440 return "transparent"; | |
2441 | |
2442 return color; | |
2443 } | |
2444 | |
2445 // parse string, returns Color | |
2446 function parseColor(str) { | |
2447 var result; | |
2448 | |
2449 // Look for rgb(num,num,num) | |
2450 if (result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str)) | |
2451 return new Color(parseInt(result[1], 10), parseInt(result[2], 10), parseInt(result[3], 10)); | |
2452 | |
2453 // Look for rgba(num,num,num,num) | |
2454 if (result = /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(str)) | |
2455 return new Color(parseInt(result[1], 10), parseInt(result[2], 10), parseInt(result[3], 10), parseFloat(result[4])); | |
2456 | |
2457 // Look for rgb(num%,num%,num%) | |
2458 if (result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str)) | |
2459 return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55); | |
2460 | |
2461 // Look for rgba(num%,num%,num%,num) | |
2462 if (result = /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(str)) | |
2463 return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55, parseFloat(result[4])); | |
2464 | |
2465 // Look for #a0b1c2 | |
2466 if (result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str)) | |
2467 return new Color(parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)); | |
2468 | |
2469 // Look for #fff | |
2470 if (result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str)) | |
2471 return new Color(parseInt(result[1]+result[1], 16), parseInt(result[2]+result[2], 16), parseInt(result[3]+result[3], 16)); | |
2472 | |
2473 // Otherwise, we're most likely dealing with a named color | |
2474 var name = L.trim(str).toLowerCase(); | |
2475 if (name == "transparent") | |
2476 return new Color(255, 255, 255, 0); | |
2477 else { | |
2478 result = lookupColors[name]; | |
2479 return new Color(result[0], result[1], result[2]); | |
2480 } | |
2481 } | |
2482 | |
2483 })(); |