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