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
|
});
|