You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3068 lines
121 KiB
3068 lines
121 KiB
/** |
|
* |
|
* jquery.sparkline.js |
|
* |
|
* v2.4.1 |
|
* (c) Splunk, Inc |
|
* Contact: Gareth Watts (gareth@splunk.com) |
|
* http://omnipotent.net/jquery.sparkline/ |
|
* |
|
* Generates inline sparkline charts from data supplied either to the method |
|
* or inline in HTML |
|
* |
|
* Compatible with Internet Explorer 6.0+ and modern browsers equipped with the canvas tag |
|
* (Firefox 2.0+, Safari, Opera, etc) |
|
* |
|
* License: New BSD License |
|
* |
|
* Copyright (c) 2012, Splunk Inc. |
|
* All rights reserved. |
|
* |
|
* Redistribution and use in source and binary forms, with or without modification, |
|
* are permitted provided that the following conditions are met: |
|
* |
|
* * Redistributions of source code must retain the above copyright notice, |
|
* this list of conditions and the following disclaimer. |
|
* * Redistributions in binary form must reproduce the above copyright notice, |
|
* this list of conditions and the following disclaimer in the documentation |
|
* and/or other materials provided with the distribution. |
|
* * Neither the name of Splunk Inc nor the names of its contributors may |
|
* be used to endorse or promote products derived from this software without |
|
* specific prior written permission. |
|
* |
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY |
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT |
|
* SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT |
|
* OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
|
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
* |
|
* |
|
* Usage: |
|
* $(selector).sparkline(values, options) |
|
* |
|
* If values is undefined or set to 'html' then the data values are read from the specified tag: |
|
* <p>Sparkline: <span class="sparkline">1,4,6,6,8,5,3,5</span></p> |
|
* $('.sparkline').sparkline(); |
|
* There must be no spaces in the enclosed data set |
|
* |
|
* Otherwise values must be an array of numbers or null values |
|
* <p>Sparkline: <span id="sparkline1">This text replaced if the browser is compatible</span></p> |
|
* $('#sparkline1').sparkline([1,4,6,6,8,5,3,5]) |
|
* $('#sparkline2').sparkline([1,4,6,null,null,5,3,5]) |
|
* |
|
* Values can also be specified in an HTML comment, or as a values attribute: |
|
* <p>Sparkline: <span class="sparkline"><!--1,4,6,6,8,5,3,5 --></span></p> |
|
* <p>Sparkline: <span class="sparkline" values="1,4,6,6,8,5,3,5"></span></p> |
|
* $('.sparkline').sparkline(); |
|
* |
|
* For line charts, x values can also be specified: |
|
* <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> |
|
* $('#sparkline1').sparkline([ [1,1], [2.7,4], [3.4,6], [5,6], [6,8], [8.7,5], [9,3], [10,5] ]) |
|
* |
|
* By default, options should be passed in as the second argument to the sparkline function: |
|
* $('.sparkline').sparkline([1,2,3,4], {type: 'bar'}) |
|
* |
|
* Options can also be set by passing them on the tag itself. This feature is disabled by default though |
|
* as there's a slight performance overhead: |
|
* $('.sparkline').sparkline([1,2,3,4], {enableTagOptions: true}) |
|
* <p>Sparkline: <span class="sparkline" sparkType="bar" sparkBarColor="red">loading</span></p> |
|
* Prefix all options supplied as tag attribute with "spark" (configurable by setting tagOptionsPrefix) |
|
* |
|
* Supported options: |
|
* lineColor - Color of the line used for the chart |
|
* fillColor - Color used to fill in the chart - Set to '' or false for a transparent chart |
|
* width - Width of the chart - Defaults to 3 times the number of values in pixels |
|
* height - Height of the chart - Defaults to the height of the containing element |
|
* chartRangeMin - Specify the minimum value to use for the Y range of the chart - Defaults to the minimum value supplied |
|
* chartRangeMax - Specify the maximum value to use for the Y range of the chart - Defaults to the maximum value supplied |
|
* chartRangeClip - Clip out of range values to the max/min specified by chartRangeMin and chartRangeMax |
|
* chartRangeMinX - Specify the minimum value to use for the X range of the chart - Defaults to the minimum value supplied |
|
* chartRangeMaxX - Specify the maximum value to use for the X range of the chart - Defaults to the maximum value supplied |
|
* composite - If true then don't erase any existing chart attached to the tag, but draw |
|
* another chart over the top - Note that width and height are ignored if an |
|
* existing chart is detected. |
|
* tagValuesAttribute - Name of tag attribute to check for data values - Defaults to 'values' |
|
* enableTagOptions - Whether to check tags for sparkline options |
|
* tagOptionsPrefix - Prefix used for options supplied as tag attributes - Defaults to 'spark' |
|
* disableHiddenCheck - If set to true, then the plugin will assume that charts will never be drawn into a |
|
* hidden dom element, avoding a browser reflow |
|
* disableInteraction - If set to true then all mouseover/click interaction behaviour will be disabled, |
|
* making the plugin perform much like it did in 1.x |
|
* disableTooltips - If set to true then tooltips will be disabled - Defaults to false (tooltips enabled) |
|
* disableHighlight - If set to true then highlighting of selected chart elements on mouseover will be disabled |
|
* defaults to false (highlights enabled) |
|
* highlightLighten - Factor to lighten/darken highlighted chart values by - Defaults to 1.4 for a 40% increase |
|
* tooltipContainer - Specify which DOM element the tooltip should be rendered into - defaults to document.body |
|
* tooltipClassname - Optional CSS classname to apply to tooltips - If not specified then a default style will be applied |
|
* tooltipOffsetX - How many pixels away from the mouse pointer to render the tooltip on the X axis |
|
* tooltipOffsetY - How many pixels away from the mouse pointer to render the tooltip on the r axis |
|
* tooltipFormatter - Optional callback that allows you to override the HTML displayed in the tooltip |
|
* callback is given arguments of (sparkline, options, fields) |
|
* tooltipChartTitle - If specified then the tooltip uses the string specified by this setting as a title |
|
* tooltipFormat - A format string or SPFormat object (or an array thereof for multiple entries) |
|
* to control the format of the tooltip |
|
* tooltipPrefix - A string to prepend to each field displayed in a tooltip |
|
* tooltipSuffix - A string to append to each field displayed in a tooltip |
|
* tooltipSkipNull - If true then null values will not have a tooltip displayed (defaults to true) |
|
* tooltipValueLookups - An object or range map to map field values to tooltip strings |
|
* (eg. to map -1 to "Lost", 0 to "Draw", and 1 to "Win") |
|
* numberFormatter - Optional callback for formatting numbers in tooltips |
|
* numberDigitGroupSep - Character to use for group separator in numbers "1,234" - Defaults to "," |
|
* numberDecimalMark - Character to use for the decimal point when formatting numbers - Defaults to "." |
|
* numberDigitGroupCount - Number of digits between group separator - Defaults to 3 |
|
* |
|
* There are 7 types of sparkline, selected by supplying a "type" option of 'line' (default), |
|
* 'bar', 'tristate', 'bullet', 'discrete', 'pie' or 'box' |
|
* line - Line chart. Options: |
|
* spotColor - Set to '' to not end each line in a circular spot |
|
* minSpotColor - If set, color of spot at minimum value |
|
* maxSpotColor - If set, color of spot at maximum value |
|
* spotRadius - Radius in pixels |
|
* lineWidth - Width of line in pixels |
|
* normalRangeMin |
|
* normalRangeMax - If set draws a filled horizontal bar between these two values marking the "normal" |
|
* or expected range of values |
|
* normalRangeColor - Color to use for the above bar |
|
* drawNormalOnTop - Draw the normal range above the chart fill color if true |
|
* defaultPixelsPerValue - Defaults to 3 pixels of width for each value in the chart |
|
* highlightSpotColor - The color to use for drawing a highlight spot on mouseover - Set to null to disable |
|
* highlightLineColor - The color to use for drawing a highlight line on mouseover - Set to null to disable |
|
* valueSpots - Specify which points to draw spots on, and in which color. Accepts a range map |
|
* |
|
* bar - Bar chart. Options: |
|
* barColor - Color of bars for postive values |
|
* negBarColor - Color of bars for negative values |
|
* zeroColor - Color of bars with zero values |
|
* nullColor - Color of bars with null values - Defaults to omitting the bar entirely |
|
* barWidth - Width of bars in pixels |
|
* colorMap - Optional mappnig of values to colors to override the *BarColor values above |
|
* can be an Array of values to control the color of individual bars or a range map |
|
* to specify colors for individual ranges of values |
|
* barSpacing - Gap between bars in pixels |
|
* zeroAxis - Centers the y-axis around zero if true |
|
* |
|
* tristate - Charts values of win (>0), lose (<0) or draw (=0) |
|
* posBarColor - Color of win values |
|
* negBarColor - Color of lose values |
|
* zeroBarColor - Color of draw values |
|
* barWidth - Width of bars in pixels |
|
* barSpacing - Gap between bars in pixels |
|
* colorMap - Optional mappnig of values to colors to override the *BarColor values above |
|
* can be an Array of values to control the color of individual bars or a range map |
|
* to specify colors for individual ranges of values |
|
* |
|
* discrete - Options: |
|
* lineHeight - Height of each line in pixels - Defaults to 30% of the graph height |
|
* thesholdValue - Values less than this value will be drawn using thresholdColor instead of lineColor |
|
* thresholdColor |
|
* |
|
* bullet - Values for bullet graphs msut be in the order: target, performance, range1, range2, range3, ... |
|
* options: |
|
* targetColor - The color of the vertical target marker |
|
* targetWidth - The width of the target marker in pixels |
|
* performanceColor - The color of the performance measure horizontal bar |
|
* rangeColors - Colors to use for each qualitative range background color |
|
* |
|
* pie - Pie chart. Options: |
|
* sliceColors - An array of colors to use for pie slices |
|
* offset - Angle in degrees to offset the first slice - Try -90 or +90 |
|
* borderWidth - Width of border to draw around the pie chart, in pixels - Defaults to 0 (no border) |
|
* borderColor - Color to use for the pie chart border - Defaults to #000 |
|
* |
|
* box - Box plot. Options: |
|
* raw - Set to true to supply pre-computed plot points as values |
|
* values should be: low_outlier, low_whisker, q1, median, q3, high_whisker, high_outlier |
|
* When set to false you can supply any number of values and the box plot will |
|
* be computed for you. Default is false. |
|
* showOutliers - Set to true (default) to display outliers as circles |
|
* outlierIQR - Interquartile range used to determine outliers. Default 1.5 |
|
* boxLineColor - Outline color of the box |
|
* boxFillColor - Fill color for the box |
|
* whiskerColor - Line color used for whiskers |
|
* outlierLineColor - Outline color of outlier circles |
|
* outlierFillColor - Fill color of the outlier circles |
|
* spotRadius - Radius of outlier circles |
|
* medianColor - Line color of the median line |
|
* target - Draw a target cross hair at the supplied value (default undefined) |
|
* |
|
* |
|
* |
|
* Examples: |
|
* $('#sparkline1').sparkline(myvalues, { lineColor: '#f00', fillColor: false }); |
|
* $('.barsparks').sparkline('html', { type:'bar', height:'40px', barWidth:5 }); |
|
* $('#tristate').sparkline([1,1,-1,1,0,0,-1], { type:'tristate' }): |
|
* $('#discrete').sparkline([1,3,4,5,5,3,4,5], { type:'discrete' }); |
|
* $('#bullet').sparkline([10,12,12,9,7], { type:'bullet' }); |
|
* $('#pie').sparkline([1,1,2], { type:'pie' }); |
|
*/ |
|
|
|
/*jslint regexp: true, browser: true, jquery: true, white: true, nomen: false, plusplus: false, maxerr: 500, indent: 4 */ |
|
|
|
(function(document, Math, undefined) { // performance/minified-size optimization |
|
(function(factory) { |
|
if(typeof define === 'function' && define.amd) { |
|
define(['jquery'], factory); |
|
} else if (jQuery && !jQuery.fn.sparkline) { |
|
factory(jQuery); |
|
} |
|
} |
|
(function($) { |
|
'use strict'; |
|
|
|
var UNSET_OPTION = {}, |
|
getDefaults, createClass, SPFormat, clipval, quartile, normalizeValue, normalizeValues, |
|
remove, isNumber, all, sum, addCSS, ensureArray, formatNumber, RangeMap, |
|
MouseHandler, Tooltip, barHighlightMixin, |
|
line, bar, tristate, discrete, bullet, pie, box, defaultStyles, initStyles, |
|
VShape, VCanvas_base, VCanvas_canvas, VCanvas_vml, pending, shapeCount = 0; |
|
|
|
/** |
|
* Default configuration settings |
|
*/ |
|
getDefaults = function () { |
|
return { |
|
// Settings common to most/all chart types |
|
common: { |
|
type: 'line', |
|
lineColor: '#00f', |
|
fillColor: '#cdf', |
|
defaultPixelsPerValue: 3, |
|
width: 'auto', |
|
height: 'auto', |
|
composite: false, |
|
tagValuesAttribute: 'values', |
|
tagOptionsPrefix: 'spark', |
|
enableTagOptions: false, |
|
enableHighlight: true, |
|
highlightLighten: 1.4, |
|
tooltipSkipNull: true, |
|
tooltipPrefix: '', |
|
tooltipSuffix: '', |
|
disableHiddenCheck: false, |
|
numberFormatter: false, |
|
numberDigitGroupCount: 3, |
|
numberDigitGroupSep: ',', |
|
numberDecimalMark: '.', |
|
disableTooltips: false, |
|
disableInteraction: false |
|
}, |
|
// Defaults for line charts |
|
line: { |
|
spotColor: '#f80', |
|
highlightSpotColor: '#5f5', |
|
highlightLineColor: '#f22', |
|
spotRadius: 1.5, |
|
minSpotColor: '#f80', |
|
maxSpotColor: '#f80', |
|
lineWidth: 1, |
|
normalRangeMin: undefined, |
|
normalRangeMax: undefined, |
|
normalRangeColor: '#ccc', |
|
drawNormalOnTop: false, |
|
chartRangeMin: undefined, |
|
chartRangeMax: undefined, |
|
chartRangeMinX: undefined, |
|
chartRangeMaxX: undefined, |
|
tooltipFormat: new SPFormat('<span style="color: {{color}}">●</span> {{prefix}}{{y}}{{suffix}}') |
|
}, |
|
// Defaults for bar charts |
|
bar: { |
|
barColor: '#3366cc', |
|
negBarColor: '#f44', |
|
stackedBarColor: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00', |
|
'#dd4477', '#0099c6', '#990099'], |
|
zeroColor: undefined, |
|
nullColor: undefined, |
|
zeroAxis: true, |
|
barWidth: 4, |
|
barSpacing: 1, |
|
chartRangeMax: undefined, |
|
chartRangeMin: undefined, |
|
chartRangeClip: false, |
|
colorMap: undefined, |
|
tooltipFormat: new SPFormat('<span style="color: {{color}}">●</span> {{prefix}}{{value}}{{suffix}}') |
|
}, |
|
// Defaults for tristate charts |
|
tristate: { |
|
barWidth: 4, |
|
barSpacing: 1, |
|
posBarColor: '#6f6', |
|
negBarColor: '#f44', |
|
zeroBarColor: '#999', |
|
colorMap: {}, |
|
tooltipFormat: new SPFormat('<span style="color: {{color}}">●</span> {{value:map}}'), |
|
tooltipValueLookups: { map: { '-1': 'Loss', '0': 'Draw', '1': 'Win' } } |
|
}, |
|
// Defaults for discrete charts |
|
discrete: { |
|
lineHeight: 'auto', |
|
thresholdColor: undefined, |
|
thresholdValue: 0, |
|
chartRangeMax: undefined, |
|
chartRangeMin: undefined, |
|
chartRangeClip: false, |
|
tooltipFormat: new SPFormat('{{prefix}}{{value}}{{suffix}}') |
|
}, |
|
// Defaults for bullet charts |
|
bullet: { |
|
targetColor: '#f33', |
|
targetWidth: 3, // width of the target bar in pixels |
|
performanceColor: '#33f', |
|
rangeColors: ['#d3dafe', '#a8b6ff', '#7f94ff'], |
|
base: undefined, // set this to a number to change the base start number |
|
tooltipFormat: new SPFormat('{{fieldkey:fields}} - {{value}}'), |
|
tooltipValueLookups: { fields: {r: 'Range', p: 'Performance', t: 'Target'} } |
|
}, |
|
// Defaults for pie charts |
|
pie: { |
|
offset: 0, |
|
sliceColors: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00', |
|
'#dd4477', '#0099c6', '#990099'], |
|
borderWidth: 0, |
|
borderColor: '#000', |
|
tooltipFormat: new SPFormat('<span style="color: {{color}}">●</span> {{value}} ({{percent.1}}%)') |
|
}, |
|
// Defaults for box plots |
|
box: { |
|
raw: false, |
|
boxLineColor: '#000', |
|
boxFillColor: '#cdf', |
|
whiskerColor: '#000', |
|
outlierLineColor: '#333', |
|
outlierFillColor: '#fff', |
|
medianColor: '#f00', |
|
showOutliers: true, |
|
outlierIQR: 1.5, |
|
spotRadius: 1.5, |
|
target: undefined, |
|
targetColor: '#4a2', |
|
chartRangeMax: undefined, |
|
chartRangeMin: undefined, |
|
tooltipFormat: new SPFormat('{{field:fields}}: {{value}}'), |
|
tooltipFormatFieldlistKey: 'field', |
|
tooltipValueLookups: { fields: { lq: 'Lower Quartile', med: 'Median', |
|
uq: 'Upper Quartile', lo: 'Left Outlier', ro: 'Right Outlier', |
|
lw: 'Left Whisker', rw: 'Right Whisker'} } |
|
} |
|
}; |
|
}; |
|
|
|
// You can have tooltips use a css class other than jqstooltip by specifying tooltipClassname |
|
defaultStyles = '.jqstooltip { ' + |
|
'position: absolute;' + |
|
'left: 0px;' + |
|
'top: 0px;' + |
|
'visibility: hidden;' + |
|
'background: rgb(0, 0, 0) transparent;' + |
|
'background-color: rgba(0,0,0,0.6);' + |
|
'filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000);' + |
|
'-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000)";' + |
|
'color: white;' + |
|
'font: 10px arial, san serif;' + |
|
'text-align: left;' + |
|
'white-space: nowrap;' + |
|
'padding: 5px;' + |
|
'border: 1px solid white;' + |
|
'box-sizing: content-box;' + |
|
'z-index: 10000;' + |
|
'}' + |
|
'.jqsfield { ' + |
|
'color: white;' + |
|
'font: 10px arial, san serif;' + |
|
'text-align: left;' + |
|
'}'; |
|
|
|
/** |
|
* Utilities |
|
*/ |
|
|
|
createClass = function (/* [baseclass, [mixin, ...]], definition */) { |
|
var Class, args; |
|
Class = function () { |
|
this.init.apply(this, arguments); |
|
}; |
|
if (arguments.length > 1) { |
|
if (arguments[0]) { |
|
Class.prototype = $.extend(new arguments[0](), arguments[arguments.length - 1]); |
|
Class._super = arguments[0].prototype; |
|
} else { |
|
Class.prototype = arguments[arguments.length - 1]; |
|
} |
|
if (arguments.length > 2) { |
|
args = Array.prototype.slice.call(arguments, 1, -1); |
|
args.unshift(Class.prototype); |
|
$.extend.apply($, args); |
|
} |
|
} else { |
|
Class.prototype = arguments[0]; |
|
} |
|
Class.prototype.cls = Class; |
|
return Class; |
|
}; |
|
|
|
/** |
|
* Wraps a format string for tooltips |
|
* {{x}} |
|
* {{x.2} |
|
* {{x:months}} |
|
*/ |
|
$.SPFormatClass = SPFormat = createClass({ |
|
fre: /\{\{([\w.]+?)(:(.+?))?\}\}/g, |
|
precre: /(\w+)\.(\d+)/, |
|
|
|
init: function (format, fclass) { |
|
this.format = format; |
|
this.fclass = fclass; |
|
}, |
|
|
|
render: function (fieldset, lookups, options) { |
|
var self = this, |
|
fields = fieldset, |
|
match, token, lookupkey, fieldvalue, prec; |
|
return this.format.replace(this.fre, function () { |
|
var lookup; |
|
token = arguments[1]; |
|
lookupkey = arguments[3]; |
|
match = self.precre.exec(token); |
|
if (match) { |
|
prec = match[2]; |
|
token = match[1]; |
|
} else { |
|
prec = false; |
|
} |
|
fieldvalue = fields[token]; |
|
if (fieldvalue === undefined) { |
|
return ''; |
|
} |
|
if (lookupkey && lookups && lookups[lookupkey]) { |
|
lookup = lookups[lookupkey]; |
|
if (lookup.get) { // RangeMap |
|
return lookups[lookupkey].get(fieldvalue) || fieldvalue; |
|
} else { |
|
return lookups[lookupkey][fieldvalue] || fieldvalue; |
|
} |
|
} |
|
if (isNumber(fieldvalue)) { |
|
if (options.get('numberFormatter')) { |
|
fieldvalue = options.get('numberFormatter')(fieldvalue); |
|
} else { |
|
fieldvalue = formatNumber(fieldvalue, prec, |
|
options.get('numberDigitGroupCount'), |
|
options.get('numberDigitGroupSep'), |
|
options.get('numberDecimalMark')); |
|
} |
|
} |
|
return fieldvalue; |
|
}); |
|
} |
|
}); |
|
|
|
// convience method to avoid needing the new operator |
|
$.spformat = function(format, fclass) { |
|
return new SPFormat(format, fclass); |
|
}; |
|
|
|
clipval = function (val, min, max) { |
|
if (val < min) { |
|
return min; |
|
} |
|
if (val > max) { |
|
return max; |
|
} |
|
return val; |
|
}; |
|
|
|
quartile = function (values, q) { |
|
var vl; |
|
if (q === 2) { |
|
vl = Math.floor(values.length / 2); |
|
return values.length % 2 ? values[vl] : (values[vl-1] + values[vl]) / 2; |
|
} else { |
|
if (values.length % 2 ) { // odd |
|
vl = (values.length * q + q) / 4; |
|
return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl-1]; |
|
} else { //even |
|
vl = (values.length * q + 2) / 4; |
|
return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl-1]; |
|
|
|
} |
|
} |
|
}; |
|
|
|
normalizeValue = function (val) { |
|
var nf; |
|
switch (val) { |
|
case 'undefined': |
|
val = undefined; |
|
break; |
|
case 'null': |
|
val = null; |
|
break; |
|
case 'true': |
|
val = true; |
|
break; |
|
case 'false': |
|
val = false; |
|
break; |
|
default: |
|
nf = parseFloat(val); |
|
if (val == nf) { |
|
val = nf; |
|
} |
|
} |
|
return val; |
|
}; |
|
|
|
normalizeValues = function (vals) { |
|
var i, result = []; |
|
for (i = vals.length; i--;) { |
|
result[i] = normalizeValue(vals[i]); |
|
} |
|
return result; |
|
}; |
|
|
|
remove = function (vals, filter) { |
|
var i, vl, result = []; |
|
for (i = 0, vl = vals.length; i < vl; i++) { |
|
if (vals[i] !== filter) { |
|
result.push(vals[i]); |
|
} |
|
} |
|
return result; |
|
}; |
|
|
|
isNumber = function (num) { |
|
return !isNaN(parseFloat(num)) && isFinite(num); |
|
}; |
|
|
|
formatNumber = function (num, prec, groupsize, groupsep, decsep) { |
|
var p, i; |
|
num = (prec === false ? parseFloat(num).toString() : num.toFixed(prec)).split(''); |
|
p = (p = $.inArray('.', num)) < 0 ? num.length : p; |
|
if (p < num.length) { |
|
num[p] = decsep; |
|
} |
|
for (i = p - groupsize; i > 0; i -= groupsize) { |
|
num.splice(i, 0, groupsep); |
|
} |
|
return num.join(''); |
|
}; |
|
|
|
// determine if all values of an array match a value |
|
// returns true if the array is empty |
|
all = function (val, arr, ignoreNull) { |
|
var i; |
|
for (i = arr.length; i--; ) { |
|
if (ignoreNull && arr[i] === null) continue; |
|
if (arr[i] !== val) { |
|
return false; |
|
} |
|
} |
|
return true; |
|
}; |
|
|
|
// sums the numeric values in an array, ignoring other values |
|
sum = function (vals) { |
|
var total = 0, i; |
|
for (i = vals.length; i--;) { |
|
total += typeof vals[i] === 'number' ? vals[i] : 0; |
|
} |
|
return total; |
|
}; |
|
|
|
ensureArray = function (val) { |
|
return $.isArray(val) ? val : [val]; |
|
}; |
|
|
|
// http://paulirish.com/2008/bookmarklet-inject-new-css-rules/ |
|
addCSS = function(css) { |
|
var tag, iefail; |
|
if (document.createStyleSheet) { |
|
try { |
|
document.createStyleSheet().cssText = css; |
|
return; |
|
} catch (e) { |
|
// IE <= 9 maxes out at 31 stylesheets; inject into page instead. |
|
iefail = true; |
|
} |
|
} |
|
tag = document.createElement('style'); |
|
tag.type = 'text/css'; |
|
document.getElementsByTagName('head')[0].appendChild(tag); |
|
if (iefail) { |
|
document.styleSheets[document.styleSheets.length - 1].cssText = css; |
|
} else { |
|
tag[(typeof document.body.style.WebkitAppearance == 'string') /* webkit only */ ? 'innerText' : 'innerHTML'] = css; |
|
} |
|
}; |
|
|
|
// Provide a cross-browser interface to a few simple drawing primitives |
|
$.fn.simpledraw = function (width, height, useExisting, interact) { |
|
var target, mhandler; |
|
if (useExisting && (target = this.data('_jqs_vcanvas'))) { |
|
return target; |
|
} |
|
|
|
if ($.fn.sparkline.canvas === false) { |
|
// We've already determined that neither Canvas nor VML are available |
|
return false; |
|
|
|
} else if ($.fn.sparkline.canvas === undefined) { |
|
// No function defined yet -- need to see if we support Canvas or VML |
|
var el = document.createElement('canvas'); |
|
if (!!(el.getContext && el.getContext('2d'))) { |
|
// Canvas is available |
|
$.fn.sparkline.canvas = function(width, height, target, interact) { |
|
return new VCanvas_canvas(width, height, target, interact); |
|
}; |
|
} else if (document.namespaces && !document.namespaces.v) { |
|
// VML is available |
|
document.namespaces.add('v', 'urn:schemas-microsoft-com:vml', '#default#VML'); |
|
$.fn.sparkline.canvas = function(width, height, target, interact) { |
|
return new VCanvas_vml(width, height, target); |
|
}; |
|
} else { |
|
// Neither Canvas nor VML are available |
|
$.fn.sparkline.canvas = false; |
|
return false; |
|
} |
|
} |
|
|
|
if (width === undefined) { |
|
width = $(this).innerWidth(); |
|
} |
|
if (height === undefined) { |
|
height = $(this).innerHeight(); |
|
} |
|
|
|
target = $.fn.sparkline.canvas(width, height, this, interact); |
|
|
|
mhandler = $(this).data('_jqs_mhandler'); |
|
if (mhandler) { |
|
mhandler.registerCanvas(target); |
|
} |
|
return target; |
|
}; |
|
|
|
$.fn.cleardraw = function () { |
|
var target = this.data('_jqs_vcanvas'); |
|
if (target) { |
|
target.reset(); |
|
} |
|
}; |
|
|
|
$.RangeMapClass = RangeMap = createClass({ |
|
init: function (map) { |
|
var key, range, rangelist = []; |
|
for (key in map) { |
|
if (map.hasOwnProperty(key) && typeof key === 'string' && key.indexOf(':') > -1) { |
|
range = key.split(':'); |
|
range[0] = range[0].length === 0 ? -Infinity : parseFloat(range[0]); |
|
range[1] = range[1].length === 0 ? Infinity : parseFloat(range[1]); |
|
range[2] = map[key]; |
|
rangelist.push(range); |
|
} |
|
} |
|
this.map = map; |
|
this.rangelist = rangelist || false; |
|
}, |
|
|
|
get: function (value) { |
|
var rangelist = this.rangelist, |
|
i, range, result; |
|
if ((result = this.map[value]) !== undefined) { |
|
return result; |
|
} |
|
if (rangelist) { |
|
for (i = rangelist.length; i--;) { |
|
range = rangelist[i]; |
|
if (range[0] <= value && range[1] >= value) { |
|
return range[2]; |
|
} |
|
} |
|
} |
|
return undefined; |
|
} |
|
}); |
|
|
|
// Convenience function |
|
$.range_map = function(map) { |
|
return new RangeMap(map); |
|
}; |
|
|
|
MouseHandler = createClass({ |
|
init: function (el, options) { |
|
var $el = $(el); |
|
this.$el = $el; |
|
this.options = options; |
|
this.currentPageX = 0; |
|
this.currentPageY = 0; |
|
this.el = el; |
|
this.splist = []; |
|
this.tooltip = null; |
|
this.over = false; |
|
this.displayTooltips = !options.get('disableTooltips'); |
|
this.highlightEnabled = !options.get('disableHighlight'); |
|
}, |
|
|
|
registerSparkline: function (sp) { |
|
this.splist.push(sp); |
|
if (this.over) { |
|
this.updateDisplay(); |
|
} |
|
}, |
|
|
|
registerCanvas: function (canvas) { |
|
var $canvas = $(canvas.canvas); |
|
this.canvas = canvas; |
|
this.$canvas = $canvas; |
|
$canvas.mouseenter($.proxy(this.mouseenter, this)); |
|
$canvas.mouseleave($.proxy(this.mouseleave, this)); |
|
$canvas.click($.proxy(this.mouseclick, this)); |
|
}, |
|
|
|
reset: function (removeTooltip) { |
|
this.splist = []; |
|
if (this.tooltip && removeTooltip) { |
|
this.tooltip.remove(); |
|
this.tooltip = undefined; |
|
} |
|
}, |
|
|
|
mouseclick: function (e) { |
|
var clickEvent = $.Event('sparklineClick'); |
|
clickEvent.originalEvent = e; |
|
clickEvent.sparklines = this.splist; |
|
this.$el.trigger(clickEvent); |
|
}, |
|
|
|
mouseenter: function (e) { |
|
$(document.body).unbind('mousemove.jqs'); |
|
$(document.body).bind('mousemove.jqs', $.proxy(this.mousemove, this)); |
|
this.over = true; |
|
this.currentPageX = e.pageX; |
|
this.currentPageY = e.pageY; |
|
this.currentEl = e.target; |
|
if (!this.tooltip && this.displayTooltips) { |
|
this.tooltip = new Tooltip(this.options); |
|
this.tooltip.updatePosition(e.pageX, e.pageY); |
|
} |
|
this.updateDisplay(); |
|
}, |
|
|
|
mouseleave: function () { |
|
$(document.body).unbind('mousemove.jqs'); |
|
var splist = this.splist, |
|
spcount = splist.length, |
|
needsRefresh = false, |
|
sp, i; |
|
this.over = false; |
|
this.currentEl = null; |
|
|
|
if (this.tooltip) { |
|
this.tooltip.remove(); |
|
this.tooltip = null; |
|
} |
|
|
|
for (i = 0; i < spcount; i++) { |
|
sp = splist[i]; |
|
if (sp.clearRegionHighlight()) { |
|
needsRefresh = true; |
|
} |
|
} |
|
|
|
if (needsRefresh) { |
|
this.canvas.render(); |
|
} |
|
}, |
|
|
|
mousemove: function (e) { |
|
this.currentPageX = e.pageX; |
|
this.currentPageY = e.pageY; |
|
this.currentEl = e.target; |
|
if (this.tooltip) { |
|
this.tooltip.updatePosition(e.pageX, e.pageY); |
|
} |
|
this.updateDisplay(); |
|
}, |
|
|
|
updateDisplay: function () { |
|
var splist = this.splist, |
|
spcount = splist.length, |
|
needsRefresh = false, |
|
offset = this.$canvas.offset(), |
|
localX = this.currentPageX - offset.left, |
|
localY = this.currentPageY - offset.top, |
|
tooltiphtml, sp, i, result, changeEvent; |
|
if (!this.over) { |
|
return; |
|
} |
|
for (i = 0; i < spcount; i++) { |
|
sp = splist[i]; |
|
result = sp.setRegionHighlight(this.currentEl, localX, localY); |
|
if (result) { |
|
needsRefresh = true; |
|
} |
|
} |
|
if (needsRefresh) { |
|
changeEvent = $.Event('sparklineRegionChange'); |
|
changeEvent.sparklines = this.splist; |
|
this.$el.trigger(changeEvent); |
|
if (this.tooltip) { |
|
tooltiphtml = ''; |
|
for (i = 0; i < spcount; i++) { |
|
sp = splist[i]; |
|
tooltiphtml += sp.getCurrentRegionTooltip(); |
|
} |
|
this.tooltip.setContent(tooltiphtml); |
|
} |
|
if (!this.disableHighlight) { |
|
this.canvas.render(); |
|
} |
|
} |
|
if (result === null) { |
|
this.mouseleave(); |
|
} |
|
} |
|
}); |
|
|
|
|
|
Tooltip = createClass({ |
|
sizeStyle: 'position: static !important;' + |
|
'display: block !important;' + |
|
'visibility: hidden !important;' + |
|
'float: left !important;', |
|
|
|
init: function (options) { |
|
var tooltipClassname = options.get('tooltipClassname', 'jqstooltip'), |
|
sizetipStyle = this.sizeStyle, |
|
offset; |
|
this.container = options.get('tooltipContainer') || document.body; |
|
this.tooltipOffsetX = options.get('tooltipOffsetX', 10); |
|
this.tooltipOffsetY = options.get('tooltipOffsetY', 12); |
|
// remove any previous lingering tooltip |
|
$('#jqssizetip').remove(); |
|
$('#jqstooltip').remove(); |
|
this.sizetip = $('<div/>', { |
|
id: 'jqssizetip', |
|
style: sizetipStyle, |
|
'class': tooltipClassname |
|
}); |
|
this.tooltip = $('<div/>', { |
|
id: 'jqstooltip', |
|
'class': tooltipClassname |
|
}).appendTo(this.container); |
|
// account for the container's location |
|
offset = this.tooltip.offset(); |
|
this.offsetLeft = offset.left; |
|
this.offsetTop = offset.top; |
|
this.hidden = true; |
|
$(window).unbind('resize.jqs scroll.jqs'); |
|
$(window).bind('resize.jqs scroll.jqs', $.proxy(this.updateWindowDims, this)); |
|
this.updateWindowDims(); |
|
}, |
|
|
|
updateWindowDims: function () { |
|
this.scrollTop = $(window).scrollTop(); |
|
this.scrollLeft = $(window).scrollLeft(); |
|
this.scrollRight = this.scrollLeft + $(window).width(); |
|
this.updatePosition(); |
|
}, |
|
|
|
getSize: function (content) { |
|
this.sizetip.html(content).appendTo(this.container); |
|
this.width = this.sizetip.width() + 1; |
|
this.height = this.sizetip.height(); |
|
this.sizetip.remove(); |
|
}, |
|
|
|
setContent: function (content) { |
|
if (!content) { |
|
this.tooltip.css('visibility', 'hidden'); |
|
this.hidden = true; |
|
return; |
|
} |
|
this.getSize(content); |
|
this.tooltip.html(content) |
|
.css({ |
|
'width': this.width, |
|
'height': this.height, |
|
'visibility': 'visible' |
|
}); |
|
if (this.hidden) { |
|
this.hidden = false; |
|
this.updatePosition(); |
|
} |
|
}, |
|
|
|
updatePosition: function (x, y) { |
|
if (x === undefined) { |
|
if (this.mousex === undefined) { |
|
return; |
|
} |
|
x = this.mousex - this.offsetLeft; |
|
y = this.mousey - this.offsetTop; |
|
|
|
} else { |
|
this.mousex = x = x - this.offsetLeft; |
|
this.mousey = y = y - this.offsetTop; |
|
} |
|
if (!this.height || !this.width || this.hidden) { |
|
return; |
|
} |
|
|
|
y -= this.height + this.tooltipOffsetY; |
|
x += this.tooltipOffsetX; |
|
|
|
if (y < this.scrollTop) { |
|
y = this.scrollTop; |
|
} |
|
if (x < this.scrollLeft) { |
|
x = this.scrollLeft; |
|
} else if (x + this.width > this.scrollRight) { |
|
x = this.scrollRight - this.width; |
|
} |
|
|
|
this.tooltip.css({ |
|
'left': x, |
|
'top': y |
|
}); |
|
}, |
|
|
|
remove: function () { |
|
this.tooltip.remove(); |
|
this.sizetip.remove(); |
|
this.sizetip = this.tooltip = undefined; |
|
$(window).unbind('resize.jqs scroll.jqs'); |
|
} |
|
}); |
|
|
|
initStyles = function() { |
|
addCSS(defaultStyles); |
|
}; |
|
|
|
$(initStyles); |
|
|
|
pending = []; |
|
$.fn.sparkline = function (userValues, userOptions) { |
|
return this.each(function () { |
|
var options = new $.fn.sparkline.options(this, userOptions), |
|
$this = $(this), |
|
render, i; |
|
render = function () { |
|
var values, width, height, tmp, mhandler, sp, vals; |
|
if (userValues === 'html' || userValues === undefined) { |
|
vals = this.getAttribute(options.get('tagValuesAttribute')); |
|
if (vals === undefined || vals === null) { |
|
vals = $this.html(); |
|
} |
|
values = vals.replace(/(^\s*<!--)|(-->\s*$)|\s+/g, '').split(','); |
|
} else { |
|
values = userValues; |
|
} |
|
|
|
width = options.get('width') === 'auto' ? values.length * options.get('defaultPixelsPerValue') : options.get('width'); |
|
if (options.get('height') === 'auto') { |
|
if (!options.get('composite') || !$.data(this, '_jqs_vcanvas')) { |
|
// must be a better way to get the line height |
|
tmp = document.createElement('span'); |
|
tmp.innerHTML = 'a'; |
|
$this.html(tmp); |
|
height = $(tmp).innerHeight() || $(tmp).height(); |
|
$(tmp).remove(); |
|
tmp = null; |
|
} |
|
} else { |
|
height = options.get('height'); |
|
} |
|
|
|
if (!options.get('disableInteraction')) { |
|
mhandler = $.data(this, '_jqs_mhandler'); |
|
if (!mhandler) { |
|
mhandler = new MouseHandler(this, options); |
|
$.data(this, '_jqs_mhandler', mhandler); |
|
} else if (!options.get('composite')) { |
|
mhandler.reset(); |
|
} |
|
} else { |
|
mhandler = false; |
|
} |
|
|
|
if (options.get('composite') && !$.data(this, '_jqs_vcanvas')) { |
|
if (!$.data(this, '_jqs_errnotify')) { |
|
alert('Attempted to attach a composite sparkline to an element with no existing sparkline'); |
|
$.data(this, '_jqs_errnotify', true); |
|
} |
|
return; |
|
} |
|
|
|
sp = new $.fn.sparkline[options.get('type')](this, values, options, width, height); |
|
|
|
sp.render(); |
|
|
|
if (mhandler) { |
|
mhandler.registerSparkline(sp); |
|
} |
|
}; |
|
if (($(this).html() && !options.get('disableHiddenCheck') && $(this).is(':hidden')) || !$(this).parents('body').length) { |
|
if (!options.get('composite') && $.data(this, '_jqs_pending')) { |
|
// remove any existing references to the element |
|
for (i = pending.length; i; i--) { |
|
if (pending[i - 1][0] == this) { |
|
pending.splice(i - 1, 1); |
|
} |
|
} |
|
} |
|
pending.push([this, render]); |
|
$.data(this, '_jqs_pending', true); |
|
} else { |
|
render.call(this); |
|
} |
|
}); |
|
}; |
|
|
|
$.fn.sparkline.defaults = getDefaults(); |
|
|
|
|
|
$.sparkline_display_visible = function () { |
|
var el, i, pl; |
|
var done = []; |
|
for (i = 0, pl = pending.length; i < pl; i++) { |
|
el = pending[i][0]; |
|
if ($(el).is(':visible') && !$(el).parents().is(':hidden')) { |
|
pending[i][1].call(el); |
|
$.data(pending[i][0], '_jqs_pending', false); |
|
done.push(i); |
|
} else if (!$(el).closest('html').length && !$.data(el, '_jqs_pending')) { |
|
// element has been inserted and removed from the DOM |
|
// If it was not yet inserted into the dom then the .data request |
|
// will return true. |
|
// removing from the dom causes the data to be removed. |
|
$.data(pending[i][0], '_jqs_pending', false); |
|
done.push(i); |
|
} |
|
} |
|
for (i = done.length; i; i--) { |
|
pending.splice(done[i - 1], 1); |
|
} |
|
}; |
|
|
|
|
|
/** |
|
* User option handler |
|
*/ |
|
$.fn.sparkline.options = createClass({ |
|
init: function (tag, userOptions) { |
|
var extendedOptions, defaults, base, tagOptionType; |
|
this.userOptions = userOptions = userOptions || {}; |
|
this.tag = tag; |
|
this.tagValCache = {}; |
|
defaults = $.fn.sparkline.defaults; |
|
base = defaults.common; |
|
this.tagOptionsPrefix = userOptions.enableTagOptions && (userOptions.tagOptionsPrefix || base.tagOptionsPrefix); |
|
|
|
tagOptionType = this.getTagSetting('type'); |
|
if (tagOptionType === UNSET_OPTION) { |
|
extendedOptions = defaults[userOptions.type || base.type]; |
|
} else { |
|
extendedOptions = defaults[tagOptionType]; |
|
} |
|
this.mergedOptions = $.extend({}, base, extendedOptions, userOptions); |
|
}, |
|
|
|
|
|
getTagSetting: function (key) { |
|
var prefix = this.tagOptionsPrefix, |
|
val, i, pairs, keyval; |
|
if (prefix === false || prefix === undefined) { |
|
return UNSET_OPTION; |
|
} |
|
if (this.tagValCache.hasOwnProperty(key)) { |
|
val = this.tagValCache.key; |
|
} else { |
|
val = this.tag.getAttribute(prefix + key); |
|
if (val === undefined || val === null) { |
|
val = UNSET_OPTION; |
|
} else if (val.substr(0, 1) === '[') { |
|
val = val.substr(1, val.length - 2).split(','); |
|
for (i = val.length; i--;) { |
|
val[i] = normalizeValue(val[i].replace(/(^\s*)|(\s*$)/g, '')); |
|
} |
|
} else if (val.substr(0, 1) === '{') { |
|
pairs = val.substr(1, val.length - 2).split(','); |
|
val = {}; |
|
for (i = pairs.length; i--;) { |
|
keyval = pairs[i].split(':', 2); |
|
val[keyval[0].replace(/(^\s*)|(\s*$)/g, '')] = normalizeValue(keyval[1].replace(/(^\s*)|(\s*$)/g, '')); |
|
} |
|
} else { |
|
val = normalizeValue(val); |
|
} |
|
this.tagValCache.key = val; |
|
} |
|
return val; |
|
}, |
|
|
|
get: function (key, defaultval) { |
|
var tagOption = this.getTagSetting(key), |
|
result; |
|
if (tagOption !== UNSET_OPTION) { |
|
return tagOption; |
|
} |
|
return (result = this.mergedOptions[key]) === undefined ? defaultval : result; |
|
} |
|
}); |
|
|
|
|
|
$.fn.sparkline._base = createClass({ |
|
disabled: false, |
|
|
|
init: function (el, values, options, width, height) { |
|
this.el = el; |
|
this.$el = $(el); |
|
this.values = values; |
|
this.options = options; |
|
this.width = width; |
|
this.height = height; |
|
this.currentRegion = undefined; |
|
}, |
|
|
|
/** |
|
* Setup the canvas |
|
*/ |
|
initTarget: function () { |
|
var interactive = !this.options.get('disableInteraction'); |
|
if (!(this.target = this.$el.simpledraw(this.width, this.height, this.options.get('composite'), interactive))) { |
|
this.disabled = true; |
|
} else { |
|
this.canvasWidth = this.target.pixelWidth; |
|
this.canvasHeight = this.target.pixelHeight; |
|
} |
|
}, |
|
|
|
/** |
|
* Actually render the chart to the canvas |
|
*/ |
|
render: function () { |
|
if (this.disabled) { |
|
this.el.innerHTML = ''; |
|
return false; |
|
} |
|
return true; |
|
}, |
|
|
|
/** |
|
* Return a region id for a given x/y co-ordinate |
|
*/ |
|
getRegion: function (x, y) { |
|
}, |
|
|
|
/** |
|
* Highlight an item based on the moused-over x,y co-ordinate |
|
*/ |
|
setRegionHighlight: function (el, x, y) { |
|
var currentRegion = this.currentRegion, |
|
highlightEnabled = !this.options.get('disableHighlight'), |
|
newRegion; |
|
if (x > this.canvasWidth || y > this.canvasHeight || x < 0 || y < 0) { |
|
return null; |
|
} |
|
newRegion = this.getRegion(el, x, y); |
|
if (currentRegion !== newRegion) { |
|
if (currentRegion !== undefined && highlightEnabled) { |
|
this.removeHighlight(); |
|
} |
|
this.currentRegion = newRegion; |
|
if (newRegion !== undefined && highlightEnabled) { |
|
this.renderHighlight(); |
|
} |
|
return true; |
|
} |
|
return false; |
|
}, |
|
|
|
/** |
|
* Reset any currently highlighted item |
|
*/ |
|
clearRegionHighlight: function () { |
|
if (this.currentRegion !== undefined) { |
|
this.removeHighlight(); |
|
this.currentRegion = undefined; |
|
return true; |
|
} |
|
return false; |
|
}, |
|
|
|
renderHighlight: function () { |
|
this.changeHighlight(true); |
|
}, |
|
|
|
removeHighlight: function () { |
|
this.changeHighlight(false); |
|
}, |
|
|
|
changeHighlight: function (highlight) {}, |
|
|
|
/** |
|
* Fetch the HTML to display as a tooltip |
|
*/ |
|
getCurrentRegionTooltip: function () { |
|
var options = this.options, |
|
header = '', |
|
entries = [], |
|
fields, formats, formatlen, fclass, text, i, |
|
showFields, showFieldsKey, newFields, fv, |
|
formatter, format, fieldlen, j; |
|
if (this.currentRegion === undefined) { |
|
return ''; |
|
} |
|
fields = this.getCurrentRegionFields(); |
|
formatter = options.get('tooltipFormatter'); |
|
if (formatter) { |
|
return formatter(this, options, fields); |
|
} |
|
if (options.get('tooltipChartTitle')) { |
|
header += '<div class="jqs jqstitle">' + options.get('tooltipChartTitle') + '</div>\n'; |
|
} |
|
formats = this.options.get('tooltipFormat'); |
|
if (!formats) { |
|
return ''; |
|
} |
|
if (!$.isArray(formats)) { |
|
formats = [formats]; |
|
} |
|
if (!$.isArray(fields)) { |
|
fields = [fields]; |
|
} |
|
showFields = this.options.get('tooltipFormatFieldlist'); |
|
showFieldsKey = this.options.get('tooltipFormatFieldlistKey'); |
|
if (showFields && showFieldsKey) { |
|
// user-selected ordering of fields |
|
newFields = []; |
|
for (i = fields.length; i--;) { |
|
fv = fields[i][showFieldsKey]; |
|
if ((j = $.inArray(fv, showFields)) != -1) { |
|
newFields[j] = fields[i]; |
|
} |
|
} |
|
fields = newFields; |
|
} |
|
formatlen = formats.length; |
|
fieldlen = fields.length; |
|
for (i = 0; i < formatlen; i++) { |
|
format = formats[i]; |
|
if (typeof format === 'string') { |
|
format = new SPFormat(format); |
|
} |
|
fclass = format.fclass || 'jqsfield'; |
|
for (j = 0; j < fieldlen; j++) { |
|
if (!fields[j].isNull || !options.get('tooltipSkipNull')) { |
|
$.extend(fields[j], { |
|
prefix: options.get('tooltipPrefix'), |
|
suffix: options.get('tooltipSuffix') |
|
}); |
|
text = format.render(fields[j], options.get('tooltipValueLookups'), options); |
|
entries.push('<div class="' + fclass + '">' + text + '</div>'); |
|
} |
|
} |
|
} |
|
if (entries.length) { |
|
return header + entries.join('\n'); |
|
} |
|
return ''; |
|
}, |
|
|
|
getCurrentRegionFields: function () {}, |
|
|
|
calcHighlightColor: function (color, options) { |
|
var highlightColor = options.get('highlightColor'), |
|
lighten = options.get('highlightLighten'), |
|
parse, mult, rgbnew, i; |
|
if (highlightColor) { |
|
return highlightColor; |
|
} |
|
if (lighten) { |
|
// extract RGB values |
|
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); |
|
if (parse) { |
|
rgbnew = []; |
|
mult = color.length === 4 ? 16 : 1; |
|
for (i = 0; i < 3; i++) { |
|
rgbnew[i] = clipval(Math.round(parseInt(parse[i + 1], 16) * mult * lighten), 0, 255); |
|
} |
|
return 'rgb(' + rgbnew.join(',') + ')'; |
|
} |
|
|
|
} |
|
return color; |
|
} |
|
|
|
}); |
|
|
|
barHighlightMixin = { |
|
changeHighlight: function (highlight) { |
|
var currentRegion = this.currentRegion, |
|
target = this.target, |
|
shapeids = this.regionShapes[currentRegion], |
|
newShapes; |
|
// will be null if the region value was null |
|
if (shapeids) { |
|
newShapes = this.renderRegion(currentRegion, highlight); |
|
if ($.isArray(newShapes) || $.isArray(shapeids)) { |
|
target.replaceWithShapes(shapeids, newShapes); |
|
this.regionShapes[currentRegion] = $.map(newShapes, function (newShape) { |
|
return newShape.id; |
|
}); |
|
} else { |
|
target.replaceWithShape(shapeids, newShapes); |
|
this.regionShapes[currentRegion] = newShapes.id; |
|
} |
|
} |
|
}, |
|
|
|
render: function () { |
|
var values = this.values, |
|
target = this.target, |
|
regionShapes = this.regionShapes, |
|
shapes, ids, i, j; |
|
|
|
if (!this.cls._super.render.call(this)) { |
|
return; |
|
} |
|
for (i = values.length; i--;) { |
|
shapes = this.renderRegion(i); |
|
if (shapes) { |
|
if ($.isArray(shapes)) { |
|
ids = []; |
|
for (j = shapes.length; j--;) { |
|
shapes[j].append(); |
|
ids.push(shapes[j].id); |
|
} |
|
regionShapes[i] = ids; |
|
} else { |
|
shapes.append(); |
|
regionShapes[i] = shapes.id; // store just the shapeid |
|
} |
|
} else { |
|
// null value |
|
regionShapes[i] = null; |
|
} |
|
} |
|
target.render(); |
|
} |
|
}; |
|
|
|
/** |
|
* Line charts |
|
*/ |
|
$.fn.sparkline.line = line = createClass($.fn.sparkline._base, { |
|
type: 'line', |
|
|
|
init: function (el, values, options, width, height) { |
|
line._super.init.call(this, el, values, options, width, height); |
|
this.vertices = []; |
|
this.regionMap = []; |
|
this.xvalues = []; |
|
this.yvalues = []; |
|
this.yminmax = []; |
|
this.hightlightSpotId = null; |
|
this.lastShapeId = null; |
|
this.initTarget(); |
|
}, |
|
|
|
getRegion: function (el, x, y) { |
|
var i, |
|
regionMap = this.regionMap; // maps regions to value positions |
|
for (i = regionMap.length; i--;) { |
|
if (regionMap[i] !== null && x >= regionMap[i][0] && x <= regionMap[i][1]) { |
|
return regionMap[i][2]; |
|
} |
|
} |
|
return undefined; |
|
}, |
|
|
|
getCurrentRegionFields: function () { |
|
var currentRegion = this.currentRegion; |
|
return { |
|
isNull: this.yvalues[currentRegion] === null, |
|
x: this.xvalues[currentRegion], |
|
y: this.yvalues[currentRegion], |
|
color: this.options.get('lineColor'), |
|
fillColor: this.options.get('fillColor'), |
|
offset: currentRegion |
|
}; |
|
}, |
|
|
|
renderHighlight: function () { |
|
var currentRegion = this.currentRegion, |
|
target = this.target, |
|
vertex = this.vertices[currentRegion], |
|
options = this.options, |
|
spotRadius = options.get('spotRadius'), |
|
highlightSpotColor = options.get('highlightSpotColor'), |
|
highlightLineColor = options.get('highlightLineColor'), |
|
highlightSpot, highlightLine; |
|
|
|
if (!vertex) { |
|
return; |
|
} |
|
if (spotRadius && highlightSpotColor) { |
|
highlightSpot = target.drawCircle(vertex[0], vertex[1], |
|
spotRadius, undefined, highlightSpotColor); |
|
this.highlightSpotId = highlightSpot.id; |
|
target.insertAfterShape(this.lastShapeId, highlightSpot); |
|
} |
|
if (highlightLineColor) { |
|
highlightLine = target.drawLine(vertex[0], this.canvasTop, vertex[0], |
|
this.canvasTop + this.canvasHeight, highlightLineColor); |
|
this.highlightLineId = highlightLine.id; |
|
target.insertAfterShape(this.lastShapeId, highlightLine); |
|
} |
|
}, |
|
|
|
removeHighlight: function () { |
|
var target = this.target; |
|
if (this.highlightSpotId) { |
|
target.removeShapeId(this.highlightSpotId); |
|
this.highlightSpotId = null; |
|
} |
|
if (this.highlightLineId) { |
|
target.removeShapeId(this.highlightLineId); |
|
this.highlightLineId = null; |
|
} |
|
}, |
|
|
|
scanValues: function () { |
|
var values = this.values, |
|
valcount = values.length, |
|
xvalues = this.xvalues, |
|
yvalues = this.yvalues, |
|
yminmax = this.yminmax, |
|
i, val, isStr, isArray, sp; |
|
for (i = 0; i < valcount; i++) { |
|
val = values[i]; |
|
isStr = typeof(values[i]) === 'string'; |
|
isArray = typeof(values[i]) === 'object' && values[i] instanceof Array; |
|
sp = isStr && values[i].split(':'); |
|
if (isStr && sp.length === 2) { // x:y |
|
xvalues.push(Number(sp[0])); |
|
yvalues.push(Number(sp[1])); |
|
yminmax.push(Number(sp[1])); |
|
} else if (isArray) { |
|
xvalues.push(val[0]); |
|
yvalues.push(val[1]); |
|
yminmax.push(val[1]); |
|
} else { |
|
xvalues.push(i); |
|
if (values[i] === null || values[i] === 'null') { |
|
yvalues.push(null); |
|
} else { |
|
yvalues.push(Number(val)); |
|
yminmax.push(Number(val)); |
|
} |
|
} |
|
} |
|
if (this.options.get('xvalues')) { |
|
xvalues = this.options.get('xvalues'); |
|
} |
|
|
|
this.maxy = this.maxyorg = Math.max.apply(Math, yminmax); |
|
this.miny = this.minyorg = Math.min.apply(Math, yminmax); |
|
|
|
this.maxx = Math.max.apply(Math, xvalues); |
|
this.minx = Math.min.apply(Math, xvalues); |
|
|
|
this.xvalues = xvalues; |
|
this.yvalues = yvalues; |
|
this.yminmax = yminmax; |
|
|
|
}, |
|
|
|
processRangeOptions: function () { |
|
var options = this.options, |
|
normalRangeMin = options.get('normalRangeMin'), |
|
normalRangeMax = options.get('normalRangeMax'); |
|
|
|
if (normalRangeMin !== undefined) { |
|
if (normalRangeMin < this.miny) { |
|
this.miny = normalRangeMin; |
|
} |
|
if (normalRangeMax > this.maxy) { |
|
this.maxy = normalRangeMax; |
|
} |
|
} |
|
if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.miny)) { |
|
this.miny = options.get('chartRangeMin'); |
|
} |
|
if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.maxy)) { |
|
this.maxy = options.get('chartRangeMax'); |
|
} |
|
if (options.get('chartRangeMinX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMinX') < this.minx)) { |
|
this.minx = options.get('chartRangeMinX'); |
|
} |
|
if (options.get('chartRangeMaxX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMaxX') > this.maxx)) { |
|
this.maxx = options.get('chartRangeMaxX'); |
|
} |
|
|
|
}, |
|
|
|
drawNormalRange: function (canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey) { |
|
var normalRangeMin = this.options.get('normalRangeMin'), |
|
normalRangeMax = this.options.get('normalRangeMax'), |
|
ytop = canvasTop + Math.round(canvasHeight - (canvasHeight * ((normalRangeMax - this.miny) / rangey))), |
|
height = Math.round((canvasHeight * (normalRangeMax - normalRangeMin)) / rangey); |
|
this.target.drawRect(canvasLeft, ytop, canvasWidth, height, undefined, this.options.get('normalRangeColor')).append(); |
|
}, |
|
|
|
render: function () { |
|
var options = this.options, |
|
target = this.target, |
|
canvasWidth = this.canvasWidth, |
|
canvasHeight = this.canvasHeight, |
|
vertices = this.vertices, |
|
spotRadius = options.get('spotRadius'), |
|
regionMap = this.regionMap, |
|
rangex, rangey, yvallast, |
|
canvasTop, canvasLeft, |
|
vertex, path, paths, x, y, xnext, xpos, xposnext, |
|
last, next, yvalcount, lineShapes, fillShapes, plen, |
|
valueSpots, hlSpotsEnabled, color, xvalues, yvalues, i; |
|
|
|
if (!line._super.render.call(this)) { |
|
return; |
|
} |
|
|
|
this.scanValues(); |
|
this.processRangeOptions(); |
|
|
|
xvalues = this.xvalues; |
|
yvalues = this.yvalues; |
|
|
|
if (!this.yminmax.length || this.yvalues.length < 2) { |
|
// empty or all null valuess |
|
return; |
|
} |
|
|
|
canvasTop = canvasLeft = 0; |
|
|
|
rangex = this.maxx - this.minx === 0 ? 1 : this.maxx - this.minx; |
|
rangey = this.maxy - this.miny === 0 ? 1 : this.maxy - this.miny; |
|
yvallast = this.yvalues.length - 1; |
|
|
|
if (spotRadius && (canvasWidth < (spotRadius * 4) || canvasHeight < (spotRadius * 4))) { |
|
spotRadius = 0; |
|
} |
|
if (spotRadius) { |
|
// adjust the canvas size as required so that spots will fit |
|
hlSpotsEnabled = options.get('highlightSpotColor') && !options.get('disableInteraction'); |
|
if (hlSpotsEnabled || options.get('minSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.miny)) { |
|
canvasHeight -= Math.ceil(spotRadius); |
|
} |
|
if (hlSpotsEnabled || options.get('maxSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.maxy)) { |
|
canvasHeight -= Math.ceil(spotRadius); |
|
canvasTop += Math.ceil(spotRadius); |
|
} |
|
if (hlSpotsEnabled || |
|
((options.get('minSpotColor') || options.get('maxSpotColor')) && (yvalues[0] === this.miny || yvalues[0] === this.maxy))) { |
|
canvasLeft += Math.ceil(spotRadius); |
|
canvasWidth -= Math.ceil(spotRadius); |
|
} |
|
if (hlSpotsEnabled || options.get('spotColor') || |
|
(options.get('minSpotColor') || options.get('maxSpotColor') && |
|
(yvalues[yvallast] === this.miny || yvalues[yvallast] === this.maxy))) { |
|
canvasWidth -= Math.ceil(spotRadius); |
|
} |
|
} |
|
|
|
|
|
canvasHeight--; |
|
|
|
if (options.get('normalRangeMin') !== undefined && !options.get('drawNormalOnTop')) { |
|
this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey); |
|
} |
|
|
|
path = []; |
|
paths = [path]; |
|
last = next = null; |
|
yvalcount = yvalues.length; |
|
for (i = 0; i < yvalcount; i++) { |
|
x = xvalues[i]; |
|
xnext = xvalues[i + 1]; |
|
y = yvalues[i]; |
|
xpos = canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)); |
|
xposnext = i < yvalcount - 1 ? canvasLeft + Math.round((xnext - this.minx) * (canvasWidth / rangex)) : canvasWidth; |
|
next = xpos + ((xposnext - xpos) / 2); |
|
regionMap[i] = [last || 0, next, i]; |
|
last = next; |
|
if (y === null) { |
|
if (i) { |
|
if (yvalues[i - 1] !== null) { |
|
path = []; |
|
paths.push(path); |
|
} |
|
vertices.push(null); |
|
} |
|
} else { |
|
if (y < this.miny) { |
|
y = this.miny; |
|
} |
|
if (y > this.maxy) { |
|
y = this.maxy; |
|
} |
|
if (!path.length) { |
|
// previous value was null |
|
path.push([xpos, canvasTop + canvasHeight]); |
|
} |
|
vertex = [xpos, canvasTop + Math.round(canvasHeight - (canvasHeight * ((y - this.miny) / rangey)))]; |
|
path.push(vertex); |
|
vertices.push(vertex); |
|
} |
|
} |
|
|
|
lineShapes = []; |
|
fillShapes = []; |
|
plen = paths.length; |
|
for (i = 0; i < plen; i++) { |
|
path = paths[i]; |
|
if (path.length) { |
|
if (options.get('fillColor')) { |
|
path.push([path[path.length - 1][0], (canvasTop + canvasHeight)]); |
|
fillShapes.push(path.slice(0)); |
|
path.pop(); |
|
} |
|
// if there's only a single point in this path, then we want to display it |
|
// as a vertical line which means we keep path[0] as is |
|
if (path.length > 2) { |
|
// else we want the first value |
|
path[0] = [path[0][0], path[1][1]]; |
|
} |
|
lineShapes.push(path); |
|
} |
|
} |
|
|
|
// draw the fill first, then optionally the normal range, then the line on top of that |
|
plen = fillShapes.length; |
|
for (i = 0; i < plen; i++) { |
|
target.drawShape(fillShapes[i], |
|
options.get('fillColor'), options.get('fillColor')).append(); |
|
} |
|
|
|
if (options.get('normalRangeMin') !== undefined && options.get('drawNormalOnTop')) { |
|
this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey); |
|
} |
|
|
|
plen = lineShapes.length; |
|
for (i = 0; i < plen; i++) { |
|
target.drawShape(lineShapes[i], options.get('lineColor'), undefined, |
|
options.get('lineWidth')).append(); |
|
} |
|
|
|
if (spotRadius && options.get('valueSpots')) { |
|
valueSpots = options.get('valueSpots'); |
|
if (valueSpots.get === undefined) { |
|
valueSpots = new RangeMap(valueSpots); |
|
} |
|
for (i = 0; i < yvalcount; i++) { |
|
color = valueSpots.get(yvalues[i]); |
|
if (color) { |
|
target.drawCircle(canvasLeft + Math.round((xvalues[i] - this.minx) * (canvasWidth / rangex)), |
|
canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[i] - this.miny) / rangey))), |
|
spotRadius, undefined, |
|
color).append(); |
|
} |
|
} |
|
|
|
} |
|
if (spotRadius && options.get('spotColor') && yvalues[yvallast] !== null) { |
|
target.drawCircle(canvasLeft + Math.round((xvalues[xvalues.length - 1] - this.minx) * (canvasWidth / rangex)), |
|
canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[yvallast] - this.miny) / rangey))), |
|
spotRadius, undefined, |
|
options.get('spotColor')).append(); |
|
} |
|
if (this.maxy !== this.minyorg) { |
|
if (spotRadius && options.get('minSpotColor')) { |
|
x = xvalues[$.inArray(this.minyorg, yvalues)]; |
|
target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)), |
|
canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.minyorg - this.miny) / rangey))), |
|
spotRadius, undefined, |
|
options.get('minSpotColor')).append(); |
|
} |
|
if (spotRadius && options.get('maxSpotColor')) { |
|
x = xvalues[$.inArray(this.maxyorg, yvalues)]; |
|
target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)), |
|
canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.maxyorg - this.miny) / rangey))), |
|
spotRadius, undefined, |
|
options.get('maxSpotColor')).append(); |
|
} |
|
} |
|
|
|
this.lastShapeId = target.getLastShapeId(); |
|
this.canvasTop = canvasTop; |
|
target.render(); |
|
} |
|
}); |
|
|
|
/** |
|
* Bar charts |
|
*/ |
|
$.fn.sparkline.bar = bar = createClass($.fn.sparkline._base, barHighlightMixin, { |
|
type: 'bar', |
|
|
|
init: function (el, values, options, width, height) { |
|
var barWidth = parseInt(options.get('barWidth'), 10), |
|
barSpacing = parseInt(options.get('barSpacing'), 10), |
|
chartRangeMin = options.get('chartRangeMin'), |
|
chartRangeMax = options.get('chartRangeMax'), |
|
chartRangeClip = options.get('chartRangeClip'), |
|
stackMin = Infinity, |
|
stackMax = -Infinity, |
|
isStackString, groupMin, groupMax, stackRanges, |
|
numValues, i, vlen, range, zeroAxis, xaxisOffset, min, max, clipMin, clipMax, |
|
stacked, vlist, j, slen, svals, val, yoffset, yMaxCalc, canvasHeightEf; |
|
bar._super.init.call(this, el, values, options, width, height); |
|
|
|
// scan values to determine whether to stack bars |
|
for (i = 0, vlen = values.length; i < vlen; i++) { |
|
val = values[i]; |
|
isStackString = typeof(val) === 'string' && val.indexOf(':') > -1; |
|
if (isStackString || $.isArray(val)) { |
|
stacked = true; |
|
if (isStackString) { |
|
val = values[i] = normalizeValues(val.split(':')); |
|
} |
|
val = remove(val, null); // min/max will treat null as zero |
|
groupMin = Math.min.apply(Math, val); |
|
groupMax = Math.max.apply(Math, val); |
|
if (groupMin < stackMin) { |
|
stackMin = groupMin; |
|
} |
|
if (groupMax > stackMax) { |
|
stackMax = groupMax; |
|
} |
|
} |
|
} |
|
|
|
this.stacked = stacked; |
|
this.regionShapes = {}; |
|
this.barWidth = barWidth; |
|
this.barSpacing = barSpacing; |
|
this.totalBarWidth = barWidth + barSpacing; |
|
this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing); |
|
|
|
this.initTarget(); |
|
|
|
if (chartRangeClip) { |
|
clipMin = chartRangeMin === undefined ? -Infinity : chartRangeMin; |
|
clipMax = chartRangeMax === undefined ? Infinity : chartRangeMax; |
|
} |
|
|
|
numValues = []; |
|
stackRanges = stacked ? [] : numValues; |
|
var stackTotals = []; |
|
var stackRangesNeg = []; |
|
for (i = 0, vlen = values.length; i < vlen; i++) { |
|
if (stacked) { |
|
vlist = values[i]; |
|
values[i] = svals = []; |
|
stackTotals[i] = 0; |
|
stackRanges[i] = stackRangesNeg[i] = 0; |
|
for (j = 0, slen = vlist.length; j < slen; j++) { |
|
val = svals[j] = chartRangeClip ? clipval(vlist[j], clipMin, clipMax) : vlist[j]; |
|
if (val !== null) { |
|
if (val > 0) { |
|
stackTotals[i] += val; |
|
} |
|
if (stackMin < 0 && stackMax > 0) { |
|
if (val < 0) { |
|
stackRangesNeg[i] += Math.abs(val); |
|
} else { |
|
stackRanges[i] += val; |
|
} |
|
} else { |
|
stackRanges[i] += Math.abs(val - (val < 0 ? stackMax : stackMin)); |
|
} |
|
numValues.push(val); |
|
} |
|
} |
|
} else { |
|
val = chartRangeClip ? clipval(values[i], clipMin, clipMax) : values[i]; |
|
val = values[i] = normalizeValue(val); |
|
if (val !== null) { |
|
numValues.push(val); |
|
} |
|
} |
|
} |
|
this.max = max = Math.max.apply(Math, numValues); |
|
this.min = min = Math.min.apply(Math, numValues); |
|
this.stackMax = stackMax = stacked ? Math.max.apply(Math, stackTotals) : max; |
|
this.stackMin = stackMin = stacked ? Math.min.apply(Math, numValues) : min; |
|
|
|
if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < min)) { |
|
min = options.get('chartRangeMin'); |
|
} |
|
if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > max)) { |
|
max = options.get('chartRangeMax'); |
|
} |
|
|
|
this.zeroAxis = zeroAxis = options.get('zeroAxis', true); |
|
if (min <= 0 && max >= 0 && zeroAxis) { |
|
xaxisOffset = 0; |
|
} else if (zeroAxis == false) { |
|
xaxisOffset = min; |
|
} else if (min > 0) { |
|
xaxisOffset = min; |
|
} else { |
|
xaxisOffset = max; |
|
} |
|
this.xaxisOffset = xaxisOffset; |
|
|
|
range = stacked ? (Math.max.apply(Math, stackRanges) + Math.max.apply(Math, stackRangesNeg)) : max - min; |
|
|
|
// as we plot zero/min values a single pixel line, we add a pixel to all other |
|
// values - Reduce the effective canvas size to suit |
|
this.canvasHeightEf = (zeroAxis && min < 0) ? this.canvasHeight - 2 : this.canvasHeight - 1; |
|
|
|
if (min < xaxisOffset) { |
|
yMaxCalc = (stacked && max >= 0) ? stackMax : max; |
|
yoffset = (yMaxCalc - xaxisOffset) / range * this.canvasHeight; |
|
if (yoffset !== Math.ceil(yoffset)) { |
|
this.canvasHeightEf -= 2; |
|
yoffset = Math.ceil(yoffset); |
|
} |
|
} else { |
|
yoffset = this.canvasHeight; |
|
} |
|
this.yoffset = yoffset; |
|
|
|
if ($.isArray(options.get('colorMap'))) { |
|
this.colorMapByIndex = options.get('colorMap'); |
|
this.colorMapByValue = null; |
|
} else { |
|
this.colorMapByIndex = null; |
|
this.colorMapByValue = options.get('colorMap'); |
|
if (this.colorMapByValue && this.colorMapByValue.get === undefined) { |
|
this.colorMapByValue = new RangeMap(this.colorMapByValue); |
|
} |
|
} |
|
|
|
this.range = range; |
|
}, |
|
|
|
getRegion: function (el, x, y) { |
|
var result = Math.floor(x / this.totalBarWidth); |
|
return (result < 0 || result >= this.values.length) ? undefined : result; |
|
}, |
|
|
|
getCurrentRegionFields: function () { |
|
var currentRegion = this.currentRegion, |
|
values = ensureArray(this.values[currentRegion]), |
|
result = [], |
|
value, i; |
|
for (i = values.length; i--;) { |
|
value = values[i]; |
|
result.push({ |
|
isNull: value === null, |
|
value: value, |
|
color: this.calcColor(i, value, currentRegion), |
|
offset: currentRegion |
|
}); |
|
} |
|
return result; |
|
}, |
|
|
|
calcColor: function (stacknum, value, valuenum) { |
|
var colorMapByIndex = this.colorMapByIndex, |
|
colorMapByValue = this.colorMapByValue, |
|
options = this.options, |
|
color, newColor; |
|
if (this.stacked) { |
|
color = options.get('stackedBarColor'); |
|
} else { |
|
color = (value < 0) ? options.get('negBarColor') : options.get('barColor'); |
|
} |
|
if (value === 0 && options.get('zeroColor') !== undefined) { |
|
color = options.get('zeroColor'); |
|
} |
|
if (colorMapByValue && (newColor = colorMapByValue.get(value))) { |
|
color = newColor; |
|
} else if (colorMapByIndex && colorMapByIndex.length > valuenum) { |
|
color = colorMapByIndex[valuenum]; |
|
} |
|
return $.isArray(color) ? color[stacknum % color.length] : color; |
|
}, |
|
|
|
/** |
|
* Render bar(s) for a region |
|
*/ |
|
renderRegion: function (valuenum, highlight) { |
|
var vals = this.values[valuenum], |
|
options = this.options, |
|
xaxisOffset = this.xaxisOffset, |
|
result = [], |
|
range = this.range, |
|
stacked = this.stacked, |
|
target = this.target, |
|
x = valuenum * this.totalBarWidth, |
|
canvasHeightEf = this.canvasHeightEf, |
|
yoffset = this.yoffset, |
|
y, height, color, isNull, yoffsetNeg, i, valcount, val, minPlotted, allMin; |
|
|
|
vals = $.isArray(vals) ? vals : [vals]; |
|
valcount = vals.length; |
|
val = vals[0]; |
|
isNull = all(null, vals); |
|
allMin = all(xaxisOffset, vals, true); |
|
|
|
if (isNull) { |
|
if (options.get('nullColor')) { |
|
color = highlight ? options.get('nullColor') : this.calcHighlightColor(options.get('nullColor'), options); |
|
y = (yoffset > 0) ? yoffset - 1 : yoffset; |
|
return target.drawRect(x, y, this.barWidth - 1, 0, color, color); |
|
} else { |
|
return undefined; |
|
} |
|
} |
|
yoffsetNeg = yoffset; |
|
for (i = 0; i < valcount; i++) { |
|
val = vals[i]; |
|
|
|
if (stacked && val === xaxisOffset) { |
|
if (!allMin || minPlotted) { |
|
continue; |
|
} |
|
minPlotted = true; |
|
} |
|
|
|
if (range > 0) { |
|
height = Math.floor(canvasHeightEf * ((Math.abs(val - xaxisOffset) / range))) + 1; |
|
} else { |
|
height = 1; |
|
} |
|
if (val < xaxisOffset || (val === xaxisOffset && yoffset === 0)) { |
|
y = yoffsetNeg; |
|
yoffsetNeg += height; |
|
} else { |
|
y = yoffset - height; |
|
yoffset -= height; |
|
} |
|
color = this.calcColor(i, val, valuenum); |
|
if (highlight) { |
|
color = this.calcHighlightColor(color, options); |
|
} |
|
result.push(target.drawRect(x, y, this.barWidth - 1, height - 1, color, color)); |
|
} |
|
if (result.length === 1) { |
|
return result[0]; |
|
} |
|
return result; |
|
} |
|
}); |
|
|
|
/** |
|
* Tristate charts |
|
*/ |
|
$.fn.sparkline.tristate = tristate = createClass($.fn.sparkline._base, barHighlightMixin, { |
|
type: 'tristate', |
|
|
|
init: function (el, values, options, width, height) { |
|
var barWidth = parseInt(options.get('barWidth'), 10), |
|
barSpacing = parseInt(options.get('barSpacing'), 10); |
|
tristate._super.init.call(this, el, values, options, width, height); |
|
|
|
this.regionShapes = {}; |
|
this.barWidth = barWidth; |
|
this.barSpacing = barSpacing; |
|
this.totalBarWidth = barWidth + barSpacing; |
|
this.values = $.map(values, Number); |
|
this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing); |
|
|
|
if ($.isArray(options.get('colorMap'))) { |
|
this.colorMapByIndex = options.get('colorMap'); |
|
this.colorMapByValue = null; |
|
} else { |
|
this.colorMapByIndex = null; |
|
this.colorMapByValue = options.get('colorMap'); |
|
if (this.colorMapByValue && this.colorMapByValue.get === undefined) { |
|
this.colorMapByValue = new RangeMap(this.colorMapByValue); |
|
} |
|
} |
|
this.initTarget(); |
|
}, |
|
|
|
getRegion: function (el, x, y) { |
|
return Math.floor(x / this.totalBarWidth); |
|
}, |
|
|
|
getCurrentRegionFields: function () { |
|
var currentRegion = this.currentRegion; |
|
return { |
|
isNull: this.values[currentRegion] === undefined, |
|
value: this.values[currentRegion], |
|
color: this.calcColor(this.values[currentRegion], currentRegion), |
|
offset: currentRegion |
|
}; |
|
}, |
|
|
|
calcColor: function (value, valuenum) { |
|
var values = this.values, |
|
options = this.options, |
|
colorMapByIndex = this.colorMapByIndex, |
|
colorMapByValue = this.colorMapByValue, |
|
color, newColor; |
|
|
|
if (colorMapByValue && (newColor = colorMapByValue.get(value))) { |
|
color = newColor; |
|
} else if (colorMapByIndex && colorMapByIndex.length > valuenum) { |
|
color = colorMapByIndex[valuenum]; |
|
} else if (values[valuenum] < 0) { |
|
color = options.get('negBarColor'); |
|
} else if (values[valuenum] > 0) { |
|
color = options.get('posBarColor'); |
|
} else { |
|
color = options.get('zeroBarColor'); |
|
} |
|
return color; |
|
}, |
|
|
|
renderRegion: function (valuenum, highlight) { |
|
var values = this.values, |
|
options = this.options, |
|
target = this.target, |
|
canvasHeight, height, halfHeight, |
|
x, y, color; |
|
|
|
canvasHeight = target.pixelHeight; |
|
halfHeight = Math.round(canvasHeight / 2); |
|
|
|
x = valuenum * this.totalBarWidth; |
|
if (values[valuenum] < 0) { |
|
y = halfHeight; |
|
height = halfHeight - 1; |
|
} else if (values[valuenum] > 0) { |
|
y = 0; |
|
height = halfHeight - 1; |
|
} else { |
|
y = halfHeight - 1; |
|
height = 2; |
|
} |
|
color = this.calcColor(values[valuenum], valuenum); |
|
if (color === null) { |
|
return; |
|
} |
|
if (highlight) { |
|
color = this.calcHighlightColor(color, options); |
|
} |
|
return target.drawRect(x, y, this.barWidth - 1, height - 1, color, color); |
|
} |
|
}); |
|
|
|
/** |
|
* Discrete charts |
|
*/ |
|
$.fn.sparkline.discrete = discrete = createClass($.fn.sparkline._base, barHighlightMixin, { |
|
type: 'discrete', |
|
|
|
init: function (el, values, options, width, height) { |
|
discrete._super.init.call(this, el, values, options, width, height); |
|
|
|
this.regionShapes = {}; |
|
this.values = values = $.map(values, Number); |
|
this.min = Math.min.apply(Math, values); |
|
this.max = Math.max.apply(Math, values); |
|
this.range = this.max - this.min; |
|
this.width = width = options.get('width') === 'auto' ? values.length * 2 : this.width; |
|
this.interval = Math.floor(width / values.length); |
|
this.itemWidth = width / values.length; |
|
if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.min)) { |
|
this.min = options.get('chartRangeMin'); |
|
} |
|
if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.max)) { |
|
this.max = options.get('chartRangeMax'); |
|
} |
|
this.initTarget(); |
|
if (this.target) { |
|
this.lineHeight = options.get('lineHeight') === 'auto' ? Math.round(this.canvasHeight * 0.3) : options.get('lineHeight'); |
|
} |
|
}, |
|
|
|
getRegion: function (el, x, y) { |
|
return Math.floor(x / this.itemWidth); |
|
}, |
|
|
|
getCurrentRegionFields: function () { |
|
var currentRegion = this.currentRegion; |
|
return { |
|
isNull: this.values[currentRegion] === undefined, |
|
value: this.values[currentRegion], |
|
offset: currentRegion |
|
}; |
|
}, |
|
|
|
renderRegion: function (valuenum, highlight) { |
|
var values = this.values, |
|
options = this.options, |
|
min = this.min, |
|
max = this.max, |
|
range = this.range, |
|
interval = this.interval, |
|
target = this.target, |
|
canvasHeight = this.canvasHeight, |
|
lineHeight = this.lineHeight, |
|
pheight = canvasHeight - lineHeight, |
|
ytop, val, color, x; |
|
|
|
val = clipval(values[valuenum], min, max); |
|
x = valuenum * interval; |
|
ytop = Math.round(pheight - pheight * ((val - min) / range)); |
|
color = (options.get('thresholdColor') && val < options.get('thresholdValue')) ? options.get('thresholdColor') : options.get('lineColor'); |
|
if (highlight) { |
|
color = this.calcHighlightColor(color, options); |
|
} |
|
return target.drawLine(x, ytop, x, ytop + lineHeight, color); |
|
} |
|
}); |
|
|
|
/** |
|
* Bullet charts |
|
*/ |
|
$.fn.sparkline.bullet = bullet = createClass($.fn.sparkline._base, { |
|
type: 'bullet', |
|
|
|
init: function (el, values, options, width, height) { |
|
var min, max, vals; |
|
bullet._super.init.call(this, el, values, options, width, height); |
|
|
|
// values: target, performance, range1, range2, range3 |
|
this.values = values = normalizeValues(values); |
|
// target or performance could be null |
|
vals = values.slice(); |
|
vals[0] = vals[0] === null ? vals[2] : vals[0]; |
|
vals[1] = values[1] === null ? vals[2] : vals[1]; |
|
min = Math.min.apply(Math, values); |
|
max = Math.max.apply(Math, values); |
|
if (options.get('base') === undefined) { |
|
min = min < 0 ? min : 0; |
|
} else { |
|
min = options.get('base'); |
|
} |
|
this.min = min; |
|
this.max = max; |
|
this.range = max - min; |
|
this.shapes = {}; |
|
this.valueShapes = {}; |
|
this.regiondata = {}; |
|
this.width = width = options.get('width') === 'auto' ? '4.0em' : width; |
|
this.target = this.$el.simpledraw(width, height, options.get('composite')); |
|
if (!values.length) { |
|
this.disabled = true; |
|
} |
|
this.initTarget(); |
|
}, |
|
|
|
getRegion: function (el, x, y) { |
|
var shapeid = this.target.getShapeAt(el, x, y); |
|
return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined; |
|
}, |
|
|
|
getCurrentRegionFields: function () { |
|
var currentRegion = this.currentRegion; |
|
return { |
|
fieldkey: currentRegion.substr(0, 1), |
|
value: this.values[currentRegion.substr(1)], |
|
region: currentRegion |
|
}; |
|
}, |
|
|
|
changeHighlight: function (highlight) { |
|
var currentRegion = this.currentRegion, |
|
shapeid = this.valueShapes[currentRegion], |
|
shape; |
|
delete this.shapes[shapeid]; |
|
switch (currentRegion.substr(0, 1)) { |
|
case 'r': |
|
shape = this.renderRange(currentRegion.substr(1), highlight); |
|
break; |
|
case 'p': |
|
shape = this.renderPerformance(highlight); |
|
break; |
|
case 't': |
|
shape = this.renderTarget(highlight); |
|
break; |
|
} |
|
this.valueShapes[currentRegion] = shape.id; |
|
this.shapes[shape.id] = currentRegion; |
|
this.target.replaceWithShape(shapeid, shape); |
|
}, |
|
|
|
renderRange: function (rn, highlight) { |
|
var rangeval = this.values[rn], |
|
rangewidth = Math.round(this.canvasWidth * ((rangeval - this.min) / this.range)), |
|
color = this.options.get('rangeColors')[rn - 2]; |
|
if (highlight) { |
|
color = this.calcHighlightColor(color, this.options); |
|
} |
|
return this.target.drawRect(0, 0, rangewidth - 1, this.canvasHeight - 1, color, color); |
|
}, |
|
|
|
renderPerformance: function (highlight) { |
|
var perfval = this.values[1], |
|
perfwidth = Math.round(this.canvasWidth * ((perfval - this.min) / this.range)), |
|
color = this.options.get('performanceColor'); |
|
if (highlight) { |
|
color = this.calcHighlightColor(color, this.options); |
|
} |
|
return this.target.drawRect(0, Math.round(this.canvasHeight * 0.3), perfwidth - 1, |
|
Math.round(this.canvasHeight * 0.4) - 1, color, color); |
|
}, |
|
|
|
renderTarget: function (highlight) { |
|
var targetval = this.values[0], |
|
x = Math.round(this.canvasWidth * ((targetval - this.min) / this.range) - (this.options.get('targetWidth') / 2)), |
|
targettop = Math.round(this.canvasHeight * 0.10), |
|
targetheight = this.canvasHeight - (targettop * 2), |
|
color = this.options.get('targetColor'); |
|
if (highlight) { |
|
color = this.calcHighlightColor(color, this.options); |
|
} |
|
return this.target.drawRect(x, targettop, this.options.get('targetWidth') - 1, targetheight - 1, color, color); |
|
}, |
|
|
|
render: function () { |
|
var vlen = this.values.length, |
|
target = this.target, |
|
i, shape; |
|
if (!bullet._super.render.call(this)) { |
|
return; |
|
} |
|
for (i = 2; i < vlen; i++) { |
|
shape = this.renderRange(i).append(); |
|
this.shapes[shape.id] = 'r' + i; |
|
this.valueShapes['r' + i] = shape.id; |
|
} |
|
if (this.values[1] !== null) { |
|
shape = this.renderPerformance().append(); |
|
this.shapes[shape.id] = 'p1'; |
|
this.valueShapes.p1 = shape.id; |
|
} |
|
if (this.values[0] !== null) { |
|
shape = this.renderTarget().append(); |
|
this.shapes[shape.id] = 't0'; |
|
this.valueShapes.t0 = shape.id; |
|
} |
|
target.render(); |
|
} |
|
}); |
|
|
|
/** |
|
* Pie charts |
|
*/ |
|
$.fn.sparkline.pie = pie = createClass($.fn.sparkline._base, { |
|
type: 'pie', |
|
|
|
init: function (el, values, options, width, height) { |
|
var total = 0, i; |
|
|
|
pie._super.init.call(this, el, values, options, width, height); |
|
|
|
this.shapes = {}; // map shape ids to value offsets |
|
this.valueShapes = {}; // maps value offsets to shape ids |
|
this.values = values = $.map(values, Number); |
|
|
|
if (options.get('width') === 'auto') { |
|
this.width = this.height; |
|
} |
|
|
|
if (values.length > 0) { |
|
for (i = values.length; i--;) { |
|
total += values[i]; |
|
} |
|
} |
|
this.total = total; |
|
this.initTarget(); |
|
this.radius = Math.floor(Math.min(this.canvasWidth, this.canvasHeight) / 2); |
|
}, |
|
|
|
getRegion: function (el, x, y) { |
|
var shapeid = this.target.getShapeAt(el, x, y); |
|
return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined; |
|
}, |
|
|
|
getCurrentRegionFields: function () { |
|
var currentRegion = this.currentRegion; |
|
return { |
|
isNull: this.values[currentRegion] === undefined, |
|
value: this.values[currentRegion], |
|
percent: this.values[currentRegion] / this.total * 100, |
|
color: this.options.get('sliceColors')[currentRegion % this.options.get('sliceColors').length], |
|
offset: currentRegion |
|
}; |
|
}, |
|
|
|
changeHighlight: function (highlight) { |
|
var currentRegion = this.currentRegion, |
|
newslice = this.renderSlice(currentRegion, highlight), |
|
shapeid = this.valueShapes[currentRegion]; |
|
delete this.shapes[shapeid]; |
|
this.target.replaceWithShape(shapeid, newslice); |
|
this.valueShapes[currentRegion] = newslice.id; |
|
this.shapes[newslice.id] = currentRegion; |
|
}, |
|
|
|
renderSlice: function (valuenum, highlight) { |
|
var target = this.target, |
|
options = this.options, |
|
radius = this.radius, |
|
borderWidth = options.get('borderWidth'), |
|
offset = options.get('offset'), |
|
circle = 2 * Math.PI, |
|
values = this.values, |
|
total = this.total, |
|
next = offset ? (2*Math.PI)*(offset/360) : 0, |
|
start, end, i, vlen, color; |
|
|
|
vlen = values.length; |
|
for (i = 0; i < vlen; i++) { |
|
start = next; |
|
end = next; |
|
if (total > 0) { // avoid divide by zero |
|
end = next + (circle * (values[i] / total)); |
|
} |
|
if (valuenum === i) { |
|
color = options.get('sliceColors')[i % options.get('sliceColors').length]; |
|
if (highlight) { |
|
color = this.calcHighlightColor(color, options); |
|
} |
|
|
|
return target.drawPieSlice(radius, radius, radius - borderWidth, start, end, undefined, color); |
|
} |
|
next = end; |
|
} |
|
}, |
|
|
|
render: function () { |
|
var target = this.target, |
|
values = this.values, |
|
options = this.options, |
|
radius = this.radius, |
|
borderWidth = options.get('borderWidth'), |
|
donutWidth = options.get('donutWidth'), |
|
shape, i; |
|
|
|
if (!pie._super.render.call(this)) { |
|
return; |
|
} |
|
if (borderWidth) { |
|
target.drawCircle(radius, radius, Math.floor(radius - (borderWidth / 2)), |
|
options.get('borderColor'), undefined, borderWidth).append(); |
|
} |
|
for (i = values.length; i--;) { |
|
if (values[i]) { // don't render zero values |
|
shape = this.renderSlice(i).append(); |
|
this.valueShapes[i] = shape.id; // store just the shapeid |
|
this.shapes[shape.id] = i; |
|
} |
|
} |
|
if (donutWidth) { |
|
target.drawCircle(radius, radius, radius - donutWidth, options.get('donutColor'), |
|
options.get('donutColor'), 0).append(); |
|
} |
|
target.render(); |
|
} |
|
}); |
|
|
|
/** |
|
* Box plots |
|
*/ |
|
$.fn.sparkline.box = box = createClass($.fn.sparkline._base, { |
|
type: 'box', |
|
|
|
init: function (el, values, options, width, height) { |
|
box._super.init.call(this, el, values, options, width, height); |
|
this.values = $.map(values, Number); |
|
this.width = options.get('width') === 'auto' ? '4.0em' : width; |
|
this.initTarget(); |
|
if (!this.values.length) { |
|
this.disabled = 1; |
|
} |
|
}, |
|
|
|
/** |
|
* Simulate a single region |
|
*/ |
|
getRegion: function () { |
|
return 1; |
|
}, |
|
|
|
getCurrentRegionFields: function () { |
|
var result = [ |
|
{ field: 'lq', value: this.quartiles[0] }, |
|
{ field: 'med', value: this.quartiles[1] }, |
|
{ field: 'uq', value: this.quartiles[2] } |
|
]; |
|
if (this.loutlier !== undefined) { |
|
result.push({ field: 'lo', value: this.loutlier}); |
|
} |
|
if (this.routlier !== undefined) { |
|
result.push({ field: 'ro', value: this.routlier}); |
|
} |
|
if (this.lwhisker !== undefined) { |
|
result.push({ field: 'lw', value: this.lwhisker}); |
|
} |
|
if (this.rwhisker !== undefined) { |
|
result.push({ field: 'rw', value: this.rwhisker}); |
|
} |
|
return result; |
|
}, |
|
|
|
render: function () { |
|
var target = this.target, |
|
values = this.values, |
|
vlen = values.length, |
|
options = this.options, |
|
canvasWidth = this.canvasWidth, |
|
canvasHeight = this.canvasHeight, |
|
minValue = options.get('chartRangeMin') === undefined ? Math.min.apply(Math, values) : options.get('chartRangeMin'), |
|
maxValue = options.get('chartRangeMax') === undefined ? Math.max.apply(Math, values) : options.get('chartRangeMax'), |
|
canvasLeft = 0, |
|
lwhisker, loutlier, iqr, q1, q2, q3, rwhisker, routlier, i, |
|
size, unitSize; |
|
|
|
if (!box._super.render.call(this)) { |
|
return; |
|
} |
|
|
|
if (options.get('raw')) { |
|
if (options.get('showOutliers') && values.length > 5) { |
|
loutlier = values[0]; |
|
lwhisker = values[1]; |
|
q1 = values[2]; |
|
q2 = values[3]; |
|
q3 = values[4]; |
|
rwhisker = values[5]; |
|
routlier = values[6]; |
|
} else { |
|
lwhisker = values[0]; |
|
q1 = values[1]; |
|
q2 = values[2]; |
|
q3 = values[3]; |
|
rwhisker = values[4]; |
|
} |
|
} else { |
|
values.sort(function (a, b) { return a - b; }); |
|
q1 = quartile(values, 1); |
|
q2 = quartile(values, 2); |
|
q3 = quartile(values, 3); |
|
iqr = q3 - q1; |
|
if (options.get('showOutliers')) { |
|
lwhisker = rwhisker = undefined; |
|
for (i = 0; i < vlen; i++) { |
|
if (lwhisker === undefined && values[i] > q1 - (iqr * options.get('outlierIQR'))) { |
|
lwhisker = values[i]; |
|
} |
|
if (values[i] < q3 + (iqr * options.get('outlierIQR'))) { |
|
rwhisker = values[i]; |
|
} |
|
} |
|
loutlier = values[0]; |
|
routlier = values[vlen - 1]; |
|
} else { |
|
lwhisker = values[0]; |
|
rwhisker = values[vlen - 1]; |
|
} |
|
} |
|
this.quartiles = [q1, q2, q3]; |
|
this.lwhisker = lwhisker; |
|
this.rwhisker = rwhisker; |
|
this.loutlier = loutlier; |
|
this.routlier = routlier; |
|
|
|
unitSize = canvasWidth / (maxValue - minValue + 1); |
|
if (options.get('showOutliers')) { |
|
canvasLeft = Math.ceil(options.get('spotRadius')); |
|
canvasWidth -= 2 * Math.ceil(options.get('spotRadius')); |
|
unitSize = canvasWidth / (maxValue - minValue + 1); |
|
if (loutlier < lwhisker) { |
|
target.drawCircle((loutlier - minValue) * unitSize + canvasLeft, |
|
canvasHeight / 2, |
|
options.get('spotRadius'), |
|
options.get('outlierLineColor'), |
|
options.get('outlierFillColor')).append(); |
|
} |
|
if (routlier > rwhisker) { |
|
target.drawCircle((routlier - minValue) * unitSize + canvasLeft, |
|
canvasHeight / 2, |
|
options.get('spotRadius'), |
|
options.get('outlierLineColor'), |
|
options.get('outlierFillColor')).append(); |
|
} |
|
} |
|
|
|
// box |
|
target.drawRect( |
|
Math.round((q1 - minValue) * unitSize + canvasLeft), |
|
Math.round(canvasHeight * 0.1), |
|
Math.round((q3 - q1) * unitSize), |
|
Math.round(canvasHeight * 0.8), |
|
options.get('boxLineColor'), |
|
options.get('boxFillColor')).append(); |
|
// left whisker |
|
target.drawLine( |
|
Math.round((lwhisker - minValue) * unitSize + canvasLeft), |
|
Math.round(canvasHeight / 2), |
|
Math.round((q1 - minValue) * unitSize + canvasLeft), |
|
Math.round(canvasHeight / 2), |
|
options.get('lineColor')).append(); |
|
target.drawLine( |
|
Math.round((lwhisker - minValue) * unitSize + canvasLeft), |
|
Math.round(canvasHeight / 4), |
|
Math.round((lwhisker - minValue) * unitSize + canvasLeft), |
|
Math.round(canvasHeight - canvasHeight / 4), |
|
options.get('whiskerColor')).append(); |
|
// right whisker |
|
target.drawLine(Math.round((rwhisker - minValue) * unitSize + canvasLeft), |
|
Math.round(canvasHeight / 2), |
|
Math.round((q3 - minValue) * unitSize + canvasLeft), |
|
Math.round(canvasHeight / 2), |
|
options.get('lineColor')).append(); |
|
target.drawLine( |
|
Math.round((rwhisker - minValue) * unitSize + canvasLeft), |
|
Math.round(canvasHeight / 4), |
|
Math.round((rwhisker - minValue) * unitSize + canvasLeft), |
|
Math.round(canvasHeight - canvasHeight / 4), |
|
options.get('whiskerColor')).append(); |
|
// median line |
|
target.drawLine( |
|
Math.round((q2 - minValue) * unitSize + canvasLeft), |
|
Math.round(canvasHeight * 0.1), |
|
Math.round((q2 - minValue) * unitSize + canvasLeft), |
|
Math.round(canvasHeight * 0.9), |
|
options.get('medianColor')).append(); |
|
if (options.get('target')) { |
|
size = Math.ceil(options.get('spotRadius')); |
|
target.drawLine( |
|
Math.round((options.get('target') - minValue) * unitSize + canvasLeft), |
|
Math.round((canvasHeight / 2) - size), |
|
Math.round((options.get('target') - minValue) * unitSize + canvasLeft), |
|
Math.round((canvasHeight / 2) + size), |
|
options.get('targetColor')).append(); |
|
target.drawLine( |
|
Math.round((options.get('target') - minValue) * unitSize + canvasLeft - size), |
|
Math.round(canvasHeight / 2), |
|
Math.round((options.get('target') - minValue) * unitSize + canvasLeft + size), |
|
Math.round(canvasHeight / 2), |
|
options.get('targetColor')).append(); |
|
} |
|
target.render(); |
|
} |
|
}); |
|
|
|
// Setup a very simple "virtual canvas" to make drawing the few shapes we need easier |
|
// This is accessible as $(foo).simpledraw() |
|
|
|
VShape = createClass({ |
|
init: function (target, id, type, args) { |
|
this.target = target; |
|
this.id = id; |
|
this.type = type; |
|
this.args = args; |
|
}, |
|
append: function () { |
|
this.target.appendShape(this); |
|
return this; |
|
} |
|
}); |
|
|
|
VCanvas_base = createClass({ |
|
_pxregex: /(\d+)(px)?\s*$/i, |
|
|
|
init: function (width, height, target) { |
|
if (!width) { |
|
return; |
|
} |
|
this.width = width; |
|
this.height = height; |
|
this.target = target; |
|
this.lastShapeId = null; |
|
if (target[0]) { |
|
target = target[0]; |
|
} |
|
$.data(target, '_jqs_vcanvas', this); |
|
}, |
|
|
|
drawLine: function (x1, y1, x2, y2, lineColor, lineWidth) { |
|
return this.drawShape([[x1, y1], [x2, y2]], lineColor, lineWidth); |
|
}, |
|
|
|
drawShape: function (path, lineColor, fillColor, lineWidth) { |
|
return this._genShape('Shape', [path, lineColor, fillColor, lineWidth]); |
|
}, |
|
|
|
drawCircle: function (x, y, radius, lineColor, fillColor, lineWidth) { |
|
return this._genShape('Circle', [x, y, radius, lineColor, fillColor, lineWidth]); |
|
}, |
|
|
|
drawPieSlice: function (x, y, radius, startAngle, endAngle, lineColor, fillColor) { |
|
return this._genShape('PieSlice', [x, y, radius, startAngle, endAngle, lineColor, fillColor]); |
|
}, |
|
|
|
drawRect: function (x, y, width, height, lineColor, fillColor) { |
|
return this._genShape('Rect', [x, y, width, height, lineColor, fillColor]); |
|
}, |
|
|
|
getElement: function () { |
|
return this.canvas; |
|
}, |
|
|
|
/** |
|
* Return the most recently inserted shape id |
|
*/ |
|
getLastShapeId: function () { |
|
return this.lastShapeId; |
|
}, |
|
|
|
/** |
|
* Clear and reset the canvas |
|
*/ |
|
reset: function () { |
|
alert('reset not implemented'); |
|
}, |
|
|
|
_insert: function (el, target) { |
|
$(target).html(el); |
|
}, |
|
|
|
/** |
|
* Calculate the pixel dimensions of the canvas |
|
*/ |
|
_calculatePixelDims: function (width, height, canvas) { |
|
// XXX This should probably be a configurable option |
|
var match; |
|
match = this._pxregex.exec(height); |
|
if (match) { |
|
this.pixelHeight = match[1]; |
|
} else { |
|
this.pixelHeight = $(canvas).height(); |
|
} |
|
match = this._pxregex.exec(width); |
|
if (match) { |
|
this.pixelWidth = match[1]; |
|
} else { |
|
this.pixelWidth = $(canvas).width(); |
|
} |
|
}, |
|
|
|
/** |
|
* Generate a shape object and id for later rendering |
|
*/ |
|
_genShape: function (shapetype, shapeargs) { |
|
var id = shapeCount++; |
|
shapeargs.unshift(id); |
|
return new VShape(this, id, shapetype, shapeargs); |
|
}, |
|
|
|
/** |
|
* Add a shape to the end of the render queue |
|
*/ |
|
appendShape: function (shape) { |
|
alert('appendShape not implemented'); |
|
}, |
|
|
|
/** |
|
* Replace one shape with another |
|
*/ |
|
replaceWithShape: function (shapeid, shape) { |
|
alert('replaceWithShape not implemented'); |
|
}, |
|
|
|
/** |
|
* Insert one shape after another in the render queue |
|
*/ |
|
insertAfterShape: function (shapeid, shape) { |
|
alert('insertAfterShape not implemented'); |
|
}, |
|
|
|
/** |
|
* Remove a shape from the queue |
|
*/ |
|
removeShapeId: function (shapeid) { |
|
alert('removeShapeId not implemented'); |
|
}, |
|
|
|
/** |
|
* Find a shape at the specified x/y co-ordinates |
|
*/ |
|
getShapeAt: function (el, x, y) { |
|
alert('getShapeAt not implemented'); |
|
}, |
|
|
|
/** |
|
* Render all queued shapes onto the canvas |
|
*/ |
|
render: function () { |
|
alert('render not implemented'); |
|
} |
|
}); |
|
|
|
VCanvas_canvas = createClass(VCanvas_base, { |
|
init: function (width, height, target, interact) { |
|
VCanvas_canvas._super.init.call(this, width, height, target); |
|
this.canvas = document.createElement('canvas'); |
|
if (target[0]) { |
|
target = target[0]; |
|
} |
|
$.data(target, '_jqs_vcanvas', this); |
|
$(this.canvas).css({ display: 'inline-block', width: width, height: height, verticalAlign: 'top' }); |
|
this._insert(this.canvas, target); |
|
this._calculatePixelDims(width, height, this.canvas); |
|
this.canvas.width = this.pixelWidth; |
|
this.canvas.height = this.pixelHeight; |
|
this.interact = interact; |
|
this.shapes = {}; |
|
this.shapeseq = []; |
|
this.currentTargetShapeId = undefined; |
|
$(this.canvas).css({width: this.pixelWidth, height: this.pixelHeight}); |
|
}, |
|
|
|
_getContext: function (lineColor, fillColor, lineWidth) { |
|
var context = this.canvas.getContext('2d'); |
|
if (lineColor !== undefined) { |
|
context.strokeStyle = lineColor; |
|
} |
|
context.lineWidth = lineWidth === undefined ? 1 : lineWidth; |
|
if (fillColor !== undefined) { |
|
context.fillStyle = fillColor; |
|
} |
|
return context; |
|
}, |
|
|
|
reset: function () { |
|
var context = this._getContext(); |
|
context.clearRect(0, 0, this.pixelWidth, this.pixelHeight); |
|
this.shapes = {}; |
|
this.shapeseq = []; |
|
this.currentTargetShapeId = undefined; |
|
}, |
|
|
|
_drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) { |
|
var context = this._getContext(lineColor, fillColor, lineWidth), |
|
i, plen; |
|
context.beginPath(); |
|
context.moveTo(path[0][0] + 0.5, path[0][1] + 0.5); |
|
for (i = 1, plen = path.length; i < plen; i++) { |
|
context.lineTo(path[i][0] + 0.5, path[i][1] + 0.5); // the 0.5 offset gives us crisp pixel-width lines |
|
} |
|
if (lineColor !== undefined) { |
|
context.stroke(); |
|
} |
|
if (fillColor !== undefined) { |
|
context.fill(); |
|
} |
|
if (this.targetX !== undefined && this.targetY !== undefined && |
|
context.isPointInPath(this.targetX, this.targetY)) { |
|
this.currentTargetShapeId = shapeid; |
|
} |
|
}, |
|
|
|
_drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) { |
|
var context = this._getContext(lineColor, fillColor, lineWidth); |
|
context.beginPath(); |
|
context.arc(x, y, radius, 0, 2 * Math.PI, false); |
|
if (this.targetX !== undefined && this.targetY !== undefined && |
|
context.isPointInPath(this.targetX, this.targetY)) { |
|
this.currentTargetShapeId = shapeid; |
|
} |
|
if (lineColor !== undefined) { |
|
context.stroke(); |
|
} |
|
if (fillColor !== undefined) { |
|
context.fill(); |
|
} |
|
}, |
|
|
|
_drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) { |
|
var context = this._getContext(lineColor, fillColor); |
|
context.beginPath(); |
|
context.moveTo(x, y); |
|
context.arc(x, y, radius, startAngle, endAngle, false); |
|
context.lineTo(x, y); |
|
context.closePath(); |
|
if (lineColor !== undefined) { |
|
context.stroke(); |
|
} |
|
if (fillColor) { |
|
context.fill(); |
|
} |
|
if (this.targetX !== undefined && this.targetY !== undefined && |
|
context.isPointInPath(this.targetX, this.targetY)) { |
|
this.currentTargetShapeId = shapeid; |
|
} |
|
}, |
|
|
|
_drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) { |
|
return this._drawShape(shapeid, [[x, y], [x + width, y], [x + width, y + height], [x, y + height], [x, y]], lineColor, fillColor); |
|
}, |
|
|
|
appendShape: function (shape) { |
|
this.shapes[shape.id] = shape; |
|
this.shapeseq.push(shape.id); |
|
this.lastShapeId = shape.id; |
|
return shape.id; |
|
}, |
|
|
|
replaceWithShape: function (shapeid, shape) { |
|
var shapeseq = this.shapeseq, |
|
i; |
|
this.shapes[shape.id] = shape; |
|
for (i = shapeseq.length; i--;) { |
|
if (shapeseq[i] == shapeid) { |
|
shapeseq[i] = shape.id; |
|
} |
|
} |
|
delete this.shapes[shapeid]; |
|
}, |
|
|
|
replaceWithShapes: function (shapeids, shapes) { |
|
var shapeseq = this.shapeseq, |
|
shapemap = {}, |
|
sid, i, first; |
|
|
|
for (i = shapeids.length; i--;) { |
|
shapemap[shapeids[i]] = true; |
|
} |
|
for (i = shapeseq.length; i--;) { |
|
sid = shapeseq[i]; |
|
if (shapemap[sid]) { |
|
shapeseq.splice(i, 1); |
|
delete this.shapes[sid]; |
|
first = i; |
|
} |
|
} |
|
for (i = shapes.length; i--;) { |
|
shapeseq.splice(first, 0, shapes[i].id); |
|
this.shapes[shapes[i].id] = shapes[i]; |
|
} |
|
|
|
}, |
|
|
|
insertAfterShape: function (shapeid, shape) { |
|
var shapeseq = this.shapeseq, |
|
i; |
|
for (i = shapeseq.length; i--;) { |
|
if (shapeseq[i] === shapeid) { |
|
shapeseq.splice(i + 1, 0, shape.id); |
|
this.shapes[shape.id] = shape; |
|
return; |
|
} |
|
} |
|
}, |
|
|
|
removeShapeId: function (shapeid) { |
|
var shapeseq = this.shapeseq, |
|
i; |
|
for (i = shapeseq.length; i--;) { |
|
if (shapeseq[i] === shapeid) { |
|
shapeseq.splice(i, 1); |
|
break; |
|
} |
|
} |
|
delete this.shapes[shapeid]; |
|
}, |
|
|
|
getShapeAt: function (el, x, y) { |
|
this.targetX = x; |
|
this.targetY = y; |
|
this.render(); |
|
return this.currentTargetShapeId; |
|
}, |
|
|
|
render: function () { |
|
var shapeseq = this.shapeseq, |
|
shapes = this.shapes, |
|
shapeCount = shapeseq.length, |
|
context = this._getContext(), |
|
shapeid, shape, i; |
|
context.clearRect(0, 0, this.pixelWidth, this.pixelHeight); |
|
for (i = 0; i < shapeCount; i++) { |
|
shapeid = shapeseq[i]; |
|
shape = shapes[shapeid]; |
|
this['_draw' + shape.type].apply(this, shape.args); |
|
} |
|
if (!this.interact) { |
|
// not interactive so no need to keep the shapes array |
|
this.shapes = {}; |
|
this.shapeseq = []; |
|
} |
|
} |
|
|
|
}); |
|
|
|
VCanvas_vml = createClass(VCanvas_base, { |
|
init: function (width, height, target) { |
|
var groupel; |
|
VCanvas_vml._super.init.call(this, width, height, target); |
|
if (target[0]) { |
|
target = target[0]; |
|
} |
|
$.data(target, '_jqs_vcanvas', this); |
|
this.canvas = document.createElement('span'); |
|
$(this.canvas).css({ display: 'inline-block', position: 'relative', overflow: 'hidden', width: width, height: height, margin: '0px', padding: '0px', verticalAlign: 'top'}); |
|
this._insert(this.canvas, target); |
|
this._calculatePixelDims(width, height, this.canvas); |
|
this.canvas.width = this.pixelWidth; |
|
this.canvas.height = this.pixelHeight; |
|
groupel = '<v:group coordorigin="0 0" coordsize="' + this.pixelWidth + ' ' + this.pixelHeight + '"' + |
|
' style="position:absolute;top:0;left:0;width:' + this.pixelWidth + 'px;height=' + this.pixelHeight + 'px;"></v:group>'; |
|
this.canvas.insertAdjacentHTML('beforeEnd', groupel); |
|
this.group = $(this.canvas).children()[0]; |
|
this.rendered = false; |
|
this.prerender = ''; |
|
}, |
|
|
|
_drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) { |
|
var vpath = [], |
|
initial, stroke, fill, closed, vel, plen, i; |
|
for (i = 0, plen = path.length; i < plen; i++) { |
|
vpath[i] = '' + (path[i][0]) + ',' + (path[i][1]); |
|
} |
|
initial = vpath.splice(0, 1); |
|
lineWidth = lineWidth === undefined ? 1 : lineWidth; |
|
stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" '; |
|
fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" '; |
|
closed = vpath[0] === vpath[vpath.length - 1] ? 'x ' : ''; |
|
vel = '<v:shape coordorigin="0 0" coordsize="' + this.pixelWidth + ' ' + this.pixelHeight + '" ' + |
|
' id="jqsshape' + shapeid + '" ' + |
|
stroke + |
|
fill + |
|
' style="position:absolute;left:0px;top:0px;height:' + this.pixelHeight + 'px;width:' + this.pixelWidth + 'px;padding:0px;margin:0px;" ' + |
|
' path="m ' + initial + ' l ' + vpath.join(', ') + ' ' + closed + 'e">' + |
|
' </v:shape>'; |
|
return vel; |
|
}, |
|
|
|
_drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) { |
|
var stroke, fill, vel; |
|
x -= radius; |
|
y -= radius; |
|
stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" '; |
|
fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" '; |
|
vel = '<v:oval ' + |
|
' id="jqsshape' + shapeid + '" ' + |
|
stroke + |
|
fill + |
|
' style="position:absolute;top:' + y + 'px; left:' + x + 'px; width:' + (radius * 2) + 'px; height:' + (radius * 2) + 'px"></v:oval>'; |
|
return vel; |
|
|
|
}, |
|
|
|
_drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) { |
|
var vpath, startx, starty, endx, endy, stroke, fill, vel; |
|
if (startAngle === endAngle) { |
|
return ''; // VML seems to have problem when start angle equals end angle. |
|
} |
|
if ((endAngle - startAngle) === (2 * Math.PI)) { |
|
startAngle = 0.0; // VML seems to have a problem when drawing a full circle that doesn't start 0 |
|
endAngle = (2 * Math.PI); |
|
} |
|
|
|
startx = x + Math.round(Math.cos(startAngle) * radius); |
|
starty = y + Math.round(Math.sin(startAngle) * radius); |
|
endx = x + Math.round(Math.cos(endAngle) * radius); |
|
endy = y + Math.round(Math.sin(endAngle) * radius); |
|
|
|
if (startx === endx && starty === endy) { |
|
if ((endAngle - startAngle) < Math.PI) { |
|
// Prevent very small slices from being mistaken as a whole pie |
|
return ''; |
|
} |
|
// essentially going to be the entire circle, so ignore startAngle |
|
startx = endx = x + radius; |
|
starty = endy = y; |
|
} |
|
|
|
if (startx === endx && starty === endy && (endAngle - startAngle) < Math.PI) { |
|
return ''; |
|
} |
|
|
|
vpath = [x - radius, y - radius, x + radius, y + radius, startx, starty, endx, endy]; |
|
stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="1px" strokeColor="' + lineColor + '" '; |
|
fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" '; |
|
vel = '<v:shape coordorigin="0 0" coordsize="' + this.pixelWidth + ' ' + this.pixelHeight + '" ' + |
|
' id="jqsshape' + shapeid + '" ' + |
|
stroke + |
|
fill + |
|
' style="position:absolute;left:0px;top:0px;height:' + this.pixelHeight + 'px;width:' + this.pixelWidth + 'px;padding:0px;margin:0px;" ' + |
|
' path="m ' + x + ',' + y + ' wa ' + vpath.join(', ') + ' x e">' + |
|
' </v:shape>'; |
|
return vel; |
|
}, |
|
|
|
_drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) { |
|
return this._drawShape(shapeid, [[x, y], [x, y + height], [x + width, y + height], [x + width, y], [x, y]], lineColor, fillColor); |
|
}, |
|
|
|
reset: function () { |
|
this.group.innerHTML = ''; |
|
}, |
|
|
|
appendShape: function (shape) { |
|
var vel = this['_draw' + shape.type].apply(this, shape.args); |
|
if (this.rendered) { |
|
this.group.insertAdjacentHTML('beforeEnd', vel); |
|
} else { |
|
this.prerender += vel; |
|
} |
|
this.lastShapeId = shape.id; |
|
return shape.id; |
|
}, |
|
|
|
replaceWithShape: function (shapeid, shape) { |
|
var existing = $('#jqsshape' + shapeid), |
|
vel = this['_draw' + shape.type].apply(this, shape.args); |
|
existing[0].outerHTML = vel; |
|
}, |
|
|
|
replaceWithShapes: function (shapeids, shapes) { |
|
// replace the first shapeid with all the new shapes then toast the remaining old shapes |
|
var existing = $('#jqsshape' + shapeids[0]), |
|
replace = '', |
|
slen = shapes.length, |
|
i; |
|
for (i = 0; i < slen; i++) { |
|
replace += this['_draw' + shapes[i].type].apply(this, shapes[i].args); |
|
} |
|
existing[0].outerHTML = replace; |
|
for (i = 1; i < shapeids.length; i++) { |
|
$('#jqsshape' + shapeids[i]).remove(); |
|
} |
|
}, |
|
|
|
insertAfterShape: function (shapeid, shape) { |
|
var existing = $('#jqsshape' + shapeid), |
|
vel = this['_draw' + shape.type].apply(this, shape.args); |
|
existing[0].insertAdjacentHTML('afterEnd', vel); |
|
}, |
|
|
|
removeShapeId: function (shapeid) { |
|
var existing = $('#jqsshape' + shapeid); |
|
this.group.removeChild(existing[0]); |
|
}, |
|
|
|
getShapeAt: function (el, x, y) { |
|
var shapeid = el.id.substr(8); |
|
return shapeid; |
|
}, |
|
|
|
render: function () { |
|
if (!this.rendered) { |
|
// batch the intial render into a single repaint |
|
this.group.innerHTML = this.prerender; |
|
this.rendered = true; |
|
} |
|
} |
|
}); |
|
|
|
}))}(document, Math));
|
|
|