3 * Pure JavaScript plotting plugin using jQuery
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.
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 .
18 * If you are feeling kind and generous, consider supporting the project by
19 * making a donation at: http://www.jqplot.com/donate.php .
21 * sprintf functions contained in jqplot.sprintf.js by 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."
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.
38 * Funnel charts will draw a single series
41 * To use this renderer, you need to include the
42 * funnel renderer plugin, for example:
44 * > <script type="text/javascript" src="plugins/jqplot.funnelRenderer.js"></script>
46 * Properties described here are passed into the $.jqplot function
47 * as options on the series renderer. For example:
49 * > plot2 = $.jqplot('chart2', [s1, s2], {
51 * > renderer:$.jqplot.FunnelRenderer,
53 * > sectionMargin: 12,
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.
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.
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.
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.
87 $.jqplot.FunnelRenderer = function(){
88 $.jqplot.LineRenderer.call(this);
91 $.jqplot.FunnelRenderer.prototype = new $.jqplot.LineRenderer();
92 $.jqplot.FunnelRenderer.prototype.constructor = $.jqplot.FunnelRenderer;
94 // called with scope of a series
95 $.jqplot.FunnelRenderer.prototype.init = function(options, plot) {
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;
105 // true or false, wether to fill the areas.
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;
112 // transparency of the shadow (0 = transparent, 1 = opaque)
113 this.shadowAlpha = 0.07;
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 = [];
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;
134 // width of line if areas are stroked and not filled.
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';
152 this.tickRenderer = $.jqplot.FunnelTickRenderer;
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;
159 $.extend(true, this, options);
161 // index of the currenty highlighted point, if any
162 this._highlightedPoint = null;
164 // lengths of bases, or horizontal sides of areas of trapezoid.
168 // areas of segments.
170 // vertical lengths of segments.
172 // angle of the funnel to vertical.
174 this._dataIndices = [];
177 this._unorderedData = $.extend(true, [], this.data);
178 var idxs = $.extend(true, [], this.data);
179 for (var i=0; i<idxs.length; i++) {
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]);
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);
199 this.highlightColors.push('rgb('+newrgb[0]+','+newrgb[1]+','+newrgb[2]+')');
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);
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.
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]]);
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;
229 this._bases = new Array(td.length + 1);
230 this._lengths = new Array(td.length);
235 $.jqplot.FunnelRenderer.prototype.makeGridData = function(data, plot) {
236 // set gridData property. This will hold angle in radians of each data point.
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]]);
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;
249 this._bases = new Array(td.length + 1);
250 this._lengths = new Array(td.length);
255 $.jqplot.FunnelRenderer.prototype.drawSection = function (ctx, vertices, color, isShadow) {
256 var fill = this.fill;
257 var lineWidth = this.lineWidth;
261 for (var i=0; i<this.shadowDepth; i++) {
263 ctx.translate(this.shadowOffset*Math.cos(this.shadowAngle/180*Math.PI), this.shadowOffset*Math.sin(this.shadowAngle/180*Math.PI));
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]);
291 for (var i=0; i<this.shadowDepth; i++) {
299 // called with scope of series
300 $.jqplot.FunnelRenderer.prototype.draw = function (ctx, gd, options, plot) {
302 var opts = (options != undefined) ? options : {};
303 // offset and direction of offset due to legend placement
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) {
313 offx = li.width + li.xoffset;
316 offx = li.width + li.xoffset;
319 offx = li.width + li.xoffset;
322 offx = li.width + li.xoffset;
326 offx = li.width + li.xoffset;
330 offx = li.width + li.xoffset;
334 offy = li.height + li.yoffset;
337 offy = li.height + li.yoffset;
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;
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);
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];
374 this._lengths[i] = guess;
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];
383 lsum += this._lengths[i];
386 // figure out vertices of each section
387 this._vertices = new Array(gd.length);
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]];
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];
401 return [(y - b)/m, y];
404 function findright (l) {
405 var m = (p1[1] - p3[1])/(p1[0] - p3[0]);
406 var b = p1[1] - m*p1[0];
409 return [(y - b)/m, y];
412 var x = offx, y = offy;
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;
425 else if (i > 0 && i < gd.length-1) {
428 else if (i == gd.length -1) {
431 v.push(findleft(h+adj));
432 v.push(findright(h+adj));
433 h += this._lengths[i];
437 else if (i > 0 && i < gd.length-1) {
440 else if (i == gd.length - 1) {
443 v.push(findright(h+adj));
444 v.push(findleft(h+adj));
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);
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]);
459 if (this.showDataLabels && gd[i][1]*100 >= this.dataLabelThreshold) {
462 if (this.dataLabels == 'label') {
463 fstr = this.dataLabelFormatString || '%s';
464 label = $.jqplot.sprintf(fstr, gd[i][0]);
466 else if (this.dataLabels == 'value') {
467 fstr = this.dataLabelFormatString || '%d';
468 label = $.jqplot.sprintf(fstr, this.data[i][1]);
470 else if (this.dataLabels == 'percent') {
471 fstr = this.dataLabelFormatString || '%d%%';
472 label = $.jqplot.sprintf(fstr, gd[i][1]*100);
474 else if (this.dataLabels.constructor == Array) {
475 fstr = this.dataLabelFormatString || '%s';
476 label = $.jqplot.sprintf(fstr, this.dataLabels[this._dataIndices[i]]);
479 var fact = (this._radius ) * this.dataLabelPositionFactor + this.sliceMargin + this.dataLabelNudge;
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;
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;
489 labelelem.css({left: x, top: y});
496 $.jqplot.FunnelAxisRenderer = function() {
497 $.jqplot.LinearAxisRenderer.call(this);
500 $.jqplot.FunnelAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
501 $.jqplot.FunnelAxisRenderer.prototype.constructor = $.jqplot.FunnelAxisRenderer;
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){
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};
519 this.showTicks = false;
521 this.showMark = false;
528 * Class: $.jqplot.FunnelLegendRenderer
529 * Legend Renderer specific to funnel plots. Set by default
530 * when the user creates a funnel plot.
532 $.jqplot.FunnelLegendRenderer = function(){
533 $.jqplot.TableLegendRenderer.call(this);
536 $.jqplot.FunnelLegendRenderer.prototype = new $.jqplot.TableLegendRenderer();
537 $.jqplot.FunnelLegendRenderer.prototype.constructor = $.jqplot.FunnelLegendRenderer;
539 $.jqplot.FunnelLegendRenderer.prototype.init = function(options) {
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);
551 // called with context of legend
552 $.jqplot.FunnelLegendRenderer.prototype.draw = function() {
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.
574 var colorGenerator = new $.jqplot.ColorGenerator(s.seriesColors);
578 if (this.numberRows) {
579 nr = this.numberRows;
580 if (!this.numberColumns){
581 nc = Math.ceil(pd.length/nr);
584 nc = this.numberColumns;
587 else if (this.numberColumns) {
588 nc = this.numberColumns;
589 nr = Math.ceil(pd.length/this.numberColumns);
596 var i, j, tr, td1, td2, lt, rs, color;
599 for (i=0; i<nr; i++) {
601 tr = $('<tr class="jqplot-table-legend"></tr>').prependTo(this._elem);
604 tr = $('<tr class="jqplot-table-legend"></tr>').appendTo(this._elem);
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();
626 rs = (pad) ? this.rowSpacing : '0';
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>'+
631 td2 = $('<td class="jqplot-table-legend" style="padding-top:'+rs+';"></td>');
632 if (this.escapeHtml){
656 // $.jqplot.FunnelLegendRenderer.prototype.pack = function(offsets) {
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) {
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);
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);
675 // var a = offsets.right + this.xoffset;
676 // var b = grid._top + this.yoffset;
677 // this._elem.css({right:a, top:b});
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});
685 // var a = offsets.right + this.xoffset;
686 // var b = offsets.bottom + this.yoffset;
687 // this._elem.css({right:a, bottom:b});
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});
695 // var a = grid._left + this.xoffset;
696 // var b = offsets.bottom + this.yoffset;
697 // this._elem.css({left:a, bottom:b});
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});
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});
713 // switch (this.location) {
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);
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);
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});
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});
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});
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});
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});
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});
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});
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
775 if (options.seriesDefaults.renderer == $.jqplot.FunnelRenderer) {
778 else if (options.series) {
779 for (var i=0; i < options.series.length; i++) {
780 if (options.series[i].renderer == $.jqplot.FunnelRenderer) {
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};
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;
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;
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);
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;
831 plot.plugins.funnelRenderer.highlightedSeriesIndex = null;
832 plot.target.trigger('jqplotDataUnhighlight');
835 function handleMove(ev, gridpos, datapos, neighbor, plot) {
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]);
851 else if (neighbor == null) {
856 function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
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]);
868 else if (neighbor == null) {
873 function handleMouseUp(ev, gridpos, datapos, neighbor, plot) {
874 var idx = plot.plugins.funnelRenderer.highlightedSeriesIndex;
875 if (idx != null && plot.series[idx].highlightMouseDown) {
880 function handleClick(ev, gridpos, datapos, neighbor, plot) {
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);
891 function handleRightClick(ev, gridpos, datapos, neighbor, plot) {
893 var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
894 var idx = plot.plugins.funnelRenderer.highlightedSeriesIndex;
895 if (idx != null && plot.series[idx].highlightMouseDown) {
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);
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;
916 this.plugins.funnelRenderer = {};
917 this.plugins.funnelRenderer.highlightCanvas = new $.jqplot.GenericCanvas();
919 // do we have any data labels? if so, put highlight canvas before those
920 var labels = $(this.targetId+' .jqplot-data-label');
922 $(labels[0]).before(this.plugins.funnelRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-funnelRenderer-highlight-canvas', this._plotDimensions, this));
924 // else put highlight canvas before event canvas.
926 this.eventCanvas._elem.before(this.plugins.funnelRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-funnelRenderer-highlight-canvas', this._plotDimensions, this));
928 var hctx = this.plugins.funnelRenderer.highlightCanvas.setContext();
929 this.eventCanvas._elem.bind('mouseleave', {plot:this}, function (ev) { unhighlight(ev.data.plot); });
932 $.jqplot.preInitHooks.push(preInit);
934 $.jqplot.FunnelTickRenderer = function() {
935 $.jqplot.AxisTickRenderer.call(this);
938 $.jqplot.FunnelTickRenderer.prototype = new $.jqplot.AxisTickRenderer();
939 $.jqplot.FunnelTickRenderer.prototype.constructor = $.jqplot.FunnelTickRenderer;