Project

General

Profile

Download (78.1 KB) Statistics
| Branch: | Tag: | Revision:
1
/**
2
 * Class: ProtoChart 
3
 * Version: v0.5 beta
4
 * 
5
 * ProtoChart is a charting lib on top of Prototype.
6
 * This library is heavily motivated by excellent work done by:
7
 * * Flot <http://code.google.com/p/flot/>
8
 * * Flotr <http://solutoire.com/flotr/>
9
 * 
10
 * Complete examples can be found at: <http://www.deensoft.com/lab/protochart>
11
 */
12

    
13
/**
14
 * Events:
15
 * ProtoChart:mousemove - Fired when mouse is moved over the chart
16
 * ProtoChart:plotclick - Fired when graph is clicked
17
 * ProtoChart:dataclick - Fired when graph is clicked AND the click is on a data point
18
 * ProtoChart:selected	- Fired when certain region on the graph is selected
19
 * ProtoChart:hit		- Fired when mouse is moved near or over certain data point on the graph
20
 */
21

    
22

    
23
if(!Proto) var Proto = {};
24

    
25
Proto.Chart = Class.create({
26
	/**
27
	 * Function: 
28
	 * {Object} elem
29
	 * {Object} data
30
	 * {Object} options
31
	 */
32
	initialize: function(elem, data, options)
33
	{
34
		options = options || {};
35
		this.graphData = [];
36
		/**
37
		 * Property: options
38
		 * 
39
		 * Description: Various options can be set. More details in description.
40
		 * 
41
		 * colors:
42
		 * {Array}		- pass in a array which contains strings of colors you want to use. Default has 6 color set.
43
		 * 
44
		 * legend:
45
		 * {BOOL} 		- show				- if you want to show the legend. Default is false
46
		 * {integer} 	- noColumns			- Number of columns for the legend. Default is 1
47
		 * {function} 	- labelFormatter	- A function that returns a string. The function is called with a string and is expected to return a string. Default = null
48
		 * {string}		- labelBoxBorderColor - border color for the little label boxes. Default #CCC
49
		 * {HTMLElem}	- container			- an HTML id or HTML element where the legend should be rendered. If left null means to put the legend on top of the Chart
50
		 * {string}		- position			- position for the legend on the Chart. Default value 'ne'
51
		 * {integer}	- margin			- default valud of 5
52
		 * {string} 	- backgroundColor	- default to null (which means auto-detect)
53
		 * {float} 		- backgroundOpacity - leave it 0 to avoid background
54
		 * 
55
		 * xaxis (yaxis) options:
56
		 * {string} 	- mode 		- default is null but you can pass a string "time" to indicate time series
57
		 * {integer}	- min
58
		 * {integer}	- max
59
		 * {float}		- autoscaleMargin - in % to add if auto-setting min/max
60
		 * {mixed}		- ticks - either [1, 3] or [[1, "a"], 3] or a function which gets axis info and returns ticks
61
		 * {function} 	- tickFormatter - A function that returns a string as a tick label. Default is null
62
		 * {float}		- tickDecimals
63
		 * {integer}	- tickSize
64
		 * {integer} 	- minTickSize
65
		 * {array}		- monthNames
66
		 * {string}		- timeformat
67
		 * 
68
		 * Points / Lines / Bars options:
69
		 * {bool}		- show, default is false
70
		 * {integer}	- radius: default is 3
71
		 * {integer}	- lineWidth : default is 2
72
		 * {bool}		- fill : default is true
73
		 * {string}		- fillColor: default is #ffffff
74
		 * 
75
		 * Grid options:
76
		 * {string}		- color
77
		 * {string}		- backgroundColor 	- defualt is *null*
78
		 * {string}		- tickColor			- default is *#dddddd*
79
		 * {integer}	- labelMargin		- should be in pixels default is 3
80
		 * {integer}	- borderWidth		- default *1*
81
		 * {bool}		- clickable 		- default *null* - pass in TRUE if you wish to monitor click events
82
		 * {mixed}		- coloredAreas 		- default *null* - pass in mixed object eg. {x1, x2}
83
		 * {string}		- coloredAreasColor	- default *#f4f4f4*
84
		 * {bool}		- drawXAxis			- default *true*
85
		 * {bool}		- drawYAxis			- default *true*
86
		 * 
87
		 * selection options:
88
		 * {string}		- mode : either "x", "y" or "xy"
89
		 * {string}		- color : string
90
		 */
91
		this.options = this.merge(options,{
92
			colors: ["#edc240", "#00A8F0", "#C0D800", "#cb4b4b", "#4da74d", "#9440ed"],
93
            legend: {
94
                show: false,
95
                noColumns: 1,
96
                labelFormatter: null,
97
                labelBoxBorderColor: "#ccc",
98
                container: null, 
99
                position: "ne",
100
                margin: 5,
101
                backgroundColor: null,
102
                backgroundOpacity: 0.85
103
            },
104
            xaxis: {
105
				mode: null, 
106
                min: null,
107
                max: null,
108
                autoscaleMargin: null,
109
                ticks: null,
110
                tickFormatter: null,
111
                tickDecimals: null,
112
                tickSize: null,
113
                minTickSize: null,
114
                monthNames: null,
115
                timeformat: null
116
            },
117
            yaxis: {
118
				mode: null,
119
				min: null,
120
				max: null,
121
				ticks: null,
122
				tickFormatter: null,
123
				tickDecimals: null,
124
				tickSize: null,
125
				minTickSize: null,
126
				monthNames: null,
127
				timeformat: null,				
128
                autoscaleMargin: 0.02
129
            },
130

    
131
            points: {
132
                show: false,
133
                radius: 3,
134
                lineWidth: 2,
135
                fill: true,
136
                fillColor: "#ffffff"
137
            },
138
            lines: {
139
                show: false,
140
                lineWidth: 2,
141
                fill: false,
142
                fillColor: null
143
            },
144
            bars: {
145
                show: false,
146
                lineWidth: 2,
147
                barWidth: 1,
148
                fill: true,
149
                fillColor: null,
150
				showShadow: false,
151
				fillOpacity: 0.4,
152
				autoScale: true
153
            },
154
			pies: {
155
				show: false,
156
				radius: 50,
157
				borderWidth: 1,
158
				fill: true,
159
				fillColor: null,
160
				fillOpacity: 0.90,
161
				labelWidth: 30,
162
				fontSize: 11,
163
				autoScale: true
164
			},
165
            grid: {
166
                color: "#545454",
167
                backgroundColor: null,
168
                tickColor: "#dddddd",
169
                labelMargin: 3,
170
                borderWidth: 1,
171
                clickable: null,
172
                coloredAreas: null,
173
                coloredAreasColor: "#f4f4f4",
174
				drawXAxis: true,
175
				drawYAxis: true
176
            },
177
			mouse: {
178
				track: false,
179
				position: 'se',
180
				fixedPosition: true,
181
				clsName: 'mouseValHolder',
182
				trackFormatter: this.defaultTrackFormatter,
183
				margin: 3,
184
				color: '#ff3f19',
185
				trackDecimals: 1,
186
				sensibility: 2,
187
				radius: 5,
188
				lineColor: '#cb4b4b'
189
			},
190
            selection: {
191
                mode: null,
192
                color: "#97CBFF"
193
            },
194
			allowDataClick: true,
195
			makeRandomColor: false,
196
            shadowSize: 4			
197
		});
198
		
199
		/*
200
		 * Local variables.
201
		 */
202
		this.canvas = null; 
203
		this.overlay = null;
204
		this.eventHolder = null;
205
		this.context = null;
206
		this.overlayContext = null;
207
		
208
		this.domObj = $(elem);
209

    
210
		this.xaxis = {};
211
		this.yaxis = {};
212
		this.chartOffset = {left: 0, right: 0, top: 0, bottom: 0};
213
		this.yLabelMaxWidth = 0;
214
		this.yLabelMaxHeight = 0;
215
		this.xLabelBoxWidth = 0;
216
		this.canvasWidth = 0;
217
		this.canvasHeight = 0;
218
		this.chartWidth = 0;
219
		this.chartHeight = 0;
220
		this.hozScale = 0;
221
		this.vertScale = 0;
222
		this.workarounds = {};
223
		
224
		this.domObj = $(elem);
225
		
226
		this.barDataRange = [];
227
		
228
        this.lastMousePos = { pageX: null, pageY: null };
229
        this.selection = { first: { x: -1, y: -1}, second: { x: -1, y: -1} };
230
        this.prevSelection = null;
231
        this.selectionInterval = null;
232
        this.ignoreClick = false;	
233
		this.prevHit = null;
234
			
235
		if(this.options.makeRandomColor)
236
			this.options.color = this.makeRandomColor(this.options.colors);
237
		
238
		this.setData(data);
239
		this.constructCanvas();
240
		this.setupGrid();
241
		this.draw();
242
	},
243
	/**
244
	 * Private function internally used.
245
	 */
246
	merge: function(src, dest)
247
	{
248
		var result = dest || {};
249
		for(var i in src){		  
250
			result[i] = (typeof(src[i]) == 'object' && !(src[i].constructor == Array || src[i].constructor == RegExp)) ? this.merge(src[i], dest[i]) : result[i] = src[i];		
251
		}
252
		return result;	
253
	},
254
	/**
255
	 * Function: setData
256
	 * {Object} data
257
	 * 
258
	 * Description:
259
	 * Sets datasoruces properly then sets the Bar Width accordingly, then copies the default data options and then processes the graph data
260
	 * 
261
	 * Returns: none
262
	 * 
263
	 */	
264
	setData: function(data) 
265
	{
266
        this.graphData = this.parseData(data);		
267
		this.setBarWidth();
268
        this.copyGraphDataOptions();
269
        this.processGraphData();
270
    },
271
	/**
272
	 * Function: parseData
273
	 * {Object} data
274
	 * 
275
	 * Return: 
276
	 * {Object} result
277
	 * 
278
	 * Description:
279
	 * Takes the provided data object and converts it into generic data that we can understand. User can pass in data in 3 different ways:
280
	 * - [d1, d2]
281
	 * - [{data: d1, label: "data1"}, {data: d2, label: "data2"}]
282
	 * - [d1, {data: d1, label: "data1"}]
283
	 * 
284
	 * This function parses these senarios and makes it readable
285
	 */
286
	parseData: function(data)
287
	{
288
		var res = [];
289
		data.each(function(d){
290
			var s;
291
			if(d.data) {
292
				s = {};
293
				for(var v in d) {
294
					s[v] = d[v];
295
				}
296
			}
297
			else {
298
				s = {data: d};
299
			}
300
			res.push(s);
301
		}.bind(this));
302
		return res;
303
	},
304
	/**
305
	 * function: makeRandomColor
306
	 * {Object} colorSet
307
	 * 
308
	 * Return: 
309
	 * {Array} result - array containing random colors
310
	 */
311
	makeRandomColor: function(colorSet)
312
	{
313
		var randNum = Math.floor(Math.random() * colorSet.length);
314
		var randArr = [];
315
		var newArr = [];
316
		randArr.push(randNum);
317
		
318
		while(randArr.length < colorSet.length)
319
		{
320
			var tempNum = Math.floor(Math.random() * colorSet.length);
321
	
322
			while(checkExisted(tempNum, randArr))
323
				tempNum = Math.floor(Math.random() * colorSet.length);
324
				
325
			randArr.push(tempNum);
326
		}
327
		
328
		randArr.each(function(ra){
329
			newArr.push(colorSet[ra]);
330
			
331
		}.bind(this));
332
		return newArr;		
333
	},
334
	/**
335
	 * function: checkExisted
336
	 * {Object} needle
337
	 * {Object} haystack
338
	 * 
339
	 * return: 
340
	 * {bool} existed - true if it finds needle in the haystack
341
	 */
342
	checkExisted: function(needle, haystack)
343
	{
344
		var existed = false;
345
		haystack.each(function(aNeedle){
346
			if(aNeedle == needle) {
347
				existed = true;
348
				throw $break;
349
			}
350
		}.bind(this));
351
		return existed;
352
	},
353
	/**
354
	 * function: setBarWidth
355
	 * 
356
	 * Description: sets the bar width for Bar Graph, you should enable *autoScale* property for bar graph
357
	 */
358
	setBarWidth: function()
359
	{
360
		if(this.options.bars.show && this.options.bars.autoScale)
361
		{
362
			this.options.bars.barWidth = 1 / this.graphData.length / 1.2;	
363
		}
364
	},
365
	/**
366
	 * Function: copyGraphDataOptions
367
	 * 
368
	 * Description: Private function that goes through each graph data (series) and assigned the graph
369
	 * properties to it.
370
	 */
371
	copyGraphDataOptions: function()
372
	{
373
		var i, neededColors = this.graphData.length, usedColors = [], assignedColors = [];
374
		
375
		this.graphData.each(function(gd){
376
			var sc = gd.color;
377
			if(sc) {
378
				--neededColors;
379
				if(Object.isNumber(sc)) {
380
					assignedColors.push(sc);
381
				}
382
				else {
383
					usedColors.push(this.parseColor(sc));
384
				}
385
			}
386
		}.bind(this));
387
		
388
		
389
		assignedColors.each(function(ac){
390
			neededColors = Math.max(neededColors, ac + 1);
391
		});
392

    
393
        var colors = [];
394
        var variation = 0;
395
        i = 0;
396
        while (colors.length < neededColors) {
397
            var c;
398
            if (this.options.colors.length == i) {
399
				c = new Proto.Color(100, 100, 100);
400
			}
401
			else {
402
				c = this.parseColor(this.options.colors[i]);
403
			}
404

    
405
            var sign = variation % 2 == 1 ? -1 : 1;
406
            var factor = 1 + sign * Math.ceil(variation / 2) * 0.2;
407
            c.scale(factor, factor, factor);
408

    
409
            colors.push(c);
410
            
411
            ++i;
412
            if (i >= this.options.colors.length) {
413
                i = 0;
414
                ++variation;
415
            }
416
        }
417

    
418
        var colorIndex = 0, s;
419
		
420
		this.graphData.each(function(gd){
421
			if(gd.color == null)
422
			{
423
				gd.color = colors[colorIndex].toString();
424
				++colorIndex;
425
			}
426
			else if(Object.isNumber(gd.color)) {
427
				gd.color = colors[gd.color].toString();
428
			}
429
			
430
            gd.lines = Object.extend(Object.clone(this.options.lines), gd.lines); 
431
            gd.points = Object.extend(Object.clone(this.options.points), gd.points); 
432
            gd.bars = Object.extend(Object.clone(this.options.bars), gd.bars); 
433
            gd.mouse = Object.extend(Object.clone(this.options.mouse), gd.mouse);
434
            if (gd.shadowSize == null) {
435
                gd.shadowSize = this.options.shadowSize;
436
			}
437
		}.bind(this));
438
			
439
	},
440
	/**
441
	 * Function: processGraphData
442
	 * 
443
	 * Description: processes graph data, setup xaxis and yaxis min and max points. 
444
	 */
445
	processGraphData: function() {
446
		
447
		this.xaxis.datamin = this.yaxis.datamin = Number.MAX_VALUE;
448
		this.xaxis.datamax = this.yaxis.datamax = Number.MIN_VALUE;
449
			
450
		this.graphData.each(function(gd) {
451
			var data = gd.data;
452
			data.each(function(d){
453
				if(d == null) {
454
					return;
455
				}
456
				
457
				var x = d[0], y = d[1];
458
				if(!x || !y || isNaN(x = +x) || isNaN(y = +y)) {
459
					d = null;
460
					return;
461
				}
462
				
463
				if (x < this.xaxis.datamin)
464
					this.xaxis.datamin = x;
465
				if (x > this.xaxis.datamax)
466
					this.xaxis.datamax = x;
467
				if (y < this.yaxis.datamin)
468
					this.yaxis.datamin = y;
469
				if (y > this.yaxis.datamax)
470
					this.yaxis.datamax = y;
471
			}.bind(this));
472
		}.bind(this));
473

    
474
		
475
		if (this.xaxis.datamin == Number.MAX_VALUE)
476
			this.xaxis.datamin = 0;
477
		if (this.yaxis.datamin == Number.MAX_VALUE)
478
			this.yaxis.datamin = 0;
479
		if (this.xaxis.datamax == Number.MIN_VALUE)
480
			this.xaxis.datamax = 1;
481
		if (this.yaxis.datamax == Number.MIN_VALUE)
482
			this.yaxis.datamax = 1;
483
	},
484
	/**
485
	 * Function: constructCanvas
486
	 * 
487
	 * Description: constructs the main canvas for drawing. It replicates the HTML elem (usually DIV) passed
488
	 * in via constructor. If there is no height/width assigned to the HTML elem then we take a default size
489
	 * of 400px (width) and 300px (height)
490
	 */
491
	constructCanvas: function() {
492

    
493
		this.canvasWidth = this.domObj.getWidth();
494
		this.canvasHeight = this.domObj.getHeight();
495
		this.domObj.update(""); // clear target
496
		this.domObj.setStyle({
497
			"position": "relative"
498
		}); 
499

    
500
		if (this.canvasWidth <= 0) {
501
			this.canvasWdith = 400;
502
		}
503
		if(this.canvasHeight <= 0) {
504
			this.canvasHeight = 300;
505
		}
506
		
507
		this.canvas = (Prototype.Browser.IE) ? document.createElement("canvas") : new Element("CANVAS", {'width': this.canvasWidth, 'height': this.canvasHeight});
508
		Element.extend(this.canvas);
509
		this.canvas.style.width = this.canvasWidth + "px";
510
		this.canvas.style.height = this.canvasHeight + "px";
511
		
512
		this.domObj.appendChild(this.canvas);
513
		
514
		if (Prototype.Browser.IE) // excanvas hack
515
		{
516
			this.canvas = $(window.G_vmlCanvasManager.initElement(this.canvas));
517
		}
518
		this.canvas = $(this.canvas);
519
		
520
		this.context = this.canvas.getContext("2d");
521

    
522
		this.overlay = (Prototype.Browser.IE) ? document.createElement("canvas") :  new Element("CANVAS", {'width': this.canvasWidth, 'height': this.canvasHeight});
523
		Element.extend(this.overlay);
524
		this.overlay.style.width = this.canvasWidth + "px";
525
		this.overlay.style.height = this.canvasHeight + "px";
526
		this.overlay.style.position = "absolute";
527
		this.overlay.style.left = "0px";
528
		this.overlay.style.right = "0px";
529
		
530
		this.overlay.setStyle({
531
			'position': 'absolute',
532
			'left': '0px',
533
			'right': '0px'
534
		});
535
		this.domObj.appendChild(this.overlay);
536
		
537
		if (Prototype.Browser.IE) {
538
			this.overlay = $(window.G_vmlCanvasManager.initElement(this.overlay));
539
		}
540
		
541
		this.overlay = $(this.overlay);
542
		this.overlayContext = this.overlay.getContext("2d");
543

    
544
		if(this.options.selection.mode)
545
		{
546
			this.overlay.observe('mousedown', this.onMouseDown.bind(this));
547
			this.overlay.observe('mousemove', this.onMouseMove.bind(this));
548
		}
549
		if(this.options.grid.clickable) {
550
			this.overlay.observe('click', this.onClick.bind(this));
551
		}
552
		if(this.options.mouse.track)
553
		{
554
			this.overlay.observe('mousemove', this.onMouseMove.bind(this));
555
		}
556
	},
557
	/**
558
	 * function: setupGrid
559
	 * 
560
	 * Description: a container function that does a few interesting things.
561
	 * 
562
	 * 1. calls <extendXRangeIfNeededByBar> function which makes sure that our axis are expanded if needed
563
	 * 
564
	 * 2. calls <setRange> function providing xaxis options which fixes the ranges according to data points
565
	 * 
566
	 * 3. calls <prepareTickGeneration> function for xaxis which generates ticks according to options provided by user
567
	 * 
568
	 * 4. calls <setTicks> function for xaxis that sets the ticks
569
	 * 
570
	 * similar sequence is called for y-axis. 
571
	 * 
572
	 * At the end if this is a pie chart than we insert Labels (around the pie chart) via <insertLabels> and we also call <insertLegend>
573
	 */
574
	setupGrid: function()
575
	{
576
		if(this.options.bars.show)
577
		{
578
			this.xaxis.max += 0.5;
579
			this.xaxis.min -= 0.5;
580
		}
581
		//x-axis
582
		this.extendXRangeIfNeededByBar();
583
		this.setRange(this.xaxis, this.options.xaxis);
584
		this.prepareTickGeneration(this.xaxis, this.options.xaxis);
585
		this.setTicks(this.xaxis, this.options.xaxis);
586
		
587
		
588
		//y-axis
589
		this.setRange(this.yaxis, this.options.yaxis);
590
		this.prepareTickGeneration(this.yaxis, this.options.yaxis);
591
		this.setTicks(this.yaxis, this.options.yaxis);
592
		this.setSpacing();
593
		
594
		if(!this.options.pies.show)
595
		{
596
			this.insertLabels();
597
		}
598
		this.insertLegend();
599
	},
600
	/**
601
	 * function: setRange
602
	 * 
603
	 * parameters:
604
	 * {Object} axis
605
	 * {Object} axisOptions
606
	 */
607
	setRange: function(axis, axisOptions) {
608
		var min = axisOptions.min != null ? axisOptions.min : axis.datamin;
609
		var max = axisOptions.max != null ? axisOptions.max : axis.datamax;
610

    
611
		if (max - min == 0.0) {
612
			// degenerate case
613
			var widen;
614
			if (max == 0.0)
615
				widen = 1.0;
616
			else
617
				widen = 0.01;
618

    
619
			min -= widen;
620
			max += widen;
621
		}
622
		else {
623
			// consider autoscaling
624
			var margin = axisOptions.autoscaleMargin;
625
			if (margin != null) {
626
				if (axisOptions.min == null) {
627
					min -= (max - min) * margin;
628
					// make sure we don't go below zero if all values
629
					// are positive
630
					if (min < 0 && axis.datamin >= 0)
631
						min = 0;
632
				}
633
				if (axisOptions.max == null) {
634
					max += (max - min) * margin;
635
					if (max > 0 && axis.datamax <= 0)
636
						max = 0;
637
				}
638
			}
639
		}
640
		axis.min = min;
641
		axis.max = max;
642
	},
643
	/**
644
	 * function: prepareTickGeneration
645
	 * 
646
	 * Parameters:
647
	 * {Object} axis
648
	 * {Object} axisOptions
649
	 */
650
	prepareTickGeneration: function(axis, axisOptions) {
651
		// estimate number of ticks
652
		var noTicks;
653
		if (Object.isNumber(axisOptions.ticks) && axisOptions.ticks > 0)
654
			noTicks = axisOptions.ticks;
655
		else if (axis == this.xaxis)
656
			noTicks = this.canvasWidth / 100;
657
		else
658
			noTicks = this.canvasHeight / 60;
659
		
660
		var delta = (axis.max - axis.min) / noTicks;
661
		var size, generator, unit, formatter, i, magn, norm;
662

    
663
		if (axisOptions.mode == "time") {
664
			function formatDate(d, fmt, monthNames) {
665
				var leftPad = function(n) {
666
					n = "" + n;
667
					return n.length == 1 ? "0" + n : n;
668
				};
669
				
670
				var r = [];
671
				var escape = false;
672
				if (monthNames == null)
673
					monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
674
				for (var i = 0; i < fmt.length; ++i) {
675
					var c = fmt.charAt(i);
676
					
677
					if (escape) {
678
						switch (c) {
679
						case 'h': c = "" + d.getHours(); break;
680
						case 'H': c = leftPad(d.getHours()); break;
681
						case 'M': c = leftPad(d.getMinutes()); break;
682
						case 'S': c = leftPad(d.getSeconds()); break;
683
						case 'd': c = "" + d.getDate(); break;
684
						case 'm': c = "" + (d.getMonth() + 1); break;
685
						case 'y': c = "" + d.getFullYear(); break;
686
						case 'b': c = "" + monthNames[d.getMonth()]; break;
687
						}
688
						r.push(c);
689
						escape = false;
690
					}
691
					else {
692
						if (c == "%")
693
							escape = true;
694
						else
695
							r.push(c);
696
					}
697
				}
698
				return r.join("");
699
			}
700
			
701
				
702
			// map of app. size of time units in milliseconds
703
			var timeUnitSize = {
704
				"second": 1000,
705
				"minute": 60 * 1000,
706
				"hour": 60 * 60 * 1000,
707
				"day": 24 * 60 * 60 * 1000,
708
				"month": 30 * 24 * 60 * 60 * 1000,
709
				"year": 365.2425 * 24 * 60 * 60 * 1000
710
			};
711

    
712

    
713
			// the allowed tick sizes, after 1 year we use
714
			// an integer algorithm
715
			var spec = [
716
				[1, "second"], [2, "second"], [5, "second"], [10, "second"],
717
				[30, "second"], 
718
				[1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
719
				[30, "minute"], 
720
				[1, "hour"], [2, "hour"], [4, "hour"],
721
				[8, "hour"], [12, "hour"],
722
				[1, "day"], [2, "day"], [3, "day"],
723
				[0.25, "month"], [0.5, "month"], [1, "month"],
724
				[2, "month"], [3, "month"], [6, "month"],
725
				[1, "year"]
726
			];
727

    
728
			var minSize = 0;
729
			if (axisOptions.minTickSize != null) {
730
				if (typeof axisOptions.tickSize == "number")
731
					minSize = axisOptions.tickSize;
732
				else
733
					minSize = axisOptions.minTickSize[0] * timeUnitSize[axisOptions.minTickSize[1]];
734
			}
735
			
736
			for (i = 0; i < spec.length - 1; ++i) {
737
				if (delta < (spec[i][0] * timeUnitSize[spec[i][1]] + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) {
738
					break;
739
				}
740
			}
741
			
742
			size = spec[i][0];
743
			unit = spec[i][1];
744
			
745
			// special-case the possibility of several years
746
			if (unit == "year") {
747
				magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10));
748
				norm = (delta / timeUnitSize.year) / magn;
749
				if (norm < 1.5)
750
					size = 1;
751
				else if (norm < 3)
752
					size = 2;
753
				else if (norm < 7.5)
754
					size = 5;
755
				else
756
					size = 10;
757

    
758
				size *= magn;
759
			}
760

    
761
			if (axisOptions.tickSize) {
762
				size = axisOptions.tickSize[0];
763
				unit = axisOptions.tickSize[1];
764
			}
765
			
766
			var floorInBase = this.floorInBase; //gives us a reference to a global function.. 
767
			
768
			generator = function(axis) {
769
				var ticks = [],
770
					tickSize = axis.tickSize[0], unit = axis.tickSize[1],
771
					d = new Date(axis.min);
772
				
773
				var step = tickSize * timeUnitSize[unit];
774
				
775
				
776
				
777
				if (unit == "second")
778
					d.setSeconds(floorInBase(d.getSeconds(), tickSize));
779
				if (unit == "minute")
780
					d.setMinutes(floorInBase(d.getMinutes(), tickSize));
781
				if (unit == "hour")
782
					d.setHours(floorInBase(d.getHours(), tickSize));
783
				if (unit == "month")
784
					d.setMonth(floorInBase(d.getMonth(), tickSize));
785
				if (unit == "year")
786
					d.setFullYear(floorInBase(d.getFullYear(), tickSize));
787
				
788
				// reset smaller components
789
				d.setMilliseconds(0);
790
				if (step >= timeUnitSize.minute)
791
					d.setSeconds(0);
792
				if (step >= timeUnitSize.hour)
793
					d.setMinutes(0);
794
				if (step >= timeUnitSize.day)
795
					d.setHours(0);
796
				if (step >= timeUnitSize.day * 4)
797
					d.setDate(1);
798
				if (step >= timeUnitSize.year)
799
					d.setMonth(0);
800

    
801

    
802
				var carry = 0, v;
803
				do {
804
					v = d.getTime();
805
					ticks.push({ v: v, label: axis.tickFormatter(v, axis) });
806
					if (unit == "month") {
807
						if (tickSize < 1) {
808
							d.setDate(1);
809
							var start = d.getTime();
810
							d.setMonth(d.getMonth() + 1);
811
							var end = d.getTime();
812
							d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
813
							carry = d.getHours();
814
							d.setHours(0);
815
						}
816
						else
817
							d.setMonth(d.getMonth() + tickSize);
818
					}
819
					else if (unit == "year") {
820
						d.setFullYear(d.getFullYear() + tickSize);
821
					}
822
					else
823
						d.setTime(v + step);
824
				} while (v < axis.max);
825

    
826
				return ticks;
827
			};
828

    
829
			formatter = function (v, axis) {
830
				var d = new Date(v);
831

    
832
				// first check global format
833
				if (axisOptions.timeformat != null)
834
					return formatDate(d, axisOptions.timeformat, axisOptions.monthNames);
835
				
836
				var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
837
				var span = axis.max - axis.min;
838
				
839
				if (t < timeUnitSize.minute)
840
					fmt = "%h:%M:%S";
841
				else if (t < timeUnitSize.day) {
842
					if (span < 2 * timeUnitSize.day)
843
						fmt = "%h:%M";
844
					else
845
						fmt = "%b %d %h:%M";
846
				}
847
				else if (t < timeUnitSize.month)
848
					fmt = "%b %d";
849
				else if (t < timeUnitSize.year) {
850
					if (span < timeUnitSize.year)
851
						fmt = "%b";
852
					else
853
						fmt = "%b %y";
854
				}
855
				else
856
					fmt = "%y";
857
				
858
				return formatDate(d, fmt, axisOptions.monthNames);
859
			};
860
		}
861
		else {
862
			// pretty rounding of base-10 numbers
863
			var maxDec = axisOptions.tickDecimals;
864
			var dec = -Math.floor(Math.log(delta) / Math.LN10);
865
			if (maxDec != null && dec > maxDec)
866
				dec = maxDec;
867
			
868
			magn = Math.pow(10, -dec);
869
			norm = delta / magn; // norm is between 1.0 and 10.0
870
			
871
			if (norm < 1.5)
872
				size = 1;
873
			else if (norm < 3) {
874
				size = 2;
875
				// special case for 2.5, requires an extra decimal
876
				if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
877
					size = 2.5;
878
					++dec;
879
				}
880
			}
881
			else if (norm < 7.5)
882
				size = 5;
883
			else
884
				size = 10;
885

    
886
			size *= magn;
887
			
888
			if (axisOptions.minTickSize != null && size < axisOptions.minTickSize)
889
				size = axisOptions.minTickSize;
890

    
891
			if (axisOptions.tickSize != null)
892
				size = axisOptions.tickSize;
893
			
894
			axis.tickDecimals = Math.max(0, (maxDec != null) ? maxDec : dec);
895
			
896
			var floorInBase = this.floorInBase;
897
			
898
			generator = function (axis) {
899
				var ticks = [];
900
				var start = floorInBase(axis.min, axis.tickSize);
901
				// then spew out all possible ticks
902
				var i = 0, v;
903
				do {
904
					v = start + i * axis.tickSize;
905
					ticks.push({ v: v, label: axis.tickFormatter(v, axis) });
906
					++i;
907
				} while (v < axis.max);
908
				return ticks;
909
			};
910

    
911
			formatter = function (v, axis) {
912
				if(v) {
913
				return v.toFixed(axis.tickDecimals);
914
				}
915
				return 0;
916
			};
917
		}
918

    
919
		axis.tickSize = unit ? [size, unit] : size;
920
		axis.tickGenerator = generator;
921
		if (Object.isFunction(axisOptions.tickFormatter))
922
			axis.tickFormatter = function (v, axis) { return "" + axisOptions.tickFormatter(v, axis); };
923
		else
924
			axis.tickFormatter = formatter;
925
	},
926
	/**
927
	 * function: extendXRangeIfNeededByBar
928
	 */
929
	extendXRangeIfNeededByBar: function() {
930

    
931
		if (this.options.xaxis.max == null) {
932
			// great, we're autoscaling, check if we might need a bump
933
			var newmax = this.xaxis.max;
934
			this.graphData.each(function(gd){
935
				if(gd.bars.show && gd.bars.barWidth + this.xaxis.datamax > newmax)
936
				{
937
					newmax = this.xaxis.datamax + gd.bars.barWidth;
938
				}
939
			}.bind(this));
940
			this.xaxis.nax = newmax;
941
			
942
		}
943
	},
944
	/**
945
	 * function: setTicks
946
	 * 
947
	 * parameters:
948
	 * {Object} axis
949
	 * {Object} axisOptions
950
	 */
951
	setTicks: function(axis, axisOptions) {
952
		axis.ticks = [];
953
		
954
		if (axisOptions.ticks == null)
955
			axis.ticks = axis.tickGenerator(axis);
956
		else if (typeof axisOptions.ticks == "number") {
957
			if (axisOptions.ticks > 0)
958
				axis.ticks = axis.tickGenerator(axis);
959
		}
960
		else if (axisOptions.ticks) {
961
			var ticks = axisOptions.ticks;
962

    
963
			if (Object.isFunction(ticks))
964
				// generate the ticks
965
				ticks = ticks({ min: axis.min, max: axis.max });
966
			
967
			// clean up the user-supplied ticks, copy them over
968
			//var i, v;
969
			ticks.each(function(t, i){
970
				var v = null;
971
				var label = null;
972
				if(typeof t == 'object') {
973
					v = t[0];
974
					if(t.length > 1) { label = t[1]; }
975
				}
976
				else {
977
					v = t;
978
				}
979
				if(!label) {
980
					label = axis.tickFormatter(v, axis);
981
				}
982
				axis.ticks[i] = {v: v, label: label}
983
			}.bind(this));
984

    
985
		}
986

    
987
		if (axisOptions.autoscaleMargin != null && axis.ticks.length > 0) {
988
			if (axisOptions.min == null)
989
				axis.min = Math.min(axis.min, axis.ticks[0].v);
990
			if (axisOptions.max == null && axis.ticks.length > 1)
991
				axis.max = Math.min(axis.max, axis.ticks[axis.ticks.length - 1].v);
992
		}
993
	},
994
	/**
995
	 * Function: setSpacing
996
	 * 
997
	 * Parameters: none
998
	 */
999
	setSpacing: function() {
1000
		// calculate y label dimensions
1001
		var i, labels = [], l;
1002
		for (i = 0; i < this.yaxis.ticks.length; ++i) {
1003
			l = this.yaxis.ticks[i].label;
1004

    
1005
			if (l)
1006
				labels.push('<div class="tickLabel">' + l + '</div>');
1007
		}
1008

    
1009
		if (labels.length > 0) {
1010
			var dummyDiv = new Element('div', {'style': 'position:absolute;top:-10000px;font-size:smaller'});
1011
			dummyDiv.update(labels.join(""));
1012
			this.domObj.insert(dummyDiv);
1013
			this.yLabelMaxWidth = dummyDiv.getWidth();
1014
			this.yLabelMaxHeight = dummyDiv.select('div')[0].getHeight();
1015
			dummyDiv.remove();
1016
		}
1017

    
1018
		var maxOutset = this.options.grid.borderWidth;
1019
		if (this.options.points.show)
1020
			maxOutset = Math.max(maxOutset, this.options.points.radius + this.options.points.lineWidth/2);
1021
		for (i = 0; i < this.graphData.length; ++i) {
1022
			if (this.graphData[i].points.show)
1023
				maxOutset = Math.max(maxOutset, this.graphData[i].points.radius + this.graphData[i].points.lineWidth/2);
1024
		}
1025

    
1026
		this.chartOffset.left = this.chartOffset.right = this.chartOffset.top = this.chartOffset.bottom = maxOutset;
1027
		
1028
		this.chartOffset.left += this.yLabelMaxWidth + this.options.grid.labelMargin;
1029
		this.chartWidth = this.canvasWidth - this.chartOffset.left - this.chartOffset.right;
1030

    
1031
		this.xLabelBoxWidth = this.chartWidth / 6;
1032
		labels = [];
1033

    
1034
		for (i = 0; i < this.xaxis.ticks.length; ++i) {
1035
			l = this.xaxis.ticks[i].label;
1036
			if (l) {
1037
				labels.push('<span class="tickLabel" width="' + this.xLabelBoxWidth + '">' + l + '</span>');
1038
			}
1039
		}
1040

    
1041
		var xLabelMaxHeight = 0;
1042
		if (labels.length > 0) {
1043
			var dummyDiv = new Element('div', {'style': 'position:absolute;top:-10000px;font-size:smaller'});
1044
			dummyDiv.update(labels.join(""));
1045
			this.domObj.appendChild(dummyDiv);	
1046
			xLabelMaxHeight = dummyDiv.getHeight();
1047
			dummyDiv.remove();
1048
		}
1049

    
1050
		this.chartOffset.bottom += xLabelMaxHeight + this.options.grid.labelMargin;
1051
		this.chartHeight = this.canvasHeight - this.chartOffset.bottom - this.chartOffset.top;
1052
		this.hozScale = this.chartWidth / (this.xaxis.max - this.xaxis.min);
1053
		this.vertScale = this.chartHeight / (this.yaxis.max - this.yaxis.min);
1054
	},
1055
	/**
1056
	 * function: draw
1057
	 */
1058
	draw: function() {
1059
		if(this.options.bars.show)
1060
		{
1061
			this.extendXRangeIfNeededByBar();
1062
			this.setSpacing();
1063
			this.drawGrid();
1064
			this.drawBarGraph(this.graphData, this.barDataRange);
1065
		}
1066
		else if(this.options.pies.show)
1067
		{
1068
			this.preparePieData(this.graphData);
1069
			this.drawPieGraph(this.graphData);
1070
		}
1071
		else
1072
		{
1073
			this.drawGrid();
1074
			for (var i = 0; i < this.graphData.length; i++) {
1075
				this.drawGraph(this.graphData[i]);
1076
			}
1077
		}
1078
	},
1079
    /**
1080
     * function: translateHoz
1081
     * 
1082
     * Paramters:
1083
     * {Object} x
1084
     * 
1085
     * Description: Given a value this function translate it to relative x coord on canvas
1086
     */
1087
	translateHoz: function(x) {
1088
        return (x - this.xaxis.min) * this.hozScale;
1089
    },
1090
	/**
1091
	 * function: translateVert
1092
	 * 
1093
	 * parameters:
1094
	 * {Object} y
1095
	 * 
1096
	 * Description: Given a value this function translate it to relative y coord on canvas
1097
	 */
1098
    translateVert: function(y) {
1099
        return this.chartHeight - (y - this.yaxis.min) * this.vertScale;
1100
    },	
1101
	/**
1102
	 * function: drawGrid
1103
	 * 
1104
	 * parameters: none
1105
	 * 
1106
	 * description: draws the actual grid on the canvas
1107
	 */
1108
	drawGrid: function() {
1109
		var i;
1110
		
1111
		this.context.save();
1112
		this.context.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
1113
		this.context.translate(this.chartOffset.left, this.chartOffset.top);
1114

    
1115
		// draw background, if any
1116
		if (this.options.grid.backgroundColor != null) {
1117
			this.context.fillStyle = this.options.grid.backgroundColor;
1118
			this.context.fillRect(0, 0, this.chartWidth, this.chartHeight);
1119
		}
1120

    
1121
		// draw colored areas
1122
		if (this.options.grid.coloredAreas) {
1123
			var areas = this.options.grid.coloredAreas;
1124
			if (Object.isFunction(areas)) {
1125
				areas = areas({ xmin: this.xaxis.min, xmax: this.xaxis.max, ymin: this.yaxis.min, ymax: this.yaxis.max });
1126
			}
1127
			
1128
			areas.each(function(a){
1129
				// clip
1130
				if (a.x1 == null || a.x1 < this.xaxis.min)
1131
					a.x1 = this.xaxis.min;
1132
				if (a.x2 == null || a.x2 > this.xaxis.max)
1133
					a.x2 = this.xaxis.max;
1134
				if (a.y1 == null || a.y1 < this.yaxis.min)
1135
					a.y1 = this.yaxis.min;
1136
				if (a.y2 == null || a.y2 > this.yaxis.max)
1137
					a.y2 = this.yaxis.max;
1138

    
1139
				var tmp;
1140
				if (a.x1 > a.x2) {
1141
					tmp = a.x1;
1142
					a.x1 = a.x2;
1143
					a.x2 = tmp;
1144
				}
1145
				if (a.y1 > a.y2) {
1146
					tmp = a.y1;
1147
					a.y1 = a.y2;
1148
					a.y2 = tmp;
1149
				}
1150

    
1151
				if (a.x1 >= this.xaxis.max || a.x2 <= this.xaxis.min || a.x1 == a.x2
1152
					|| a.y1 >= this.yaxis.max || a.y2 <= this.yaxis.min || a.y1 == a.y2)
1153
					return;
1154

    
1155
				this.context.fillStyle = a.color || this.options.grid.coloredAreasColor;
1156
				this.context.fillRect(Math.floor(this.translateHoz(a.x1)), Math.floor(this.translateVert(a.y2)),
1157
							 Math.floor(this.translateHoz(a.x2) - this.translateHoz(a.x1)), Math.floor(this.translateVert(a.y1) - this.translateVert(a.y2)));				
1158
			}.bind(this));
1159

    
1160
			
1161
		}
1162
		
1163
		// draw the inner grid
1164
		this.context.lineWidth = 1;
1165
		this.context.strokeStyle = this.options.grid.tickColor;
1166
		this.context.beginPath();
1167
		var v;
1168
		if (this.options.grid.drawXAxis) {
1169
			this.xaxis.ticks.each(function(aTick){
1170
				v = aTick.v;
1171
				if(v <= this.xaxis.min || v >= this.xaxis.max) {
1172
					return;
1173
				}
1174
				this.context.moveTo(Math.floor(this.translateHoz(v)) + this.context.lineWidth / 2, 0);
1175
				this.context.lineTo(Math.floor(this.translateHoz(v)) + this.context.lineWidth / 2, this.chartHeight);
1176
			}.bind(this));
1177

    
1178
		}
1179
		
1180
		if (this.options.grid.drawYAxis) {
1181
			this.yaxis.ticks.each(function(aTick){
1182
				v = aTick.v;
1183
				if(v <= this.yaxis.min || v >= this.yaxis.max) {
1184
					return;
1185
				}
1186
				this.context.moveTo(0, Math.floor(this.translateVert(v)) + this.context.lineWidth / 2);
1187
				this.context.lineTo(this.chartWidth, Math.floor(this.translateVert(v)) + this.context.lineWidth / 2);
1188
			}.bind(this));
1189
			
1190
		}
1191
		this.context.stroke();
1192
		
1193
		if (this.options.grid.borderWidth) {
1194
			// draw border
1195
			this.context.lineWidth = this.options.grid.borderWidth;
1196
			this.context.strokeStyle = this.options.grid.color;
1197
			this.context.lineJoin = "round";
1198
			this.context.strokeRect(0, 0, this.chartWidth, this.chartHeight);
1199
			this.context.restore();
1200
		}
1201
	},
1202
	/**
1203
	 * function: insertLabels
1204
	 * 
1205
	 * parameters: none
1206
	 * 
1207
	 * description: inserts the label with proper spacing. Both on X and Y axis
1208
	 */
1209
	insertLabels: function() {
1210
		this.domObj.select(".tickLabels").invoke('remove');
1211
		
1212
		var i, tick;
1213
		var html = '<div class="tickLabels" style="font-size:smaller;color:' + this.options.grid.color + '">';
1214
		
1215
		// do the x-axis
1216
		this.xaxis.ticks.each(function(tick){
1217
			if (!tick.label || tick.v < this.xaxis.min || tick.v > this.xaxis.max)
1218
				return;
1219
			html += '<div style="position:absolute;top:' + (this.chartOffset.top + this.chartHeight + this.options.grid.labelMargin) + 'px;left:' + (this.chartOffset.left + this.translateHoz(tick.v) - this.xLabelBoxWidth/2) + 'px;width:' + this.xLabelBoxWidth + 'px;text-align:center" class="tickLabel">' + tick.label + "</div>";
1220
			
1221
		}.bind(this));
1222
		
1223
		// do the y-axis
1224
		this.yaxis.ticks.each(function(tick){
1225
			if (!tick.label || tick.v < this.yaxis.min || tick.v > this.yaxis.max)
1226
				return;
1227
			html += '<div id="ylabels" style="position:absolute;top:' + (this.chartOffset.top + this.translateVert(tick.v) - this.yLabelMaxHeight/2) + 'px;left:0;width:' + this.yLabelMaxWidth + 'px;text-align:right" class="tickLabel">' + tick.label + "</div>";
1228
		}.bind(this));
1229

    
1230
		html += '</div>';
1231
		
1232
		this.domObj.insert(html);
1233
	},
1234
	/**
1235
	 * function: drawGraph
1236
	 * 
1237
	 * Paramters:
1238
	 * {Object} graphData
1239
	 * 
1240
	 * Description: given a graphData (series) this function calls a proper lower level method to draw it.
1241
	 */
1242
	drawGraph: function(graphData) {
1243
		if (graphData.lines.show || (!graphData.bars.show && !graphData.points.show))
1244
			this.drawGraphLines(graphData);
1245
		if (graphData.bars.show)
1246
			this.drawGraphBar(graphData);
1247
		if (graphData.points.show)
1248
			this.drawGraphPoints(graphData);
1249
	},
1250
	/**
1251
	 * function: plotLine
1252
	 * 
1253
	 * parameters:
1254
	 * {Object} data
1255
	 * {Object} offset
1256
	 * 
1257
	 * description: 
1258
	 * Helper function that plots a line based on the data provided
1259
	 */
1260
	plotLine: function(data, offset) {
1261
        var prev, cur = null, drawx = null, drawy = null;
1262
        
1263
        this.context.beginPath();
1264
        for (var i = 0; i < data.length; ++i) {
1265
            prev = cur;
1266
            cur = data[i];
1267

    
1268
            if (prev == null || cur == null)
1269
                continue;
1270
            
1271
            var x1 = prev[0], y1 = prev[1],
1272
                x2 = cur[0], y2 = cur[1];
1273

    
1274
            // clip with ymin
1275
            if (y1 <= y2 && y1 < this.yaxis.min) {
1276
                if (y2 < this.yaxis.min)
1277
                    continue;   // line segment is outside
1278
                // compute new intersection point
1279
                x1 = (this.yaxis.min - y1) / (y2 - y1) * (x2 - x1) + x1;
1280
                y1 = this.yaxis.min;
1281
            }
1282
            else if (y2 <= y1 && y2 < this.yaxis.min) {
1283
                if (y1 < this.yaxis.min)
1284
                    continue;
1285
                x2 = (this.yaxis.min - y1) / (y2 - y1) * (x2 - x1) + x1;
1286
                y2 = this.yaxis.min;
1287
            }
1288

    
1289
            // clip with ymax
1290
            if (y1 >= y2 && y1 > this.yaxis.max) {
1291
                if (y2 > this.yaxis.max)
1292
                    continue;
1293
                x1 = (this.yaxis.max - y1) / (y2 - y1) * (x2 - x1) + x1;
1294
                y1 = this.yaxis.max;
1295
            }
1296
            else if (y2 >= y1 && y2 > this.yaxis.max) {
1297
                if (y1 > this.yaxis.max)
1298
                    continue;
1299
                x2 = (this.yaxis.max - y1) / (y2 - y1) * (x2 - x1) + x1;
1300
                y2 = this.yaxis.max;
1301
            }
1302

    
1303
            // clip with xmin
1304
            if (x1 <= x2 && x1 < this.xaxis.min) {
1305
                if (x2 < this.xaxis.min)
1306
                    continue;
1307
                y1 = (this.xaxis.min - x1) / (x2 - x1) * (y2 - y1) + y1;
1308
                x1 = this.xaxis.min;
1309
            }
1310
            else if (x2 <= x1 && x2 < this.xaxis.min) {
1311
                if (x1 < this.xaxis.min)
1312
                    continue;
1313
                y2 = (this.xaxis.min - x1) / (x2 - x1) * (y2 - y1) + y1;
1314
                x2 = this.xaxis.min;
1315
            }
1316

    
1317
            // clip with xmax
1318
            if (x1 >= x2 && x1 > this.xaxis.max) {
1319
                if (x2 > this.xaxis.max)
1320
                    continue;
1321
                y1 = (this.xaxis.max - x1) / (x2 - x1) * (y2 - y1) + y1;
1322
                x1 = this.xaxis.max;
1323
            }
1324
            else if (x2 >= x1 && x2 > this.xaxis.max) {
1325
                if (x1 > this.xaxis.max)
1326
                    continue;
1327
                y2 = (this.xaxis.max - x1) / (x2 - x1) * (y2 - y1) + y1;
1328
                x2 = this.xaxis.max;
1329
            }
1330

    
1331
            if (drawx != this.translateHoz(x1) || drawy != this.translateVert(y1) + offset)
1332
                this.context.moveTo(this.translateHoz(x1), this.translateVert(y1) + offset);
1333
            
1334
            drawx = this.translateHoz(x2);
1335
            drawy = this.translateVert(y2) + offset;
1336
            this.context.lineTo(drawx, drawy);
1337
        }
1338
        this.context.stroke();
1339
    },
1340
	/**
1341
	 * function: plotLineArea
1342
	 * 
1343
	 * parameters:
1344
	 * {Object} data
1345
	 * 
1346
	 * description:
1347
	 * Helper functoin that plots a colored line graph. This function
1348
	 * takes the data nad then fill in the area on the graph properly
1349
	 */
1350
	plotLineArea: function(data) {
1351
        var prev, cur = null;
1352
        
1353
        var bottom = Math.min(Math.max(0, this.yaxis.min), this.yaxis.max);
1354
        var top, lastX = 0;
1355

    
1356
        var areaOpen = false;
1357
        
1358
        for (var i = 0; i < data.length; ++i) {
1359
            prev = cur;
1360
            cur = data[i];
1361

    
1362
            if (areaOpen && prev != null && cur == null) {
1363
                // close area
1364
                this.context.lineTo(this.translateHoz(lastX), this.translateVert(bottom));
1365
                this.context.fill();
1366
                areaOpen = false;
1367
                continue;
1368
            }
1369

    
1370
            if (prev == null || cur == null)
1371
                continue;
1372
                
1373
            var x1 = prev[0], y1 = prev[1],
1374
                x2 = cur[0], y2 = cur[1];
1375

    
1376
            // clip x values
1377
            
1378
            // clip with xmin
1379
            if (x1 <= x2 && x1 < this.xaxis.min) {
1380
                if (x2 < this.xaxis.min)
1381
                    continue;
1382
                y1 = (this.xaxis.min - x1) / (x2 - x1) * (y2 - y1) + y1;
1383
                x1 = this.xaxis.min;
1384
            }
1385
            else if (x2 <= x1 && x2 < this.xaxis.min) {
1386
                if (x1 < this.xaxis.min)
1387
                    continue;
1388
                y2 = (this.xaxis.min - x1) / (x2 - x1) * (y2 - y1) + y1;
1389
                x2 = this.xaxis.min;
1390
            }
1391

    
1392
            // clip with xmax
1393
            if (x1 >= x2 && x1 > this.xaxis.max) {
1394
                if (x2 > this.xaxis.max)
1395
                    continue;
1396
                y1 = (this.xaxis.max - x1) / (x2 - x1) * (y2 - y1) + y1;
1397
                x1 = this.xaxis.max;
1398
            }
1399
            else if (x2 >= x1 && x2 > this.xaxis.max) {
1400
                if (x1 > this.xaxis.max)
1401
                    continue;
1402
                y2 = (this.xaxis.max - x1) / (x2 - x1) * (y2 - y1) + y1;
1403
                x2 = this.xaxis.max;
1404
            }
1405

    
1406
            if (!areaOpen) {
1407
                // open area
1408
                this.context.beginPath();
1409
                this.context.moveTo(this.translateHoz(x1), this.translateVert(bottom));
1410
                areaOpen = true;
1411
            }
1412
            
1413
            // now first check the case where both is outside
1414
            if (y1 >= this.yaxis.max && y2 >= this.yaxis.max) {
1415
                this.context.lineTo(this.translateHoz(x1), this.translateVert(this.yaxis.max));
1416
                this.context.lineTo(this.translateHoz(x2), this.translateVert(this.yaxis.max));
1417
                continue;
1418
            }
1419
            else if (y1 <= this.yaxis.min && y2 <= this.yaxis.min) {
1420
                this.context.lineTo(this.translateHoz(x1), this.translateVert(this.yaxis.min));
1421
                this.context.lineTo(this.translateHoz(x2), this.translateVert(this.yaxis.min));
1422
                continue;
1423
            }
1424
            
1425
            var x1old = x1, x2old = x2;
1426

    
1427
            // clip with ymin
1428
            if (y1 <= y2 && y1 < this.yaxis.min && y2 >= this.yaxis.min) {
1429
                x1 = (this.yaxis.min - y1) / (y2 - y1) * (x2 - x1) + x1;
1430
                y1 = this.yaxis.min;
1431
            }
1432
            else if (y2 <= y1 && y2 < this.yaxis.min && y1 >= this.yaxis.min) {
1433
                x2 = (this.yaxis.min - y1) / (y2 - y1) * (x2 - x1) + x1;
1434
                y2 = this.yaxis.min;
1435
            }
1436

    
1437
            // clip with ymax
1438
            if (y1 >= y2 && y1 > this.yaxis.max && y2 <= this.yaxis.max) {
1439
                x1 = (this.yaxis.max - y1) / (y2 - y1) * (x2 - x1) + x1;
1440
                y1 = this.yaxis.max;
1441
            }
1442
            else if (y2 >= y1 && y2 > this.yaxis.max && y1 <= this.yaxis.max) {
1443
                x2 = (this.yaxis.max - y1) / (y2 - y1) * (x2 - x1) + x1;
1444
                y2 = this.yaxis.max;
1445
            }
1446

    
1447

    
1448
            // if the x value was changed we got a rectangle
1449
            // to fill
1450
            if (x1 != x1old) {
1451
                if (y1 <= this.yaxis.min)
1452
                    top = this.yaxis.min;
1453
                else
1454
                    top = this.yaxis.max;
1455
                
1456
                this.context.lineTo(this.translateHoz(x1old), this.translateVert(top));
1457
                this.context.lineTo(this.translateHoz(x1), this.translateVert(top));
1458
            }
1459
            
1460
            // fill the triangles
1461
            this.context.lineTo(this.translateHoz(x1), this.translateVert(y1));
1462
            this.context.lineTo(this.translateHoz(x2), this.translateVert(y2));
1463

    
1464
            // fill the other rectangle if it's there
1465
            if (x2 != x2old) {
1466
                if (y2 <= this.yaxis.min)
1467
                    top = this.yaxis.min;
1468
                else
1469
                    top = this.yaxis.max;
1470
                
1471
                this.context.lineTo(this.translateHoz(x2old), this.translateVert(top));
1472
                this.context.lineTo(this.translateHoz(x2), this.translateVert(top));
1473
            }
1474

    
1475
            lastX = Math.max(x2, x2old);
1476
        }
1477

    
1478
        if (areaOpen) {
1479
            this.context.lineTo(this.translateHoz(lastX), this.translateVert(bottom));
1480
            this.context.fill();
1481
        }
1482
    },		
1483
	/**
1484
	 * function: drawGraphLines
1485
	 * 
1486
	 * parameters:
1487
	 * {Object} graphData
1488
	 * 
1489
	 * description:
1490
	 * Main function that daws the line graph. This function is called 
1491
	 * if <options> lines property is set to show or no other type of 
1492
	 * graph is specified. This function depends on <plotLineArea> and 
1493
	 * <plotLine> functions.
1494
	 */
1495
	drawGraphLines: function(graphData) {
1496
        this.context.save();
1497
        this.context.translate(this.chartOffset.left, this.chartOffset.top);
1498
        this.context.lineJoin = "round";
1499

    
1500
        var lw = graphData.lines.lineWidth;
1501
        var sw = graphData.shadowSize;
1502
        // FIXME: consider another form of shadow when filling is turned on
1503
        if (sw > 0) {
1504
            // draw shadow in two steps
1505
            this.context.lineWidth = sw / 2;
1506
            this.context.strokeStyle = "rgba(0,0,0,0.1)";
1507
            this.plotLine(graphData.data, lw/2 + sw/2 + this.context.lineWidth/2);
1508

    
1509
            this.context.lineWidth = sw / 2;
1510
            this.context.strokeStyle = "rgba(0,0,0,0.2)";
1511
            this.plotLine(graphData.data, lw/2 + this.context.lineWidth/2);
1512
        }
1513

    
1514
        this.context.lineWidth = lw;
1515
        this.context.strokeStyle = graphData.color;
1516
        if (graphData.lines.fill) {
1517
            this.context.fillStyle = graphData.lines.fillColor != null ? graphData.lines.fillColor : this.parseColor(graphData.color).scale(null, null, null, 0.4).toString();
1518
            this.plotLineArea(graphData.data, 0);
1519
        }
1520

    
1521
        this.plotLine(graphData.data, 0);
1522
        this.context.restore();
1523
    },
1524
	/**
1525
	 * function: plotPoints
1526
	 * 
1527
	 * parameters:
1528
	 * {Object} data
1529
	 * {Object} radius
1530
	 * {Object} fill
1531
	 * 
1532
	 * description:
1533
	 * Helper function that draws the point graph according to the data provided. Size of each 
1534
	 * point is provided by radius variable and fill specifies if points 
1535
	 * are filled
1536
	 */
1537
	plotPoints: function(data, radius, fill) {
1538
        for (var i = 0; i < data.length; ++i) {
1539
            if (data[i] == null)
1540
                continue;
1541
            
1542
            var x = data[i][0], y = data[i][1];
1543
            if (x < this.xaxis.min || x > this.xaxis.max || y < this.yaxis.min || y > this.yaxis.max)
1544
                continue;
1545
            
1546
            this.context.beginPath();
1547
            this.context.arc(this.translateHoz(x), this.translateVert(y), radius, 0, 2 * Math.PI, true);
1548
            if (fill)
1549
                this.context.fill();
1550
            this.context.stroke();
1551
        }
1552
    },
1553
	/**
1554
	 * function: plotPointShadows
1555
	 * 
1556
	 * parameters:
1557
	 * {Object} data
1558
	 * {Object} offset
1559
	 * {Object} radius
1560
	 * 
1561
	 * description: 
1562
	 * Helper function that draws the shadows for the points.
1563
	 */
1564
    plotPointShadows: function(data, offset, radius) {
1565
        for (var i = 0; i < data.length; ++i) {
1566
            if (data[i] == null)
1567
                continue;
1568
            
1569
            var x = data[i][0], y = data[i][1];
1570
            if (x < this.xaxis.min || x > this.xaxis.max || y < this.yaxis.min || y > this.yaxis.max)
1571
                continue;
1572
            this.context.beginPath();
1573
            this.context.arc(this.translateHoz(x), this.translateVert(y) + offset, radius, 0, Math.PI, false);
1574
            this.context.stroke();
1575
        }
1576
    },
1577
	/**
1578
	 * function: drawGraphPoints
1579
	 * 
1580
	 * paramters:
1581
	 * {Object} graphData
1582
	 * 
1583
	 * description:
1584
	 * Draws the point graph onto the canvas. This function depends on helper 
1585
	 * functions <plotPointShadows> and <plotPoints>
1586
	 */
1587
    drawGraphPoints: function(graphData) {
1588
       	this.context.save();
1589
        this.context.translate(this.chartOffset.left, this.chartOffset.top);
1590

    
1591
        var lw = graphData.lines.lineWidth;
1592
        var sw = graphData.shadowSize;
1593
        if (sw > 0) {
1594
            // draw shadow in two steps
1595
            this.context.lineWidth = sw / 2;
1596
            this.context.strokeStyle = "rgba(0,0,0,0.1)";
1597
            this.plotPointShadows(graphData.data, sw/2 + this.context.lineWidth/2, graphData.points.radius);
1598

    
1599
            this.context.lineWidth = sw / 2;
1600
            this.context.strokeStyle = "rgba(0,0,0,0.2)";
1601
            this.plotPointShadows(graphData.data, this.context.lineWidth/2, graphData.points.radius);
1602
        }
1603

    
1604
        this.context.lineWidth = graphData.points.lineWidth;
1605
        this.context.strokeStyle = graphData.color;
1606
        this.context.fillStyle = graphData.points.fillColor != null ? graphData.points.fillColor : graphData.color;
1607
        this.plotPoints(graphData.data, graphData.points.radius, graphData.points.fill);
1608
        this.context.restore();
1609
    },
1610
	/**
1611
	 * function: preparePieData
1612
	 * 
1613
	 * parameters:
1614
	 * {Object} graphData
1615
	 * 
1616
	 * Description: 
1617
	 * Helper function that manipulates the given data stream so that it can 
1618
	 * be plotted as a Pie Chart
1619
	 */
1620
	preparePieData: function(graphData)
1621
	{
1622
		for(i = 0; i < graphData.length; i++)
1623
		{
1624
			var data = 0;
1625
			for(j = 0; j < graphData[i].data.length; j++){
1626
				data += parseInt(graphData[i].data[j][1]);
1627
			}
1628
			graphData[i].data = data;
1629
		}
1630
	},
1631
	/**
1632
	 * function: drawPieShadow
1633
	 * 
1634
	 * {Object} anchorX
1635
	 * {Object} anchorY
1636
	 * {Object} radius
1637
	 * 
1638
	 * description:
1639
	 * Helper function that draws a shadow for the Pie Chart. This just draws 
1640
	 * a circle with offset that simulates shadow. We do not give each piece 
1641
	 * of the pie an individual shadow.
1642
	 */
1643
	drawPieShadow: function(anchorX, anchorY, radius)
1644
	{
1645
		this.context.beginPath();
1646
		this.context.moveTo(anchorX, anchorY);
1647
		this.context.fillStyle = 'rgba(0,0,0,' + 0.1 + ')';
1648
		startAngle = 0;
1649
		endAngle = (Math.PI/180)*360;	
1650
		this.context.arc(anchorX + 2, anchorY +2, radius + (this.options.shadowSize/2), startAngle, endAngle, false);
1651
		this.context.fill();
1652
		this.context.closePath();
1653
	},
1654
	/**
1655
	 * function: drawPieGraph
1656
	 * 
1657
	 * parameters:
1658
	 * {Object} graphData
1659
	 * 
1660
	 * description: 
1661
	 * Draws the actual pie chart. This function depends on helper function 
1662
	 * <drawPieShadow> to draw the actual shadow
1663
	 */
1664
	drawPieGraph: function(graphData)
1665
	{
1666
		var sumData = 0;
1667
		var radius = 0;
1668
		var centerX = this.chartWidth/2;
1669
		var centerY = this.chartHeight/2;
1670
		var startAngle = 0;
1671
		var endAngle = 0;
1672
		var fontSize = this.options.pies.fontSize;
1673
		var labelWidth = this.options.pies.labelWidth;
1674

    
1675
		//determine Pie Radius
1676
		if(!this.options.pies.autoScale)
1677
			radius = this.options.pies.radius;
1678
		else
1679
			radius = (this.chartHeight * 0.85)/2;
1680

    
1681
		var labelRadius = radius * 1.05;
1682

    
1683
		for(i = 0; i < graphData.length; i++)
1684
			sumData += graphData[i].data;
1685

    
1686
		// used to adjust labels so that everything adds up to 100%
1687
		totalPct = 0;
1688
		
1689
		//lets draw the shadow first.. we don't need an individual shadow to every pie rather we just
1690
		//draw a circle underneath to simulate the shadow...
1691
		this.drawPieShadow(centerX, centerY, radius, 0, 0); 
1692
		
1693
		//lets draw the actual pie chart now.
1694
		graphData.each(function(gd, j){
1695
			var pct = gd.data / sumData;
1696
			startAngle = endAngle;
1697
			endAngle += pct * (2 * Math.PI);
1698
			var sliceMiddle = (endAngle - startAngle) / 2 + startAngle;
1699
			var labelX = centerX + Math.cos(sliceMiddle) * labelRadius;
1700
			var labelY = centerY + Math.sin(sliceMiddle) * labelRadius;
1701
			var anchorX = centerX;
1702
			var anchorY = centerY;
1703
			var textAlign = null;
1704
			var verticalAlign = null;
1705
			var left = 0;
1706
			var top = 0;
1707
			
1708
			//draw pie:
1709
			//drawing pie	
1710
			this.context.beginPath();
1711
			this.context.moveTo(anchorX, anchorY);				
1712
			this.context.arc(anchorX, anchorY, radius, startAngle, endAngle, false);
1713
			this.context.closePath();
1714
			this.context.fillStyle = this.parseColor(gd.color).scale(null, null, null, this.options.pies.fillOpacity).toString();
1715

    
1716
			if(this.options.pies.fill)	{ this.context.fill(); }
1717

    
1718
			// drawing labels
1719
			if (sliceMiddle <= 0.25 * (2 * Math.PI)) 
1720
			{
1721
				// text on top and align left
1722
				textAlign = "left";
1723
				verticalAlign = "top";
1724
				left = labelX;
1725
				top = labelY + fontSize;
1726
			}
1727
			else if (sliceMiddle > 0.25 * (2 * Math.PI) && sliceMiddle <= 0.5 * (2 * Math.PI)) 
1728
			{
1729
				// text on bottom and align left
1730
				textAlign = "left";
1731
				verticalAlign = "bottom";
1732
				left = labelX - labelWidth;
1733
				top = labelY;
1734
			}
1735
			else if (sliceMiddle > 0.5 * (2 * Math.PI) && sliceMiddle <= 0.75 * (2 * Math.PI)) 
1736
			{
1737
				// text on bottom and align right
1738
				textAlign = "right";
1739
				verticalAlign = "bottom";
1740
				left = labelX - labelWidth;
1741
				top = labelY - fontSize;
1742
			}
1743
			else 
1744
			{
1745
				// text on top and align right
1746
				textAlign = "right";
1747
				verticalAlign = "bottom";
1748
				left = labelX;
1749
				top = labelY - fontSize;
1750
			}
1751

    
1752
			left = left + "px";
1753
			top = top + "px";
1754
			var textVal = Math.round(pct * 100);
1755

    
1756
			if (j == graphData.length - 1) {
1757
				if (textVal + totalPct < 100) {
1758
					textVal = textVal + 1;
1759
				} else if (textVal + totalPct > 100) {
1760
					textVal = textVal - 1;
1761
				};
1762
			}
1763

    
1764
			var html = "<div style=\"position: absolute;zindex:11; width:" + labelWidth + "px;fontSize:" + fontSize + "px;overflow:hidden;top:"+ top + ";left:"+ left + ";textAlign:" + textAlign + ";verticalAlign:" + verticalAlign +"\">" +  textVal + "%</div>";
1765
			//$(html).appendTo(target);
1766
			this.domObj.insert(html);
1767

    
1768
			totalPct = totalPct + textVal;			
1769
		}.bind(this));
1770
		
1771
	},
1772
	/**
1773
	 * function: drawBarGraph
1774
	 * 
1775
	 * parameters:
1776
	 * {Object} graphData
1777
	 * {Object} barDataRange
1778
	 * 
1779
	 * description: 
1780
	 * Goes through each series in graphdata and passes it onto <drawBarGraphs> function
1781
	 */
1782
	drawBarGraph: function(graphData, barDataRange)
1783
	{
1784
		graphData.each(function(gd, i){
1785
			this.drawGraphBars(gd, i, graphData.size(), barDataRange);
1786
		}.bind(this));
1787
	},
1788
	/**
1789
	 * function: drawGraphBar
1790
	 * 
1791
	 * parameters:
1792
	 * {Object} graphData
1793
	 * 
1794
	 * description:
1795
	 * This function is called when an individual series in GraphData is bar graph and plots it
1796
	 */
1797
	drawGraphBar: function(graphData)
1798
	{
1799
		this.drawGraphBars(graphData, 0, this.graphData.length, this.barDataRange);			
1800
	},	
1801
	/**
1802
	 * function: plotBars
1803
	 * 
1804
	 * parameters:
1805
	 * {Object} graphData
1806
	 * {Object} data
1807
	 * {Object} barWidth
1808
	 * {Object} offset
1809
	 * {Object} fill
1810
	 * {Object} counter
1811
	 * {Object} total
1812
	 * {Object} barDataRange
1813
	 * 
1814
	 * description: 
1815
	 * Helper function that draws the bar graph based on data.
1816
	 */
1817
	plotBars: function(graphData, data, barWidth, offset, fill,counter, total, barDataRange) {
1818
		var shift = 0;
1819
		
1820
		if(total % 2 == 0)
1821
		{
1822
			shift = (1 + ((counter  - total /2 ) - 1)) * barWidth;
1823
		}
1824
		else
1825
		{
1826
			var interval = 0.5;			
1827
			if(counter == (total/2 - interval )) {
1828
				shift = - barWidth * interval;
1829
			}
1830
			else {
1831
				shift = (interval + (counter  - Math.round(total/2))) * barWidth;
1832
			}
1833
		}
1834

    
1835
		var rangeData = [];
1836
		data.each(function(d){
1837
			if(!d) return;
1838
			
1839
			var x = d[0], y = d[1];
1840
			var drawLeft = true, drawTop = true, drawRight = true;
1841
			var left = x + shift, right = x + barWidth + shift, bottom = 0, top = y;
1842
			var rangeDataPoint = {};
1843
			rangeDataPoint.left = left;
1844
			rangeDataPoint.right = right;
1845
			rangeDataPoint.value = top;
1846
			rangeData.push(rangeDataPoint);
1847

    
1848
			if (right < this.xaxis.min || left > this.xaxis.max || top < this.yaxis.min || bottom > this.yaxis.max)
1849
				return;
1850

    
1851
			// clip
1852
			if (left < this.xaxis.min) {
1853
				left = this.xaxis.min;
1854
				drawLeft = false;
1855
			}
1856

    
1857
			if (right > this.xaxis.max) {
1858
				right = this.xaxis.max;
1859
				drawRight = false;
1860
			}
1861

    
1862
			if (bottom < this.yaxis.min)
1863
				bottom = this.yaxis.min;
1864

    
1865
			if (top > this.yaxis.max) {
1866
				top = this.yaxis.max;
1867
				drawTop = false;
1868
			}
1869
			
1870
			if(graphData.bars.showShadow && graphData.shadowSize > 0)
1871
				this.plotShadowOutline(graphData, this.context.strokeStyle, left, bottom, top, right, drawLeft, drawRight, drawTop);
1872
				
1873
			// fill the bar
1874
			if (fill) {
1875
				this.context.beginPath();
1876
				this.context.moveTo(this.translateHoz(left), this.translateVert(bottom) + offset);
1877
				this.context.lineTo(this.translateHoz(left), this.translateVert(top) + offset);
1878
				this.context.lineTo(this.translateHoz(right), this.translateVert(top) + offset);
1879
				this.context.lineTo(this.translateHoz(right), this.translateVert(bottom) + offset);
1880
				this.context.fill();
1881
			}
1882

    
1883
			// draw outline
1884
			if (drawLeft || drawRight || drawTop) {
1885
				this.context.beginPath();
1886
				this.context.moveTo(this.translateHoz(left), this.translateVert(bottom) + offset);
1887
				if (drawLeft)
1888
					this.context.lineTo(this.translateHoz(left), this.translateVert(top) + offset);
1889
				else
1890
					this.context.moveTo(this.translateHoz(left), this.translateVert(top) + offset);
1891

    
1892
				if (drawTop)
1893
					this.context.lineTo(this.translateHoz(right), this.translateVert(top) + offset);
1894
				else
1895
					this.context.moveTo(this.translateHoz(right), this.translateVert(top) + offset);
1896
				if (drawRight)
1897
					this.context.lineTo(this.translateHoz(right), this.translateVert(bottom) + offset);
1898
				else
1899
					this.context.moveTo(this.translateHoz(right), this.translateVert(bottom) + offset);
1900
				this.context.stroke();
1901
			}
1902
		}.bind(this));
1903
		
1904
		barDataRange.push(rangeData);
1905
	},
1906
	/**
1907
	 * function: plotShadowOutline
1908
	 * 
1909
	 * parameters:
1910
	 * {Object} graphData
1911
	 * {Object} orgStrokeStyle
1912
	 * {Object} left
1913
	 * {Object} bottom
1914
	 * {Object} top
1915
	 * {Object} right
1916
	 * {Object} drawLeft
1917
	 * {Object} drawRight
1918
	 * {Object} drawTop
1919
	 * 
1920
	 * description:
1921
	 * Helper function that draws a outline simulating shadow for bar chart
1922
	 */
1923
	plotShadowOutline: function(graphData, orgStrokeStyle, left, bottom, top, right, drawLeft, drawRight, drawTop)
1924
	{
1925
		var orgOpac = 0.3;
1926
		
1927
		for(var n = 1; n <= this.options.shadowSize/2; n++)
1928
		{
1929
			var opac = orgOpac * n;
1930
			this.context.beginPath();
1931
			this.context.strokeStyle = "rgba(0,0,0," + opac + ")";
1932

    
1933
			this.context.moveTo(this.translateHoz(left) + n, this.translateVert(bottom));
1934

    
1935
			if(drawLeft)
1936
				this.context.lineTo(this.translateHoz(left) + n, this.translateVert(top) - n);
1937
			else
1938
				this.context.moveTo(this.translateHoz(left) + n, this.translateVert(top) - n);
1939

    
1940
			if(drawTop)	
1941
				this.context.lineTo(this.translateHoz(right) + n, this.translateVert(top) - n);
1942
			else
1943
				this.context.moveTo(this.translateHoz(right) + n, this.translateVert(top) - n);
1944

    
1945
			if(drawRight)
1946
				this.context.lineTo(this.translateHoz(right) + n, this.translateVert(bottom));
1947
			else
1948
				this.context.lineTo(this.translateHoz(right) + n, this.translateVert(bottom));
1949

    
1950
			this.context.stroke();
1951
			this.context.closePath();
1952
		}
1953

    
1954
		this.context.strokeStyle = orgStrokeStyle;
1955
	},
1956
	/**
1957
	 * function: drawGraphBars
1958
	 * 
1959
	 * parameters:
1960
	 * {Object} graphData 
1961
	 * {Object} counter
1962
	 * {Object} total
1963
	 * {Object} barDataRange
1964
	 * 
1965
	 * description:
1966
	 * Draws the actual bar graphs. Calls <plotBars> to draw the individual bar
1967
	 */
1968
	drawGraphBars: function(graphData, counter, total, barDataRange){
1969
		this.context.save();
1970
		this.context.translate(this.chartOffset.left, this.chartOffset.top);
1971
		this.context.lineJoin = "round";
1972

    
1973
		var bw = graphData.bars.barWidth;
1974
		var lw = Math.min(graphData.bars.lineWidth, bw);
1975

    
1976

    
1977
		this.context.lineWidth = lw;
1978
		this.context.strokeStyle = graphData.color;
1979
		if (graphData.bars.fill) {
1980
			this.context.fillStyle = graphData.bars.fillColor != null ? graphData.bars.fillColor : this.parseColor(graphData.color).scale(null, null, null, this.options.bars.fillOpacity).toString();
1981
		}
1982
		this.plotBars(graphData, graphData.data, bw, 0, graphData.bars.fill, counter, total, barDataRange);
1983
		this.context.restore();
1984
	},
1985
	/**
1986
	 * function: insertLegend
1987
	 * 
1988
	 * description:
1989
	 * inserts legend onto the graph. *legend: {show: true}* must be set in <options> 
1990
	 * for for this to work.
1991
	 */
1992
	insertLegend: function() {
1993
		this.domObj.select(".legend").invoke('remove');
1994

    
1995
		if (!this.options.legend.show)
1996
			return;
1997
		
1998
		var fragments = [];
1999
		var rowStarted = false;
2000
		this.graphData.each(function(gd, index){
2001
			if(!gd.label) {
2002
				return;
2003
			}
2004
			if(index % this.options.legend.noColumns == 0) {
2005
				if(rowStarted) {
2006
					fragments.push('</tr>');
2007
				}
2008
				fragments.push('<tr>');
2009
				rowStarted = true;
2010
			}
2011
			var label = gd.label;
2012
			if(this.options.legend.labelFormatter != null) {
2013
				label = this.options.legend.labelFormatter(label);
2014
			}
2015
			
2016
			fragments.push(
2017
				'<td class="legendColorBox"><div style="border:1px solid ' + this.options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:14px;height:10px;background-color:' + gd.color + ';overflow:hidden"></div></div></td>' +
2018
				'<td class="legendLabel">' + label + '</td>');
2019
			
2020
		}.bind(this));
2021

    
2022
		if (rowStarted)
2023
			fragments.push('</tr>');
2024
		
2025
		if(fragments.length > 0){
2026
			var table = '<table style="font-size:smaller;color:' + this.options.grid.color + '">' + fragments.join("") + '</table>';
2027
			if($(this.options.legend.container) != null){
2028
				$(this.options.legend.container).insert(table);
2029
			}else{
2030
				var pos = '';
2031
				var p = this.options.legend.position, m = this.options.legend.margin;
2032
				
2033
				if(p.charAt(0) == 'n') pos += 'top:' + (m + this.chartOffset.top) + 'px;';
2034
				else if(p.charAt(0) == 's') pos += 'bottom:' + (m + this.chartOffset.bottom) + 'px;';					
2035
				if(p.charAt(1) == 'e') pos += 'right:' + (m + this.chartOffset.right) + 'px;';
2036
				else if(p.charAt(1) == 'w') pos += 'left:' + (m + this.chartOffset.bottom) + 'px;';
2037
				var div = this.domObj.insert('<div class="ProtoChart-legend" style="border: 1px solid '+this.options.legend.borderColor+'; position:absolute;z-index:2;' + pos +'">' + table + '</div>').getElementsBySelector('div.ProtoChart-legend').first();
2038
				
2039
				if(this.options.legend.backgroundOpacity != 0.0){
2040
					var c = this.options.legend.backgroundColor;
2041
					if(c == null){
2042
						var tmp = (this.options.grid.backgroundColor != null) ? this.options.grid.backgroundColor : this.extractColor(div);
2043
						c = this.parseColor(tmp).adjust(null, null, null, 1).toString();
2044
					}
2045
					this.domObj.insert('<div class="ProtoChart-legend-bg" style="position:absolute;width:' + div.getWidth() + 'px;height:' + div.getHeight() + 'px;' + pos +'background-color:' + c + ';"> </div>').select('div.ProtoChart-legend-bg').first().setStyle({
2046
						'opacity': this.options.legend.backgroundOpacity
2047
					});						
2048
				}
2049
			}
2050
		}
2051
	},
2052
	/**
2053
	 * Function: onMouseMove
2054
	 * 
2055
	 * parameters:
2056
	 * event: {Object} ev
2057
	 * 
2058
	 * Description:
2059
	 * Called whenever the mouse is moved on the graph. This takes care of the mousetracking.
2060
	 * This event also fires <ProtoChart:mousemove> event, which gets current position of the 
2061
	 * mouse as a parameters. 
2062
	 */
2063
	onMouseMove: function(ev) {
2064
		var e = ev || window.event;
2065
		if (e.pageX == null && e.clientX != null) {
2066
			var de = document.documentElement, b = $(document.body);
2067
			this.lastMousePos.pageX = e.clientX + (de && de.scrollLeft || b.scrollLeft || 0);
2068
			this.lastMousePos.pageY = e.clientY + (de && de.scrollTop || b.scrollTop || 0);
2069
		}
2070
		else {
2071
			this.lastMousePos.pageX = e.pageX;
2072
			this.lastMousePos.pageY = e.pageY;
2073
		}
2074
		
2075
		var offset = this.overlay.cumulativeOffset();
2076
		var pos = {
2077
			x: this.xaxis.min + (e.pageX - offset.left - this.chartOffset.left) / this.hozScale,
2078
			y: this.yaxis.max - (e.pageY - offset.top - this.chartOffset.top) / this.vertScale
2079
		};
2080
		
2081
		if(this.options.mouse.track && this.selectionInterval == null) {
2082
			this.hit(ev, pos);
2083
		}
2084
		this.domObj.fire("ProtoChart:mousemove", [ pos ]);
2085
	},
2086
	/**
2087
	 * Function: onMouseDown
2088
	 * 
2089
	 * Parameters:
2090
	 * Event - {Object} e
2091
	 * 
2092
	 * Description:
2093
	 * Called whenever the mouse is clicked.
2094
	 */
2095
	onMouseDown: function(e) {
2096
		if (e.which != 1)  // only accept left-click
2097
			return;
2098
		
2099
		document.body.focus();
2100

    
2101
		if (document.onselectstart !== undefined && this.workarounds.onselectstart == null) {
2102
			this.workarounds.onselectstart = document.onselectstart;
2103
			document.onselectstart = function () { return false; };
2104
		}
2105
		if (document.ondrag !== undefined && this.workarounds.ondrag == null) {
2106
			this.workarounds.ondrag = document.ondrag;
2107
			document.ondrag = function () { return false; };
2108
		}
2109
		
2110
		this.setSelectionPos(this.selection.first, e);
2111
			
2112
		if (this.selectionInterval != null)
2113
			clearInterval(this.selectionInterval);
2114
		this.lastMousePos.pageX = null;
2115
		this.selectionInterval = setInterval(this.updateSelectionOnMouseMove.bind(this), 200);
2116

    
2117
		this.overlay.observe("mouseup", this.onSelectionMouseUp.bind(this));
2118
	},
2119
	/**
2120
	 * Function: onClick
2121
	 * parameters:
2122
	 * Event - {Object} e
2123
	 * Description: 
2124
	 * Handles the "click" event on the chart. This function fires <ProtoChart:plotclick> event. If
2125
	 * <options.allowDataClick> is enabled then it also fires <ProtoChart:dataclick> event which gives
2126
	 * you access to exact data point where user clicked.
2127
	 */
2128
	onClick: function(e) {
2129
		if (this.ignoreClick) {
2130
			this.ignoreClick = false;
2131
			return;
2132
		}
2133
		var offset = this.overlay.cumulativeOffset(); 
2134
		var pos ={
2135
			x: this.xaxis.min + (e.pageX - offset.left - this.chartOffset.left) / this.hozScale,
2136
			y: this.yaxis.max - (e.pageY - offset.top - this.chartOffset.top) / this.vertScale
2137
		};
2138
		this.domObj.fire("ProtoChart:plotclick", [ pos ]);
2139

    
2140
		if(this.options.allowDataClick)
2141
		{
2142
			var dataPoint = {};
2143
			if(this.options.points.show)
2144
			{
2145
				dataPoint = this.getDataClickPoint(pos, this.options);
2146
				this.domObj.fire("ProtoChart:dataclick", [dataPoint]);
2147
			}
2148
			else if(this.options.lines.show && this.options.points.show)
2149
			{
2150
				dataPoint = this.getDataClickPoint(pos, this.options);
2151
				this.domObj.fire("ProtoChart:dataclick", [dataPoint]);
2152
			}
2153
			else if(this.options.bars.show)
2154
			{
2155
				if(this.barDataRange.length > 0)
2156
				{
2157
					dataPoint = this.getDataClickPoint(pos, this.options, this.barDataRange);
2158
					this.domObj.fire("ProtoChart:dataclick", [dataPoint]);
2159
				}
2160
			}
2161
		}
2162
	},
2163
	/**
2164
	 * Internal function used by onClick method.
2165
	 */
2166
	getDataClickPoint: function(pos, options, barDataRange)
2167
	{
2168
		pos.x = parseInt(pos.x);
2169
		pos.y = parseInt(pos.y);
2170
		var yClick = pos.y.toFixed(0);
2171
		var dataVal = {};
2172

    
2173
		dataVal.position = pos;
2174
		dataVal.value = '';
2175

    
2176
		if(options.points.show)
2177
		{
2178
			this.graphData.each(function(gd){
2179
				var temp = gd.data;
2180
				var xClick = parseInt(pos.x.toFixed(0));
2181
				if(xClick < 0) { xClick = 0; }
2182
				if(temp[xClick] && yClick >= temp[xClick][1] - (this.options.points.radius * 10) && yClick <= temp[xClick][1] + (this.options.points.radius * 10)) {
2183
					dataVal.value = temp[xClick][1];
2184
					throw $break;
2185
				}
2186
				
2187
			}.bind(this));
2188
		}
2189
		else if(options.bars.show)
2190
		{
2191
			xClick = pos.x;
2192
			this.barDataRange.each(function(barData){
2193
				barData.each(function(data){
2194
					var temp = data;
2195
					if(xClick > temp.left && xClick < temp.right) {
2196
						dataVal.value = temp.value;
2197
						throw $break;
2198
					}
2199
				}.bind(this));
2200
			}.bind(this));
2201

    
2202
		}
2203

    
2204
		return dataVal;
2205
	},
2206
	/**
2207
	 * Function: triggerSelectedEvent
2208
	 * 
2209
	 * Description:
2210
	 * Internal function called when a selection on the graph is made. This function
2211
	 * fires <ProtoChart:selected> event which has a parameter representing the selection
2212
	 * {
2213
	 * 	x1: {int}, y1: {int},
2214
	 * 	x2: {int}, y2: {int}
2215
	 * }
2216
	 */
2217
	triggerSelectedEvent: function() {
2218
		var x1, x2, y1, y2;
2219
		if (this.selection.first.x <= this.selection.second.x) {
2220
			x1 = this.selection.first.x;
2221
			x2 = this.selection.second.x;
2222
		}
2223
		else {
2224
			x1 = this.selection.second.x;
2225
			x2 = this.selection.first.x;
2226
		}
2227

    
2228
		if (this.selection.first.y >= this.selection.second.y) {
2229
			y1 = this.selection.first.y;
2230
			y2 = this.selection.second.y;
2231
		}
2232
		else {
2233
			y1 = this.selection.second.y;
2234
			y2 = this.selection.first.y;
2235
		}
2236
		
2237
		x1 = this.xaxis.min + x1 / this.hozScale;
2238
		x2 = this.xaxis.min + x2 / this.hozScale;
2239

    
2240
		y1 = this.yaxis.max - y1 / this.vertScale;
2241
		y2 = this.yaxis.max - y2 / this.vertScale;
2242

    
2243
		this.domObj.fire("ProtoChart:selected", [ { x1: x1, y1: y1, x2: x2, y2: y2 } ]);
2244
	},
2245
	/**
2246
	 * Internal function
2247
	 */
2248
	onSelectionMouseUp: function(e) {
2249
		if (document.onselectstart !== undefined)
2250
			document.onselectstart = this.workarounds.onselectstart;
2251
		if (document.ondrag !== undefined)
2252
			document.ondrag = this.workarounds.ondrag;
2253
		
2254
		if (this.selectionInterval != null) {
2255
			clearInterval(this.selectionInterval);
2256
			this.selectionInterval = null;
2257
		}
2258

    
2259
		this.setSelectionPos(this.selection.second, e);
2260
		this.clearSelection();
2261
		if (!this.selectionIsSane() || e.which != 1)
2262
			return false;
2263
		
2264
		this.drawSelection();
2265
		this.triggerSelectedEvent();
2266
		this.ignoreClick = true;
2267

    
2268
		return false;
2269
	},
2270
	setSelectionPos: function(pos, e) {
2271
		var offset = $(this.overlay).cumulativeOffset();
2272
		if (this.options.selection.mode == "y") {
2273
			if (pos == this.selection.first)
2274
				pos.x = 0;
2275
			else
2276
				pos.x = this.chartWidth;
2277
		}
2278
		else {
2279
			pos.x = e.pageX - offset.left - this.chartOffset.left;
2280
			pos.x = Math.min(Math.max(0, pos.x), this.chartWidth);
2281
		}
2282

    
2283
		if (this.options.selection.mode == "x") {
2284
			if (pos == this.selection.first)
2285
				pos.y = 0;
2286
			else
2287
				pos.y = this.chartHeight;
2288
		}
2289
		else {
2290
			pos.y = e.pageY - offset.top - this.chartOffset.top;
2291
			pos.y = Math.min(Math.max(0, pos.y), this.chartHeight);
2292
		}
2293
	},
2294
	updateSelectionOnMouseMove: function() {
2295
		if (this.lastMousePos.pageX == null)
2296
			return;
2297
		
2298
		this.setSelectionPos(this.selection.second, this.lastMousePos);
2299
		this.clearSelection();
2300
		if (this.selectionIsSane())
2301
			this.drawSelection();
2302
	},
2303
	clearSelection: function() {
2304
		if (this.prevSelection == null)
2305
			return;
2306

    
2307
		var x = Math.min(this.prevSelection.first.x, this.prevSelection.second.x),
2308
			y = Math.min(this.prevSelection.first.y, this.prevSelection.second.y),
2309
			w = Math.abs(this.prevSelection.second.x - this.prevSelection.first.x),
2310
			h = Math.abs(this.prevSelection.second.y - this.prevSelection.first.y);
2311
		
2312
		this.overlayContext.clearRect(x + this.chartOffset.left - this.overlayContext.lineWidth,
2313
					   		y + this.chartOffset.top - this.overlayContext.lineWidth,
2314
					   		w + this.overlayContext.lineWidth*2,
2315
					   		h + this.overlayContext.lineWidth*2);
2316
		
2317
		this.prevSelection = null;
2318
	},
2319
	/**
2320
	 * Function: setSelection
2321
	 * 
2322
	 * Parameters:
2323
	 * Area - {Object} area represented as a range like: {x1: 3, y1: 3, x2: 4, y2: 8}
2324
	 * 
2325
	 * Description: 
2326
	 * Sets the current graph selection to the provided range. Calls <drawSelection> and 
2327
	 * <triggerSelectedEvent> functions internally.
2328
	 */
2329
	setSelection: function(area) {
2330
		this.clearSelection();
2331
		
2332
		if (this.options.selection.mode == "x") {
2333
			this.selection.first.y = 0;
2334
			this.selection.second.y = this.chartHeight;
2335
		}
2336
		else {
2337
			this.selection.first.y = (this.yaxis.max - area.y1) * this.vertScale;
2338
			this.selection.second.y = (this.yaxis.max - area.y2) * this.vertScale;
2339
		}
2340
		if (this.options.selection.mode == "y") {
2341
			this.selection.first.x = 0;
2342
			this.selection.second.x = this.chartWidth;
2343
		}
2344
		else {
2345
			this.selection.first.x = (area.x1 - this.xaxis.min) * this.hozScale;
2346
			this.selection.second.x = (area.x2 - this.xaxis.min) * this.hozScale;
2347
		}
2348

    
2349
		this.drawSelection();
2350
		this.triggerSelectedEvent();
2351
	},
2352
	/**
2353
	 * Function: drawSelection
2354
	 * Description: Internal function called to draw the selection made on the graph. 
2355
	 */
2356
	drawSelection: function() {
2357
		if (this.prevSelection != null &&
2358
			this.selection.first.x == this.prevSelection.first.x &&
2359
			this.selection.first.y == this.prevSelection.first.y && 
2360
			this.selection.second.x == this.prevSelection.second.x &&
2361
			this.selection.second.y == this.prevSelection.second.y)
2362
		{
2363
			return;	
2364
		}
2365
		
2366
		this.overlayContext.strokeStyle = this.parseColor(this.options.selection.color).scale(null, null, null, 0.8).toString();
2367
		this.overlayContext.lineWidth = 1;
2368
		this.context.lineJoin = "round";
2369
		this.overlayContext.fillStyle = this.parseColor(this.options.selection.color).scale(null, null, null, 0.4).toString();
2370

    
2371
		this.prevSelection = { first:  { x: this.selection.first.x,
2372
									y: this.selection.first.y },
2373
						  second: { x: this.selection.second.x,
2374
									y: this.selection.second.y } };
2375

    
2376
		var x = Math.min(this.selection.first.x, this.selection.second.x),
2377
			y = Math.min(this.selection.first.y, this.selection.second.y),
2378
			w = Math.abs(this.selection.second.x - this.selection.first.x),
2379
			h = Math.abs(this.selection.second.y - this.selection.first.y);
2380
		
2381
		this.overlayContext.fillRect(x + this.chartOffset.left, y + this.chartOffset.top, w, h);
2382
		this.overlayContext.strokeRect(x + this.chartOffset.left, y + this.chartOffset.top, w, h);
2383
	},
2384
	/**
2385
	 * Internal function
2386
	 */
2387
	selectionIsSane: function() {
2388
		var minSize = 5;
2389
		return Math.abs(this.selection.second.x - this.selection.first.x) >= minSize &&
2390
			Math.abs(this.selection.second.y - this.selection.first.y) >= minSize;
2391
	},
2392
	/**
2393
	 * Internal function that formats the track. This is the format the text is shown when mouse
2394
	 * tracking is enabled.
2395
	 */
2396
	defaultTrackFormatter: function(val)
2397
	{
2398
		return '['+val.x+', '+val.y+']';
2399
	},
2400
	/**
2401
	 * Function: clearHit
2402
	 */
2403
	clearHit: function(){
2404
		if(this.prevHit){
2405
			this.overlayContext.clearRect(
2406
				this.translateHoz(this.prevHit.x) + this.chartOffset.left - this.options.mouse.radius*2,
2407
				this.translateVert(this.prevHit.y) + this.chartOffset.top - this.options.mouse.radius*2,
2408
				this.options.mouse.radius*3 + this.options.points.lineWidth*3, 
2409
				this.options.mouse.radius*3 + this.options.points.lineWidth*3
2410
			);
2411
			this.prevHit = null;
2412
		}		
2413
	},	
2414
	/**
2415
	 * Function: hit
2416
	 * 
2417
	 * Parameters: 
2418
	 * 	event - {Object} event object
2419
	 * 	mouse - {Object} mouse object that is used to keep track of mouse movement
2420
	 * 
2421
	 * Description:
2422
	 * 	If hit occurs this function will fire a ProtoChart:hit event.
2423
	 */
2424
	hit: function(event, mouse){	
2425
		/**
2426
		 * Nearest data element.
2427
		 */
2428
		var n = {
2429
			dist:Number.MAX_VALUE,
2430
			x:null,
2431
			y:null,
2432
			mouse:null
2433
		};
2434
		
2435
		
2436
		for(var i = 0, data, xsens, ysens; i < this.graphData.length; i++){
2437
			if(!this.graphData[i].mouse.track) continue;
2438
			data = this.graphData[i].data;				
2439
			xsens = (this.hozScale*this.graphData[i].mouse.sensibility);
2440
			ysens = (this.vertScale*this.graphData[i].mouse.sensibility);
2441
			for(var j = 0, xabs, yabs; j < data.length; j++){
2442
				xabs = this.hozScale*Math.abs(data[j][0] - mouse.x);
2443
				yabs = this.vertScale*Math.abs(data[j][1] - mouse.y);
2444
				
2445
				if(xabs < xsens && yabs < ysens && (xabs+yabs) < n.dist){
2446
					n.dist = (xabs+yabs);
2447
					n.x = data[j][0];
2448
					n.y = data[j][1];
2449
					n.mouse = this.graphData[i].mouse;
2450
				}
2451
			}
2452
		}
2453
		
2454
		if(n.mouse && n.mouse.track && !this.prevHit || (this.prevHit && n.x != this.prevHit.x && n.y != this.prevHit.y)){
2455
			var el = this.domObj.select('.'+this.options.mouse.clsName).first();
2456
			if(!el){
2457
				var pos = '', p = this.options.mouse.position, m = this.options.mouse.margin;					
2458
				if(p.charAt(0) == 'n') pos += 'top:' + (m + this.chartOffset.top) + 'px;';
2459
				else if(p.charAt(0) == 's') pos += 'bottom:' + (m + this.chartOffset.bottom) + 'px;';					
2460
				if(p.charAt(1) == 'e') pos += 'right:' + (m + this.chartOffset.right) + 'px;';
2461
				else if(p.charAt(1) == 'w') pos += 'left:' + (m + this.chartOffset.bottom) + 'px;';
2462
				
2463
				this.domObj.insert('<div class="'+this.options.mouse.clsName+'" style="display:none;position:absolute;'+pos+'"></div>');
2464
				return;
2465
			}
2466
			if(n.x !== null && n.y !== null){
2467
				el.setStyle({display:'block'});					
2468
				
2469
				this.clearHit();
2470
				if(n.mouse.lineColor != null){
2471
					this.overlayContext.save();
2472
					this.overlayContext.translate(this.chartOffset.left, this.chartOffset.top);
2473
					this.overlayContext.lineWidth = this.options.points.lineWidth;
2474
					this.overlayContext.strokeStyle = n.mouse.lineColor;
2475
					this.overlayContext.fillStyle = '#ffffff';
2476
					this.overlayContext.beginPath();
2477
					
2478
					
2479
					this.overlayContext.arc(this.translateHoz(n.x), this.translateVert(n.y), this.options.mouse.radius, 0, 2 * Math.PI, true);
2480
					this.overlayContext.fill();
2481
					this.overlayContext.stroke();
2482
					this.overlayContext.restore();
2483
				} 
2484
				this.prevHit = n;
2485
								
2486
				var decimals = n.mouse.trackDecimals;
2487
				if(decimals == null || decimals < 0) decimals = 0;
2488
				if(!this.options.mouse.fixedPosition)
2489
				{
2490
					el.setStyle({
2491
						left: (this.translateHoz(n.x) + this.options.mouse.radius + 10) + "px",
2492
						top: (this.translateVert(n.y) + this.options.mouse.radius + 10) + "px"
2493
					});
2494
				}
2495
				el.innerHTML = n.mouse.trackFormatter({x: n.x.toFixed(decimals), y: n.y.toFixed(decimals)});
2496
				this.domObj.fire( 'ProtoChart:hit', [n] )					
2497
			}else if(this.options.prevHit){
2498
				el.setStyle({display:'none'});
2499
				this.clearHit();
2500
			}
2501
		}
2502
	},	
2503
	/**
2504
	 * Internal function
2505
	 */
2506
	floorInBase: function(n, base) {
2507
        return base * Math.floor(n / base);
2508
    },	
2509
	/**
2510
	 * Function: extractColor
2511
	 * 
2512
	 * Parameters:
2513
	 * 		element - HTML element or ID of an HTML element
2514
	 * 
2515
	 * Returns: 
2516
	 * 		color in string format
2517
	 */
2518
	extractColor: function(element)
2519
	{
2520
		var color;
2521
		do
2522
		{
2523
			color = $(element).getStyle('background-color').toLowerCase();
2524
			if(color  != '' && color != 'transparent')
2525
			{
2526
				break;
2527
			}
2528
			element = element.up(0); //or else just get the parent ....
2529
		} while(element.nodeName.toLowerCase() != 'body');
2530
		
2531
		//safari fix
2532
		if(color == 'rgba(0, 0, 0, 0)') 
2533
			return 'transparent';
2534
		return color;
2535
	},
2536
	/**
2537
	 * Function: parseColor
2538
	 * 
2539
	 * Parameters: 
2540
	 * 		str - color string in different formats
2541
	 * 
2542
	 * Returns:
2543
	 * 		a Proto.Color Object - use toString() function to retreive the color in rgba/rgb format
2544
	 */
2545
	parseColor: function(str)
2546
	{
2547
		var result;
2548
	
2549
		/**
2550
		 * rgb(num,num,num)
2551
		 */
2552
		if((result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str)))
2553
			return new Proto.Color(parseInt(result[1]), parseInt(result[2]), parseInt(result[3]));
2554
	
2555
		/**
2556
		 * rgba(num,num,num,num)
2557
		 */
2558
		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)))
