Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

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