first commit
[wnstats.git] / public / javascripts / jqplot / plugins / jqplot.funnelRenderer.js
blobc72660cc901073c13ae8e800c9503846a3554862
1 /**
2  * jqPlot
3  * Pure JavaScript plotting plugin using jQuery
4  *
5  * Version: 1.0.4
6  * Revision: 1121
7  *
8  * Copyright (c) 2009-2012 Chris Leonello
9  * jqPlot is currently available for use in all personal or commercial projects 
10  * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL 
11  * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can 
12  * choose the license that best suits your project and use it accordingly. 
13  *
14  * Although not required, the author would appreciate an email letting him 
15  * know of any substantial use of jqPlot.  You can reach the author at: 
16  * chris at jqplot dot com or see http://www.jqplot.com/info.php .
17  *
18  * If you are feeling kind and generous, consider supporting the project by
19  * making a donation at: http://www.jqplot.com/donate.php .
20  *
21  * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
22  *
23  *     version 2007.04.27
24  *     author Ash Searle
25  *     http://hexmen.com/blog/2007/03/printf-sprintf/
26  *     http://hexmen.com/js/sprintf.js
27  *     The author (Ash Searle) has placed this code in the public domain:
28  *     "This code is unrestricted: you are free to use it however you like."
29  * 
30  */
31 (function($) {
32     /**
33      * Class: $.jqplot.FunnelRenderer
34      * Plugin renderer to draw a funnel chart.
35      * x values, if present, will be used as labels.
36      * y values give area size.
37      * 
38      * Funnel charts will draw a single series
39      * only.
40      * 
41      * To use this renderer, you need to include the 
42      * funnel renderer plugin, for example:
43      * 
44      * > <script type="text/javascript" src="plugins/jqplot.funnelRenderer.js"></script>
45      * 
46      * Properties described here are passed into the $.jqplot function
47      * as options on the series renderer.  For example:
48      * 
49      * > plot2 = $.jqplot('chart2', [s1, s2], {
50      * >     seriesDefaults: {
51      * >         renderer:$.jqplot.FunnelRenderer,
52      * >         rendererOptions:{
53      * >              sectionMargin: 12,
54      * >              widthRatio: 0.3
55      * >          }
56      * >      }
57      * > });
58      * 
59      * IMPORTANT
60      * 
61      * *The funnel renderer will reorder data in descending order* so the largest value in
62      * the data set is first and displayed on top of the funnel.  Data will then
63      * be displayed in descending order down the funnel.  The area of each funnel
64      * section will correspond to the value of each data point relative to the sum
65      * of all values.  That is section area is proportional to section value divided by 
66      * sum of all section values.
67      * 
68      * If your data is not in descending order when passed into the plot, *it will be
69      * reordered* when stored in the series.data property.  A copy of the unordered
70      * data is kept in the series._unorderedData property.
71      * 
72      * A funnel plot will trigger events on the plot target
73      * according to user interaction.  All events return the event object,
74      * the series index, the point (section) index, and the point data for 
75      * the appropriate section. *Note* the point index will referr to the ordered
76      * data, not the original unordered data.
77      * 
78      * 'jqplotDataMouseOver' - triggered when mousing over a section.
79      * 'jqplotDataHighlight' - triggered the first time user mouses over a section,
80      * if highlighting is enabled.
81      * 'jqplotDataUnhighlight' - triggered when a user moves the mouse out of
82      * a highlighted section.
83      * 'jqplotDataClick' - triggered when the user clicks on a section.
84      * 'jqplotDataRightClick' - tiggered when the user right clicks on a section if
85      * the "captureRightClick" option is set to true on the plot.
86      */
87     $.jqplot.FunnelRenderer = function(){
88         $.jqplot.LineRenderer.call(this);
89     };
90     
91     $.jqplot.FunnelRenderer.prototype = new $.jqplot.LineRenderer();
92     $.jqplot.FunnelRenderer.prototype.constructor = $.jqplot.FunnelRenderer;
93     
94     // called with scope of a series
95     $.jqplot.FunnelRenderer.prototype.init = function(options, plot) {
96         // Group: Properties
97         //
98         // prop: padding
99         // padding between the funnel and plot edges, legend, etc.
100         this.padding = {top: 20, right: 20, bottom: 20, left: 20};
101         // prop: sectionMargin
102         // spacing between funnel sections in pixels.
103         this.sectionMargin = 6;
104         // prop: fill
105         // true or false, wether to fill the areas.
106         this.fill = true;
107         // prop: shadowOffset
108         // offset of the shadow from the area and offset of 
109         // each succesive stroke of the shadow from the last.
110         this.shadowOffset = 2;
111         // prop: shadowAlpha
112         // transparency of the shadow (0 = transparent, 1 = opaque)
113         this.shadowAlpha = 0.07;
114         // prop: shadowDepth
115         // number of strokes to apply to the shadow, 
116         // each stroke offset shadowOffset from the last.
117         this.shadowDepth = 5;
118         // prop: highlightMouseOver
119         // True to highlight area when moused over.
120         // This must be false to enable highlightMouseDown to highlight when clicking on a area.
121         this.highlightMouseOver = true;
122         // prop: highlightMouseDown
123         // True to highlight when a mouse button is pressed over a area.
124         // This will be disabled if highlightMouseOver is true.
125         this.highlightMouseDown = false;
126         // prop: highlightColors
127         // array of colors to use when highlighting an area.
128         this.highlightColors = [];
129         // prop: widthRatio
130         // The ratio of the width of the top of the funnel to the bottom.
131         // a ratio of 0 will make an upside down pyramid. 
132         this.widthRatio = 0.2;
133         // prop: lineWidth
134         // width of line if areas are stroked and not filled.
135         this.lineWidth = 2;
136         // prop: dataLabels
137         // Either 'label', 'value', 'percent' or an array of labels to place on the pie slices.
138         // Defaults to percentage of each pie slice.
139         this.dataLabels = 'percent';
140         // prop: showDataLabels
141         // true to show data labels on slices.
142         this.showDataLabels = false;
143         // prop: dataLabelFormatString
144         // Format string for data labels.  If none, '%s' is used for "label" and for arrays, '%d' for value and '%d%%' for percentage.
145         this.dataLabelFormatString = null;
146         // prop: dataLabelThreshold
147         // Threshhold in percentage (0 - 100) of pie area, below which no label will be displayed.
148         // This applies to all label types, not just to percentage labels.
149         this.dataLabelThreshold = 3;
150         this._type = 'funnel';
151         
152         this.tickRenderer = $.jqplot.FunnelTickRenderer;
153         
154         // if user has passed in highlightMouseDown option and not set highlightMouseOver, disable highlightMouseOver
155         if (options.highlightMouseDown && options.highlightMouseOver == null) {
156             options.highlightMouseOver = false;
157         }
158         
159         $.extend(true, this, options);
160         
161         // index of the currenty highlighted point, if any
162         this._highlightedPoint = null;
163         
164         // lengths of bases, or horizontal sides of areas of trapezoid.
165         this._bases = [];
166         // total area
167         this._atot;
168         // areas of segments.
169         this._areas = [];
170         // vertical lengths of segments.
171         this._lengths = [];
172         // angle of the funnel to vertical.
173         this._angle;
174         this._dataIndices = [];
175         
176         // sort data
177         this._unorderedData = $.extend(true, [], this.data);
178         var idxs = $.extend(true, [], this.data);
179         for (var i=0; i<idxs.length; i++) {
180             idxs[i].push(i);
181         }
182         this.data.sort( function (a, b) { return b[1] - a[1]; } );
183         idxs.sort( function (a, b) { return b[1] - a[1]; });
184         for (var i=0; i<idxs.length; i++) {
185             this._dataIndices.push(idxs[i][2]);
186         }
187         
188         // set highlight colors if none provided
189         if (this.highlightColors.length == 0) {
190             for (var i=0; i<this.seriesColors.length; i++){
191                 var rgba = $.jqplot.getColorComponents(this.seriesColors[i]);
192                 var newrgb = [rgba[0], rgba[1], rgba[2]];
193                 var sum = newrgb[0] + newrgb[1] + newrgb[2];
194                 for (var j=0; j<3; j++) {
195                     // when darkening, lowest color component can be is 60.
196                     newrgb[j] = (sum > 570) ?  newrgb[j] * 0.8 : newrgb[j] + 0.4 * (255 - newrgb[j]);
197                     newrgb[j] = parseInt(newrgb[j], 10);
198                 }
199                 this.highlightColors.push('rgb('+newrgb[0]+','+newrgb[1]+','+newrgb[2]+')');
200             }
201         }
203         plot.postParseOptionsHooks.addOnce(postParseOptions);
204         plot.postInitHooks.addOnce(postInit);
205         plot.eventListenerHooks.addOnce('jqplotMouseMove', handleMove);
206         plot.eventListenerHooks.addOnce('jqplotMouseDown', handleMouseDown);
207         plot.eventListenerHooks.addOnce('jqplotMouseUp', handleMouseUp);
208         plot.eventListenerHooks.addOnce('jqplotClick', handleClick);
209         plot.eventListenerHooks.addOnce('jqplotRightClick', handleRightClick);
210         plot.postDrawHooks.addOnce(postPlotDraw);        
211         
212     };
213     
214     // gridData will be of form [label, percentage of total]
215     $.jqplot.FunnelRenderer.prototype.setGridData = function(plot) {
216         // set gridData property.  This will hold angle in radians of each data point.
217         var sum = 0;
218         var td = [];
219         for (var i=0; i<this.data.length; i++){
220             sum += this.data[i][1];
221             td.push([this.data[i][0], this.data[i][1]]);
222         }
223         
224         // normalize y values, so areas are proportional.
225         for (var i=0; i<td.length; i++) {
226             td[i][1] = td[i][1]/sum;
227         }
228         
229         this._bases = new Array(td.length + 1);
230         this._lengths = new Array(td.length);
231         
232         this.gridData = td;
233     };
234     
235     $.jqplot.FunnelRenderer.prototype.makeGridData = function(data, plot) {
236         // set gridData property.  This will hold angle in radians of each data point.
237         var sum = 0;
238         var td = [];
239         for (var i=0; i<this.data.length; i++){
240             sum += this.data[i][1];
241             td.push([this.data[i][0], this.data[i][1]]);
242         }
243         
244         // normalize y values, so areas are proportional.
245         for (var i=0; i<td.length; i++) {
246             td[i][1] = td[i][1]/sum;
247         }
248         
249         this._bases = new Array(td.length + 1);
250         this._lengths = new Array(td.length);
251         
252         return td;
253     };
254     
255     $.jqplot.FunnelRenderer.prototype.drawSection = function (ctx, vertices, color, isShadow) {
256         var fill = this.fill;
257         var lineWidth = this.lineWidth;
258         ctx.save();
259         
260         if (isShadow) {
261             for (var i=0; i<this.shadowDepth; i++) {
262                 ctx.save();
263                 ctx.translate(this.shadowOffset*Math.cos(this.shadowAngle/180*Math.PI), this.shadowOffset*Math.sin(this.shadowAngle/180*Math.PI));
264                 doDraw();
265             }
266         }
267         
268         else {
269             doDraw();
270         }
271         
272         function doDraw () {
273             ctx.beginPath();  
274             ctx.fillStyle = color;
275             ctx.strokeStyle = color;
276             ctx.lineWidth = lineWidth;
277             ctx.moveTo(vertices[0][0], vertices[0][1]);
278             for (var i=1; i<4; i++) {
279                 ctx.lineTo(vertices[i][0], vertices[i][1]);
280             }
281             ctx.closePath();
282             if (fill) {
283                 ctx.fill();
284             }
285             else {
286                 ctx.stroke();
287             }
288         }
289         
290         if (isShadow) {
291             for (var i=0; i<this.shadowDepth; i++) {
292                 ctx.restore();
293             }
294         }
295         
296         ctx.restore();
297     };
298     
299     // called with scope of series
300     $.jqplot.FunnelRenderer.prototype.draw = function (ctx, gd, options, plot) {
301         var i;
302         var opts = (options != undefined) ? options : {};
303         // offset and direction of offset due to legend placement
304         var offx = 0;
305         var offy = 0;
306         var trans = 1;
307         this._areas = [];
308         // var colorGenerator = new this.colorGenerator(this.seriesColors);
309         if (options.legendInfo && options.legendInfo.placement == 'insideGrid') {
310             var li = options.legendInfo;
311             switch (li.location) {
312                 case 'nw':
313                     offx = li.width + li.xoffset;
314                     break;
315                 case 'w':
316                     offx = li.width + li.xoffset;
317                     break;
318                 case 'sw':
319                     offx = li.width + li.xoffset;
320                     break;
321                 case 'ne':
322                     offx = li.width + li.xoffset;
323                     trans = -1;
324                     break;
325                 case 'e':
326                     offx = li.width + li.xoffset;
327                     trans = -1;
328                     break;
329                 case 'se':
330                     offx = li.width + li.xoffset;
331                     trans = -1;
332                     break;
333                 case 'n':
334                     offy = li.height + li.yoffset;
335                     break;
336                 case 's':
337                     offy = li.height + li.yoffset;
338                     trans = -1;
339                     break;
340                 default:
341                     break;
342             }
343         }
344         
345         var loff = (trans==1) ? this.padding.left + offx : this.padding.left;
346         var toff = (trans==1) ? this.padding.top + offy : this.padding.top;
347         var roff = (trans==-1) ? this.padding.right + offx : this.padding.right;
348         var boff = (trans==-1) ? this.padding.bottom + offy : this.padding.bottom;
349         
350         var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow;
351         var showLine = (opts.showLine != undefined) ? opts.showLine : this.showLine;
352         var fill = (opts.fill != undefined) ? opts.fill : this.fill;
353         var cw = ctx.canvas.width;
354         var ch = ctx.canvas.height;
355         this._bases[0] = cw - loff - roff;
356         var ltot = this._length = ch - toff - boff;
358         var hend = this._bases[0]*this.widthRatio;
359         this._atot = ltot/2 * (this._bases[0] + this._bases[0]*this.widthRatio);
361         this._angle = Math.atan((this._bases[0] - hend)/2/ltot);
363         for (i=0; i<gd.length; i++) {
364             this._areas.push(gd[i][1] * this._atot);
365         }
367         
368         var guess, err, count, lsum=0;
369         var tolerance = 0.0001;
371         for (i=0; i<this._areas.length; i++) {
372             guess = this._areas[i]/this._bases[i];
373             err = 999999;
374             this._lengths[i] = guess;
375             count = 0;
376             while (err > this._lengths[i]*tolerance && count < 100) {
377                 this._lengths[i] = this._areas[i]/(this._bases[i] - this._lengths[i] * Math.tan(this._angle));
378                 err = Math.abs(this._lengths[i] - guess);
379                 this._bases[i+1] = this._bases[i] - (2*this._lengths[i]*Math.tan(this._angle));
380                 guess = this._lengths[i];
381                 count++;
382             }
383             lsum += this._lengths[i];
384         }
385         
386         // figure out vertices of each section
387         this._vertices = new Array(gd.length);
388         
389         // these are 4 coners of entire trapezoid
390         var p0 = [loff, toff],
391             p1 = [loff+this._bases[0], toff],
392             p2 = [loff + (this._bases[0] - this._bases[this._bases.length-1])/2, toff + this._length],
393             p3 = [p2[0] + this._bases[this._bases.length-1], p2[1]];
394             
395         // equations of right and left sides, returns x, y values given height of section (y value)
396         function findleft (l) {
397             var m = (p0[1] - p2[1])/(p0[0] - p2[0]);
398             var b = p0[1] - m*p0[0];
399             var y = l + p0[1];
400             
401             return [(y - b)/m, y];
402         }
403         
404         function findright (l) {
405             var m = (p1[1] - p3[1])/(p1[0] - p3[0]);
406             var b = p1[1] - m*p1[0];
407             var y = l + p1[1];
408             
409             return [(y - b)/m, y];
410         }
411         
412         var x = offx, y = offy;
413         var h=0, adj=0;
414         
415         for (i=0; i<gd.length; i++) {
416             this._vertices[i] = new Array();
417             var v = this._vertices[i];
418             var sm = this.sectionMargin;
419             if (i == 0) {
420                 adj = 0;
421             }
422             if (i == 1) {
423                 adj = sm/3;
424             }
425             else if (i > 0 && i < gd.length-1) {
426                 adj = sm/2;
427             }
428             else if (i == gd.length -1) {
429                 adj = 2*sm/3;
430             }
431             v.push(findleft(h+adj));
432             v.push(findright(h+adj));
433             h += this._lengths[i];
434             if (i == 0) {
435                 adj = -2*sm/3;
436             }
437             else if (i > 0 && i < gd.length-1) {
438                 adj = -sm/2;
439             }
440             else if (i == gd.length - 1) {
441                 adj = 0;
442             }
443             v.push(findright(h+adj));
444             v.push(findleft(h+adj));
445             
446         }
448         if (this.shadow) {
449             var shadowColor = 'rgba(0,0,0,'+this.shadowAlpha+')';
450             for (var i=0; i<gd.length; i++) {
451                 this.renderer.drawSection.call (this, ctx, this._vertices[i], shadowColor, true);
452             }
453             
454         }
455         for (var i=0; i<gd.length; i++) {
456             var v = this._vertices[i];
457             this.renderer.drawSection.call (this, ctx, v, this.seriesColors[i]);
458             
459             if (this.showDataLabels && gd[i][1]*100 >= this.dataLabelThreshold) {
460                 var fstr, label;
461                 
462                 if (this.dataLabels == 'label') {
463                     fstr = this.dataLabelFormatString || '%s';
464                     label = $.jqplot.sprintf(fstr, gd[i][0]);
465                 }
466                 else if (this.dataLabels == 'value') {
467                     fstr = this.dataLabelFormatString || '%d';
468                     label = $.jqplot.sprintf(fstr, this.data[i][1]);
469                 }
470                 else if (this.dataLabels == 'percent') {
471                     fstr = this.dataLabelFormatString || '%d%%';
472                     label = $.jqplot.sprintf(fstr, gd[i][1]*100);
473                 }
474                 else if (this.dataLabels.constructor == Array) {
475                     fstr = this.dataLabelFormatString || '%s';
476                     label = $.jqplot.sprintf(fstr, this.dataLabels[this._dataIndices[i]]);
477                 }
478                 
479                 var fact = (this._radius ) * this.dataLabelPositionFactor + this.sliceMargin + this.dataLabelNudge;
480                 
481                 var x = (v[0][0] + v[1][0])/2 + this.canvas._offsets.left;
482                 var y = (v[1][1] + v[2][1])/2 + this.canvas._offsets.top;
483                 
484                 var labelelem = $('<span class="jqplot-funnel-series jqplot-data-label" style="position:absolute;">' + label + '</span>').insertBefore(plot.eventCanvas._elem);
485                 x -= labelelem.width()/2;
486                 y -= labelelem.height()/2;
487                 x = Math.round(x);
488                 y = Math.round(y);
489                 labelelem.css({left: x, top: y});
490             }
491             
492         }
493                
494     };
495     
496     $.jqplot.FunnelAxisRenderer = function() {
497         $.jqplot.LinearAxisRenderer.call(this);
498     };
499     
500     $.jqplot.FunnelAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
501     $.jqplot.FunnelAxisRenderer.prototype.constructor = $.jqplot.FunnelAxisRenderer;
502         
503     
504     // There are no traditional axes on a funnel chart.  We just need to provide
505     // dummy objects with properties so the plot will render.
506     // called with scope of axis object.
507     $.jqplot.FunnelAxisRenderer.prototype.init = function(options){
508         //
509         this.tickRenderer = $.jqplot.FunnelTickRenderer;
510         $.extend(true, this, options);
511         // I don't think I'm going to need _dataBounds here.
512         // have to go Axis scaling in a way to fit chart onto plot area
513         // and provide u2p and p2u functionality for mouse cursor, etc.
514         // for convienence set _dataBounds to 0 and 100 and
515         // set min/max to 0 and 100.
516         this._dataBounds = {min:0, max:100};
517         this.min = 0;
518         this.max = 100;
519         this.showTicks = false;
520         this.ticks = [];
521         this.showMark = false;
522         this.show = false; 
523     };
524     
525     
526     
527     /**
528      * Class: $.jqplot.FunnelLegendRenderer
529      * Legend Renderer specific to funnel plots.  Set by default
530      * when the user creates a funnel plot.
531      */
532     $.jqplot.FunnelLegendRenderer = function(){
533         $.jqplot.TableLegendRenderer.call(this);
534     };
535     
536     $.jqplot.FunnelLegendRenderer.prototype = new $.jqplot.TableLegendRenderer();
537     $.jqplot.FunnelLegendRenderer.prototype.constructor = $.jqplot.FunnelLegendRenderer;
538     
539     $.jqplot.FunnelLegendRenderer.prototype.init = function(options) {
540         // Group: Properties
541         //
542         // prop: numberRows
543         // Maximum number of rows in the legend.  0 or null for unlimited.
544         this.numberRows = null;
545         // prop: numberColumns
546         // Maximum number of columns in the legend.  0 or null for unlimited.
547         this.numberColumns = null;
548         $.extend(true, this, options);
549     };
550     
551     // called with context of legend
552     $.jqplot.FunnelLegendRenderer.prototype.draw = function() {
553         var legend = this;
554         if (this.show) {
555             var series = this._series;
556             var ss = 'position:absolute;';
557             ss += (this.background) ? 'background:'+this.background+';' : '';
558             ss += (this.border) ? 'border:'+this.border+';' : '';
559             ss += (this.fontSize) ? 'font-size:'+this.fontSize+';' : '';
560             ss += (this.fontFamily) ? 'font-family:'+this.fontFamily+';' : '';
561             ss += (this.textColor) ? 'color:'+this.textColor+';' : '';
562             ss += (this.marginTop != null) ? 'margin-top:'+this.marginTop+';' : '';
563             ss += (this.marginBottom != null) ? 'margin-bottom:'+this.marginBottom+';' : '';
564             ss += (this.marginLeft != null) ? 'margin-left:'+this.marginLeft+';' : '';
565             ss += (this.marginRight != null) ? 'margin-right:'+this.marginRight+';' : '';
566             this._elem = $('<table class="jqplot-table-legend" style="'+ss+'"></table>');
567             // Funnel charts legends don't go by number of series, but by number of data points
568             // in the series.  Refactor things here for that.
569             
570             var pad = false, 
571                 reverse = false,
572                 nr, nc;
573             var s = series[0];
574             var colorGenerator = new $.jqplot.ColorGenerator(s.seriesColors);
575             
576             if (s.show) {
577                 var pd = s.data;
578                 if (this.numberRows) {
579                     nr = this.numberRows;
580                     if (!this.numberColumns){
581                         nc = Math.ceil(pd.length/nr);
582                     }
583                     else{
584                         nc = this.numberColumns;
585                     }
586                 }
587                 else if (this.numberColumns) {
588                     nc = this.numberColumns;
589                     nr = Math.ceil(pd.length/this.numberColumns);
590                 }
591                 else {
592                     nr = pd.length;
593                     nc = 1;
594                 }
595                 
596                 var i, j, tr, td1, td2, lt, rs, color;
597                 var idx = 0;    
598                 
599                 for (i=0; i<nr; i++) {
600                     if (reverse){
601                         tr = $('<tr class="jqplot-table-legend"></tr>').prependTo(this._elem);
602                     }
603                     else{
604                         tr = $('<tr class="jqplot-table-legend"></tr>').appendTo(this._elem);
605                     }
606                     for (j=0; j<nc; j++) {
607                         if (idx < pd.length){
608                             lt = this.labels[idx] || pd[idx][0].toString();
609                             color = colorGenerator.next();
610                             if (!reverse){
611                                 if (i>0){
612                                     pad = true;
613                                 }
614                                 else{
615                                     pad = false;
616                                 }
617                             }
618                             else{
619                                 if (i == nr -1){
620                                     pad = false;
621                                 }
622                                 else{
623                                     pad = true;
624                                 }
625                             }
626                             rs = (pad) ? this.rowSpacing : '0';
627                 
628                             td1 = $('<td class="jqplot-table-legend" style="text-align:center;padding-top:'+rs+';">'+
629                                 '<div><div class="jqplot-table-legend-swatch" style="border-color:'+color+';"></div>'+
630                                 '</div></td>');
631                             td2 = $('<td class="jqplot-table-legend" style="padding-top:'+rs+';"></td>');
632                             if (this.escapeHtml){
633                                 td2.text(lt);
634                             }
635                             else {
636                                 td2.html(lt);
637                             }
638                             if (reverse) {
639                                 td2.prependTo(tr);
640                                 td1.prependTo(tr);
641                             }
642                             else {
643                                 td1.appendTo(tr);
644                                 td2.appendTo(tr);
645                             }
646                             pad = true;
647                         }
648                         idx++;
649                     }   
650                 }
651             }
652         }
653         return this._elem;                
654     };
655     
656     // $.jqplot.FunnelLegendRenderer.prototype.pack = function(offsets) {
657     //     if (this.show) {
658     //         // fake a grid for positioning
659     //         var grid = {_top:offsets.top, _left:offsets.left, _right:offsets.right, _bottom:this._plotDimensions.height - offsets.bottom};        
660     //         if (this.placement == 'insideGrid') {
661     //             switch (this.location) {
662     //                 case 'nw':
663     //                     var a = grid._left + this.xoffset;
664     //                     var b = grid._top + this.yoffset;
665     //                     this._elem.css('left', a);
666     //                     this._elem.css('top', b);
667     //                     break;
668     //                 case 'n':
669     //                     var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
670     //                     var b = grid._top + this.yoffset;
671     //                     this._elem.css('left', a);
672     //                     this._elem.css('top', b);
673     //                     break;
674     //                 case 'ne':
675     //                     var a = offsets.right + this.xoffset;
676     //                     var b = grid._top + this.yoffset;
677     //                     this._elem.css({right:a, top:b});
678     //                     break;
679     //                 case 'e':
680     //                     var a = offsets.right + this.xoffset;
681     //                     var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
682     //                     this._elem.css({right:a, top:b});
683     //                     break;
684     //                 case 'se':
685     //                     var a = offsets.right + this.xoffset;
686     //                     var b = offsets.bottom + this.yoffset;
687     //                     this._elem.css({right:a, bottom:b});
688     //                     break;
689     //                 case 's':
690     //                     var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
691     //                     var b = offsets.bottom + this.yoffset;
692     //                     this._elem.css({left:a, bottom:b});
693     //                     break;
694     //                 case 'sw':
695     //                     var a = grid._left + this.xoffset;
696     //                     var b = offsets.bottom + this.yoffset;
697     //                     this._elem.css({left:a, bottom:b});
698     //                     break;
699     //                 case 'w':
700     //                     var a = grid._left + this.xoffset;
701     //                     var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
702     //                     this._elem.css({left:a, top:b});
703     //                     break;
704     //                 default:  // same as 'se'
705     //                     var a = grid._right - this.xoffset;
706     //                     var b = grid._bottom + this.yoffset;
707     //                     this._elem.css({right:a, bottom:b});
708     //                     break;
709     //             }
710     //             
711     //         }
712     //         else {
713     //             switch (this.location) {
714     //                 case 'nw':
715     //                     var a = this._plotDimensions.width - grid._left + this.xoffset;
716     //                     var b = grid._top + this.yoffset;
717     //                     this._elem.css('right', a);
718     //                     this._elem.css('top', b);
719     //                     break;
720     //                 case 'n':
721     //                     var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
722     //                     var b = this._plotDimensions.height - grid._top + this.yoffset;
723     //                     this._elem.css('left', a);
724     //                     this._elem.css('bottom', b);
725     //                     break;
726     //                 case 'ne':
727     //                     var a = this._plotDimensions.width - offsets.right + this.xoffset;
728     //                     var b = grid._top + this.yoffset;
729     //                     this._elem.css({left:a, top:b});
730     //                     break;
731     //                 case 'e':
732     //                     var a = this._plotDimensions.width - offsets.right + this.xoffset;
733     //                     var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
734     //                     this._elem.css({left:a, top:b});
735     //                     break;
736     //                 case 'se':
737     //                     var a = this._plotDimensions.width - offsets.right + this.xoffset;
738     //                     var b = offsets.bottom + this.yoffset;
739     //                     this._elem.css({left:a, bottom:b});
740     //                     break;
741     //                 case 's':
742     //                     var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
743     //                     var b = this._plotDimensions.height - offsets.bottom + this.yoffset;
744     //                     this._elem.css({left:a, top:b});
745     //                     break;
746     //                 case 'sw':
747     //                     var a = this._plotDimensions.width - grid._left + this.xoffset;
748     //                     var b = offsets.bottom + this.yoffset;
749     //                     this._elem.css({right:a, bottom:b});
750     //                     break;
751     //                 case 'w':
752     //                     var a = this._plotDimensions.width - grid._left + this.xoffset;
753     //                     var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
754     //                     this._elem.css({right:a, top:b});
755     //                     break;
756     //                 default:  // same as 'se'
757     //                     var a = grid._right - this.xoffset;
758     //                     var b = grid._bottom + this.yoffset;
759     //                     this._elem.css({right:a, bottom:b});
760     //                     break;
761     //             }
762     //         }
763     //     } 
764     // };
765     
766     // setup default renderers for axes and legend so user doesn't have to
767     // called with scope of plot
768     function preInit(target, data, options) {
769         options = options || {};
770         options.axesDefaults = options.axesDefaults || {};
771         options.legend = options.legend || {};
772         options.seriesDefaults = options.seriesDefaults || {};
773         // only set these if there is a funnel series
774         var setopts = false;
775         if (options.seriesDefaults.renderer == $.jqplot.FunnelRenderer) {
776             setopts = true;
777         }
778         else if (options.series) {
779             for (var i=0; i < options.series.length; i++) {
780                 if (options.series[i].renderer == $.jqplot.FunnelRenderer) {
781                     setopts = true;
782                 }
783             }
784         }
785         
786         if (setopts) {
787             options.axesDefaults.renderer = $.jqplot.FunnelAxisRenderer;
788             options.legend.renderer = $.jqplot.FunnelLegendRenderer;
789             options.legend.preDraw = true;
790             options.sortData = false;
791             options.seriesDefaults.pointLabels = {show: false};
792         }
793     }
794     
795     function postInit(target, data, options) {
796         // if multiple series, add a reference to the previous one so that
797         // funnel rings can nest.
798         for (var i=0; i<this.series.length; i++) {
799             if (this.series[i].renderer.constructor == $.jqplot.FunnelRenderer) {
800                 // don't allow mouseover and mousedown at same time.
801                 if (this.series[i].highlightMouseOver) {
802                     this.series[i].highlightMouseDown = false;
803                 }
804             }
805         }
806     }
807     
808     // called with scope of plot
809     function postParseOptions(options) {
810         for (var i=0; i<this.series.length; i++) {
811             this.series[i].seriesColors = this.seriesColors;
812             this.series[i].colorGenerator = $.jqplot.colorGenerator;
813         }
814     }
815     
816     function highlight (plot, sidx, pidx) {
817         var s = plot.series[sidx];
818         var canvas = plot.plugins.funnelRenderer.highlightCanvas;
819         canvas._ctx.clearRect(0,0,canvas._ctx.canvas.width, canvas._ctx.canvas.height);
820         s._highlightedPoint = pidx;
821         plot.plugins.funnelRenderer.highlightedSeriesIndex = sidx;
822         s.renderer.drawSection.call(s, canvas._ctx, s._vertices[pidx], s.highlightColors[pidx], false);
823     }
824     
825     function unhighlight (plot) {
826         var canvas = plot.plugins.funnelRenderer.highlightCanvas;
827         canvas._ctx.clearRect(0,0, canvas._ctx.canvas.width, canvas._ctx.canvas.height);
828         for (var i=0; i<plot.series.length; i++) {
829             plot.series[i]._highlightedPoint = null;
830         }
831         plot.plugins.funnelRenderer.highlightedSeriesIndex = null;
832         plot.target.trigger('jqplotDataUnhighlight');
833     }
834     
835     function handleMove(ev, gridpos, datapos, neighbor, plot) {
836         if (neighbor) {
837             var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
838             var evt1 = jQuery.Event('jqplotDataMouseOver');
839             evt1.pageX = ev.pageX;
840             evt1.pageY = ev.pageY;
841             plot.target.trigger(evt1, ins);
842             if (plot.series[ins[0]].highlightMouseOver && !(ins[0] == plot.plugins.funnelRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
843                 var evt = jQuery.Event('jqplotDataHighlight');
844                 evt.which = ev.which;
845                 evt.pageX = ev.pageX;
846                 evt.pageY = ev.pageY;
847                 plot.target.trigger(evt, ins);
848                 highlight (plot, ins[0], ins[1]);
849             }
850         }
851         else if (neighbor == null) {
852             unhighlight (plot);
853         }
854     }
855     
856     function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
857         if (neighbor) {
858             var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
859             if (plot.series[ins[0]].highlightMouseDown && !(ins[0] == plot.plugins.funnelRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
860                 var evt = jQuery.Event('jqplotDataHighlight');
861                 evt.which = ev.which;
862                 evt.pageX = ev.pageX;
863                 evt.pageY = ev.pageY;
864                 plot.target.trigger(evt, ins);
865                 highlight (plot, ins[0], ins[1]);
866             }
867         }
868         else if (neighbor == null) {
869             unhighlight (plot);
870         }
871     }
872     
873     function handleMouseUp(ev, gridpos, datapos, neighbor, plot) {
874         var idx = plot.plugins.funnelRenderer.highlightedSeriesIndex;
875         if (idx != null && plot.series[idx].highlightMouseDown) {
876             unhighlight(plot);
877         }
878     }
879     
880     function handleClick(ev, gridpos, datapos, neighbor, plot) {
881         if (neighbor) {
882             var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
883             var evt = jQuery.Event('jqplotDataClick');
884             evt.which = ev.which;
885             evt.pageX = ev.pageX;
886             evt.pageY = ev.pageY;
887             plot.target.trigger(evt, ins);
888         }
889     }
890     
891     function handleRightClick(ev, gridpos, datapos, neighbor, plot) {
892         if (neighbor) {
893             var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
894             var idx = plot.plugins.funnelRenderer.highlightedSeriesIndex;
895             if (idx != null && plot.series[idx].highlightMouseDown) {
896                 unhighlight(plot);
897             }
898             var evt = jQuery.Event('jqplotDataRightClick');
899             evt.which = ev.which;
900             evt.pageX = ev.pageX;
901             evt.pageY = ev.pageY;
902             plot.target.trigger(evt, ins);
903         }
904     }
905     
906     // called within context of plot
907     // create a canvas which we can draw on.
908     // insert it before the eventCanvas, so eventCanvas will still capture events.
909     function postPlotDraw() {
910         // Memory Leaks patch    
911         if (this.plugins.funnelRenderer && this.plugins.funnelRenderer.highlightCanvas) {
912             this.plugins.funnelRenderer.highlightCanvas.resetCanvas();
913             this.plugins.funnelRenderer.highlightCanvas = null;
914         }
916         this.plugins.funnelRenderer = {};
917         this.plugins.funnelRenderer.highlightCanvas = new $.jqplot.GenericCanvas();
918         
919         // do we have any data labels?  if so, put highlight canvas before those
920         var labels = $(this.targetId+' .jqplot-data-label');
921         if (labels.length) {
922             $(labels[0]).before(this.plugins.funnelRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-funnelRenderer-highlight-canvas', this._plotDimensions, this));
923         }
924         // else put highlight canvas before event canvas.
925         else {
926             this.eventCanvas._elem.before(this.plugins.funnelRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-funnelRenderer-highlight-canvas', this._plotDimensions, this));
927         }
928         var hctx = this.plugins.funnelRenderer.highlightCanvas.setContext();
929         this.eventCanvas._elem.bind('mouseleave', {plot:this}, function (ev) { unhighlight(ev.data.plot); });
930     }
931     
932     $.jqplot.preInitHooks.push(preInit);
933     
934     $.jqplot.FunnelTickRenderer = function() {
935         $.jqplot.AxisTickRenderer.call(this);
936     };
937     
938     $.jqplot.FunnelTickRenderer.prototype = new $.jqplot.AxisTickRenderer();
939     $.jqplot.FunnelTickRenderer.prototype.constructor = $.jqplot.FunnelTickRenderer;
940     
941 })(jQuery);
942     
943