2559
			return new Proto.Color(parseInt(result[1]), parseInt(result[2]), parseInt(result[3]), parseFloat(result[4]));
2560
			
2561
		/**
2562
		 * rgb(num%,num%,num%)
2563
		 */
2564
		if((result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str)))
2565
			return new Proto.Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55);
2566
	
2567
		/**
2568
		 * rgba(num%,num%,num%,num)
2569
		 */
2570
		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)))
2571
			return new Proto.Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55, parseFloat(result[4]));
2572
			
2573
		/**
2574
		 * #a0b1c2
2575
		 */
2576
		if((result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str)))
2577
			return new Proto.Color(parseInt(result[1],16), parseInt(result[2],16), parseInt(result[3],16));
2578
	
2579
		/**
2580
		 * #fff
2581
		 */
2582
		if((result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str)))
2583
			return new Proto.Color(parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16));
2584

    
2585
		/**
2586
		 * Otherwise, check if user wants transparent .. or we just return a standard color;
2587
		 */
2588
		var name = str.strip().toLowerCase();
2589
		if(name == 'transparent'){
2590
			return new Proto.Color(255, 255, 255, 0);
2591
		}
2592

    
2593
		return new Proto.Color(100,100,100, 1);
2594
					
2595
	}		
2596
});
2597

    
2598
if(!Proto) var Proto = {};
2599

    
2600
/**
2601
 * Class: Proto.Color
2602
 * 
2603
 * Helper class that manipulates colors using RGBA values. 
2604
 * 
2605
 */
