source: pro-violet-viettel/docs/template/assets/js/uncompressed/jquery.sparkline.js

Last change on this file was 400, checked in by dungnv, 11 years ago
  • Property svn:mime-type set to text/plain
File size: 120.6 KB
RevLine 
[400]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));
Note: See TracBrowser for help on using the repository browser.