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 * outlierIQR - Interquartile range used to determine outliers. Default 1.5
183 * boxLineColor - Outline color of the box
184 * boxFillColor - Fill color for the box
185 * whiskerColor - Line color used for whiskers
186 * outlierLineColor - Outline color of outlier circles
187 * outlierFillColor - Fill color of the outlier circles
188 * spotRadius - Radius of outlier circles
189 * medianColor - Line color of the median line
190 * target - Draw a target cross hair at the supplied value (default undefined)
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 */
205 (function(document, Math, undefined) { // performance/minified-size optimization
207 if(typeof define === 'function' && define.amd) {
208 define(['jquery'], factory);
209 } else if (jQuery && !jQuery.fn.sparkline) {
216 var UNSET_OPTION = {},
217 getDefaults, createClass, SPFormat, clipval, quartile, normalizeValue, normalizeValues,
218 remove, isNumber, all, sum, addCSS, ensureArray, formatNumber, RangeMap,
219 MouseHandler, Tooltip, barHighlightMixin,
220 line, bar, tristate, discrete, bullet, pie, box, defaultStyles, initStyles,
221 VShape, VCanvas_base, VCanvas_canvas, VCanvas_vml, pending, shapeCount = 0;
224 * Default configuration settings
226 getDefaults = function () {
228 // Settings common to most/all chart types
233 defaultPixelsPerValue: 3,
237 tagValuesAttribute: 'values',
238 tagOptionsPrefix: 'spark',
239 enableTagOptions: false,
240 enableHighlight: true,
241 highlightLighten: 1.4,
242 tooltipSkipNull: true,
245 disableHiddenCheck: false,
246 numberFormatter: false,
247 numberDigitGroupCount: 3,
248 numberDigitGroupSep: ',',
249 numberDecimalMark: '.',
250 disableTooltips: false,
251 disableInteraction: false
253 // Defaults for line charts
256 highlightSpotColor: '#5f5',
257 highlightLineColor: '#f22',
259 minSpotColor: '#f80',
260 maxSpotColor: '#f80',
262 normalRangeMin: undefined,
263 normalRangeMax: undefined,
264 normalRangeColor: '#ccc',
265 drawNormalOnTop: false,
266 chartRangeMin: undefined,
267 chartRangeMax: undefined,
268 chartRangeMinX: undefined,
269 chartRangeMaxX: undefined,
270 tooltipFormat: new SPFormat('<span style="color: {{color}}">●</span> {{prefix}}{{y}}{{suffix}}')
272 // Defaults for bar charts
276 stackedBarColor: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00',
277 '#dd4477', '#0099c6', '#990099'],
278 zeroColor: undefined,
279 nullColor: undefined,
283 chartRangeMax: undefined,
284 chartRangeMin: undefined,
285 chartRangeClip: false,
287 tooltipFormat: new SPFormat('<span style="color: {{color}}">●</span> {{prefix}}{{value}}{{suffix}}')
289 // Defaults for tristate charts
295 zeroBarColor: '#999',
297 tooltipFormat: new SPFormat('<span style="color: {{color}}">●</span> {{value:map}}'),
298 tooltipValueLookups: { map: { '-1': 'Loss', '0': 'Draw', '1': 'Win' } }
300 // Defaults for discrete charts
303 thresholdColor: undefined,
305 chartRangeMax: undefined,
306 chartRangeMin: undefined,
307 chartRangeClip: false,
308 tooltipFormat: new SPFormat('{{prefix}}{{value}}{{suffix}}')
310 // Defaults for bullet charts
313 targetWidth: 3, // width of the target bar in pixels
314 performanceColor: '#33f',
315 rangeColors: ['#d3dafe', '#a8b6ff', '#7f94ff'],
316 base: undefined, // set this to a number to change the base start number
317 tooltipFormat: new SPFormat('{{fieldkey:fields}} - {{value}}'),
318 tooltipValueLookups: { fields: {r: 'Range', p: 'Performance', t: 'Target'} }
320 // Defaults for pie charts
323 sliceColors: ['#3366cc', '#dc3912', '#ff9900', '#109618', '#66aa00',
324 '#dd4477', '#0099c6', '#990099'],
327 tooltipFormat: new SPFormat('<span style="color: {{color}}">●</span> {{value}} ({{percent.1}}%)')
329 // Defaults for box plots
332 boxLineColor: '#000',
333 boxFillColor: '#cdf',
334 whiskerColor: '#000',
335 outlierLineColor: '#333',
336 outlierFillColor: '#fff',
343 chartRangeMax: undefined,
344 chartRangeMin: undefined,
345 tooltipFormat: new SPFormat('{{field:fields}}: {{value}}'),
346 tooltipFormatFieldlistKey: 'field',
347 tooltipValueLookups: { fields: { lq: 'Lower Quartile', med: 'Median',
348 uq: 'Upper Quartile', lo: 'Left Outlier', ro: 'Right Outlier',
349 lw: 'Left Whisker', rw: 'Right Whisker'} }
354 // You can have tooltips use a css class other than jqstooltip by specifying tooltipClassname
355 defaultStyles = '.jqstooltip { ' +
356 'position: absolute;' +
359 'visibility: hidden;' +
360 'background: rgb(0, 0, 0) transparent;' +
361 'background-color: rgba(0,0,0,0.6);' +
362 'filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000);' +
363 '-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000)";' +
365 'font: 10px arial, san serif;' +
366 'text-align: left;' +
367 'white-space: nowrap;' +
369 'border: 1px solid white;' +
374 'font: 10px arial, san serif;' +
375 'text-align: left;' +
382 createClass = function (/* [baseclass, [mixin, ...]], definition */) {
384 Class = function () {
385 this.init.apply(this, arguments);
387 if (arguments.length > 1) {
389 Class.prototype = $.extend(new arguments[0](), arguments[arguments.length - 1]);
390 Class._super = arguments[0].prototype;
392 Class.prototype = arguments[arguments.length - 1];
394 if (arguments.length > 2) {
395 args = Array.prototype.slice.call(arguments, 1, -1);
396 args.unshift(Class.prototype);
397 $.extend.apply($, args);
400 Class.prototype = arguments[0];
402 Class.prototype.cls = Class;
407 * Wraps a format string for tooltips
412 $.SPFormatClass = SPFormat = createClass({
413 fre: /\{\{([\w.]+?)(:(.+?))?\}\}/g,
414 precre: /(\w+)\.(\d+)/,
416 init: function (format, fclass) {
417 this.format = format;
418 this.fclass = fclass;
421 render: function (fieldset, lookups, options) {
424 match, token, lookupkey, fieldvalue, prec;
425 return this.format.replace(this.fre, function () {
427 token = arguments[1];
428 lookupkey = arguments[3];
429 match = self.precre.exec(token);
436 fieldvalue = fields[token];
437 if (fieldvalue === undefined) {
440 if (lookupkey && lookups && lookups[lookupkey]) {
441 lookup = lookups[lookupkey];
442 if (lookup.get) { // RangeMap
443 return lookups[lookupkey].get(fieldvalue) || fieldvalue;
445 return lookups[lookupkey][fieldvalue] || fieldvalue;
448 if (isNumber(fieldvalue)) {
449 if (options.get('numberFormatter')) {
450 fieldvalue = options.get('numberFormatter')(fieldvalue);
452 fieldvalue = formatNumber(fieldvalue, prec,
453 options.get('numberDigitGroupCount'),
454 options.get('numberDigitGroupSep'),
455 options.get('numberDecimalMark'));
463 // convience method to avoid needing the new operator
464 $.spformat = function(format, fclass) {
465 return new SPFormat(format, fclass);
468 clipval = function (val, min, max) {
478 quartile = function (values, q) {
481 vl = Math.floor(values.length / 2);
482 return values.length % 2 ? values[vl] : (values[vl-1] + values[vl]) / 2;
484 if (values.length % 2 ) { // odd
485 vl = (values.length * q + q) / 4;
486 return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl-1];
488 vl = (values.length * q + 2) / 4;
489 return vl % 1 ? (values[Math.floor(vl)] + values[Math.floor(vl) - 1]) / 2 : values[vl-1];
495 normalizeValue = function (val) {
511 nf = parseFloat(val);
519 normalizeValues = function (vals) {
521 for (i = vals.length; i--;) {
522 result[i] = normalizeValue(vals[i]);
527 remove = function (vals, filter) {
528 var i, vl, result = [];
529 for (i = 0, vl = vals.length; i < vl; i++) {
530 if (vals[i] !== filter) {
531 result.push(vals[i]);
537 isNumber = function (num) {
538 return !isNaN(parseFloat(num)) && isFinite(num);
541 formatNumber = function (num, prec, groupsize, groupsep, decsep) {
543 num = (prec === false ? parseFloat(num).toString() : num.toFixed(prec)).split('');
544 p = (p = $.inArray('.', num)) < 0 ? num.length : p;
545 if (p < num.length) {
548 for (i = p - groupsize; i > 0; i -= groupsize) {
549 num.splice(i, 0, groupsep);
554 // determine if all values of an array match a value
555 // returns true if the array is empty
556 all = function (val, arr, ignoreNull) {
558 for (i = arr.length; i--; ) {
559 if (ignoreNull && arr[i] === null) continue;
560 if (arr[i] !== val) {
567 // sums the numeric values in an array, ignoring other values
568 sum = function (vals) {
570 for (i = vals.length; i--;) {
571 total += typeof vals[i] === 'number' ? vals[i] : 0;
576 ensureArray = function (val) {
577 return $.isArray(val) ? val : [val];
580 // http://paulirish.com/2008/bookmarklet-inject-new-css-rules/
581 addCSS = function(css) {
583 //if ('\v' == 'v') /* ie only */ {
584 if (document.createStyleSheet) {
585 document.createStyleSheet().cssText = css;
587 tag = document.createElement('style');
588 tag.type = 'text/css';
589 document.getElementsByTagName('head')[0].appendChild(tag);
590 tag[(typeof document.body.style.WebkitAppearance == 'string') /* webkit only */ ? 'innerText' : 'innerHTML'] = css;
594 // Provide a cross-browser interface to a few simple drawing primitives
595 $.fn.simpledraw = function (width, height, useExisting, interact) {
596 var target, mhandler;
597 if (useExisting && (target = this.data('_jqs_vcanvas'))) {
601 if ($.fn.sparkline.canvas === false) {
602 // We've already determined that neither Canvas nor VML are available
605 } else if ($.fn.sparkline.canvas === undefined) {
606 // No function defined yet -- need to see if we support Canvas or VML
607 var el = document.createElement('canvas');
608 if (!!(el.getContext && el.getContext('2d'))) {
609 // Canvas is available
610 $.fn.sparkline.canvas = function(width, height, target, interact) {
611 return new VCanvas_canvas(width, height, target, interact);
613 } else if (document.namespaces && !document.namespaces.v) {
615 document.namespaces.add('v', 'urn:schemas-microsoft-com:vml', '#default#VML');
616 $.fn.sparkline.canvas = function(width, height, target, interact) {
617 return new VCanvas_vml(width, height, target);
620 // Neither Canvas nor VML are available
621 $.fn.sparkline.canvas = false;
626 if (width === undefined) {
627 width = $(this).innerWidth();
629 if (height === undefined) {
630 height = $(this).innerHeight();
633 target = $.fn.sparkline.canvas(width, height, this, interact);
635 mhandler = $(this).data('_jqs_mhandler');
637 mhandler.registerCanvas(target);
642 $.fn.cleardraw = function () {
643 var target = this.data('_jqs_vcanvas');
649 $.RangeMapClass = RangeMap = createClass({
650 init: function (map) {
651 var key, range, rangelist = [];
653 if (map.hasOwnProperty(key) && typeof key === 'string' && key.indexOf(':') > -1) {
654 range = key.split(':');
655 range[0] = range[0].length === 0 ? -Infinity : parseFloat(range[0]);
656 range[1] = range[1].length === 0 ? Infinity : parseFloat(range[1]);
658 rangelist.push(range);
662 this.rangelist = rangelist || false;
665 get: function (value) {
666 var rangelist = this.rangelist,
668 if ((result = this.map[value]) !== undefined) {
672 for (i = rangelist.length; i--;) {
673 range = rangelist[i];
674 if (range[0] <= value && range[1] >= value) {
683 // Convenience function
684 $.range_map = function(map) {
685 return new RangeMap(map);
688 MouseHandler = createClass({
689 init: function (el, options) {
692 this.options = options;
693 this.currentPageX = 0;
694 this.currentPageY = 0;
699 this.displayTooltips = !options.get('disableTooltips');
700 this.highlightEnabled = !options.get('disableHighlight');
703 registerSparkline: function (sp) {
704 this.splist.push(sp);
706 this.updateDisplay();
710 registerCanvas: function (canvas) {
711 var $canvas = $(canvas.canvas);
712 this.canvas = canvas;
713 this.$canvas = $canvas;
714 $canvas.mouseenter($.proxy(this.mouseenter, this));
715 $canvas.mouseleave($.proxy(this.mouseleave, this));
716 $canvas.click($.proxy(this.mouseclick, this));
719 reset: function (removeTooltip) {
721 if (this.tooltip && removeTooltip) {
722 this.tooltip.remove();
723 this.tooltip = undefined;
727 mouseclick: function (e) {
728 var clickEvent = $.Event('sparklineClick');
729 clickEvent.originalEvent = e;
730 clickEvent.sparklines = this.splist;
731 this.$el.trigger(clickEvent);
734 mouseenter: function (e) {
735 $(document.body).unbind('mousemove.jqs');
736 $(document.body).bind('mousemove.jqs', $.proxy(this.mousemove, this));
738 this.currentPageX = e.pageX;
739 this.currentPageY = e.pageY;
740 this.currentEl = e.target;
741 if (!this.tooltip && this.displayTooltips) {
742 this.tooltip = new Tooltip(this.options);
743 this.tooltip.updatePosition(e.pageX, e.pageY);
745 this.updateDisplay();
748 mouseleave: function () {
749 $(document.body).unbind('mousemove.jqs');
750 var splist = this.splist,
751 spcount = splist.length,
752 needsRefresh = false,
755 this.currentEl = null;
758 this.tooltip.remove();
762 for (i = 0; i < spcount; i++) {
764 if (sp.clearRegionHighlight()) {
770 this.canvas.render();
774 mousemove: function (e) {
775 this.currentPageX = e.pageX;
776 this.currentPageY = e.pageY;
777 this.currentEl = e.target;
779 this.tooltip.updatePosition(e.pageX, e.pageY);
781 this.updateDisplay();
784 updateDisplay: function () {
785 var splist = this.splist,
786 spcount = splist.length,
787 needsRefresh = false,
788 offset = this.$canvas.offset(),
789 localX = this.currentPageX - offset.left,
790 localY = this.currentPageY - offset.top,
791 tooltiphtml, sp, i, result, changeEvent;
795 for (i = 0; i < spcount; i++) {
797 result = sp.setRegionHighlight(this.currentEl, localX, localY);
803 changeEvent = $.Event('sparklineRegionChange');
804 changeEvent.sparklines = this.splist;
805 this.$el.trigger(changeEvent);
808 for (i = 0; i < spcount; i++) {
810 tooltiphtml += sp.getCurrentRegionTooltip();
812 this.tooltip.setContent(tooltiphtml);
814 if (!this.disableHighlight) {
815 this.canvas.render();
818 if (result === null) {
825 Tooltip = createClass({
826 sizeStyle: 'position: static !important;' +
827 'display: block !important;' +
828 'visibility: hidden !important;' +
829 'float: left !important;',
831 init: function (options) {
832 var tooltipClassname = options.get('tooltipClassname', 'jqstooltip'),
833 sizetipStyle = this.sizeStyle,
835 this.container = options.get('tooltipContainer') || document.body;
836 this.tooltipOffsetX = options.get('tooltipOffsetX', 10);
837 this.tooltipOffsetY = options.get('tooltipOffsetY', 12);
838 // remove any previous lingering tooltip
839 $('#jqssizetip').remove();
840 $('#jqstooltip').remove();
841 this.sizetip = $('<div/>', {
844 'class': tooltipClassname
846 this.tooltip = $('<div/>', {
848 'class': tooltipClassname
849 }).appendTo(this.container);
850 // account for the container's location
851 offset = this.tooltip.offset();
852 this.offsetLeft = offset.left;
853 this.offsetTop = offset.top;
855 $(window).unbind('resize.jqs scroll.jqs');
856 $(window).bind('resize.jqs scroll.jqs', $.proxy(this.updateWindowDims, this));
857 this.updateWindowDims();
860 updateWindowDims: function () {
861 this.scrollTop = $(window).scrollTop();
862 this.scrollLeft = $(window).scrollLeft();
863 this.scrollRight = this.scrollLeft + $(window).width();
864 this.updatePosition();
867 getSize: function (content) {
868 this.sizetip.html(content).appendTo(this.container);
869 this.width = this.sizetip.width() + 1;
870 this.height = this.sizetip.height();
871 this.sizetip.remove();
874 setContent: function (content) {
876 this.tooltip.css('visibility', 'hidden');
880 this.getSize(content);
881 this.tooltip.html(content)
884 'height': this.height,
885 'visibility': 'visible'
889 this.updatePosition();
893 updatePosition: function (x, y) {
894 if (x === undefined) {
895 if (this.mousex === undefined) {
898 x = this.mousex - this.offsetLeft;
899 y = this.mousey - this.offsetTop;
902 this.mousex = x = x - this.offsetLeft;
903 this.mousey = y = y - this.offsetTop;
905 if (!this.height || !this.width || this.hidden) {
909 y -= this.height + this.tooltipOffsetY;
910 x += this.tooltipOffsetX;
912 if (y < this.scrollTop) {
915 if (x < this.scrollLeft) {
917 } else if (x + this.width > this.scrollRight) {
918 x = this.scrollRight - this.width;
927 remove: function () {
928 this.tooltip.remove();
929 this.sizetip.remove();
930 this.sizetip = this.tooltip = undefined;
931 $(window).unbind('resize.jqs scroll.jqs');
935 initStyles = function() {
936 addCSS(defaultStyles);
942 $.fn.sparkline = function (userValues, userOptions) {
943 return this.each(function () {
944 var options = new $.fn.sparkline.options(this, userOptions),
947 render = function () {
948 var values, width, height, tmp, mhandler, sp, vals;
949 if (userValues === 'html' || userValues === undefined) {
950 vals = this.getAttribute(options.get('tagValuesAttribute'));
951 if (vals === undefined || vals === null) {
954 values = vals.replace(/(^\s*<!--)|(-->\s*$)|\s+/g, '').split(',');
959 width = options.get('width') === 'auto' ? values.length * options.get('defaultPixelsPerValue') : options.get('width');
960 if (options.get('height') === 'auto') {
961 if (!options.get('composite') || !$.data(this, '_jqs_vcanvas')) {
962 // must be a better way to get the line height
963 tmp = document.createElement('span');
966 height = $(tmp).innerHeight() || $(tmp).height();
971 height = options.get('height');
974 if (!options.get('disableInteraction')) {
975 mhandler = $.data(this, '_jqs_mhandler');
977 mhandler = new MouseHandler(this, options);
978 $.data(this, '_jqs_mhandler', mhandler);
979 } else if (!options.get('composite')) {
986 if (options.get('composite') && !$.data(this, '_jqs_vcanvas')) {
987 if (!$.data(this, '_jqs_errnotify')) {
988 alert('Attempted to attach a composite sparkline to an element with no existing sparkline');
989 $.data(this, '_jqs_errnotify', true);
994 sp = new $.fn.sparkline[options.get('type')](this, values, options, width, height);
999 mhandler.registerSparkline(sp);
1002 if (($(this).html() && !options.get('disableHiddenCheck') && $(this).is(':hidden')) || !$(this).parents('body').length) {
1003 if (!options.get('composite') && $.data(this, '_jqs_pending')) {
1004 // remove any existing references to the element
1005 for (i = pending.length; i; i--) {
1006 if (pending[i - 1][0] == this) {
1007 pending.splice(i - 1, 1);
1011 pending.push([this, render]);
1012 $.data(this, '_jqs_pending', true);
1019 $.fn.sparkline.defaults = getDefaults();
1022 $.sparkline_display_visible = function () {
1025 for (i = 0, pl = pending.length; i < pl; i++) {
1027 if ($(el).is(':visible') && !$(el).parents().is(':hidden')) {
1028 pending[i][1].call(el);
1029 $.data(pending[i][0], '_jqs_pending', false);
1031 } else if (!$(el).closest('html').length && !$.data(el, '_jqs_pending')) {
1032 // element has been inserted and removed from the DOM
1033 // If it was not yet inserted into the dom then the .data request
1034 // will return true.
1035 // removing from the dom causes the data to be removed.
1036 $.data(pending[i][0], '_jqs_pending', false);
1040 for (i = done.length; i; i--) {
1041 pending.splice(done[i - 1], 1);
1047 * User option handler
1049 $.fn.sparkline.options = createClass({
1050 init: function (tag, userOptions) {
1051 var extendedOptions, defaults, base, tagOptionType;
1052 this.userOptions = userOptions = userOptions || {};
1054 this.tagValCache = {};
1055 defaults = $.fn.sparkline.defaults;
1056 base = defaults.common;
1057 this.tagOptionsPrefix = userOptions.enableTagOptions && (userOptions.tagOptionsPrefix || base.tagOptionsPrefix);
1059 tagOptionType = this.getTagSetting('type');
1060 if (tagOptionType === UNSET_OPTION) {
1061 extendedOptions = defaults[userOptions.type || base.type];
1063 extendedOptions = defaults[tagOptionType];
1065 this.mergedOptions = $.extend({}, base, extendedOptions, userOptions);
1069 getTagSetting: function (key) {
1070 var prefix = this.tagOptionsPrefix,
1071 val, i, pairs, keyval;
1072 if (prefix === false || prefix === undefined) {
1073 return UNSET_OPTION;
1075 if (this.tagValCache.hasOwnProperty(key)) {
1076 val = this.tagValCache.key;
1078 val = this.tag.getAttribute(prefix + key);
1079 if (val === undefined || val === null) {
1081 } else if (val.substr(0, 1) === '[') {
1082 val = val.substr(1, val.length - 2).split(',');
1083 for (i = val.length; i--;) {
1084 val[i] = normalizeValue(val[i].replace(/(^\s*)|(\s*$)/g, ''));
1086 } else if (val.substr(0, 1) === '{') {
1087 pairs = val.substr(1, val.length - 2).split(',');
1089 for (i = pairs.length; i--;) {
1090 keyval = pairs[i].split(':', 2);
1091 val[keyval[0].replace(/(^\s*)|(\s*$)/g, '')] = normalizeValue(keyval[1].replace(/(^\s*)|(\s*$)/g, ''));
1094 val = normalizeValue(val);
1096 this.tagValCache.key = val;
1101 get: function (key, defaultval) {
1102 var tagOption = this.getTagSetting(key),
1104 if (tagOption !== UNSET_OPTION) {
1107 return (result = this.mergedOptions[key]) === undefined ? defaultval : result;
1112 $.fn.sparkline._base = createClass({
1115 init: function (el, values, options, width, height) {
1118 this.values = values;
1119 this.options = options;
1121 this.height = height;
1122 this.currentRegion = undefined;
1128 initTarget: function () {
1129 var interactive = !this.options.get('disableInteraction');
1130 if (!(this.target = this.$el.simpledraw(this.width, this.height, this.options.get('composite'), interactive))) {
1131 this.disabled = true;
1133 this.canvasWidth = this.target.pixelWidth;
1134 this.canvasHeight = this.target.pixelHeight;
1139 * Actually render the chart to the canvas
1141 render: function () {
1142 if (this.disabled) {
1143 this.el.innerHTML = '';
1150 * Return a region id for a given x/y co-ordinate
1152 getRegion: function (x, y) {
1156 * Highlight an item based on the moused-over x,y co-ordinate
1158 setRegionHighlight: function (el, x, y) {
1159 var currentRegion = this.currentRegion,
1160 highlightEnabled = !this.options.get('disableHighlight'),
1162 if (x > this.canvasWidth || y > this.canvasHeight || x < 0 || y < 0) {
1165 newRegion = this.getRegion(el, x, y);
1166 if (currentRegion !== newRegion) {
1167 if (currentRegion !== undefined && highlightEnabled) {
1168 this.removeHighlight();
1170 this.currentRegion = newRegion;
1171 if (newRegion !== undefined && highlightEnabled) {
1172 this.renderHighlight();
1180 * Reset any currently highlighted item
1182 clearRegionHighlight: function () {
1183 if (this.currentRegion !== undefined) {
1184 this.removeHighlight();
1185 this.currentRegion = undefined;
1191 renderHighlight: function () {
1192 this.changeHighlight(true);
1195 removeHighlight: function () {
1196 this.changeHighlight(false);
1199 changeHighlight: function (highlight) {},
1202 * Fetch the HTML to display as a tooltip
1204 getCurrentRegionTooltip: function () {
1205 var options = this.options,
1208 fields, formats, formatlen, fclass, text, i,
1209 showFields, showFieldsKey, newFields, fv,
1210 formatter, format, fieldlen, j;
1211 if (this.currentRegion === undefined) {
1214 fields = this.getCurrentRegionFields();
1215 formatter = options.get('tooltipFormatter');
1217 return formatter(this, options, fields);
1219 if (options.get('tooltipChartTitle')) {
1220 header += '<div class="jqs jqstitle">' + options.get('tooltipChartTitle') + '</div>\n';
1222 formats = this.options.get('tooltipFormat');
1226 if (!$.isArray(formats)) {
1227 formats = [formats];
1229 if (!$.isArray(fields)) {
1232 showFields = this.options.get('tooltipFormatFieldlist');
1233 showFieldsKey = this.options.get('tooltipFormatFieldlistKey');
1234 if (showFields && showFieldsKey) {
1235 // user-selected ordering of fields
1237 for (i = fields.length; i--;) {
1238 fv = fields[i][showFieldsKey];
1239 if ((j = $.inArray(fv, showFields)) != -1) {
1240 newFields[j] = fields[i];
1245 formatlen = formats.length;
1246 fieldlen = fields.length;
1247 for (i = 0; i < formatlen; i++) {
1248 format = formats[i];
1249 if (typeof format === 'string') {
1250 format = new SPFormat(format);
1252 fclass = format.fclass || 'jqsfield';
1253 for (j = 0; j < fieldlen; j++) {
1254 if (!fields[j].isNull || !options.get('tooltipSkipNull')) {
1255 $.extend(fields[j], {
1256 prefix: options.get('tooltipPrefix'),
1257 suffix: options.get('tooltipSuffix')
1259 text = format.render(fields[j], options.get('tooltipValueLookups'), options);
1260 entries.push('<div class="' + fclass + '">' + text + '</div>');
1264 if (entries.length) {
1265 return header + entries.join('\n');
1270 getCurrentRegionFields: function () {},
1272 calcHighlightColor: function (color, options) {
1273 var highlightColor = options.get('highlightColor'),
1274 lighten = options.get('highlightLighten'),
1275 parse, mult, rgbnew, i;
1276 if (highlightColor) {
1277 return highlightColor;
1280 // extract RGB values
1281 parse = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(color) || /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(color);
1284 mult = color.length === 4 ? 16 : 1;
1285 for (i = 0; i < 3; i++) {
1286 rgbnew[i] = clipval(Math.round(parseInt(parse[i + 1], 16) * mult * lighten), 0, 255);
1288 return 'rgb(' + rgbnew.join(',') + ')';
1297 barHighlightMixin = {
1298 changeHighlight: function (highlight) {
1299 var currentRegion = this.currentRegion,
1300 target = this.target,
1301 shapeids = this.regionShapes[currentRegion],
1303 // will be null if the region value was null
1305 newShapes = this.renderRegion(currentRegion, highlight);
1306 if ($.isArray(newShapes) || $.isArray(shapeids)) {
1307 target.replaceWithShapes(shapeids, newShapes);
1308 this.regionShapes[currentRegion] = $.map(newShapes, function (newShape) {
1312 target.replaceWithShape(shapeids, newShapes);
1313 this.regionShapes[currentRegion] = newShapes.id;
1318 render: function () {
1319 var values = this.values,
1320 target = this.target,
1321 regionShapes = this.regionShapes,
1324 if (!this.cls._super.render.call(this)) {
1327 for (i = values.length; i--;) {
1328 shapes = this.renderRegion(i);
1330 if ($.isArray(shapes)) {
1332 for (j = shapes.length; j--;) {
1334 ids.push(shapes[j].id);
1336 regionShapes[i] = ids;
1339 regionShapes[i] = shapes.id; // store just the shapeid
1343 regionShapes[i] = null;
1353 $.fn.sparkline.line = line = createClass($.fn.sparkline._base, {
1356 init: function (el, values, options, width, height) {
1357 line._super.init.call(this, el, values, options, width, height);
1359 this.regionMap = [];
1363 this.hightlightSpotId = null;
1364 this.lastShapeId = null;
1368 getRegion: function (el, x, y) {
1370 regionMap = this.regionMap; // maps regions to value positions
1371 for (i = regionMap.length; i--;) {
1372 if (regionMap[i] !== null && x >= regionMap[i][0] && x <= regionMap[i][1]) {
1373 return regionMap[i][2];
1379 getCurrentRegionFields: function () {
1380 var currentRegion = this.currentRegion;
1382 isNull: this.yvalues[currentRegion] === null,
1383 x: this.xvalues[currentRegion],
1384 y: this.yvalues[currentRegion],
1385 color: this.options.get('lineColor'),
1386 fillColor: this.options.get('fillColor'),
1387 offset: currentRegion
1391 renderHighlight: function () {
1392 var currentRegion = this.currentRegion,
1393 target = this.target,
1394 vertex = this.vertices[currentRegion],
1395 options = this.options,
1396 spotRadius = options.get('spotRadius'),
1397 highlightSpotColor = options.get('highlightSpotColor'),
1398 highlightLineColor = options.get('highlightLineColor'),
1399 highlightSpot, highlightLine;
1404 if (spotRadius && highlightSpotColor) {
1405 highlightSpot = target.drawCircle(vertex[0], vertex[1],
1406 spotRadius, undefined, highlightSpotColor);
1407 this.highlightSpotId = highlightSpot.id;
1408 target.insertAfterShape(this.lastShapeId, highlightSpot);
1410 if (highlightLineColor) {
1411 highlightLine = target.drawLine(vertex[0], this.canvasTop, vertex[0],
1412 this.canvasTop + this.canvasHeight, highlightLineColor);
1413 this.highlightLineId = highlightLine.id;
1414 target.insertAfterShape(this.lastShapeId, highlightLine);
1418 removeHighlight: function () {
1419 var target = this.target;
1420 if (this.highlightSpotId) {
1421 target.removeShapeId(this.highlightSpotId);
1422 this.highlightSpotId = null;
1424 if (this.highlightLineId) {
1425 target.removeShapeId(this.highlightLineId);
1426 this.highlightLineId = null;
1430 scanValues: function () {
1431 var values = this.values,
1432 valcount = values.length,
1433 xvalues = this.xvalues,
1434 yvalues = this.yvalues,
1435 yminmax = this.yminmax,
1436 i, val, isStr, isArray, sp;
1437 for (i = 0; i < valcount; i++) {
1439 isStr = typeof(values[i]) === 'string';
1440 isArray = typeof(values[i]) === 'object' && values[i] instanceof Array;
1441 sp = isStr && values[i].split(':');
1442 if (isStr && sp.length === 2) { // x:y
1443 xvalues.push(Number(sp[0]));
1444 yvalues.push(Number(sp[1]));
1445 yminmax.push(Number(sp[1]));
1446 } else if (isArray) {
1447 xvalues.push(val[0]);
1448 yvalues.push(val[1]);
1449 yminmax.push(val[1]);
1452 if (values[i] === null || values[i] === 'null') {
1455 yvalues.push(Number(val));
1456 yminmax.push(Number(val));
1460 if (this.options.get('xvalues')) {
1461 xvalues = this.options.get('xvalues');
1464 this.maxy = this.maxyorg = Math.max.apply(Math, yminmax);
1465 this.miny = this.minyorg = Math.min.apply(Math, yminmax);
1467 this.maxx = Math.max.apply(Math, xvalues);
1468 this.minx = Math.min.apply(Math, xvalues);
1470 this.xvalues = xvalues;
1471 this.yvalues = yvalues;
1472 this.yminmax = yminmax;
1476 processRangeOptions: function () {
1477 var options = this.options,
1478 normalRangeMin = options.get('normalRangeMin'),
1479 normalRangeMax = options.get('normalRangeMax');
1481 if (normalRangeMin !== undefined) {
1482 if (normalRangeMin < this.miny) {
1483 this.miny = normalRangeMin;
1485 if (normalRangeMax > this.maxy) {
1486 this.maxy = normalRangeMax;
1489 if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.miny)) {
1490 this.miny = options.get('chartRangeMin');
1492 if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.maxy)) {
1493 this.maxy = options.get('chartRangeMax');
1495 if (options.get('chartRangeMinX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMinX') < this.minx)) {
1496 this.minx = options.get('chartRangeMinX');
1498 if (options.get('chartRangeMaxX') !== undefined && (options.get('chartRangeClipX') || options.get('chartRangeMaxX') > this.maxx)) {
1499 this.maxx = options.get('chartRangeMaxX');
1504 drawNormalRange: function (canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey) {
1505 var normalRangeMin = this.options.get('normalRangeMin'),
1506 normalRangeMax = this.options.get('normalRangeMax'),
1507 ytop = canvasTop + Math.round(canvasHeight - (canvasHeight * ((normalRangeMax - this.miny) / rangey))),
1508 height = Math.round((canvasHeight * (normalRangeMax - normalRangeMin)) / rangey);
1509 this.target.drawRect(canvasLeft, ytop, canvasWidth, height, undefined, this.options.get('normalRangeColor')).append();
1512 render: function () {
1513 var options = this.options,
1514 target = this.target,
1515 canvasWidth = this.canvasWidth,
1516 canvasHeight = this.canvasHeight,
1517 vertices = this.vertices,
1518 spotRadius = options.get('spotRadius'),
1519 regionMap = this.regionMap,
1520 rangex, rangey, yvallast,
1521 canvasTop, canvasLeft,
1522 vertex, path, paths, x, y, xnext, xpos, xposnext,
1523 last, next, yvalcount, lineShapes, fillShapes, plen,
1524 valueSpots, hlSpotsEnabled, color, xvalues, yvalues, i;
1526 if (!line._super.render.call(this)) {
1531 this.processRangeOptions();
1533 xvalues = this.xvalues;
1534 yvalues = this.yvalues;
1536 if (!this.yminmax.length || this.yvalues.length < 2) {
1537 // empty or all null valuess
1541 canvasTop = canvasLeft = 0;
1543 rangex = this.maxx - this.minx === 0 ? 1 : this.maxx - this.minx;
1544 rangey = this.maxy - this.miny === 0 ? 1 : this.maxy - this.miny;
1545 yvallast = this.yvalues.length - 1;
1547 if (spotRadius && (canvasWidth < (spotRadius * 4) || canvasHeight < (spotRadius * 4))) {
1551 // adjust the canvas size as required so that spots will fit
1552 hlSpotsEnabled = options.get('highlightSpotColor') && !options.get('disableInteraction');
1553 if (hlSpotsEnabled || options.get('minSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.miny)) {
1554 canvasHeight -= Math.ceil(spotRadius);
1556 if (hlSpotsEnabled || options.get('maxSpotColor') || (options.get('spotColor') && yvalues[yvallast] === this.maxy)) {
1557 canvasHeight -= Math.ceil(spotRadius);
1558 canvasTop += Math.ceil(spotRadius);
1560 if (hlSpotsEnabled ||
1561 ((options.get('minSpotColor') || options.get('maxSpotColor')) && (yvalues[0] === this.miny || yvalues[0] === this.maxy))) {
1562 canvasLeft += Math.ceil(spotRadius);
1563 canvasWidth -= Math.ceil(spotRadius);
1565 if (hlSpotsEnabled || options.get('spotColor') ||
1566 (options.get('minSpotColor') || options.get('maxSpotColor') &&
1567 (yvalues[yvallast] === this.miny || yvalues[yvallast] === this.maxy))) {
1568 canvasWidth -= Math.ceil(spotRadius);
1575 if (options.get('normalRangeMin') !== undefined && !options.get('drawNormalOnTop')) {
1576 this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey);
1582 yvalcount = yvalues.length;
1583 for (i = 0; i < yvalcount; i++) {
1585 xnext = xvalues[i + 1];
1587 xpos = canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex));
1588 xposnext = i < yvalcount - 1 ? canvasLeft + Math.round((xnext - this.minx) * (canvasWidth / rangex)) : canvasWidth;
1589 next = xpos + ((xposnext - xpos) / 2);
1590 regionMap[i] = [last || 0, next, i];
1594 if (yvalues[i - 1] !== null) {
1598 vertices.push(null);
1601 if (y < this.miny) {
1604 if (y > this.maxy) {
1608 // previous value was null
1609 path.push([xpos, canvasTop + canvasHeight]);
1611 vertex = [xpos, canvasTop + Math.round(canvasHeight - (canvasHeight * ((y - this.miny) / rangey)))];
1613 vertices.push(vertex);
1619 plen = paths.length;
1620 for (i = 0; i < plen; i++) {
1623 if (options.get('fillColor')) {
1624 path.push([path[path.length - 1][0], (canvasTop + canvasHeight)]);
1625 fillShapes.push(path.slice(0));
1628 // if there's only a single point in this path, then we want to display it
1629 // as a vertical line which means we keep path[0] as is
1630 if (path.length > 2) {
1631 // else we want the first value
1632 path[0] = [path[0][0], path[1][1]];
1634 lineShapes.push(path);
1638 // draw the fill first, then optionally the normal range, then the line on top of that
1639 plen = fillShapes.length;
1640 for (i = 0; i < plen; i++) {
1641 target.drawShape(fillShapes[i],
1642 options.get('fillColor'), options.get('fillColor')).append();
1645 if (options.get('normalRangeMin') !== undefined && options.get('drawNormalOnTop')) {
1646 this.drawNormalRange(canvasLeft, canvasTop, canvasHeight, canvasWidth, rangey);
1649 plen = lineShapes.length;
1650 for (i = 0; i < plen; i++) {
1651 target.drawShape(lineShapes[i], options.get('lineColor'), undefined,
1652 options.get('lineWidth')).append();
1655 if (spotRadius && options.get('valueSpots')) {
1656 valueSpots = options.get('valueSpots');
1657 if (valueSpots.get === undefined) {
1658 valueSpots = new RangeMap(valueSpots);
1660 for (i = 0; i < yvalcount; i++) {
1661 color = valueSpots.get(yvalues[i]);
1663 target.drawCircle(canvasLeft + Math.round((xvalues[i] - this.minx) * (canvasWidth / rangex)),
1664 canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[i] - this.miny) / rangey))),
1665 spotRadius, undefined,
1671 if (spotRadius && options.get('spotColor') && yvalues[yvallast] !== null) {
1672 target.drawCircle(canvasLeft + Math.round((xvalues[xvalues.length - 1] - this.minx) * (canvasWidth / rangex)),
1673 canvasTop + Math.round(canvasHeight - (canvasHeight * ((yvalues[yvallast] - this.miny) / rangey))),
1674 spotRadius, undefined,
1675 options.get('spotColor')).append();
1677 if (this.maxy !== this.minyorg) {
1678 if (spotRadius && options.get('minSpotColor')) {
1679 x = xvalues[$.inArray(this.minyorg, yvalues)];
1680 target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)),
1681 canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.minyorg - this.miny) / rangey))),
1682 spotRadius, undefined,
1683 options.get('minSpotColor')).append();
1685 if (spotRadius && options.get('maxSpotColor')) {
1686 x = xvalues[$.inArray(this.maxyorg, yvalues)];
1687 target.drawCircle(canvasLeft + Math.round((x - this.minx) * (canvasWidth / rangex)),
1688 canvasTop + Math.round(canvasHeight - (canvasHeight * ((this.maxyorg - this.miny) / rangey))),
1689 spotRadius, undefined,
1690 options.get('maxSpotColor')).append();
1694 this.lastShapeId = target.getLastShapeId();
1695 this.canvasTop = canvasTop;
1703 $.fn.sparkline.bar = bar = createClass($.fn.sparkline._base, barHighlightMixin, {
1706 init: function (el, values, options, width, height) {
1707 var barWidth = parseInt(options.get('barWidth'), 10),
1708 barSpacing = parseInt(options.get('barSpacing'), 10),
1709 chartRangeMin = options.get('chartRangeMin'),
1710 chartRangeMax = options.get('chartRangeMax'),
1711 chartRangeClip = options.get('chartRangeClip'),
1712 stackMin = Infinity,
1713 stackMax = -Infinity,
1714 isStackString, groupMin, groupMax, stackRanges,
1715 numValues, i, vlen, range, zeroAxis, xaxisOffset, min, max, clipMin, clipMax,
1716 stacked, vlist, j, slen, svals, val, yoffset, yMaxCalc, canvasHeightEf;
1717 bar._super.init.call(this, el, values, options, width, height);
1719 // scan values to determine whether to stack bars
1720 for (i = 0, vlen = values.length; i < vlen; i++) {
1722 isStackString = typeof(val) === 'string' && val.indexOf(':') > -1;
1723 if (isStackString || $.isArray(val)) {
1725 if (isStackString) {
1726 val = values[i] = normalizeValues(val.split(':'));
1728 val = remove(val, null); // min/max will treat null as zero
1729 groupMin = Math.min.apply(Math, val);
1730 groupMax = Math.max.apply(Math, val);
1731 if (groupMin < stackMin) {
1732 stackMin = groupMin;
1734 if (groupMax > stackMax) {
1735 stackMax = groupMax;
1740 this.stacked = stacked;
1741 this.regionShapes = {};
1742 this.barWidth = barWidth;
1743 this.barSpacing = barSpacing;
1744 this.totalBarWidth = barWidth + barSpacing;
1745 this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing);
1749 if (chartRangeClip) {
1750 clipMin = chartRangeMin === undefined ? -Infinity : chartRangeMin;
1751 clipMax = chartRangeMax === undefined ? Infinity : chartRangeMax;
1755 stackRanges = stacked ? [] : numValues;
1756 var stackTotals = [];
1757 var stackRangesNeg = [];
1758 for (i = 0, vlen = values.length; i < vlen; i++) {
1761 values[i] = svals = [];
1763 stackRanges[i] = stackRangesNeg[i] = 0;
1764 for (j = 0, slen = vlist.length; j < slen; j++) {
1765 val = svals[j] = chartRangeClip ? clipval(vlist[j], clipMin, clipMax) : vlist[j];
1768 stackTotals[i] += val;
1770 if (stackMin < 0 && stackMax > 0) {
1772 stackRangesNeg[i] += Math.abs(val);
1774 stackRanges[i] += val;
1777 stackRanges[i] += Math.abs(val - (val < 0 ? stackMax : stackMin));
1779 numValues.push(val);
1783 val = chartRangeClip ? clipval(values[i], clipMin, clipMax) : values[i];
1784 val = values[i] = normalizeValue(val);
1786 numValues.push(val);
1790 this.max = max = Math.max.apply(Math, numValues);
1791 this.min = min = Math.min.apply(Math, numValues);
1792 this.stackMax = stackMax = stacked ? Math.max.apply(Math, stackTotals) : max;
1793 this.stackMin = stackMin = stacked ? Math.min.apply(Math, numValues) : min;
1795 if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < min)) {
1796 min = options.get('chartRangeMin');
1798 if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > max)) {
1799 max = options.get('chartRangeMax');
1802 this.zeroAxis = zeroAxis = options.get('zeroAxis', true);
1803 if (min <= 0 && max >= 0 && zeroAxis) {
1805 } else if (zeroAxis == false) {
1807 } else if (min > 0) {
1812 this.xaxisOffset = xaxisOffset;
1814 range = stacked ? (Math.max.apply(Math, stackRanges) + Math.max.apply(Math, stackRangesNeg)) : max - min;
1816 // as we plot zero/min values a single pixel line, we add a pixel to all other
1817 // values - Reduce the effective canvas size to suit
1818 this.canvasHeightEf = (zeroAxis && min < 0) ? this.canvasHeight - 2 : this.canvasHeight - 1;
1820 if (min < xaxisOffset) {
1821 yMaxCalc = (stacked && max >= 0) ? stackMax : max;
1822 yoffset = (yMaxCalc - xaxisOffset) / range * this.canvasHeight;
1823 if (yoffset !== Math.ceil(yoffset)) {
1824 this.canvasHeightEf -= 2;
1825 yoffset = Math.ceil(yoffset);
1828 yoffset = this.canvasHeight;
1830 this.yoffset = yoffset;
1832 if ($.isArray(options.get('colorMap'))) {
1833 this.colorMapByIndex = options.get('colorMap');
1834 this.colorMapByValue = null;
1836 this.colorMapByIndex = null;
1837 this.colorMapByValue = options.get('colorMap');
1838 if (this.colorMapByValue && this.colorMapByValue.get === undefined) {
1839 this.colorMapByValue = new RangeMap(this.colorMapByValue);
1846 getRegion: function (el, x, y) {
1847 var result = Math.floor(x / this.totalBarWidth);
1848 return (result < 0 || result >= this.values.length) ? undefined : result;
1851 getCurrentRegionFields: function () {
1852 var currentRegion = this.currentRegion,
1853 values = ensureArray(this.values[currentRegion]),
1856 for (i = values.length; i--;) {
1859 isNull: value === null,
1861 color: this.calcColor(i, value, currentRegion),
1862 offset: currentRegion
1868 calcColor: function (stacknum, value, valuenum) {
1869 var colorMapByIndex = this.colorMapByIndex,
1870 colorMapByValue = this.colorMapByValue,
1871 options = this.options,
1874 color = options.get('stackedBarColor');
1876 color = (value < 0) ? options.get('negBarColor') : options.get('barColor');
1878 if (value === 0 && options.get('zeroColor') !== undefined) {
1879 color = options.get('zeroColor');
1881 if (colorMapByValue && (newColor = colorMapByValue.get(value))) {
1883 } else if (colorMapByIndex && colorMapByIndex.length > valuenum) {
1884 color = colorMapByIndex[valuenum];
1886 return $.isArray(color) ? color[stacknum % color.length] : color;
1890 * Render bar(s) for a region
1892 renderRegion: function (valuenum, highlight) {
1893 var vals = this.values[valuenum],
1894 options = this.options,
1895 xaxisOffset = this.xaxisOffset,
1898 stacked = this.stacked,
1899 target = this.target,
1900 x = valuenum * this.totalBarWidth,
1901 canvasHeightEf = this.canvasHeightEf,
1902 yoffset = this.yoffset,
1903 y, height, color, isNull, yoffsetNeg, i, valcount, val, minPlotted, allMin;
1905 vals = $.isArray(vals) ? vals : [vals];
1906 valcount = vals.length;
1908 isNull = all(null, vals);
1909 allMin = all(xaxisOffset, vals, true);
1912 if (options.get('nullColor')) {
1913 color = highlight ? options.get('nullColor') : this.calcHighlightColor(options.get('nullColor'), options);
1914 y = (yoffset > 0) ? yoffset - 1 : yoffset;
1915 return target.drawRect(x, y, this.barWidth - 1, 0, color, color);
1920 yoffsetNeg = yoffset;
1921 for (i = 0; i < valcount; i++) {
1924 if (stacked && val === xaxisOffset) {
1925 if (!allMin || minPlotted) {
1932 height = Math.floor(canvasHeightEf * ((Math.abs(val - xaxisOffset) / range))) + 1;
1936 if (val < xaxisOffset || (val === xaxisOffset && yoffset === 0)) {
1938 yoffsetNeg += height;
1940 y = yoffset - height;
1943 color = this.calcColor(i, val, valuenum);
1945 color = this.calcHighlightColor(color, options);
1947 result.push(target.drawRect(x, y, this.barWidth - 1, height - 1, color, color));
1949 if (result.length === 1) {
1959 $.fn.sparkline.tristate = tristate = createClass($.fn.sparkline._base, barHighlightMixin, {
1962 init: function (el, values, options, width, height) {
1963 var barWidth = parseInt(options.get('barWidth'), 10),
1964 barSpacing = parseInt(options.get('barSpacing'), 10);
1965 tristate._super.init.call(this, el, values, options, width, height);
1967 this.regionShapes = {};
1968 this.barWidth = barWidth;
1969 this.barSpacing = barSpacing;
1970 this.totalBarWidth = barWidth + barSpacing;
1971 this.values = $.map(values, Number);
1972 this.width = width = (values.length * barWidth) + ((values.length - 1) * barSpacing);
1974 if ($.isArray(options.get('colorMap'))) {
1975 this.colorMapByIndex = options.get('colorMap');
1976 this.colorMapByValue = null;
1978 this.colorMapByIndex = null;
1979 this.colorMapByValue = options.get('colorMap');
1980 if (this.colorMapByValue && this.colorMapByValue.get === undefined) {
1981 this.colorMapByValue = new RangeMap(this.colorMapByValue);
1987 getRegion: function (el, x, y) {
1988 return Math.floor(x / this.totalBarWidth);
1991 getCurrentRegionFields: function () {
1992 var currentRegion = this.currentRegion;
1994 isNull: this.values[currentRegion] === undefined,
1995 value: this.values[currentRegion],
1996 color: this.calcColor(this.values[currentRegion], currentRegion),
1997 offset: currentRegion
2001 calcColor: function (value, valuenum) {
2002 var values = this.values,
2003 options = this.options,
2004 colorMapByIndex = this.colorMapByIndex,
2005 colorMapByValue = this.colorMapByValue,
2008 if (colorMapByValue && (newColor = colorMapByValue.get(value))) {
2010 } else if (colorMapByIndex && colorMapByIndex.length > valuenum) {
2011 color = colorMapByIndex[valuenum];
2012 } else if (values[valuenum] < 0) {
2013 color = options.get('negBarColor');
2014 } else if (values[valuenum] > 0) {
2015 color = options.get('posBarColor');
2017 color = options.get('zeroBarColor');
2022 renderRegion: function (valuenum, highlight) {
2023 var values = this.values,
2024 options = this.options,
2025 target = this.target,
2026 canvasHeight, height, halfHeight,
2029 canvasHeight = target.pixelHeight;
2030 halfHeight = Math.round(canvasHeight / 2);
2032 x = valuenum * this.totalBarWidth;
2033 if (values[valuenum] < 0) {
2035 height = halfHeight - 1;
2036 } else if (values[valuenum] > 0) {
2038 height = halfHeight - 1;
2043 color = this.calcColor(values[valuenum], valuenum);
2044 if (color === null) {
2048 color = this.calcHighlightColor(color, options);
2050 return target.drawRect(x, y, this.barWidth - 1, height - 1, color, color);
2057 $.fn.sparkline.discrete = discrete = createClass($.fn.sparkline._base, barHighlightMixin, {
2060 init: function (el, values, options, width, height) {
2061 discrete._super.init.call(this, el, values, options, width, height);
2063 this.regionShapes = {};
2064 this.values = values = $.map(values, Number);
2065 this.min = Math.min.apply(Math, values);
2066 this.max = Math.max.apply(Math, values);
2067 this.range = this.max - this.min;
2068 this.width = width = options.get('width') === 'auto' ? values.length * 2 : this.width;
2069 this.interval = Math.floor(width / values.length);
2070 this.itemWidth = width / values.length;
2071 if (options.get('chartRangeMin') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMin') < this.min)) {
2072 this.min = options.get('chartRangeMin');
2074 if (options.get('chartRangeMax') !== undefined && (options.get('chartRangeClip') || options.get('chartRangeMax') > this.max)) {
2075 this.max = options.get('chartRangeMax');
2079 this.lineHeight = options.get('lineHeight') === 'auto' ? Math.round(this.canvasHeight * 0.3) : options.get('lineHeight');
2083 getRegion: function (el, x, y) {
2084 return Math.floor(x / this.itemWidth);
2087 getCurrentRegionFields: function () {
2088 var currentRegion = this.currentRegion;
2090 isNull: this.values[currentRegion] === undefined,
2091 value: this.values[currentRegion],
2092 offset: currentRegion
2096 renderRegion: function (valuenum, highlight) {
2097 var values = this.values,
2098 options = this.options,
2102 interval = this.interval,
2103 target = this.target,
2104 canvasHeight = this.canvasHeight,
2105 lineHeight = this.lineHeight,
2106 pheight = canvasHeight - lineHeight,
2107 ytop, val, color, x;
2109 val = clipval(values[valuenum], min, max);
2110 x = valuenum * interval;
2111 ytop = Math.round(pheight - pheight * ((val - min) / range));
2112 color = (options.get('thresholdColor') && val < options.get('thresholdValue')) ? options.get('thresholdColor') : options.get('lineColor');
2114 color = this.calcHighlightColor(color, options);
2116 return target.drawLine(x, ytop, x, ytop + lineHeight, color);
2123 $.fn.sparkline.bullet = bullet = createClass($.fn.sparkline._base, {
2126 init: function (el, values, options, width, height) {
2128 bullet._super.init.call(this, el, values, options, width, height);
2130 // values: target, performance, range1, range2, range3
2131 this.values = values = normalizeValues(values);
2132 // target or performance could be null
2133 vals = values.slice();
2134 vals[0] = vals[0] === null ? vals[2] : vals[0];
2135 vals[1] = values[1] === null ? vals[2] : vals[1];
2136 min = Math.min.apply(Math, values);
2137 max = Math.max.apply(Math, values);
2138 if (options.get('base') === undefined) {
2139 min = min < 0 ? min : 0;
2141 min = options.get('base');
2145 this.range = max - min;
2147 this.valueShapes = {};
2148 this.regiondata = {};
2149 this.width = width = options.get('width') === 'auto' ? '4.0em' : width;
2150 this.target = this.$el.simpledraw(width, height, options.get('composite'));
2151 if (!values.length) {
2152 this.disabled = true;
2157 getRegion: function (el, x, y) {
2158 var shapeid = this.target.getShapeAt(el, x, y);
2159 return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined;
2162 getCurrentRegionFields: function () {
2163 var currentRegion = this.currentRegion;
2165 fieldkey: currentRegion.substr(0, 1),
2166 value: this.values[currentRegion.substr(1)],
2167 region: currentRegion
2171 changeHighlight: function (highlight) {
2172 var currentRegion = this.currentRegion,
2173 shapeid = this.valueShapes[currentRegion],
2175 delete this.shapes[shapeid];
2176 switch (currentRegion.substr(0, 1)) {
2178 shape = this.renderRange(currentRegion.substr(1), highlight);
2181 shape = this.renderPerformance(highlight);
2184 shape = this.renderTarget(highlight);
2187 this.valueShapes[currentRegion] = shape.id;
2188 this.shapes[shape.id] = currentRegion;
2189 this.target.replaceWithShape(shapeid, shape);
2192 renderRange: function (rn, highlight) {
2193 var rangeval = this.values[rn],
2194 rangewidth = Math.round(this.canvasWidth * ((rangeval - this.min) / this.range)),
2195 color = this.options.get('rangeColors')[rn - 2];
2197 color = this.calcHighlightColor(color, this.options);
2199 return this.target.drawRect(0, 0, rangewidth - 1, this.canvasHeight - 1, color, color);
2202 renderPerformance: function (highlight) {
2203 var perfval = this.values[1],
2204 perfwidth = Math.round(this.canvasWidth * ((perfval - this.min) / this.range)),
2205 color = this.options.get('performanceColor');
2207 color = this.calcHighlightColor(color, this.options);
2209 return this.target.drawRect(0, Math.round(this.canvasHeight * 0.3), perfwidth - 1,
2210 Math.round(this.canvasHeight * 0.4) - 1, color, color);
2213 renderTarget: function (highlight) {
2214 var targetval = this.values[0],
2215 x = Math.round(this.canvasWidth * ((targetval - this.min) / this.range) - (this.options.get('targetWidth') / 2)),
2216 targettop = Math.round(this.canvasHeight * 0.10),
2217 targetheight = this.canvasHeight - (targettop * 2),
2218 color = this.options.get('targetColor');
2220 color = this.calcHighlightColor(color, this.options);
2222 return this.target.drawRect(x, targettop, this.options.get('targetWidth') - 1, targetheight - 1, color, color);
2225 render: function () {
2226 var vlen = this.values.length,
2227 target = this.target,
2229 if (!bullet._super.render.call(this)) {
2232 for (i = 2; i < vlen; i++) {
2233 shape = this.renderRange(i).append();
2234 this.shapes[shape.id] = 'r' + i;
2235 this.valueShapes['r' + i] = shape.id;
2237 if (this.values[1] !== null) {
2238 shape = this.renderPerformance().append();
2239 this.shapes[shape.id] = 'p1';
2240 this.valueShapes.p1 = shape.id;
2242 if (this.values[0] !== null) {
2243 shape = this.renderTarget().append();
2244 this.shapes[shape.id] = 't0';
2245 this.valueShapes.t0 = shape.id;
2254 $.fn.sparkline.pie = pie = createClass($.fn.sparkline._base, {
2257 init: function (el, values, options, width, height) {
2260 pie._super.init.call(this, el, values, options, width, height);
2262 this.shapes = {}; // map shape ids to value offsets
2263 this.valueShapes = {}; // maps value offsets to shape ids
2264 this.values = values = $.map(values, Number);
2266 if (options.get('width') === 'auto') {
2267 this.width = this.height;
2270 if (values.length > 0) {
2271 for (i = values.length; i--;) {
2277 this.radius = Math.floor(Math.min(this.canvasWidth, this.canvasHeight) / 2);
2280 getRegion: function (el, x, y) {
2281 var shapeid = this.target.getShapeAt(el, x, y);
2282 return (shapeid !== undefined && this.shapes[shapeid] !== undefined) ? this.shapes[shapeid] : undefined;
2285 getCurrentRegionFields: function () {
2286 var currentRegion = this.currentRegion;
2288 isNull: this.values[currentRegion] === undefined,
2289 value: this.values[currentRegion],
2290 percent: this.values[currentRegion] / this.total * 100,
2291 color: this.options.get('sliceColors')[currentRegion % this.options.get('sliceColors').length],
2292 offset: currentRegion
2296 changeHighlight: function (highlight) {
2297 var currentRegion = this.currentRegion,
2298 newslice = this.renderSlice(currentRegion, highlight),
2299 shapeid = this.valueShapes[currentRegion];
2300 delete this.shapes[shapeid];
2301 this.target.replaceWithShape(shapeid, newslice);
2302 this.valueShapes[currentRegion] = newslice.id;
2303 this.shapes[newslice.id] = currentRegion;
2306 renderSlice: function (valuenum, highlight) {
2307 var target = this.target,
2308 options = this.options,
2309 radius = this.radius,
2310 borderWidth = options.get('borderWidth'),
2311 offset = options.get('offset'),
2312 circle = 2 * Math.PI,
2313 values = this.values,
2315 next = offset ? (2*Math.PI)*(offset/360) : 0,
2316 start, end, i, vlen, color;
2318 vlen = values.length;
2319 for (i = 0; i < vlen; i++) {
2322 if (total > 0) { // avoid divide by zero
2323 end = next + (circle * (values[i] / total));
2325 if (valuenum === i) {
2326 color = options.get('sliceColors')[i % options.get('sliceColors').length];
2328 color = this.calcHighlightColor(color, options);
2331 return target.drawPieSlice(radius, radius, radius - borderWidth, start, end, undefined, color);
2337 render: function () {
2338 var target = this.target,
2339 values = this.values,
2340 options = this.options,
2341 radius = this.radius,
2342 borderWidth = options.get('borderWidth'),
2345 if (!pie._super.render.call(this)) {
2349 target.drawCircle(radius, radius, Math.floor(radius - (borderWidth / 2)),
2350 options.get('borderColor'), undefined, borderWidth).append();
2352 for (i = values.length; i--;) {
2353 if (values[i]) { // don't render zero values
2354 shape = this.renderSlice(i).append();
2355 this.valueShapes[i] = shape.id; // store just the shapeid
2356 this.shapes[shape.id] = i;
2366 $.fn.sparkline.box = box = createClass($.fn.sparkline._base, {
2369 init: function (el, values, options, width, height) {
2370 box._super.init.call(this, el, values, options, width, height);
2371 this.values = $.map(values, Number);
2372 this.width = options.get('width') === 'auto' ? '4.0em' : width;
2374 if (!this.values.length) {
2380 * Simulate a single region
2382 getRegion: function () {
2386 getCurrentRegionFields: function () {
2388 { field: 'lq', value: this.quartiles[0] },
2389 { field: 'med', value: this.quartiles[1] },
2390 { field: 'uq', value: this.quartiles[2] }
2392 if (this.loutlier !== undefined) {
2393 result.push({ field: 'lo', value: this.loutlier});
2395 if (this.routlier !== undefined) {
2396 result.push({ field: 'ro', value: this.routlier});
2398 if (this.lwhisker !== undefined) {
2399 result.push({ field: 'lw', value: this.lwhisker});
2401 if (this.rwhisker !== undefined) {
2402 result.push({ field: 'rw', value: this.rwhisker});
2407 render: function () {
2408 var target = this.target,
2409 values = this.values,
2410 vlen = values.length,
2411 options = this.options,
2412 canvasWidth = this.canvasWidth,
2413 canvasHeight = this.canvasHeight,
2414 minValue = options.get('chartRangeMin') === undefined ? Math.min.apply(Math, values) : options.get('chartRangeMin'),
2415 maxValue = options.get('chartRangeMax') === undefined ? Math.max.apply(Math, values) : options.get('chartRangeMax'),
2417 lwhisker, loutlier, iqr, q1, q2, q3, rwhisker, routlier, i,
2420 if (!box._super.render.call(this)) {
2424 if (options.get('raw')) {
2425 if (options.get('showOutliers') && values.length > 5) {
2426 loutlier = values[0];
2427 lwhisker = values[1];
2431 rwhisker = values[5];
2432 routlier = values[6];
2434 lwhisker = values[0];
2438 rwhisker = values[4];
2441 values.sort(function (a, b) { return a - b; });
2442 q1 = quartile(values, 1);
2443 q2 = quartile(values, 2);
2444 q3 = quartile(values, 3);
2446 if (options.get('showOutliers')) {
2447 lwhisker = rwhisker = undefined;
2448 for (i = 0; i < vlen; i++) {
2449 if (lwhisker === undefined && values[i] > q1 - (iqr * options.get('outlierIQR'))) {
2450 lwhisker = values[i];
2452 if (values[i] < q3 + (iqr * options.get('outlierIQR'))) {
2453 rwhisker = values[i];
2456 loutlier = values[0];
2457 routlier = values[vlen - 1];
2459 lwhisker = values[0];
2460 rwhisker = values[vlen - 1];
2463 this.quartiles = [q1, q2, q3];
2464 this.lwhisker = lwhisker;
2465 this.rwhisker = rwhisker;
2466 this.loutlier = loutlier;
2467 this.routlier = routlier;
2469 unitSize = canvasWidth / (maxValue - minValue + 1);
2470 if (options.get('showOutliers')) {
2471 canvasLeft = Math.ceil(options.get('spotRadius'));
2472 canvasWidth -= 2 * Math.ceil(options.get('spotRadius'));
2473 unitSize = canvasWidth / (maxValue - minValue + 1);
2474 if (loutlier < lwhisker) {
2475 target.drawCircle((loutlier - minValue) * unitSize + canvasLeft,
2477 options.get('spotRadius'),
2478 options.get('outlierLineColor'),
2479 options.get('outlierFillColor')).append();
2481 if (routlier > rwhisker) {
2482 target.drawCircle((routlier - minValue) * unitSize + canvasLeft,
2484 options.get('spotRadius'),
2485 options.get('outlierLineColor'),
2486 options.get('outlierFillColor')).append();
2492 Math.round((q1 - minValue) * unitSize + canvasLeft),
2493 Math.round(canvasHeight * 0.1),
2494 Math.round((q3 - q1) * unitSize),
2495 Math.round(canvasHeight * 0.8),
2496 options.get('boxLineColor'),
2497 options.get('boxFillColor')).append();
2500 Math.round((lwhisker - minValue) * unitSize + canvasLeft),
2501 Math.round(canvasHeight / 2),
2502 Math.round((q1 - minValue) * unitSize + canvasLeft),
2503 Math.round(canvasHeight / 2),
2504 options.get('lineColor')).append();
2506 Math.round((lwhisker - minValue) * unitSize + canvasLeft),
2507 Math.round(canvasHeight / 4),
2508 Math.round((lwhisker - minValue) * unitSize + canvasLeft),
2509 Math.round(canvasHeight - canvasHeight / 4),
2510 options.get('whiskerColor')).append();
2512 target.drawLine(Math.round((rwhisker - minValue) * unitSize + canvasLeft),
2513 Math.round(canvasHeight / 2),
2514 Math.round((q3 - minValue) * unitSize + canvasLeft),
2515 Math.round(canvasHeight / 2),
2516 options.get('lineColor')).append();
2518 Math.round((rwhisker - minValue) * unitSize + canvasLeft),
2519 Math.round(canvasHeight / 4),
2520 Math.round((rwhisker - minValue) * unitSize + canvasLeft),
2521 Math.round(canvasHeight - canvasHeight / 4),
2522 options.get('whiskerColor')).append();
2525 Math.round((q2 - minValue) * unitSize + canvasLeft),
2526 Math.round(canvasHeight * 0.1),
2527 Math.round((q2 - minValue) * unitSize + canvasLeft),
2528 Math.round(canvasHeight * 0.9),
2529 options.get('medianColor')).append();
2530 if (options.get('target')) {
2531 size = Math.ceil(options.get('spotRadius'));
2533 Math.round((options.get('target') - minValue) * unitSize + canvasLeft),
2534 Math.round((canvasHeight / 2) - size),
2535 Math.round((options.get('target') - minValue) * unitSize + canvasLeft),
2536 Math.round((canvasHeight / 2) + size),
2537 options.get('targetColor')).append();
2539 Math.round((options.get('target') - minValue) * unitSize + canvasLeft - size),
2540 Math.round(canvasHeight / 2),
2541 Math.round((options.get('target') - minValue) * unitSize + canvasLeft + size),
2542 Math.round(canvasHeight / 2),
2543 options.get('targetColor')).append();
2549 // Setup a very simple "virtual canvas" to make drawing the few shapes we need easier
2550 // This is accessible as $(foo).simpledraw()
2552 VShape = createClass({
2553 init: function (target, id, type, args) {
2554 this.target = target;
2559 append: function () {
2560 this.target.appendShape(this);
2565 VCanvas_base = createClass({
2566 _pxregex: /(\d+)(px)?\s*$/i,
2568 init: function (width, height, target) {
2573 this.height = height;
2574 this.target = target;
2575 this.lastShapeId = null;
2579 $.data(target, '_jqs_vcanvas', this);
2582 drawLine: function (x1, y1, x2, y2, lineColor, lineWidth) {
2583 return this.drawShape([[x1, y1], [x2, y2]], lineColor, lineWidth);
2586 drawShape: function (path, lineColor, fillColor, lineWidth) {
2587 return this._genShape('Shape', [path, lineColor, fillColor, lineWidth]);
2590 drawCircle: function (x, y, radius, lineColor, fillColor, lineWidth) {
2591 return this._genShape('Circle', [x, y, radius, lineColor, fillColor, lineWidth]);
2594 drawPieSlice: function (x, y, radius, startAngle, endAngle, lineColor, fillColor) {
2595 return this._genShape('PieSlice', [x, y, radius, startAngle, endAngle, lineColor, fillColor]);
2598 drawRect: function (x, y, width, height, lineColor, fillColor) {
2599 return this._genShape('Rect', [x, y, width, height, lineColor, fillColor]);
2602 getElement: function () {
2607 * Return the most recently inserted shape id
2609 getLastShapeId: function () {
2610 return this.lastShapeId;
2614 * Clear and reset the canvas
2616 reset: function () {
2617 alert('reset not implemented');
2620 _insert: function (el, target) {
2625 * Calculate the pixel dimensions of the canvas
2627 _calculatePixelDims: function (width, height, canvas) {
2628 // XXX This should probably be a configurable option
2630 match = this._pxregex.exec(height);
2632 this.pixelHeight = match[1];
2634 this.pixelHeight = $(canvas).height();
2636 match = this._pxregex.exec(width);
2638 this.pixelWidth = match[1];
2640 this.pixelWidth = $(canvas).width();
2645 * Generate a shape object and id for later rendering
2647 _genShape: function (shapetype, shapeargs) {
2648 var id = shapeCount++;
2649 shapeargs.unshift(id);
2650 return new VShape(this, id, shapetype, shapeargs);
2654 * Add a shape to the end of the render queue
2656 appendShape: function (shape) {
2657 alert('appendShape not implemented');
2661 * Replace one shape with another
2663 replaceWithShape: function (shapeid, shape) {
2664 alert('replaceWithShape not implemented');
2668 * Insert one shape after another in the render queue
2670 insertAfterShape: function (shapeid, shape) {
2671 alert('insertAfterShape not implemented');
2675 * Remove a shape from the queue
2677 removeShapeId: function (shapeid) {
2678 alert('removeShapeId not implemented');
2682 * Find a shape at the specified x/y co-ordinates
2684 getShapeAt: function (el, x, y) {
2685 alert('getShapeAt not implemented');
2689 * Render all queued shapes onto the canvas
2691 render: function () {
2692 alert('render not implemented');
2696 VCanvas_canvas = createClass(VCanvas_base, {
2697 init: function (width, height, target, interact) {
2698 VCanvas_canvas._super.init.call(this, width, height, target);
2699 this.canvas = document.createElement('canvas');
2703 $.data(target, '_jqs_vcanvas', this);
2704 $(this.canvas).css({ display: 'inline-block', width: width, height: height, verticalAlign: 'top' });
2705 this._insert(this.canvas, target);
2706 this._calculatePixelDims(width, height, this.canvas);
2707 this.canvas.width = this.pixelWidth;
2708 this.canvas.height = this.pixelHeight;
2709 this.interact = interact;
2712 this.currentTargetShapeId = undefined;
2713 $(this.canvas).css({width: this.pixelWidth, height: this.pixelHeight});
2716 _getContext: function (lineColor, fillColor, lineWidth) {
2717 var context = this.canvas.getContext('2d');
2718 if (lineColor !== undefined) {
2719 context.strokeStyle = lineColor;
2721 context.lineWidth = lineWidth === undefined ? 1 : lineWidth;
2722 if (fillColor !== undefined) {
2723 context.fillStyle = fillColor;
2728 reset: function () {
2729 var context = this._getContext();
2730 context.clearRect(0, 0, this.pixelWidth, this.pixelHeight);
2733 this.currentTargetShapeId = undefined;
2736 _drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) {
2737 var context = this._getContext(lineColor, fillColor, lineWidth),
2739 context.beginPath();
2740 context.moveTo(path[0][0] + 0.5, path[0][1] + 0.5);
2741 for (i = 1, plen = path.length; i < plen; i++) {
2742 context.lineTo(path[i][0] + 0.5, path[i][1] + 0.5); // the 0.5 offset gives us crisp pixel-width lines
2744 if (lineColor !== undefined) {
2747 if (fillColor !== undefined) {
2750 if (this.targetX !== undefined && this.targetY !== undefined &&
2751 context.isPointInPath(this.targetX, this.targetY)) {
2752 this.currentTargetShapeId = shapeid;
2756 _drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) {
2757 var context = this._getContext(lineColor, fillColor, lineWidth);
2758 context.beginPath();
2759 context.arc(x, y, radius, 0, 2 * Math.PI, false);
2760 if (this.targetX !== undefined && this.targetY !== undefined &&
2761 context.isPointInPath(this.targetX, this.targetY)) {
2762 this.currentTargetShapeId = shapeid;
2764 if (lineColor !== undefined) {
2767 if (fillColor !== undefined) {
2772 _drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) {
2773 var context = this._getContext(lineColor, fillColor);
2774 context.beginPath();
2775 context.moveTo(x, y);
2776 context.arc(x, y, radius, startAngle, endAngle, false);
2777 context.lineTo(x, y);
2778 context.closePath();
2779 if (lineColor !== undefined) {
2785 if (this.targetX !== undefined && this.targetY !== undefined &&
2786 context.isPointInPath(this.targetX, this.targetY)) {
2787 this.currentTargetShapeId = shapeid;
2791 _drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) {
2792 return this._drawShape(shapeid, [[x, y], [x + width, y], [x + width, y + height], [x, y + height], [x, y]], lineColor, fillColor);
2795 appendShape: function (shape) {
2796 this.shapes[shape.id] = shape;
2797 this.shapeseq.push(shape.id);
2798 this.lastShapeId = shape.id;
2802 replaceWithShape: function (shapeid, shape) {
2803 var shapeseq = this.shapeseq,
2805 this.shapes[shape.id] = shape;
2806 for (i = shapeseq.length; i--;) {
2807 if (shapeseq[i] == shapeid) {
2808 shapeseq[i] = shape.id;
2811 delete this.shapes[shapeid];
2814 replaceWithShapes: function (shapeids, shapes) {
2815 var shapeseq = this.shapeseq,
2819 for (i = shapeids.length; i--;) {
2820 shapemap[shapeids[i]] = true;
2822 for (i = shapeseq.length; i--;) {
2824 if (shapemap[sid]) {
2825 shapeseq.splice(i, 1);
2826 delete this.shapes[sid];
2830 for (i = shapes.length; i--;) {
2831 shapeseq.splice(first, 0, shapes[i].id);
2832 this.shapes[shapes[i].id] = shapes[i];
2837 insertAfterShape: function (shapeid, shape) {
2838 var shapeseq = this.shapeseq,
2840 for (i = shapeseq.length; i--;) {
2841 if (shapeseq[i] === shapeid) {
2842 shapeseq.splice(i + 1, 0, shape.id);
2843 this.shapes[shape.id] = shape;
2849 removeShapeId: function (shapeid) {
2850 var shapeseq = this.shapeseq,
2852 for (i = shapeseq.length; i--;) {
2853 if (shapeseq[i] === shapeid) {
2854 shapeseq.splice(i, 1);
2858 delete this.shapes[shapeid];
2861 getShapeAt: function (el, x, y) {
2865 return this.currentTargetShapeId;
2868 render: function () {
2869 var shapeseq = this.shapeseq,
2870 shapes = this.shapes,
2871 shapeCount = shapeseq.length,
2872 context = this._getContext(),
2874 context.clearRect(0, 0, this.pixelWidth, this.pixelHeight);
2875 for (i = 0; i < shapeCount; i++) {
2876 shapeid = shapeseq[i];
2877 shape = shapes[shapeid];
2878 this['_draw' + shape.type].apply(this, shape.args);
2880 if (!this.interact) {
2881 // not interactive so no need to keep the shapes array
2889 VCanvas_vml = createClass(VCanvas_base, {
2890 init: function (width, height, target) {
2892 VCanvas_vml._super.init.call(this, width, height, target);
2896 $.data(target, '_jqs_vcanvas', this);
2897 this.canvas = document.createElement('span');
2898 $(this.canvas).css({ display: 'inline-block', position: 'relative', overflow: 'hidden', width: width, height: height, margin: '0px', padding: '0px', verticalAlign: 'top'});
2899 this._insert(this.canvas, target);
2900 this._calculatePixelDims(width, height, this.canvas);
2901 this.canvas.width = this.pixelWidth;
2902 this.canvas.height = this.pixelHeight;
2903 groupel = '<v:group coordorigin="0 0" coordsize="' + this.pixelWidth + ' ' + this.pixelHeight + '"' +
2904 ' style="position:absolute;top:0;left:0;width:' + this.pixelWidth + 'px;height=' + this.pixelHeight + 'px;"></v:group>';
2905 this.canvas.insertAdjacentHTML('beforeEnd', groupel);
2906 this.group = $(this.canvas).children()[0];
2907 this.rendered = false;
2908 this.prerender = '';
2911 _drawShape: function (shapeid, path, lineColor, fillColor, lineWidth) {
2913 initial, stroke, fill, closed, vel, plen, i;
2914 for (i = 0, plen = path.length; i < plen; i++) {
2915 vpath[i] = '' + (path[i][0]) + ',' + (path[i][1]);
2917 initial = vpath.splice(0, 1);
2918 lineWidth = lineWidth === undefined ? 1 : lineWidth;
2919 stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" ';
2920 fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
2921 closed = vpath[0] === vpath[vpath.length - 1] ? 'x ' : '';
2922 vel = '<v:shape coordorigin="0 0" coordsize="' + this.pixelWidth + ' ' + this.pixelHeight + '" ' +
2923 ' id="jqsshape' + shapeid + '" ' +
2926 ' style="position:absolute;left:0px;top:0px;height:' + this.pixelHeight + 'px;width:' + this.pixelWidth + 'px;padding:0px;margin:0px;" ' +
2927 ' path="m ' + initial + ' l ' + vpath.join(', ') + ' ' + closed + 'e">' +
2932 _drawCircle: function (shapeid, x, y, radius, lineColor, fillColor, lineWidth) {
2933 var stroke, fill, vel;
2936 stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="' + lineWidth + 'px" strokeColor="' + lineColor + '" ';
2937 fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
2939 ' id="jqsshape' + shapeid + '" ' +
2942 ' style="position:absolute;top:' + y + 'px; left:' + x + 'px; width:' + (radius * 2) + 'px; height:' + (radius * 2) + 'px"></v:oval>';
2947 _drawPieSlice: function (shapeid, x, y, radius, startAngle, endAngle, lineColor, fillColor) {
2948 var vpath, startx, starty, endx, endy, stroke, fill, vel;
2949 if (startAngle === endAngle) {
2950 return ''; // VML seems to have problem when start angle equals end angle.
2952 if ((endAngle - startAngle) === (2 * Math.PI)) {
2953 startAngle = 0.0; // VML seems to have a problem when drawing a full circle that doesn't start 0
2954 endAngle = (2 * Math.PI);
2957 startx = x + Math.round(Math.cos(startAngle) * radius);
2958 starty = y + Math.round(Math.sin(startAngle) * radius);
2959 endx = x + Math.round(Math.cos(endAngle) * radius);
2960 endy = y + Math.round(Math.sin(endAngle) * radius);
2962 if (startx === endx && starty === endy) {
2963 if ((endAngle - startAngle) < Math.PI) {
2964 // Prevent very small slices from being mistaken as a whole pie
2967 // essentially going to be the entire circle, so ignore startAngle
2968 startx = endx = x + radius;
2972 if (startx === endx && starty === endy && (endAngle - startAngle) < Math.PI) {
2976 vpath = [x - radius, y - radius, x + radius, y + radius, startx, starty, endx, endy];
2977 stroke = lineColor === undefined ? ' stroked="false" ' : ' strokeWeight="1px" strokeColor="' + lineColor + '" ';
2978 fill = fillColor === undefined ? ' filled="false"' : ' fillColor="' + fillColor + '" filled="true" ';
2979 vel = '<v:shape coordorigin="0 0" coordsize="' + this.pixelWidth + ' ' + this.pixelHeight + '" ' +
2980 ' id="jqsshape' + shapeid + '" ' +
2983 ' style="position:absolute;left:0px;top:0px;height:' + this.pixelHeight + 'px;width:' + this.pixelWidth + 'px;padding:0px;margin:0px;" ' +
2984 ' path="m ' + x + ',' + y + ' wa ' + vpath.join(', ') + ' x e">' +
2989 _drawRect: function (shapeid, x, y, width, height, lineColor, fillColor) {
2990 return this._drawShape(shapeid, [[x, y], [x, y + height], [x + width, y + height], [x + width, y], [x, y]], lineColor, fillColor);
2993 reset: function () {
2994 this.group.innerHTML = '';
2997 appendShape: function (shape) {
2998 var vel = this['_draw' + shape.type].apply(this, shape.args);
2999 if (this.rendered) {
3000 this.group.insertAdjacentHTML('beforeEnd', vel);
3002 this.prerender += vel;
3004 this.lastShapeId = shape.id;
3008 replaceWithShape: function (shapeid, shape) {
3009 var existing = $('#jqsshape' + shapeid),
3010 vel = this['_draw' + shape.type].apply(this, shape.args);
3011 existing[0].outerHTML = vel;
3014 replaceWithShapes: function (shapeids, shapes) {
3015 // replace the first shapeid with all the new shapes then toast the remaining old shapes
3016 var existing = $('#jqsshape' + shapeids[0]),
3018 slen = shapes.length,
3020 for (i = 0; i < slen; i++) {
3021 replace += this['_draw' + shapes[i].type].apply(this, shapes[i].args);
3023 existing[0].outerHTML = replace;
3024 for (i = 1; i < shapeids.length; i++) {
3025 $('#jqsshape' + shapeids[i]).remove();
3029 insertAfterShape: function (shapeid, shape) {
3030 var existing = $('#jqsshape' + shapeid),
3031 vel = this['_draw' + shape.type].apply(this, shape.args);
3032 existing[0].insertAdjacentHTML('afterEnd', vel);
3035 removeShapeId: function (shapeid) {
3036 var existing = $('#jqsshape' + shapeid);
3037 this.group.removeChild(existing[0]);
3040 getShapeAt: function (el, x, y) {
3041 var shapeid = el.id.substr(8);
3045 render: function () {
3046 if (!this.rendered) {
3047 // batch the intial render into a single repaint
3048 this.group.innerHTML = this.prerender;
3049 this.rendered = true;
3054 }))}(document, Math));