initial commit
[power-brultech.git] / js / jquery.sparkline.js
1 /**
2 *
3 * jquery.sparkline.js
4 *
5 * v2.0
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 * For line charts, x values can also be specified:
63 *   <p>Sparkline: <span class="sparkline">1:1,2.7:4,3.4:6,5:6,6:8,8.7:5,9:3,10:5</span></p>
64 *    $('#sparkline1').sparkline([ [1,1], [2.7,4], [3.4,6], [5,6], [6,8], [8.7,5], [9,3], [10,5] ])
65 *
66 * By default, options should be passed in as teh second argument to the sparkline function:
67 *   $('.sparkline').sparkline([1,2,3,4], {type: 'bar'})
68 *
69 * Options can also be set by passing them on the tag itself.  This feature is disabled by default though
70 * as there's a slight performance overhead:
71 *   $('.sparkline').sparkline([1,2,3,4], {enableTagOptions: true})
72 *   <p>Sparkline: <span class="sparkline" sparkType="bar" sparkBarColor="red">loading</span></p>
73 * Prefix all options supplied as tag attribute with "spark" (configurable by setting tagOptionPrefix)
74 *
75 * Supported options:
76 *   lineColor - Color of the line used for the chart
77 *   fillColor - Color used to fill in the chart - Set to '' or false for a transparent chart
78 *   width - Width of the chart - Defaults to 3 times the number of values in pixels
79 *   height - Height of the chart - Defaults to the height of the containing element
80 *   chartRangeMin - Specify the minimum value to use for the Y range of the chart - Defaults to the minimum value supplied
81 *   chartRangeMax - Specify the maximum value to use for the Y range of the chart - Defaults to the maximum value supplied
82 *   chartRangeClip - Clip out of range values to the max/min specified by chartRangeMin and chartRangeMax
83 *   chartRangeMinX - Specify the minimum value to use for the X range of the chart - Defaults to the minimum value supplied
84 *   chartRangeMaxX - Specify the maximum value to use for the X range of the chart - Defaults to the maximum value supplied
85 *   composite - If true then don't erase any existing chart attached to the tag, but draw
86 *           another chart over the top - Note that width and height are ignored if an
87 *           existing chart is detected.
88 *   tagValuesAttribute - Name of tag attribute to check for data values - Defaults to 'values'
89 *   enableTagOptions - Whether to check tags for sparkline options
90 *   tagOptionPrefix - Prefix used for options supplied as tag attributes - Defaults to 'spark'
91 *   disableHiddenCheck - If set to true, then the plugin will assume that charts will never be drawn into a
92 *           hidden dom element, avoding a browser reflow
93 *   disableInteraction - If set to true then all mouseover/click interaction behaviour will be disabled,
94 *       making the plugin perform much like it did in 1.x
95 *   disableTooltips - If set to true then tooltips will be disabled - Defaults to false (tooltips enabled)
96 *   disableHighlight - If set to true then highlighting of selected chart elements on mouseover will be disabled
97 *       defaults to false (highlights enabled)
98 *   highlightLighten - Factor to lighten/darken highlighted chart values by - Defaults to 1.4 for a 40% increase
99 *   tooltipContainer - Specify which DOM element the tooltip should be rendered into - defaults to document.body
100 *   tooltipClassname - Optional CSS classname to apply to tooltips - If not specified then a default style will be applied
101 *   tooltipOffsetX - How many pixels away from the mouse pointer to render the tooltip on the X axis
102 *   tooltipOffsetY - How many pixels away from the mouse pointer to render the tooltip on the r axis
103 *   tooltipFormatter  - Optional callback that allows you to override the HTML displayed in the tooltip
104 *       callback is given arguments of (sparkline, options, fields)
105 *   tooltipChartTitle - If specified then the tooltip uses the string specified by this setting as a title
106 *   tooltipFormat - A format string or SPFormat object  (or an array thereof for multiple entries)
107 *       to control the format of the tooltip
108 *   tooltipPrefix - A string to prepend to each field displayed in a tooltip
109 *   tooltipSuffix - A string to append to each field displayed in a tooltip
110 *   tooltipSkipNull - If true then null values will not have a tooltip displayed (defaults to true)
111 *   tooltipValueLookups - An object or range map to map field values to tooltip strings
112 *       (eg. to map -1 to "Lost", 0 to "Draw", and 1 to "Win")
113 *   numberFormatter - Optional callback for formatting numbers in tooltips
114 *   numberDigitGroupSep - Character to use for group separator in numbers "1,234" - Defaults to ","
115 *   numberDecimalMark - Character to use for the decimal point when formatting numbers - Defaults to "."
116 *   numberDigitGroupCount - Number of digits between group separator - Defaults to 3
117 *
118 * There are 7 types of sparkline, selected by supplying a "type" option of 'line' (default),
119 * 'bar', 'tristate', 'bullet', 'discrete', 'pie' or 'box'
120 *    line - Line chart.  Options:
121 *       spotColor - Set to '' to not end each line in a circular spot
122 *       minSpotColor - If set, color of spot at minimum value
123 *       maxSpotColor - If set, color of spot at maximum value
124 *       spotRadius - Radius in pixels
125 *       lineWidth - Width of line in pixels
126 *       normalRangeMin
127 *       normalRangeMax - If set draws a filled horizontal bar between these two values marking the "normal"
128 *                      or expected range of values
129 *       normalRangeColor - Color to use for the above bar
130 *       drawNormalOnTop - Draw the normal range above the chart fill color if true
131 *       defaultPixelsPerValue - Defaults to 3 pixels of width for each value in the chart
132 *       highlightSpotColor - The color to use for drawing a highlight spot on mouseover - Set to null to disable
133 *       highlightLineColor - The color to use for drawing a highlight line on mouseover - Set to null to disable
134 *       valueSpots - Specify which points to draw spots on, and in which color.  Accepts a range map
135 *
136 *   bar - Bar chart.  Options:
137 *       barColor - Color of bars for postive values
138 *       negBarColor - Color of bars for negative values
139 *       zeroColor - Color of bars with zero values
140 *       nullColor - Color of bars with null values - Defaults to omitting the bar entirely
141 *       barWidth - Width of bars in pixels
142 *       colorMap - Optional mappnig of values to colors to override the *BarColor values above
143 *                  can be an Array of values to control the color of individual bars or a range map
144 *                  to specify colors for individual ranges of values
145 *       barSpacing - Gap between bars in pixels
146 *       zeroAxis - Centers the y-axis around zero if true
147 *
148 *   tristate - Charts values of win (>0), lose (<0) or draw (=0)
149 *       posBarColor - Color of win values
150 *       negBarColor - Color of lose values
151 *       zeroBarColor - Color of draw values
152 *       barWidth - Width of bars in pixels
153 *       barSpacing - Gap between bars in pixels
154 *       colorMap - Optional mappnig of values to colors to override the *BarColor values above
155 *                  can be an Array of values to control the color of individual bars or a range map
156 *                  to specify colors for individual ranges of values
157 *
158 *   discrete - Options:
159 *       lineHeight - Height of each line in pixels - Defaults to 30% of the graph height
160 *       thesholdValue - Values less than this value will be drawn using thresholdColor instead of lineColor
161 *       thresholdColor
162 *
163 *   bullet - Values for bullet graphs msut be in the order: target, performance, range1, range2, range3, ...
164 *       options:
165 *       targetColor - The color of the vertical target marker
166 *       targetWidth - The width of the target marker in pixels
167 *       performanceColor - The color of the performance measure horizontal bar
168 *       rangeColors - Colors to use for each qualitative range background color
169 *
170 *   pie - Pie chart. Options:
171 *       sliceColors - An array of colors to use for pie slices
172 *       offset - Angle in degrees to offset the first slice - Try -90 or +90
173 *       borderWidth - Width of border to draw around the pie chart, in pixels - Defaults to 0 (no border)
174 *       borderColor - Color to use for the pie chart border - Defaults to #000
175 *
176 *   box - Box plot. Options:
177 *       raw - Set to true to supply pre-computed plot points as values
178 *             values should be: low_outlier, low_whisker, q1, median, q3, high_whisker, high_outlier
179 *             When set to false you can supply any number of values and the box plot will
180 *             be computed for you.  Default is false.
181 *       showOutliers - Set to true (default) to display outliers as circles
182 *       outlierIRQ - Interquartile range used to determine outliers.  Default 1.5
183 *       boxLineColor - Outline color of the box
184 *       boxFillColor - Fill color for the box
185 *       whiskerColor - Line color used for whiskers
186 *       outlierLineColor - Outline color of outlier circles
187 *       outlierFillColor - Fill color of the outlier circles
188 *       spotRadius - Radius of outlier circles
189 *       medianColor - Line color of the median line
190 *       target - Draw a target cross hair at the supplied value (default undefined)
191 *
192 *
193 *
194 *   Examples:
195 *   $('#sparkline1').sparkline(myvalues, { lineColor: '#f00', fillColor: false });
196 *   $('.barsparks').sparkline('html', { type:'bar', height:'40px', barWidth:5 });
197 *   $('#tristate').sparkline([1,1,-1,1,0,0,-1], { type:'tristate' }):
198 *   $('#discrete').sparkline([1,3,4,5,5,3,4,5], { type:'discrete' });
199 *   $('#bullet').sparkline([10,12,12,9,7], { type:'bullet' });
200 *   $('#pie').sparkline([1,1,2], { type:'pie' });
201 */
202
203 /*jslint regexp: true, browser: true, jquery: true, white: true, nomen: false, plusplus: false, maxerr: 500, indent: 4 */
204
205 (function ($) {
206     'use strict';
207
208     var UNSET_OPTION = {},
209         getDefaults, createClass, SPFormat, clipval, quartile, normalizeValue, normalizeValues,
210         remove, isNumber, all, sum, addCSS, ensureArray, formatNumber, RangeMap,
211         MouseHandler, Tooltip, barHighlightMixin,
212         line, bar, tristate, discrete, bullet, pie, box, defaultStyles, initStyles,
213          VShape, VCanvas_base, VCanvas_canvas, VCanvas_vml, pending, shapeCount = 0;
214
215     /**
216      * Default configuration settings
217      */
218     getDefaults = function () {
219         return {
220             // Settings common to most/all chart types
221             common: {
222                 type: 'line',
223                 lineColor: '#00f',
224                 fillColor: '#cdf',
225                 defaultPixelsPerValue: 3,
226                 width: 'auto',
227                 height: 'auto',
228                 composite: false,
229                 tagValuesAttribute: 'values',
230                 tagOptionsPrefix: 'spark',
231                 enableTagOptions: false,
232                 enableHighlight: true,
233                 highlightLighten: 1.4,
234                 tooltipSkipNull: true,
235                 tooltipPrefix: '',
236                 tooltipSuffix: '',
237                 disableHiddenCheck: false,
238                 numberFormatter: false,
239                 numberDigitGroupCount: 3,
240                 numberDigitGroupSep: ',',
241                 numberDecimalMark: '.',
242                 disableTooltips: false,
243                 disableInteraction: false
244             },
245             // Defaults for line charts
246             line: {
247                 spotColor: '#f80',
248                 highlightSpotColor: '#5f5',
249                 highlightLineColor: '#f22',
250                 spotRadius: 1.5,
251                 minSpotColor: '#f80',
252                 maxSpotColor: '#f80',
253                 lineWidth: 1,
254                 normalRangeMin: undefined,
255                 normalRangeMax: undefined,
256                 normalRangeColor: '#ccc',
257                 drawNormalOnTop: false,
258                 chartRangeMin: undefined,
259                 chartRangeMax: undefined,
260                 chartRangeMinX: undefined,
261                 chartRangeMaxX: undefined,
262                 tooltipFormat: new SPFormat('<span style="color: {{color}}">&#9679;</span> {{prefix}}{{y}}{{suffix}}')
263             },
264             // Defaults for bar charts
265             bar: {
266                 barColor: '#3366cc',
267                 negBarColor: '#f44',
268                 stackedBarColor: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00',
269                     '#dd4477', '#0099c6', '#990099'],
270                 zeroColor: undefined,
271                 nullColor: undefined,
272                 zeroAxis: true,
273                 barWidth: 4,
274                 barSpacing: 1,
275                 chartRangeMax: undefined,
276                 chartRangeMin: undefined,
277                 chartRangeClip: false,
278                 colorMap: undefined,
279                 tooltipFormat: new SPFormat('<span style="color: {{color}}">&#9679;</span> {{prefix}}{{value}}{{suffix}}')
280             },
281             // Defaults for tristate charts
282             tristate: {
283                 barWidth: 4,
284                 barSpacing: 1,
285                 posBarColor: '#6f6',
286                 negBarColor: '#f44',
287                 zeroBarColor: '#999',
288                 colorMap: {},
289                 tooltipFormat: new SPFormat('<span style="color: {{color}}">&#9679;</span> {{value:map}}'),
290                 tooltipValueLookups: { map: { '-1': 'Loss', '0': 'Draw', '1': 'Win' } }
291             },
292             // Defaults for discrete charts
293             discrete: {
294                 lineHeight: 'auto',
295                 thresholdColor: undefined,
296                 thresholdValue: 0,
297                 chartRangeMax: undefined,
298                 chartRangeMin: undefined,
299                 chartRangeClip: false,
300                 tooltipFormat: new SPFormat('{{prefix}}{{value}}{{suffix}}')
301             },
302             // Defaults for bullet charts
303             bullet: {
304                 targetColor: '#f33',
305                 targetWidth: 3, // width of the target bar in pixels
306                 performanceColor: '#33f',
307                 rangeColors: ['#d3dafe', '#a8b6ff', '#7f94ff'],
308                 base: undefined, // set this to a number to change the base start number
309                 tooltipFormat: new SPFormat('{{fieldkey:fields}} - {{value}}'),
310                 tooltipValueLookups: { fields: {r: 'Range', p: 'Performance', t: 'Target'} }
311             },
312             // Defaults for pie charts
313             pie: {
314                 offset: 0,
315                 sliceColors: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00',
316                     '#dd4477', '#0099c6', '#990099'],
317                 borderWidth: 0,
318                 borderColor: '#000',
319                 tooltipFormat: new SPFormat('<span style="color: {{color}}">&#9679;</span> {{value}} ({{percent.1}}%)')
320             },
321             // Defaults for box plots
322             box: {
323                 raw: false,
324                 boxLineColor: '#000',
325                 boxFillColor: '#cdf',
326                 whiskerColor: '#000',
327                 outlierLineColor: '#333',
328                 outlierFillColor: '#fff',
329                 medianColor: '#f00',
330                 showOutliers: true,
331                 outlierIQR: 1.5,
332                 spotRadius: 1.5,
333                 target: undefined,
334                 targetColor: '#4a2',
335                 chartRangeMax: undefined,
336                 chartRangeMin: undefined,
337                 tooltipFormat: new SPFormat('{{field:fields}}: {{value}}'),
338                 tooltipFormatFieldlistKey: 'field',
339                 tooltipValueLookups: { fields: { lq: 'Lower Quartile', med: 'Median',
340                     uq: 'Upper Quartile', lo: 'Left Outlier', ro: 'Right Outlier',
341                     lw: 'Left Whisker', rw: 'Right Whisker'} }
342             }
343         };
344     };
345
346     // You can have tooltips use a css class other than jqstooltip by specifying tooltipClassname
347     defaultStyles = '.jqstooltip { ' +
348             'position: absolute;' +
349             'left: 0px;' +
350             'top: 0px;' +
351             'visibility: hidden;' +
352             'background: rgb(0, 0, 0) transparent;' +
353             'background-color: rgba(0,0,0,0.6);' +
354             'filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000);' +
355             '-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000)";' +
356             'color: white;' +
357             'font: 10px arial, san serif;' +
358             'text-align: left;' +
359             'white-space: nowrap;' +
360             'padding: 5px;' +
361             'border: 1px solid white;' +
362             '}' +
363             '.jqsfield { ' +
364             'color: white;' +
365             'font: 10px arial, san serif;' +
366             'text-align: left;' +
367             '}';
368
369     initStyles = function() {
370         addCSS(defaultStyles);
371     };
372
373     $(initStyles);
374
375     /**
376      * Utilities
377      */
378
379     createClass = function (/* [baseclass, [mixin, ...]], definition */) {
380         var Class, args;
381         Class = function () {
382             this.init.apply(this, arguments);
383         };
384         if (arguments.length > 1) {
385             if (arguments[0]) {
386                 Class.prototype = $.extend(new arguments[0](), arguments[arguments.length - 1]);
387                 Class._super = arguments[0].prototype;
388             } else {
389                 Class.prototype = arguments[arguments.length - 1];
390             }
391             if (arguments.length > 2) {
392                 args = Array.prototype.slice.call(arguments, 1, -1);
393                 args.unshift(Class.prototype);
394                 $.extend.apply($, args);
395             }
396         } else {
397             Class.prototype = arguments[0];
398         }
399         Class.prototype.cls = Class;
400         return Class;
401     };
402
403     /**
404      * Wraps a format string for tooltips
405      * {{x}}
406      * {{x.2}
407      * {{x:months}}
408      */
409     $.SPFormatClass = SPFormat = createClass({
410         fre: /\{\{([\w.]+?)(:(.+?))?\}\}/g,
411         precre: /(\w+)\.(\d+)/,
412
413         init: function (format, fclass) {
414             this.format = format;
415             this.fclass = fclass;
416         },
417
418         render: function (fieldset, lookups, options) {
419             var self = this,
420                 fields = fieldset,
421                 match, token, lookupkey, fieldvalue, prec;
422             return this.format.replace(this.fre, function () {
423                 var lookup;
424                 token = arguments[1];
425                 lookupkey = arguments[3];
426                 match = self.precre.exec(token);
427                 if (match) {
428                     prec = match[2];
429                     token = match[1];
430                 } else {
431                     prec = false;
432                 }
433                 fieldvalue = fields[token];
434                 if (fieldvalue === undefined) {
435                     return '';
436                 }
437                 if (lookupkey && lookups && lookups[lookupkey]) {
438                     lookup = lookups[lookupkey];
439                     if (lookup.get) { // RangeMap
440                         return lookups[lookupkey].get(fieldvalue) || fieldvalue;
441                     } else {
442                         return lookups[lookupkey][fieldvalue] || fieldvalue;
443                     }
444                 }
445                 if (isNumber(fieldvalue)) {
446                     if (options.get('numberFormatter')) {
447                         fieldvalue = options.get('numberFormatter')(fieldvalue);
448                     } else {
449                         fieldvalue = formatNumber(fieldvalue, prec,
450                             options.get('numberDigitGroupCount'),
451                             options.get('numberDigitGroupSep'),
452                             options.get('numberDecimalMark'));
453                     }
454                 }
455                 return fieldvalue;
456             });
457         }
458     });
459
460     // convience method to avoid needing the new operator
461     $.spformat = function(format, fclass) {
462         return new SPFormat(format, fclass);
463     };
464
465     clipval = function (val, min, max) {
466         if (val < min) {
467             return min;
468         }
469         if (val > max) {
470             return max;
471         }
472         return val;
473     };
474
475     quartile = function (values, q) {
476         var vl;
477         if (q === 2) {
478             vl = Math.floor(values.length / 2);
479             return values.length % 2 ? values[vl] : (values[vl] + values[vl + 1]) / 2;
480         } else {
481             vl = Math.floor(values.length / 4);
482             return values.length % 2 ? (values[vl * q] + values[vl * q + 1]) / 2 : values[vl * q];
483         }
484     };
485
486     normalizeValue = function (val) {
487         var nf;
488         switch (val) {
489             case 'undefined':
490                 val = undefined;
491                 break;
492             case 'null':
493                 val = null;
494                 break;
495             case 'true':
496                 val = true;
497                 break;
498             case 'false':
499                 val = false;
500                 break;
501             default:
502                 nf = parseFloat(val);
503                 if (val == nf) {
504                     val = nf;
505                 }
506         }
507         return val;
508     };
509
510     normalizeValues = function (vals) {
511         var i, result = [];
512         for (i = vals.length; i--;) {
513             result[i] = normalizeValue(vals[i]);
514         }
515         return result;
516     };
517
518     remove = function (vals, filter) {
519         var i, vl, result = [];
520         for (i = 0, vl = vals.length; i < vl; i++) {
521             if (vals[i] !== filter) {
522                 result.push(vals[i]);
523             }
524         }
525         return result;
526     };
527
528     isNumber = function (num) {
529         return !isNaN(parseFloat(num)) && isFinite(num);
530     };
531
532     formatNumber = function (num, prec, groupsize, groupsep, decsep) {
533         var p, i;
534         num = (prec === false ? parseFloat(num).toString() : num.toFixed(prec)).split('');
535         p = (p = $.inArray('.', num)) < 0 ? num.length : p;
536         if (p < num.length) {
537             num[p] = decsep;
538         }
539         for (i = p - groupsize; i > 0; i -= groupsize) {
540             num.splice(i, 0, groupsep);
541         }
542         return num.join('');
543     };
544
545     // determine if all values of an array match a value
546     // returns true if the array is empty
547     all = function (val, arr, ignoreNull) {
548         var i;
549         for (i = arr.length; i--; ) {
550             if (arr[i] !== val || (!ignoreNull && val === null)) {
551                 return false;
552             }
553         }
554         return true;
555     };
556
557     // sums the numeric values in an array, ignoring other values
558     sum = function (vals) {
559         var total = 0, i;
560         for (i = vals.length; i--;) {
561             total += typeof vals[i] === 'number' ? vals[i] : 0;
562         }
563         return total;
564     };
565
566     ensureArray = function (val) {
567         return $.isArray(val) ? val : [val];
568     };
569
570     // http://paulirish.com/2008/bookmarklet-inject-new-css-rules/
571     addCSS = function(css) {
572         var tag;
573         //if ('\v' == 'v') /* ie only */ {
574         if (document.createStyleSheet) {
575             document.createStyleSheet().cssText = css;
576         } else {
577             tag = document.createElement('style');
578             tag.type = 'text/css';
579             document.getElementsByTagName('head')[0].appendChild(tag);
580             tag[(typeof document.body.style.WebkitAppearance == 'string') /* webkit only */ ? 'innerText' : 'innerHTML'] = css;
581         }
582     };
583
584     // Provide a cross-browser interface to a few simple drawing primitives
585     $.fn.simpledraw = function (width, height, useExisting, interact) {
586         var target, mhandler;
587         if (useExisting && (target = this.data('_jqs_vcanvas'))) {
588             return target;
589         }
590         if (width === undefined) {
591             width = $(this).innerWidth();
592         }
593         if (height === undefined) {
594             height = $(this).innerHeight();
595         }
596         if ($.browser.hasCanvas) {
597             target = new VCanvas_canvas(width, height, this, interact);
598         } else if ($.browser.msie) {
599             target = new VCanvas_vml(width, height, this);
600         } else {
601             return false;
602         }
603         mhandler = $(this).data('_jqs_mhandler');
604         if (mhandler) {
605             mhandler.registerCanvas(target);
606         }
607         return target;
608     };
609
610     $.fn.cleardraw = function () {
611         var target = this.data('_jqs_vcanvas');
612         if (target) {
613             target.reset();
614         }
615     };
616
617     $.RangeMapClass = RangeMap = createClass({
618         init: function (map) {
619             var key, range, rangelist = [];
620             for (key in map) {
621                 if (map.hasOwnProperty(key) && typeof key === 'string' && key.indexOf(':') > -1) {
622                     range = key.split(':');
623                     range[0] = range[0].length === 0 ? -Infinity : parseFloat(range[0]);
624                     range[1] = range[1].length === 0 ? Infinity : parseFloat(range[1]);
625                     range[2] = map[key];
626                     rangelist.push(range);
627                 }
628             }
629             this.map = map;
630             this.rangelist = rangelist || false;
631         },
632
633         get: function (value) {
634             var rangelist = this.rangelist,
635                 i, range, result;
636             if ((result = this.map[value]) !== undefined) {
637                 return result;
638             }
639             if (rangelist) {
640                 for (i = rangelist.length; i--;) {
641                     range = rangelist[i];
642                     if (range[0] <= value && range[1] >= value) {
643                         return range[2];
644                     }
645                 }
646             }
647             return undefined;
648         }
649     });
650
651     // Convenience function
652     $.range_map = function(map) {
653         return new RangeMap(map);
654     };
655
656     MouseHandler = createClass({
657         init: function (el, options) {
658             var $el = $(el);
659             this.$el = $el;
660             this.options = options;
661             this.currentPageX = 0;
662             this.currentPageY = 0;
663             this.el = el;
664             this.splist = [];
665             this.tooltip = null;
666             this.over = false;
667             this.displayTooltips = !options.get('disableTooltips');
668             this.highlightEnabled = !options.get('disableHighlight');
669         },
670
671         registerSparkline: function (sp) {
672             this.splist.push(sp);
673             if (this.over) {
674                 this.updateDisplay();
675             }
676         },
677
678         registerCanvas: function (canvas) {
679             var $canvas = $(canvas.canvas);
680             this.canvas = canvas;
681             this.$canvas = $canvas;
682             $canvas.mouseenter($.proxy(this.mouseenter, this));
683             $canvas.mouseleave($.proxy(this.mouseleave, this));
684             $canvas.click($.proxy(this.mouseclick, this));
685         },
686
687         reset: function (removeTooltip) {
688             this.splist = [];
689             if (this.tooltip && removeTooltip) {
690                 this.tooltip.remove();
691                 this.tooltip = undefined;
692             }
693         },
694
695         mouseclick: function (e) {
696             var clickEvent = $.Event('sparklineClick');
697             clickEvent.originalEvent = e;
698             clickEvent.sparklines = this.splist;
699             this.$el.trigger(clickEvent);
700         },
701
702         mouseenter: function (e) {
703             $(document.body).unbind('mousemove.jqs');
704             $(document.body).bind('mousemove.jqs', $.proxy(this.mousemove, this));
705             this.over = true;
706             this.currentPageX = e.pageX;
707             this.currentPageY = e.pageY;
708             this.currentEl = e.target;
709             if (!this.tooltip && this.displayTooltips) {
710                 this.tooltip = new Tooltip(this.options);
711                 this.tooltip.updatePosition(e.pageX, e.pageY);
712             }
713             this.updateDisplay();
714         },
715
716         mouseleave: function () {
717             $(document.body).unbind('mousemove.jqs');
718             var splist = this.splist,
719                  spcount = splist.length,
720                  needsRefresh = false,
721                  sp, i;
722             this.over = false;
723             this.currentEl = null;
724
725             if (this.tooltip) {
726                 this.tooltip.remove();
727                 this.tooltip = null;
728             }
729
730             for (i = 0; i < spcount; i++) {
731                 sp = splist[i];
732                 if (sp.clearRegionHighlight()) {
733                     needsRefresh = true;
734                 }
735             }
736
737             if (needsRefresh) {
738                 this.canvas.render();
739             }
740         },
741
742         mousemove: function (e) {
743             this.currentPageX = e.pageX;
744             this.currentPageY = e.pageY;
745             this.currentEl = e.target;
746             if (this.tooltip) {
747                 this.tooltip.updatePosition(e.pageX, e.pageY);
748             }
749             this.updateDisplay();
750         },
751
752         updateDisplay: function () {
753             var splist = this.splist,
754                  spcount = splist.length,
755                  needsRefresh = false,
756                  offset = this.$canvas.offset(),
757                  localX = this.currentPageX - offset.left,
758                  localY = this.currentPageY - offset.top,
759                  tooltiphtml, sp, i, result, changeEvent;
760             if (!this.over) {
761                 return;
762             }
763             for (i = 0; i < spcount; i++) {
764                 sp = splist[i];
765                 result = sp.setRegionHighlight(this.currentEl, localX, localY);
766                 if (result) {
767                     needsRefresh = true;
768                 }
769             }
770             if (needsRefresh) {
771                 changeEvent = $.Event('sparklineRegionChange');
772                 changeEvent.sparklines = this.splist;
773                 this.$el.trigger(changeEvent);
774                 if (this.tooltip) {
775                     tooltiphtml = '';
776                     for (i = 0; i < spcount; i++) {
777                         sp = splist[i];
778                         tooltiphtml += sp.getCurrentRegionTooltip();
779                     }
780                     this.tooltip.setContent(tooltiphtml);
781                 }
782                 if (!this.disableHighlight) {
783                     this.canvas.render();
784                 }
785             }
786             if (result === null) {
787                 this.mouseleave();
788             }
789         }
790     });
791
792
793     Tooltip = createClass({
794         sizeStyle: 'position: static !important;' +
795             'display: block !important;' +
796             'visibility: hidden !important;' +
797             'float: left !important;',
798
799         init: function (options) {
800             var tooltipClassname = options.get('tooltipClassname', 'jqstooltip'),
801                 sizetipStyle = this.sizeStyle,
802                 offset;
803             this.container = options.get('tooltipContainer') || document.body;
804             this.tooltipOffsetX = options.get('tooltipOffsetX', 10);
805             this.tooltipOffsetY = options.get('tooltipOffsetY', 12);
806             // remove any previous lingering tooltip
807             $('#jqssizetip').remove();
808             $('#jqstooltip').remove();
809             this.sizetip = $('<div/>', {
810                 id: 'jqssizetip',
811                 style: sizetipStyle,
812                 'class': tooltipClassname
813             });
814             this.tooltip = $('<div/>', {
815                 id: 'jqstooltip',
816                 'class': tooltipClassname
817             }).appendTo(this.container);
818             // account for the container's location
819             offset = this.tooltip.offset();
820             this.offsetLeft = offset.left;
821             this.offsetTop = offset.top;
822             this.hidden = true;
823             $(window).unbind('resize.jqs scroll.jqs');
824             $(window).bind('resize.jqs scroll.jqs', $.proxy(this.updateWindowDims, this));
825             this.updateWindowDims();
826         },
827
828         updateWindowDims: function () {
829             this.scrollTop = $(window).scrollTop();
830             this.scrollLeft = $(window).scrollLeft();
831             this.scrollRight = this.scrollLeft + $(window).width();
832             this.updatePosition();
833         },
834
835         getSize: function (content) {
836             this.sizetip.html(content).appendTo(this.container);
837             this.width = this.sizetip.width() + 1;
838             this.height = this.sizetip.height();
839             this.sizetip.remove();
840         },
841
842         setContent: function (content) {
843             if (!content) {
844                 this.tooltip.css('visibility', 'hidden');
845                 this.hidden = true;
846                 return;
847             }
848             this.getSize(content);
849             this.tooltip.html(content)
850                 .css({
851                     'width': this.width,
852                     'height': this.height,
853                     'visibility': 'visible'
854                 });
855             if (this.hidden) {
856                 this.hidden = false;
857                 this.updatePosition();
858             }
859         },
860
861         updatePosition: function (x, y) {
862             if (x === undefined) {
863                 if (this.mousex === undefined) {
864                     return;
865                 }
866                 x = this.mousex - this.offsetLeft;
867                 y = this.mousey - this.offsetTop;
868
869             } else {
870                 this.mousex = x = x - this.offsetLeft;
871                 this.mousey = y = y - this.offsetTop;
872             }
873             if (!this.height || !this.width || this.hidden) {
874                 return;
875             }
876
877             y -= this.height + this.tooltipOffsetY;
878             x += this.tooltipOffsetX;
879
880             if (y < this.scrollTop) {
881                 y = this.scrollTop;
882             }
883             if (x < this.scrollLeft) {
884                 x = this.scrollLeft;
885             } else if (x + this.width > this.scrollRight) {
886                 x = this.scrollRight - this.width;
887             }
888
889             this.tooltip.css({
890                 'left': x,
891                 'top': y
892             });
893         },
894
895         remove: function () {
896             this.tooltip.remove();
897             this.sizetip.remove();
898             this.sizetip = this.tooltip = undefined;
899             $(window).unbind('resize.jqs scroll.jqs');
900         }
901     });
902
903     pending = [];
904     $.fn.sparkline = function (userValues, userOptions) {
905         return this.each(function () {
906             var options = new $.fn.sparkline.options(this, userOptions),
907                  $this = $(this),
908                  render, i;
909             render = function () {
910                 var values, width, height, tmp, mhandler, sp, vals;
911                 if (userValues === 'html' || userValues === undefined) {
912                     vals = this.getAttribute(options.get('tagValuesAttribute'));
913                     if (vals === undefined || vals === null) {
914                         vals = $this.html();
915                     }
916                     values = vals.replace(/(^\s*<!--)|(-->\s*$)|\s+/g, '').split(',');
917                 } else {
918                     values = userValues;
919                 }
920
921                 width = options.get('width') === 'auto' ? values.length * options.get('defaultPixelsPerValue') : options.get('width');
922                 if (options.get('height') === 'auto') {
923                     if (!options.get('composite') || !$.data(this, '_jqs_vcanvas')) {
924                         // must be a better way to get the line height
925                         tmp = document.createElement('span');
926                         tmp.innerHTML = 'a';
927                         $this.html(tmp);
928                         height = $(tmp).innerHeight() || $(tmp).height();
929                         $(tmp).remove();
930                         tmp = null;
931                     }
932                 } else {
933                     height = options.get('height');
934                 }
935
936                 if (!options.get('disableInteraction')) {
937                     mhandler = $.data(this, '_jqs_mhandler');
938                     if (!mhandler) {
939                         mhandler = new MouseHandler(this, options);
940                         $.data(this, '_jqs_mhandler', mhandler);
941                     } else if (!options.get('composite')) {
942                         mhandler.reset();
943                     }
944                 } else {
945                     mhandler = false;
946                 }
947
948                 if (options.get('composite') && !$.data(this, '_jqs_vcanvas')) {
949                     if (!$.data(this, '_jqs_errnotify')) {
950                         alert('Attempted to attach a composite sparkline to an element with no existing sparkline');
951                         $.data(this, '_jqs_errnotify', true);
952                     }
953                     return;
954                 }
955
956                 sp = new $.fn.sparkline[options.get('type')](this, values, options, width, height);
957
958                 sp.render();
959
960                 if (mhandler) {
961                     mhandler.registerSparkline(sp);
962                 }
963             };
964             // jQuery 1.3.0 completely changed the meaning of :hidden :-/
965             if (($(this).html() && !options.get('disableHiddenCheck') && $(this).is(':hidden')) || ($.fn.jquery < '1.3.0' && $(this).parents().is(':hidden')) || !$(this).parents('body').length) {
966                 if (!options.get('composite') && $.data(this, '_jqs_pending')) {
967                     // remove any existing references to the element
968                     for (i = pending.length; i; i--) {
969                         if (pending[i - 1][0] == this) {
970                             pending.splice(i - 1, 1);
971                         }
972                     }
973                 }
974                 pending.push([this, render]);
975                 $.data(this, '_jqs_pending', true);
976             } else {
977                 render.call(this);
978             }
979         });
980     };
981
982     $.fn.sparkline.defaults = getDefaults();
983
984
985     $.sparkline_display_visible = function () {
986         var el, i, pl;
987         var done = [];
988         for (i = 0, pl = pending.length; i < pl; i++) {
989             el = pending[i][0];
990             if ($(el).is(':visible') && !$(el).parents().is(':hidden')) {
991                 pending[i][1].call(el);
992                 $.data(pending[i][0], '_jqs_pending', false);
993                 done.push(i);
994             } else if (!$(el).closest('html').length && !$.data(el, '_jqs_pending')) {
995                 // element has been inserted and removed from the DOM
996                 // If it was not yet inserted into the dom then the .data request
997                 // will return true.
998                 // removing from the dom causes the data to be removed.
999                 $.data(pending[i][0], '_jqs_pending', false);
1000                 done.push(i);
1001             }
1002         }
1003         for (i = done.length; i; i--) {
1004             pending.splice(done[i - 1], 1);
1005         }
1006     };
1007
1008
1009     /**
1010      * User option handler
1011      */
1012     $.fn.sparkline.options = createClass({
1013         init: function (tag, userOptions) {
1014             var extendedOptions, defaults, base, tagOptionType;
1015             this.userOptions = userOptions = userOptions || {};
1016             this.tag = tag;
1017             this.tagValCache = {};
1018             defaults = $.fn.sparkline.defaults;
1019             base = defaults.common;
1020             this.tagOptionsPrefix = userOptions.enableTagOptions && (userOptions.tagOptionsPrefix || base.tagOptionsPrefix);
1021
1022             tagOptionType = this.getTagSetting('type');
1023             if (tagOptionType === UNSET_OPTION) {
1024                 extendedOptions = defaults[userOptions.type || base.type];
1025             } else {
1026                 extendedOptions = defaults[tagOptionType];
1027             }
1028             this.mergedOptions = $.extend({}, base, extendedOptions, userOptions);
1029         },
1030
1031
1032         getTagSetting: function (key) {
1033             var prefix = this.tagOptionsPrefix,
1034                 val, i, pairs, keyval;
1035             if (prefix === false || prefix === undefined) {
1036                 return UNSET_OPTION;
1037             }
1038             if (this.tagValCache.hasOwnProperty(key)) {
1039                 val = this.tagValCache.key;
1040             } else {
1041                 val = this.tag.getAttribute(prefix + key);
1042                 if (val === undefined || val === null) {
1043                     val = UNSET_OPTION;
1044                 } else if (val.substr(0, 1) === '[') {
1045                     val = val.substr(1, val.length - 2).split(',');
1046                     for (i = val.length; i--;) {
1047                         val[i] = normalizeValue(val[i].replace(/(^\s*)|(\s*$)/g, ''));
1048                     }
1049                 } else if (val.substr(0, 1) === '{') {
1050                     pairs = val.substr(1, val.length - 2).split(',');
1051                     val = {};
1052                     for (i = pairs.length; i--;) {
1053                         keyval = pairs[i].split(':', 2);
1054                         val[keyval[0].replace(/(^\s*)|(\s*$)/g, '')] = normalizeValue(keyval[1].replace(/(^\s*)|(\s*$)/g, ''));
1055                     }
1056                 } else {
1057                     val = normalizeValue(val);
1058                 }
1059                 this.tagValCache.key = val;
1060             }
1061             return val;
1062         },
1063
1064         get: function (key, defaultval) {
1065             var tagOption = this.getTagSetting(key),
1066                 result;
1067             if (tagOption !== UNSET_OPTION) {
1068                 return tagOption;
1069             }
1070             return (result = this.mergedOptions[key]) === undefined ? defaultval : result;
1071         }
1072     });
1073
1074
1075     $.fn.sparkline._base = createClass({
1076         disabled: false,
1077
1078         init: function (el, values, options, width, height) {
1079             this.el = el;
1080             this.$el = $(el);
1081             this.values = values;
1082             this.options = options;
1083             this.width = width;
1084             this.height = height;
1085             this.currentRegion = undefined;
1086         },
1087
1088         /**
1089          * Setup the canvas
1090          */
1091         initTarget: function () {
1092             var interactive = !this.options.get('disableInteraction');
1093             if (!(this.target = this.$el.simpledraw(this.width, this.height, this.options.get('composite'), interactive))) {
1094                 this.disabled = true;
1095             } else {
1096                 this.canvasWidth = this.target.pixelWidth;
1097                 this.canvasHeight = this.target.pixelHeight;
1098             }
1099         },
1100
1101         /**
1102          * Actually render the chart to the canvas
1103          */
1104         render: function () {
1105             if (this.disabled) {
1106                 this.el.innerHTML = '';
1107                 return false;
1108             }
1109             return true;
1110         },
1111
1112         /**
1113          * Return a region id for a given x/y co-ordinate
1114          */
1115         getRegion: function (x, y) {
1116         },
1117
1118         /**
1119          * Highlight an item based on the moused-over x,y co-ordinate
1120          */
1121         setRegionHighlight: function (el, x, y) {
1122             var currentRegion = this.currentRegion,
1123                 highlightEnabled = !this.options.get('disableHighlight'),
1124                 newRegion;
1125             if (x > this.canvasWidth || y > this.canvasHeight || x < 0 || y < 0) {
1126                 return null;
1127             }
1128             newRegion = this.getRegion(el, x, y);
1129             if (currentRegion !== newRegion) {
1130                 if (currentRegion !== undefined && highlightEnabled) {
1131                     this.removeHighlight();
1132                 }
1133                 this.currentRegion = newRegion;
1134                 if (newRegion !== undefined && highlightEnabled) {
1135                     this.renderHighlight();
1136                 }
1137                 return true;
1138             }
1139             return false;
1140         },
1141
1142         /**
1143          * Reset any currently highlighted item
1144          */
1145         clearRegionHighlight: function () {
1146             if (this.currentRegion !== undefined) {
1147                 this.removeHighlight();
1148                 this.currentRegion = undefined;
1149                 return true;
1150             }
1151             return false;
1152         },
1153
1154         renderHighlight: function () {
1155             this.changeHighlight(true);
1156         },
1157
1158         removeHighlight: function () {
1159             this.changeHighlight(false);
1160         },
1161
1162         changeHighlight: function (highlight)  {},
1163
1164         /**
1165          * Fetch the HTML to display as a tooltip
1166          */
1167         getCurrentRegionTooltip: function () {
1168             var options = this.options,
1169                 header = '',
1170                 entries = [],
1171                 fields, formats, formatlen, fclass, text, i,
1172                 showFields, showFieldsKey, newFields, fv,
1173                 formatter, format, fieldlen, j;
1174             if (this.currentRegion === undefined) {
1175                 return '';
1176             }
1177             fields = this.getCurrentRegionFields();
1178             formatter = options.get('tooltipFormatter');
1179             if (formatter) {
1180                 return formatter(this, options, fields);
1181             }
1182             if (options.get('tooltipChartTitle')) {
1183                 header += '<div class="jqs jqstitle">' + options.get('tooltipChartTitle') + '</div>\n';
1184             }
1185             formats = this.options.get('tooltipFormat');
1186             if (!formats) {
1187                 return '';
1188             }
1189             if (!$.isArray(formats)) {
1190                 formats = [formats];
1191             }
1192             if (!$.isArray(fields)) {
1193                 fields = [fields];
1194             }
1195             showFields = this.options.get('tooltipFormatFieldlist');
1196             showFieldsKey = this.options.get('tooltipFormatFieldlistKey');
1197             if (showFields && showFieldsKey) {
1198                 // user-selected ordering of fields
1199                 newFields = [];
1200                 for (i = fields.length; i--;) {
1201                     fv = fields[i][showFieldsKey];
1202                     if ((j = $.inArray(fv, showFields)) != -1) {
1203                         newFields[j] = fields[i];
1204                     }
1205                 }
1206                 fields = newFields;
1207             }
1208             formatlen = formats.length;
1209             fieldlen = fields.length;
1210             for (i = 0; i < formatlen; i++) {
1211                 format = formats[i];
1212                 if (typeof format === 'string') {
1213                     format = new SPFormat(format);
1214                 }
1215                 fclass = format.fclass || 'jqsfield';
1216                 for (j = 0; j < fieldlen; j++) {
1217                     if (!fields[j].isNull || !options.get('tooltipSkipNull')) {
1218                         $.extend(fields[j], {
1219                             prefix: options.get('tooltipPrefix'),
1220                             suffix: options.get('tooltipSuffix')
1221                         });
1222                         text = format.render(fields[j], options.get('tooltipValueLookups'), options);
1223                         entries.push('<div class="' + fclass + '">' + text + '</div>');
1224                     }
1225                 }
1226             }
1227             if (entries.length) {
1228                 return header + entries.join('\n');
1229             }
1230             return '';
1231         },
1232
1233         getCurrentRegionFields: function () {},
1234
1235         calcHighlightColor: function (color, options) {
1236             var highlightColor = options.get('highlightColor'),
1237                 lighten = options.get('highlightLighten'),
1238                 parse, mult, rgbnew, i;
1239             if (highlightColor) {
1240                 return highlightColor;
1241             }
1242             if (lighten) {
1243                 // extract RGB values
1244                 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);
1245                 if (parse) {
1246                     rgbnew = [];
1247                     mult = color.length === 4 ? 16 : 1;
1248                     for (i = 0; i < 3; i++) {
1249                         rgbnew[i] = clipval(Math.round(parseInt(parse[i + 1], 16) * mult * lighten), 0, 255);
1250                     }
1251                     return 'rgb(' + rgbnew.join(',') + ')';
1252                 }
1253
1254             }
1255             return color;
1256         }
1257
1258     });
1259
1260     barHighlightMixin = {
1261         changeHighlight: function (highlight) {
1262             var currentRegion = this.currentRegion,
1263                 target = this.target,
1264                 shapeids = this.regionShapes[currentRegion],
1265                 newShapes;
1266             // will be null if the region value was null
1267             if (shapeids) {
1268                 newShapes = this.renderRegion(currentRegion, highlight);
1269                 if ($.isArray(newShapes) || $.isArray(shapeids)) {
1270                     target.replaceWithShapes(shapeids, newShapes);
1271                     this.regionShapes[currentRegion] = $.map(newShapes, function (newShape) {
1272                         return newShape.id;
1273                     });
1274                 } else {
1275                     target.replaceWithShape(shapeids, newShapes);
1276                     this.regionShapes[currentRegion] = newShapes.id;
1277                 }
1278             }
1279         },
1280
1281         render: function () {
1282             var values = this.values,
1283                 target = this.target,
1284                 regionShapes = this.regionShapes,
1285                 shapes, ids, i, j;
1286
1287             if (!this.cls._super.render.call(this)) {
1288                 return;
1289             }
1290             for (i = values.length; i--;) {
1291                 shapes = this.renderRegion(i);
1292                 if (shapes) {
1293                     if ($.isArray(shapes)) {
1294                         ids = [];
1295                         for (j = shapes.length; j--;) {
1296                             shapes[j].append();
1297                             ids.push(shapes[j].id);
1298                         }
1299                         regionShapes[i] = ids;
1300                     } else {
1301                         shapes.append();
1302                         regionShapes[i] = shapes.id; // store just the shapeid
1303                     }
1304                 } else {
1305                     // null value
1306                     regionShapes[i] = null;
1307                 }
1308             }
1309             target.render();
1310         }
1311     };
1312
1313     /**
1314      * Line charts
1315      */
1316     $.fn.sparkline.line = line = createClass($.fn.sparkline._base, {
1317         type: 'line',
1318
1319         init: function (el, values, options, width, height) {
1320             line._super.init.call(this, el, values, options, width, height);
1321             this.vertices = [];
1322             this.regionMap = [];
1323             this.xvalues = [];
1324             this.yvalues = [];
1325             this.yminmax = [];
1326             this.hightlightSpotId = null;
1327             this.lastShapeId = null;
1328             this.initTarget();
1329         },
1330
1331         getRegion: function (el, x, y) {
1332             var i,
1333                 regionMap = this.regionMap; // maps regions to value positions
1334             for (i = regionMap.length; i--;) {
1335                 if (regionMap[i] !== null && x >= regionMap[i][0] && x <= regionMap[i][1]) {
1336                     return regionMap[i][2];
1337                 }
1338             }
1339             return undefined;
1340         },
1341
1342         getCurrentRegionFields: function () {
1343             var currentRegion = this.currentRegion;
1344             return {
1345                 isNull: this.yvalues[currentRegion] === null,
1346                 x: this.xvalues[currentRegion],
1347                 y: this.yvalues[currentRegion],
1348                 color: this.options.get('lineColor'),
1349                 fillColor: this.options.get('fillColor'),
1350                 offset: currentRegion
1351             };
1352         },
1353
1354         renderHighlight: function () {
1355             var currentRegion = this.currentRegion,
1356                 target = this.target,
1357                 vertex = this.vertices[currentRegion],
1358                 options = this.options,
1359                 spotRadius = options.get('spotRadius'),
1360                 highlightSpotColor = options.get('highlightSpotColor'),
1361                 highlightLineColor = options.get('highlightLineColor'),
1362                 highlightSpot, highlightLine;
1363
1364             if (!vertex) {
1365                 return;
1366             }
1367             if (spotRadius && highlightSpotColor) {
1368                 highlightSpot = target.drawCircle(vertex[0], vertex[1],
1369                     spotRadius, undefined, highlightSpotColor);
1370                 this.highlightSpotId = highlightSpot.id;
1371                 target.insertAfterShape(this.lastShapeId, highlightSpot);
1372             }
1373             if (highlightLineColor) {
1374                 highlightLine = target.drawLine(vertex[0], this.canvasTop, vertex[0],
1375                     this.canvasTop + this.canvasHeight, highlightLineColor);
1376                 this.highlightLineId = highlightLine.id;
1377                 target.insertAfterShape(this.lastShapeId, highlightLine);
1378             }
1379         },
1380
1381         removeHighlight: function () {
1382             var target = this.target;
1383             if (this.highlightSpotId) {
1384                 target.removeShapeId(this.highlightSpotId);
1385                 this.highlightSpotId = null;
1386             }
1387             if (this.highlightLineId) {
1388                 target.removeShapeId(this.highlightLineId);
1389                 this.highlightLineId = null;
1390             }
1391         },
1392
1393         scanValues: function () {
1394             var values = this.values,
1395                 valcount = values.length,
1396                 xvalues = this.xvalues,
1397                 yvalues = this.yvalues,
1398                 yminmax = this.yminmax,
1399                 i, val, isStr, isArray, sp;
1400             for (i = 0; i < valcount; i++) {
1401                 val = values[i];
1402                 isStr = typeof(values[i]) === 'string';
1403                 isArray = typeof(values[i]) === 'object' && values[i] instanceof Array;
1404                 sp = isStr && values[i].split(':');
1405                 if (isStr && sp.length === 2) { // x:y
1406                     xvalues.push(Number(sp[0]));
1407                     yvalues.push(Number(sp[1]));
1408                     yminmax.push(Number(sp[1]));
1409                 } else if (isArray) {
1410                     xvalues.push(val[0]);
1411                     yvalues.push(val[1]);
1412                     yminmax.push(val[1]);
1413                 } else {
1414                     xvalues.push(i);
1415                     if (values[i] === null || values[i] === 'null') {
1416                         yvalues.push(null);
1417                     } else {
1418                         yvalues.push(Number(val));
1419                         yminmax.push(Number(val));
1420                     }
1421                 }
1422             }
1423             if (this.options.get('xvalues')) {
1424                 xvalues = this.options.get('xvalues');
1425             }
1426
1427             this.maxy = this.maxyorg = Math.max.apply(Math, yminmax);
1428             this.miny = this.minyorg = Math.min.apply(Math, yminmax);
1429
1430             this.maxx = Math.max.apply(Math, xvalues);
1431             this.minx = Math.min.apply(Math, xvalues);
1432
1433             this.xvalues = xvalues;
1434             this.yvalues = yvalues;
1435             this.yminmax = yminmax;
1436
1437         },
1438
1439         processRangeOptions: function () {
1440             var options = this.options,
1441                 normalRangeMin = options.get('normalRangeMin'),
1442                 normalRangeMax = options.get('normalRangeMax');
1443
1444             if (normalRangeMin !== undefined) {
1445                 if (normalRangeMin < this.miny) {
1446                     this.miny = normalRangeMin;
1447                 }
1448                 if (normalRangeMax > this.maxy) {
1449                     this.maxy = normalRangeMax;
1450                 }
1451             }
1452             if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.miny)) {
1453                 this.miny = options.get('chartRangeMin');
1454             }
1455             if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.maxy)) {
1456                 this.maxy = options.get('chartRangeMax');
1457             }
1458             if (options.get('chartRangeMinX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMinX') < this.minx)) {
1459                 this.minx = options.get('chartRangeMinX');
1460             }
1461             if (options.get('chartRangeMaxX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMaxX') > this.maxx)) {
1462                 this.maxx = options.get('chartRangeMaxX');
1463             }
1464
1465         },
1466
1467         drawNormalRange: function (canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey) {
1468             var normalRangeMin = this.options.get('normalRangeMin'),
1469                 normalRangeMax = this.options.get('normalRangeMax'),
1470                 ytop = canvasTop + Math.round(canvasHeight - (canvasHeight * ((normalRangeMax - this.miny) / rangey))),
1471                 height = Math.round((canvasHeight * (normalRangeMax - normalRangeMin)) / rangey);
1472             this.target.drawRect(canvasLeft, ytop, canvasWidth, height, undefined, this.options.get('normalRangeColor')).append();
1473         },
1474
1475         render: function () {
1476             var options = this.options,
1477                 target = this.target,
1478                 canvasWidth = this.canvasWidth,
1479                 canvasHeight = this.canvasHeight,
1480                 vertices = this.vertices,
1481                 spotRadius = options.get('spotRadius'),
1482                 regionMap = this.regionMap,
1483                 rangex, rangey, yvallast,
1484                 canvasTop, canvasLeft,
1485                 vertex, path, paths, x, y, xnext, xpos, xposnext,
1486                 last, next, yvalcount, lineShapes, fillShapes, plen,
1487                 valueSpots, color, xvalues, yvalues, i;
1488
1489             if (!line._super.render.call(this)) {
1490                 return;
1491             }
1492
1493             this.scanValues();
1494             this.processRangeOptions();
1495
1496             xvalues = this.xvalues;
1497             yvalues = this.yvalues;
1498
1499             if (!this.yminmax.length || this.yvalues.length < 2) {
1500                 // empty or all null valuess
1501                 return;
1502             }
1503
1504             canvasTop = canvasLeft = 0;
1505
1506             rangex = this.maxx - this.minx === 0 ? 1 : this.maxx - this.minx;
1507             rangey = this.maxy - this.miny === 0 ? 1 : this.maxy - this.miny;
1508             yvallast = this.yvalues.length - 1;
1509
1510             if (spotRadius && (canvasWidth < (spotRadius * 4) || canvasHeight < (spotRadius * 4))) {
1511                 spotRadius = 0;
1512             }
1513             if (spotRadius) {
1514                 // adjust the canvas size as required so that spots will fit
1515                 if (options.get('minSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.miny)) {
1516                     canvasHeight -= Math.ceil(spotRadius);
1517                 }
1518                 if (options.get('maxSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.maxy)) {
1519                     canvasHeight -= Math.ceil(spotRadius);
1520                     canvasTop += Math.ceil(spotRadius);
1521                 }
1522                 if ((options.get('minSpotColor') || options.get('maxSpotColor')) && (yvalues[0] === this.miny || yvalues[0] === this.maxy)) {
1523                     canvasLeft += Math.ceil(spotRadius);
1524                     canvasWidth -= Math.ceil(spotRadius);
1525                 }
1526                 if (options.get('spotColor') ||
1527                     (options.get('minSpotColor') || options.get('maxSpotColor') &&
1528                         (yvalues[yvallast] === this.miny || yvalues[yvallast] === this.maxy))) {
1529                     canvasWidth -= Math.ceil(spotRadius);
1530                 }
1531             }
1532
1533
1534             canvasHeight--;
1535
1536             if (options.get('normalRangeMin') && !options.get('drawNormalOnTop')) {
1537                 this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey);
1538             }
1539
1540             path = [];
1541             paths = [path];
1542             last = next = null;
1543             yvalcount = yvalues.length;
1544             for (i = 0; i < yvalcount; i++) {
1545                 x = xvalues[i];
1546                 xnext = xvalues[i + 1];
1547                 y = yvalues[i];
1548                 xpos = canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex));
1549                 xposnext = i < yvalcount - 1 ? canvasLeft + Math.round((xnext - this.minx) * (canvasWidth / rangex)) : canvasWidth;
1550                 next = xpos + ((xposnext - xpos) / 2);
1551                 regionMap[i] = [last || 0, next, i];
1552                 last = next;
1553                 if (y === null) {
1554                     if (i) {
1555                         if (yvalues[i - 1] !== null) {
1556                             path = [];
1557                             paths.push(path);
1558                             vertices.push(null);
1559                         }
1560                     }
1561                 } else {
1562                     if (y < this.miny) {
1563                         y = this.miny;
1564                     }
1565                     if (y > this.maxy) {
1566                         y = this.maxy;
1567                     }
1568                     if (!path.length) {
1569                         // previous value was null
1570                         path.push([xpos, canvasTop + canvasHeight]);
1571                     }
1572                     vertex = [xpos, canvasTop + Math.round(canvasHeight - (canvasHeight * ((y - this.miny) / rangey)))];
1573                     path.push(vertex);
1574                     vertices.push(vertex);
1575                 }
1576             }
1577
1578             lineShapes = [];
1579             fillShapes = [];
1580             plen = paths.length;
1581             for (i = 0; i < plen; i++) {
1582                 path = paths[i];
1583                 if (path.length) {
1584                     if (options.get('fillColor')) {
1585                         path.push([path[path.length - 1][0], (canvasTop + canvasHeight)]);
1586                         fillShapes.push(path.slice(0));
1587                         path.pop();
1588                     }
1589                     // if there's only a single point in this path, then we want to display it
1590                     // as a vertical line which means we keep path[0]  as is
1591                     if (path.length > 2) {
1592                         // else we want the first value
1593                         path[0] = [path[0][0], path[1][1]];
1594                     }
1595                     lineShapes.push(path);
1596                 }
1597             }
1598
1599             // draw the fill first, then optionally the normal range, then the line on top of that
1600             plen = fillShapes.length;
1601             for (i = 0; i < plen; i++) {
1602                 target.drawShape(fillShapes[i],
1603                     options.get('fillColor'), options.get('fillColor')).append();
1604             }
1605
1606             if (options.get('normalRangeMin') && options.get('drawNormalOnTop')) {
1607                 this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey);
1608             }
1609
1610             plen = lineShapes.length;
1611             for (i = 0; i < plen; i++) {
1612                 target.drawShape(lineShapes[i], options.get('lineColor'), undefined,
1613                     options.get('lineWidth')).append();
1614             }
1615
1616             if (spotRadius && options.get('valueSpots')) {
1617                 valueSpots = options.get('valueSpots');
1618                 if (valueSpots.get === undefined) {
1619                     valueSpots = new RangeMap(valueSpots);
1620                 }
1621                 for (i = 0; i < yvalcount; i++) {
1622                     color = valueSpots.get(yvalues[i]);
1623                     if (color) {
1624                         target.drawCircle(canvasLeft + Math.round((xvalues[i] - this.minx) * (canvasWidth / rangex)),
1625                             canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[i] - this.miny) / rangey))),
1626                             spotRadius, undefined,
1627                             color).append();
1628                     }
1629                 }
1630
1631             }
1632             if (spotRadius && options.get('spotColor')) {
1633                 target.drawCircle(canvasLeft + Math.round((xvalues[xvalues.length - 1] - this.minx) * (canvasWidth / rangex)),
1634                     canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[yvallast] - this.miny) / rangey))),
1635                     spotRadius, undefined,
1636                     options.get('spotColor')).append();
1637             }
1638             if (this.maxy !== this.minyorg) {
1639                 if (spotRadius && options.get('minSpotColor')) {
1640                     x = xvalues[$.inArray(this.minyorg, yvalues)];
1641                     target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)),
1642                         canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.minyorg - this.miny) / rangey))),
1643                         spotRadius, undefined,
1644                         options.get('minSpotColor')).append();
1645                 }
1646                 if (spotRadius && options.get('maxSpotColor')) {
1647                     x = xvalues[$.inArray(this.maxyorg, yvalues)];
1648                     target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)),
1649                         canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.maxyorg - this.miny) / rangey))),
1650                         spotRadius, undefined,
1651                         options.get('maxSpotColor')).append();
1652                 }
1653             }
1654
1655             this.lastShapeId = target.getLastShapeId();
1656             this.canvasTop = canvasTop;
1657             target.render();
1658         }
1659     });
1660
1661     /**
1662      * Bar charts
1663      */
1664     $.fn.sparkline.bar = bar = createClass($.fn.sparkline._base, barHighlightMixin, {
1665         type: 'bar',
1666
1667         init: function (el, values, options, width, height) {
1668             var barWidth = parseInt(options.get('barWidth'), 10),
1669                 barSpacing = parseInt(options.get('barSpacing'), 10),
1670                 chartRangeMin = options.get('chartRangeMin'),
1671                 chartRangeMax = options.get('chartRangeMax'),
1672                 chartRangeClip = options.get('chartRangeClip'),
1673                 stackMin = Infinity,
1674                 stackMax = -Infinity,
1675                 isStackString, groupMin, groupMax, stackRanges,
1676                 numValues, i, vlen, range, zeroAxis, xaxisOffset, min, max, clipMin, clipMax,
1677                 stacked, vlist, j, slen, svals, val, yoffset, yMaxCalc, canvasHeightEf;
1678             bar._super.init.call(this, el, values, options, width, height);
1679
1680             // scan values to determine whether to stack bars
1681             for (i = 0, vlen = values.length; i < vlen; i++) {
1682                 val = values[i];
1683                 isStackString = typeof(val) === 'string' && val.indexOf(':') > -1;
1684                 if (isStackString || $.isArray(val)) {
1685                     stacked = true;
1686                     if (isStackString) {
1687                         val = values[i] = normalizeValues(val.split(':'));
1688                     }
1689                     val = remove(val, null); // min/max will treat null as zero
1690                     groupMin = Math.min.apply(Math, val);
1691                     groupMax = Math.max.apply(Math, val);
1692                     if (groupMin < stackMin) {
1693                         stackMin = groupMin;
1694                     }
1695                     if (groupMax > stackMax) {
1696                         stackMax = groupMax;
1697                     }
1698                 }
1699             }
1700
1701             this.stacked = stacked;
1702             this.regionShapes = {};
1703             this.barWidth = barWidth;
1704             this.barSpacing = barSpacing;
1705             this.totalBarWidth = barWidth + barSpacing;
1706             this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing);
1707
1708             this.initTarget();
1709
1710             if (chartRangeClip) {
1711                 clipMin = chartRangeMin === undefined ? -Infinity : chartRangeMin;
1712                 clipMax = chartRangeMax === undefined ? Infinity : chartRangeMax;
1713             }
1714
1715             numValues = [];
1716             stackRanges = stacked ? [] : numValues;
1717             var stackTotals = [];
1718             var stackRangesNeg = [];
1719             for (i = 0, vlen = values.length; i < vlen; i++) {
1720                 if (stacked) {
1721                     vlist = values[i];
1722                     values[i] = svals = [];
1723                     stackTotals[i] = 0;
1724                     stackRanges[i] = stackRangesNeg[i] = 0;
1725                     for (j = 0, slen = vlist.length; j < slen; j++) {
1726                         val = svals[j] = chartRangeClip ? clipval(vlist[j], clipMin, clipMax) : vlist[j];
1727                         if (val !== null) {
1728                             if (val > 0) {
1729                                 stackTotals[i] += val;
1730                             }
1731                             if (stackMin < 0 && stackMax > 0) {
1732                                 if (val < 0) {
1733                                     stackRangesNeg[i] += Math.abs(val);
1734                                 } else {
1735                                     stackRanges[i] += val;
1736                                 }
1737                             } else {
1738                                 stackRanges[i] += Math.abs(val - (val < 0 ? stackMax : stackMin));
1739                             }
1740                             numValues.push(val);
1741                         }
1742                     }
1743                 } else {
1744                     val = chartRangeClip ? clipval(values[i], clipMin, clipMax) : values[i];
1745                     val = values[i] = normalizeValue(val);
1746                     if (val !== null) {
1747                         numValues.push(val);
1748                     }
1749                 }
1750             }
1751             this.max = max = Math.max.apply(Math, numValues);
1752             this.min = min = Math.min.apply(Math, numValues);
1753             this.stackMax = stackMax = stacked ? Math.max.apply(Math, stackTotals) : max;
1754             this.stackMin = stackMin = stacked ? Math.min.apply(Math, numValues) : min;
1755
1756             if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < min)) {
1757                 min = options.get('chartRangeMin');
1758             }
1759             if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > max)) {
1760                 max = options.get('chartRangeMax');
1761             }
1762
1763             this.zeroAxis = zeroAxis = options.get('zeroAxis', true);
1764             if (min <= 0 && max >= 0 && zeroAxis) {
1765                 xaxisOffset = 0;
1766             } else if (zeroAxis == false) {
1767                 xaxisOffset = min;
1768             } else if (min > 0) {
1769                 xaxisOffset = min;
1770             } else {
1771                 xaxisOffset = max;
1772             }
1773             this.xaxisOffset = xaxisOffset;
1774
1775             range = stacked ? (Math.max.apply(Math, stackRanges) + Math.max.apply(Math, stackRangesNeg)) : max - min;
1776
1777             // as we plot zero/min values a single pixel line, we add a pixel to all other
1778             // values - Reduce the effective canvas size to suit
1779             this.canvasHeightEf = (zeroAxis && min < 0) ? this.canvasHeight - 2 : this.canvasHeight - 1;
1780
1781             if (min < xaxisOffset) {
1782                 yMaxCalc = (stacked && max >= 0) ? stackMax : max;
1783                 yoffset = (yMaxCalc - xaxisOffset) / range * this.canvasHeight;
1784                 if (yoffset !== Math.ceil(yoffset)) {
1785                     this.canvasHeightEf -= 2;
1786                     yoffset = Math.ceil(yoffset);
1787                 }
1788             } else {
1789                 yoffset = this.canvasHeight;
1790             }
1791             this.yoffset = yoffset;
1792
1793             if ($.isArray(options.get('colorMap'))) {
1794                 this.colorMapByIndex = options.get('colorMap');
1795                 this.colorMapByValue = null;
1796             } else {
1797                 this.colorMapByIndex = null;
1798                 this.colorMapByValue = options.get('colorMap');
1799                 if (this.colorMapByValue && this.colorMapByValue.get === undefined) {
1800                     this.colorMapByValue = new RangeMap(this.colorMapByValue);
1801                 }
1802             }
1803
1804             this.range = range;
1805         },
1806
1807         getRegion: function (el, x, y) {
1808             var result = Math.floor(x / this.totalBarWidth);
1809             return (result < 0 || result >= this.values.length) ? undefined : result;
1810         },
1811
1812         getCurrentRegionFields: function () {
1813             var currentRegion = this.currentRegion,
1814                 values = ensureArray(this.values[currentRegion]),
1815                 result = [],
1816                 value, i;
1817             for (i = values.length; i--;) {
1818                 value = values[i];
1819                 result.push({
1820                     isNull: value === null,
1821                     value: value,
1822                     color: this.calcColor(i, value, currentRegion),
1823                     offset: currentRegion
1824                 });
1825             }
1826             return result;
1827         },
1828
1829         calcColor: function (stacknum, value, valuenum) {
1830             var colorMapByIndex = this.colorMapByIndex,
1831                 colorMapByValue = this.colorMapByValue,
1832                 options = this.options,
1833                 color, newColor;
1834             if (this.stacked) {
1835                 color = options.get('stackedBarColor');
1836             } else {
1837                 color = (value < 0) ? options.get('negBarColor') : options.get('barColor');
1838             }
1839             if (value === 0 && options.get('zeroColor') !== undefined) {
1840                 color = options.get('zeroColor');
1841             }
1842             if (colorMapByValue && (newColor = colorMapByValue.get(value))) {
1843                 color = newColor;
1844             } else if (colorMapByIndex && colorMapByIndex.length > valuenum) {
1845                 color = colorMapByIndex[valuenum];
1846             }
1847             return $.isArray(color) ? color[stacknum % color.length] : color;
1848         },
1849
1850         /**
1851          * Render bar(s) for a region
1852          */
1853         renderRegion: function (valuenum, highlight) {
1854             var vals = this.values[valuenum],
1855                 options = this.options,
1856                 xaxisOffset = this.xaxisOffset,
1857                 result = [],
1858                 range = this.range,
1859                 stacked = this.stacked,
1860                 target = this.target,
1861                 x = valuenum * this.totalBarWidth,
1862                 canvasHeightEf = this.canvasHeightEf,
1863                 yoffset = this.yoffset,
1864                 y, height, color, isNull, yoffsetNeg, i, valcount, val, minPlotted, allMin;
1865
1866             vals = $.isArray(vals) ? vals : [vals];
1867             valcount = vals.length;
1868             val = vals[0];
1869             isNull = all(null, vals);
1870             allMin = all(xaxisOffset, vals, true);
1871
1872             if (isNull) {
1873                 if (options.get('nullColor')) {
1874                     color = highlight ? options.get('nullColor') : this.calcHighlightColor(options.get('nullColor'), options);
1875                     y = (yoffset > 0) ? yoffset - 1 : yoffset;
1876                     return target.drawRect(x, y, this.barWidth - 1, 0, color, color);
1877                 } else {
1878                     return undefined;
1879                 }
1880             }
1881             yoffsetNeg = yoffset;
1882             for (i = 0; i < valcount; i++) {
1883                 val = vals[i];
1884
1885                 if (stacked && val === xaxisOffset) {
1886                     if (!allMin || minPlotted) {
1887                         continue;
1888                     }
1889                     minPlotted = true;
1890                 }
1891
1892                 if (range > 0) {
1893                     height = Math.floor(canvasHeightEf * ((Math.abs(val - xaxisOffset) / range))) + 1;
1894                 } else {
1895                     height = 1;
1896                 }
1897                 if (val < xaxisOffset || (val === xaxisOffset && yoffset === 0)) {
1898                     y = yoffsetNeg;
1899                     yoffsetNeg += height;
1900                 } else {
1901                     y = yoffset - height;
1902                     yoffset -= height;
1903                 }
1904                 color = this.calcColor(i, val, valuenum);
1905                 if (highlight) {
1906                     color = this.calcHighlightColor(color, options);
1907                 }
1908                 result.push(target.drawRect(x, y, this.barWidth - 1, height - 1, color, color));
1909             }
1910             if (result.length === 1) {
1911                 return result[0];
1912             }
1913             return result;
1914         }
1915     });
1916
1917     /**
1918      * Tristate charts
1919      */
1920     $.fn.sparkline.tristate = tristate = createClass($.fn.sparkline._base, barHighlightMixin, {
1921         type: 'tristate',
1922
1923         init: function (el, values, options, width, height) {
1924             var barWidth = parseInt(options.get('barWidth'), 10),
1925                 barSpacing = parseInt(options.get('barSpacing'), 10);
1926             tristate._super.init.call(this, el, values, options, width, height);
1927
1928             this.regionShapes = {};
1929             this.barWidth = barWidth;
1930             this.barSpacing = barSpacing;
1931             this.totalBarWidth = barWidth + barSpacing;
1932             this.values = $.map(values, Number);
1933             this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing);
1934
1935             if ($.isArray(options.get('colorMap'))) {
1936                 this.colorMapByIndex = options.get('colorMap');
1937                 this.colorMapByValue = null;
1938             } else {
1939                 this.colorMapByIndex = null;
1940                 this.colorMapByValue = options.get('colorMap');
1941                 if (this.colorMapByValue && this.colorMapByValue.get === undefined) {
1942                     this.colorMapByValue = new RangeMap(this.colorMapByValue);
1943                 }
1944             }
1945             this.initTarget();
1946         },
1947
1948         getRegion: function (el, x, y) {
1949             return Math.floor(x / this.totalBarWidth);
1950         },
1951
1952         getCurrentRegionFields: function () {
1953             var currentRegion = this.currentRegion;
1954             return {
1955                 isNull: this.values[currentRegion] === undefined,
1956                 value: this.values[currentRegion],
1957                 color: this.calcColor(this.values[currentRegion], currentRegion),
1958                 offset: currentRegion
1959             };
1960         },
1961
1962         calcColor: function (value, valuenum) {
1963             var values = this.values,
1964                 options = this.options,
1965                 colorMapByIndex = this.colorMapByIndex,
1966                 colorMapByValue = this.colorMapByValue,
1967                 color, newColor;
1968
1969             if (colorMapByValue && (newColor = colorMapByValue.get(value))) {
1970                 color = newColor;
1971             } else if (colorMapByIndex && colorMapByIndex.length > valuenum) {
1972                 color = colorMapByIndex[valuenum];
1973             } else if (values[valuenum] < 0) {
1974                 color = options.get('negBarColor');
1975             } else if (values[valuenum] > 0) {
1976                 color = options.get('posBarColor');
1977             } else {
1978                 color = options.get('zeroBarColor');
1979             }
1980             return color;
1981         },
1982
1983         renderRegion: function (valuenum, highlight) {
1984             var values = this.values,
1985                 options = this.options,
1986                 target = this.target,
1987                 canvasHeight, height, halfHeight,
1988                 x, y, color;
1989
1990             canvasHeight = target.pixelHeight;
1991             halfHeight = Math.round(canvasHeight / 2);
1992
1993             x = valuenum * this.totalBarWidth;
1994             if (values[valuenum] < 0) {
1995                 y = halfHeight;
1996                 height = halfHeight - 1;
1997             } else if (values[valuenum] > 0) {
1998                 y = 0;
1999                 height = halfHeight - 1;
2000             } else {
2001                 y = halfHeight - 1;
2002                 height = 2;
2003             }
2004             color = this.calcColor(values[valuenum], valuenum);
2005             if (color === null) {
2006                 return;
2007             }
2008             if (highlight) {
2009                 color = this.calcHighlightColor(color, options);
2010             }
2011             return target.drawRect(x, y, this.barWidth - 1, height - 1, color, color);
2012         }
2013     });
2014
2015     /**
2016      * Discrete charts
2017      */
2018     $.fn.sparkline.discrete = discrete = createClass($.fn.sparkline._base, barHighlightMixin, {
2019         type: 'discrete',
2020
2021         init: function (el, values, options, width, height) {
2022             discrete._super.init.call(this, el, values, options, width, height);
2023
2024             this.regionShapes = {};
2025             this.values = values = $.map(values, Number);
2026             this.min = Math.min.apply(Math, values);
2027             this.max = Math.max.apply(Math, values);
2028             this.range = this.max - this.min;
2029             this.width = width = options.get('width') === 'auto' ? values.length * 2 : this.width;
2030             this.interval = Math.floor(width / values.length);
2031             this.itemWidth = width / values.length;
2032             if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.min)) {
2033                 this.min = options.get('chartRangeMin');
2034             }
2035             if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.max)) {
2036                 this.max = options.get('chartRangeMax');
2037             }
2038             this.initTarget();
2039             if (this.target) {
2040                 this.lineHeight = options.get('lineHeight') === 'auto' ? Math.round(this.canvasHeight * 0.3) : options.get('lineHeight');
2041             }
2042         },
2043
2044         getRegion: function (el, x, y) {
2045             return Math.floor(x / this.itemWidth);
2046         },
2047
2048         getCurrentRegionFields: function () {
2049             var currentRegion = this.currentRegion;
2050             return {
2051                 isNull: this.values[currentRegion] === undefined,
2052                 value: this.values[currentRegion],
2053                 offset: currentRegion
2054             };
2055         },
2056
2057         renderRegion: function (valuenum, highlight) {
2058             var values = this.values,
2059                 options = this.options,
2060                 min = this.min,
2061                 max = this.max,
2062                 range = this.range,
2063                 interval = this.interval,
2064                 target = this.target,
2065                 canvasHeight = this.canvasHeight,
2066                 lineHeight = this.lineHeight,
2067                 pheight = canvasHeight - lineHeight,
2068                 ytop, val, color, x;
2069
2070             val = clipval(values[valuenum], min, max);
2071             x = valuenum * interval;
2072             ytop = Math.round(pheight - pheight * ((val - min) / range));
2073             color = (options.get('thresholdColor') && val < options.get('thresholdValue')) ? options.get('thresholdColor') : options.get('lineColor');
2074             if (highlight) {
2075                 color = this.calcHighlightColor(color, options);
2076             }
2077             return target.drawLine(x, ytop, x, ytop + lineHeight, color);
2078         }
2079     });
2080
2081     /**
2082      * Bullet charts
2083      */
2084     $.fn.sparkline.bullet = bullet = createClass($.fn.sparkline._base, {
2085         type: 'bullet',
2086
2087         init: function (el, values, options, width, height) {
2088             var min, max;
2089             bullet._super.init.call(this, el, values, options, width, height);
2090
2091             // values: target, performance, range1, range2, range3
2092             values = $.map(values, Number);
2093             min = Math.min.apply(Math, values);
2094             max = Math.max.apply(Math, values);
2095             if (options.get('base') === undefined) {
2096                 min = min < 0 ? min : 0;
2097             } else {
2098                 min = options.get('base');
2099             }
2100             this.min = min;
2101             this.max = max;
2102             this.range = max - min;
2103             this.shapes = {};
2104             this.valueShapes = {};
2105             this.regiondata = {};
2106             this.width = width = options.get('width') === 'auto' ? '4.0em' : width;
2107             this.target = this.$el.simpledraw(width, height, options.get('composite'));
2108             if (!values.length) {
2109                 this.disabled = true;
2110             }
2111             this.initTarget();
2112         },
2113
2114         getRegion: function (el, x, y) {
2115             var shapeid = this.target.getShapeAt(el, x, y);
2116             return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined;
2117         },
2118
2119         getCurrentRegionFields: function () {
2120             var currentRegion = this.currentRegion;
2121             return {
2122                 fieldkey: currentRegion.substr(0, 1),
2123                 value: this.values[currentRegion.substr(1)],
2124                 region: currentRegion
2125             };
2126         },
2127
2128         changeHighlight: function (highlight) {
2129             var currentRegion = this.currentRegion,
2130                 shapeid = this.valueShapes[currentRegion],
2131                 shape;
2132             delete this.shapes[shapeid];
2133             switch (currentRegion.substr(0, 1)) {
2134                 case 'r':
2135                     shape = this.renderRange(currentRegion.substr(1), highlight);
2136                     break;
2137                 case 'p':
2138                     shape = this.renderPerformance(highlight);
2139                     break;
2140                 case 't':
2141                     shape = this.renderTarget(highlight);
2142                     break;
2143             }
2144             this.valueShapes[currentRegion] = shape.id;
2145             this.shapes[shape.id] = currentRegion;
2146             this.target.replaceWithShape(shapeid, shape);
2147         },
2148
2149         renderRange: function (rn, highlight) {
2150             var rangeval = this.values[rn],
2151                 rangewidth = Math.round(this.canvasWidth * ((rangeval - this.min) / this.range)),
2152                 color = this.options.get('rangeColors')[rn - 2];
2153             if (highlight) {
2154                 color = this.calcHighlightColor(color, this.options);
2155             }
2156             return this.target.drawRect(0, 0, rangewidth - 1, this.canvasHeight - 1, color, color);
2157         },
2158
2159         renderPerformance: function (highlight) {
2160             var perfval = this.values[1],
2161                 perfwidth = Math.round(this.canvasWidth * ((perfval - this.min) / this.range)),
2162                 color = this.options.get('performanceColor');
2163             if (highlight) {
2164                 color = this.calcHighlightColor(color, this.options);
2165             }
2166             return this.target.drawRect(0, Math.round(this.canvasHeight * 0.3), perfwidth - 1,
2167                 Math.round(this.canvasHeight * 0.4) - 1, color, color);
2168         },
2169
2170         renderTarget: function (highlight) {
2171             var targetval = this.values[0],
2172                 x = Math.round(this.canvasWidth * ((targetval - this.min) / this.range) - (this.options.get('targetWidth') / 2)),
2173                 targettop = Math.round(this.canvasHeight * 0.10),
2174                 targetheight = this.canvasHeight - (targettop * 2),
2175                 color = this.options.get('targetColor');
2176             if (highlight) {
2177                 color = this.calcHighlightColor(color, this.options);
2178             }
2179             return this.target.drawRect(x, targettop, this.options.get('targetWidth') - 1, targetheight - 1, color, color);
2180         },
2181
2182         render: function () {
2183             var vlen = this.values.length,
2184                 target = this.target,
2185                 i, shape;
2186             if (!bullet._super.render.call(this)) {
2187                 return;
2188             }
2189             for (i = 2; i < vlen; i++) {
2190                 shape = this.renderRange(i).append();
2191                 this.shapes[shape.id] = 'r' + i;
2192                 this.valueShapes['r' + i] = shape.id;
2193             }
2194             shape = this.renderPerformance().append();
2195             this.shapes[shape.id] = 'p1';
2196             this.valueShapes.p1 = shape.id;
2197             shape = this.renderTarget().append();
2198             this.shapes[shape.id] = 't0';
2199             this.valueShapes.t0 = shape.id;
2200             target.render();
2201         }
2202     });
2203
2204     /**
2205      * Pie charts
2206      */
2207     $.fn.sparkline.pie = pie = createClass($.fn.sparkline._base, {
2208         type: 'pie',
2209
2210         init: function (el, values, options, width, height) {
2211             var total = 0, i;
2212
2213             pie._super.init.call(this, el, values, options, width, height);
2214
2215             this.shapes = {}; // map shape ids to value offsets
2216             this.valueShapes = {}; // maps value offsets to shape ids
2217             this.values = values = $.map(values, Number);
2218
2219             if (options.get('width') === 'auto') {
2220                 this.width = this.height;
2221             }
2222
2223             if (values.length > 0) {
2224                 for (i = values.length; i--;) {
2225                     total += values[i];
2226                 }
2227             }
2228             this.total = total;
2229             this.initTarget();
2230             this.radius = Math.floor(Math.min(this.canvasWidth, this.canvasHeight) / 2);
2231         },
2232
2233         getRegion: function (el, x, y) {
2234             var shapeid = this.target.getShapeAt(el, x, y);
2235             return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined;
2236         },
2237
2238         getCurrentRegionFields: function () {
2239             var currentRegion = this.currentRegion;
2240             return {
2241                 isNull: this.values[currentRegion] === undefined,
2242                 value: this.values[currentRegion],
2243                 percent: this.values[currentRegion] / this.total * 100,
2244                 color: this.options.get('sliceColors')[currentRegion % this.options.get('sliceColors').length],
2245                 offset: currentRegion
2246             };
2247         },
2248
2249         changeHighlight: function (highlight) {
2250             var currentRegion = this.currentRegion,
2251                  newslice = this.renderSlice(currentRegion, highlight),
2252                  shapeid = this.valueShapes[currentRegion];
2253             delete this.shapes[shapeid];
2254             this.target.replaceWithShape(shapeid, newslice);
2255             this.valueShapes[currentRegion] = newslice.id;
2256             this.shapes[newslice.id] = currentRegion;
2257         },
2258
2259         renderSlice: function (valuenum, highlight) {
2260             var target = this.target,
2261                 options = this.options,
2262                 radius = this.radius,
2263                 borderWidth = options.get('borderWidth'),
2264                 offset = options.get('offset'),
2265                 circle = 2 * Math.PI,
2266                 values = this.values,
2267                 total = this.total,
2268                 next = offset ? (2*Math.PI)*(offset/360) : 0,
2269                 start, end, i, vlen, color;
2270
2271             vlen = values.length;
2272             for (i = 0; i < vlen; i++) {
2273                 start = next;
2274                 end = next;
2275                 if (total > 0) {  // avoid divide by zero
2276                     end = next + (circle * (values[i] / total));
2277                 }
2278                 if (valuenum === i) {
2279                     color = options.get('sliceColors')[i % options.get('sliceColors').length];
2280                     if (highlight) {
2281                         color = this.calcHighlightColor(color, options);
2282                     }
2283
2284                     return target.drawPieSlice(radius, radius, radius - borderWidth, start, end, undefined, color);
2285                 }
2286                 next = end;
2287             }
2288         },
2289
2290         render: function () {
2291             var target = this.target,
2292                 values = this.values,
2293                 options = this.options,
2294                 radius = this.radius,
2295                 borderWidth = options.get('borderWidth'),
2296                 shape, i;
2297
2298             if (!pie._super.render.call(this)) {
2299                 return;
2300             }
2301             if (borderWidth) {
2302                 target.drawCircle(radius, radius, Math.floor(radius - (borderWidth / 2)),
2303                     options.get('borderColor'), undefined, borderWidth).append();
2304             }
2305             for (i = values.length; i--;) {
2306                 shape = this.renderSlice(i).append();
2307                 this.valueShapes[i] = shape.id; // store just the shapeid
2308                 this.shapes[shape.id] = i;
2309             }
2310             target.render();
2311         }
2312     });
2313
2314     /**
2315      * Box plots
2316      */
2317     $.fn.sparkline.box = box = createClass($.fn.sparkline._base, {
2318         type: 'box',
2319
2320         init: function (el, values, options, width, height) {
2321             box._super.init.call(this, el, values, options, width, height);
2322             this.values = $.map(values, Number);
2323             this.width = options.get('width') === 'auto' ? '4.0em' : width;
2324             this.initTarget();
2325             if (!this.values.length) {
2326                 this.disabled = 1;
2327             }
2328         },
2329
2330         /**
2331          * Simulate a single region
2332          */
2333         getRegion: function () {
2334             return 1;
2335         },
2336
2337         getCurrentRegionFields: function () {
2338             var result = [
2339                 { field: 'lq', value: this.quartiles[0] },
2340                 { field: 'med', value: this.quartiles[1] },
2341                 { field: 'uq', value: this.quartiles[2] },
2342                 { field: 'lo', value: this.loutlier },
2343                 { field: 'ro', value: this.routlier }
2344             ];
2345             if (this.lwhisker !== undefined) {
2346                 result.push({ field: 'lw', value: this.lwhisker});
2347             }
2348             if (this.rwhisker !== undefined) {
2349                 result.push({ field: 'rw', value: this.rwhisker});
2350             }
2351             return result;
2352         },
2353
2354         render: function () {
2355             var target = this.target,
2356                 values = this.values,
2357                 vlen = values.length,
2358                 options = this.options,
2359                 canvasWidth = this.canvasWidth,
2360                 canvasHeight = this.canvasHeight,
2361                 minValue = options.get('chartRangeMin') === undefined ? Math.min.apply(Math, values) : options.get('chartRangeMin'),
2362                 maxValue = options.get('chartRangeMax') === undefined ? Math.max.apply(Math, values) : options.get('chartRangeMax'),
2363                 canvasLeft = 0,
2364                 lwhisker, loutlier, iqr, q1, q2, q3, rwhisker, routlier, i,
2365                 size, unitSize;
2366
2367             if (!box._super.render.call(this)) {
2368                 return;
2369             }
2370
2371             if (options.get('raw')) {
2372                 if (options.get('showOutliers') && values.length > 5) {
2373                     loutlier = values[0];
2374                     lwhisker = values[1];
2375                     q1 = values[2];
2376                     q2 = values[3];
2377                     q3 = values[4];
2378                     rwhisker = values[5];
2379                     routlier = values[6];
2380                 } else {
2381                     lwhisker = values[0];
2382                     q1 = values[1];
2383                     q2 = values[2];
2384                     q3 = values[3];
2385                     rwhisker = values[4];
2386                 }
2387             } else {
2388                 values.sort(function (a, b) { return a - b; });
2389                 q1 = quartile(values, 1);
2390                 q2 = quartile(values, 2);
2391                 q3 = quartile(values, 3);
2392                 iqr = q3 - q1;
2393                 if (options.get('showOutliers')) {
2394                     lwhisker = rwhisker = undefined;
2395                     for (i = 0; i < vlen; i++) {
2396                         if (lwhisker === undefined && values[i] > q1 - (iqr * options.get('outlierIQR'))) {
2397                             lwhisker = values[i];
2398                         }
2399                         if (values[i] < q3 + (iqr * options.get('outlierIQR'))) {
2400                             rwhisker = values[i];
2401                         }
2402                     }
2403                     loutlier = values[0];
2404                     routlier = values[vlen - 1];
2405                 } else {
2406                     lwhisker = values[0];
2407                     rwhisker = values[vlen - 1];
2408                 }
2409             }
2410             this.quartiles = [q1, q2, q3];
2411             this.lwhisker = lwhisker;
2412             this.rwhisker = rwhisker;
2413             this.loutlier = loutlier;
2414             this.routlier = routlier;
2415
2416             unitSize = canvasWidth / (maxValue - minValue + 1);
2417             if (options.get('showOutliers')) {
2418                 canvasLeft = Math.ceil(options.get('spotRadius'));
2419                 canvasWidth -= 2 * Math.ceil(options.get('spotRadius'));
2420                 unitSize = canvasWidth / (maxValue - minValue + 1);
2421                 if (loutlier < lwhisker) {
2422                     target.drawCircle((loutlier - minValue) * unitSize + canvasLeft,
2423                         canvasHeight / 2,
2424                         options.get('spotRadius'),
2425                         options.get('outlierLineColor'),
2426                         options.get('outlierFillColor')).append();
2427                 }
2428                 if (routlier > rwhisker) {
2429                     target.drawCircle((routlier - minValue) * unitSize + canvasLeft,
2430                         canvasHeight / 2,
2431                         options.get('spotRadius'),
2432                         options.get('outlierLineColor'),
2433                         options.get('outlierFillColor')).append();
2434                 }
2435             }
2436
2437             // box
2438             target.drawRect(
2439                 Math.round((q1 - minValue) * unitSize + canvasLeft),
2440                 Math.round(canvasHeight * 0.1),
2441                 Math.round((q3 - q1) * unitSize),
2442                 Math.round(canvasHeight * 0.8),
2443                 options.get('boxLineColor'),
2444                 options.get('boxFillColor')).append();
2445             // left whisker
2446             target.drawLine(
2447                 Math.round((lwhisker - minValue) * unitSize + canvasLeft),
2448                 Math.round(canvasHeight / 2),
2449                 Math.round((q1 - minValue) * unitSize + canvasLeft),
2450                 Math.round(canvasHeight / 2),
2451                 options.get('lineColor')).append();
2452             target.drawLine(
2453                 Math.round((lwhisker - minValue) * unitSize + canvasLeft),
2454                 Math.round(canvasHeight / 4),
2455                 Math.round((lwhisker - minValue) * unitSize + canvasLeft),
2456                 Math.round(canvasHeight - canvasHeight / 4),
2457                 options.get('whiskerColor')).append();
2458             // right whisker
2459             target.drawLine(Math.round((rwhisker - minValue) * unitSize + canvasLeft),
2460                 Math.round(canvasHeight / 2),
2461                 Math.round((q3 - minValue) * unitSize + canvasLeft),
2462                 Math.round(canvasHeight / 2),
2463                 options.get('lineColor')).append();
2464             target.drawLine(
2465                 Math.round((rwhisker - minValue) * unitSize + canvasLeft),
2466                 Math.round(canvasHeight / 4),
2467                 Math.round((rwhisker - minValue) * unitSize + canvasLeft),
2468                 Math.round(canvasHeight - canvasHeight / 4),
2469                 options.get('whiskerColor')).append();
2470             // median line
2471             target.drawLine(
2472                 Math.round((q2 - minValue) * unitSize + canvasLeft),
2473                 Math.round(canvasHeight * 0.1),
2474                 Math.round((q2 - minValue) * unitSize + canvasLeft),
2475                 Math.round(canvasHeight * 0.9),
2476                 options.get('medianColor')).append();
2477             if (options.get('target')) {
2478                 size = Math.ceil(options.get('spotRadius'));
2479                 target.drawLine(
2480                     Math.round((options.get('target') - minValue) * unitSize + canvasLeft),
2481                     Math.round((canvasHeight / 2) - size),
2482                     Math.round((options.get('target') - minValue) * unitSize + canvasLeft),
2483                     Math.round((canvasHeight / 2) + size),
2484                     options.get('targetColor')).append();
2485                 target.drawLine(
2486                     Math.round((options.get('target') - minValue) * unitSize + canvasLeft - size),
2487                     Math.round(canvasHeight / 2),
2488                     Math.round((options.get('target') - minValue) * unitSize + canvasLeft + size),
2489                     Math.round(canvasHeight / 2),
2490                     options.get('targetColor')).append();
2491             }
2492             target.render();
2493         }
2494     });
2495
2496     // Setup a very simple "virtual canvas" to make drawing the few shapes we need easier
2497     // This is accessible as $(foo).simpledraw()
2498
2499     if ($.browser.msie && !document.namespaces.v) {
2500         document.namespaces.add('v', 'urn:schemas-microsoft-com:vml', '#default#VML');
2501     }
2502
2503     if ($.browser.hasCanvas === undefined) {
2504         $.browser.hasCanvas = document.createElement('canvas').getContext !== undefined;
2505     }
2506
2507     VShape = createClass({
2508         init: function (target, id, type, args) {
2509             this.target = target;
2510             this.id = id;
2511             this.type = type;
2512             this.args = args;
2513         },
2514         append: function () {
2515             this.target.appendShape(this);
2516             return this;
2517         }
2518     });
2519
2520     VCanvas_base = createClass({
2521         _pxregex: /(\d+)(px)?\s*$/i,
2522
2523         init: function (width, height, target) {
2524             if (!width) {
2525                 return;
2526             }
2527             this.width = width;
2528             this.height = height;
2529             this.target = target;
2530             this.lastShapeId = null;
2531             if (target[0]) {
2532                 target = target[0];
2533             }
2534             $.data(target, '_jqs_vcanvas', this);
2535         },
2536
2537         drawLine: function (x1, y1, x2, y2, lineColor, lineWidth) {
2538             return this.drawShape([[x1, y1], [x2, y2]], lineColor, lineWidth);
2539         },
2540
2541         drawShape: function (path, lineColor, fillColor, lineWidth) {
2542             return this._genShape('Shape', [path, lineColor, fillColor, lineWidth]);
2543         },
2544
2545         drawCircle: function (x, y, radius, lineColor, fillColor, lineWidth) {
2546             return this._genShape('Circle', [x, y, radius, lineColor, fillColor, lineWidth]);
2547         },
2548
2549         drawPieSlice: function (x, y, radius, startAngle, endAngle, lineColor, fillColor) {
2550             return this._genShape('PieSlice', [x, y, radius, startAngle, endAngle, lineColor, fillColor]);
2551         },
2552
2553         drawRect: function (x, y, width, height, lineColor, fillColor) {
2554             return this._genShape('Rect', [x, y, width, height, lineColor, fillColor]);
2555         },
2556
2557         getElement: function () {
2558             return this.canvas;
2559         },
2560
2561         /**
2562          * Return the most recently inserted shape id
2563          */
2564         getLastShapeId: function () {
2565             return this.lastShapeId;
2566         },
2567
2568         /**
2569          * Clear and reset the canvas
2570          */
2571         reset: function () {
2572             alert('reset not implemented');
2573         },
2574
2575         _insert: function (el, target) {
2576             $(target).html(el);
2577         },
2578
2579         /**
2580          * Calculate the pixel dimensions of the canvas
2581          */
2582         _calculatePixelDims: function (width, height, canvas) {
2583             // XXX This should probably be a configurable option
2584             var match;
2585             match = this._pxregex.exec(height);
2586             if (match) {
2587                 this.pixelHeight = match[1];
2588             } else {
2589                 this.pixelHeight = $(canvas).height();
2590             }
2591             match = this._pxregex.exec(width);
2592             if (match) {
2593                 this.pixelWidth = match[1];
2594             } else {
2595                 this.pixelWidth = $(canvas).width();
2596             }
2597         },
2598
2599         /**
2600          * Generate a shape object and id for later rendering
2601          */
2602         _genShape: function (shapetype, shapeargs) {
2603             var id = shapeCount++;
2604             shapeargs.unshift(id);
2605             return new VShape(this, id, shapetype, shapeargs);
2606         },
2607
2608         /**
2609          * Add a shape to the end of the render queue
2610          */
2611         appendShape: function (shape) {
2612             alert('appendShape not implemented');
2613         },
2614
2615         /**
2616          * Replace one shape with another
2617          */
2618         replaceWithShape: function (shapeid, shape) {
2619             alert('replaceWithShape not implemented');
2620         },
2621
2622         /**
2623          * Insert one shape after another in the render queue
2624          */
2625         insertAfterShape: function (shapeid, shape) {
2626             alert('insertAfterShape not implemented');
2627         },
2628
2629         /**
2630          * Remove a shape from the queue
2631          */
2632         removeShapeId: function (shapeid) {
2633             alert('removeShapeId not implemented');
2634         },
2635
2636         /**
2637          * Find a shape at the specified x/y co-ordinates
2638          */
2639         getShapeAt: function (el, x, y) {
2640             alert('getShapeAt not implemented');
2641         },
2642
2643         /**
2644          * Render all queued shapes onto the canvas
2645          */
2646         render: function () {
2647             alert('render not implemented');
2648         }
2649     });
2650
2651     VCanvas_canvas = createClass(VCanvas_base, {
2652         init: function (width, height, target, interact) {
2653             VCanvas_canvas._super.init.call(this, width, height, target);
2654             this.canvas = document.createElement('canvas');
2655             if (target[0]) {
2656                 target = target[0];
2657             }
2658             $.data(target, '_jqs_vcanvas', this);
2659             $(this.canvas).css({ display: 'inline-block', width: width, height: height, verticalAlign: 'top' });
2660             this._insert(this.canvas, target);
2661             this._calculatePixelDims(width, height, this.canvas);
2662             this.canvas.width = this.pixelWidth;
2663             this.canvas.height = this.pixelHeight;
2664             this.interact = interact;
2665             this.shapes = {};
2666             this.shapeseq = [];
2667             this.currentTargetShapeId = undefined;
2668             $(this.canvas).css({width: this.pixelWidth, height: this.pixelHeight});
2669         },
2670
2671         _getContext: function (lineColor, fillColor, lineWidth) {
2672             var context = this.canvas.getContext('2d');
2673             if (lineColor !== undefined) {
2674                 context.strokeStyle = lineColor;
2675             }
2676             context.lineWidth = lineWidth === undefined ? 1 : lineWidth;
2677             if (fillColor !== undefined) {
2678                 context.fillStyle = fillColor;
2679             }
2680             return context;
2681         },
2682
2683         reset: function () {
2684             var context = this._getContext();
2685             context.clearRect(0, 0, this.pixelWidth, this.pixelHeight);
2686             this.shapes = {};
2687             this.shapeseq = [];
2688             this.currentTargetShapeId = undefined;
2689         },
2690
2691         _drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) {
2692             var context = this._getContext(lineColor, fillColor, lineWidth),
2693                 i, plen;
2694             context.beginPath();
2695             context.moveTo(path[0][0] + 0.5, path[0][1] + 0.5);
2696             for (i = 1, plen = path.length; i < plen; i++) {
2697                 context.lineTo(path[i][0] + 0.5, path[i][1] + 0.5); // the 0.5 offset gives us crisp pixel-width lines
2698             }
2699             if (lineColor !== undefined) {
2700                 context.stroke();
2701             }
2702             if (fillColor !== undefined) {
2703                 context.fill();
2704             }
2705             if (this.targetX !== undefined && this.targetY !== undefined &&
2706                 context.isPointInPath(this.targetX, this.targetY)) {
2707                 this.currentTargetShapeId = shapeid;
2708             }
2709         },
2710
2711         _drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) {
2712             var context = this._getContext(lineColor, fillColor, lineWidth);
2713             context.beginPath();
2714             context.arc(x, y, radius, 0, 2 * Math.PI, false);
2715             if (this.targetX !== undefined && this.targetY !== undefined &&
2716                 context.isPointInPath(this.targetX, this.targetY)) {
2717                 this.currentTargetShapeId = shapeid;
2718             }
2719             if (lineColor !== undefined) {
2720                 context.stroke();
2721             }
2722             if (fillColor !== undefined) {
2723                 context.fill();
2724             }
2725         },
2726
2727         _drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) {
2728             var context = this._getContext(lineColor, fillColor);
2729             context.beginPath();
2730             context.moveTo(x, y);
2731             context.arc(x, y, radius, startAngle, endAngle, false);
2732             context.lineTo(x, y);
2733             context.closePath();
2734             if (lineColor !== undefined) {
2735                 context.stroke();
2736             }
2737             if (fillColor) {
2738                 context.fill();
2739             }
2740             if (this.targetX !== undefined && this.targetY !== undefined &&
2741                 context.isPointInPath(this.targetX, this.targetY)) {
2742                 this.currentTargetShapeId = shapeid;
2743             }
2744         },
2745
2746         _drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) {
2747             return this._drawShape(shapeid, [[x, y], [x + width, y], [x + width, y + height], [x, y + height], [x, y]], lineColor, fillColor);
2748         },
2749
2750         appendShape: function (shape) {
2751             this.shapes[shape.id] = shape;
2752             this.shapeseq.push(shape.id);
2753             this.lastShapeId = shape.id;
2754             return shape.id;
2755         },
2756
2757         replaceWithShape: function (shapeid, shape) {
2758             var shapeseq = this.shapeseq,
2759                 i;
2760             this.shapes[shape.id] = shape;
2761             for (i = shapeseq.length; i--;) {
2762                 if (shapeseq[i] == shapeid) {
2763                     shapeseq[i] = shape.id;
2764                 }
2765             }
2766             delete this.shapes[shapeid];
2767         },
2768
2769         replaceWithShapes: function (shapeids, shapes) {
2770             var shapeseq = this.shapeseq,
2771                 shapemap = {},
2772                 sid, i, first;
2773
2774             for (i = shapeids.length; i--;) {
2775                 shapemap[shapeids[i]] = true;
2776             }
2777             for (i = shapeseq.length; i--;) {
2778                 sid = shapeseq[i];
2779                 if (shapemap[sid]) {
2780                     shapeseq.splice(i, 1);
2781                     delete this.shapes[sid];
2782                     first = i;
2783                 }
2784             }
2785             for (i = shapes.length; i--;) {
2786                 shapeseq.splice(first, 0, shapes[i].id);
2787                 this.shapes[shapes[i].id] = shapes[i];
2788             }
2789
2790         },
2791
2792         insertAfterShape: function (shapeid, shape) {
2793             var shapeseq = this.shapeseq,
2794                 i;
2795             for (i = shapeseq.length; i--;) {
2796                 if (shapeseq[i] === shapeid) {
2797                     shapeseq.splice(i + 1, 0, shape.id);
2798                     this.shapes[shape.id] = shape;
2799                     return;
2800                 }
2801             }
2802         },
2803
2804         removeShapeId: function (shapeid) {
2805             var shapeseq = this.shapeseq,
2806                 i;
2807             for (i = shapeseq.length; i--;) {
2808                 if (shapeseq[i] === shapeid) {
2809                     shapeseq.splice(i, 1);
2810                     break;
2811                 }
2812             }
2813             delete this.shapes[shapeid];
2814         },
2815
2816         getShapeAt: function (el, x, y) {
2817             this.targetX = x;
2818             this.targetY = y;
2819             this.render();
2820             return this.currentTargetShapeId;
2821         },
2822
2823         render: function () {
2824             var shapeseq = this.shapeseq,
2825                 shapes = this.shapes,
2826                 shapeCount = shapeseq.length,
2827                 context = this._getContext(),
2828                 shapeid, shape, i;
2829             context.clearRect(0, 0, this.pixelWidth, this.pixelHeight);
2830             for (i = 0; i < shapeCount; i++) {
2831                 shapeid = shapeseq[i];
2832                 shape = shapes[shapeid];
2833                 this['_draw' + shape.type].apply(this, shape.args);
2834             }
2835             if (!this.interact) {
2836                 // not interactive so no need to keep the shapes array
2837                 this.shapes = {};
2838                 this.shapeseq = [];
2839             }
2840         }
2841
2842     });
2843
2844     VCanvas_vml = createClass(VCanvas_base, {
2845         init: function (width, height, target) {
2846             var groupel;
2847             VCanvas_vml._super.init.call(this, width, height, target);
2848             if (target[0]) {
2849                 target = target[0];
2850             }
2851             $.data(target, '_jqs_vcanvas', this);
2852             this.canvas = document.createElement('span');
2853             $(this.canvas).css({ display: 'inline-block', position: 'relative', overflow: 'hidden', width: width, height: height, margin: '0px', padding: '0px', verticalAlign: 'top'});
2854             this._insert(this.canvas, target);
2855             this._calculatePixelDims(width, height, this.canvas);
2856             this.canvas.width = this.pixelWidth;
2857             this.canvas.height = this.pixelHeight;
2858             groupel = '<v:group coordorigin="0 0" coordsize="' + this.pixelWidth + ' ' + this.pixelHeight + '"' +
2859                     ' style="position:absolute;top:0;left:0;width:' + this.pixelWidth + 'px;height=' + this.pixelHeight + 'px;"></v:group>';
2860             this.canvas.insertAdjacentHTML('beforeEnd', groupel);
2861             this.group = $(this.canvas).children()[0];
2862             this.rendered = false;
2863             this.prerender = '';
2864         },
2865
2866         _drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) {
2867             var vpath = [],
2868                 initial, stroke, fill, closed, vel, plen, i;
2869             for (i = 0, plen = path.length; i < plen; i++) {
2870                 vpath[i] = '' + (path[i][0]) + ',' + (path[i][1]);
2871             }
2872             initial = vpath.splice(0, 1);
2873             lineWidth = lineWidth === undefined ? 1 : lineWidth;
2874             stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" ';
2875             fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
2876             closed = vpath[0] === vpath[vpath.length - 1] ? 'x ' : '';
2877             vel = '<v:shape coordorigin="0 0" coordsize="' + this.pixelWidth + ' ' + this.pixelHeight + '" ' +
2878                  ' id="jqsshape' + shapeid + '" ' +
2879                  stroke +
2880                  fill +
2881                 ' style="position:absolute;left:0px;top:0px;height:' + this.pixelHeight + 'px;width:' + this.pixelWidth + 'px;padding:0px;margin:0px;" ' +
2882                 ' path="m ' + initial + ' l ' + vpath.join(', ') + ' ' + closed + 'e">' +
2883                 ' </v:shape>';
2884             return vel;
2885         },
2886
2887         _drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) {
2888             var stroke, fill, vel;
2889             x -= radius;
2890             y -= radius;
2891             stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" ';
2892             fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
2893             vel = '<v:oval ' +
2894                  ' id="jqsshape' + shapeid + '" ' +
2895                 stroke +
2896                 fill +
2897                 ' style="position:absolute;top:' + y + 'px; left:' + x + 'px; width:' + (radius * 2) + 'px; height:' + (radius * 2) + 'px"></v:oval>';
2898             return vel;
2899
2900         },
2901
2902         _drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) {
2903             var vpath, startx, starty, endx, endy, stroke, fill, vel;
2904             if (startAngle === endAngle) {
2905                 return;  // VML seems to have problem when start angle equals end angle.
2906             }
2907             if ((endAngle - startAngle) === (2 * Math.PI)) {
2908                 startAngle = 0.0;  // VML seems to have a problem when drawing a full circle that doesn't start 0
2909                 endAngle = (2 * Math.PI);
2910             }
2911
2912             startx = x + Math.round(Math.cos(startAngle) * radius);
2913             starty = y + Math.round(Math.sin(startAngle) * radius);
2914             endx = x + Math.round(Math.cos(endAngle) * radius);
2915             endy = y + Math.round(Math.sin(endAngle) * radius);
2916
2917             // Prevent very small slices from being mistaken as a whole pie
2918             if (startx === endx && starty === endy && (endAngle - startAngle) < Math.PI) {
2919                 return;
2920             }
2921
2922             vpath = [x - radius, y - radius, x + radius, y + radius, startx, starty, endx, endy];
2923             stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="1px" strokeColor="' + lineColor + '" ';
2924             fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
2925             vel = '<v:shape coordorigin="0 0" coordsize="' + this.pixelWidth + ' ' + this.pixelHeight + '" ' +
2926                  ' id="jqsshape' + shapeid + '" ' +
2927                  stroke +
2928                  fill +
2929                 ' style="position:absolute;left:0px;top:0px;height:' + this.pixelHeight + 'px;width:' + this.pixelWidth + 'px;padding:0px;margin:0px;" ' +
2930                 ' path="m ' + x + ',' + y + ' wa ' + vpath.join(', ') + ' x e">' +
2931                 ' </v:shape>';
2932             return vel;
2933         },
2934
2935         _drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) {
2936             return this._drawShape(shapeid, [[x, y], [x, y + height], [x + width, y + height], [x + width, y], [x, y]], lineColor, fillColor);
2937         },
2938
2939         reset: function () {
2940             this.group.innerHTML = '';
2941         },
2942
2943         appendShape: function (shape) {
2944             var vel = this['_draw' + shape.type].apply(this, shape.args);
2945             if (this.rendered) {
2946                 this.group.insertAdjacentHTML('beforeEnd', vel);
2947             } else {
2948                 this.prerender += vel;
2949             }
2950             this.lastShapeId = shape.id;
2951             return shape.id;
2952         },
2953
2954         replaceWithShape: function (shapeid, shape) {
2955             var existing = $('#jqsshape' + shapeid),
2956                 vel = this['_draw' + shape.type].apply(this, shape.args);
2957             existing[0].outerHTML = vel;
2958         },
2959
2960         replaceWithShapes: function (shapeids, shapes) {
2961             // replace the first shapeid with all the new shapes then toast the remaining old shapes
2962             var existing = $('#jqsshape' + shapeids[0]),
2963                 replace = '',
2964                 slen = shapes.length,
2965                 i;
2966             for (i = 0; i < slen; i++) {
2967                 replace += this['_draw' + shapes[i].type].apply(this, shapes[i].args);
2968             }
2969             existing[0].outerHTML = replace;
2970             for (i = 1; i < shapeids.length; i++) {
2971                 $('#jqsshape' + shapeids[i]).remove();
2972             }
2973         },
2974
2975         insertAfterShape: function (shapeid, shape) {
2976             var existing = $('#jqsshape' + shapeid),
2977                  vel = this['_draw' + shape.type].apply(this, shape.args);
2978             existing[0].insertAdjacentHTML('afterEnd', vel);
2979         },
2980
2981         removeShapeId: function (shapeid) {
2982             var existing = $('#jqsshape' + shapeid);
2983             this.group.removeChild(existing[0]);
2984         },
2985
2986         getShapeAt: function (el, x, y) {
2987             var shapeid = el.id.substr(8);
2988             return shapeid;
2989         },
2990
2991         render: function () {
2992             if (!this.rendered) {
2993                 // batch the intial render into a single repaint
2994                 this.group.innerHTML = this.prerender;
2995                 this.rendered = true;
2996             }
2997         }
2998     });
2999
3000
3001 })(jQuery);