7 * Contact: Gareth Watts (gareth@splunk.com)
8 * http://omnipotent.net/jquery.sparkline/
10 * Generates inline sparkline charts from data supplied either to the method
13 * Compatible with Internet Explorer 6.0+ and modern browsers equipped with the canvas tag
14 * (Firefox 2.0+, Safari, Opera, etc)
16 * License: New BSD License
18 * Copyright (c) 2012, Splunk Inc.
19 * All rights reserved.
21 * Redistribution and use in source and binary forms, with or without modification,
22 * are permitted provided that the following conditions are met:
24 * * Redistributions of source code must retain the above copyright notice,
25 * this list of conditions and the following disclaimer.
26 * * Redistributions in binary form must reproduce the above copyright notice,
27 * this list of conditions and the following disclaimer in the documentation
28 * and/or other materials provided with the distribution.
29 * * Neither the name of Splunk Inc nor the names of its contributors may
30 * be used to endorse or promote products derived from this software without
31 * specific prior written permission.
33 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
34 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
35 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
36 * SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
37 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
38 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
39 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
40 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
41 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
45 * $(selector).sparkline(values, options)
47 * If values is undefined or set to 'html' then the data values are read from the specified tag:
48 * <p>Sparkline: <span class="sparkline">1,4,6,6,8,5,3,5</span></p>
49 * $('.sparkline').sparkline();
50 * There must be no spaces in the enclosed data set
52 * Otherwise values must be an array of numbers or null values
53 * <p>Sparkline: <span id="sparkline1">This text replaced if the browser is compatible</span></p>
54 * $('#sparkline1').sparkline([1,4,6,6,8,5,3,5])
55 * $('#sparkline2').sparkline([1,4,6,null,null,5,3,5])
57 * Values can also be specified in an HTML comment, or as a values attribute:
58 * <p>Sparkline: <span class="sparkline"><!--1,4,6,6,8,5,3,5 --></span></p>
59 * <p>Sparkline: <span class="sparkline" values="1,4,6,6,8,5,3,5"></span></p>
60 * $('.sparkline').sparkline();
62 * For line charts, x values can also be specified:
63 * <p>Sparkline: <span class="sparkline">1:1,2.7:4,3.4:6,5:6,6:8,8.7:5,9:3,10:5</span></p>
64 * $('#sparkline1').sparkline([ [1,1], [2.7,4], [3.4,6], [5,6], [6,8], [8.7,5], [9,3], [10,5] ])
66 * By default, options should be passed in as teh second argument to the sparkline function:
67 * $('.sparkline').sparkline([1,2,3,4], {type: 'bar'})
69 * Options can also be set by passing them on the tag itself. This feature is disabled by default though
70 * as there's a slight performance overhead:
71 * $('.sparkline').sparkline([1,2,3,4], {enableTagOptions: true})
72 * <p>Sparkline: <span class="sparkline" sparkType="bar" sparkBarColor="red">loading</span></p>
73 * Prefix all options supplied as tag attribute with "spark" (configurable by setting tagOptionPrefix)
76 * lineColor - Color of the line used for the chart
77 * fillColor - Color used to fill in the chart - Set to '' or false for a transparent chart
78 * width - Width of the chart - Defaults to 3 times the number of values in pixels
79 * height - Height of the chart - Defaults to the height of the containing element
80 * chartRangeMin - Specify the minimum value to use for the Y range of the chart - Defaults to the minimum value supplied
81 * chartRangeMax - Specify the maximum value to use for the Y range of the chart - Defaults to the maximum value supplied
82 * chartRangeClip - Clip out of range values to the max/min specified by chartRangeMin and chartRangeMax
83 * chartRangeMinX - Specify the minimum value to use for the X range of the chart - Defaults to the minimum value supplied
84 * chartRangeMaxX - Specify the maximum value to use for the X range of the chart - Defaults to the maximum value supplied
85 * composite - If true then don't erase any existing chart attached to the tag, but draw
86 * another chart over the top - Note that width and height are ignored if an
87 * existing chart is detected.
88 * tagValuesAttribute - Name of tag attribute to check for data values - Defaults to 'values'
89 * enableTagOptions - Whether to check tags for sparkline options
90 * tagOptionPrefix - Prefix used for options supplied as tag attributes - Defaults to 'spark'
91 * disableHiddenCheck - If set to true, then the plugin will assume that charts will never be drawn into a
92 * hidden dom element, avoding a browser reflow
93 * disableInteraction - If set to true then all mouseover/click interaction behaviour will be disabled,
94 * making the plugin perform much like it did in 1.x
95 * disableTooltips - If set to true then tooltips will be disabled - Defaults to false (tooltips enabled)
96 * disableHighlight - If set to true then highlighting of selected chart elements on mouseover will be disabled
97 * defaults to false (highlights enabled)
98 * highlightLighten - Factor to lighten/darken highlighted chart values by - Defaults to 1.4 for a 40% increase
99 * tooltipContainer - Specify which DOM element the tooltip should be rendered into - defaults to document.body
100 * tooltipClassname - Optional CSS classname to apply to tooltips - If not specified then a default style will be applied
101 * tooltipOffsetX - How many pixels away from the mouse pointer to render the tooltip on the X axis
102 * tooltipOffsetY - How many pixels away from the mouse pointer to render the tooltip on the r axis
103 * tooltipFormatter - Optional callback that allows you to override the HTML displayed in the tooltip
104 * callback is given arguments of (sparkline, options, fields)
105 * tooltipChartTitle - If specified then the tooltip uses the string specified by this setting as a title
106 * tooltipFormat - A format string or SPFormat object (or an array thereof for multiple entries)
107 * to control the format of the tooltip
108 * tooltipPrefix - A string to prepend to each field displayed in a tooltip
109 * tooltipSuffix - A string to append to each field displayed in a tooltip
110 * tooltipSkipNull - If true then null values will not have a tooltip displayed (defaults to true)
111 * tooltipValueLookups - An object or range map to map field values to tooltip strings
112 * (eg. to map -1 to "Lost", 0 to "Draw", and 1 to "Win")
113 * numberFormatter - Optional callback for formatting numbers in tooltips
114 * numberDigitGroupSep - Character to use for group separator in numbers "1,234" - Defaults to ","
115 * numberDecimalMark - Character to use for the decimal point when formatting numbers - Defaults to "."
116 * numberDigitGroupCount - Number of digits between group separator - Defaults to 3
118 * There are 7 types of sparkline, selected by supplying a "type" option of 'line' (default),
119 * 'bar', 'tristate', 'bullet', 'discrete', 'pie' or 'box'
120 * line - Line chart. Options:
121 * spotColor - Set to '' to not end each line in a circular spot
122 * minSpotColor - If set, color of spot at minimum value
123 * maxSpotColor - If set, color of spot at maximum value
124 * spotRadius - Radius in pixels
125 * lineWidth - Width of line in pixels
127 * normalRangeMax - If set draws a filled horizontal bar between these two values marking the "normal"
128 * or expected range of values
129 * normalRangeColor - Color to use for the above bar
130 * drawNormalOnTop - Draw the normal range above the chart fill color if true
131 * defaultPixelsPerValue - Defaults to 3 pixels of width for each value in the chart
132 * highlightSpotColor - The color to use for drawing a highlight spot on mouseover - Set to null to disable
133 * highlightLineColor - The color to use for drawing a highlight line on mouseover - Set to null to disable
134 * valueSpots - Specify which points to draw spots on, and in which color. Accepts a range map
136 * bar - Bar chart. Options:
137 * barColor - Color of bars for postive values
138 * negBarColor - Color of bars for negative values
139 * zeroColor - Color of bars with zero values
140 * nullColor - Color of bars with null values - Defaults to omitting the bar entirely
141 * barWidth - Width of bars in pixels
142 * colorMap - Optional mappnig of values to colors to override the *BarColor values above
143 * can be an Array of values to control the color of individual bars or a range map
144 * to specify colors for individual ranges of values
145 * barSpacing - Gap between bars in pixels
146 * zeroAxis - Centers the y-axis around zero if true
148 * tristate - Charts values of win (>0), lose (<0) or draw (=0)
149 * posBarColor - Color of win values
150 * negBarColor - Color of lose values
151 * zeroBarColor - Color of draw values
152 * barWidth - Width of bars in pixels
153 * barSpacing - Gap between bars in pixels
154 * colorMap - Optional mappnig of values to colors to override the *BarColor values above
155 * can be an Array of values to control the color of individual bars or a range map
156 * to specify colors for individual ranges of values
158 * discrete - Options:
159 * lineHeight - Height of each line in pixels - Defaults to 30% of the graph height
160 * thesholdValue - Values less than this value will be drawn using thresholdColor instead of lineColor
163 * bullet - Values for bullet graphs msut be in the order: target, performance, range1, range2, range3, ...
165 * targetColor - The color of the vertical target marker
166 * targetWidth - The width of the target marker in pixels
167 * performanceColor - The color of the performance measure horizontal bar
168 * rangeColors - Colors to use for each qualitative range background color
170 * pie - Pie chart. Options:
171 * sliceColors - An array of colors to use for pie slices
172 * offset - Angle in degrees to offset the first slice - Try -90 or +90
173 * borderWidth - Width of border to draw around the pie chart, in pixels - Defaults to 0 (no border)
174 * borderColor - Color to use for the pie chart border - Defaults to #000
176 * box - Box plot. Options:
177 * raw - Set to true to supply pre-computed plot points as values
178 * values should be: low_outlier, low_whisker, q1, median, q3, high_whisker, high_outlier
179 * When set to false you can supply any number of values and the box plot will
180 * be computed for you. Default is false.
181 * showOutliers - Set to true (default) to display outliers as circles
182 * outlierIRQ - Interquartile range used to determine outliers. Default 1.5
183 * boxLineColor - Outline color of the box
184 * boxFillColor - Fill color for the box
185 * whiskerColor - Line color used for whiskers
186 * outlierLineColor - Outline color of outlier circles
187 * outlierFillColor - Fill color of the outlier circles
188 * spotRadius - Radius of outlier circles
189 * medianColor - Line color of the median line
190 * target - Draw a target cross hair at the supplied value (default undefined)
195 * $('#sparkline1').sparkline(myvalues, { lineColor: '#f00', fillColor: false });
196 * $('.barsparks').sparkline('html', { type:'bar', height:'40px', barWidth:5 });
197 * $('#tristate').sparkline([1,1,-1,1,0,0,-1], { type:'tristate' }):
198 * $('#discrete').sparkline([1,3,4,5,5,3,4,5], { type:'discrete' });
199 * $('#bullet').sparkline([10,12,12,9,7], { type:'bullet' });
200 * $('#pie').sparkline([1,1,2], { type:'pie' });
203 /*jslint regexp: true, browser: true, jquery: true, white: true, nomen: false, plusplus: false, maxerr: 500, indent: 4 */
208 var UNSET_OPTION = {},
209 getDefaults, createClass, SPFormat, clipval, quartile, normalizeValue, normalizeValues,
210 remove, isNumber, all, sum, addCSS, ensureArray, formatNumber, RangeMap,
211 MouseHandler, Tooltip, barHighlightMixin,
212 line, bar, tristate, discrete, bullet, pie, box, defaultStyles, initStyles,
213 VShape, VCanvas_base, VCanvas_canvas, VCanvas_vml, pending, shapeCount = 0;
216 * Default configuration settings
218 getDefaults = function () {
220 // Settings common to most/all chart types
225 defaultPixelsPerValue: 3,
229 tagValuesAttribute: 'values',
230 tagOptionsPrefix: 'spark',
231 enableTagOptions: false,
232 enableHighlight: true,
233 highlightLighten: 1.4,
234 tooltipSkipNull: true,
237 disableHiddenCheck: false,
238 numberFormatter: false,
239 numberDigitGroupCount: 3,
240 numberDigitGroupSep: ',',
241 numberDecimalMark: '.',
242 disableTooltips: false,
243 disableInteraction: false
245 // Defaults for line charts
248 highlightSpotColor: '#5f5',
249 highlightLineColor: '#f22',
251 minSpotColor: '#f80',
252 maxSpotColor: '#f80',
254 normalRangeMin: undefined,
255 normalRangeMax: undefined,
256 normalRangeColor: '#ccc',
257 drawNormalOnTop: false,
258 chartRangeMin: undefined,
259 chartRangeMax: undefined,
260 chartRangeMinX: undefined,
261 chartRangeMaxX: undefined,
262 tooltipFormat: new SPFormat('<span style="color: {{color}}">●</span> {{prefix}}{{y}}{{suffix}}')
264 // Defaults for bar charts
268 stackedBarColor: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00',
269 '#dd4477', '#0099c6', '#990099'],
270 zeroColor: undefined,
271 nullColor: undefined,
275 chartRangeMax: undefined,
276 chartRangeMin: undefined,
277 chartRangeClip: false,
279 tooltipFormat: new SPFormat('<span style="color: {{color}}">●</span> {{prefix}}{{value}}{{suffix}}')
281 // Defaults for tristate charts
287 zeroBarColor: '#999',
289 tooltipFormat: new SPFormat('<span style="color: {{color}}">●</span> {{value:map}}'),
290 tooltipValueLookups: { map: { '-1': 'Loss', '0': 'Draw', '1': 'Win' } }
292 // Defaults for discrete charts
295 thresholdColor: undefined,
297 chartRangeMax: undefined,
298 chartRangeMin: undefined,
299 chartRangeClip: false,
300 tooltipFormat: new SPFormat('{{prefix}}{{value}}{{suffix}}')
302 // Defaults for bullet charts
305 targetWidth: 3, // width of the target bar in pixels
306 performanceColor: '#33f',
307 rangeColors: ['#d3dafe', '#a8b6ff', '#7f94ff'],
308 base: undefined, // set this to a number to change the base start number
309 tooltipFormat: new SPFormat('{{fieldkey:fields}} - {{value}}'),
310 tooltipValueLookups: { fields: {r: 'Range', p: 'Performance', t: 'Target'} }
312 // Defaults for pie charts
315 sliceColors: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00',
316 '#dd4477', '#0099c6', '#990099'],
319 tooltipFormat: new SPFormat('<span style="color: {{color}}">●</span> {{value}} ({{percent.1}}%)')
321 // Defaults for box plots
324 boxLineColor: '#000',
325 boxFillColor: '#cdf',
326 whiskerColor: '#000',
327 outlierLineColor: '#333',
328 outlierFillColor: '#fff',
335 chartRangeMax: undefined,
336 chartRangeMin: undefined,
337 tooltipFormat: new SPFormat('{{field:fields}}: {{value}}'),
338 tooltipFormatFieldlistKey: 'field',
339 tooltipValueLookups: { fields: { lq: 'Lower Quartile', med: 'Median',
340 uq: 'Upper Quartile', lo: 'Left Outlier', ro: 'Right Outlier',
341 lw: 'Left Whisker', rw: 'Right Whisker'} }
346 // You can have tooltips use a css class other than jqstooltip by specifying tooltipClassname
347 defaultStyles = '.jqstooltip { ' +
348 'position: absolute;' +
351 'visibility: hidden;' +
352 'background: rgb(0, 0, 0) transparent;' +
353 'background-color: rgba(0,0,0,0.6);' +
354 'filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000);' +
355 '-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000)";' +
357 'font: 10px arial, san serif;' +
358 'text-align: left;' +
359 'white-space: nowrap;' +
361 'border: 1px solid white;' +
365 'font: 10px arial, san serif;' +
366 'text-align: left;' +
369 initStyles = function() {
370 addCSS(defaultStyles);
379 createClass = function (/* [baseclass, [mixin, ...]], definition */) {
381 Class = function () {
382 this.init.apply(this, arguments);
384 if (arguments.length > 1) {
386 Class.prototype = $.extend(new arguments[0](), arguments[arguments.length - 1]);
387 Class._super = arguments[0].prototype;
389 Class.prototype = arguments[arguments.length - 1];
391 if (arguments.length > 2) {
392 args = Array.prototype.slice.call(arguments, 1, -1);
393 args.unshift(Class.prototype);
394 $.extend.apply($, args);
397 Class.prototype = arguments[0];
399 Class.prototype.cls = Class;
404 * Wraps a format string for tooltips
409 $.SPFormatClass = SPFormat = createClass({
410 fre: /\{\{([\w.]+?)(:(.+?))?\}\}/g,
411 precre: /(\w+)\.(\d+)/,
413 init: function (format, fclass) {
414 this.format = format;
415 this.fclass = fclass;
418 render: function (fieldset, lookups, options) {
421 match, token, lookupkey, fieldvalue, prec;
422 return this.format.replace(this.fre, function () {
424 token = arguments[1];
425 lookupkey = arguments[3];
426 match = self.precre.exec(token);
433 fieldvalue = fields[token];
434 if (fieldvalue === undefined) {
437 if (lookupkey && lookups && lookups[lookupkey]) {
438 lookup = lookups[lookupkey];
439 if (lookup.get) { // RangeMap
440 return lookups[lookupkey].get(fieldvalue) || fieldvalue;
442 return lookups[lookupkey][fieldvalue] || fieldvalue;
445 if (isNumber(fieldvalue)) {
446 if (options.get('numberFormatter')) {
447 fieldvalue = options.get('numberFormatter')(fieldvalue);
449 fieldvalue = formatNumber(fieldvalue, prec,
450 options.get('numberDigitGroupCount'),
451 options.get('numberDigitGroupSep'),
452 options.get('numberDecimalMark'));
460 // convience method to avoid needing the new operator
461 $.spformat = function(format, fclass) {
462 return new SPFormat(format, fclass);
465 clipval = function (val, min, max) {
475 quartile = function (values, q) {
478 vl = Math.floor(values.length / 2);
479 return values.length % 2 ? values[vl] : (values[vl] + values[vl + 1]) / 2;
481 vl = Math.floor(values.length / 4);
482 return values.length % 2 ? (values[vl * q] + values[vl * q + 1]) / 2 : values[vl * q];
486 normalizeValue = function (val) {
502 nf = parseFloat(val);
510 normalizeValues = function (vals) {
512 for (i = vals.length; i--;) {
513 result[i] = normalizeValue(vals[i]);
518 remove = function (vals, filter) {
519 var i, vl, result = [];
520 for (i = 0, vl = vals.length; i < vl; i++) {
521 if (vals[i] !== filter) {
522 result.push(vals[i]);
528 isNumber = function (num) {
529 return !isNaN(parseFloat(num)) && isFinite(num);
532 formatNumber = function (num, prec, groupsize, groupsep, decsep) {
534 num = (prec === false ? parseFloat(num).toString() : num.toFixed(prec)).split('');
535 p = (p = $.inArray('.', num)) < 0 ? num.length : p;
536 if (p < num.length) {
539 for (i = p - groupsize; i > 0; i -= groupsize) {
540 num.splice(i, 0, groupsep);
545 // determine if all values of an array match a value
546 // returns true if the array is empty
547 all = function (val, arr, ignoreNull) {
549 for (i = arr.length; i--; ) {
550 if (arr[i] !== val || (!ignoreNull && val === null)) {
557 // sums the numeric values in an array, ignoring other values
558 sum = function (vals) {
560 for (i = vals.length; i--;) {
561 total += typeof vals[i] === 'number' ? vals[i] : 0;
566 ensureArray = function (val) {
567 return $.isArray(val) ? val : [val];
570 // http://paulirish.com/2008/bookmarklet-inject-new-css-rules/
571 addCSS = function(css) {
573 //if ('\v' == 'v') /* ie only */ {
574 if (document.createStyleSheet) {
575 document.createStyleSheet().cssText = css;
577 tag = document.createElement('style');
578 tag.type = 'text/css';
579 document.getElementsByTagName('head')[0].appendChild(tag);
580 tag[(typeof document.body.style.WebkitAppearance == 'string') /* webkit only */ ? 'innerText' : 'innerHTML'] = css;
584 // Provide a cross-browser interface to a few simple drawing primitives
585 $.fn.simpledraw = function (width, height, useExisting, interact) {
586 var target, mhandler;
587 if (useExisting && (target = this.data('_jqs_vcanvas'))) {
590 if (width === undefined) {
591 width = $(this).innerWidth();
593 if (height === undefined) {
594 height = $(this).innerHeight();
596 if ($.browser.hasCanvas) {
597 target = new VCanvas_canvas(width, height, this, interact);
598 } else if ($.browser.msie) {
599 target = new VCanvas_vml(width, height, this);
603 mhandler = $(this).data('_jqs_mhandler');
605 mhandler.registerCanvas(target);
610 $.fn.cleardraw = function () {
611 var target = this.data('_jqs_vcanvas');
617 $.RangeMapClass = RangeMap = createClass({
618 init: function (map) {
619 var key, range, rangelist = [];
621 if (map.hasOwnProperty(key) && typeof key === 'string' && key.indexOf(':') > -1) {
622 range = key.split(':');
623 range[0] = range[0].length === 0 ? -Infinity : parseFloat(range[0]);
624 range[1] = range[1].length === 0 ? Infinity : parseFloat(range[1]);
626 rangelist.push(range);
630 this.rangelist = rangelist || false;
633 get: function (value) {
634 var rangelist = this.rangelist,
636 if ((result = this.map[value]) !== undefined) {
640 for (i = rangelist.length; i--;) {
641 range = rangelist[i];
642 if (range[0] <= value && range[1] >= value) {
651 // Convenience function
652 $.range_map = function(map) {
653 return new RangeMap(map);
656 MouseHandler = createClass({
657 init: function (el, options) {
660 this.options = options;
661 this.currentPageX = 0;
662 this.currentPageY = 0;
667 this.displayTooltips = !options.get('disableTooltips');
668 this.highlightEnabled = !options.get('disableHighlight');
671 registerSparkline: function (sp) {
672 this.splist.push(sp);
674 this.updateDisplay();
678 registerCanvas: function (canvas) {
679 var $canvas = $(canvas.canvas);
680 this.canvas = canvas;
681 this.$canvas = $canvas;
682 $canvas.mouseenter($.proxy(this.mouseenter, this));
683 $canvas.mouseleave($.proxy(this.mouseleave, this));
684 $canvas.click($.proxy(this.mouseclick, this));
687 reset: function (removeTooltip) {
689 if (this.tooltip && removeTooltip) {
690 this.tooltip.remove();
691 this.tooltip = undefined;
695 mouseclick: function (e) {
696 var clickEvent = $.Event('sparklineClick');
697 clickEvent.originalEvent = e;
698 clickEvent.sparklines = this.splist;
699 this.$el.trigger(clickEvent);
702 mouseenter: function (e) {
703 $(document.body).unbind('mousemove.jqs');
704 $(document.body).bind('mousemove.jqs', $.proxy(this.mousemove, this));
706 this.currentPageX = e.pageX;
707 this.currentPageY = e.pageY;
708 this.currentEl = e.target;
709 if (!this.tooltip && this.displayTooltips) {
710 this.tooltip = new Tooltip(this.options);
711 this.tooltip.updatePosition(e.pageX, e.pageY);
713 this.updateDisplay();
716 mouseleave: function () {
717 $(document.body).unbind('mousemove.jqs');
718 var splist = this.splist,
719 spcount = splist.length,
720 needsRefresh = false,
723 this.currentEl = null;
726 this.tooltip.remove();
730 for (i = 0; i < spcount; i++) {
732 if (sp.clearRegionHighlight()) {
738 this.canvas.render();
742 mousemove: function (e) {
743 this.currentPageX = e.pageX;
744 this.currentPageY = e.pageY;
745 this.currentEl = e.target;
747 this.tooltip.updatePosition(e.pageX, e.pageY);
749 this.updateDisplay();
752 updateDisplay: function () {
753 var splist = this.splist,
754 spcount = splist.length,
755 needsRefresh = false,
756 offset = this.$canvas.offset(),
757 localX = this.currentPageX - offset.left,
758 localY = this.currentPageY - offset.top,
759 tooltiphtml, sp, i, result, changeEvent;
763 for (i = 0; i < spcount; i++) {
765 result = sp.setRegionHighlight(this.currentEl, localX, localY);
771 changeEvent = $.Event('sparklineRegionChange');
772 changeEvent.sparklines = this.splist;
773 this.$el.trigger(changeEvent);
776 for (i = 0; i < spcount; i++) {
778 tooltiphtml += sp.getCurrentRegionTooltip();
780 this.tooltip.setContent(tooltiphtml);
782 if (!this.disableHighlight) {
783 this.canvas.render();
786 if (result === null) {
793 Tooltip = createClass({
794 sizeStyle: 'position: static !important;' +
795 'display: block !important;' +
796 'visibility: hidden !important;' +
797 'float: left !important;',
799 init: function (options) {
800 var tooltipClassname = options.get('tooltipClassname', 'jqstooltip'),
801 sizetipStyle = this.sizeStyle,
803 this.container = options.get('tooltipContainer') || document.body;
804 this.tooltipOffsetX = options.get('tooltipOffsetX', 10);
805 this.tooltipOffsetY = options.get('tooltipOffsetY', 12);
806 // remove any previous lingering tooltip
807 $('#jqssizetip').remove();
808 $('#jqstooltip').remove();
809 this.sizetip = $('<div/>', {
812 'class': tooltipClassname
814 this.tooltip = $('<div/>', {
816 'class': tooltipClassname
817 }).appendTo(this.container);
818 // account for the container's location
819 offset = this.tooltip.offset();
820 this.offsetLeft = offset.left;
821 this.offsetTop = offset.top;
823 $(window).unbind('resize.jqs scroll.jqs');
824 $(window).bind('resize.jqs scroll.jqs', $.proxy(this.updateWindowDims, this));
825 this.updateWindowDims();
828 updateWindowDims: function () {
829 this.scrollTop = $(window).scrollTop();
830 this.scrollLeft = $(window).scrollLeft();
831 this.scrollRight = this.scrollLeft + $(window).width();
832 this.updatePosition();
835 getSize: function (content) {
836 this.sizetip.html(content).appendTo(this.container);
837 this.width = this.sizetip.width() + 1;
838 this.height = this.sizetip.height();
839 this.sizetip.remove();
842 setContent: function (content) {
844 this.tooltip.css('visibility', 'hidden');
848 this.getSize(content);
849 this.tooltip.html(content)
852 'height': this.height,
853 'visibility': 'visible'
857 this.updatePosition();
861 updatePosition: function (x, y) {
862 if (x === undefined) {
863 if (this.mousex === undefined) {
866 x = this.mousex - this.offsetLeft;
867 y = this.mousey - this.offsetTop;
870 this.mousex = x = x - this.offsetLeft;
871 this.mousey = y = y - this.offsetTop;
873 if (!this.height || !this.width || this.hidden) {
877 y -= this.height + this.tooltipOffsetY;
878 x += this.tooltipOffsetX;
880 if (y < this.scrollTop) {
883 if (x < this.scrollLeft) {
885 } else if (x + this.width > this.scrollRight) {
886 x = this.scrollRight - this.width;
895 remove: function () {
896 this.tooltip.remove();
897 this.sizetip.remove();
898 this.sizetip = this.tooltip = undefined;
899 $(window).unbind('resize.jqs scroll.jqs');
904 $.fn.sparkline = function (userValues, userOptions) {
905 return this.each(function () {
906 var options = new $.fn.sparkline.options(this, userOptions),
909 render = function () {
910 var values, width, height, tmp, mhandler, sp, vals;
911 if (userValues === 'html' || userValues === undefined) {
912 vals = this.getAttribute(options.get('tagValuesAttribute'));
913 if (vals === undefined || vals === null) {
916 values = vals.replace(/(^\s*<!--)|(-->\s*$)|\s+/g, '').split(',');
921 width = options.get('width') === 'auto' ? values.length * options.get('defaultPixelsPerValue') : options.get('width');
922 if (options.get('height') === 'auto') {
923 if (!options.get('composite') || !$.data(this, '_jqs_vcanvas')) {
924 // must be a better way to get the line height
925 tmp = document.createElement('span');
928 height = $(tmp).innerHeight() || $(tmp).height();
933 height = options.get('height');
936 if (!options.get('disableInteraction')) {
937 mhandler = $.data(this, '_jqs_mhandler');
939 mhandler = new MouseHandler(this, options);
940 $.data(this, '_jqs_mhandler', mhandler);
941 } else if (!options.get('composite')) {
948 if (options.get('composite') && !$.data(this, '_jqs_vcanvas')) {
949 if (!$.data(this, '_jqs_errnotify')) {
950 alert('Attempted to attach a composite sparkline to an element with no existing sparkline');
951 $.data(this, '_jqs_errnotify', true);
956 sp = new $.fn.sparkline[options.get('type')](this, values, options, width, height);
961 mhandler.registerSparkline(sp);
964 // jQuery 1.3.0 completely changed the meaning of :hidden :-/
965 if (($(this).html() && !options.get('disableHiddenCheck') && $(this).is(':hidden')) || ($.fn.jquery < '1.3.0' && $(this).parents().is(':hidden')) || !$(this).parents('body').length) {
966 if (!options.get('composite') && $.data(this, '_jqs_pending')) {
967 // remove any existing references to the element
968 for (i = pending.length; i; i--) {
969 if (pending[i - 1][0] == this) {
970 pending.splice(i - 1, 1);
974 pending.push([this, render]);
975 $.data(this, '_jqs_pending', true);
982 $.fn.sparkline.defaults = getDefaults();
985 $.sparkline_display_visible = function () {
988 for (i = 0, pl = pending.length; i < pl; i++) {
990 if ($(el).is(':visible') && !$(el).parents().is(':hidden')) {
991 pending[i][1].call(el);
992 $.data(pending[i][0], '_jqs_pending', false);
994 } else if (!$(el).closest('html').length && !$.data(el, '_jqs_pending')) {
995 // element has been inserted and removed from the DOM
996 // If it was not yet inserted into the dom then the .data request
998 // removing from the dom causes the data to be removed.
999 $.data(pending[i][0], '_jqs_pending', false);
1003 for (i = done.length; i; i--) {
1004 pending.splice(done[i - 1], 1);
1010 * User option handler
1012 $.fn.sparkline.options = createClass({
1013 init: function (tag, userOptions) {
1014 var extendedOptions, defaults, base, tagOptionType;
1015 this.userOptions = userOptions = userOptions || {};
1017 this.tagValCache = {};
1018 defaults = $.fn.sparkline.defaults;
1019 base = defaults.common;
1020 this.tagOptionsPrefix = userOptions.enableTagOptions && (userOptions.tagOptionsPrefix || base.tagOptionsPrefix);
1022 tagOptionType = this.getTagSetting('type');
1023 if (tagOptionType === UNSET_OPTION) {
1024 extendedOptions = defaults[userOptions.type || base.type];
1026 extendedOptions = defaults[tagOptionType];
1028 this.mergedOptions = $.extend({}, base, extendedOptions, userOptions);
1032 getTagSetting: function (key) {
1033 var prefix = this.tagOptionsPrefix,
1034 val, i, pairs, keyval;
1035 if (prefix === false || prefix === undefined) {
1036 return UNSET_OPTION;
1038 if (this.tagValCache.hasOwnProperty(key)) {
1039 val = this.tagValCache.key;
1041 val = this.tag.getAttribute(prefix + key);
1042 if (val === undefined || val === null) {
1044 } else if (val.substr(0, 1) === '[') {
1045 val = val.substr(1, val.length - 2).split(',');
1046 for (i = val.length; i--;) {
1047 val[i] = normalizeValue(val[i].replace(/(^\s*)|(\s*$)/g, ''));
1049 } else if (val.substr(0, 1) === '{') {
1050 pairs = val.substr(1, val.length - 2).split(',');
1052 for (i = pairs.length; i--;) {
1053 keyval = pairs[i].split(':', 2);
1054 val[keyval[0].replace(/(^\s*)|(\s*$)/g, '')] = normalizeValue(keyval[1].replace(/(^\s*)|(\s*$)/g, ''));
1057 val = normalizeValue(val);
1059 this.tagValCache.key = val;
1064 get: function (key, defaultval) {
1065 var tagOption = this.getTagSetting(key),
1067 if (tagOption !== UNSET_OPTION) {
1070 return (result = this.mergedOptions[key]) === undefined ? defaultval : result;
1075 $.fn.sparkline._base = createClass({
1078 init: function (el, values, options, width, height) {
1081 this.values = values;
1082 this.options = options;
1084 this.height = height;
1085 this.currentRegion = undefined;
1091 initTarget: function () {
1092 var interactive = !this.options.get('disableInteraction');
1093 if (!(this.target = this.$el.simpledraw(this.width, this.height, this.options.get('composite'), interactive))) {
1094 this.disabled = true;
1096 this.canvasWidth = this.target.pixelWidth;
1097 this.canvasHeight = this.target.pixelHeight;
1102 * Actually render the chart to the canvas
1104 render: function () {
1105 if (this.disabled) {
1106 this.el.innerHTML = '';
1113 * Return a region id for a given x/y co-ordinate
1115 getRegion: function (x, y) {
1119 * Highlight an item based on the moused-over x,y co-ordinate
1121 setRegionHighlight: function (el, x, y) {
1122 var currentRegion = this.currentRegion,
1123 highlightEnabled = !this.options.get('disableHighlight'),
1125 if (x > this.canvasWidth || y > this.canvasHeight || x < 0 || y < 0) {
1128 newRegion = this.getRegion(el, x, y);
1129 if (currentRegion !== newRegion) {
1130 if (currentRegion !== undefined && highlightEnabled) {
1131 this.removeHighlight();
1133 this.currentRegion = newRegion;
1134 if (newRegion !== undefined && highlightEnabled) {
1135 this.renderHighlight();
1143 * Reset any currently highlighted item
1145 clearRegionHighlight: function () {
1146 if (this.currentRegion !== undefined) {
1147 this.removeHighlight();
1148 this.currentRegion = undefined;
1154 renderHighlight: function () {
1155 this.changeHighlight(true);
1158 removeHighlight: function () {
1159 this.changeHighlight(false);
1162 changeHighlight: function (highlight) {},
1165 * Fetch the HTML to display as a tooltip
1167 getCurrentRegionTooltip: function () {
1168 var options = this.options,
1171 fields, formats, formatlen, fclass, text, i,
1172 showFields, showFieldsKey, newFields, fv,
1173 formatter, format, fieldlen, j;
1174 if (this.currentRegion === undefined) {
1177 fields = this.getCurrentRegionFields();
1178 formatter = options.get('tooltipFormatter');
1180 return formatter(this, options, fields);
1182 if (options.get('tooltipChartTitle')) {
1183 header += '<div class="jqs jqstitle">' + options.get('tooltipChartTitle') + '</div>\n';
1185 formats = this.options.get('tooltipFormat');
1189 if (!$.isArray(formats)) {
1190 formats = [formats];
1192 if (!$.isArray(fields)) {
1195 showFields = this.options.get('tooltipFormatFieldlist');
1196 showFieldsKey = this.options.get('tooltipFormatFieldlistKey');
1197 if (showFields && showFieldsKey) {
1198 // user-selected ordering of fields
1200 for (i = fields.length; i--;) {
1201 fv = fields[i][showFieldsKey];
1202 if ((j = $.inArray(fv, showFields)) != -1) {
1203 newFields[j] = fields[i];
1208 formatlen = formats.length;
1209 fieldlen = fields.length;
1210 for (i = 0; i < formatlen; i++) {
1211 format = formats[i];
1212 if (typeof format === 'string') {
1213 format = new SPFormat(format);
1215 fclass = format.fclass || 'jqsfield';
1216 for (j = 0; j < fieldlen; j++) {
1217 if (!fields[j].isNull || !options.get('tooltipSkipNull')) {
1218 $.extend(fields[j], {
1219 prefix: options.get('tooltipPrefix'),
1220 suffix: options.get('tooltipSuffix')
1222 text = format.render(fields[j], options.get('tooltipValueLookups'), options);
1223 entries.push('<div class="' + fclass + '">' + text + '</div>');
1227 if (entries.length) {
1228 return header + entries.join('\n');
1233 getCurrentRegionFields: function () {},
1235 calcHighlightColor: function (color, options) {
1236 var highlightColor = options.get('highlightColor'),
1237 lighten = options.get('highlightLighten'),
1238 parse, mult, rgbnew, i;
1239 if (highlightColor) {
1240 return highlightColor;
1243 // extract RGB values
1244 parse = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(color) || /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(color);
1247 mult = color.length === 4 ? 16 : 1;
1248 for (i = 0; i < 3; i++) {
1249 rgbnew[i] = clipval(Math.round(parseInt(parse[i + 1], 16) * mult * lighten), 0, 255);
1251 return 'rgb(' + rgbnew.join(',') + ')';
1260 barHighlightMixin = {
1261 changeHighlight: function (highlight) {
1262 var currentRegion = this.currentRegion,
1263 target = this.target,
1264 shapeids = this.regionShapes[currentRegion],
1266 // will be null if the region value was null
1268 newShapes = this.renderRegion(currentRegion, highlight);
1269 if ($.isArray(newShapes) || $.isArray(shapeids)) {
1270 target.replaceWithShapes(shapeids, newShapes);
1271 this.regionShapes[currentRegion] = $.map(newShapes, function (newShape) {
1275 target.replaceWithShape(shapeids, newShapes);
1276 this.regionShapes[currentRegion] = newShapes.id;
1281 render: function () {
1282 var values = this.values,
1283 target = this.target,
1284 regionShapes = this.regionShapes,
1287 if (!this.cls._super.render.call(this)) {
1290 for (i = values.length; i--;) {
1291 shapes = this.renderRegion(i);
1293 if ($.isArray(shapes)) {
1295 for (j = shapes.length; j--;) {
1297 ids.push(shapes[j].id);
1299 regionShapes[i] = ids;
1302 regionShapes[i] = shapes.id; // store just the shapeid
1306 regionShapes[i] = null;
1316 $.fn.sparkline.line = line = createClass($.fn.sparkline._base, {
1319 init: function (el, values, options, width, height) {
1320 line._super.init.call(this, el, values, options, width, height);
1322 this.regionMap = [];
1326 this.hightlightSpotId = null;
1327 this.lastShapeId = null;
1331 getRegion: function (el, x, y) {
1333 regionMap = this.regionMap; // maps regions to value positions
1334 for (i = regionMap.length; i--;) {
1335 if (regionMap[i] !== null && x >= regionMap[i][0] && x <= regionMap[i][1]) {
1336 return regionMap[i][2];
1342 getCurrentRegionFields: function () {
1343 var currentRegion = this.currentRegion;
1345 isNull: this.yvalues[currentRegion] === null,
1346 x: this.xvalues[currentRegion],
1347 y: this.yvalues[currentRegion],
1348 color: this.options.get('lineColor'),
1349 fillColor: this.options.get('fillColor'),
1350 offset: currentRegion
1354 renderHighlight: function () {
1355 var currentRegion = this.currentRegion,
1356 target = this.target,
1357 vertex = this.vertices[currentRegion],
1358 options = this.options,
1359 spotRadius = options.get('spotRadius'),
1360 highlightSpotColor = options.get('highlightSpotColor'),
1361 highlightLineColor = options.get('highlightLineColor'),
1362 highlightSpot, highlightLine;
1367 if (spotRadius && highlightSpotColor) {
1368 highlightSpot = target.drawCircle(vertex[0], vertex[1],
1369 spotRadius, undefined, highlightSpotColor);
1370 this.highlightSpotId = highlightSpot.id;
1371 target.insertAfterShape(this.lastShapeId, highlightSpot);
1373 if (highlightLineColor) {
1374 highlightLine = target.drawLine(vertex[0], this.canvasTop, vertex[0],
1375 this.canvasTop + this.canvasHeight, highlightLineColor);
1376 this.highlightLineId = highlightLine.id;
1377 target.insertAfterShape(this.lastShapeId, highlightLine);
1381 removeHighlight: function () {
1382 var target = this.target;
1383 if (this.highlightSpotId) {
1384 target.removeShapeId(this.highlightSpotId);
1385 this.highlightSpotId = null;
1387 if (this.highlightLineId) {
1388 target.removeShapeId(this.highlightLineId);
1389 this.highlightLineId = null;
1393 scanValues: function () {
1394 var values = this.values,
1395 valcount = values.length,
1396 xvalues = this.xvalues,
1397 yvalues = this.yvalues,
1398 yminmax = this.yminmax,
1399 i, val, isStr, isArray, sp;
1400 for (i = 0; i < valcount; i++) {
1402 isStr = typeof(values[i]) === 'string';
1403 isArray = typeof(values[i]) === 'object' && values[i] instanceof Array;
1404 sp = isStr && values[i].split(':');
1405 if (isStr && sp.length === 2) { // x:y
1406 xvalues.push(Number(sp[0]));
1407 yvalues.push(Number(sp[1]));
1408 yminmax.push(Number(sp[1]));
1409 } else if (isArray) {
1410 xvalues.push(val[0]);
1411 yvalues.push(val[1]);
1412 yminmax.push(val[1]);
1415 if (values[i] === null || values[i] === 'null') {
1418 yvalues.push(Number(val));
1419 yminmax.push(Number(val));
1423 if (this.options.get('xvalues')) {
1424 xvalues = this.options.get('xvalues');
1427 this.maxy = this.maxyorg = Math.max.apply(Math, yminmax);
1428 this.miny = this.minyorg = Math.min.apply(Math, yminmax);
1430 this.maxx = Math.max.apply(Math, xvalues);
1431 this.minx = Math.min.apply(Math, xvalues);
1433 this.xvalues = xvalues;
1434 this.yvalues = yvalues;
1435 this.yminmax = yminmax;
1439 processRangeOptions: function () {
1440 var options = this.options,
1441 normalRangeMin = options.get('normalRangeMin'),
1442 normalRangeMax = options.get('normalRangeMax');
1444 if (normalRangeMin !== undefined) {
1445 if (normalRangeMin < this.miny) {
1446 this.miny = normalRangeMin;
1448 if (normalRangeMax > this.maxy) {
1449 this.maxy = normalRangeMax;
1452 if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.miny)) {
1453 this.miny = options.get('chartRangeMin');
1455 if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.maxy)) {
1456 this.maxy = options.get('chartRangeMax');
1458 if (options.get('chartRangeMinX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMinX') < this.minx)) {
1459 this.minx = options.get('chartRangeMinX');
1461 if (options.get('chartRangeMaxX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMaxX') > this.maxx)) {
1462 this.maxx = options.get('chartRangeMaxX');
1467 drawNormalRange: function (canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey) {
1468 var normalRangeMin = this.options.get('normalRangeMin'),
1469 normalRangeMax = this.options.get('normalRangeMax'),
1470 ytop = canvasTop + Math.round(canvasHeight - (canvasHeight * ((normalRangeMax - this.miny) / rangey))),
1471 height = Math.round((canvasHeight * (normalRangeMax - normalRangeMin)) / rangey);
1472 this.target.drawRect(canvasLeft, ytop, canvasWidth, height, undefined, this.options.get('normalRangeColor')).append();
1475 render: function () {
1476 var options = this.options,
1477 target = this.target,
1478 canvasWidth = this.canvasWidth,
1479 canvasHeight = this.canvasHeight,
1480 vertices = this.vertices,
1481 spotRadius = options.get('spotRadius'),
1482 regionMap = this.regionMap,
1483 rangex, rangey, yvallast,
1484 canvasTop, canvasLeft,
1485 vertex, path, paths, x, y, xnext, xpos, xposnext,
1486 last, next, yvalcount, lineShapes, fillShapes, plen,
1487 valueSpots, color, xvalues, yvalues, i;
1489 if (!line._super.render.call(this)) {
1494 this.processRangeOptions();
1496 xvalues = this.xvalues;
1497 yvalues = this.yvalues;
1499 if (!this.yminmax.length || this.yvalues.length < 2) {
1500 // empty or all null valuess
1504 canvasTop = canvasLeft = 0;
1506 rangex = this.maxx - this.minx === 0 ? 1 : this.maxx - this.minx;
1507 rangey = this.maxy - this.miny === 0 ? 1 : this.maxy - this.miny;
1508 yvallast = this.yvalues.length - 1;
1510 if (spotRadius && (canvasWidth < (spotRadius * 4) || canvasHeight < (spotRadius * 4))) {
1514 // adjust the canvas size as required so that spots will fit
1515 if (options.get('minSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.miny)) {
1516 canvasHeight -= Math.ceil(spotRadius);
1518 if (options.get('maxSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.maxy)) {
1519 canvasHeight -= Math.ceil(spotRadius);
1520 canvasTop += Math.ceil(spotRadius);
1522 if ((options.get('minSpotColor') || options.get('maxSpotColor')) && (yvalues[0] === this.miny || yvalues[0] === this.maxy)) {
1523 canvasLeft += Math.ceil(spotRadius);
1524 canvasWidth -= Math.ceil(spotRadius);
1526 if (options.get('spotColor') ||
1527 (options.get('minSpotColor') || options.get('maxSpotColor') &&
1528 (yvalues[yvallast] === this.miny || yvalues[yvallast] === this.maxy))) {
1529 canvasWidth -= Math.ceil(spotRadius);
1536 if (options.get('normalRangeMin') && !options.get('drawNormalOnTop')) {
1537 this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey);
1543 yvalcount = yvalues.length;
1544 for (i = 0; i < yvalcount; i++) {
1546 xnext = xvalues[i + 1];
1548 xpos = canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex));
1549 xposnext = i < yvalcount - 1 ? canvasLeft + Math.round((xnext - this.minx) * (canvasWidth / rangex)) : canvasWidth;
1550 next = xpos + ((xposnext - xpos) / 2);
1551 regionMap[i] = [last || 0, next, i];
1555 if (yvalues[i - 1] !== null) {
1558 vertices.push(null);
1562 if (y < this.miny) {
1565 if (y > this.maxy) {
1569 // previous value was null
1570 path.push([xpos, canvasTop + canvasHeight]);
1572 vertex = [xpos, canvasTop + Math.round(canvasHeight - (canvasHeight * ((y - this.miny) / rangey)))];
1574 vertices.push(vertex);
1580 plen = paths.length;
1581 for (i = 0; i < plen; i++) {
1584 if (options.get('fillColor')) {
1585 path.push([path[path.length - 1][0], (canvasTop + canvasHeight)]);
1586 fillShapes.push(path.slice(0));
1589 // if there's only a single point in this path, then we want to display it
1590 // as a vertical line which means we keep path[0] as is
1591 if (path.length > 2) {
1592 // else we want the first value
1593 path[0] = [path[0][0], path[1][1]];
1595 lineShapes.push(path);
1599 // draw the fill first, then optionally the normal range, then the line on top of that
1600 plen = fillShapes.length;
1601 for (i = 0; i < plen; i++) {
1602 target.drawShape(fillShapes[i],
1603 options.get('fillColor'), options.get('fillColor')).append();
1606 if (options.get('normalRangeMin') && options.get('drawNormalOnTop')) {
1607 this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey);
1610 plen = lineShapes.length;
1611 for (i = 0; i < plen; i++) {
1612 target.drawShape(lineShapes[i], options.get('lineColor'), undefined,
1613 options.get('lineWidth')).append();
1616 if (spotRadius && options.get('valueSpots')) {
1617 valueSpots = options.get('valueSpots');
1618 if (valueSpots.get === undefined) {
1619 valueSpots = new RangeMap(valueSpots);
1621 for (i = 0; i < yvalcount; i++) {
1622 color = valueSpots.get(yvalues[i]);
1624 target.drawCircle(canvasLeft + Math.round((xvalues[i] - this.minx) * (canvasWidth / rangex)),
1625 canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[i] - this.miny) / rangey))),
1626 spotRadius, undefined,
1632 if (spotRadius && options.get('spotColor')) {
1633 target.drawCircle(canvasLeft + Math.round((xvalues[xvalues.length - 1] - this.minx) * (canvasWidth / rangex)),
1634 canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[yvallast] - this.miny) / rangey))),
1635 spotRadius, undefined,
1636 options.get('spotColor')).append();
1638 if (this.maxy !== this.minyorg) {
1639 if (spotRadius && options.get('minSpotColor')) {
1640 x = xvalues[$.inArray(this.minyorg, yvalues)];
1641 target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)),
1642 canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.minyorg - this.miny) / rangey))),
1643 spotRadius, undefined,
1644 options.get('minSpotColor')).append();
1646 if (spotRadius && options.get('maxSpotColor')) {
1647 x = xvalues[$.inArray(this.maxyorg, yvalues)];
1648 target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)),
1649 canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.maxyorg - this.miny) / rangey))),
1650 spotRadius, undefined,
1651 options.get('maxSpotColor')).append();
1655 this.lastShapeId = target.getLastShapeId();
1656 this.canvasTop = canvasTop;
1664 $.fn.sparkline.bar = bar = createClass($.fn.sparkline._base, barHighlightMixin, {
1667 init: function (el, values, options, width, height) {
1668 var barWidth = parseInt(options.get('barWidth'), 10),
1669 barSpacing = parseInt(options.get('barSpacing'), 10),
1670 chartRangeMin = options.get('chartRangeMin'),
1671 chartRangeMax = options.get('chartRangeMax'),
1672 chartRangeClip = options.get('chartRangeClip'),
1673 stackMin = Infinity,
1674 stackMax = -Infinity,
1675 isStackString, groupMin, groupMax, stackRanges,
1676 numValues, i, vlen, range, zeroAxis, xaxisOffset, min, max, clipMin, clipMax,
1677 stacked, vlist, j, slen, svals, val, yoffset, yMaxCalc, canvasHeightEf;
1678 bar._super.init.call(this, el, values, options, width, height);
1680 // scan values to determine whether to stack bars
1681 for (i = 0, vlen = values.length; i < vlen; i++) {
1683 isStackString = typeof(val) === 'string' && val.indexOf(':') > -1;
1684 if (isStackString || $.isArray(val)) {
1686 if (isStackString) {
1687 val = values[i] = normalizeValues(val.split(':'));
1689 val = remove(val, null); // min/max will treat null as zero
1690 groupMin = Math.min.apply(Math, val);
1691 groupMax = Math.max.apply(Math, val);
1692 if (groupMin < stackMin) {
1693 stackMin = groupMin;
1695 if (groupMax > stackMax) {
1696 stackMax = groupMax;
1701 this.stacked = stacked;
1702 this.regionShapes = {};
1703 this.barWidth = barWidth;
1704 this.barSpacing = barSpacing;
1705 this.totalBarWidth = barWidth + barSpacing;
1706 this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing);
1710 if (chartRangeClip) {
1711 clipMin = chartRangeMin === undefined ? -Infinity : chartRangeMin;
1712 clipMax = chartRangeMax === undefined ? Infinity : chartRangeMax;
1716 stackRanges = stacked ? [] : numValues;
1717 var stackTotals = [];
1718 var stackRangesNeg = [];
1719 for (i = 0, vlen = values.length; i < vlen; i++) {
1722 values[i] = svals = [];
1724 stackRanges[i] = stackRangesNeg[i] = 0;
1725 for (j = 0, slen = vlist.length; j < slen; j++) {
1726 val = svals[j] = chartRangeClip ? clipval(vlist[j], clipMin, clipMax) : vlist[j];
1729 stackTotals[i] += val;
1731 if (stackMin < 0 && stackMax > 0) {
1733 stackRangesNeg[i] += Math.abs(val);
1735 stackRanges[i] += val;
1738 stackRanges[i] += Math.abs(val - (val < 0 ? stackMax : stackMin));
1740 numValues.push(val);
1744 val = chartRangeClip ? clipval(values[i], clipMin, clipMax) : values[i];
1745 val = values[i] = normalizeValue(val);
1747 numValues.push(val);
1751 this.max = max = Math.max.apply(Math, numValues);
1752 this.min = min = Math.min.apply(Math, numValues);
1753 this.stackMax = stackMax = stacked ? Math.max.apply(Math, stackTotals) : max;
1754 this.stackMin = stackMin = stacked ? Math.min.apply(Math, numValues) : min;
1756 if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < min)) {
1757 min = options.get('chartRangeMin');
1759 if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > max)) {
1760 max = options.get('chartRangeMax');
1763 this.zeroAxis = zeroAxis = options.get('zeroAxis', true);
1764 if (min <= 0 && max >= 0 && zeroAxis) {
1766 } else if (zeroAxis == false) {
1768 } else if (min > 0) {
1773 this.xaxisOffset = xaxisOffset;
1775 range = stacked ? (Math.max.apply(Math, stackRanges) + Math.max.apply(Math, stackRangesNeg)) : max - min;
1777 // as we plot zero/min values a single pixel line, we add a pixel to all other
1778 // values - Reduce the effective canvas size to suit
1779 this.canvasHeightEf = (zeroAxis && min < 0) ? this.canvasHeight - 2 : this.canvasHeight - 1;
1781 if (min < xaxisOffset) {
1782 yMaxCalc = (stacked && max >= 0) ? stackMax : max;
1783 yoffset = (yMaxCalc - xaxisOffset) / range * this.canvasHeight;
1784 if (yoffset !== Math.ceil(yoffset)) {
1785 this.canvasHeightEf -= 2;
1786 yoffset = Math.ceil(yoffset);
1789 yoffset = this.canvasHeight;
1791 this.yoffset = yoffset;
1793 if ($.isArray(options.get('colorMap'))) {
1794 this.colorMapByIndex = options.get('colorMap');
1795 this.colorMapByValue = null;
1797 this.colorMapByIndex = null;
1798 this.colorMapByValue = options.get('colorMap');
1799 if (this.colorMapByValue && this.colorMapByValue.get === undefined) {
1800 this.colorMapByValue = new RangeMap(this.colorMapByValue);
1807 getRegion: function (el, x, y) {
1808 var result = Math.floor(x / this.totalBarWidth);
1809 return (result < 0 || result >= this.values.length) ? undefined : result;
1812 getCurrentRegionFields: function () {
1813 var currentRegion = this.currentRegion,
1814 values = ensureArray(this.values[currentRegion]),
1817 for (i = values.length; i--;) {
1820 isNull: value === null,
1822 color: this.calcColor(i, value, currentRegion),
1823 offset: currentRegion
1829 calcColor: function (stacknum, value, valuenum) {
1830 var colorMapByIndex = this.colorMapByIndex,
1831 colorMapByValue = this.colorMapByValue,
1832 options = this.options,
1835 color = options.get('stackedBarColor');
1837 color = (value < 0) ? options.get('negBarColor') : options.get('barColor');
1839 if (value === 0 && options.get('zeroColor') !== undefined) {
1840 color = options.get('zeroColor');
1842 if (colorMapByValue && (newColor = colorMapByValue.get(value))) {
1844 } else if (colorMapByIndex && colorMapByIndex.length > valuenum) {
1845 color = colorMapByIndex[valuenum];
1847 return $.isArray(color) ? color[stacknum % color.length] : color;
1851 * Render bar(s) for a region
1853 renderRegion: function (valuenum, highlight) {
1854 var vals = this.values[valuenum],
1855 options = this.options,
1856 xaxisOffset = this.xaxisOffset,
1859 stacked = this.stacked,
1860 target = this.target,
1861 x = valuenum * this.totalBarWidth,
1862 canvasHeightEf = this.canvasHeightEf,
1863 yoffset = this.yoffset,
1864 y, height, color, isNull, yoffsetNeg, i, valcount, val, minPlotted, allMin;
1866 vals = $.isArray(vals) ? vals : [vals];
1867 valcount = vals.length;
1869 isNull = all(null, vals);
1870 allMin = all(xaxisOffset, vals, true);
1873 if (options.get('nullColor')) {
1874 color = highlight ? options.get('nullColor') : this.calcHighlightColor(options.get('nullColor'), options);
1875 y = (yoffset > 0) ? yoffset - 1 : yoffset;
1876 return target.drawRect(x, y, this.barWidth - 1, 0, color, color);
1881 yoffsetNeg = yoffset;
1882 for (i = 0; i < valcount; i++) {
1885 if (stacked && val === xaxisOffset) {
1886 if (!allMin || minPlotted) {
1893 height = Math.floor(canvasHeightEf * ((Math.abs(val - xaxisOffset) / range))) + 1;
1897 if (val < xaxisOffset || (val === xaxisOffset && yoffset === 0)) {
1899 yoffsetNeg += height;
1901 y = yoffset - height;
1904 color = this.calcColor(i, val, valuenum);
1906 color = this.calcHighlightColor(color, options);
1908 result.push(target.drawRect(x, y, this.barWidth - 1, height - 1, color, color));
1910 if (result.length === 1) {
1920 $.fn.sparkline.tristate = tristate = createClass($.fn.sparkline._base, barHighlightMixin, {
1923 init: function (el, values, options, width, height) {
1924 var barWidth = parseInt(options.get('barWidth'), 10),
1925 barSpacing = parseInt(options.get('barSpacing'), 10);
1926 tristate._super.init.call(this, el, values, options, width, height);
1928 this.regionShapes = {};
1929 this.barWidth = barWidth;
1930 this.barSpacing = barSpacing;
1931 this.totalBarWidth = barWidth + barSpacing;
1932 this.values = $.map(values, Number);
1933 this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing);
1935 if ($.isArray(options.get('colorMap'))) {
1936 this.colorMapByIndex = options.get('colorMap');
1937 this.colorMapByValue = null;
1939 this.colorMapByIndex = null;
1940 this.colorMapByValue = options.get('colorMap');
1941 if (this.colorMapByValue && this.colorMapByValue.get === undefined) {
1942 this.colorMapByValue = new RangeMap(this.colorMapByValue);
1948 getRegion: function (el, x, y) {
1949 return Math.floor(x / this.totalBarWidth);
1952 getCurrentRegionFields: function () {
1953 var currentRegion = this.currentRegion;
1955 isNull: this.values[currentRegion] === undefined,
1956 value: this.values[currentRegion],
1957 color: this.calcColor(this.values[currentRegion], currentRegion),
1958 offset: currentRegion
1962 calcColor: function (value, valuenum) {
1963 var values = this.values,
1964 options = this.options,
1965 colorMapByIndex = this.colorMapByIndex,
1966 colorMapByValue = this.colorMapByValue,
1969 if (colorMapByValue && (newColor = colorMapByValue.get(value))) {
1971 } else if (colorMapByIndex && colorMapByIndex.length > valuenum) {
1972 color = colorMapByIndex[valuenum];
1973 } else if (values[valuenum] < 0) {
1974 color = options.get('negBarColor');
1975 } else if (values[valuenum] > 0) {
1976 color = options.get('posBarColor');
1978 color = options.get('zeroBarColor');
1983 renderRegion: function (valuenum, highlight) {
1984 var values = this.values,
1985 options = this.options,
1986 target = this.target,
1987 canvasHeight, height, halfHeight,
1990 canvasHeight = target.pixelHeight;
1991 halfHeight = Math.round(canvasHeight / 2);
1993 x = valuenum * this.totalBarWidth;
1994 if (values[valuenum] < 0) {
1996 height = halfHeight - 1;
1997 } else if (values[valuenum] > 0) {
1999 height = halfHeight - 1;
2004 color = this.calcColor(values[valuenum], valuenum);
2005 if (color === null) {
2009 color = this.calcHighlightColor(color, options);
2011 return target.drawRect(x, y, this.barWidth - 1, height - 1, color, color);
2018 $.fn.sparkline.discrete = discrete = createClass($.fn.sparkline._base, barHighlightMixin, {
2021 init: function (el, values, options, width, height) {
2022 discrete._super.init.call(this, el, values, options, width, height);
2024 this.regionShapes = {};
2025 this.values = values = $.map(values, Number);
2026 this.min = Math.min.apply(Math, values);
2027 this.max = Math.max.apply(Math, values);
2028 this.range = this.max - this.min;
2029 this.width = width = options.get('width') === 'auto' ? values.length * 2 : this.width;
2030 this.interval = Math.floor(width / values.length);
2031 this.itemWidth = width / values.length;
2032 if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.min)) {
2033 this.min = options.get('chartRangeMin');
2035 if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.max)) {
2036 this.max = options.get('chartRangeMax');
2040 this.lineHeight = options.get('lineHeight') === 'auto' ? Math.round(this.canvasHeight * 0.3) : options.get('lineHeight');
2044 getRegion: function (el, x, y) {
2045 return Math.floor(x / this.itemWidth);
2048 getCurrentRegionFields: function () {
2049 var currentRegion = this.currentRegion;
2051 isNull: this.values[currentRegion] === undefined,
2052 value: this.values[currentRegion],
2053 offset: currentRegion
2057 renderRegion: function (valuenum, highlight) {
2058 var values = this.values,
2059 options = this.options,
2063 interval = this.interval,
2064 target = this.target,
2065 canvasHeight = this.canvasHeight,
2066 lineHeight = this.lineHeight,
2067 pheight = canvasHeight - lineHeight,
2068 ytop, val, color, x;
2070 val = clipval(values[valuenum], min, max);
2071 x = valuenum * interval;
2072 ytop = Math.round(pheight - pheight * ((val - min) / range));
2073 color = (options.get('thresholdColor') && val < options.get('thresholdValue')) ? options.get('thresholdColor') : options.get('lineColor');
2075 color = this.calcHighlightColor(color, options);
2077 return target.drawLine(x, ytop, x, ytop + lineHeight, color);
2084 $.fn.sparkline.bullet = bullet = createClass($.fn.sparkline._base, {
2087 init: function (el, values, options, width, height) {
2089 bullet._super.init.call(this, el, values, options, width, height);
2091 // values: target, performance, range1, range2, range3
2092 values = $.map(values, Number);
2093 min = Math.min.apply(Math, values);
2094 max = Math.max.apply(Math, values);
2095 if (options.get('base') === undefined) {
2096 min = min < 0 ? min : 0;
2098 min = options.get('base');
2102 this.range = max - min;
2104 this.valueShapes = {};
2105 this.regiondata = {};
2106 this.width = width = options.get('width') === 'auto' ? '4.0em' : width;
2107 this.target = this.$el.simpledraw(width, height, options.get('composite'));
2108 if (!values.length) {
2109 this.disabled = true;
2114 getRegion: function (el, x, y) {
2115 var shapeid = this.target.getShapeAt(el, x, y);
2116 return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined;
2119 getCurrentRegionFields: function () {
2120 var currentRegion = this.currentRegion;
2122 fieldkey: currentRegion.substr(0, 1),
2123 value: this.values[currentRegion.substr(1)],
2124 region: currentRegion
2128 changeHighlight: function (highlight) {
2129 var currentRegion = this.currentRegion,
2130 shapeid = this.valueShapes[currentRegion],
2132 delete this.shapes[shapeid];
2133 switch (currentRegion.substr(0, 1)) {
2135 shape = this.renderRange(currentRegion.substr(1), highlight);
2138 shape = this.renderPerformance(highlight);
2141 shape = this.renderTarget(highlight);
2144 this.valueShapes[currentRegion] = shape.id;
2145 this.shapes[shape.id] = currentRegion;
2146 this.target.replaceWithShape(shapeid, shape);
2149 renderRange: function (rn, highlight) {
2150 var rangeval = this.values[rn],
2151 rangewidth = Math.round(this.canvasWidth * ((rangeval - this.min) / this.range)),
2152 color = this.options.get('rangeColors')[rn - 2];
2154 color = this.calcHighlightColor(color, this.options);
2156 return this.target.drawRect(0, 0, rangewidth - 1, this.canvasHeight - 1, color, color);
2159 renderPerformance: function (highlight) {
2160 var perfval = this.values[1],
2161 perfwidth = Math.round(this.canvasWidth * ((perfval - this.min) / this.range)),
2162 color = this.options.get('performanceColor');
2164 color = this.calcHighlightColor(color, this.options);
2166 return this.target.drawRect(0, Math.round(this.canvasHeight * 0.3), perfwidth - 1,
2167 Math.round(this.canvasHeight * 0.4) - 1, color, color);
2170 renderTarget: function (highlight) {
2171 var targetval = this.values[0],
2172 x = Math.round(this.canvasWidth * ((targetval - this.min) / this.range) - (this.options.get('targetWidth') / 2)),
2173 targettop = Math.round(this.canvasHeight * 0.10),
2174 targetheight = this.canvasHeight - (targettop * 2),
2175 color = this.options.get('targetColor');
2177 color = this.calcHighlightColor(color, this.options);
2179 return this.target.drawRect(x, targettop, this.options.get('targetWidth') - 1, targetheight - 1, color, color);
2182 render: function () {
2183 var vlen = this.values.length,
2184 target = this.target,
2186 if (!bullet._super.render.call(this)) {
2189 for (i = 2; i < vlen; i++) {
2190 shape = this.renderRange(i).append();
2191 this.shapes[shape.id] = 'r' + i;
2192 this.valueShapes['r' + i] = shape.id;
2194 shape = this.renderPerformance().append();
2195 this.shapes[shape.id] = 'p1';
2196 this.valueShapes.p1 = shape.id;
2197 shape = this.renderTarget().append();
2198 this.shapes[shape.id] = 't0';
2199 this.valueShapes.t0 = shape.id;
2207 $.fn.sparkline.pie = pie = createClass($.fn.sparkline._base, {
2210 init: function (el, values, options, width, height) {
2213 pie._super.init.call(this, el, values, options, width, height);
2215 this.shapes = {}; // map shape ids to value offsets
2216 this.valueShapes = {}; // maps value offsets to shape ids
2217 this.values = values = $.map(values, Number);
2219 if (options.get('width') === 'auto') {
2220 this.width = this.height;
2223 if (values.length > 0) {
2224 for (i = values.length; i--;) {
2230 this.radius = Math.floor(Math.min(this.canvasWidth, this.canvasHeight) / 2);
2233 getRegion: function (el, x, y) {
2234 var shapeid = this.target.getShapeAt(el, x, y);
2235 return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined;
2238 getCurrentRegionFields: function () {
2239 var currentRegion = this.currentRegion;
2241 isNull: this.values[currentRegion] === undefined,
2242 value: this.values[currentRegion],
2243 percent: this.values[currentRegion] / this.total * 100,
2244 color: this.options.get('sliceColors')[currentRegion % this.options.get('sliceColors').length],
2245 offset: currentRegion
2249 changeHighlight: function (highlight) {
2250 var currentRegion = this.currentRegion,
2251 newslice = this.renderSlice(currentRegion, highlight),
2252 shapeid = this.valueShapes[currentRegion];
2253 delete this.shapes[shapeid];
2254 this.target.replaceWithShape(shapeid, newslice);
2255 this.valueShapes[currentRegion] = newslice.id;
2256 this.shapes[newslice.id] = currentRegion;
2259 renderSlice: function (valuenum, highlight) {
2260 var target = this.target,
2261 options = this.options,
2262 radius = this.radius,
2263 borderWidth = options.get('borderWidth'),
2264 offset = options.get('offset'),
2265 circle = 2 * Math.PI,
2266 values = this.values,
2268 next = offset ? (2*Math.PI)*(offset/360) : 0,
2269 start, end, i, vlen, color;
2271 vlen = values.length;
2272 for (i = 0; i < vlen; i++) {
2275 if (total > 0) { // avoid divide by zero
2276 end = next + (circle * (values[i] / total));
2278 if (valuenum === i) {
2279 color = options.get('sliceColors')[i % options.get('sliceColors').length];
2281 color = this.calcHighlightColor(color, options);
2284 return target.drawPieSlice(radius, radius, radius - borderWidth, start, end, undefined, color);
2290 render: function () {
2291 var target = this.target,
2292 values = this.values,
2293 options = this.options,
2294 radius = this.radius,
2295 borderWidth = options.get('borderWidth'),
2298 if (!pie._super.render.call(this)) {
2302 target.drawCircle(radius, radius, Math.floor(radius - (borderWidth / 2)),
2303 options.get('borderColor'), undefined, borderWidth).append();
2305 for (i = values.length; i--;) {
2306 shape = this.renderSlice(i).append();
2307 this.valueShapes[i] = shape.id; // store just the shapeid
2308 this.shapes[shape.id] = i;
2317 $.fn.sparkline.box = box = createClass($.fn.sparkline._base, {
2320 init: function (el, values, options, width, height) {
2321 box._super.init.call(this, el, values, options, width, height);
2322 this.values = $.map(values, Number);
2323 this.width = options.get('width') === 'auto' ? '4.0em' : width;
2325 if (!this.values.length) {
2331 * Simulate a single region
2333 getRegion: function () {
2337 getCurrentRegionFields: function () {
2339 { field: 'lq', value: this.quartiles[0] },
2340 { field: 'med', value: this.quartiles[1] },
2341 { field: 'uq', value: this.quartiles[2] },
2342 { field: 'lo', value: this.loutlier },
2343 { field: 'ro', value: this.routlier }
2345 if (this.lwhisker !== undefined) {
2346 result.push({ field: 'lw', value: this.lwhisker});
2348 if (this.rwhisker !== undefined) {
2349 result.push({ field: 'rw', value: this.rwhisker});
2354 render: function () {
2355 var target = this.target,
2356 values = this.values,
2357 vlen = values.length,
2358 options = this.options,
2359 canvasWidth = this.canvasWidth,
2360 canvasHeight = this.canvasHeight,
2361 minValue = options.get('chartRangeMin') === undefined ? Math.min.apply(Math, values) : options.get('chartRangeMin'),
2362 maxValue = options.get('chartRangeMax') === undefined ? Math.max.apply(Math, values) : options.get('chartRangeMax'),
2364 lwhisker, loutlier, iqr, q1, q2, q3, rwhisker, routlier, i,
2367 if (!box._super.render.call(this)) {
2371 if (options.get('raw')) {
2372 if (options.get('showOutliers') && values.length > 5) {
2373 loutlier = values[0];
2374 lwhisker = values[1];
2378 rwhisker = values[5];
2379 routlier = values[6];
2381 lwhisker = values[0];
2385 rwhisker = values[4];
2388 values.sort(function (a, b) { return a - b; });
2389 q1 = quartile(values, 1);
2390 q2 = quartile(values, 2);
2391 q3 = quartile(values, 3);
2393 if (options.get('showOutliers')) {
2394 lwhisker = rwhisker = undefined;
2395 for (i = 0; i < vlen; i++) {
2396 if (lwhisker === undefined && values[i] > q1 - (iqr * options.get('outlierIQR'))) {
2397 lwhisker = values[i];
2399 if (values[i] < q3 + (iqr * options.get('outlierIQR'))) {
2400 rwhisker = values[i];
2403 loutlier = values[0];
2404 routlier = values[vlen - 1];
2406 lwhisker = values[0];
2407 rwhisker = values[vlen - 1];
2410 this.quartiles = [q1, q2, q3];
2411 this.lwhisker = lwhisker;
2412 this.rwhisker = rwhisker;
2413 this.loutlier = loutlier;
2414 this.routlier = routlier;
2416 unitSize = canvasWidth / (maxValue - minValue + 1);
2417 if (options.get('showOutliers')) {
2418 canvasLeft = Math.ceil(options.get('spotRadius'));
2419 canvasWidth -= 2 * Math.ceil(options.get('spotRadius'));
2420 unitSize = canvasWidth / (maxValue - minValue + 1);
2421 if (loutlier < lwhisker) {
2422 target.drawCircle((loutlier - minValue) * unitSize + canvasLeft,
2424 options.get('spotRadius'),
2425 options.get('outlierLineColor'),
2426 options.get('outlierFillColor')).append();
2428 if (routlier > rwhisker) {
2429 target.drawCircle((routlier - minValue) * unitSize + canvasLeft,
2431 options.get('spotRadius'),
2432 options.get('outlierLineColor'),
2433 options.get('outlierFillColor')).append();
2439 Math.round((q1 - minValue) * unitSize + canvasLeft),
2440 Math.round(canvasHeight * 0.1),
2441 Math.round((q3 - q1) * unitSize),
2442 Math.round(canvasHeight * 0.8),
2443 options.get('boxLineColor'),
2444 options.get('boxFillColor')).append();
2447 Math.round((lwhisker - minValue) * unitSize + canvasLeft),
2448 Math.round(canvasHeight / 2),
2449 Math.round((q1 - minValue) * unitSize + canvasLeft),
2450 Math.round(canvasHeight / 2),
2451 options.get('lineColor')).append();
2453 Math.round((lwhisker - minValue) * unitSize + canvasLeft),
2454 Math.round(canvasHeight / 4),
2455 Math.round((lwhisker - minValue) * unitSize + canvasLeft),
2456 Math.round(canvasHeight - canvasHeight / 4),
2457 options.get('whiskerColor')).append();
2459 target.drawLine(Math.round((rwhisker - minValue) * unitSize + canvasLeft),
2460 Math.round(canvasHeight / 2),
2461 Math.round((q3 - minValue) * unitSize + canvasLeft),
2462 Math.round(canvasHeight / 2),
2463 options.get('lineColor')).append();
2465 Math.round((rwhisker - minValue) * unitSize + canvasLeft),
2466 Math.round(canvasHeight / 4),
2467 Math.round((rwhisker - minValue) * unitSize + canvasLeft),
2468 Math.round(canvasHeight - canvasHeight / 4),
2469 options.get('whiskerColor')).append();
2472 Math.round((q2 - minValue) * unitSize + canvasLeft),
2473 Math.round(canvasHeight * 0.1),
2474 Math.round((q2 - minValue) * unitSize + canvasLeft),
2475 Math.round(canvasHeight * 0.9),
2476 options.get('medianColor')).append();
2477 if (options.get('target')) {
2478 size = Math.ceil(options.get('spotRadius'));
2480 Math.round((options.get('target') - minValue) * unitSize + canvasLeft),
2481 Math.round((canvasHeight / 2) - size),
2482 Math.round((options.get('target') - minValue) * unitSize + canvasLeft),
2483 Math.round((canvasHeight / 2) + size),
2484 options.get('targetColor')).append();
2486 Math.round((options.get('target') - minValue) * unitSize + canvasLeft - size),
2487 Math.round(canvasHeight / 2),
2488 Math.round((options.get('target') - minValue) * unitSize + canvasLeft + size),
2489 Math.round(canvasHeight / 2),
2490 options.get('targetColor')).append();
2496 // Setup a very simple "virtual canvas" to make drawing the few shapes we need easier
2497 // This is accessible as $(foo).simpledraw()
2499 if ($.browser.msie && !document.namespaces.v) {
2500 document.namespaces.add('v', 'urn:schemas-microsoft-com:vml', '#default#VML');
2503 if ($.browser.hasCanvas === undefined) {
2504 $.browser.hasCanvas = document.createElement('canvas').getContext !== undefined;
2507 VShape = createClass({
2508 init: function (target, id, type, args) {
2509 this.target = target;
2514 append: function () {
2515 this.target.appendShape(this);
2520 VCanvas_base = createClass({
2521 _pxregex: /(\d+)(px)?\s*$/i,
2523 init: function (width, height, target) {
2528 this.height = height;
2529 this.target = target;
2530 this.lastShapeId = null;
2534 $.data(target, '_jqs_vcanvas', this);
2537 drawLine: function (x1, y1, x2, y2, lineColor, lineWidth) {
2538 return this.drawShape([[x1, y1], [x2, y2]], lineColor, lineWidth);
2541 drawShape: function (path, lineColor, fillColor, lineWidth) {
2542 return this._genShape('Shape', [path, lineColor, fillColor, lineWidth]);
2545 drawCircle: function (x, y, radius, lineColor, fillColor, lineWidth) {
2546 return this._genShape('Circle', [x, y, radius, lineColor, fillColor, lineWidth]);
2549 drawPieSlice: function (x, y, radius, startAngle, endAngle, lineColor, fillColor) {
2550 return this._genShape('PieSlice', [x, y, radius, startAngle, endAngle, lineColor, fillColor]);
2553 drawRect: function (x, y, width, height, lineColor, fillColor) {
2554 return this._genShape('Rect', [x, y, width, height, lineColor, fillColor]);
2557 getElement: function () {
2562 * Return the most recently inserted shape id
2564 getLastShapeId: function () {
2565 return this.lastShapeId;
2569 * Clear and reset the canvas
2571 reset: function () {
2572 alert('reset not implemented');
2575 _insert: function (el, target) {
2580 * Calculate the pixel dimensions of the canvas
2582 _calculatePixelDims: function (width, height, canvas) {
2583 // XXX This should probably be a configurable option
2585 match = this._pxregex.exec(height);
2587 this.pixelHeight = match[1];
2589 this.pixelHeight = $(canvas).height();
2591 match = this._pxregex.exec(width);
2593 this.pixelWidth = match[1];
2595 this.pixelWidth = $(canvas).width();
2600 * Generate a shape object and id for later rendering
2602 _genShape: function (shapetype, shapeargs) {
2603 var id = shapeCount++;
2604 shapeargs.unshift(id);
2605 return new VShape(this, id, shapetype, shapeargs);
2609 * Add a shape to the end of the render queue
2611 appendShape: function (shape) {
2612 alert('appendShape not implemented');
2616 * Replace one shape with another
2618 replaceWithShape: function (shapeid, shape) {
2619 alert('replaceWithShape not implemented');
2623 * Insert one shape after another in the render queue
2625 insertAfterShape: function (shapeid, shape) {
2626 alert('insertAfterShape not implemented');
2630 * Remove a shape from the queue
2632 removeShapeId: function (shapeid) {
2633 alert('removeShapeId not implemented');
2637 * Find a shape at the specified x/y co-ordinates
2639 getShapeAt: function (el, x, y) {
2640 alert('getShapeAt not implemented');
2644 * Render all queued shapes onto the canvas
2646 render: function () {
2647 alert('render not implemented');
2651 VCanvas_canvas = createClass(VCanvas_base, {
2652 init: function (width, height, target, interact) {
2653 VCanvas_canvas._super.init.call(this, width, height, target);
2654 this.canvas = document.createElement('canvas');
2658 $.data(target, '_jqs_vcanvas', this);
2659 $(this.canvas).css({ display: 'inline-block', width: width, height: height, verticalAlign: 'top' });
2660 this._insert(this.canvas, target);
2661 this._calculatePixelDims(width, height, this.canvas);
2662 this.canvas.width = this.pixelWidth;
2663 this.canvas.height = this.pixelHeight;
2664 this.interact = interact;
2667 this.currentTargetShapeId = undefined;
2668 $(this.canvas).css({width: this.pixelWidth, height: this.pixelHeight});
2671 _getContext: function (lineColor, fillColor, lineWidth) {
2672 var context = this.canvas.getContext('2d');
2673 if (lineColor !== undefined) {
2674 context.strokeStyle = lineColor;
2676 context.lineWidth = lineWidth === undefined ? 1 : lineWidth;
2677 if (fillColor !== undefined) {
2678 context.fillStyle = fillColor;
2683 reset: function () {
2684 var context = this._getContext();
2685 context.clearRect(0, 0, this.pixelWidth, this.pixelHeight);
2688 this.currentTargetShapeId = undefined;
2691 _drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) {
2692 var context = this._getContext(lineColor, fillColor, lineWidth),
2694 context.beginPath();
2695 context.moveTo(path[0][0] + 0.5, path[0][1] + 0.5);
2696 for (i = 1, plen = path.length; i < plen; i++) {
2697 context.lineTo(path[i][0] + 0.5, path[i][1] + 0.5); // the 0.5 offset gives us crisp pixel-width lines
2699 if (lineColor !== undefined) {
2702 if (fillColor !== undefined) {
2705 if (this.targetX !== undefined && this.targetY !== undefined &&
2706 context.isPointInPath(this.targetX, this.targetY)) {
2707 this.currentTargetShapeId = shapeid;
2711 _drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) {
2712 var context = this._getContext(lineColor, fillColor, lineWidth);
2713 context.beginPath();
2714 context.arc(x, y, radius, 0, 2 * Math.PI, false);
2715 if (this.targetX !== undefined && this.targetY !== undefined &&
2716 context.isPointInPath(this.targetX, this.targetY)) {
2717 this.currentTargetShapeId = shapeid;
2719 if (lineColor !== undefined) {
2722 if (fillColor !== undefined) {
2727 _drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) {
2728 var context = this._getContext(lineColor, fillColor);
2729 context.beginPath();
2730 context.moveTo(x, y);
2731 context.arc(x, y, radius, startAngle, endAngle, false);
2732 context.lineTo(x, y);
2733 context.closePath();
2734 if (lineColor !== undefined) {
2740 if (this.targetX !== undefined && this.targetY !== undefined &&
2741 context.isPointInPath(this.targetX, this.targetY)) {
2742 this.currentTargetShapeId = shapeid;
2746 _drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) {
2747 return this._drawShape(shapeid, [[x, y], [x + width, y], [x + width, y + height], [x, y + height], [x, y]], lineColor, fillColor);
2750 appendShape: function (shape) {
2751 this.shapes[shape.id] = shape;
2752 this.shapeseq.push(shape.id);
2753 this.lastShapeId = shape.id;
2757 replaceWithShape: function (shapeid, shape) {
2758 var shapeseq = this.shapeseq,
2760 this.shapes[shape.id] = shape;
2761 for (i = shapeseq.length; i--;) {
2762 if (shapeseq[i] == shapeid) {
2763 shapeseq[i] = shape.id;
2766 delete this.shapes[shapeid];
2769 replaceWithShapes: function (shapeids, shapes) {
2770 var shapeseq = this.shapeseq,
2774 for (i = shapeids.length; i--;) {
2775 shapemap[shapeids[i]] = true;
2777 for (i = shapeseq.length; i--;) {
2779 if (shapemap[sid]) {
2780 shapeseq.splice(i, 1);
2781 delete this.shapes[sid];
2785 for (i = shapes.length; i--;) {
2786 shapeseq.splice(first, 0, shapes[i].id);
2787 this.shapes[shapes[i].id] = shapes[i];
2792 insertAfterShape: function (shapeid, shape) {
2793 var shapeseq = this.shapeseq,
2795 for (i = shapeseq.length; i--;) {
2796 if (shapeseq[i] === shapeid) {
2797 shapeseq.splice(i + 1, 0, shape.id);
2798 this.shapes[shape.id] = shape;
2804 removeShapeId: function (shapeid) {
2805 var shapeseq = this.shapeseq,
2807 for (i = shapeseq.length; i--;) {
2808 if (shapeseq[i] === shapeid) {
2809 shapeseq.splice(i, 1);
2813 delete this.shapes[shapeid];
2816 getShapeAt: function (el, x, y) {
2820 return this.currentTargetShapeId;
2823 render: function () {
2824 var shapeseq = this.shapeseq,
2825 shapes = this.shapes,
2826 shapeCount = shapeseq.length,
2827 context = this._getContext(),
2829 context.clearRect(0, 0, this.pixelWidth, this.pixelHeight);
2830 for (i = 0; i < shapeCount; i++) {
2831 shapeid = shapeseq[i];
2832 shape = shapes[shapeid];
2833 this['_draw' + shape.type].apply(this, shape.args);
2835 if (!this.interact) {
2836 // not interactive so no need to keep the shapes array
2844 VCanvas_vml = createClass(VCanvas_base, {
2845 init: function (width, height, target) {
2847 VCanvas_vml._super.init.call(this, width, height, target);
2851 $.data(target, '_jqs_vcanvas', this);
2852 this.canvas = document.createElement('span');
2853 $(this.canvas).css({ display: 'inline-block', position: 'relative', overflow: 'hidden', width: width, height: height, margin: '0px', padding: '0px', verticalAlign: 'top'});
2854 this._insert(this.canvas, target);
2855 this._calculatePixelDims(width, height, this.canvas);
2856 this.canvas.width = this.pixelWidth;
2857 this.canvas.height = this.pixelHeight;
2858 groupel = '<v:group coordorigin="0 0" coordsize="' + this.pixelWidth + ' ' + this.pixelHeight + '"' +
2859 ' style="position:absolute;top:0;left:0;width:' + this.pixelWidth + 'px;height=' + this.pixelHeight + 'px;"></v:group>';
2860 this.canvas.insertAdjacentHTML('beforeEnd', groupel);
2861 this.group = $(this.canvas).children()[0];
2862 this.rendered = false;
2863 this.prerender = '';
2866 _drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) {
2868 initial, stroke, fill, closed, vel, plen, i;
2869 for (i = 0, plen = path.length; i < plen; i++) {
2870 vpath[i] = '' + (path[i][0]) + ',' + (path[i][1]);
2872 initial = vpath.splice(0, 1);
2873 lineWidth = lineWidth === undefined ? 1 : lineWidth;
2874 stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" ';
2875 fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
2876 closed = vpath[0] === vpath[vpath.length - 1] ? 'x ' : '';
2877 vel = '<v:shape coordorigin="0 0" coordsize="' + this.pixelWidth + ' ' + this.pixelHeight + '" ' +
2878 ' id="jqsshape' + shapeid + '" ' +
2881 ' style="position:absolute;left:0px;top:0px;height:' + this.pixelHeight + 'px;width:' + this.pixelWidth + 'px;padding:0px;margin:0px;" ' +
2882 ' path="m ' + initial + ' l ' + vpath.join(', ') + ' ' + closed + 'e">' +
2887 _drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) {
2888 var stroke, fill, vel;
2891 stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" ';
2892 fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
2894 ' id="jqsshape' + shapeid + '" ' +
2897 ' style="position:absolute;top:' + y + 'px; left:' + x + 'px; width:' + (radius * 2) + 'px; height:' + (radius * 2) + 'px"></v:oval>';
2902 _drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) {
2903 var vpath, startx, starty, endx, endy, stroke, fill, vel;
2904 if (startAngle === endAngle) {
2905 return; // VML seems to have problem when start angle equals end angle.
2907 if ((endAngle - startAngle) === (2 * Math.PI)) {
2908 startAngle = 0.0; // VML seems to have a problem when drawing a full circle that doesn't start 0
2909 endAngle = (2 * Math.PI);
2912 startx = x + Math.round(Math.cos(startAngle) * radius);
2913 starty = y + Math.round(Math.sin(startAngle) * radius);
2914 endx = x + Math.round(Math.cos(endAngle) * radius);
2915 endy = y + Math.round(Math.sin(endAngle) * radius);
2917 // Prevent very small slices from being mistaken as a whole pie
2918 if (startx === endx && starty === endy && (endAngle - startAngle) < Math.PI) {
2922 vpath = [x - radius, y - radius, x + radius, y + radius, startx, starty, endx, endy];
2923 stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="1px" strokeColor="' + lineColor + '" ';
2924 fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
2925 vel = '<v:shape coordorigin="0 0" coordsize="' + this.pixelWidth + ' ' + this.pixelHeight + '" ' +
2926 ' id="jqsshape' + shapeid + '" ' +
2929 ' style="position:absolute;left:0px;top:0px;height:' + this.pixelHeight + 'px;width:' + this.pixelWidth + 'px;padding:0px;margin:0px;" ' +
2930 ' path="m ' + x + ',' + y + ' wa ' + vpath.join(', ') + ' x e">' +
2935 _drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) {
2936 return this._drawShape(shapeid, [[x, y], [x, y + height], [x + width, y + height], [x + width, y], [x, y]], lineColor, fillColor);
2939 reset: function () {
2940 this.group.innerHTML = '';
2943 appendShape: function (shape) {
2944 var vel = this['_draw' + shape.type].apply(this, shape.args);
2945 if (this.rendered) {
2946 this.group.insertAdjacentHTML('beforeEnd', vel);
2948 this.prerender += vel;
2950 this.lastShapeId = shape.id;
2954 replaceWithShape: function (shapeid, shape) {
2955 var existing = $('#jqsshape' + shapeid),
2956 vel = this['_draw' + shape.type].apply(this, shape.args);
2957 existing[0].outerHTML = vel;
2960 replaceWithShapes: function (shapeids, shapes) {
2961 // replace the first shapeid with all the new shapes then toast the remaining old shapes
2962 var existing = $('#jqsshape' + shapeids[0]),
2964 slen = shapes.length,
2966 for (i = 0; i < slen; i++) {
2967 replace += this['_draw' + shapes[i].type].apply(this, shapes[i].args);
2969 existing[0].outerHTML = replace;
2970 for (i = 1; i < shapeids.length; i++) {
2971 $('#jqsshape' + shapeids[i]).remove();
2975 insertAfterShape: function (shapeid, shape) {
2976 var existing = $('#jqsshape' + shapeid),
2977 vel = this['_draw' + shape.type].apply(this, shape.args);
2978 existing[0].insertAdjacentHTML('afterEnd', vel);
2981 removeShapeId: function (shapeid) {
2982 var existing = $('#jqsshape' + shapeid);
2983 this.group.removeChild(existing[0]);
2986 getShapeAt: function (el, x, y) {
2987 var shapeid = el.id.substr(8);
2991 render: function () {
2992 if (!this.rendered) {
2993 // batch the intial render into a single repaint
2994 this.group.innerHTML = this.prerender;
2995 this.rendered = true;