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