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