2606

    
2607
Proto.Color = Class.create({
2608
	initialize: function(r, g, b, a) {
2609
		this.rgba = ['r', 'g', 'b', 'a'];
2610
		var x = 4;
2611
		while(-1<--x) {
2612
			this[this.rgba[x]] = arguments[x] || ((x==3) ? 1.0 : 0);
2613
		}
2614
	},
2615
	toString: function()  {
2616
		if(this.a >= 1.0) {
2617
			return "rgb(" + [this.r, this.g, this.b].join(",") +")";
2618
		}
2619
		else {
2620
			return "rgba("+[this.r, this.g, this.b, this.a].join(",")+")";
2621
		}
2622
	},
2623
	scale: function(rf, gf, bf, af) {
2624
		x = 4;
2625
		while(-1<--x) {
2626
			if(arguments[x] != null) {
2627
				this[this.rgba[x]] *= arguments[x];
2628
			}
2629
		}
2630
		return this.normalize();
2631
	},
2632
	adjust: function(rd, gd, bd, ad) {
2633
        x = 4; //rgba.length
2634
        while (-1<--x) {
2635
            if (arguments[x] != null)
2636
                this[this.rgba[x]] += arguments[x];
2637
        }
2638
        return this.normalize();		
2639
	},
2640
	clone: function() {
2641
        return new Proto.Color(this.r, this.b, this.g, this.a);
2642
    },
2643
	limit: function(val,minVal,maxVal) {
2644
        return Math.max(Math.min(val, maxVal), minVal);
2645
    },
2646
    normalize: function() {
2647
        this.r = this.limit(parseInt(this.r), 0, 255);
2648
        this.g = this.limit(parseInt(this.g), 0, 255);
2649
        this.b = this.limit(parseInt(this.b), 0, 255);
2650
        this.a = this.limit(this.a, 0, 1);
2651
        return this;
2652
    }
2653
});
(1-1/3)