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