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