Add a SCORE_NONE, and fix a typo.
[remoteglot] / www / js / jquery.sparkline.js
1 /**
2 *
3 * jquery.sparkline.js
4 *
5 * v2.1.2
6 * (c) Splunk, Inc
7 * Contact: Gareth Watts (gareth@splunk.com)
8 * http://omnipotent.net/jquery.sparkline/
9 *
10 * Generates inline sparkline charts from data supplied either to the method
11 * or inline in HTML
12 *
13 * Compatible with Internet Explorer 6.0+ and modern browsers equipped with the canvas tag
14 * (Firefox 2.0+, Safari, Opera, etc)
15 *
16 * License: New BSD License
17 *
18 * Copyright (c) 2012, Splunk Inc.
19 * All rights reserved.
20 *
21 * Redistribution and use in source and binary forms, with or without modification,
22 * are permitted provided that the following conditions are met:
23 *
24 *     * Redistributions of source code must retain the above copyright notice,
25 *       this list of conditions and the following disclaimer.
26 *     * Redistributions in binary form must reproduce the above copyright notice,
27 *       this list of conditions and the following disclaimer in the documentation
28 *       and/or other materials provided with the distribution.
29 *     * Neither the name of Splunk Inc nor the names of its contributors may
30 *       be used to endorse or promote products derived from this software without
31 *       specific prior written permission.
32 *
33 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
34 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
35 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
36 * SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
37 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
38 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
39 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
40 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
41 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42 *
43 *
44 * Usage:
45 *  $(selector).sparkline(values, options)
46 *
47 * If values is undefined or set to 'html' then the data values are read from the specified tag:
48 *   <p>Sparkline: <span class="sparkline">1,4,6,6,8,5,3,5</span></p>
49 *   $('.sparkline').sparkline();
50 * There must be no spaces in the enclosed data set
51 *
52 * Otherwise values must be an array of numbers or null values
53 *    <p>Sparkline: <span id="sparkline1">This text replaced if the browser is compatible</span></p>
54 *    $('#sparkline1').sparkline([1,4,6,6,8,5,3,5])
55 *    $('#sparkline2').sparkline([1,4,6,null,null,5,3,5])
56 *
57 * Values can also be specified in an HTML comment, or as a values attribute:
58 *    <p>Sparkline: <span class="sparkline"><!--1,4,6,6,8,5,3,5 --></span></p>
59 *    <p>Sparkline: <span class="sparkline" values="1,4,6,6,8,5,3,5"></span></p>
60 *    $('.sparkline').sparkline();
61 *
62 * By default, options should be passed in as teh second argument to the sparkline function:
63 *   $('.sparkline').sparkline([1,2,3,4], {type: 'bar'})
64 *
65 * Supported options:
66 *   lineColor - Color of the line used for the chart
67 *   fillColor - Color used to fill in the chart - Set to '' or false for a transparent chart
68 *   width - Width of the chart - Defaults to 3 times the number of values in pixels
69 *   height - Height of the chart - Defaults to the height of the containing element
70 *   chartRangeMin - Specify the minimum value to use for the Y range of the chart - Defaults to the minimum value supplied
71 *   chartRangeMax - Specify the maximum value to use for the Y range of the chart - Defaults to the maximum value supplied
72 *   chartRangeClip - Clip out of range values to the max/min specified by chartRangeMin and chartRangeMax
73 *   chartRangeMinX - Specify the minimum value to use for the X range of the chart - Defaults to the minimum value supplied
74 *   chartRangeMaxX - Specify the maximum value to use for the X range of the chart - Defaults to the maximum value supplied
75 *   tagValuesAttribute - Name of tag attribute to check for data values - Defaults to 'values'
76 *   disableInteraction - If set to true then all mouseover/click interaction behaviour will be disabled,
77 *       making the plugin perform much like it did in 1.x
78 *   disableTooltips - If set to true then tooltips will be disabled - Defaults to false (tooltips enabled)
79 *   disableHighlight - If set to true then highlighting of selected chart elements on mouseover will be disabled
80 *       defaults to false (highlights enabled)
81 *   highlightLighten - Factor to lighten/darken highlighted chart values by - Defaults to 1.4 for a 40% increase
82 *   tooltipContainer - Specify which DOM element the tooltip should be rendered into - defaults to document.body
83 *   tooltipClassname - Optional CSS classname to apply to tooltips - If not specified then a default style will be applied
84 *   tooltipOffsetX - How many pixels away from the mouse pointer to render the tooltip on the X axis
85 *   tooltipOffsetY - How many pixels away from the mouse pointer to render the tooltip on the r axis
86 *   tooltipFormatter  - Optional callback that allows you to override the HTML displayed in the tooltip
87 *       callback is given arguments of (sparkline, options, fields)
88 *   tooltipChartTitle - If specified then the tooltip uses the string specified by this setting as a title
89 *   tooltipPrefix - A string to prepend to each field displayed in a tooltip
90 *   tooltipSuffix - A string to append to each field displayed in a tooltip
91 *   tooltipSkipNull - If true then null values will not have a tooltip displayed (defaults to true)
92 *   tooltipValueLookups - An object or range map to map field values to tooltip strings
93 *       (eg. to map -1 to "Lost", 0 to "Draw", and 1 to "Win")
94 *   numberFormatter - Optional callback for formatting numbers in tooltips
95 *   numberDigitGroupSep - Character to use for group separator in numbers "1,234" - Defaults to ","
96 *   numberDecimalMark - Character to use for the decimal point when formatting numbers - Defaults to "."
97 *   numberDigitGroupCount - Number of digits between group separator - Defaults to 3
98 *
99 * There is 1 type of sparkline, selected by supplying a "type" option of 'bar' (default),
100 *
101 *   bar - Bar chart.  Options:
102 *       barColor - Color of bars for postive values
103 *       negBarColor - Color of bars for negative values
104 *       zeroColor - Color of bars with zero values
105 *       nullColor - Color of bars with null values - Defaults to omitting the bar entirely
106 *       barWidth - Width of bars in pixels
107 *       barSpacing - Gap between bars in pixels
108 *       zeroAxis - Centers the y-axis around zero if true
109 *
110 *
111 *
112 *
113 *
114 *
115 *   Examples:
116 *   $('#sparkline1').sparkline(myvalues, { lineColor: '#f00', fillColor: false });
117 *   $('.barsparks').sparkline('html', { type:'bar', height:'40px', barWidth:5 });
118 */
119
120 /*jslint regexp: true, browser: true, jquery: true, white: true, nomen: false, plusplus: false, maxerr: 500, indent: 4 */
121
122 (function(document, Math, undefined) { // performance/minified-size optimization
123 (function(factory) {
124     if(typeof define === 'function' && define.amd) {
125         define(['jquery'], factory);
126     } else if (jQuery && !jQuery.fn.sparkline) {
127         factory(jQuery);
128     }
129 }
130 (function($) {
131     'use strict';
132
133     var UNSET_OPTION = {},
134         getDefaults, createClass, clipval, quartile, normalizeValue, normalizeValues,
135         remove, isNumber, all, sum, addCSS, ensureArray, formatNumber,
136         MouseHandler, Tooltip, barHighlightMixin,
137         bar, defaultStyles, initStyles,
138         VShape, VCanvas_base, VCanvas_canvas, pending, shapeCount = 0;
139
140     /**
141      * Default configuration settings
142      */
143     getDefaults = function () {
144         return {
145             // Settings common to most/all chart types
146             common: {
147                 type: 'bar',
148                 lineColor: '#00f',
149                 fillColor: '#cdf',
150                 defaultPixelsPerValue: 3,
151                 width: 'auto',
152                 height: 'auto',
153                 tagValuesAttribute: 'values',
154                 enableHighlight: true,
155                 highlightLighten: 1.4,
156                 tooltipSkipNull: true,
157                 tooltipPrefix: '',
158                 tooltipSuffix: '',
159                 numberFormatter: false,
160                 numberDigitGroupCount: 3,
161                 numberDigitGroupSep: ',',
162                 numberDecimalMark: '.',
163                 disableTooltips: false,
164                 disableInteraction: false
165             },
166             // Defaults for bar charts
167             bar: {
168                 barColor: '#3366cc',
169                 negBarColor: '#f44',
170                 zeroColor: undefined,
171                 nullColor: undefined,
172                 zeroAxis: true,
173                 barWidth: 4,
174                 barSpacing: 1,
175                 chartRangeMax: undefined,
176                 chartRangeMin: undefined,
177                 chartRangeClip: false,
178                 tooltipFormat: '',
179             },
180         };
181     };
182
183     // You can have tooltips use a css class other than jqstooltip by specifying tooltipClassname
184     defaultStyles = '.jqstooltip { ' +
185             'position: absolute;' +
186             'left: 0px;' +
187             'top: 0px;' +
188             'visibility: hidden;' +
189             'background: rgb(0, 0, 0) transparent;' +
190             'background-color: rgba(0,0,0,0.6);' +
191             'filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000);' +
192             '-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000)";' +
193             'color: white;' +
194             'font: 10px arial, san serif;' +
195             'text-align: left;' +
196             'white-space: nowrap;' +
197             'padding: 5px;' +
198             'border: 1px solid white;' +
199             'z-index: 10000;' +
200             '}' +
201             '.jqsfield { ' +
202             'color: white;' +
203             'font: 10px arial, san serif;' +
204             'text-align: left;' +
205             '}';
206
207     /**
208      * Utilities
209      */
210
211     createClass = function (/* [baseclass, [mixin, ...]], definition */) {
212         var Class, args;
213         Class = function () {
214             this.init.apply(this, arguments);
215         };
216         if (arguments.length > 1) {
217             if (arguments[0]) {
218                 Class.prototype = $.extend(new arguments[0](), arguments[arguments.length - 1]);
219                 Class._super = arguments[0].prototype;
220             } else {
221                 Class.prototype = arguments[arguments.length - 1];
222             }
223             if (arguments.length > 2) {
224                 args = Array.prototype.slice.call(arguments, 1, -1);
225                 args.unshift(Class.prototype);
226                 $.extend.apply($, args);
227             }
228         } else {
229             Class.prototype = arguments[0];
230         }
231         Class.prototype.cls = Class;
232         return Class;
233     };
234
235     clipval = function (val, min, max) {
236         if (val < min) {
237             return min;
238         }
239         if (val > max) {
240             return max;
241         }
242         return val;
243     };
244
245     quartile = function (values, q) {
246         var vl;
247         if (q === 2) {
248             vl = Math.floor(values.length / 2);
249             return values.length % 2 ? values[vl] : (values[vl-1] + values[vl]) / 2;
250         } else {
251             if (values.length % 2 ) { // odd
252                 vl = (values.length * q + q) / 4;
253                 return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl-1];
254             } else { //even
255                 vl = (values.length * q + 2) / 4;
256                 return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 :  values[vl-1];
257
258             }
259         }
260     };
261
262     normalizeValue = function (val) {
263         var nf;
264         switch (val) {
265             case 'undefined':
266                 val = undefined;
267                 break;
268             case 'null':
269                 val = null;
270                 break;
271             case 'true':
272                 val = true;
273                 break;
274             case 'false':
275                 val = false;
276                 break;
277             default:
278                 nf = parseFloat(val);
279                 if (val == nf) {
280                     val = nf;
281                 }
282         }
283         return val;
284     };
285
286     normalizeValues = function (vals) {
287         var i, result = [];
288         for (i = vals.length; i--;) {
289             result[i] = normalizeValue(vals[i]);
290         }
291         return result;
292     };
293
294     remove = function (vals, filter) {
295         var i, vl, result = [];
296         for (i = 0, vl = vals.length; i < vl; i++) {
297             if (vals[i] !== filter) {
298                 result.push(vals[i]);
299             }
300         }
301         return result;
302     };
303
304     isNumber = function (num) {
305         return !isNaN(parseFloat(num)) && isFinite(num);
306     };
307
308     formatNumber = function (num, prec, groupsize, groupsep, decsep) {
309         var p, i;
310         num = (prec === false ? parseFloat(num).toString() : num.toFixed(prec)).split('');
311         p = (p = $.inArray('.', num)) < 0 ? num.length : p;
312         if (p < num.length) {
313             num[p] = decsep;
314         }
315         for (i = p - groupsize; i > 0; i -= groupsize) {
316             num.splice(i, 0, groupsep);
317         }
318         return num.join('');
319     };
320
321     // determine if all values of an array match a value
322     // returns true if the array is empty
323     all = function (val, arr, ignoreNull) {
324         var i;
325         for (i = arr.length; i--; ) {
326             if (ignoreNull && arr[i] === null) continue;
327             if (arr[i] !== val) {
328                 return false;
329             }
330         }
331         return true;
332     };
333
334     // sums the numeric values in an array, ignoring other values
335     sum = function (vals) {
336         var total = 0, i;
337         for (i = vals.length; i--;) {
338             total += typeof vals[i] === 'number' ? vals[i] : 0;
339         }
340         return total;
341     };
342
343     ensureArray = function (val) {
344         return $.isArray(val) ? val : [val];
345     };
346
347     // http://paulirish.com/2008/bookmarklet-inject-new-css-rules/
348     addCSS = function(css) {
349         var tag;
350         //if ('\v' == 'v') /* ie only */ {
351         if (document.createStyleSheet) {
352             document.createStyleSheet().cssText = css;
353         } else {
354             tag = document.createElement('style');
355             tag.type = 'text/css';
356             document.getElementsByTagName('head')[0].appendChild(tag);
357             tag[(typeof document.body.style.WebkitAppearance == 'string') /* webkit only */ ? 'innerText' : 'innerHTML'] = css;
358         }
359     };
360
361     // Provide a cross-browser interface to a few simple drawing primitives
362     $.fn.simpledraw = function (width, height, useExisting, interact) {
363         var target, mhandler;
364         if (useExisting && (target = this.data('_jqs_vcanvas'))) {
365             return target;
366         }
367
368         if ($.fn.sparkline.canvas === false) {
369             // We've already determined that neither Canvas nor VML are available
370             return false;
371
372         } else if ($.fn.sparkline.canvas === undefined) {
373             // No function defined yet -- need to see if we support Canvas or VML
374             var el = document.createElement('canvas');
375             if (!!(el.getContext && el.getContext('2d'))) {
376                 // Canvas is available
377                 $.fn.sparkline.canvas = function(width, height, target, interact) {
378                     return new VCanvas_canvas(width, height, target, interact);
379                 };
380             } else {
381                 // Neither Canvas nor VML are available
382                 $.fn.sparkline.canvas = false;
383                 return false;
384             }
385         }
386
387         if (width === undefined) {
388             width = $(this).innerWidth();
389         }
390         if (height === undefined) {
391             height = $(this).innerHeight();
392         }
393
394         target = $.fn.sparkline.canvas(width, height, this, interact);
395
396         mhandler = $(this).data('_jqs_mhandler');
397         if (mhandler) {
398             mhandler.registerCanvas(target);
399         }
400         return target;
401     };
402
403     $.fn.cleardraw = function () {
404         var target = this.data('_jqs_vcanvas');
405         if (target) {
406             target.reset();
407         }
408     };
409
410     MouseHandler = createClass({
411         init: function (el, options) {
412             var $el = $(el);
413             this.$el = $el;
414             this.options = options;
415             this.currentPageX = 0;
416             this.currentPageY = 0;
417             this.el = el;
418             this.splist = [];
419             this.tooltip = null;
420             this.over = false;
421             this.displayTooltips = !options.get('disableTooltips');
422             this.highlightEnabled = !options.get('disableHighlight');
423         },
424
425         registerSparkline: function (sp) {
426             this.splist.push(sp);
427             if (this.over) {
428                 this.updateDisplay();
429             }
430         },
431
432         registerCanvas: function (canvas) {
433             var $canvas = $(canvas.canvas);
434             this.canvas = canvas;
435             this.$canvas = $canvas;
436             $canvas.mouseenter($.proxy(this.mouseenter, this));
437             $canvas.mouseleave($.proxy(this.mouseleave, this));
438             $canvas.click($.proxy(this.mouseclick, this));
439         },
440
441         reset: function (removeTooltip) {
442             this.splist = [];
443             if (this.tooltip && removeTooltip) {
444                 this.tooltip.remove();
445                 this.tooltip = undefined;
446             }
447         },
448
449         mouseclick: function (e) {
450             var clickEvent = $.Event('sparklineClick');
451             clickEvent.originalEvent = e;
452             clickEvent.sparklines = this.splist;
453             this.$el.trigger(clickEvent);
454         },
455
456         mouseenter: function (e) {
457             $(document.body).unbind('mousemove.jqs');
458             $(document.body).bind('mousemove.jqs', $.proxy(this.mousemove, this));
459             this.over = true;
460             this.currentPageX = e.pageX;
461             this.currentPageY = e.pageY;
462             this.currentEl = e.target;
463             if (!this.tooltip && this.displayTooltips) {
464                 this.tooltip = new Tooltip(this.options);
465                 this.tooltip.updatePosition(e.pageX, e.pageY);
466             }
467             this.updateDisplay();
468         },
469
470         mouseleave: function () {
471             $(document.body).unbind('mousemove.jqs');
472             var splist = this.splist,
473                  spcount = splist.length,
474                  needsRefresh = false,
475                  sp, i;
476             this.over = false;
477             this.currentEl = null;
478
479             if (this.tooltip) {
480                 this.tooltip.remove();
481                 this.tooltip = null;
482             }
483
484             for (i = 0; i < spcount; i++) {
485                 sp = splist[i];
486                 if (sp.clearRegionHighlight()) {
487                     needsRefresh = true;
488                 }
489             }
490
491             if (needsRefresh) {
492                 this.canvas.render();
493             }
494         },
495
496         mousemove: function (e) {
497             this.currentPageX = e.pageX;
498             this.currentPageY = e.pageY;
499             this.currentEl = e.target;
500             if (this.tooltip) {
501                 this.tooltip.updatePosition(e.pageX, e.pageY);
502             }
503             this.updateDisplay();
504         },
505
506         updateDisplay: function () {
507             var splist = this.splist,
508                  spcount = splist.length,
509                  needsRefresh = false,
510                  offset = this.$canvas.offset(),
511                  localX = this.currentPageX - offset.left,
512                  localY = this.currentPageY - offset.top,
513                  tooltiphtml, sp, i, result, changeEvent;
514             if (!this.over) {
515                 return;
516             }
517             for (i = 0; i < spcount; i++) {
518                 sp = splist[i];
519                 result = sp.setRegionHighlight(this.currentEl, localX, localY);
520                 if (result) {
521                     needsRefresh = true;
522                 }
523             }
524             if (needsRefresh) {
525                 changeEvent = $.Event('sparklineRegionChange');
526                 changeEvent.sparklines = this.splist;
527                 this.$el.trigger(changeEvent);
528                 if (this.tooltip) {
529                     tooltiphtml = '';
530                     for (i = 0; i < spcount; i++) {
531                         sp = splist[i];
532                         tooltiphtml += sp.getCurrentRegionTooltip();
533                     }
534                     this.tooltip.setContent(tooltiphtml);
535                 }
536                 if (!this.disableHighlight) {
537                     this.canvas.render();
538                 }
539             }
540             if (result === null) {
541                 this.mouseleave();
542             }
543         }
544     });
545
546
547     Tooltip = createClass({
548         sizeStyle: 'position: static !important;' +
549             'display: block !important;' +
550             'visibility: hidden !important;' +
551             'float: left !important;',
552
553         init: function (options) {
554             var tooltipClassname = options.get('tooltipClassname', 'jqstooltip'),
555                 sizetipStyle = this.sizeStyle,
556                 offset;
557             this.container = options.get('tooltipContainer') || document.body;
558             this.tooltipOffsetX = options.get('tooltipOffsetX', 10);
559             this.tooltipOffsetY = options.get('tooltipOffsetY', 12);
560             // remove any previous lingering tooltip
561             $('#jqssizetip').remove();
562             $('#jqstooltip').remove();
563             this.sizetip = $('<div/>', {
564                 id: 'jqssizetip',
565                 style: sizetipStyle,
566                 'class': tooltipClassname
567             });
568             this.tooltip = $('<div/>', {
569                 id: 'jqstooltip',
570                 'class': tooltipClassname
571             }).appendTo(this.container);
572             // account for the container's location
573             offset = this.tooltip.offset();
574             this.offsetLeft = offset.left;
575             this.offsetTop = offset.top;
576             this.hidden = true;
577             $(window).unbind('resize.jqs scroll.jqs');
578             $(window).bind('resize.jqs scroll.jqs', $.proxy(this.updateWindowDims, this));
579             this.updateWindowDims();
580         },
581
582         updateWindowDims: function () {
583             this.scrollTop = $(window).scrollTop();
584             this.scrollLeft = $(window).scrollLeft();
585             this.scrollRight = this.scrollLeft + $(window).width();
586             this.updatePosition();
587         },
588
589         getSize: function (content) {
590             this.sizetip.html(content).appendTo(this.container);
591             this.width = this.sizetip.width() + 1;
592             this.height = this.sizetip.height();
593             this.sizetip.remove();
594         },
595
596         setContent: function (content) {
597             if (!content) {
598                 this.tooltip.css('visibility', 'hidden');
599                 this.hidden = true;
600                 return;
601             }
602             this.getSize(content);
603             this.tooltip.html(content)
604                 .css({
605                     'width': this.width,
606                     'height': this.height,
607                     'visibility': 'visible'
608                 });
609             if (this.hidden) {
610                 this.hidden = false;
611                 this.updatePosition();
612             }
613         },
614
615         updatePosition: function (x, y) {
616             if (x === undefined) {
617                 if (this.mousex === undefined) {
618                     return;
619                 }
620                 x = this.mousex - this.offsetLeft;
621                 y = this.mousey - this.offsetTop;
622
623             } else {
624                 this.mousex = x = x - this.offsetLeft;
625                 this.mousey = y = y - this.offsetTop;
626             }
627             if (!this.height || !this.width || this.hidden) {
628                 return;
629             }
630
631             y -= this.height + this.tooltipOffsetY;
632             x += this.tooltipOffsetX;
633
634             if (y < this.scrollTop) {
635                 y = this.scrollTop;
636             }
637             if (x < this.scrollLeft) {
638                 x = this.scrollLeft;
639             } else if (x + this.width > this.scrollRight) {
640                 x = this.scrollRight - this.width;
641             }
642
643             this.tooltip.css({
644                 'left': x,
645                 'top': y
646             });
647         },
648
649         remove: function () {
650             this.tooltip.remove();
651             this.sizetip.remove();
652             this.sizetip = this.tooltip = undefined;
653             $(window).unbind('resize.jqs scroll.jqs');
654         }
655     });
656
657     initStyles = function() {
658         addCSS(defaultStyles);
659     };
660
661     $(initStyles);
662
663     pending = [];
664     $.fn.sparkline = function (userValues, userOptions) {
665         return this.each(function () {
666             var options = new $.fn.sparkline.options(this, userOptions),
667                  $this = $(this),
668                  render, i;
669             render = function () {
670                 var values, width, height, tmp, mhandler, sp, vals;
671                 if (userValues === 'html' || userValues === undefined) {
672                     vals = this.getAttribute(options.get('tagValuesAttribute'));
673                     if (vals === undefined || vals === null) {
674                         vals = $this.html();
675                     }
676                     values = vals.replace(/(^\s*<!--)|(-->\s*$)|\s+/g, '').split(',');
677                 } else {
678                     values = userValues;
679                 }
680
681                 width = options.get('width') === 'auto' ? values.length * options.get('defaultPixelsPerValue') : options.get('width');
682                 if (options.get('height') === 'auto') {
683                         // must be a better way to get the line height
684                         tmp = document.createElement('span');
685                         tmp.innerHTML = 'a';
686                         $this.html(tmp);
687                         height = $(tmp).innerHeight() || $(tmp).height();
688                         $(tmp).remove();
689                         tmp = null;
690                 } else {
691                     height = options.get('height');
692                 }
693
694                 if (!options.get('disableInteraction')) {
695                     mhandler = $.data(this, '_jqs_mhandler');
696                     if (!mhandler) {
697                         mhandler = new MouseHandler(this, options);
698                         $.data(this, '_jqs_mhandler', mhandler);
699                     } else {
700                         mhandler.reset();
701                     }
702                 } else {
703                     mhandler = false;
704                 }
705
706                 sp = new $.fn.sparkline[options.get('type')](this, values, options, width, height);
707
708                 sp.render();
709
710                 if (mhandler) {
711                     mhandler.registerSparkline(sp);
712                 }
713             };
714             render.call(this);
715         });
716     };
717
718     $.fn.sparkline.defaults = getDefaults();
719
720
721     $.sparkline_display_visible = function () {
722         var el, i, pl;
723         var done = [];
724         for (i = 0, pl = pending.length; i < pl; i++) {
725             el = pending[i][0];
726                 pending[i][1].call(el);
727                 $.data(pending[i][0], '_jqs_pending', false);
728                 done.push(i);
729         }
730         for (i = done.length; i; i--) {
731             pending.splice(done[i - 1], 1);
732         }
733     };
734
735
736     /**
737      * User option handler
738      */
739     $.fn.sparkline.options = createClass({
740         init: function (tag, userOptions) {
741             var extendedOptions, defaults, base;
742             this.userOptions = userOptions = userOptions || {};
743             this.tag = tag;
744             this.tagValCache = {};
745             defaults = $.fn.sparkline.defaults;
746             base = defaults.common;
747
748             extendedOptions = defaults[userOptions.type || base.type];
749             this.mergedOptions = $.extend({}, base, extendedOptions, userOptions);
750         },
751
752
753         get: function (key, defaultval) {
754             var result;
755             return (result = this.mergedOptions[key]) === undefined ? defaultval : result;
756         }
757     });
758
759
760     $.fn.sparkline._base = createClass({
761         disabled: false,
762
763         init: function (el, values, options, width, height) {
764             this.el = el;
765             this.$el = $(el);
766             this.values = values;
767             this.options = options;
768             this.width = width;
769             this.height = height;
770             this.currentRegion = undefined;
771         },
772
773         /**
774          * Setup the canvas
775          */
776         initTarget: function () {
777             var interactive = !this.options.get('disableInteraction');
778             if (!(this.target = this.$el.simpledraw(this.width, this.height, false, interactive))) {
779                 this.disabled = true;
780             } else {
781                 this.canvasWidth = this.target.pixelWidth;
782                 this.canvasHeight = this.target.pixelHeight;
783             }
784         },
785
786         /**
787          * Actually render the chart to the canvas
788          */
789         render: function () {
790             if (this.disabled) {
791                 this.el.innerHTML = '';
792                 return false;
793             }
794             return true;
795         },
796
797         /**
798          * Return a region id for a given x/y co-ordinate
799          */
800         getRegion: function (x, y) {
801         },
802
803         /**
804          * Highlight an item based on the moused-over x,y co-ordinate
805          */
806         setRegionHighlight: function (el, x, y) {
807             var currentRegion = this.currentRegion,
808                 highlightEnabled = !this.options.get('disableHighlight'),
809                 newRegion;
810             if (x > this.canvasWidth || y > this.canvasHeight || x < 0 || y < 0) {
811                 return null;
812             }
813             newRegion = this.getRegion(el, x, y);
814             if (currentRegion !== newRegion) {
815                 if (currentRegion !== undefined && highlightEnabled) {
816                     this.removeHighlight();
817                 }
818                 this.currentRegion = newRegion;
819                 if (newRegion !== undefined && highlightEnabled) {
820                     this.renderHighlight();
821                 }
822                 return true;
823             }
824             return false;
825         },
826
827         /**
828          * Reset any currently highlighted item
829          */
830         clearRegionHighlight: function () {
831             if (this.currentRegion !== undefined) {
832                 this.removeHighlight();
833                 this.currentRegion = undefined;
834                 return true;
835             }
836             return false;
837         },
838
839         renderHighlight: function () {
840             this.changeHighlight(true);
841         },
842
843         removeHighlight: function () {
844             this.changeHighlight(false);
845         },
846
847         changeHighlight: function (highlight)  {},
848
849         /**
850          * Fetch the HTML to display as a tooltip
851          */
852         getCurrentRegionTooltip: function () {
853             var options = this.options,
854                 header = '',
855                 entries = [],
856                 fields, formats, formatlen, fclass, text, i,
857                 showFields, showFieldsKey, newFields, fv,
858                 formatter, format, fieldlen, j;
859             if (this.currentRegion === undefined) {
860                 return '';
861             }
862             fields = this.getCurrentRegionFields();
863             formatter = options.get('tooltipFormatter');
864             if (formatter) {
865                 return formatter(this, options, fields);
866             }
867             if (options.get('tooltipChartTitle')) {
868                 header += '<div class="jqs jqstitle">' + options.get('tooltipChartTitle') + '</div>\n';
869             }
870             formats = this.options.get('tooltipFormat');
871             if (!formats) {
872                 return '';
873             }
874             if (!$.isArray(formats)) {
875                 formats = [formats];
876             }
877             if (!$.isArray(fields)) {
878                 fields = [fields];
879             }
880             showFields = this.options.get('tooltipFormatFieldlist');
881             showFieldsKey = this.options.get('tooltipFormatFieldlistKey');
882             if (showFields && showFieldsKey) {
883                 // user-selected ordering of fields
884                 newFields = [];
885                 for (i = fields.length; i--;) {
886                     fv = fields[i][showFieldsKey];
887                     if ((j = $.inArray(fv, showFields)) != -1) {
888                         newFields[j] = fields[i];
889                     }
890                 }
891                 fields = newFields;
892             }
893             formatlen = formats.length;
894             fieldlen = fields.length;
895             for (i = 0; i < formatlen; i++) {
896                 format = formats[i];
897                 fclass = format.fclass || 'jqsfield';
898                 for (j = 0; j < fieldlen; j++) {
899                     if (!fields[j].isNull || !options.get('tooltipSkipNull')) {
900                         $.extend(fields[j], {
901                             prefix: options.get('tooltipPrefix'),
902                             suffix: options.get('tooltipSuffix')
903                         });
904                         text = format.render(fields[j], options.get('tooltipValueLookups'), options);
905                         entries.push('<div class="' + fclass + '">' + text + '</div>');
906                     }
907                 }
908             }
909             if (entries.length) {
910                 return header + entries.join('\n');
911             }
912             return '';
913         },
914
915         getCurrentRegionFields: function () {},
916
917         calcHighlightColor: function (color, options) {
918             var highlightColor = options.get('highlightColor'),
919                 lighten = options.get('highlightLighten'),
920                 parse, mult, rgbnew, i;
921             if (highlightColor) {
922                 return highlightColor;
923             }
924             if (lighten) {
925                 // extract RGB values
926                 parse = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(color) || /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(color);
927                 if (parse) {
928                     rgbnew = [];
929                     mult = color.length === 4 ? 16 : 1;
930                     for (i = 0; i < 3; i++) {
931                         rgbnew[i] = clipval(Math.round(parseInt(parse[i + 1], 16) * mult * lighten), 0, 255);
932                     }
933                     return 'rgb(' + rgbnew.join(',') + ')';
934                 }
935
936             }
937             return color;
938         }
939
940     });
941
942     barHighlightMixin = {
943         changeHighlight: function (highlight) {
944             var currentRegion = this.currentRegion,
945                 target = this.target,
946                 shapeids = this.regionShapes[currentRegion],
947                 newShapes;
948             // will be null if the region value was null
949             if (shapeids) {
950                 newShapes = this.renderRegion(currentRegion, highlight);
951                 if ($.isArray(newShapes) || $.isArray(shapeids)) {
952                     target.replaceWithShapes(shapeids, newShapes);
953                     this.regionShapes[currentRegion] = $.map(newShapes, function (newShape) {
954                         return newShape.id;
955                     });
956                 } else {
957                     target.replaceWithShape(shapeids, newShapes);
958                     this.regionShapes[currentRegion] = newShapes.id;
959                 }
960             }
961         },
962
963         render: function () {
964             var values = this.values,
965                 target = this.target,
966                 regionShapes = this.regionShapes,
967                 shapes, ids, i, j;
968
969             if (!this.cls._super.render.call(this)) {
970                 return;
971             }
972             for (i = values.length; i--;) {
973                 shapes = this.renderRegion(i);
974                 if (shapes) {
975                     if ($.isArray(shapes)) {
976                         ids = [];
977                         for (j = shapes.length; j--;) {
978                             shapes[j].append();
979                             ids.push(shapes[j].id);
980                         }
981                         regionShapes[i] = ids;
982                     } else {
983                         shapes.append();
984                         regionShapes[i] = shapes.id; // store just the shapeid
985                     }
986                 } else {
987                     // null value
988                     regionShapes[i] = null;
989                 }
990             }
991             target.render();
992         }
993     };
994
995     /**
996      * Bar charts
997      */
998     $.fn.sparkline.bar = bar = createClass($.fn.sparkline._base, barHighlightMixin, {
999         type: 'bar',
1000
1001         init: function (el, values, options, width, height) {
1002             var barWidth = parseInt(options.get('barWidth'), 10),
1003                 barSpacing = parseInt(options.get('barSpacing'), 10),
1004                 chartRangeMin = options.get('chartRangeMin'),
1005                 chartRangeMax = options.get('chartRangeMax'),
1006                 chartRangeClip = options.get('chartRangeClip'),
1007                 groupMin, groupMax,
1008                 numValues, i, vlen, range, zeroAxis, xaxisOffset, min, max, clipMin, clipMax,
1009                 vlist, j, slen, svals, val, yoffset, yMaxCalc, canvasHeightEf;
1010             bar._super.init.call(this, el, values, options, width, height);
1011
1012             this.regionShapes = {};
1013             this.barWidth = barWidth;
1014             this.barSpacing = barSpacing;
1015             this.totalBarWidth = barWidth + barSpacing;
1016             this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing);
1017
1018             this.initTarget();
1019
1020             if (chartRangeClip) {
1021                 clipMin = chartRangeMin === undefined ? -Infinity : chartRangeMin;
1022                 clipMax = chartRangeMax === undefined ? Infinity : chartRangeMax;
1023             }
1024
1025             numValues = [];
1026             for (i = 0, vlen = values.length; i < vlen; i++) {
1027                     val = chartRangeClip ? clipval(values[i], clipMin, clipMax) : values[i];
1028                     val = values[i] = normalizeValue(val);
1029                     if (val !== null) {
1030                         numValues.push(val);
1031                     }
1032             }
1033             this.max = max = Math.max.apply(Math, numValues);
1034             this.min = min = Math.min.apply(Math, numValues);
1035
1036             if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < min)) {
1037                 min = options.get('chartRangeMin');
1038             }
1039             if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > max)) {
1040                 max = options.get('chartRangeMax');
1041             }
1042
1043             this.zeroAxis = zeroAxis = options.get('zeroAxis', true);
1044             if (min <= 0 && max >= 0 && zeroAxis) {
1045                 xaxisOffset = 0;
1046             } else if (zeroAxis == false) {
1047                 xaxisOffset = min;
1048             } else if (min > 0) {
1049                 xaxisOffset = min;
1050             } else {
1051                 xaxisOffset = max;
1052             }
1053             this.xaxisOffset = xaxisOffset;
1054
1055             range = max - min;
1056
1057             // as we plot zero/min values a single pixel line, we add a pixel to all other
1058             // values - Reduce the effective canvas size to suit
1059             this.canvasHeightEf = (zeroAxis && min < 0) ? this.canvasHeight - 2 : this.canvasHeight - 1;
1060
1061             if (min < xaxisOffset) {
1062                 yMaxCalc = max;
1063                 yoffset = (yMaxCalc - xaxisOffset) / range * this.canvasHeight;
1064                 if (yoffset !== Math.ceil(yoffset)) {
1065                     this.canvasHeightEf -= 2;
1066                     yoffset = Math.ceil(yoffset);
1067                 }
1068             } else {
1069                 yoffset = this.canvasHeight;
1070             }
1071             this.yoffset = yoffset;
1072
1073             this.range = range;
1074         },
1075
1076         getRegion: function (el, x, y) {
1077             var result = Math.floor(x / this.totalBarWidth);
1078             return (result < 0 || result >= this.values.length) ? undefined : result;
1079         },
1080
1081         getCurrentRegionFields: function () {
1082             var currentRegion = this.currentRegion,
1083                 values = ensureArray(this.values[currentRegion]),
1084                 result = [],
1085                 value, i;
1086             for (i = values.length; i--;) {
1087                 value = values[i];
1088                 result.push({
1089                     isNull: value === null,
1090                     value: value,
1091                     color: this.calcColor(i, value, currentRegion),
1092                     offset: currentRegion
1093                 });
1094             }
1095             return result;
1096         },
1097
1098         calcColor: function (stacknum, value, valuenum) {
1099             var color, newColor,
1100                 options = this.options;
1101                 color = (value < 0) ? options.get('negBarColor') : options.get('barColor');
1102             if (value === 0 && options.get('zeroColor') !== undefined) {
1103                 color = options.get('zeroColor');
1104             }
1105             return color;
1106         },
1107
1108         /**
1109          * Render bar(s) for a region
1110          */
1111         renderRegion: function (valuenum, highlight) {
1112             var vals = this.values[valuenum],
1113                 options = this.options,
1114                 xaxisOffset = this.xaxisOffset,
1115                 result = [],
1116                 range = this.range,
1117                 target = this.target,
1118                 x = valuenum * this.totalBarWidth,
1119                 canvasHeightEf = this.canvasHeightEf,
1120                 yoffset = this.yoffset,
1121                 y, height, color, isNull, yoffsetNeg, i, valcount, val, minPlotted, allMin;
1122
1123             vals = $.isArray(vals) ? vals : [vals];
1124             valcount = vals.length;
1125             val = vals[0];
1126             isNull = all(null, vals);
1127             allMin = all(xaxisOffset, vals, true);
1128
1129             if (isNull) {
1130                 if (options.get('nullColor')) {
1131                     color = highlight ? options.get('nullColor') : this.calcHighlightColor(options.get('nullColor'), options);
1132                     y = (yoffset > 0) ? yoffset - 1 : yoffset;
1133                     return target.drawRect(x, y, this.barWidth - 1, 0, color, color);
1134                 } else {
1135                     return undefined;
1136                 }
1137             }
1138             yoffsetNeg = yoffset;
1139             for (i = 0; i < valcount; i++) {
1140                 val = vals[i];
1141
1142                 if (range > 0) {
1143                     height = Math.floor(canvasHeightEf * ((Math.abs(val - xaxisOffset) / range))) + 1;
1144                 } else {
1145                     height = 1;
1146                 }
1147                 if (val < xaxisOffset || (val === xaxisOffset && yoffset === 0)) {
1148                     y = yoffsetNeg;
1149                     yoffsetNeg += height;
1150                 } else {
1151                     y = yoffset - height;
1152                     yoffset -= height;
1153                 }
1154                 color = this.calcColor(i, val, valuenum);
1155                 if (highlight) {
1156                     color = this.calcHighlightColor(color, options);
1157                 }
1158                 result.push(target.drawRect(x, y, this.barWidth - 1, height - 1, color, color));
1159             }
1160             if (result.length === 1) {
1161                 return result[0];
1162             }
1163             return result;
1164         }
1165     });
1166
1167     // Setup a very simple "virtual canvas" to make drawing the few shapes we need easier
1168     // This is accessible as $(foo).simpledraw()
1169
1170     VShape = createClass({
1171         init: function (target, id, type, args) {
1172             this.target = target;
1173             this.id = id;
1174             this.type = type;
1175             this.args = args;
1176         },
1177         append: function () {
1178             this.target.appendShape(this);
1179             return this;
1180         }
1181     });
1182
1183     VCanvas_base = createClass({
1184         _pxregex: /(\d+)(px)?\s*$/i,
1185
1186         init: function (width, height, target) {
1187             if (!width) {
1188                 return;
1189             }
1190             this.width = width;
1191             this.height = height;
1192             this.target = target;
1193             this.lastShapeId = null;
1194             if (target[0]) {
1195                 target = target[0];
1196             }
1197             $.data(target, '_jqs_vcanvas', this);
1198         },
1199
1200         drawLine: function (x1, y1, x2, y2, lineColor, lineWidth) {
1201             return this.drawShape([[x1, y1], [x2, y2]], lineColor, lineWidth);
1202         },
1203
1204         drawShape: function (path, lineColor, fillColor, lineWidth) {
1205             return this._genShape('Shape', [path, lineColor, fillColor, lineWidth]);
1206         },
1207
1208         drawRect: function (x, y, width, height, lineColor, fillColor) {
1209             return this._genShape('Rect', [x, y, width, height, lineColor, fillColor]);
1210         },
1211
1212         getElement: function () {
1213             return this.canvas;
1214         },
1215
1216         /**
1217          * Return the most recently inserted shape id
1218          */
1219         getLastShapeId: function () {
1220             return this.lastShapeId;
1221         },
1222
1223         /**
1224          * Clear and reset the canvas
1225          */
1226         reset: function () {
1227             alert('reset not implemented');
1228         },
1229
1230         _insert: function (el, target) {
1231             $(target).html(el);
1232         },
1233
1234         /**
1235          * Calculate the pixel dimensions of the canvas
1236          */
1237         _calculatePixelDims: function (width, height, canvas) {
1238             // XXX This should probably be a configurable option
1239             var match;
1240             match = this._pxregex.exec(height);
1241             if (match) {
1242                 this.pixelHeight = match[1];
1243             } else {
1244                 this.pixelHeight = $(canvas).height();
1245             }
1246             match = this._pxregex.exec(width);
1247             if (match) {
1248                 this.pixelWidth = match[1];
1249             } else {
1250                 this.pixelWidth = $(canvas).width();
1251             }
1252         },
1253
1254         /**
1255          * Generate a shape object and id for later rendering
1256          */
1257         _genShape: function (shapetype, shapeargs) {
1258             var id = shapeCount++;
1259             shapeargs.unshift(id);
1260             return new VShape(this, id, shapetype, shapeargs);
1261         },
1262
1263         /**
1264          * Add a shape to the end of the render queue
1265          */
1266         appendShape: function (shape) {
1267             alert('appendShape not implemented');
1268         },
1269
1270         /**
1271          * Replace one shape with another
1272          */
1273         replaceWithShape: function (shapeid, shape) {
1274             alert('replaceWithShape not implemented');
1275         },
1276
1277         /**
1278          * Insert one shape after another in the render queue
1279          */
1280         insertAfterShape: function (shapeid, shape) {
1281             alert('insertAfterShape not implemented');
1282         },
1283
1284         /**
1285          * Remove a shape from the queue
1286          */
1287         removeShapeId: function (shapeid) {
1288             alert('removeShapeId not implemented');
1289         },
1290
1291         /**
1292          * Find a shape at the specified x/y co-ordinates
1293          */
1294         getShapeAt: function (el, x, y) {
1295             alert('getShapeAt not implemented');
1296         },
1297
1298         /**
1299          * Render all queued shapes onto the canvas
1300          */
1301         render: function () {
1302             alert('render not implemented');
1303         }
1304     });
1305
1306     VCanvas_canvas = createClass(VCanvas_base, {
1307         init: function (width, height, target, interact) {
1308             VCanvas_canvas._super.init.call(this, width, height, target);
1309             this.canvas = document.createElement('canvas');
1310             if (target[0]) {
1311                 target = target[0];
1312             }
1313             $.data(target, '_jqs_vcanvas', this);
1314             $(this.canvas).css({ display: 'inline-block', width: width, height: height, verticalAlign: 'top' });
1315             this._insert(this.canvas, target);
1316             this._calculatePixelDims(width, height, this.canvas);
1317             this.canvas.width = this.pixelWidth;
1318             this.canvas.height = this.pixelHeight;
1319             this.interact = interact;
1320             this.shapes = {};
1321             this.shapeseq = [];
1322             this.currentTargetShapeId = undefined;
1323             $(this.canvas).css({width: this.pixelWidth, height: this.pixelHeight});
1324         },
1325
1326         _getContext: function (lineColor, fillColor, lineWidth) {
1327             var context = this.canvas.getContext('2d');
1328             if (lineColor !== undefined) {
1329                 context.strokeStyle = lineColor;
1330             }
1331             context.lineWidth = lineWidth === undefined ? 1 : lineWidth;
1332             if (fillColor !== undefined) {
1333                 context.fillStyle = fillColor;
1334             }
1335             return context;
1336         },
1337
1338         reset: function () {
1339             var context = this._getContext();
1340             context.clearRect(0, 0, this.pixelWidth, this.pixelHeight);
1341             this.shapes = {};
1342             this.shapeseq = [];
1343             this.currentTargetShapeId = undefined;
1344         },
1345
1346         _drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) {
1347             var context = this._getContext(lineColor, fillColor, lineWidth),
1348                 i, plen;
1349             context.beginPath();
1350             context.moveTo(path[0][0] + 0.5, path[0][1] + 0.5);
1351             for (i = 1, plen = path.length; i < plen; i++) {
1352                 context.lineTo(path[i][0] + 0.5, path[i][1] + 0.5); // the 0.5 offset gives us crisp pixel-width lines
1353             }
1354             if (lineColor !== undefined) {
1355                 context.stroke();
1356             }
1357             if (fillColor !== undefined) {
1358                 context.fill();
1359             }
1360             if (this.targetX !== undefined && this.targetY !== undefined &&
1361                 context.isPointInPath(this.targetX, this.targetY)) {
1362                 this.currentTargetShapeId = shapeid;
1363             }
1364         },
1365
1366         _drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) {
1367             return this._drawShape(shapeid, [[x, y], [x + width, y], [x + width, y + height], [x, y + height], [x, y]], lineColor, fillColor);
1368         },
1369
1370         appendShape: function (shape) {
1371             this.shapes[shape.id] = shape;
1372             this.shapeseq.push(shape.id);
1373             this.lastShapeId = shape.id;
1374             return shape.id;
1375         },
1376
1377         replaceWithShape: function (shapeid, shape) {
1378             var shapeseq = this.shapeseq,
1379                 i;
1380             this.shapes[shape.id] = shape;
1381             for (i = shapeseq.length; i--;) {
1382                 if (shapeseq[i] == shapeid) {
1383                     shapeseq[i] = shape.id;
1384                 }
1385             }
1386             delete this.shapes[shapeid];
1387         },
1388
1389         replaceWithShapes: function (shapeids, shapes) {
1390             var shapeseq = this.shapeseq,
1391                 shapemap = {},
1392                 sid, i, first;
1393
1394             for (i = shapeids.length; i--;) {
1395                 shapemap[shapeids[i]] = true;
1396             }
1397             for (i = shapeseq.length; i--;) {
1398                 sid = shapeseq[i];
1399                 if (shapemap[sid]) {
1400                     shapeseq.splice(i, 1);
1401                     delete this.shapes[sid];
1402                     first = i;
1403                 }
1404             }
1405             for (i = shapes.length; i--;) {
1406                 shapeseq.splice(first, 0, shapes[i].id);
1407                 this.shapes[shapes[i].id] = shapes[i];
1408             }
1409
1410         },
1411
1412         insertAfterShape: function (shapeid, shape) {
1413             var shapeseq = this.shapeseq,
1414                 i;
1415             for (i = shapeseq.length; i--;) {
1416                 if (shapeseq[i] === shapeid) {
1417                     shapeseq.splice(i + 1, 0, shape.id);
1418                     this.shapes[shape.id] = shape;
1419                     return;
1420                 }
1421             }
1422         },
1423
1424         removeShapeId: function (shapeid) {
1425             var shapeseq = this.shapeseq,
1426                 i;
1427             for (i = shapeseq.length; i--;) {
1428                 if (shapeseq[i] === shapeid) {
1429                     shapeseq.splice(i, 1);
1430                     break;
1431                 }
1432             }
1433             delete this.shapes[shapeid];
1434         },
1435
1436         getShapeAt: function (el, x, y) {
1437             this.targetX = x;
1438             this.targetY = y;
1439             this.render();
1440             return this.currentTargetShapeId;
1441         },
1442
1443         render: function () {
1444             var shapeseq = this.shapeseq,
1445                 shapes = this.shapes,
1446                 shapeCount = shapeseq.length,
1447                 context = this._getContext(),
1448                 shapeid, shape, i;
1449             context.clearRect(0, 0, this.pixelWidth, this.pixelHeight);
1450             for (i = 0; i < shapeCount; i++) {
1451                 shapeid = shapeseq[i];
1452                 shape = shapes[shapeid];
1453                 this['_draw' + shape.type].apply(this, shape.args);
1454             }
1455             if (!this.interact) {
1456                 // not interactive so no need to keep the shapes array
1457                 this.shapes = {};
1458                 this.shapeseq = [];
1459             }
1460         }
1461
1462     });
1463 }))}(document, Math));