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