1// ==ClosureCompiler== 2// @compilation_level SIMPLE_OPTIMIZATIONS 3 4/** 5 * @license Highcharts JS v2.2.1 (2012-03-15) 6 * 7 * (c) 2009-2011 Torstein Hønsi 8 * 9 * License: www.highcharts.com/license 10 */ 11 12// JSLint options: 13/*global Highcharts, document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $, console */ 14 15(function () { 16// encapsulated variables 17var UNDEFINED, 18 doc = document, 19 win = window, 20 math = Math, 21 mathRound = math.round, 22 mathFloor = math.floor, 23 mathCeil = math.ceil, 24 mathMax = math.max, 25 mathMin = math.min, 26 mathAbs = math.abs, 27 mathCos = math.cos, 28 mathSin = math.sin, 29 mathPI = math.PI, 30 deg2rad = mathPI * 2 / 360, 31 32 33 // some variables 34 userAgent = navigator.userAgent, 35 isIE = /msie/i.test(userAgent) && !win.opera, 36 docMode8 = doc.documentMode === 8, 37 isWebKit = /AppleWebKit/.test(userAgent), 38 isFirefox = /Firefox/.test(userAgent), 39 SVG_NS = 'http://www.w3.org/2000/svg', 40 hasSVG = !!doc.createElementNS && !!doc.createElementNS(SVG_NS, 'svg').createSVGRect, 41 hasBidiBug = isFirefox && parseInt(userAgent.split('Firefox/')[1], 10) < 4, // issue #38 42 useCanVG = !hasSVG && !isIE && !!doc.createElement('canvas').getContext, 43 Renderer, 44 hasTouch = doc.documentElement.ontouchstart !== UNDEFINED, 45 symbolSizes = {}, 46 idCounter = 0, 47 garbageBin, 48 defaultOptions, 49 dateFormat, // function 50 globalAnimation, 51 pathAnim, 52 timeUnits, 53 54 // some constants for frequently used strings 55 DIV = 'div', 56 ABSOLUTE = 'absolute', 57 RELATIVE = 'relative', 58 HIDDEN = 'hidden', 59 PREFIX = 'highcharts-', 60 VISIBLE = 'visible', 61 PX = 'px', 62 NONE = 'none', 63 M = 'M', 64 L = 'L', 65 /* 66 * Empirical lowest possible opacities for TRACKER_FILL 67 * IE6: 0.002 68 * IE7: 0.002 69 * IE8: 0.002 70 * IE9: 0.00000000001 (unlimited) 71 * FF: 0.00000000001 (unlimited) 72 * Chrome: 0.000001 73 * Safari: 0.000001 74 * Opera: 0.00000000001 (unlimited) 75 */ 76 TRACKER_FILL = 'rgba(192,192,192,' + (hasSVG ? 0.000001 : 0.002) + ')', // invisible but clickable 77 //TRACKER_FILL = 'rgba(192,192,192,0.5)', 78 NORMAL_STATE = '', 79 HOVER_STATE = 'hover', 80 SELECT_STATE = 'select', 81 MILLISECOND = 'millisecond', 82 SECOND = 'second', 83 MINUTE = 'minute', 84 HOUR = 'hour', 85 DAY = 'day', 86 WEEK = 'week', 87 MONTH = 'month', 88 YEAR = 'year', 89 90 // constants for attributes 91 FILL = 'fill', 92 LINEAR_GRADIENT = 'linearGradient', 93 STOPS = 'stops', 94 STROKE = 'stroke', 95 STROKE_WIDTH = 'stroke-width', 96 97 // time methods, changed based on whether or not UTC is used 98 makeTime, 99 getMinutes, 100 getHours, 101 getDay, 102 getDate, 103 getMonth, 104 getFullYear, 105 setMinutes, 106 setHours, 107 setDate, 108 setMonth, 109 setFullYear, 110 111 // check for a custom HighchartsAdapter defined prior to this file 112 globalAdapter = win.HighchartsAdapter, 113 adapter = globalAdapter || {}, 114 115 // Utility functions. If the HighchartsAdapter is not defined, adapter is an empty object 116 // and all the utility functions will be null. In that case they are populated by the 117 // default adapters below. 118 getScript = adapter.getScript, 119 each = adapter.each, 120 grep = adapter.grep, 121 offset = adapter.offset, 122 map = adapter.map, 123 merge = adapter.merge, 124 addEvent = adapter.addEvent, 125 removeEvent = adapter.removeEvent, 126 fireEvent = adapter.fireEvent, 127 animate = adapter.animate, 128 stop = adapter.stop, 129 130 // lookup over the types and the associated classes 131 seriesTypes = {}; 132 133// The Highcharts namespace 134win.Highcharts = {}; 135 136/** 137 * Extend an object with the members of another 138 * @param {Object} a The object to be extended 139 * @param {Object} b The object to add to the first one 140 */ 141function extend(a, b) { 142 var n; 143 if (!a) { 144 a = {}; 145 } 146 for (n in b) { 147 a[n] = b[n]; 148 } 149 return a; 150} 151 152/** 153 * Take an array and turn into a hash with even number arguments as keys and odd numbers as 154 * values. Allows creating constants for commonly used style properties, attributes etc. 155 * Avoid it in performance critical situations like looping 156 */ 157function hash() { 158 var i = 0, 159 args = arguments, 160 length = args.length, 161 obj = {}; 162 for (; i < length; i++) { 163 obj[args[i++]] = args[i]; 164 } 165 return obj; 166} 167 168/** 169 * Shortcut for parseInt 170 * @param {Object} s 171 * @param {Number} mag Magnitude 172 */ 173function pInt(s, mag) { 174 return parseInt(s, mag || 10); 175} 176 177/** 178 * Check for string 179 * @param {Object} s 180 */ 181function isString(s) { 182 return typeof s === 'string'; 183} 184 185/** 186 * Check for object 187 * @param {Object} obj 188 */ 189function isObject(obj) { 190 return typeof obj === 'object'; 191} 192 193/** 194 * Check for array 195 * @param {Object} obj 196 */ 197function isArray(obj) { 198 return Object.prototype.toString.call(obj) === '[object Array]'; 199} 200
201/** 202 * Check for number 203 * @param {Object} n 204 */ 205function isNumber(n) { 206 return typeof n === 'number'; 207} 208 209function log2lin(num) { 210 return math.log(num) / math.LN10; 211} 212function lin2log(num) { 213 return math.pow(10, num); 214} 215 216/** 217 * Remove last occurence of an item from an array 218 * @param {Array} arr 219 * @param {Mixed} item 220 */ 221function erase(arr, item) { 222 var i = arr.length; 223 while (i--) { 224 if (arr[i] === item) { 225 arr.splice(i, 1); 226 break; 227 } 228 } 229 //return arr; 230} 231 232/** 233 * Returns true if the object is not null or undefined. Like MooTools' $.defined. 234 * @param {Object} obj 235 */ 236function defined(obj) { 237 return obj !== UNDEFINED && obj !== null; 238} 239 240/** 241 * Set or get an attribute or an object of attributes. Can't use jQuery attr because 242 * it attempts to set expando properties on the SVG element, which is not allowed. 243 * 244 * @param {Object} elem The DOM element to receive the attribute(s) 245 * @param {String|Object} prop The property or an abject of key-value pairs 246 * @param {String} value The value if a single property is set 247 */ 248function attr(elem, prop, value) { 249 var key, 250 setAttribute = 'setAttribute', 251 ret; 252 253 // if the prop is a string 254 if (isString(prop)) { 255 // set the value 256 if (defined(value)) { 257 258 elem[setAttribute](prop, value); 259 260 // get the value 261 } else if (elem && elem.getAttribute) { // elem not defined when printing pie demo... 262 ret = elem.getAttribute(prop); 263 } 264 265 // else if prop is defined, it is a hash of key/value pairs 266 } else if (defined(prop) && isObject(prop)) { 267 for (key in prop) { 268 elem[setAttribute](key, prop[key]); 269 } 270 } 271 return ret; 272} 273/** 274 * Check if an element is an array, and if not, make it into an array. Like 275 * MooTools' $.splat. 276 */ 277function splat(obj) { 278 return isArray(obj) ? obj : [obj]; 279} 280 281 282/** 283 * Return the first value that is defined. Like MooTools' $.pick. 284 */ 285function pick() { 286 var args = arguments, 287 i, 288 arg, 289 length = args.length; 290 for (i = 0; i < length; i++) { 291 arg = args[i]; 292 if (typeof arg !== 'undefined' && arg !== null) { 293 return arg; 294 } 295 } 296} 297 298/** 299 * Set CSS on a given element 300 * @param {Object} el 301 * @param {Object} styles Style object with camel case property names 302 */ 303function css(el, styles) { 304 if (isIE) { 305 if (styles && styles.opacity !== UNDEFINED) { 306 styles.filter = 'alpha(opacity=' + (styles.opacity * 100) + ')'; 307 } 308 } 309 extend(el.style, styles); 310} 311 312/** 313 * Utility function to create element with attributes and styles 314 * @param {Object} tag 315 * @param {Object} attribs 316 * @param {Object} styles 317 * @param {Object} parent 318 * @param {Object} nopad 319 */ 320function createElement(tag, attribs, styles, parent, nopad) { 321 var el = doc.createElement(tag); 322 if (attribs) { 323 extend(el, attribs); 324 } 325 if (nopad) { 326 css(el, {padding: 0, border: NONE, margin: 0}); 327 } 328 if (styles) { 329 css(el, styles); 330 } 331 if (parent) { 332 parent.appendChild(el); 333 } 334 return el; 335} 336 337/** 338 * Extend a prototyped class by new members 339 * @param {Object} parent 340 * @param {Object} members 341 */ 342function extendClass(parent, members) { 343 var object = function () {}; 344 object.prototype = new parent(); 345 extend(object.prototype, members); 346 return object; 347} 348 349/** 350 * Format a number and return a string based on input settings 351 * @param {Number} number The input number to format 352 * @param {Number} decimals The amount of decimals 353 * @param {String} decPoint The decimal point, defaults to the one given in the lang options 354 * @param {String} thousandsSep The thousands separator, defaults to the one given in the lang options 355 */ 356function numberFormat(number, decimals, decPoint, thousandsSep) { 357 var lang = defaultOptions.lang, 358 // http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_number_format/ 359 n = number, 360 c = isNaN(decimals = mathAbs(decimals)) ? 2 : decimals, 361 d = decPoint === undefined ? lang.decimalPoint : decPoint, 362 t = thousandsSep === undefined ? lang.thousandsSep : thousandsSep, 363 s = n < 0 ? "-" : "", 364 i = String(pInt(n = mathAbs(+n || 0).toFixed(c))), 365 j = i.length > 3 ? i.length % 3 : 0; 366 367 return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + 368 (c ? d + mathAbs(n - i).toFixed(c).slice(2) : ""); 369} 370 371/** 372 * Pad a string to a given length by adding 0 to the beginning 373 * @param {Number} number 374 * @param {Number} length 375 */ 376function pad(number, length) { 377 // Create an array of the remaining length +1 and join it with 0's 378 return new Array((length || 2) + 1 - String(number).length).join(0) + number; 379} 380 381/** 382 * Based on http://www.php.net/manual/en/function.strftime.php 383 * @param {String} format 384 * @param {Number} timestamp 385 * @param {Boolean} capitalize 386 */ 387dateFormat = function (format, timestamp, capitalize) { 388 if (!defined(timestamp) || isNaN(timestamp)) { 389 return 'Invalid date'; 390 } 391 format = pick(format, '%Y-%m-%d %H:%M:%S'); 392 393 var date = new Date(timestamp), 394 key, // used in for constuct below 395 // get the basic time values 396 hours = date[getHours](), 397 day = date[getDay](), 398 dayOfMonth = date[getDate](), 399 month = date[getMonth](), 400 fullYear = date[getFullYear](), 401 lang = defaultOptions.lang, 402 langWeekdays = lang.weekdays, 403 /* // uncomment this and the 'W' format key below to enable week numbers 404 weekNumber = function () { 405 var clone = new Date(date.valueOf()), 406 day = clone[getDay]() == 0 ? 7 : clone[getDay](), 407 dayNumber; 408 clone.setDate(clone[getDate]() + 4 - day); 409 dayNumber = mathFloor((clone.getTime() - new Date(clone[getFullYear](), 0, 1, -6)) / 86400000); 410 return 1 + mathFloor(dayNumber / 7); 411 }, 412 */ 413 414 // list all format keys 415 replacements = { 416 417 // Day 418 'a': langWeekdays[day].substr(0, 3), // Short weekday, like 'Mon' 419 'A': langWeekdays[day], // Long weekday, like 'Monday' 420 'd': pad(dayOfMonth), // Two digit day of the month, 01 to 31 421 'e': dayOfMonth, // Day of the month, 1 through 31 422 423 // Week (none implemented) 424 //'W': weekNumber(), 425 426 // Month 427 'b': lang.shortMonths[month], // Short month, like 'Jan' 428 'B': lang.months[month], // Long month, like 'January' 429 'm': pad(month + 1), // Two digit month number, 01 through 12 430 431 // Year 432 'y': fullYear.toString().substr(2, 2), // Two digits year, like 09 for 2009 433 'Y': fullYear, // Four digits year, like 2009 434 435 // Time 436 'H': pad(hours), // Two digits hours in 24h format, 00 through 23 437 'I': pad((hours % 12) || 12), // Two digits hours in 12h format, 00 through 11 438 'l': (hours % 12) || 12, // Hours in 12h format, 1 through 12 439 'M': pad(date[getMinutes]()), // Two digits minutes, 00 through 59 440 'p': hours < 12 ? 'AM' : 'PM', // Upper case AM or PM
441 'P': hours < 12 ? 'am' : 'pm', // Lower case AM or PM 442 'S': pad(date.getSeconds()), // Two digits seconds, 00 through 59 443 'L': pad(mathRound(timestamp % 1000), 3) // Milliseconds (naming from Ruby) 444 }; 445 446 447 // do the replaces 448 for (key in replacements) { 449 format = format.replace('%' + key, replacements[key]); 450 } 451 452 // Optionally capitalize the string and return 453 return capitalize ? format.substr(0, 1).toUpperCase() + format.substr(1) : format; 454}; 455 456/** 457 * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5 458 * @param {Number} interval 459 * @param {Array} multiples 460 * @param {Number} magnitude 461 * @param {Object} options 462 */ 463function normalizeTickInterval(interval, multiples, magnitude, options) { 464 var normalized, i; 465 466 // round to a tenfold of 1, 2, 2.5 or 5 467 magnitude = pick(magnitude, 1); 468 normalized = interval / magnitude; 469 470 // multiples for a linear scale 471 if (!multiples) { 472 multiples = [1, 2, 2.5, 5, 10]; 473 474 // the allowDecimals option 475 if (options && options.allowDecimals === false) { 476 if (magnitude === 1) { 477 multiples = [1, 2, 5, 10]; 478 } else if (magnitude <= 0.1) { 479 multiples = [1 / magnitude]; 480 } 481 } 482 } 483 484 // normalize the interval to the nearest multiple 485 for (i = 0; i < multiples.length; i++) { 486 interval = multiples[i]; 487 if (normalized <= (multiples[i] + (multiples[i + 1] || multiples[i])) / 2) { 488 break; 489 } 490 } 491 492 // multiply back to the correct magnitude 493 interval *= magnitude; 494 495 return interval; 496} 497 498/** 499 * Get a normalized tick interval for dates. Returns a configuration object with 500 * unit range (interval), count and name. Used to prepare data for getTimeTicks. 501 * Previously this logic was part of getTimeTicks, but as getTimeTicks now runs 502 * of segments in stock charts, the normalizing logic was extracted in order to 503 * prevent it for running over again for each segment having the same interval. 504 * #662, #697. 505 */ 506function normalizeTimeTickInterval(tickInterval, unitsOption) { 507 var units = unitsOption || [[ 508 MILLISECOND, // unit name 509 [1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples 510 ], [ 511 SECOND, 512 [1, 2, 5, 10, 15, 30] 513 ], [ 514 MINUTE, 515 [1, 2, 5, 10, 15, 30] 516 ], [ 517 HOUR, 518 [1, 2, 3, 4, 6, 8, 12] 519 ], [ 520 DAY, 521 [1, 2] 522 ], [ 523 WEEK, 524 [1, 2] 525 ], [ 526 MONTH, 527 [1, 2, 3, 4, 6] 528 ], [ 529 YEAR, 530 null 531 ]], 532 unit = units[units.length - 1], // default unit is years 533 interval = timeUnits[unit[0]], 534 multiples = unit[1], 535 count, 536 i; 537 538 // loop through the units to find the one that best fits the tickInterval 539 for (i = 0; i < units.length; i++) { 540 unit = units[i]; 541 interval = timeUnits[unit[0]]; 542 multiples = unit[1]; 543 544 545 if (units[i + 1]) { 546 // lessThan is in the middle between the highest multiple and the next unit. 547 var lessThan = (interval * multiples[multiples.length - 1] + 548 timeUnits[units[i + 1][0]]) / 2; 549 550 // break and keep the current unit 551 if (tickInterval <= lessThan) { 552 break; 553 } 554 } 555 } 556 557 // prevent 2.5 years intervals, though 25, 250 etc. are allowed 558 if (interval === timeUnits[YEAR] && tickInterval < 5 * interval) { 559 multiples = [1, 2, 5]; 560 } 561 562 // prevent 2.5 years intervals, though 25, 250 etc. are allowed 563 if (interval === timeUnits[YEAR] && tickInterval < 5 * interval) { 564 multiples = [1, 2, 5]; 565 } 566 567 // get the count 568 count = normalizeTickInterval(tickInterval / interval, multiples); 569 570 return { 571 unitRange: interval, 572 count: count, 573 unitName: unit[0] 574 }; 575} 576 577/** 578 * Set the tick positions to a time unit that makes sense, for example 579 * on the first of each month or on every Monday. Return an array 580 * with the time positions. Used in datetime axes as well as for grouping 581 * data on a datetime axis. 582 * 583 * @param {Object} normalizedInterval The interval in axis values (ms) and the count 584 * @param {Number} min The minimum in axis values 585 * @param {Number} max The maximum in axis values 586 * @param {Number} startOfWeek 587 */ 588function getTimeTicks(normalizedInterval, min, max, startOfWeek) { 589 var tickPositions = [], 590 i, 591 higherRanks = {}, 592 useUTC = defaultOptions.global.useUTC, 593 minYear, // used in months and years as a basis for Date.UTC() 594 minDate = new Date(min), 595 interval = normalizedInterval.unitRange, 596 count = normalizedInterval.count; 597 598 599 600 if (interval >= timeUnits[SECOND]) { // second 601 minDate.setMilliseconds(0); 602 minDate.setSeconds(interval >= timeUnits[MINUTE] ? 0 : 603 count * mathFloor(minDate.getSeconds() / count)); 604 } 605 606 if (interval >= timeUnits[MINUTE]) { // minute 607 minDate[setMinutes](interval >= timeUnits[HOUR] ? 0 : 608 count * mathFloor(minDate[getMinutes]() / count)); 609 } 610 611 if (interval >= timeUnits[HOUR]) { // hour 612 minDate[setHours](interval >= timeUnits[DAY] ? 0 : 613 count * mathFloor(minDate[getHours]() / count)); 614 } 615 616 if (interval >= timeUnits[DAY]) { // day 617 minDate[setDate](interval >= timeUnits[MONTH] ? 1 : 618 count * mathFloor(minDate[getDate]() / count)); 619 } 620 621 if (interval >= timeUnits[MONTH]) { // month 622 minDate[setMonth](interval >= timeUnits[YEAR] ? 0 : 623 count * mathFloor(minDate[getMonth]() / count)); 624 minYear = minDate[getFullYear](); 625 } 626 627 if (interval >= timeUnits[YEAR]) { // year 628 minYear -= minYear % count; 629 minDate[setFullYear](minYear); 630 } 631 632 // week is a special case that runs outside the hierarchy 633 if (interval === timeUnits[WEEK]) { 634 // get start of current week, independent of count 635 minDate[setDate](minDate[getDate]() - minDate[getDay]() + 636 pick(startOfWeek, 1)); 637 } 638 639 640 // get tick positions 641 i = 1; 642 minYear = minDate[getFullYear](); 643 var time = minDate.getTime(), 644 minMonth = minDate[getMonth](), 645 minDateDate = minDate[getDate](); 646 647 // iterate and add tick positions at appropriate values 648 while (time < max) { 649 tickPositions.push(time); 650 651 // if the interval is years, use Date.UTC to increase years 652 if (interval === timeUnits[YEAR]) { 653 time = makeTime(minYear + i * count, 0); 654 655 // if the interval is months, use Date.UTC to increase months 656 } else if (interval === timeUnits[MONTH]) { 657 time = makeTime(minYear, minMonth + i * count); 658 659 // if we're using global time, the interval is not fixed as it jumps 660 // one hour at the DST crossover 661 } else if (!useUTC && (interval === timeUnits[DAY] || interval === timeUnits[WEEK])) { 662 time = makeTime(minYear, minMonth, minDateDate + 663 i * count * (interval === timeUnits[DAY] ? 1 : 7)); 664 665 // else, the interval is fixed and we use simple addition 666 } else { 667 time += interval * count; 668 669 // mark new days if the time is dividable by day 670 if (interval <= timeUnits[HOUR] && time % timeUnits[DAY] === 0) { 671 higherRanks[time] = DAY; 672 } 673 } 674 675 i++; 676 } 677 678 // push the last time 679 tickPositions.push(time); 680 681 // record information on the chosen unit - for dynamic label formatter 682 tickPositions.info = extend(normalizedInterval, { 683 higherRanks: higherRanks, 684 totalRange: interval * count 685 }); 686 687 return tickPositions; 688} 689 690/** 691 * Helper class that contains variuos counters that are local to the chart. 692 */ 693function ChartCounters() { 694 this.color = 0; 695 this.symbol = 0; 696} 697 698ChartCounters.prototype = { 699 /** 700 * Wraps the color counter if it reaches the specified length. 701 */ 702 wrapColor: function (length) { 703 if (this.color >= length) { 704 this.color = 0; 705 } 706 }, 707 708 /** 709 * Wraps the symbol counter if it reaches the specified length. 710 */ 711 wrapSymbol: function (length) { 712 if (this.symbol >= length) { 713 this.symbol = 0; 714 } 715 } 716}; 717 718/** 719 * Utility method extracted from Tooltip code that places a tooltip in a chart without spilling over 720 * and not covering the point it self. 721 */ 722function placeBox(boxWidth, boxHeight, outerLeft, outerTop, outerWidth, outerHeight, point, distance, preferRight) { 723 724 // keep the box within the chart area 725 var pointX = point.x, 726 pointY = point.y, 727 x = pointX + outerLeft + (preferRight ? distance : -boxWidth - distance), 728 y = pointY - boxHeight + outerTop + 15, // 15 means the point is 15 pixels up from the bottom of the tooltip 729 alignedRight; 730 731 // it is too far to the left, adjust it 732 if (x < 7) { 733 x = outerLeft + pointX + distance; 734 } 735 736 // Test to see if the tooltip is too far to the right, 737 // if it is, move it back to be inside and then up to not cover the point. 738 if ((x + boxWidth) > (outerLeft + outerWidth)) { 739 x -= (x + boxWidth) - (outerLeft + outerWidth); 740 y = pointY - boxHeight + outerTop - distance; 741 alignedRight = true; 742 } 743 744 // if it is now above the plot area, align it to the top of the plot area 745 if (y < outerTop + 5) { 746 y = outerTop + 5; 747 748 // If the tooltip is still covering the point, move it below instead 749 if (alignedRight && pointY >= y && pointY <= (y + boxHeight)) { 750 y = pointY + outerTop + distance; // below 751 } 752 } else if (y + boxHeight > outerTop + outerHeight) { 753 y = outerTop + outerHeight - boxHeight - distance; // below 754 } 755 756 return {x: x, y: y}; 757} 758 759/** 760 * Utility method that sorts an object array and keeping the order of equal items. 761 * ECMA script standard does not specify the behaviour when items are equal. 762 */ 763function stableSort(arr, sortFunction) { 764 var length = arr.length, 765 sortValue, 766 i; 767 768 // Add index to each item 769 for (i = 0; i < length; i++) { 770 arr[i].ss_i = i; // stable sort index 771 } 772 773 arr.sort(function (a, b) { 774 sortValue = sortFunction(a, b); 775 return sortValue === 0 ? a.ss_i - b.ss_i : sortValue; 776 }); 777 778 // Remove index from items 779 for (i = 0; i < length; i++) { 780 delete arr[i].ss_i; // stable sort index 781 } 782} 783 784/** 785 * Non-recursive method to find the lowest member of an array. Math.min raise
785s a maximum 786 * call stack size exceeded error in Chrome when trying to apply more than 150.000 points. This 787 * method is slightly slower, but safe. 788 */ 789function arrayMin(data) { 790 var i = data.length, 791 min = data[0]; 792 793 while (i--) { 794 if (data[i] < min) { 795 min = data[i]; 796 } 797 } 798 return min; 799} 800 801/** 802 * Non-recursive method to find the lowest member of an array. Math.min raises a maximum 803 * call stack size exceeded error in Chrome when trying to apply more than 150.000 points. This 804 * method is slightly slower, but safe. 805 */ 806function arrayMax(data) { 807 var i = data.length, 808 max = data[0]; 809 810 while (i--) { 811 if (data[i] > max) { 812 max = data[i]; 813 } 814 } 815 return max; 816} 817 818/** 819 * Utility method that destroys any SVGElement or VMLElement that are properties on the given object. 820 * It loops all properties and invokes destroy if there is a destroy method. The property is 821 * then delete'ed. 822 */ 823function destroyObjectProperties(obj) { 824 var n; 825 for (n in obj) { 826 // If the object is non-null and destroy is defined 827 if (obj[n] && obj[n].destroy) { 828 // Invoke the destroy 829 obj[n].destroy(); 830 } 831 832 // Delete the property from the object. 833 delete obj[n]; 834 } 835} 836 837 838/** 839 * Discard an element by moving it to the bin and delete 840 * @param {Object} The HTML node to discard 841 */ 842function discardElement(element) { 843 // create a garbage bin element, not part of the DOM 844 if (!garbageBin) { 845 garbageBin = createElement(DIV); 846 } 847 848 // move the node and empty bin 849 if (element) { 850 garbageBin.appendChild(element); 851 } 852 garbageBin.innerHTML = ''; 853} 854 855/** 856 * Provide error messages for debugging, with links to online explanation 857 */ 858function error(code, stop) { 859 var msg = 'Highcharts error #' + code + ': www.highcharts.com/errors/' + code; 860 if (stop) { 861 throw msg; 862 } else if (win.console) { 863 console.log(msg); 864 } 865} 866 867/** 868 * Fix JS round off float errors 869 * @param {Number} num 870 */ 871function correctFloat(num) { 872 return parseFloat( 873 num.toPrecision(14) 874 ); 875} 876 877/** 878 * The time unit lookup 879 */ 880/*jslint white: true*/ 881timeUnits = hash( 882 MILLISECOND, 1, 883 SECOND, 1000, 884 MINUTE, 60000, 885 HOUR, 3600000, 886 DAY, 24 * 3600000, 887 WEEK, 7 * 24 * 3600000, 888 MONTH, 30 * 24 * 3600000, 889 YEAR, 31556952000 890); 891/*jslint white: false*/ 892/** 893 * Path interpolation algorithm used across adapters 894 */ 895pathAnim = { 896 /** 897 * Prepare start and end values so that the path can be animated one to one 898 */ 899 init: function (elem, fromD, toD) { 900 fromD = fromD || ''; 901 var shift = elem.shift, 902 bezier = fromD.indexOf('C') > -1, 903 numParams = bezier ? 7 : 3, 904 endLength, 905 slice, 906 i, 907 start = fromD.split(' '), 908 end = [].concat(toD), // copy 909 startBaseLine, 910 endBaseLine, 911 sixify = function (arr) { // in splines make move points have six parameters like bezier curves 912 i = arr.length; 913 while (i--) { 914 if (arr[i] === M) { 915 arr.splice(i + 1, 0, arr[i + 1], arr[i + 2], arr[i + 1], arr[i + 2]); 916 } 917 } 918 }; 919 920 if (bezier) { 921 sixify(start); 922 sixify(end); 923 } 924 925 // pull out the base lines before padding 926 if (elem.isArea) { 927 startBaseLine = start.splice(start.length - 6, 6); 928 endBaseLine = end.splice(end.length - 6, 6); 929 } 930 931 // if shifting points, prepend a dummy point to the end path 932 if (shift === 1) { 933 934 end = [].concat(end).splice(0, numParams).concat(end); 935 } 936 elem.shift = 0; // reset for following animations 937 938 // copy and append last point until the length matches the end length 939 if (start.length) { 940 endLength = end.length; 941 while (start.length < endLength) { 942 943 //bezier && sixify(start); 944 slice = [].concat(start).splice(start.length - numParams, numParams); 945 if (bezier) { // disable first control point 946 slice[numParams - 6] = slice[numParams - 2]; 947 slice[numParams - 5] = slice[numParams - 1]; 948 } 949 start = start.concat(slice); 950 } 951 } 952 953 if (startBaseLine) { // append the base lines for areas 954 start = start.concat(startBaseLine); 955 end = end.concat(endBaseLine); 956 } 957 return [start, end]; 958 }, 959 960 /** 961 * Interpolate each value of the path and return the array 962 */ 963 step: function (start, end, pos, complete) { 964 var ret = [], 965 i = start.length, 966 startVal; 967 968 if (pos === 1) { // land on the final path without adjustment points appended in the ends 969 ret = complete; 970 971 } else if (i === end.length && pos < 1) { 972 while (i--) { 973 startVal = parseFloat(start[i]); 974 ret[i] = 975 isNaN(startVal) ? // a letter instruction like M or L 976 start[i] : 977 pos * (parseFloat(end[i] - startVal)) + startVal; 978 979 } 980 } else { // if animation is finished or length not matching, land on right value 981 ret = end; 982 } 983 return ret; 984 } 985}; 986 987 988/** 989 * Set the global animation to either a given value, or fall back to the 990 * given chart's animation option 991 * @param {Object} animation 992 * @param {Object} chart 993 */ 994function setAnimation(animation, chart) { 995 globalAnimation = pick(animation, chart.animation); 996} 997 998/* 999 * Define the adapter for frameworks. If an external adapter is not defined, 1000 * Highcharts reverts to the built-in jQuery adapter. 1001 */ 1002if (globalAdapter && globalAdapter.init) { 1003 // Initialize the adapter with the pathAnim object that takes care 1004 // of path animations. 1005 globalAdapter.init(pathAnim); 1006} 1007if (!globalAdapter && win.jQuery) { 1008 var jQ = jQuery; 1009 1010 /** 1011 * Downloads a script and executes a callback when done. 1012 * @param {String} scriptLocation 1013 * @param {Function} callback 1014 */ 1015 getScript = jQ.getScript; 1016 1017 /** 1018 * Utility for iterating over an array. Parameters are reversed compared to jQuery. 1019 * @param {Array} arr 1020 * @param {Function} fn 1021 */ 1022 each = function (arr, fn) { 1023 var i = 0, 1024 len = arr.length; 1025 for (; i < len; i++) { 1026 if (fn.call(arr[i], arr[i], i, arr) === false) { 1027 return i; 1028 } 1029 } 1030 }; 1031 1032 /** 1033 * Filter an array 1034 */ 1035 grep = jQ.grep; 1036 1037 /** 1038 * Map an array 1039 * @param {Array} arr 1040 * @param {Function} fn 1041 */ 1042 map = function (arr, fn) { 1043 //return jQuery.map(arr, fn); 1044 var results = [], 1045 i = 0, 1046 len = arr.length; 1047 for (; i < len; i++) { 1048 results[i] = fn.call(arr[i], arr[i], i, arr); 1049 } 1050 return results; 1051 1052 }; 1053 1054 /** 1055 * Deep merge two objects and return a third object 1056 */ 1057 merge = function () { 1058 var args = arguments; 1059 return jQ.extend(true, null, args[0], args[1], args[2], args[3]); 1060 }; 1061 1062 /** 1063 * Get the position of an element relative to the top left of the page 1064 */ 1065 offset = function (el) { 1066 return jQ(el).offset(); 1067 }; 1068 1069 /** 1070 * Add an event listener 1071 * @param {Object} el A HTML element or custom object 1072 * @param {String} event The event type 1073 * @param {Function} fn The event handler 1074 */ 1075 addEvent = function (el, event, fn) { 1076 jQ(el).bind(event, fn); 1077 }; 1078 1079 /** 1080 * Remove event added with addEvent 1081 * @param {Object} el The object 1082 * @param {String} eventType The event type. Leave blank to remove all events. 1083 * @param {Function} handler The function to remove 1084 */ 1085 removeEvent = function (el, eventType, handler) { 1086 // workaround for jQuery issue with unbinding custom events: 1087 // http://forum.jquery.com/topic/javascript-error-when-unbinding-a-custom-event-using-jquery-1-4-2 1088 var func = doc.removeEventListener ? 'removeEventListener' : 'detachEvent'; 1089 if (doc[func] && !el[func]) { 1090 el[func] = function () {}; 1091 } 1092 1093 jQ(el).unbind(eventType, handler); 1094 }; 1095 1096 /** 1097 * Fire an event on a custom object 1098 * @param {Object} el 1099 * @param {String} type 1100 * @param {Object} eventArguments 1101 * @param {Function} defaultFunction 1102 */ 1103 fireEvent = function (el, type, eventArguments, defaultFunction) { 1104 var event = jQ.Event(type), 1105 detachedType = 'detached' + type, 1106 defaultPrevented; 1107 1108 extend(event, eventArguments); 1109 1110 // Prevent jQuery from triggering the object method that is named the 1111 // same as the event. For example, if the event is 'select', jQuery 1112 // attempts calling el.select and it goes into a loop. 1113 if (el[type]) { 1114 el[detachedType] = el[type]; 1115 el[type] = null; 1116 } 1117 1118 // Wrap preventDefault and stopPropagation in try/catch blocks in 1119 // order to prevent JS errors when cancelling events on non-DOM 1120 // objects. #615. 1121 each(['preventDefault', 'stopPropagation'], function (fn) { 1122 var base = event[fn]; 1123 event[fn] = function () { 1124 try { 1125 base.call(event); 1126 } catch (e) { 1127 if (fn === 'preventDefault') { 1128 defaultPrevented = true; 1129 } 1130 } 1131 }; 1132 }); 1133 1134 // trigger it 1135 jQ(el).trigger(event); 1136 1137 // attach the method 1138 if (el[detachedType]) { 1139 el[type] = el[detachedType]; 1140 el[detachedType] = null; 1141 } 1142 1143 if (defaultFunction && !event.isDefaultPrevented() && !defaultPrevented) { 1144 defaultFunction(event); 1145 } 1146 }; 1147 1148 /** 1149 * Animate a HTML element or SVG element wrapper 1150 * @param {Object} el 1151 * @param {Object} params 1152 * @param {Object} options jQuery-like animation options: duration, easing, callback 1153 */ 1154 animate = function (el, params, options) { 1155 var $el = jQ(el); 1156 if (params.d) { 1157 el.toD = params.d; // keep the array form for paths, used in jQ.fx.step.d 1158 params.d = 1; // because in jQuery, animating to an array has a different meaning 1159 } 1160 1161 $el.stop(); 1162 $el.animate(params, options); 1163 1164 }; 1165 /** 1166 * Stop running animation 1167 */
1168 stop = function (el) { 1169 jQ(el).stop(); 1170 }; 1171 1172 1173 //=== Extend jQuery on init 1174 1175 /*jslint unparam: true*//* allow unused param x in this function */ 1176 jQ.extend(jQ.easing, { 1177 easeOutQuad: function (x, t, b, c, d) { 1178 return -c * (t /= d) * (t - 2) + b; 1179 } 1180 }); 1181 /*jslint unparam: false*/ 1182 1183 // extend the animate function to allow SVG animations 1184 var jFx = jQuery.fx, 1185 jStep = jFx.step; 1186 1187 // extend some methods to check for elem.attr, which means it is a Highcharts SVG object 1188 each(['cur', '_default', 'width', 'height'], function (fn, i) { 1189 var obj = i ? jStep : jFx.prototype, // 'cur', the getter' relates to jFx.prototype 1190 base = obj[fn], 1191 elem; 1192 1193 if (base) { // step.width and step.height don't exist in jQuery < 1.7 1194 1195 // create the extended function replacement 1196 obj[fn] = function (fx) { 1197 1198 // jFx.prototype.cur does not use fx argument 1199 fx = i ? fx : this; 1200 1201 // shortcut 1202 elem = fx.elem; 1203 1204 // jFX.prototype.cur returns the current value. The other ones are setters 1205 // and returning a value has no effect. 1206 return elem.attr ? // is SVG element wrapper 1207 elem.attr(fx.prop, fx.now) : // apply the SVG wrapper's method 1208 base.apply(this, arguments); // use jQuery's built-in method 1209 }; 1210 } 1211 }); 1212 1213 // animate paths 1214 jStep.d = function (fx) { 1215 var elem = fx.elem; 1216 1217 1218 // Normally start and end should be set in state == 0, but sometimes, 1219 // for reasons unknown, this doesn't happen. Perhaps state == 0 is skipped 1220 // in these cases 1221 if (!fx.started) { 1222 var ends = pathAnim.init(elem, elem.d, elem.toD); 1223 fx.start = ends[0]; 1224 fx.end = ends[1]; 1225 fx.started = true; 1226 } 1227 1228 1229 // interpolate each value of the path 1230 elem.attr('d', pathAnim.step(fx.start, fx.end, fx.pos, elem.toD)); 1231 1232 }; 1233} 1234 1235/* **************************************************************************** 1236 * Handle the options * 1237 *****************************************************************************/ 1238var 1239 1240defaultLabelOptions = { 1241 enabled: true, 1242 // rotation: 0, 1243 align: 'center', 1244 x: 0, 1245 y: 15, 1246 /*formatter: function () { 1247 return this.value; 1248 },*/ 1249 style: { 1250 color: '#666', 1251 fontSize: '11px', 1252 lineHeight: '14px' 1253 } 1254}; 1255 1256defaultOptions = { 1257 colors: ['#4572A7', '#AA4643', '#89A54E', '#80699B', '#3D96AE', 1258 '#DB843D', '#92A8CD', '#A47D7C', '#B5CA92'], 1259 symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'], 1260 lang: { 1261 loading: 'Loading...', 1262 months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 1263 'August', 'September', 'October', 'November', 'December'], 1264 shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], 1265 weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], 1266 decimalPoint: '.', 1267 resetZoom: 'Reset zoom', 1268 resetZoomTitle: 'Reset zoom level 1:1', 1269 thousandsSep: ',' 1270 }, 1271 global: { 1272 useUTC: true, 1273 canvasToolsURL: 'http://code.highcharts.com/2.2.1/modules/canvas-tools.js' 1274 }, 1275 chart: { 1276 //animation: true, 1277 //alignTicks: false, 1278 //reflow: true, 1279 //className: null, 1280 //events: { load, selection }, 1281 //margin: [null], 1282 //marginTop: null, 1283 //marginRight: null, 1284 //marginBottom: null, 1285 //marginLeft: null, 1286 borderColor: '#4572A7', 1287 //borderWidth: 0, 1288 borderRadius: 5, 1289 defaultSeriesType: 'line', 1290 ignoreHiddenSeries: true, 1291 //inverted: false, 1292 //shadow: false, 1293 spacingTop: 10, 1294 spacingRight: 10, 1295 spacingBottom: 15, 1296 spacingLeft: 10, 1297 style: { 1298 fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font 1299 fontSize: '12px' 1300 }, 1301 backgroundColor: '#FFFFFF',
1302 //plotBackgroundColor: null, 1303 plotBorderColor: '#C0C0C0', 1304 //plotBorderWidth: 0, 1305 //plotShadow: false, 1306 //zoomType: '' 1307 resetZoomButton: { 1308 theme: { 1309 zIndex: 20 1310 }, 1311 position: { 1312 align: 'right', 1313 x: -10, 1314 //verticalAlign: 'top', 1315 y: 10 1316 } 1317 // relativeTo: 'plot' 1318 } 1319 }, 1320 title: { 1321 text: 'Chart title', 1322 align: 'center', 1323 // floating: false, 1324 // margin: 15, 1325 // x: 0, 1326 // verticalAlign: 'top', 1327 y: 15, 1328 style: { 1329 color: '#3E576F', 1330 fontSize: '16px' 1331 } 1332 1333 }, 1334 subtitle: { 1335 text: '', 1336 align: 'center', 1337 // floating: false 1338 // x: 0, 1339 // verticalAlign: 'top', 1340 y: 30, 1341 style: { 1342 color: '#6D869F' 1343 } 1344 }, 1345 1346 plotOptions: { 1347 line: { // base series options 1348 allowPointSelect: false, 1349 showCheckbox: false, 1350 animation: { 1351 duration: 1000 1352 }, 1353 //connectNulls: false, 1354 //cursor: 'default', 1355 //clip: true, 1356 //dashStyle: null, 1357 //enableMouseTracking: true, 1358 events: {}, 1359 //legendIndex: 0, 1360 lineWidth: 2, 1361 shadow: true, 1362 // stacking: null, 1363 marker: { 1364 enabled: true, 1365 //symbol: null, 1366 lineWidth: 0, 1367 radius: 4, 1368 lineColor: '#FFFFFF', 1369 //fillColor: null, 1370 states: { // states for a single point 1371 hover: { 1372 //radius: base + 2 1373 }, 1374 select: {
1375 fillColor: '#FFFFFF', 1376 lineColor: '#000000', 1377 lineWidth: 2 1378 } 1379 } 1380 }, 1381 point: { 1382 events: {} 1383 }, 1384 dataLabels: merge(defaultLabelOptions, { 1385 enabled: false, 1386 y: -6, 1387 formatter: function () { 1388 return this.y; 1389 } 1390 // backgroundColor: undefined, 1391 // borderColor: undefined, 1392 // borderRadius: undefined, 1393 // borderWidth: undefined, 1394 // padding: 3, 1395 // shadow: false 1396 }), 1397 cropThreshold: 300, // draw points outside the plot area when the number of points is less than this 1398 pointRange: 0, 1399 //pointStart: 0, 1400 //pointInterval: 1, 1401 showInLegend: true, 1402 states: { // states for the entire series 1403 hover: { 1404 //enabled: false, 1405 //lineWidth: base + 1, 1406 marker: { 1407 // lineWidth: base + 1, 1408 // radius: base + 1 1409 } 1410 }, 1411 select: { 1412 marker: {} 1413 } 1414 }, 1415 stickyTracking: true 1416 //tooltip: { 1417 //pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b>' 1418 //valueDecimals: null, 1419 //xDateFormat: '%A, %b %e, %Y', 1420 //valuePrefix: '', 1421 //ySuffix: '' 1422 //} 1423 // turboThreshold: 1000 1424 // zIndex: null 1425 } 1426 }, 1427 labels: { 1428 //items: [], 1429 style: { 1430 //font: defaultFont, 1431 position: ABSOLUTE, 1432 color: '#3E576F' 1433 } 1434 }, 1435 legend: { 1436 enabled: true, 1437 align: 'center', 1438 //floating: false, 1439 layout: 'horizontal', 1440 labelFormatter: function () { 1441 return this.name; 1442 }, 1443 borderWidth: 1, 1444 borderColor: '#909090', 1445 borderRadius: 5, 1446 // margin: 10, 1447 // reversed: false, 1448 shadow: false, 1449 // backgroundColor: null, 1450 style: { 1451 padding: '5px' 1452 }, 1453 itemStyle: { 1454 cursor: 'pointer', 1455 color: '#3E576F' 1456 }, 1457 itemHoverStyle: { 1458 //cursor: 'pointer', removed as of #601 1459 color: '#000000' 1460 }, 1461 itemHiddenStyle: { 1462 color: '#C0C0C0' 1463 }, 1464 itemCheckboxStyle: { 1465 position: ABSOLUTE, 1466 width: '13px', // for IE precision 1467 height: '13px' 1468 }, 1469 // itemWidth: undefined, 1470 symbolWidth: 16, 1471 symbolPadding: 5, 1472 verticalAlign: 'bottom', 1473 // width: undefined, 1474 x: 0, 1475 y: 0 1476 }, 1477 1478 loading: { 1479 // hideDuration: 100, 1480 labelStyle: { 1481 fontWeight: 'bold', 1482 position: RELATIVE, 1483 top: '1em' 1484 }, 1485 // showDuration: 0, 1486 style: { 1487 position: ABSOLUTE, 1488 backgroundColor: 'white', 1489 opacity: 0.5, 1490 textAlign: 'center' 1491 } 1492 }, 1493 1494 tooltip: { 1495 enabled: true, 1496 //crosshairs: null, 1497 backgroundColor: 'rgba(255, 255, 255, .85)', 1498 borderWidth: 2, 1499 borderRadius: 5, 1500 //formatter: defaultFormatter, 1501 headerFormat: '<span style="font-size: 10px">{point.key}</span><br/>', 1502 pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b><br/>', 1503 shadow: true, 1504 shared: useCanVG, 1505 snap: hasTouch ? 25 : 10, 1506 style: { 1507 color: '#333333', 1508 fontSize: '12px', 1509 padding: '5px', 1510 whiteSpace: 'nowrap' 1511 } 1512 //xDateFormat: '%A, %b %e, %Y', 1513 //valueDecimals: null, 1514 //valuePrefix: '', 1515 //valueSuffix: '' 1516 }, 1517 1518 credits: { 1519 enabled: true, 1520 text: 'Highcharts.com', 1521 href: 'http://www.highcharts.com', 1522 position: { 1523 align: 'right', 1524 x: -10, 1525 verticalAlign: 'bottom', 1526 y: -5 1527 }, 1528 style: { 1529 cursor: 'pointer', 1530 color: '#909090', 1531 fontSize: '10px' 1532 } 1533 } 1534}; 1535 1536// Axis defaults 1537/*jslint white: true*/ 1538var defaultXAxisOptions = { 1539 // allowDecimals: null, 1540 // alternateGridColor: null, 1541 // categories: [], 1542 dateTimeLabelFormats: hash( 1543 MILLISECOND, '%H:%M:%S.%L', 1544 SECOND, '%H:%M:%S', 1545 MINUTE, '%H:%M', 1546 HOUR, '%H:%M', 1547 DAY, '%e. %b', 1548 WEEK, '%e. %b', 1549 MONTH, '%b \'%y', 1550 YEAR, '%Y' 1551 ), 1552 endOnTick: false, 1553 gridLineColor: '#C0C0C0', 1554 // gridLineDashStyle: 'solid', 1555 // gridLineWidth: 0, 1556 // reversed: false, 1557 1558 labels: defaultLabelOptions, 1559 // { step: null }, 1560 lineColor: '#C0D0E0', 1561 lineWidth: 1, 1562 //linkedTo: null, 1563 max: null, 1564 min: null, 1565 minPadding: 0.01, 1566 maxPadding: 0.01, 1567 //minRange: null, 1568 minorGridLineColor: '#E0E0E0', 1569 // minorGridLineDashStyle: null, 1570 minorGridLineWidth: 1, 1571 minorTickColor: '#A0A0A0', 1572 //minorTickInterval: null, 1573 minorTickLength: 2, 1574 minorTickPosition: 'outside', // inside or outside 1575 //minorTickWidth: 0, 1576 //opposite: false, 1577 //offset: 0, 1578 //plotBands: [{ 1579 // events: {}, 1580 // zIndex: 1, 1581 // labels: { align, x, verticalAlign, y, style, rotation, textAlign } 1582 //}], 1583 //plotLines: [{ 1584 // events: {} 1585 // dashStyle: {} 1586 // zIndex: 1587 // labels: { align, x, verticalAlign, y, style, rotation, textAlign } 1588 //}], 1589 //reversed: false, 1590 // showFirstLabel: true, 1591 // showLastLabel: true, 1592 startOfWeek: 1, 1593 startOnTick: false, 1594 tickColor: '#C0D0E0', 1595 //tickInterval: null, 1596 tickLength: 5, 1597 tickmarkPlacement: 'between', // on or between 1598 tickPixelInterval: 100, 1599 tickPosition: 'outside', 1600 tickWidth: 1, 1601 title: { 1602 //text: null, 1603 align: 'middle', // low, middle or high 1604 //margin: 0 for horizontal, 10 for vertical axes, 1605 //rotation: 0, 1606 //side: 'outside', 1607 style: { 1608 color: '#6D869F', 1609 //font: defaultFont.replace('normal', 'bold') 1610 fontWeight: 'bold' 1611 } 1612 //x: 0, 1613 //y: 0 1614 }, 1615 type: 'linear' // linear, logarithmic or datetime 1616}, 1617 1618defaultYAxisOptions = merge(defaultXAxisOptions, { 1619 endOnTick: true, 1620 gridLineWidth: 1, 1621 tickPixelInterval: 72, 1622 showLastLabel: true, 1623 labels: { 1624 align: 'right', 1625 x: -8, 1626 y: 3 1627 }, 1628 lineWidth: 0, 1629 maxPadding: 0.05, 1630 minPadding: 0.05, 1631 startOnTick: true, 1632 tickWidth: 0, 1633 title: { 1634 rotation: 270, 1635 text: 'Y-values' 1636 }, 1637 stackLabels: { 1638 enabled: false, 1639 //align: dynamic, 1640 //y: dynamic, 1641 //x: dynamic, 1642 //verticalAlign: dynamic, 1643 //textAlign: dynamic, 1644 //rotation: 0, 1645 formatter: function () { 1646 return this.total; 1647 }, 1648 style: defaultLabelOptions.style 1649 } 1650}), 1651 1652defaultLeftAxisOptions = { 1653 labels: { 1654 align: 'right', 1655 x: -8, 1656 y: null 1657 }, 1658 title: { 1659 rotation: 270 1660 } 1661}, 1662defaultRightAxisOptions = { 1663 labels: { 1664 align: 'left', 1665 x: 8, 1666 y: null 1667 }, 1668 title: { 1669 rotation: 90 1670 } 1671}, 1672defaultBottomAxisOptions = { // horizontal axis 1673 labels: { 1674 align: 'center', 1675 x: 0, 1676 y: 14, 1677 overflow: 'justify' // docs 1678 // staggerLines: null 1679 }, 1680 title: { 1681 rotation: 0 1682 } 1683}
1683, 1684defaultTopAxisOptions = merge(defaultBottomAxisOptions, { 1685 labels: { 1686 y: -5, 1687 overflow: 'justify' 1688 // staggerLines: null 1689 } 1690}); 1691/*jslint white: false*/ 1692 1693 1694 1695// Series defaults 1696var defaultPlotOptions = defaultOptions.plotOptions, 1697 defaultSeriesOptions = defaultPlotOptions.line; 1698//defaultPlotOptions.line = merge(defaultSeriesOptions); 1699defaultPlotOptions.spline = merge(defaultSeriesOptions); 1700defaultPlotOptions.scatter = merge(defaultSeriesOptions, { 1701 lineWidth: 0, 1702 states: { 1703 hover: { 1704 lineWidth: 0 1705 } 1706 }, 1707 tooltip: { 1708 headerFormat: '<span style="font-size: 10px; color:{series.color}">{series.name}</span><br/>', 1709 pointFormat: 'x: <b>{point.x}</b><br/>y: <b>{point.y}</b><br/>' 1710 } 1711}); 1712defaultPlotOptions.area = merge(defaultSeriesOptions, { 1713 threshold: 0 1714 // lineColor: null, // overrides color, but lets fillColor be unaltered 1715 // fillOpacity: 0.75, 1716 // fillColor: null 1717 1718}); 1719defaultPlotOptions.areaspline = merge(defaultPlotOptions.area); 1720defaultPlotOptions.column = merge(defaultSeriesOptions, { 1721 borderColor: '#FFFFFF', 1722 borderWidth: 1, 1723 borderRadius: 0, 1724 //colorByPoint: undefined, 1725 groupPadding: 0.2, 1726 marker: null, // point options are specified in the base options 1727 pointPadding: 0.1, 1728 //pointWidth: null, 1729 minPointLength: 0, 1730 cropThreshold: 50, // when there are more points, they will not animate out of the chart on xAxis.setExtremes 1731 pointRange: null, // null means auto, meaning 1 in a categorized axis and least distance between points if not categories 1732 states: { 1733 hover: { 1734 brightness: 0.1, 1735 shadow: false 1736 }, 1737 select: { 1738 color: '#C0C0C0', 1739 borderColor: '#000000', 1740 shadow: false 1741 } 1742 }, 1743 dataLabels: { 1744 y: null, 1745 verticalAlign: null 1746 }, 1747 threshold: 0 1748}); 1749defaultPlotOptions.bar = merge(defaultPlotOptions.column, { 1750 dataLabels: { 1751 align: 'left', 1752 x: 5, 1753 y: null, 1754 verticalAlign: 'middle' 1755 } 1756}); 1757defaultPlotOptions.pie = merge(defaultSeriesOptions, { 1758 //dragType: '', // n/a 1759 borderColor: '#FFFFFF', 1760 borderWidth: 1, 1761 center: ['50%', '50%'], 1762 colorByPoint: true, // always true for pies 1763 dataLabels: { 1764 // align: null, 1765 // connectorWidth: 1, 1766 // connectorColor: point.color, 1767 // connectorPadding: 5, 1768 distance: 30, 1769 enabled: true, 1770 formatter: function () { 1771 return this.point.name; 1772 }, 1773 // softConnector: true, 1774 y: 5 1775 }, 1776 //innerSize: 0, 1777 legendType: 'point', 1778 marker: null, // point options are specified in the base options 1779 size: '75%', 1780 showInLegend: false, 1781 slicedOffset: 10, 1782 states: { 1783 hover: { 1784 brightness: 0.1, 1785 shadow: false 1786 } 1787 } 1788 1789}); 1790 1791// set the default time methods 1792setTimeMethods(); 1793 1794 1795 1796/** 1797 * Set the time methods globally based on the useUTC option. Time method can be either 1798 * local time or UTC (default). 1799 */ 1800function setTimeMethods() { 1801 var useUTC = defaultOptions.global.useUTC, 1802 GET = useUTC ? 'getUTC' : 'get', 1803 SET = useUTC ? 'setUTC' : 'set'; 1804 1805 makeTime = useUTC ? Date.UTC : function (year, month, date, hours, minutes, seconds) { 1806 return new Date( 1807 year, 1808 month, 1809 pick(date, 1), 1810 pick(hours, 0), 1811 pick(minutes, 0), 1812 pick(seconds, 0) 1813 ).getTime(); 1814 }; 1815 getMinutes = GET + 'Minutes'; 1816 getHours = GET + 'Hours'; 1817 getDay = GET + 'Day'; 1818 getDate = GET + 'Date'; 1819 getMonth = GET + 'Month'; 1820 getFullYear = GET + 'FullYear'; 1821 setMinutes = SET + 'Minutes'; 1822 setHours = SET + 'Hours'; 1823 setDate = SET + 'Date'; 1824 setMonth = SET + 'Month'; 1825 setFullYear = SET + 'FullYear'; 1826 1827} 1828 1829/** 1830 * Merge the default options with custom options and return the new options structure 1831 * @param {Object} options The new custom options 1832 */ 1833function setOptions(options) { 1834 1835 // Pull out axis options and apply them to the respective default axis options 1836 defaultXAxisOptions = merge(defaultXAxisOptions, options.xAxis); 1837 defaultYAxisOptions = merge(defaultYAxisOptions, options.yAxis); 1838 options.xAxis = options.yAxis = UNDEFINED; 1839 1840 // Merge in the default options 1841 defaultOptions = merge(defaultOptions, options); 1842 1843 // Apply UTC 1844 setTimeMethods(); 1845 1846 return defaultOptions; 1847} 1848 1849/** 1850 * Get the updated default options. Merely exposing defaultOptions for outside modules 1851 * isn't enough because the setOptions method creates a new object. 1852 */ 1853function getOptions() { 1854 return defaultOptions; 1855} 1856 1857 1858 1859/** 1860 * Handle color operations. The object methods are chainable. 1861 * @param {String} input The input color in either rbga or hex format 1862 */ 1863var Color = function (input) { 1864 // declare variables 1865 var rgba = [], result; 1866 1867 /** 1868 * Parse the input color to rgba array 1869 * @param {String} input 1870 */ 1871 function init(input) { 1872 1873 // rgba 1874 result = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/.exec(input); 1875 if (result) { 1876 rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)]; 1877 } else { // hex 1878 result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(input); 1879 if (result) { 1880 rgba = [pInt(result[1], 16), pInt(result[2], 16), pInt(result[3], 16), 1]; 1881 } 1882 } 1883 1884 } 1885 /** 1886 * Return the color a specified format 1887 * @param {String} format 1888 */ 1889 function get(format) { 1890 var ret; 1891 1892 // it's NaN if gradient colors on a column chart 1893 if (rgba && !isNaN(rgba[0])) { 1894 if (format === 'rgb') { 1895 ret = 'rgb(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ')'; 1896 } else if (format === 'a') { 1897 ret = rgba[3]; 1898 } else { 1899 ret = 'rgba(' + rgba.join(',') + ')'; 1900 } 1901 } else { 1902 ret = input; 1903 } 1904 return ret; 1905 } 1906 1907 /** 1908 * Brighten the color 1909 * @param {Number} alpha 1910 */ 1911 function brighten(alpha) { 1912 if (isNumber(alpha) && alpha !== 0) { 1913 var i; 1914 for (i = 0; i < 3; i++) { 1915 rgba[i] += pInt(alpha * 255); 1916 1917 if (rgba[i] < 0) { 1918 rgba[i] = 0; 1919 } 1920 if (rgba[i] > 255) { 1921 rgba[i] = 255; 1922 } 1923 } 1924 } 1925 return this; 1926 } 1927 /** 1928 * Set the color's opacity to a given alpha value 1929 * @param {Number} alpha 1930 */ 1931 function setOpacity(alpha) { 1932 rgba[3] = alpha; 1933 return this; 1934 } 1935 1936 // initialize: parse the input 1937 init(input); 1938 1939 // public methods 1940 return { 1941 get: get, 1942 brighten: brighten, 1943 setOpacity: setOpacity 1944 }; 1945}; 1946 1947 1948/** 1949 * A wrapper object for SVG elements 1950 */ 1951function SVGElement() {} 1952 1953SVGElement.prototype = { 1954 /** 1955 * Initialize the SVG renderer 1956 * @param {Object} renderer 1957 * @param {String}
1957 nodeName 1958 */ 1959 init: function (renderer, nodeName) { 1960 var wrapper = this; 1961 wrapper.element = nodeName === 'span' ? 1962 createElement(nodeName) : 1963 doc.createElementNS(SVG_NS, nodeName); 1964 wrapper.renderer = renderer; 1965 /** 1966 * A collection of attribute setters. These methods, if defined, are called right before a certain 1967 * attribute is set on an element wrapper. Returning false prevents the default attribute 1968 * setter to run. Returning a value causes the default setter to set that value. Used in 1969 * Renderer.label. 1970 */ 1971 wrapper.attrSetters = {}; 1972 }, 1973 /** 1974 * Animate a given attribute 1975 * @param {Object} params 1976 * @param {Number} options The same options as in jQuery animation 1977 * @param {Function} complete Function to perform at the end of animation 1978 */ 1979 animate: function (params, options, complete) { 1980 var animOptions = pick(options, globalAnimation, true); 1981 stop(this); // stop regardless of animation actually running, or reverting to .attr (#607) 1982 if (animOptions) { 1983 animOptions = merge(animOptions); 1984 if (complete) { // allows using a callback with the global animation without overwriting it 1985 animOptions.complete = complete; 1986 } 1987 animate(this, params, animOptions); 1988 } else { 1989 this.attr(params); 1990 if (complete) { 1991 complete(); 1992 } 1993 } 1994 }, 1995 /** 1996 * Set or get a given attribute 1997 * @param {Object|String} hash 1998 * @param {Mixed|Undefined} val 1999 */ 2000 attr: function (hash, val) { 2001 var wrapper = this, 2002 key, 2003 value, 2004 result, 2005 i, 2006 child, 2007 element = wrapper.element, 2008 nodeName = element.nodeName, 2009 renderer = wrapper.renderer, 2010 skipAttr, 2011 attrSetters = wrapper.attrSetters, 2012 shadows = wrapper.shadows, 2013 hasSetSymbolSize, 2014 ret = wrapper; 2015 2016 // single key-value pair 2017 if (isString(hash) && defined(val)) { 2018 key = hash; 2019 hash = {}; 2020 hash[key] = val; 2021 } 2022 2023 // used as a getter: first argument is a string, second is undefined 2024 if (isString(hash)) { 2025 key = hash; 2026 if (nodeName === 'circle') { 2027 key = { x: 'cx', y: 'cy' }[key] || key; 2028 } else if (key === 'strokeWidth') { 2029 key = 'stroke-width'; 2030 } 2031 ret = attr(element, key) || wrapper[key] || 0; 2032 2033 if (key !== 'd' && key !== 'visibility') { // 'd' is string in animation step 2034 ret = parseFloat(ret); 2035 } 2036 2037 // setter 2038 } else { 2039 2040 for (key in hash) { 2041 skipAttr = false; // reset 2042 value = hash[key]; 2043 2044 // check for a specific attribute setter 2045 result = attrSetters[key] && attrSetters[key](value, key); 2046 2047 if (result !== false) { 2048 2049 if (result !== UNDEFINED) { 2050 value = result; // the attribute setter has returned a new value to set 2051 } 2052 2053 // paths 2054 if (key === 'd') { 2055 if (value && value.join) { // join path 2056 value = value.join(' '); 2057 } 2058 if (/(NaN| {2}|^$)/.test(value)) { 2059 value = 'M 0 0'; 2060 } 2061 wrapper.d = value; // shortcut for animations 2062 2063 // update child tspans x values 2064 } else if (key === 'x' && nodeName === 'text') { 2065 for (i = 0; i < element.childNodes.length; i++) { 2066 child = element.childNodes[i]; 2067 // if the x values are equal, the tspan represents a linebreak 2068 if (attr(child, 'x') === attr(element, 'x')) { 2069 //child.setAttribute('x', value); 2070 attr(child, 'x', value); 2071 } 2072 } 2073 2074 if (wrapper.rotation) { 2075 attr(element, 'transform', 'rotate(' + wrapper.rotation + ' ' + value + ' ' + 2076 pInt(hash.y || attr(element, 'y')) + ')'); 2077 } 2078 2079 // apply gradients 2080 } else if (key === 'fill') { 2081 value = renderer.color(value, element, key); 2082 2083 // circle x and y 2084 } else if (nodeName === 'circle' && (key === 'x' || key === 'y')) { 2085 key = { x: 'cx', y: 'cy' }[key] || key; 2086 2087 // rectangle border radius 2088 } else if (nodeName === 'rect' && key === 'r') { 2089 attr(element, { 2090 rx: value, 2091 ry: value 2092 }); 2093 skipAttr = true; 2094 2095 // translation and text rotation 2096 } else if (key === 'translateX' || key === 'translateY' || key === 'rotation' || key === 'verticalAlign') { 2097 wrapper[key] = value; 2098 wrapper.updateTransform(); 2099 skipAttr = true; 2100 2101 // apply opacity as subnode (required by legacy WebKit and Batik) 2102 } else if (key === 'stroke') { 2103 value = renderer.color(value, element, key); 2104 2105 // emulate VML's dashstyle implementation 2106 } else if (key === 'dashstyle') { 2107 key = 'stroke-dasharray'; 2108 value = value && value.toLowerCase(); 2109 if (value === 'solid') { 2110 value = NONE; 2111 } else if (value) { 2112 value = value 2113 .replace('shortdashdotdot', '3,1,1,1,1,1,') 2114 .replace('shortdashdot', '3,1,1,1') 2115 .replace('shortdot', '1,1,') 2116 .replace('shortdash', '3,1,') 2117 .replace('longdash', '8,3,') 2118 .replace(/dot/g, '1,3,') 2119 .replace('dash', '4,3,') 2120 .replace(/,$/, '') 2121 .split(','); // ending comma 2122 2123 i = value.length; 2124 while (i--) { 2125 value[i] = pInt(value[i]) * hash['stroke-width']; 2126 } 2127 value = value.join(','); 2128 } 2129 2130 // special 2131 } else if (key === 'isTracker') { 2132 wrapper[key] = value; 2133 2134 // IE9/MooTools combo: MooTools returns objects instead of numbers and IE9 Beta 2 2135 // is unable to cast them. Test again with final IE9. 2136 } else if (key === 'width') { 2137 value = pInt(value); 2138 2139 // Text alignment 2140 } else if (key === 'align') { 2141 key = 'text-anchor'; 2142 value = { left: 'start', center: 'middle', right: 'end' }[value]; 2143 2144 // Title requires a subnode, #431 2145 } else if (key === 'title') { 2146 var title = doc.createElementNS(SVG_NS, 'title'); 2147 title.appendChild(doc.createTextNode(value)); 2148 element.appendChild(title); 2149 } 2150 2151 // jQuery animate changes case 2152 if (key === 'strokeWidth') { 2153 key = 'stroke-width'; 2154 } 2155 2156 // Chrome/Win < 6 bug (http://code.google.com/p/chromium/issues/detail?id=15461) 2157 if (isWebKit && key === 'stroke-width' && value === 0) { 2158 value = 0.000001; 2159 } 2160 2161 // symbols 2162 if (wrapper.symbolName && /^(x|y|r|start|end|innerR|anchorX|anchorY)/.test(key)) { 2163 2164 2165 if (!hasSetSymbolSize) { 2166 wrapper.symbolAttr(hash); 2167 hasSetSymbolSize = true; 2168 } 2169 skipAttr = true; 2170 } 2171 2172 // let the shadow follow the main element 2173 if (shadows && /^(width|height|visibility|x|y|d|transform)$/.test(key)) { 2174 i = shadows.length; 2175 while (i--) { 2176 attr(shadows[i], key, value); 2177 } 2178 } 2179 2180 // validate heights 2181 if ((key === 'width' || key === 'height') && nodeName === 'rect' && value < 0) { 2182 value = 0; 2183 } 2184 2185 2186 2187 2188 if (key === 'text') { 2189 // only one node allowed 2190 wrapper.textStr = value; 2191 if (wrapper.added) { 2192 renderer.buildText(wrapper); 2193 } 2194 } else if (!skipAttr) { 2195 attr(element, key, value); 2196 } 2197 2198 } 2199 2200 } 2201 2202 } 2203
2204 // Workaround for our #732, WebKit's issue https://bugs.webkit.org/show_bug.cgi?id=78385 2205 // TODO: If the WebKit team fix this bug before the final release of Chrome 18, remove the workaround. 2206 if (isWebKit && /Chrome\/(18|19)/.test(userAgent)) { 2207 if (nodeName === 'text' && (hash.x !== UNDEFINED || hash.y !== UNDEFINED)) { 2208 var parent = element.parentNode, 2209 next = element.nextSibling; 2210 2211 if (parent) { 2212 parent.removeChild(element); 2213 if (next) { 2214 parent.insertBefore(element, next); 2215 } else { 2216 parent.appendChild(element); 2217 } 2218 } 2219 } 2220 } 2221 // End of workaround for #732 2222 2223 return ret; 2224 }, 2225 2226 /** 2227 * If one of the symbol size affecting parameters are changed, 2228 * check all the others only once for each call to an element's 2229 * .attr() method 2230 * @param {Object} hash 2231 */ 2232 symbolAttr: function (hash) { 2233 var wrapper = this; 2234 2235 each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR', 'anchorX', 'anchorY'], function (key) { 2236 wrapper[key] = pick(hash[key], wrapper[key]); 2237 }); 2238 2239 wrapper.attr({ 2240 d: wrapper.renderer.symbols[wrapper.symbolName](wrapper.x, wrapper.y, wrapper.width, wrapper.height, wrapper) 2241 }); 2242 }, 2243 2244 /** 2245 * Apply a clipping path to this object 2246 * @param {String} id 2247 */ 2248 clip: function (clipRect) { 2249 return this.attr('clip-path', 'url(' + this.renderer.url + '#' + clipRect.id + ')'); 2250 }, 2251 2252 /** 2253 * Calculate the coordinates needed for drawing a rectangle crisply and return the 2254 * calculated attributes 2255 * @param {Number} strokeWidth 2256 * @param {Number} x 2257 * @param {Number} y 2258 * @param {Number} width 2259 * @param {Number} height 2260 */ 2261 crisp: function (strokeWidth, x, y, width, height) { 2262 2263 var wrapper = this, 2264 key, 2265 attribs = {}, 2266 values = {}, 2267 normalizer; 2268 2269 strokeWidth = strokeWidth || wrapper.strokeWidth || (wrapper.attr && wrapper.attr('stroke-width')) || 0; 2270 normalizer = mathRound(strokeWidth) % 2 / 2; // mathRound because strokeWidth can sometimes have roundoff errors 2271 2272 // normalize for crisp edges 2273 values.x = mathFloor(x || wrapper.x || 0) + normalizer; 2274 values.y = mathFloor(y || wrapper.y || 0) + normalizer; 2275 values.width = mathFloor((width || wrapper.width || 0) - 2 * normalizer); 2276 values.height = mathFloor((height || wrapper.height || 0) - 2 * normalizer); 2277 values.strokeWidth = strokeWidth; 2278 2279 for (key in values) { 2280 if (wrapper[key] !== values[key]) { // only set attribute if changed 2281 wrapper[key] = attribs[key] = values[key]; 2282 } 2283 } 2284 2285 return attribs; 2286 }, 2287 2288 /** 2289 * Set styles for the element 2290 * @param {Object} styles 2291 */ 2292 css: function (styles) { 2293 /*jslint unparam: true*//* allow unused param a in the regexp function below */ 2294 var elemWrapper = this, 2295 elem = elemWrapper.element, 2296 textWidth = styles && styles.width && elem.nodeName === 'text', 2297 n, 2298 serializedCss = '', 2299 hyphenate = function (a, b) { return '-' + b.toLowerCase(); }; 2300 /*jslint unparam: false*/ 2301 2302 // convert legacy 2303 if (styles && styles.color) { 2304 styles.fill = styles.color; 2305 } 2306 2307 // Merge the new styles with the old ones 2308 styles = extend( 2309 elemWrapper.styles, 2310 styles 2311 ); 2312 2313 // store object 2314 elemWrapper.styles = styles; 2315 2316 // serialize and set style attribute 2317 if (isIE && !hasSVG) { // legacy IE doesn't support setting style attribute 2318 if (textWidth) { 2319 delete styles.width; 2320 } 2321 css(elemWrapper.element, styles); 2322 } else { 2323 for (n in styles) { 2324 serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':' + styles[n] + ';'; 2325 } 2326 elemWrapper.attr({ 2327 style: serializedCss 2328 }); 2329 } 2330 2331 2332 // re-build text 2333 if (textWidth && elemWrapper.added) { 2334 elemWrapper.renderer.buildText(elemWrapper); 2335 } 2336 2337 return elemWrapper; 2338 }, 2339 2340 /** 2341 * Add an event listener 2342 * @param {String} eventType 2343 * @param {Function} handler 2344 */ 2345 on: function (eventType, handler) { 2346 var fn = handler; 2347 // touch 2348 if (hasTouch && eventType === 'click') { 2349 eventType = 'touchstart'; 2350 fn = function (e) { 2351 e.preventDefault(); 2352 handler(); 2353 }; 2354 } 2355 // simplest possible event model for internal use 2356 this.element['on' + eventType] = fn; 2357 return this; 2358 }, 2359 2360 2361 /** 2362 * Move an object and its children by x and y values 2363 * @param {Number} x 2364 * @param {Number} y 2365 */ 2366 translate: function (x, y) { 2367 return this.attr({ 2368 translateX: x, 2369 translateY: y 2370 }); 2371 }, 2372 2373 /** 2374 * Invert a group, rotate and flip 2375 */ 2376 invert: function () { 2377 var wrapper = this; 2378 wrapper.inverted = true; 2379 wrapper.updateTransform(); 2380 return wrapper; 2381 }, 2382 2383 /** 2384 * Apply CSS to HTML elements. This is used in text within SVG rendering and 2385 * by the VML renderer 2386 */ 2387 htmlCss: function (styles) { 2388 var wrapper = this, 2389 element = wrapper.element, 2390 textWidth = styles && element.tagName === 'SPAN' && styles.width; 2391 2392 if (textWidth) { 2393 delete styles.width; 2394 wrapper.textWidth = textWidth; 2395 wrapper.updateTransform(); 2396 } 2397 2398 wrapper.styles = extend(wrapper.styles, styles); 2399 css(wrapper.element, styles); 2400 2401 return wrapper; 2402 }, 2403 2404 2405 2406 /** 2407 * VML and useHTML method for calculating the bounding box based on offsets 2408 * @param {Boolean} refresh Whether to force a fresh value from the DOM or to 2409 * use the cached value 2410 * 2411 * @return {Object} A hash containing values for x, y, width and height 2412 */ 2413 2414 htmlGetBBox: function (refresh) { 2415 var wrapper = this, 2416 element = wrapper.element, 2417 bBox = wrapper.bBox; 2418 2419 // faking getBBox in exported SVG in legacy IE 2420 if (!bBox || refresh) { 2421 // faking getBBox in exported SVG in legacy IE 2422 if (element.nodeName === 'text') { 2423 element.style.position = ABSOLUTE; 2424 } 2425 2426 bBox = wrapper.bBox = { 2427 x: element.offsetLeft, 2428 y: element.offsetTop, 2429 width: element.offsetWidth, 2430 height: element.offsetHeight 2431 }; 2432 } 2433
2434 return bBox; 2435 }, 2436 2437 /** 2438 * VML override private method to update elements based on internal 2439 * properties based on SVG transform 2440 */ 2441 htmlUpdateTransform: function () { 2442 // aligning non added elements is expensive 2443 if (!this.added) { 2444 this.alignOnAdd = true; 2445 return; 2446 } 2447 2448 var wrapper = this, 2449 renderer = wrapper.renderer, 2450 elem = wrapper.element, 2451 translateX = wrapper.translateX || 0, 2452 translateY = wrapper.translateY || 0, 2453 x = wrapper.x || 0, 2454 y = wrapper.y || 0, 2455 align = wrapper.textAlign || 'left', 2456 alignCorrection = { left: 0, center: 0.5, right: 1 }[align], 2457 nonLeft = align && align !== 'left', 2458 shadows = wrapper.shadows; 2459 2460 // apply translate 2461 if (translateX || translateY) { 2462 css(elem, { 2463 marginLeft: translateX, 2464 marginTop: translateY 2465 }); 2466 if (shadows) { // used in labels/tooltip 2467 each(shadows, function (shadow) { 2468 css(shadow, { 2469 marginLeft: translateX + 1, 2470 marginTop: translateY + 1 2471 }); 2472 }); 2473 } 2474 } 2475 2476 // apply inversion 2477 if (wrapper.inverted) { // wrapper is a group 2478 each(elem.childNodes, function (child) { 2479 renderer.invertChild(child, elem); 2480 }); 2481 } 2482 2483 if (elem.tagName === 'SPAN') { 2484 2485 var width, height, 2486 rotation = wrapper.rotation, 2487 baseline, 2488 radians = 0, 2489 costheta = 1, 2490 sintheta = 0, 2491 quad, 2492 textWidth = pInt(wrapper.textWidth), 2493 xCorr = wrapper.xCorr || 0, 2494 yCorr = wrapper.yCorr || 0, 2495 currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth].join(','); 2496 2497 if (currentTextTransform !== wrapper.cTT) { // do the calculations and DOM access only if properties changed 2498 2499 if (defined(rotation)) { 2500 radians = rotation * deg2rad; // deg to rad 2501 costheta = mathCos(radians); 2502 sintheta = mathSin(radians); 2503 2504 // Adjust for alignment and rotation. Rotation of useHTML content is not yet implemented 2505 // but it can probably be implemented for Firefox 3.5+ on user request. FF3.5+ 2506 // has support for CSS3 transform. The getBBox method also needs to be updated 2507 // to compensate for the rotation, like it currently does for SVG. 2508 // Test case: http://highcharts.com/tests/?file=text-rotation 2509 css(elem, { 2510 filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta, 2511 ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta, 2512 ', sizingMethod=\'auto expand\')'].join('') : NONE 2513 }); 2514 } 2515 2516 width = pick(wrapper.elemWidth, elem.offsetWidth); 2517 height = pick(wrapper.elemHeight, elem.offsetHeight); 2518 2519 // update textWidth 2520 if (width > textWidth) { 2521 css(elem, { 2522 width: textWidth + PX, 2523 display: 'block', 2524 whiteSpace: 'normal' 2525 }); 2526 width = textWidth; 2527 } 2528 2529 // correct x and y 2530 baseline = renderer.fontMetrics(elem.style.fontSize).b; 2531 xCorr = costheta < 0 && -width; 2532 yCorr = sintheta < 0 && -height; 2533 2534 // correct for baseline and corners spilling out after rotation 2535 quad = costheta * sintheta < 0; 2536 xCorr += sintheta * baseline * (quad ? 1 - alignCorrection : alignCorrection); 2537 yCorr -= costheta * baseline * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1); 2538 2539 // correct for the length/height of the text 2540 if (nonLeft) { 2541 xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1); 2542 if (rotation) { 2543 yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1); 2544 } 2545 css(elem, { 2546 textAlign: align 2547 }); 2548 } 2549 2550 // record correction 2551 wrapper.xCorr = xCorr; 2552 wrapper.yCorr = yCorr; 2553 } 2554 2555 // apply position with correction 2556 css(elem, { 2557 left: (x + xCorr) + PX, 2558 top: (y + yCorr) + PX 2559 }); 2560 2561 // record current text transform 2562 wrapper.cTT = currentTextTransform; 2563 } 2564 }, 2565 2566 /** 2567 * Private method to update the transform attribute based on internal 2568 * properties 2569 */ 2570 updateTransform: function () { 2571 var wrapper = this, 2572 translateX = wrapper.translateX || 0, 2573 translateY = wrapper.translateY || 0, 2574 inverted = wrapper.inverted, 2575 rotation = wrapper.rotation, 2576 transform = []; 2577 2578 // flipping affects translate as adjustment for flipping around the group's axis 2579 if (inverted) { 2580 translateX += wrapper.attr('width'); 2581 translateY += wrapper.attr('height'); 2582 } 2583 2584 // apply translate 2585 if (translateX || translateY) { 2586 transform.push('translate(' + translateX + ',' + translateY + ')'); 2587 } 2588 2589 // apply rotation 2590 if (inverted) { 2591 transform.push('rotate(90) scale(-1,1)'); 2592 } else if (rotation) { // text rotation 2593 transform.push('rotate(' + rotation + ' ' + wrapper.x + ' ' + wrapper.y + ')'); 2594 } 2595 2596 if (transform.length) { 2597 attr(wrapper.element, 'transform', transform.join(' ')); 2598 } 2599 }, 2600 /** 2601 * Bring the element to the front 2602 */ 2603 toFront: function () { 2604 var element = this.element; 2605 element.parentNode.appendChild(element); 2606 return this; 2607 }, 2608 2609 2610 /** 2611 * Break down alignment options like align, verticalAlign, x and y 2612 * to x and y relative to the chart. 2613 * 2614 * @param {Object} alignOptions 2615 * @param {Boolean} alignByTranslate 2616 * @param {Object} box The box to align to, needs a width and height 2617 * 2618 */ 2619 align: function (alignOptions, alignByTranslate, box) { 2620 var elemWrapper = this; 2621 2622 if (!alignOptions) { // called on resize 2623 alignOptions = elemWrapper.alignOptions; 2624 alignByTranslate = elemWrapper.alignByTranslate; 2625 } else { // first call on instanciate 2626 elemWrapper.alignOptions = alignOptions; 2627 elemWrapper.alignByTranslate = alignByTranslate; 2628 if (!box) { // boxes other than renderer handle this internally 2629 elemWrapper.renderer.alignedObjects.push(elemWrapper); 2630 } 2631 } 2632 2633 box = pick(box, elemWrapper.renderer); 2634 2635 var align = alignOptions.align, 2636 vAlign = alignOptions.verticalAlign, 2637 x = (box.x || 0) + (alignOptions.x || 0), // default: left align 2638 y = (box.y || 0) + (alignOptions.y || 0), // default: top align 2639 attribs = {}; 2640 2641 2642 // align 2643 if (/^(right|center)$/.test(align)) { 2644 x += (box.width - (alignOptions.width || 0)) / 2645 { right: 1, center: 2 }[align]; 2646 } 2647 attribs[alignByTranslate ? 'translateX' : 'x'] = mathRound(x); 2648 2649 2650 // vertical align 2651 if (/^(bottom|middle)$/.test(vAlign)) {
2652 y += (box.height - (alignOptions.height || 0)) / 2653 ({ bottom: 1, middle: 2 }[vAlign] || 1); 2654 2655 } 2656 attribs[alignByTranslate ? 'translateY' : 'y'] = mathRound(y); 2657 2658 // animate only if already placed 2659 elemWrapper[elemWrapper.placed ? 'animate' : 'attr'](attribs); 2660 elemWrapper.placed = true; 2661 elemWrapper.alignAttr = attribs; 2662 2663 return elemWrapper; 2664 }, 2665 2666 /** 2667 * Get the bounding box (width, height, x and y) for the element 2668 */ 2669 getBBox: function (refresh) { 2670 var wrapper = this, 2671 bBox, 2672 width, 2673 height, 2674 rotation = wrapper.rotation, 2675 element = wrapper.element, 2676 rad = rotation * deg2rad; 2677 2678 // SVG elements 2679 if (element.namespaceURI === SVG_NS) { 2680 try { // Fails in Firefox if the container has display: none. 2681 2682 bBox = element.getBBox ? 2683 // SVG: use extend because IE9 is not allowed to change width and height in case 2684 // of rotation (below) 2685 extend({}, element.getBBox()) : 2686 // Canvas renderer: // TODO: can this be removed now that we're checking for the SVG NS? 2687 { 2688 width: element.offsetWidth, 2689 height: element.offsetHeight 2690 }; 2691 } catch (e) {} 2692 2693 // If the bBox is not set, the try-catch block above failed. The other condition 2694 // is for Opera that returns a width of -Infinity on hidden elements. 2695 if (!bBox || bBox.width < 0) { 2696 bBox = { width: 0, height: 0 }; 2697 } 2698 2699 width = bBox.width; 2700 height = bBox.height; 2701 2702 // adjust for rotated text 2703 if (rotation) { 2704 bBox.width = mathAbs(height * mathSin(rad)) + mathAbs(width * mathCos(rad)); 2705 bBox.height = mathAbs(height * mathCos(rad)) + mathAbs(width * mathSin(rad)); 2706 } 2707 2708 // VML Renderer or useHTML within SVG 2709 } else { 2710 bBox = wrapper.htmlGetBBox(refresh); 2711 } 2712 2713 return bBox; 2714 }, 2715 2716 /** 2717 * Show the element 2718 */ 2719 show: function () { 2720 return this.attr({ visibility: VISIBLE }); 2721 }, 2722 2723 /** 2724 * Hide the element 2725 */ 2726 hide: function () { 2727 return this.attr({ visibility: HIDDEN }); 2728 }, 2729 2730 /** 2731 * Add the element 2732 * @param {Object|Undefined} parent Can be an element, an element wrapper or undefined 2733 * to append the element to the renderer.box. 2734 */ 2735 add: function (parent) { 2736 2737 var renderer = this.renderer, 2738 parentWrapper = parent || renderer, 2739 parentNode = parentWrapper.element || renderer.box, 2740 childNodes = parentNode.childNodes, 2741 element = this.element, 2742 zIndex = attr(element, 'zIndex'),
2743 otherElement, 2744 otherZIndex, 2745 i, 2746 inserted; 2747 2748 // mark as inverted 2749 this.parentInverted = parent && parent.inverted; 2750 2751 // build formatted text 2752 if (this.textStr !== undefined) { 2753 renderer.buildText(this); 2754 } 2755 2756 // mark the container as having z indexed children 2757 if (zIndex) { 2758 parentWrapper.handleZ = true; 2759 zIndex = pInt(zIndex); 2760 } 2761 2762 // insert according to this and other elements' zIndex 2763 if (parentWrapper.handleZ) { // this element or any of its siblings has a z index 2764 for (i = 0; i < childNodes.length; i++) { 2765 otherElement = childNodes[i]; 2766 otherZIndex = attr(otherElement, 'zIndex'); 2767 if (otherElement !== element && ( 2768 // insert before the first element with a higher zIndex 2769 pInt(otherZIndex) > zIndex || 2770 // if no zIndex given, insert before the first element with a zIndex 2771 (!defined(zIndex) && defined(otherZIndex)) 2772 2773 )) { 2774 parentNode.insertBefore(element, otherElement); 2775 inserted = true; 2776 break; 2777 } 2778 } 2779 } 2780 2781 // default: append at the end 2782 if (!inserted) { 2783 parentNode.appendChild(element); 2784 } 2785 2786 // mark as added 2787 this.added = true; 2788 2789 // fire an event for internal hooks 2790 fireEvent(this, 'add'); 2791 2792 return this; 2793 }, 2794 2795 /** 2796 * Removes a child either by removeChild or move to garbageBin. 2797 * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not. 2798 */ 2799 safeRemoveChild: function (element) { 2800 var parentNode = element.parentNode; 2801 if (parentNode) { 2802 parentNode.removeChild(element); 2803 } 2804 }, 2805 2806 /** 2807 * Destroy the element and element wrapper 2808 */ 2809 destroy: function () { 2810 var wrapper = this, 2811 element = wrapper.element || {}, 2812 shadows = wrapper.shadows, 2813 box = wrapper.box, 2814 key, 2815 i; 2816 2817 // remove events 2818 element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = null; 2819 stop(wrapper); // stop running animations 2820 2821 if (wrapper.clipPath) { 2822 wrapper.clipPath = wrapper.clipPath.destroy(); 2823 } 2824 2825 // Destroy stops in case this is a gradient object 2826 if (wrapper.stops) { 2827 for (i = 0; i < wrapper.stops.length; i++) { 2828 wrapper.stops[i] = wrapper.stops[i].destroy(); 2829 } 2830 wrapper.stops = null; 2831 } 2832 2833 // remove element 2834 wrapper.safeRemoveChild(element); 2835 2836 // destroy shadows 2837 if (shadows) { 2838 each(shadows, function (shadow) { 2839 wrapper.safeRemoveChild(shadow); 2840 }); 2841 } 2842 2843 // destroy label box 2844 if (box) { 2845 box.destroy(); 2846 } 2847 2848 // remove from alignObjects 2849 erase(wrapper.renderer.alignedObjects, wrapper); 2850 2851 for (key in wrapper) { 2852 delete wrapper[key]; 2853 } 2854 2855 return null; 2856 }, 2857 2858 /** 2859 * Empty a group element 2860 */ 2861 empty: function () { 2862 var element = this.element, 2863 childNodes = element.childNodes, 2864 i = childNodes.length; 2865 2866 while (i--) { 2867 element.removeChild(childNodes[i]); 2868 } 2869 }, 2870 2871 /** 2872 * Add a shadow to the element. Must be done after the element is added to the DOM 2873 * @param {Boolean} apply 2874 */ 2875 shadow: function (apply, group) { 2876 var shadows = [], 2877 i, 2878 shadow, 2879 element = this.element, 2880 2881 // compensate for inverted plot area 2882 transform = this.parentInverted ? '(-1,-1)' : '(1,1)'; 2883 2884 2885 if (apply) { 2886 for (i = 1; i <= 3; i++) { 2887 shadow = element.cloneNode(0); 2888 attr(shadow, { 2889 'isShadow': 'true', 2890 'stroke': 'rgb(0, 0, 0)', 2891 'stroke-opacity': 0.05 * i, 2892 'stroke-width': 7 - 2 * i, 2893 'transform': 'translate' + transform, 2894 'fill': NONE 2895 }); 2896 2897 if (group) { 2898 group.element.appendChild(shadow); 2899 } else { 2900 element.parentNode.insertBefore(shadow, element); 2901 } 2902 2903 shadows.push(shadow); 2904 } 2905 2906 this.shadows = shadows; 2907 } 2908 return this; 2909 2910 } 2911}; 2912 2913 2914/** 2915 * The default SVG renderer 2916 */ 2917var SVGRenderer = function () { 2918 this.init.apply(this, arguments); 2919}; 2920SVGRenderer.prototype = { 2921 Element: SVGElement, 2922 2923 /** 2924 * Initialize the SVGRenderer 2925 * @param {Object} container 2926 * @param {Number} width 2927 * @param {Number} height 2928 * @param {Boolean} forExport 2929 */ 2930 init: function (container, width, height, forExport) { 2931 var renderer = this, 2932 loc = location, 2933 boxWrapper; 2934 2935 boxWrapper = renderer.createElement('svg') 2936 .attr({ 2937 xmlns: SVG_NS, 2938 version: '1.1' 2939 }); 2940 container.appendChild(boxWrapper.element); 2941 2942 // object properties 2943 renderer.isSVG = true; 2944 renderer.box = boxWrapper.element; 2945 renderer.boxWrapper = boxWrapper; 2946 renderer.alignedObjects = []; 2947 renderer.url = isIE ? '' : loc.href.replace(/#.*?$/, '') 2948 .replace(/([\('\)])/g, '\\$1'); // Page url used for internal references. #24, #672. 2949 renderer.defs = this.createElement('defs').add(); 2950 renderer.forExport = forExport; 2951 renderer.gradients = {}; // Object where gradient SvgElements are stored 2952 2953 renderer.setSize(width, height, false); 2954 }, 2955 2956 /** 2957 * Destroys the renderer and its allocated members. 2958 */ 2959 destroy: function () { 2960 var renderer = this, 2961 rendererDefs = renderer.defs; 2962 renderer.box = null; 2963 renderer.boxWrapper = renderer.boxWrapper.destroy(); 2964 2965 // Call destroy on all gradient elements 2966 destroyObjectProperties(renderer.gradients || {}); 2967 renderer.gradients = null; 2968 2969 // Defs are null in VMLRenderer 2970 // Otherwise, destroy them here. 2971 if (rendererDefs) { 2972 renderer.defs = rendererDefs.destroy(); 2973 } 2974 2975 renderer.alignedObjects = null; 2976 2977 return null; 2978 }, 2979 2980 /** 2981 * Create a wrapper for an SVG element 2982 * @param {Object} nodeName 2983 */ 2984 createElement: function (nodeName) { 2985 var wrapper = new this.Element(); 2986 wrapper.init(this, nodeName); 2987 return wrapper; 2988 }, 2989 2990 /** 2991 * Dummy function for use in canvas renderer 2992 */ 2993 draw: function () {}, 2994 2995 /** 2996 * Parse a simple HTML string into SVG tspans 2997 * 2998 * @param {Object} textNode The parent text SVG node 2999 */ 3000 buildText: function (wrapper) { 3001 var textNode = wrapper.element, 3002 lines = pick(wrapper.textStr, '').toString() 3003 .replace(/<(b|strong)>/g, '<span style="font-weight:bold">') 3004 .replace(/<(i|em)>/g, '<span style="font-style:italic">') 3005 .replace(/<a/g, '<span') 3006 .replace(/<\/(b|strong|i|em|a)>/g, '</span>') 3007 .split(/<br.*?>/g), 3008 childNodes = textNode.childNodes, 3009 styleRegex = /style="([^"]+)"/, 3010 hrefRegex = /href="([^"]+)"/, 3011 parentX = attr(textNode, 'x'), 3012 textStyles = wrapper.styles, 3013 width = textStyles && pInt(textStyles.width), 3014 textLineHeight = textStyles && textStyles.lineHeight, 3015 lastLine, 3016 GET_COMPUTED_STYLE = 'getComputedStyle', 3017 i = childNodes.length; 3018 3019 // remove old text 3020 while (i--) { 3021 textNode.removeChild(childNodes[i]); 3022 } 3023 3024 if (width && !wrapper.added) { 3025 this.box.appendChild(textNode); // attach it to the DOM to read offset width 3026 } 3027 3028 // remove empty line at end 3029 if (lines[lines.length - 1] === '') { 3030 lines.pop(); 3031 } 3032 3033 // build the lines 3034 each(lines, function (line, lineNo) { 3035 var spans, spanNo = 0, lineHeight; 3036 3037 line = line.replace(/<span/g, '|||<span').replace(/<\/span>/g, '</span>|||'); 3038 spans = line.split('|||'); 3039 3040 each(spans, function (span) { 3041 if (span !== '' || spans.length === 1) { 3042 var attributes = {}, 3043 tspan = doc.createElementNS(SVG_NS, 'tspan'); 3044 if (styleRegex.test(span)) { 3045 attr( 3046 tspan, 3047 'style', 3048 span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2') 3049 ); 3050 } 3051 if (hrefRegex.test(span)) { 3052 attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"'); 3053 css(tspan, { cursor: 'pointer' }); 3054 } 3055 3056 span = (span.replace(/<(.|\n)*?>/g, '') || ' ') 3057 .replace(/</g, '<') 3058 .replace(/>/g, '>'); 3059 3060 // issue #38 workaround. 3061 /*if (reverse) { 3062 arr = []; 3063 i = span.length; 3064 while (i--) { 3065 arr.push(span.charAt(i)); 3066 } 3067 span = arr.join(''); 3068 }*/ 3069 3070 // add the text node 3071 tspan.appendChild(doc.createTextNode(span)); 3072 3073 if (!spanNo) { // first span in a line, align it to the left 3074 attributes.x = parentX; 3075 } else { 3076 // Firefox ignores spaces at the front or end of the tspan 3077 attributes.dx = 3; // space 3078 } 3079 3080 // first span on subsequent line, add the line height 3081 if (!spanNo) { 3082 if (lineNo) { 3083 3084 // allow getting the right offset height in exporting in IE 3085 if (!hasSVG && wrapper.renderer.forExport) { 3086 css(tspan, { display: 'block' }); 3087 } 3088 3089 // Webkit and opera sometimes return 'normal' as the line height. In that 3090 // case, webkit uses offsetHeight, while Opera falls back to 18 3091 lineHeight = win[GET_COMPUTED_STYLE] && 3092 pInt(win[GET_COMPUTED_STYLE](lastLine, null).getPropertyValue('line-height')); 3093 3094 if (!lineHeight || isNaN(lineHeight)) { 3095 lineHeight = textLineHeight || lastLine.offsetHeight || 18; 3096 } 3097 attr(tspan, 'dy', lineHeight); 3098 } 3099 lastLine = tspan; // record for use in next line 3100 } 3101 3102 // add attributes 3103 attr(tspan, attributes); 3104 3105 // append it 3106 textNode.appendChild(tspan); 3107 3108 spanNo++; 3109 3110 // check width and apply soft breaks 3111 if (width) { 3112 var words = span.replace(/-/g, '- ').split(' '), 3113 tooLong, 3114 actualWidth, 3115 rest = []; 3116 3117 while (words.length || rest.length) { 3118 actualWidth = wrapper.getBBox().width; 3119 tooLong = actualWidth > width; 3120 if (!tooLong || words.length === 1) { // new line needed 3121 words = rest; 3122 rest = []; 3123 if (words.length) { 3124 tspan = doc.createElementNS(SVG_NS, 'tspan'); 3125 attr(tspan, { 3126 dy: textLineHeight || 16, 3127 x: parentX 3128 }); 3129 textNode.appendChild(tspan); 3130 3131 if (actualWidth > width) { // a single word is pressing it out 3132 width = actualWidth; 3133 } 3134 } 3135 } else { // append to existing line tspan 3136 tspan.removeChild(tspan.firstChild); 3137 rest.unshift(words.pop()); 3138 } 3139 if (words.length) { 3140 tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-'))); 3141 } 3142 } 3143 } 3144 } 3145 }); 3146 }); 3147 }, 3148 3149 /** 3150 * Create a button with preset states 3151 * @param {String} text 3152 * @param {Number} x 3153 * @param {Number} y 3154 * @param {Function} callback 3155 * @param {Object} normalState 3156 * @param {Object} hoverState 3157 * @param {Object} pressedState 3158 */ 3159 button: function (text, x, y, callback, normalState, hoverState, pressedState) { 3160 var label = this.label(text, x, y), 3161 curState = 0, 3162 stateOptions, 3163 stateStyle, 3164 normalStyle, 3165 hoverStyle, 3166 pressedStyle, 3167 STYLE = 'style', 3168 verticalGradient = { x1: 0, y1: 0, x2: 0, y2: 1 }; 3169 3170 // prepare the attributes 3171 /*jslint white: true*/ 3172 normalState = merge(hash( 3173 STROKE_WIDTH, 1, 3174 STROKE, '#999', 3175 FILL, hash( 3176 LINEAR_GRADIENT, verticalGradient, 3177 STOPS, [ 3178 [0, '#FFF'], 3179 [1, '#DDD'] 3180 ] 3181 ), 3182 'r', 3, 3183 'padding', 3, 3184 STYLE, hash( 3185 'color', 'black' 3186 ) 3187 ), normalState); 3188 /*jslint white: false*/ 3189 normalStyle = normalState[STYLE]; 3190 delete normalState[STYLE]; 3191 3192 /*jslint white: true*/ 3193 hoverState = merge(normalState, hash( 3194 STROKE, '#68A', 3195 FILL, hash( 3196 LINEAR_GRADIENT, verticalGradient, 3197 STOPS, [ 3198 [0, '#FFF'], 3199 [1, '#ACF'] 3200 ] 3201 ) 3202 ), hoverState); 3203 /*jslint white: false*/ 3204 hoverStyle = hoverState[STYLE]; 3205 delete hoverState[STYLE]; 3206 3207 /*jslint white: true*/ 3208 pressedState = merge(normalState, hash( 3209 STROKE, '#68A', 3210 FILL, hash( 3211 LINEAR_GRADIENT, verticalGradient, 3212 STOPS, [ 3213 [0, '#9BD'], 3214 [1, '#CDF'] 3215 ] 3216 ) 3217 ), pressedState); 3218 /*jslint white: false*/ 3219 pressedStyle = pressedState[STYLE]; 3220 delete pressedState[STYLE]; 3221 3222 // add the events 3223 addEvent(label.element, 'mouseenter', function () { 3224 label.attr(hoverState) 3225 .css(hoverStyle); 3226 }); 3227 addEvent(label.element, 'mouseleave', function () { 3228 stateOptions = [normalState, hoverState, pressedState][curState]; 3229 stateStyle = [normalStyle, hoverStyle, pressedStyle][curState]; 3230 label.attr(stateOptions) 3231 .css(stateStyle); 3232 }); 3233 3234 label.setState = function (state) { 3235 curState = state; 3236 if (!state) { 3237 label.attr(normalState) 3238 .css(normalStyle); 3239 } else if (state === 2) { 3240 label.attr(pressedState) 3241 .css(pressedStyle); 3242 } 3243 }; 3244 3245 return label 3246 .on('click', function () { 3247 callback.call(label); 3248 }) 3249 .attr(normalState) 3250 .css(extend({ cursor: 'default' }, normalStyle)); 3251 }, 3252 3253 /** 3254 * Make a straight line crisper by not spilling out to neighbour pixels 3255 * @param {Array} points 3256 * @param {Number} width 3257 */ 3258 crispLine: function (points, width) { 3259 // points format: [M, 0, 0, L, 100, 0] 3260 // normalize to a crisp line 3261 if (points[1] === points[4]) { 3262 points[1] = points[4] = mathRound(points[1]) + (width % 2 / 2); 3263 } 3264 if (points[2] === points[5]) { 3265 points[2] = points[5] = mathRound(points[2]) + (width % 2 / 2); 3266 } 3267 return points; 3268 }, 3269 3270 3271 /** 3272 * Draw a path 3273 * @param {Array} path An SVG path in array form 3274 */ 3275 path: function (path) { 3276 return this.createElement('path').attr({ 3277 d: path, 3278 fill: NONE 3279 }); 3280 }, 3281 3282 /** 3283 * Draw and return an SVG circle 3284 * @param {Number} x The x position 3285 * @param {Number} y The y position 3286 * @param {Number} r The radius 3287 */ 3288 circle: function (x, y, r) { 3289 var attr = isObject(x) ? 3290 x : 3291 { 3292 x: x, 3293 y: y, 3294 r: r 3295 }; 3296 3297 return this.createElement('circle').attr(attr); 3298 }, 3299 3300 /** 3301 * Draw and return an arc 3302 * @param {Number} x X position 3303 * @param {Number} y Y position 3304 * @param {Number} r Radius 3305 * @param {Number} innerR Inner radius like used in donut charts 3306 * @param {Number} start Starting angle 3307 * @param {Number} end Ending angle 3308 */ 3309 arc: function (x, y, r, innerR, start, end) { 3310 // arcs are defined as symbols for the ability to set 3311 // attributes in attr and animate 3312 3313 if (isObject(x)) { 3314 y = x.y; 3315 r = x.r; 3316 innerR = x.innerR; 3317 start = x.start; 3318 end = x.end; 3319 x = x.x; 3320 } 3321 return this.symbol('arc', x || 0, y || 0, r || 0, r || 0, { 3322 innerR: innerR || 0, 3323 start: start || 0, 3324 end: end || 0 3325 }); 3326 }, 3327 3328 /** 3329 * Draw and return a rectangle 3330 * @param {Number} x Left position 3331 * @param {Number} y Top position 3332 * @param {Number} width 3333 * @param {Number} height 3334 * @param {Number} r Border corner radius 3335 * @param {Number} strokeWidth A stroke width can be supplied to allow crisp drawing 3336 */ 3337 rect: function (x, y, width, height, r, strokeWidth) { 3338 if (isObject(x)) { 3339 y = x.y; 3340 width = x.width; 3341 height = x.height; 3342 r = x.r; 3343 strokeWidth = x.strokeWidth; 3344 x = x.x; 3345 } 3346 var wrapper = this.createElement('rect').attr({ 3347 rx: r, 3348 ry: r, 3349 fill: NONE 3350 }); 3351 3352 return wrapper.attr(wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0))); 3353 }, 3354 3355 /** 3356 * Resize the box and re-align all aligned elements 3357 * @param {Object} width 3358 * @param {Object} height 3359 * @param {Boolean} animate 3360 * 3361 */
3362 setSize: function (width, height, animate) { 3363 var renderer = this, 3364 alignedObjects = renderer.alignedObjects, 3365 i = alignedObjects.length; 3366 3367 renderer.width = width; 3368 renderer.height = height; 3369 3370 renderer.boxWrapper[pick(animate, true) ? 'animate' : 'attr']({ 3371 width: width, 3372 height: height 3373 }); 3374 3375 while (i--) { 3376 alignedObjects[i].align(); 3377 } 3378 }, 3379 3380 /** 3381 * Create a group 3382 * @param {String} name The group will be given a class name of 'highcharts-{name}'. 3383 * This can be used for styling and scripting. 3384 */ 3385 g: function (name) { 3386 var elem = this.createElement('g'); 3387 return defined(name) ? elem.attr({ 'class': PREFIX + name }) : elem; 3388 }, 3389 3390 /** 3391 * Display an image 3392 * @param {String} src 3393 * @param {Number} x 3394 * @param {Number} y 3395 * @param {Number} width 3396 * @param {Number} height 3397 */ 3398 image: function (src, x, y, width, height) { 3399 var attribs = { 3400 preserveAspectRatio: NONE 3401 }, 3402 elemWrapper; 3403 3404 // optional properties 3405 if (arguments.length > 1) { 3406 extend(attribs, { 3407 x: x, 3408 y: y, 3409 width: width, 3410 height: height 3411 }); 3412 } 3413 3414 elemWrapper = this.createElement('image').attr(attribs); 3415 3416 // set the href in the xlink namespace 3417 if (elemWrapper.element.setAttributeNS) { 3418 elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink', 3419 'href', src); 3420 } else { 3421 // could be exporting in IE 3422 // using href throws "not supported" in ie7 and under, requries regex shim to fix later 3423 elemWrapper.element.setAttribute('hc-svg-href', src); 3424 } 3425 3426 return elemWrapper; 3427 }, 3428 3429 /** 3430 * Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object. 3431 * 3432 * @param {Object} symbol 3433 * @param {Object} x 3434 * @param {Object} y 3435 * @param {Object} radius 3436 * @param {Object} options 3437 */ 3438 symbol: function (symbol, x, y, width, height, options) { 3439 3440 var obj, 3441 3442 // get the symbol definition function 3443 symbolFn = this.symbols[symbol], 3444 3445 // check if there's a path defined for this symbol 3446 path = symbolFn && symbolFn( 3447 mathRound(x), 3448 mathRound(y), 3449 width, 3450 height, 3451 options 3452 ), 3453 3454 imageRegex = /^url\((.*?)\)$/, 3455 imageSrc, 3456 imageSize; 3457 3458 if (path) { 3459 3460 obj = this.path(path); 3461 // expando properties for use in animate and attr 3462 extend(obj, { 3463 symbolName: symbol, 3464 x: x, 3465 y: y, 3466 width: width, 3467 height: height 3468 }); 3469 if (options) { 3470 extend(obj, options); 3471 } 3472 3473 3474 // image symbols 3475 } else if (imageRegex.test(symbol)) { 3476 3477 var centerImage = function (img, size) { 3478 img.attr({ 3479 width: size[0], 3480 height: size[1] 3481 }).translate( 3482 -mathRound(size[0] / 2), 3483 -mathRound(size[1] / 2) 3484 ); 3485 }; 3486 3487 imageSrc = symbol.match(imageRegex)[1]; 3488 imageSize = symbolSizes[imageSrc]; 3489 3490 // create the image synchronously, add attribs async 3491 obj = this.image(imageSrc) 3492 .attr({ 3493 x: x, 3494 y: y 3495 }); 3496 3497 if (imageSize) { 3498 centerImage(obj, imageSize); 3499 } else { 3500 // initialize image to be 0 size so export will still function if there's no cached sizes 3501 obj.attr({ width: 0, height: 0 }); 3502 3503 // create a dummy JavaScript image to get the width and height 3504 createElement('img', { 3505 onload: function () { 3506 var img = this; 3507 3508 centerImage(obj, symbolSizes[imageSrc] = [img.width, img.height]); 3509 }, 3510 src: imageSrc 3511 }); 3512 } 3513 } 3514 3515 return obj; 3516 }, 3517 3518 /** 3519 * An extendable collection of functions for defining symbol paths. 3520 */ 3521 symbols: { 3522 'circle': function (x, y, w, h) { 3523 var cpw = 0.166 * w; 3524 return [ 3525 M, x + w / 2, y, 3526 'C', x + w + cpw, y, x + w + cpw, y + h, x + w / 2, y + h, 3527 'C', x - cpw, y + h, x - cpw, y, x + w / 2, y, 3528 'Z' 3529 ]; 3530 }, 3531 3532 'square': function (x, y, w, h) { 3533 return [ 3534 M, x, y, 3535 L, x + w, y, 3536 x + w, y + h, 3537 x, y + h, 3538 'Z' 3539 ]; 3540 }, 3541 3542 'triangle': function (x, y, w, h) { 3543 return [ 3544 M, x + w / 2, y, 3545 L, x + w, y + h, 3546 x, y + h, 3547 'Z' 3548 ]; 3549 }, 3550 3551 'triangle-down': function (x, y, w, h) { 3552 return [ 3553 M, x, y, 3554 L, x + w, y, 3555 x + w / 2, y + h, 3556 'Z' 3557 ]; 3558 }, 3559 'diamond': function (x, y, w, h) { 3560 return [ 3561 M, x + w / 2, y, 3562 L, x + w, y + h / 2, 3563 x + w / 2, y + h, 3564 x, y + h / 2, 3565 'Z' 3566 ]; 3567 }, 3568 'arc': function (x, y, w, h, options) { 3569 var start = options.start, 3570 radius = options.r || w || h, 3571 end = options.end - 0.000001, // to prevent cos and sin of start and end from becoming equal on 360 arcs 3572 innerRadius = options.innerR, 3573 cosStart = mathCos(start), 3574 sinStart = mathSin(start), 3575 cosEnd = mathCos(end), 3576 sinEnd = mathSin(end), 3577 longArc = options.end - start < mathPI ? 0 : 1; 3578 3579 return [ 3580 M, 3581 x + radius * cosStart, 3582 y + radius * sinStart, 3583 'A', // arcTo 3584 radius, // x radius
3585 radius, // y radius 3586 0, // slanting 3587 longArc, // long or short arc 3588 1, // clockwise 3589 x + radius * cosEnd, 3590 y + radius * sinEnd, 3591 L, 3592 x + innerRadius * cosEnd, 3593 y + innerRadius * sinEnd, 3594 'A', // arcTo 3595 innerRadius, // x radius 3596 innerRadius, // y radius 3597 0, // slanting 3598 longArc, // long or short arc 3599 0, // clockwise 3600 x + innerRadius * cosStart, 3601 y + innerRadius * sinStart, 3602 3603 'Z' // close 3604 ]; 3605 } 3606 }, 3607 3608 /** 3609 * Define a clipping rectangle 3610 * @param {String} id 3611 * @param {Number} x 3612 * @param {Number} y 3613 * @param {Number} width 3614 * @param {Number} height 3615 */ 3616 clipRect: function (x, y, width, height) { 3617 var wrapper, 3618 id = PREFIX + idCounter++, 3619 3620 clipPath = this.createElement('clipPath').attr({ 3621 id: id 3622 }).add(this.defs); 3623 3624 wrapper = this.rect(x, y, width, height, 0).add(clipPath); 3625 wrapper.id = id; 3626 wrapper.clipPath = clipPath; 3627 3628 return wrapper; 3629 }, 3630 3631 3632 /** 3633 * Take a color and return it if it's a string, make it a gradient if it's a 3634 * gradient configuration object. Prior to Highstock, an array was used to define 3635 * a linear gradient with pixel positions relative to the SVG. In newer versions 3636 * we change the coordinates to apply relative to the shape, using coordinates 3637 * 0-1 within the shape. To preserve backwards compatibility, linearGradient 3638 * in this definition is an object of x1, y1, x2 and y2. 3639 * 3640 * @param {Object} color The color or config object 3641 */ 3642 color: function (color, elem, prop) { 3643 var colorObject, 3644 regexRgba = /^rgba/; 3645 if (color && color.linearGradient) { 3646 var renderer = this, 3647 linearGradient = color[LINEAR_GRADIENT], 3648 relativeToShape = !isArray(linearGradient), // keep backwards compatibility 3649 id, 3650 gradients = renderer.gradients, 3651 gradientObject, 3652 x1 = linearGradient.x1 || linearGradient[0] || 0, 3653 y1 = linearGradient.y1 || linearGradient[1] || 0, 3654 x2 = linearGradient.x2 || linearGradient[2] || 0, 3655 y2 = linearGradient.y2 || linearGradient[3] || 0, 3656 stopColor, 3657 stopOpacity, 3658 // Create a unique key in order to reuse gradient objects. #671. 3659 key = [relativeToShape, x1, y1, x2, y2, color.stops.join(',')].join(','); 3660 3661 // If the gradient with the same setup is already created, reuse it 3662 if (gradients[key]) { 3663 id = attr(gradients[key].element, 'id'); 3664 3665 // If not, create a new one and keep the reference. 3666 } else { 3667 id = PREFIX + idCounter++; 3668 gradientObject = renderer.createElement(LINEAR_GRADIENT) 3669 .attr(extend({ 3670 id: id, 3671 x1: x1, 3672 y1: y1, 3673 x2: x2, 3674 y2: y2 3675 }, relativeToShape ? null : { gradientUnits: 'userSpaceOnUse' })) 3676 .add(renderer.defs); 3677 3678 // The gradient needs to keep a list of stops to be able to destroy them 3679 gradientObject.stops = []; 3680 each(color.stops, function (stop) { 3681 var stopObject; 3682 if (regexRgba.test(stop[1])) { 3683 colorObject = Color(stop[1]); 3684 stopColor = colorObject.get('rgb'); 3685 stopOpacity = colorObject.get('a'); 3686 } else { 3687 stopColor = stop[1]; 3688 stopOpacity = 1; 3689 } 3690 stopObject = renderer.createElement('stop').attr({ 3691 offset: stop[0], 3692 'stop-color': stopColor, 3693 'stop-opacity': stopOpacity 3694 }).add(gradientObject); 3695 3696 // Add the stop element to the gradient 3697 gradientObject.stops.push(stopObject); 3698 }); 3699 3700 // Keep a reference to the gradient object so it is possible to reuse it and 3701 // destroy it later 3702 gradients[key] = gradientObject; 3703 } 3704 3705 return 'url(' + this.url + '#' + id + ')'; 3706 3707 // Webkit and Batik can't show rgba. 3708 } else if (regexRgba.test(color)) { 3709 colorObject = Color(color); 3710 attr(elem, prop + '-opacity', colorObject.get('a')); 3711 3712 return colorObject.get('rgb'); 3713 3714 3715 } else { 3716 // Remove the opacity attribute added above. Does not throw if the attribute is not there. 3717 elem.removeAttribute(prop + '-opacity'); 3718 3719 return color; 3720 } 3721 3722 }, 3723 3724 3725 /** 3726 * Add text to the SVG object 3727 * @param {String} str 3728 * @param {Number} x Left position 3729 * @param {Number} y Top position 3730 * @param {Boolean} useHTML Use HTML to render the text 3731 */ 3732 text: function (str, x, y, useHTML) { 3733 3734 // declare variables 3735 var renderer = this, 3736 defaultChartStyle = defaultOptions.chart.style, 3737 wrapper; 3738 3739 if (useHTML && !renderer.forExport) { 3740 return renderer.html(str, x, y); 3741 } 3742 3743 x = mathRound(pick(x, 0)); 3744 y = mathRound(pick(y, 0)); 3745 3746 wrapper = renderer.createElement('text') 3747 .attr({ 3748 x: x, 3749 y: y, 3750 text: str 3751 }) 3752 .css({ 3753 fontFamily: defaultChartStyle.fontFamily, 3754 fontSize: defaultChartStyle.fontSize 3755 }); 3756 3757 wrapper.x = x; 3758 wrapper.y = y; 3759 return wrapper; 3760 }, 3761 3762 3763 /** 3764 * Create HTML text node. This is used by the VML renderer as well as the SVG 3765 * renderer through the useHTML option. 3766 * 3767 * @param {String} str 3768 * @param {Number} x 3769 * @param {Number} y 3770 */ 3771 html: function (str, x, y) { 3772 var defaultChartStyle = defaultOptions.chart.style, 3773 wrapper = this.createElement('span'), 3774 attrSetters = wrapper.attrSetters, 3775 element = wrapper.element, 3776 renderer = wrapper.renderer; 3777 3778 // Text setter 3779 attrSetters.text = function (value) { 3780 element.innerHTML = value; 3781 return false;
3782 }; 3783 3784 // Various setters which rely on update transform 3785 attrSetters.x = attrSetters.y = attrSetters.align = function (value, key) { 3786 if (key === 'align') { 3787 key = 'textAlign'; // Do not overwrite the SVGElement.align method. Same as VML. 3788 } 3789 wrapper[key] = value; 3790 wrapper.htmlUpdateTransform(); 3791 return false; 3792 }; 3793 3794 // Set the default attributes 3795 wrapper.attr({ 3796 text: str, 3797 x: mathRound(x), 3798 y: mathRound(y) 3799 }) 3800 .css({ 3801 position: ABSOLUTE, 3802 whiteSpace: 'nowrap', 3803 fontFamily: defaultChartStyle.fontFamily, 3804 fontSize: defaultChartStyle.fontSize 3805 }); 3806 3807 // Use the HTML specific .css method 3808 wrapper.css = wrapper.htmlCss; 3809 3810 // This is specific for HTML within SVG 3811 if (renderer.isSVG) { 3812 wrapper.add = function (svgGroupWrapper) { 3813 3814 var htmlGroup, 3815 htmlGroupStyle, 3816 container = renderer.box.parentNode; 3817 3818 // Create a mock group to hold the HTML elements 3819 if (svgGroupWrapper) { 3820 htmlGroup = svgGroupWrapper.div; 3821 if (!htmlGroup) { 3822 htmlGroup = svgGroupWrapper.div = createElement(DIV, { 3823 className: attr(svgGroupWrapper.element, 'class') 3824 }, { 3825 position: ABSOLUTE, 3826 left: svgGroupWrapper.attr('translateX') + PX, 3827 top: svgGroupWrapper.attr('translateY') + PX 3828 }, container); 3829 3830 // Ensure dynamic updating position 3831 htmlGroupStyle = htmlGroup.style; 3832 extend(svgGroupWrapper.attrSetters, { 3833 translateX: function (value) { 3834 htmlGroupStyle.left = value + PX; 3835 }, 3836 translateY: function (value) { 3837 htmlGroupStyle.top = value + PX; 3838 }, 3839 visibility: function (value, key) { 3840 htmlGroupStyle[key] = value; 3841 } 3842 }); 3843 3844 } 3845 } else { 3846 htmlGroup = container; 3847 } 3848 3849 htmlGroup.appendChild(element); 3850 3851 // Shared with VML: 3852 wrapper.added = true; 3853 if (wrapper.alignOnAdd) { 3854 wrapper.htmlUpdateTransform(); 3855 } 3856 3857 return wrapper; 3858 }; 3859 } 3860 return wrapper; 3861 }, 3862 3863 /** 3864 * Utility to return the baseline offset and total line height from the font size 3865 */ 3866 fontMetrics: function (fontSize) { 3867 fontSize = pInt(fontSize || 11); 3868 3869 // Empirical values found by comparing font size and bounding box height. 3870 // Applies to the default font family. http://jsfiddle.net/highcharts/7xvn7/ 3871 var lineHeight = fontSize < 24 ? fontSize + 4 : mathRound(fontSize * 1.2), 3872 baseline = mathRound(lineHeight * 0.8); 3873 3874 return { 3875 h: lineHeight, 3876 b: baseline 3877 }; 3878 }, 3879 3880 /** 3881 * Add a label, a text item that can hold a colored or gradient background 3882 * as well as a border and shadow. 3883 * @param {string} str 3884 * @param {Number} x 3885 * @param {Number} y 3886 * @param {String} shape 3887 * @param {Number} anchorX In case the shape has a pointer, like a flag, this is the 3888 * coordinates it should be pinned to 3889 * @param {Number} anchorY 3890 * @param {Boolean} baseline Whether to position the label relative to the text baseline, 3891 * like renderer.text, or to the upper border of the rectangle. 3892 */ 3893 label: function (str, x, y, shape, anchorX, anchorY, useHTML, baseline) { 3894 3895 var renderer = this, 3896 wrapper = renderer.g(), 3897 text = renderer.text('', 0, 0, useHTML) 3898 .attr({ 3899 zIndex: 1 3900 }) 3901 .add(wrapper), 3902 box, 3903 bBox, 3904 align = 'left', 3905 padding = 3, 3906 width, 3907 height, 3908 wrapperX, 3909 wrapperY, 3910 crispAdjust = 0, 3911 deferredAttr = {}, 3912 baselineOffset, 3913 attrSetters = wrapper.attrSetters; 3914 3915 /** 3916 * This function runs after the label is added to the DOM (when the bounding box is 3917 * available), and after the text of the label is updated to detect the new bounding 3918 * box and reflect it in the border box. 3919 */ 3920 function updateBoxSize() { 3921 var boxY, 3922 style = text.element.style; 3923 3924 bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) && 3925 text.getBBox(true); 3926 wrapper.width = (width || bBox.width) + 2 * padding; 3927 wrapper.height = (height || bBox.height) + 2 * padding; 3928 3929 // update the label-scoped y offset 3930 baselineOffset = padding + renderer.fontMetrics(style && style.fontSize).b; 3931 3932 3933 // create the border box if it is not already present 3934 if (!box) { 3935 boxY = baseline ? -baselineOffset : 0; 3936 3937 wrapper.box = box = shape ? 3938 renderer.symbol(shape, 0, boxY, wrapper.width, wrapper.height) : 3939 renderer.rect(0, boxY, wrapper.width, wrapper.height, 0, deferredAttr[STROKE_WIDTH]); 3940 box.add(wrapper); 3941 } 3942 3943 // apply the box attributes 3944 box.attr(merge({ 3945 width: wrapper.width, 3946 height: wrapper.height 3947 }, deferredAttr)); 3948 deferredAttr = null; 3949 } 3950 3951 /** 3952 * This function runs after setting text or padding, but only if padding is changed 3953 */ 3954 function updateTextPadding() { 3955 var styles = wrapper.styles, 3956 textAlign = styles && styles.textAlign, 3957 x = padding, 3958 y; 3959 3960 // determin y based on the baseline 3961 y = baseline ? 0 : baselineOffset; 3962 3963 // compensate for alignment 3964 if (defined(width) && (textAlign === 'center' || textAlign === 'right')) { 3965 x += { center: 0.5, right: 1 }[textAlign] * (width - bBox.width); 3966 } 3967 3968 // update if anything changed 3969 if (x !== text.x || y !== text.y) { 3970 text.attr({ 3971 x: x, 3972 y: y 3973 }); 3974 } 3975 3976 // record current values 3977 text.x = x; 3978 text.y = y; 3979 } 3980 3981 /** 3982 * Set a box attribute, or defer it if the box is not yet created 3983 * @param {Object} key 3984 * @param {Object} value 3985 */ 3986 function boxAttr(key, value) { 3987 if (box) { 3988 box.attr(key, value); 3989 } else { 3990 deferredAttr[key] = value; 3991 } 3992 } 3993 3994 function getSizeAfterAdd() { 3995 wrapper.attr({ 3996 text: str, // alignment is available now 3997 x: x, 3998 y: y, 3999 anchorX: anchorX, 4000 anchorY: anchorY 4001 }); 4002 } 4003 4004 /** 4005 * After the text element is added, get the desired size of the border box 4006 * and add it before the text in the DOM. 4007 */ 4008 addEvent(wrapper, 'add', getSizeAfterAdd); 4009 4010 /* 4011 * Add specific attribute setters. 4012 */ 4013 4014 // only change local variables 4015 attrSetters.width = function (value) { 4016 width = value; 4017 return false;
4018 }; 4019 attrSetters.height = function (value) { 4020 height = value; 4021 return false; 4022 }; 4023 attrSetters.padding = function (value) { 4024 if (defined(value) && value !== padding) { 4025 padding = value; 4026 updateTextPadding(); 4027 } 4028 4029 return false; 4030 }; 4031 4032 // change local variable and set attribue as well 4033 attrSetters.align = function (value) { 4034 align = value; 4035 return false; // prevent setting text-anchor on the group 4036 }; 4037 4038 // apply these to the box and the text alike 4039 attrSetters.text = function (value, key) { 4040 text.attr(key, value); 4041 updateBoxSize(); 4042 updateTextPadding(); 4043 return false; 4044 }; 4045 4046 // apply these to the box but not to the text 4047 attrSetters[STROKE_WIDTH] = function (value, key) { 4048 crispAdjust = value % 2 / 2; 4049 boxAttr(key, value); 4050 return false; 4051 }; 4052 attrSetters.stroke = attrSetters.fill = attrSetters.r = function (value, key) { 4053 boxAttr(key, value); 4054 return false; 4055 }; 4056 attrSetters.anchorX = function (value, key) { 4057 anchorX = value; 4058 boxAttr(key, value + crispAdjust - wrapperX); 4059 return false; 4060 }; 4061 attrSetters.anchorY = function (value, key) { 4062 anchorY = value; 4063 boxAttr(key, value - wrapperY); 4064 return false; 4065 }; 4066
4067 // rename attributes 4068 attrSetters.x = function (value) { 4069 value -= { left: 0, center: 0.5, right: 1 }[align] * ((width || bBox.width) + padding); 4070 wrapperX = wrapper.x = mathRound(value); // wrapper.x is for animation getter 4071 4072 wrapper.attr('translateX', wrapperX); 4073 return false; 4074 }; 4075 attrSetters.y = function (value) { 4076 wrapperY = wrapper.y = mathRound(value); 4077 wrapper.attr('translateY', value); 4078 return false; 4079 }; 4080 4081 // Redirect certain methods to either the box or the text 4082 var baseCss = wrapper.css; 4083 return extend(wrapper, { 4084 /** 4085 * Pick up some properties and apply them to the text instead of the wrapper 4086 */ 4087 css: function (styles) { 4088 if (styles) { 4089 var textStyles = {}; 4090 styles = merge({}, styles); // create a copy to avoid altering the original object (#537) 4091 each(['fontSize', 'fontWeight', 'fontFamily', 'color', 'lineHeight', 'width'], function (prop) { 4092 if (styles[prop] !== UNDEFINED) { 4093 textStyles[prop] = styles[prop]; 4094 delete styles[prop]; 4095 } 4096 }); 4097 text.css(textStyles); 4098 } 4099 return baseCss.call(wrapper, styles); 4100 }, 4101 /** 4102 * Return the bounding box of the box, not the group 4103 */ 4104 getBBox: function () { 4105 return box.getBBox(); 4106 }, 4107 /** 4108 * Apply the shadow to the box 4109 */ 4110 shadow: function (b) { 4111 box.shadow(b); 4112 return wrapper; 4113 }, 4114 /** 4115 * Destroy and release memory. 4116 */ 4117 destroy: function () { 4118 removeEvent(wrapper, 'add', getSizeAfterAdd); 4119 4120 // Added by button implementation 4121 removeEvent(wrapper.element, 'mouseenter'); 4122 removeEvent(wrapper.element, 'mouseleave'); 4123 4124 if (text) { 4125 // Destroy the text element 4126 text = text.destroy(); 4127 } 4128 // Call base implementation to destroy the rest 4129 SVGElement.prototype.destroy.call(wrapper); 4130 } 4131 }); 4132 } 4133}; // end SVGRenderer 4134 4135 4136// general renderer 4137Renderer = SVGRenderer; 4138 4139 4140/* **************************************************************************** 4141 * * 4142 * START OF INTERNET EXPLORER <= 8 SPECIFIC CODE * 4143 * * 4144 * For applications and websites that don't need IE support, like platform * 4145 * targeted mobile apps and web apps, this code can be removed. * 4146 * * 4147 *****************************************************************************/ 4148 4149/** 4150 * @constructor 4151 */ 4152var VMLRenderer; 4153if (!hasSVG && !useCanVG) { 4154 4155/** 4156 * The VML element wrapper. 4157 */ 4158var VMLElement = { 4159 4160 /** 4161 * Initialize a new VML element wrapper. It builds the markup as a string 4162 * to minimize DOM traffic. 4163 * @param {Object} renderer 4164 * @param {Object} nodeName 4165 */ 4166 init: function (renderer, nodeName) { 4167 var wrapper = this, 4168 markup = ['<', nodeName, ' filled="f" stroked="f"'], 4169 style = ['position: ', ABSOLUTE, ';']; 4170 4171 // divs and shapes need size 4172 if (nodeName === 'shape' || nodeName === DIV) { 4173 style.push('left:0;top:0;width:10px;height:10px;'); 4174 } 4175 if (docMode8) { 4176 style.push('visibility: ', nodeName === DIV ? HIDDEN : VISIBLE); 4177 } 4178 4179 markup.push(' style="', style.join(''), '"/>'); 4180 4181 // create element with default attributes and style 4182 if (nodeName) { 4183 markup = nodeName === DIV || nodeName === 'span' || nodeName === 'img' ? 4184 markup.join('') 4185 : renderer.prepVML(markup); 4186 wrapper.element = createElement(markup); 4187 } 4188 4189 wrapper.renderer = renderer; 4190 wrapper.attrSetters = {}; 4191 }, 4192 4193 /** 4194 * Add the node to the given parent 4195 * @param {Object} parent 4196 */ 4197 add: function (parent) { 4198 var wrapper = this, 4199 renderer = wrapper.renderer, 4200 element = wrapper.element, 4201 box = renderer.box, 4202 inverted = parent && parent.inverted, 4203 4204 // get the parent node 4205 parentNode = parent ? 4206 parent.element || parent : 4207 box; 4208 4209 4210 // if the parent group is inverted, apply inversion on all children 4211 if (inverted) { // only on groups 4212 renderer.invertChild(element, parentNode); 4213 } 4214 4215 // issue #140 workaround - related to #61 and #74 4216 if (docMode8 && parentNode.gVis === HIDDEN) { 4217 css(element, { visibility: HIDDEN }); 4218 } 4219 4220 // append it 4221 parentNode.appendChild(element); 4222 4223 // align text after adding to be able to read offset 4224 wrapper.added = true; 4225 if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) { 4226 wrapper.updateTransform(); 4227 } 4228 4229 // fire an event for internal hooks 4230 fireEvent(wrapper, 'add'); 4231 4232 return wrapper; 4233 }, 4234 4235 /** 4236 * In IE8 documentMode 8, we need to recursively set the visibility down in the DOM 4237 * tree for nested groups. Related to #61, #586. 4238 */ 4239 toggleChildren: function (element, visibility) { 4240 var childNodes = element.childNodes, 4241 i = childNodes.length; 4242 4243 while (i--) { 4244 4245 // apply the visibility 4246 css(childNodes[i], { visibility: visibility }); 4247 4248 // we have a nested group, apply it to its children again 4249 if (childNodes[i].nodeName === 'DIV') { 4250 this.toggleChildren(childNodes[i], visibility); 4251 } 4252 } 4253 }, 4254 4255 /** 4256 * VML always uses htmlUpdateTransform 4257 */ 4258 updateTransform: SVGElement.prototype.htmlUpdateTransform, 4259 4260 /** 4261 * Get or set attributes 4262 */ 4263 attr: function (hash, val) { 4264 var wrapper = this, 4265 key, 4266 value, 4267 i, 4268 result, 4269 element = wrapper.element || {}, 4270 elemStyle = element.style, 4271 nodeName = element.nodeName, 4272 renderer = wrapper.renderer, 4273 symbolName = wrapper.symbolName, 4274 hasSetSymbolSize, 4275 shadows = wrapper.shadows, 4276 skipAttr, 4277 attrSetters = wrapper.attrSetters, 4278 ret = wrapper; 4279 4280 // single key-value pair 4281 if (isString(hash) && defined(val)) { 4282 key = hash; 4283 hash = {}; 4284 hash[key] = val; 4285 } 4286 4287 // used as a getter, val is undefined 4288 if (isString(hash)) { 4289 key = hash; 4290 if (key === 'strokeWidth' || key === 'stroke-width') { 4291 ret = wrapper.strokeweight; 4292 } else { 4293 ret = wrapper[key]; 4294 } 4295 4296 // setter 4297 } else { 4298 for (key in hash) { 4299 value = hash[key]; 4300 skipAttr = false;
4301 4302 // check for a specific attribute setter 4303 result = attrSetters[key] && attrSetters[key](value, key); 4304 4305 if (result !== false && value !== null) { // #620 4306 4307 if (result !== UNDEFINED) { 4308 value = result; // the attribute setter has returned a new value to set 4309 } 4310 4311 4312 // prepare paths 4313 // symbols 4314 if (symbolName && /^(x|y|r|start|end|width|height|innerR|anchorX|anchorY)/.test(key)) { 4315 // if one of the symbol size affecting parameters are changed, 4316 // check all the others only once for each call to an element's 4317 // .attr() method 4318 if (!hasSetSymbolSize) { 4319 wrapper.symbolAttr(hash); 4320 4321 hasSetSymbolSize = true; 4322 } 4323 skipAttr = true; 4324 4325 } else if (key === 'd') { 4326 value = value || []; 4327 wrapper.d = value.join(' '); // used in getter for animation 4328 4329 // convert paths 4330 i = value.length; 4331 var convertedPath = []; 4332 while (i--) { 4333 4334 // Multiply by 10 to allow subpixel precision. 4335 // Substracting half a pixel seems to make the coordinates 4336 // align with SVG, but this hasn't been tested thoroughly 4337 if (isNumber(value[i])) { 4338 convertedPath[i] = mathRound(value[i] * 10) - 5; 4339 } else if (value[i] === 'Z') { // close the path 4340 convertedPath[i] = 'x'; 4341 } else { 4342 convertedPath[i] = value[i]; 4343 } 4344 4345 } 4346 value = convertedPath.join(' ') || 'x'; 4347 element.path = value; 4348 4349 // update shadows 4350 if (shadows) { 4351 i = shadows.length; 4352 while (i--) { 4353 shadows[i].path = value; 4354 } 4355 } 4356 skipAttr = true; 4357 4358 // directly mapped to css 4359 } else if (key === 'zIndex' || key === 'visibility') { 4360 4361 // workaround for #61 and #586 4362 if (docMode8 && key === 'visibility' && nodeName === 'DIV') { 4363 element.gVis = value; 4364 wrapper.toggleChildren(element, value); 4365 if (value === VISIBLE) { // #74 4366 value = null; 4367 } 4368 } 4369 4370 if (value) { 4371 elemStyle[key] = value; 4372 } 4373 4374 4375 4376 skipAttr = true; 4377 4378 // width and height 4379 } else if (key === 'width' || key === 'height') { 4380 4381 value = mathMax(0, value); // don't set width or height below zero (#311) 4382 4383 this[key] = value; // used in getter 4384 4385 // clipping rectangle special 4386 if (wrapper.updateClipping) { 4387 wrapper[key] = value; 4388 wrapper.updateClipping(); 4389 } else { 4390 // normal 4391 elemStyle[key] = value; 4392 } 4393 4394 skipAttr = true; 4395 4396 // x and y 4397 } else if (key === 'x' || key === 'y') { 4398 4399 wrapper[key] = value; // used in getter 4400 elemStyle[{ x: 'left', y: 'top' }[key]] = value; 4401 4402 // class name 4403 } else if (key === 'class') { 4404 // IE8 Standards mode has problems retrieving the className 4405 element.className = value; 4406 4407 // stroke 4408 } else if (key === 'stroke') { 4409 4410 value = renderer.color(value, element, key); 4411 4412 key = 'strokecolor'; 4413 4414 // stroke width 4415 } else if (key === 'stroke-width' || key === 'strokeWidth') { 4416 element.stroked = value ? true : false; 4417 key = 'strokeweight'; 4418 wrapper[key] = value; // used in getter, issue #113 4419 if (isNumber(value)) { 4420 value += PX; 4421 } 4422 4423 // dashStyle 4424 } else if (key === 'dashstyle') { 4425 var strokeElem = element.getElementsByTagName('stroke')[0] || 4426 createElement(renderer.prepVML(['<stroke/>']), null, null, element); 4427 strokeElem[key] = value || 'solid'; 4428 wrapper.dashstyle = value; /* because changing stroke-width will change the dash length 4429 and cause an epileptic effect */ 4430 skipAttr = true; 4431 4432 // fill 4433 } else if (key === 'fill') { 4434 4435 if (nodeName === 'SPAN') { // text color 4436 elemStyle.color = value; 4437 } else { 4438 element.filled = value !== NONE ? true : false; 4439 4440 value = renderer.color(value, element, key); 4441 4442 key = 'fillcolor'; 4443 } 4444 4445 // translation for animation 4446 } else if (key === 'translateX' || key === 'translateY' || key === 'rotation') { 4447 wrapper[key] = value; 4448 wrapper.updateTransform(); 4449 4450 skipAttr = true; 4451 4452 // text for rotated and non-rotated elements 4453 } else if (key === 'text') { 4454 this.bBox = null; 4455 element.innerHTML = value; 4456 skipAttr = true; 4457 } 4458 4459 // let the shadow follow the main element 4460 if (shadows && key === 'visibility') { 4461 i = shadows.length; 4462 while (i--) { 4463 shadows[i].style[key] = value; 4464 } 4465 } 4466 4467 4468 4469 if (!skipAttr) { 4470 if (docMode8) { // IE8 setAttribute bug 4471 element[key] = value; 4472 } else { 4473 attr(element, key, value); 4474 } 4475 } 4476 4477 } 4478 } 4479 } 4480 return ret; 4481 }, 4482 4483 /** 4484 * Set the element's clipping to a predefined rectangle 4485 * 4486 * @param {String} id The id of the clip rectangle 4487 */ 4488 clip: function (clipRect) { 4489 var wrapper = this, 4490 clipMembers = clipRect.members; 4491 4492 clipMembers.push(wrapper); 4493 wrapper.destroyClip = function () { 4494 erase(clipMembers, wrapper); 4495 }; 4496 return wrapper.css(clipRect.getCSS(wrapper.inverted)); 4497 }, 4498 4499 /** 4500 * Set styles for the element 4501 * @param {Object} styles 4502 */ 4503 css: SVGElement.prototype.htmlCss, 4504 4505 /** 4506 * Removes a child either by removeChild or move to garbageBin. 4507 * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not. 4508 */ 4509 safeRemoveChild: function (element) { 4510 // discardElement will detach the node from its parent before attaching it 4511 // to the garbage bin. Therefore it is important that the node is attached and have parent. 4512 var parentNode = element.parentNode; 4513 if (parentNode) { 4514 discardElement(element); 4515 } 4516 }, 4517 4518 /** 4519 * Extend element.destroy by removing it from the clip members array 4520 */ 4521 destroy: function () { 4522 var wrapper = this; 4523 4524 if (wrapper.destroyClip) { 4525 wrapper.destroyClip(); 4526 } 4527 4528 return SVGElement.prototype.destroy.apply(wrapper); 4529 }, 4530 4531 /** 4532 * Remove all child nodes of a group, except the v:group element 4533 */ 4534 empty: function () { 4535 var element = this.element, 4536 childNodes = element.childNodes, 4537 i = childNodes.length, 4538 node; 4539 4540 while (i--) { 4541 node = childNodes[i]; 4542 node.parentNode.removeChild(node); 4543 } 4544 }, 4545 4546 /** 4547 * Add an event listener. VML override for normalizing event parameters. 4548 * @param {String} eventType 4549 * @param {Function} handler 4550 */ 4551 on: function (eventType, handler) { 4552 // simplest possible event model for internal use 4553 this.element['on' + eventType] = function () { 4554 var evt = win.event; 4555 evt.target = evt.srcElement; 4556 handler(evt); 4557 }; 4558 return this; 4559 }, 4560 4561 /** 4562 * Apply a drop shadow by copying elements and giving them different strokes 4563 * @param {Boolean} apply 4564 */ 4565 shadow: function (apply, group) { 4566 var shadows = [], 4567 i, 4568 element = this.element, 4569 renderer = this.renderer, 4570 shadow, 4571 elemStyle = element.style, 4572 markup, 4573 path = element.path; 4574 4575 // some times empty paths are not strings 4576 if (path && typeof path.value !== 'string') { 4577 path = 'x'; 4578 } 4579
4580 if (apply) { 4581 for (i = 1; i <= 3; i++) { 4582 markup = ['<shape isShadow="true" strokeweight="', (7 - 2 * i), 4583 '" filled="false" path="', path, 4584 '" coordsize="100,100" style="', element.style.cssText, '" />']; 4585 shadow = createElement(renderer.prepVML(markup), 4586 null, { 4587 left: pInt(elemStyle.left) + 1, 4588 top: pInt(elemStyle.top) + 1 4589 } 4590 ); 4591 4592 // apply the opacity 4593 markup = ['<stroke color="black" opacity="', (0.05 * i), '"/>']; 4594 createElement(renderer.prepVML(markup), null, null, shadow); 4595 4596 4597 // insert it 4598 if (group) { 4599 group.element.appendChild(shadow); 4600 } else { 4601 element.parentNode.insertBefore(shadow, element); 4602 } 4603 4604 // record it 4605 shadows.push(shadow); 4606 4607 } 4608 4609 this.shadows = shadows; 4610 } 4611 return this; 4612 4613 } 4614}; 4615VMLElement = extendClass(SVGElement, VMLElement); 4616 4617/** 4618 * The VML renderer 4619 */ 4620var VMLRendererExtension = { // inherit SVGRenderer 4621 4622 Element: VMLElement, 4623 isIE8: userAgent.indexOf('MSIE 8.0') > -1, 4624 4625 4626 /** 4627 * Initialize the VMLRenderer 4628 * @param {Object} container 4629 * @param {Number} width 4630 * @param {Number} height 4631 */ 4632 init: function (container, width, height) { 4633 var renderer = this, 4634 boxWrapper, 4635 box; 4636 4637 renderer.alignedObjects = []; 4638 4639 boxWrapper = renderer.createElement(DIV); 4640 box = boxWrapper.element; 4641 box.style.position = RELATIVE; // for freeform drawing using renderer directly 4642 container.appendChild(boxWrapper.element); 4643 4644 4645 // generate the containing box 4646 renderer.box = box; 4647 renderer.boxWrapper = boxWrapper; 4648 4649 4650 renderer.setSize(width, height, false); 4651 4652 // The only way to make IE6 and IE7 print is to use a global namespace. However, 4653 // with IE8 the only way to make the dynamic shapes visible in screen and print mode 4654 // seems to be to add the xmlns attribute and the behaviour style inline. 4655 if (!doc.namespaces.hcv) { 4656 4657 doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml'); 4658 4659 // setup default css 4660 doc.createStyleSheet().cssText = 4661 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke' + 4662 '{ behavior:url(#default#VML); display: inline-block; } '; 4663 4664 } 4665 }, 4666 4667 /** 4668 * Define a clipping rectangle. In VML it is accomplished by storing the values 4669 * for setting the CSS style to all associated members. 4670 * 4671 * @param {Number} x 4672 * @param {Number} y 4673 * @param {Number} width 4674 * @param {Number} height 4675 */ 4676 clipRect: function (x, y, width, height) { 4677 4678 // create a dummy element 4679 var clipRect = this.createElement(); 4680 4681 // mimic a rectangle with its style object for automatic updating in attr 4682 return extend(clipRect, { 4683 members: [], 4684 left: x, 4685 top: y, 4686 width: width, 4687 height: height, 4688 getCSS: function (inverted) { 4689 var rect = this,//clipRect.element.style, 4690 top = rect.top, 4691 left = rect.left, 4692 right = left + rect.width, 4693 bottom = top + rect.height, 4694 ret = { 4695 clip: 'rect(' + 4696 mathRound(inverted ? left : top) + 'px,' + 4697 mathRound(inverted ? bottom : right) + 'px,' + 4698 mathRound(inverted ? right : bottom) + 'px,' + 4699 mathRound(inverted ? top : left) + 'px)' 4700 }; 4701 4702 // issue 74 workaround 4703 if (!inverted && docMode8) { 4704 extend(ret, { 4705 width: right + PX, 4706 height: bottom + PX 4707 }); 4708 } 4709 return ret; 4710 }, 4711 4712 // used in attr and animation to update the clipping of all members 4713 updateClipping: function () { 4714 each(clipRect.members, function (member) { 4715 member.css(clipRect.getCSS(member.inverted)); 4716 }); 4717 } 4718 }); 4719 4720 }, 4721 4722 4723 /** 4724 * Take a color and return it if it's a string, make it a gradient if it's a 4725 * gradient configuration object, and apply opacity. 4726 * 4727 * @param {Object} color The color or config object 4728 */ 4729 color: function (color, elem, prop) { 4730 var colorObject, 4731 regexRgba = /^rgba/, 4732 markup; 4733 4734 if (color && color[LINEAR_GRADIENT]) { 4735 4736 var stopColor, 4737 stopOpacity, 4738 linearGradient = color[LINEAR_GRADIENT], 4739 x1 = linearGradient.x1 || linearGradient[0] || 0, 4740 y1 = linearGradient.y1 || linearGradient[1] || 0, 4741 x2 = linearGradient.x2 || linearGradient[2] || 0, 4742 y2 = linearGradient.y2 || linearGradient[3] || 0, 4743 angle, 4744 color1, 4745 opacity1, 4746 color2, 4747 opacity2; 4748 4749 each(color.stops, function (stop, i) { 4750 if (regexRgba.test(stop[1])) { 4751 colorObject = Color(stop[1]); 4752 stopColor = colorObject.get('rgb'); 4753 stopOpacity = colorObject.get('a'); 4754 } else { 4755 stopColor = stop[1]; 4756 stopOpacity = 1; 4757 } 4758 4759 if (!i) { // first 4760 color1 = stopColor; 4761 opacity1 = stopOpacity; 4762 } else { 4763 color2 = stopColor; 4764 opacity2 = stopOpacity; 4765 } 4766 }); 4767 4768 // Apply the gradient to fills only. 4769 if (prop === 'fill') { 4770 // calculate the angle based on the linear vector 4771 angle = 90 - math.atan( 4772 (y2 - y1) / // y vector 4773 (x2 - x1) // x vector 4774 ) * 180 / mathPI; 4775 4776 4777 // when colors attribute is used, the meanings of opacity and o:opacity2 4778 // are reversed. 4779 markup = ['<fill colors="0% ', color1, ',100% ', color2, '" angle="', angle, 4780 '" opacity="', opacity2, '" o:opacity2="', opacity1, 4781 '" type="gradient" focus="100%" method="sigma" />']; 4782 createElement(this.prepVML(markup), null, null, elem); 4783 4784 // Gradients are not supported for VML stroke, return the first color. #722. 4785 } else { 4786 return stopColor; 4787 } 4788 4789 4790 // if the color is an rgba color, split it and add a fill node 4791 // to hold the opacity component 4792 } else if (regexRgba.test(color) && elem.tagName !== 'IMG') { 4793 4794 colorObject = Color(color); 4795 4796 markup = ['<', prop, ' opacity="', colorObject.get('a'), '"/>']; 4797 createElement(this.prepVML(markup), null, null, elem); 4798 4799 return colorObject.get('rgb'); 4800 4801 4802 } else { 4803 var strokeNodes = elem.getElementsByTagName(prop); 4804 if (strokeNodes.length) { 4805 strokeNodes[0].opacity = 1; 4806 } 4807 return color; 4808 } 4809 4810 }, 4811 4812 /**
4813 * Take a VML string and prepare it for either IE8 or IE6/IE7. 4814 * @param {Array} markup A string array of the VML markup to prepare 4815 */ 4816 prepVML: function (markup) { 4817 var vmlStyle = 'display:inline-block;behavior:url(#default#VML);', 4818 isIE8 = this.isIE8; 4819 4820 markup = markup.join(''); 4821 4822 if (isIE8) { // add xmlns and style inline 4823 markup = markup.replace('/>', ' xmlns="urn:schemas-microsoft-com:vml" />'); 4824 if (markup.indexOf('style="') === -1) { 4825 markup = markup.replace('/>', ' style="' + vmlStyle + '" />'); 4826 } else { 4827 markup = markup.replace('style="', 'style="' + vmlStyle); 4828 } 4829 4830 } else { // add namespace 4831 markup = markup.replace('<', '<hcv:'); 4832 } 4833 4834 return markup; 4835 }, 4836 4837 /** 4838 * Create rotated and aligned text 4839 * @param {String} str 4840 * @param {Number} x 4841 * @param {Number} y 4842 */ 4843 text: SVGRenderer.prototype.html, 4844 4845 /** 4846 * Create and return a path element 4847 * @param {Array} path 4848 */ 4849 path: function (path) { 4850 // create the shape 4851 return this.createElement('shape').attr({ 4852 // subpixel precision down to 0.1 (width and height = 10px) 4853 coordsize: '100 100', 4854 d: path 4855 }); 4856 }, 4857 4858 /** 4859 * Create and return a circle element. In VML circles are implemented as 4860 * shapes, which is faster than v:oval 4861 * @param {Number} x 4862 * @param {Number} y 4863 * @param {Number} r 4864 */ 4865 circle: function (x, y, r) { 4866 return this.symbol('circle').attr({ x: x - r, y: y - r, width: 2 * r, height: 2 * r }); 4867 }, 4868 4869 /** 4870 * Create a group using an outer div and an inner v:group to allow rotating 4871 * and flipping. A simple v:group would have problems with positioning 4872 * child HTML elements and CSS clip. 4873 * 4874 * @param {String} name The name of the group 4875 */ 4876 g: function (name) { 4877 var wrapper, 4878 attribs; 4879 4880 // set the class name 4881 if (name) { 4882 attribs = { 'className': PREFIX + name, 'class': PREFIX + name }; 4883 } 4884 4885 // the div to hold HTML and clipping 4886 wrapper = this.createElement(DIV).attr(attribs); 4887 4888 return wrapper; 4889 }, 4890 4891 /** 4892 * VML override to create a regular HTML image 4893 * @param {String} src 4894 * @param {Number} x 4895 * @param {Number} y 4896 * @param {Number} width 4897 * @param {Number} height 4898 */ 4899 image: function (src, x, y, width, height) { 4900 var obj = this.createElement('img') 4901 .attr({ src: src }); 4902 4903 if (arguments.length > 1) { 4904 obj.css({ 4905 left: x, 4906 top: y, 4907 width: width, 4908 height: height 4909 }); 4910 } 4911 return obj; 4912 }, 4913 4914 /** 4915 * VML uses a shape for rect to overcome bugs and rotation problems 4916 */ 4917 rect: function (x, y, width, height, r, strokeWidth) { 4918 4919 if (isObject(x)) { 4920 y = x.y; 4921 width = x.width; 4922 height = x.height; 4923 strokeWidth = x.strokeWidth; 4924 x = x.x; 4925 } 4926 var wrapper = this.symbol('rect'); 4927 wrapper.r = r; 4928 4929 return wrapper.attr(wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0))); 4930 }, 4931 4932 /** 4933 * In the VML renderer, each child of an inverted div (group) is inverted 4934 * @param {Object} element 4935 * @param {Object} parentNode 4936 */ 4937 invertChild: function (element, parentNode) { 4938 var parentStyle = parentNode.style; 4939 4940 css(element, { 4941 flip: 'x', 4942 left: pInt(parentStyle.width) - 10, 4943 top: pInt(parentStyle.height) - 10, 4944 rotation: -90 4945 }); 4946 }, 4947 4948 /** 4949 * Symbol definitions that override the parent SVG renderer's symbols 4950 * 4951 */ 4952 symbols: { 4953 // VML specific arc function 4954 arc: function (x, y, w, h, options) { 4955 var start = options.start, 4956 end = options.end, 4957 radius = options.r || w || h, 4958 cosStart = mathCos(start), 4959 sinStart = mathSin(start), 4960 cosEnd = mathCos(end), 4961 sinEnd = mathSin(end), 4962 innerRadius = options.innerR, 4963 circleCorrection = 0.08 / radius, // #760 4964 innerCorrection = (innerRadius && 0.25 / innerRadius) || 0; 4965 4966 if (end - start === 0) { // no angle, don't show it. 4967 return ['x']; 4968 4969 } else if (2 * mathPI - end + start < circleCorrection) { // full circle 4970 // empirical correction found by trying out the limits for different radii 4971 cosEnd = -circleCorrection; 4972 } else if (end - start < innerCorrection) { // issue #186, another mysterious VML arc problem 4973 cosEnd = mathCos(start + innerCorrection); 4974 } 4975 4976 return [ 4977 'wa', // clockwise arc to
4978 x - radius, // left 4979 y - radius, // top 4980 x + radius, // right 4981 y + radius, // bottom 4982 x + radius * cosStart, // start x 4983 y + radius * sinStart, // start y 4984 x + radius * cosEnd, // end x 4985 y + radius * sinEnd, // end y 4986 4987 4988 'at', // anti clockwise arc to 4989 x - innerRadius, // left 4990 y - innerRadius, // top 4991 x + innerRadius, // right 4992 y + innerRadius, // bottom 4993 x + innerRadius * cosEnd, // start x 4994 y + innerRadius * sinEnd, // start y 4995 x + innerRadius * cosStart, // end x 4996 y + innerRadius * sinStart, // end y 4997 4998 'x', // finish path 4999 'e' // close 5000 ]; 5001 5002 }, 5003 // Add circle symbol path. This performs significantly faster than v:oval. 5004 circle: function (x, y, w, h) { 5005 5006 return [ 5007 'wa', // clockwisearcto 5008 x, // left 5009 y, // top 5010 x + w, // right 5011 y + h, // bottom 5012 x + w, // start x 5013 y + h / 2, // start y 5014 x + w, // end x 5015 y + h / 2, // end y 5016 //'x', // finish path 5017 'e' // close 5018 ]; 5019 }, 5020 /** 5021 * Add rectangle symbol path which eases rotation and omits arcsize problems 5022 * compared to the built-in VML roundrect shape 5023 * 5024 * @param {Number} left Left position 5025 * @param {Number} top Top position 5026 * @param {Number} r Border radius 5027 * @param {Object} options Width and height 5028 */ 5029 5030 rect: function (left, top, width, height, options) { 5031 /*for (var n in r) { 5032 logTime && console .log(n) 5033 }*/ 5034 5035 if (!defined(options)) { 5036 return []; 5037 } 5038 var right = left + width, 5039 bottom = top + height, 5040 r = mathMin(options.r || 0, width, height); 5041 5042 return [ 5043 M, 5044 left + r, top, 5045 5046 L, 5047 right - r, top, 5048 'wa', 5049 right - 2 * r, top, 5050 right, top + 2 * r, 5051 right - r, top, 5052 right, top + r, 5053 5054 L, 5055 right, bottom - r, 5056 'wa', 5057 right - 2 * r, bottom - 2 * r, 5058 right, bottom, 5059 right, bottom - r, 5060 right - r, bottom, 5061 5062 L, 5063 left + r, bottom, 5064 'wa', 5065 left, bottom - 2 * r, 5066 left + 2 * r, bottom, 5067 left + r, bottom, 5068 left, bottom - r, 5069 5070 L, 5071 left, top + r, 5072 'wa', 5073 left, top, 5074 left + 2 * r, top + 2 * r, 5075 left, top + r, 5076 left + r, top, 5077 5078 5079 'x', 5080 'e' 5081 ]; 5082 5083 } 5084 } 5085}; 5086VMLRenderer = function () { 5087 this.init.apply(this, arguments); 5088}; 5089VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension); 5090 5091 // general renderer 5092 Renderer = VMLRenderer; 5093} 5094 5095/* **************************************************************************** 5096 * * 5097 * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE * 5098 * * 5099 *****************************************************************************/ 5100/* **************************************************************************** 5101 * * 5102 * START OF ANDROID < 3 SPECIFIC CODE. THIS CAN BE REMOVED IF YOU'RE NOT * 5103 * TARGETING THAT SYSTEM. * 5104 * * 5105 *****************************************************************************/ 5106var CanVGRenderer, 5107 CanVGController; 5108 5109if (useCanVG) { 5110 /** 5111 * The CanVGRenderer is empty from start to keep the source footprint small. 5112 * When requested, the CanVGController downloads the rest of the source packaged 5113 * together with the canvg library. 5114 */ 5115 CanVGRenderer = function () { 5116 // Empty constructor 5117 }; 5118 5119 /** 5120 * Handles on demand download of canvg rendering support. 5121 */ 5122 CanVGController = (function () { 5123 // List of renderering calls 5124 var deferredRenderCalls = []; 5125 5126 /** 5127 * When downloaded, we are ready to draw deferred charts. 5128 */ 5129 function drawDeferred() { 5130 var callLength = deferredRenderCalls.length, 5131 callIndex; 5132 5133 // Draw all pending render calls 5134 for (callIndex = 0; callIndex < callLength; callIndex++) { 5135 deferredRenderCalls[callIndex](); 5136 } 5137 // Clear the list 5138 deferredRenderCalls = []; 5139 } 5140 5141 return { 5142 push: function (func, scriptLocation) { 5143 // Only get the script once 5144 if (deferredRenderCalls.length === 0) { 5145 getScript(scriptLocation, drawDeferred); 5146 } 5147 // Register render call 5148 deferredRenderCalls.push(func); 5149 } 5150 }; 5151 }()); 5152} // end CanVGRenderer 5153 5154/* **************************************************************************** 5155 * * 5156 * END OF ANDROID < 3 SPECIFIC CODE * 5157 * * 5158 *****************************************************************************/ 5159 5160/** 5161 * General renderer 5162 */ 5163Renderer = VMLRenderer || CanVGRenderer || SVGRenderer; 5164 5165/** 5166 * The chart class 5167 * @param {Object} options 5168 * @param {Function} callback Function to run when the chart has loaded 5169 */ 5170function Chart(userOptions, callback) { 5171 5172 // Handle regular options 5173 var options, 5174 seriesOptions = userOptions.series;
5174 // skip merging data points to increase performance 5175 userOptions.series = null; 5176 options = merge(defaultOptions, userOptions); // do the merge 5177 options.series = userOptions.series = seriesOptions; // set back the series data 5178 5179 var optionsChart = options.chart, 5180 optionsMargin = optionsChart.margin, 5181 margin = isObject(optionsMargin) ? 5182 optionsMargin : 5183 [optionsMargin, optionsMargin, optionsMargin, optionsMargin], 5184 optionsMarginTop = pick(optionsChart.marginTop, margin[0]), 5185 optionsMarginRight = pick(optionsChart.marginRight, margin[1]), 5186 optionsMarginBottom = pick(optionsChart.marginBottom, margin[2]), 5187 optionsMarginLeft = pick(optionsChart.marginLeft, margin[3]), 5188 spacingTop = optionsChart.spacingTop, 5189 spacingRight = optionsChart.spacingRight, 5190 spacingBottom = optionsChart.spacingBottom, 5191 spacingLeft = optionsChart.spacingLeft, 5192 spacingBox, 5193 chartTitleOptions, 5194 chartSubtitleOptions, 5195 plotTop, 5196 marginRight, 5197 marginBottom, 5198 plotLeft, 5199 axisOffset, 5200 renderTo, 5201 renderToClone, 5202 container, 5203 containerId, 5204 containerWidth, 5205 containerHeight, 5206 chartWidth, 5207 chartHeight, 5208 oldChartWidth, 5209 oldChartHeight, 5210 chartBackground,
5211 plotBackground, 5212 plotBGImage, 5213 plotBorder, 5214 chart = this, 5215 chartEvents = optionsChart.events, 5216 runChartClick = chartEvents && !!chartEvents.click, 5217 eventType, 5218 isInsidePlot, // function 5219 tooltip, 5220 mouseIsDown, 5221 loadingDiv, 5222 loadingSpan, 5223 loadingShown, 5224 plotHeight, 5225 plotWidth, 5226 tracker, 5227 trackerGroup, 5228 legend, 5229 legendWidth, 5230 legendHeight, 5231 chartPosition, 5232 hasCartesianSeries = optionsChart.showAxes, 5233 isResizing = 0, 5234 axes = [], 5235 maxTicks, // handle the greatest amount of ticks on grouped axes 5236 series = [], 5237 inverted, 5238 renderer, 5239 tooltipTick, 5240 tooltipInterval, 5241 hoverX, 5242 drawChartBox, // function 5243 getMargins, // function 5244 resetMargins, // function 5245 setChartSize, // function 5246 resize, 5247 zoom, // function 5248 zoomOut; // function 5249 5250 5251 /** 5252 * Create a new axis object 5253 * @param {Object} options 5254 */ 5255 function Axis(userOptions) { 5256 5257 // Define variables 5258 var isXAxis = userOptions.isX, 5259 opposite = userOptions.opposite, // needed in setOptions 5260 horiz = inverted ? !isXAxis : isXAxis, 5261 side = horiz ? 5262 (opposite ? 0 : 2) : // top : bottom 5263 (opposite ? 1 : 3), // right : left 5264 stacks = {}, 5265 5266 options = merge( 5267 isXAxis ? defaultXAxisOptions : defaultYAxisOptions, 5268 [defaultTopAxisOptions, defaultRightAxisOptions, 5269 defaultBottomAxisOptions, defaultLeftAxisOptions][side], 5270 userOptions 5271 ), 5272 5273 axis = this, 5274 axisTitle, 5275 type = options.type, 5276 isDatetimeAxis = type === 'datetime', 5277 isLog = type === 'logarithmic', 5278 offset = options.offset || 0, 5279 xOrY = isXAxis ? 'x' : 'y', 5280 axisLength = 0, 5281 oldAxisLength, 5282 transA, // translation factor 5283 transB, // translation addend 5284 oldTransA, // used for prerendering 5285 axisLeft, 5286 axisTop, 5287 axisWidth, 5288 axisHeight, 5289 axisBottom, 5290 axisRight, 5291 translate, // fn 5292 setAxisTranslation, // fn 5293 getPlotLinePath, // fn 5294 axisGroup, 5295 gridGroup, 5296 axisLine, 5297 dataMin, 5298 dataMax, 5299 minRange = options.minRange || options.maxZoom, 5300 range = options.range, 5301 userMin, 5302 userMax, 5303 oldUserMin, 5304 oldUserMax, 5305 max = null, 5306 min = null, 5307 oldMin, 5308 oldMax, 5309 minPadding = options.minPadding, 5310 maxPadding = options.maxPadding, 5311 minPixelPadding = 0, 5312 isLinked = defined(options.linkedTo), 5313 linkedParent, 5314 ignoreMinPadding, // can be set to true by a column or bar series 5315 ignoreMaxPadding, 5316 usePercentage, 5317 events = options.events, 5318 eventType, 5319 plotLinesAndBands = [], 5320 tickInterval, 5321 minorTickInterval, 5322 magnitude, 5323 tickPositions, // array containing predefined positions 5324 tickPositioner = options.tickPositioner, 5325 ticks = {}, 5326 minorTicks = {}, 5327 alternateBands = {}, 5328 tickAmount, 5329 labelOffset, 5330 axisTitleMargin,// = options.title.margin, 5331 categories = options.categories, 5332 labelFormatter = options.labels.formatter || // can be overwritten by dynamic format 5333 function () { 5334 var value = this.value, 5335 dateTimeLabelFormat = this.dateTimeLabelFormat, 5336 ret; 5337 5338 if (dateTimeLabelFormat) { // datetime axis 5339 ret = dateFormat(dateTimeLabelFormat, value); 5340 5341 } else if (tickInterval % 1000000 === 0) { // use M abbreviation 5342 ret = (value / 1000000) + 'M'; 5343 5344 } else if (tickInterval % 1000 === 0) { // use k abbreviation 5345 ret = (value / 1000) + 'k'; 5346 5347 } else if (!categories && value >= 1000) { // add thousands separators 5348 ret = numberFormat(value, 0); 5349 5350 } else { // strings (categories) and small numbers 5351 ret = value; 5352 } 5353 return ret; 5354 }, 5355 5356 staggerLines = horiz && options.labels.staggerLines, 5357 reversed = options.reversed, 5358 tickmarkOffset = (categories && options.tickmarkPlacement === 'between') ? 0.5 : 0; 5359 5360 /** 5361 * The Tick class 5362 */ 5363 function Tick(pos, type) { 5364 var tick = this; 5365 tick.pos = pos; 5366 tick.type = type || ''; 5367 tick.isNew = true; 5368 5369 if (!type) { 5370 tick.addLabel(); 5371 } 5372 } 5373 Tick.prototype = { 5374 5375 /** 5376 * Write the tick label 5377 */ 5378 addLabel: function () { 5379 var tick = this, 5380 pos = tick.pos, 5381 labelOptions = options.labels, 5382 str, 5383 width = (categories && horiz && categories.length && 5384 !labelOptions.step && !labelOptions.staggerLines && 5385 !labelOptions.rotation && 5386 plotWidth / categories.length) || 5387 (!horiz && plotWidth / 2), 5388 isFirst = pos === tickPositions[0], 5389 isLast = pos === tickPositions[tickPositions.length - 1], 5390 css, 5391 value = categories && defined(categories[pos]) ? categories[pos] : pos, 5392 label = tick.label, 5393 tickPositionInfo = tickPositions.info, 5394 dateTimeLabelFormat; 5395 5396 // Set the datetime label format. If a higher rank is set for this position, use that. If not, 5397 // use the general format. 5398 if (isDatetimeAxis && tickPositionInfo) { 5399 dateTimeLabelFormat = options.dateTimeLabelFormats[tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName]; 5400 } 5401 5402 // set properties for access in render method 5403 tick.isFirst = isFirst; 5404 tick.isLast = isLast; 5405 5406 // get the string 5407 str = labelFormatter.call({ 5408 axis: axis, 5409 chart: chart, 5410 isFirst: isFirst, 5411 isLast: isLast, 5412 dateTimeLabelFormat: dateTimeLabelFormat, 5413 value: isLog ? correctFloat(lin2log(value)) : value 5414 }); 5415 5416 5417 // prepare CSS 5418 css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) + PX }; 5419 css = extend(css, labelOptions.style); 5420 5421 // first call 5422 if (!defined(label)) { 5423 tick.label = 5424 defined(str) && labelOptions.enabled ? 5425 renderer.text( 5426 str, 5427 0, 5428 0, 5429 labelOptions.useHTML 5430 ) 5431 .attr({ 5432 align: labelOptions.align, 5433 rotation: labelOptions.rotation 5434 }) 5435 // without position absolute, IE export sometimes is wrong 5436 .css(css) 5437 .add(axisGroup) : 5438 null; 5439 5440 // update 5441 } else if (label) { 5442 label.attr({ 5443 text: str 5444 }) 5445 .css(css); 5446 } 5447 }, 5448 /** 5449 * Get the offset height or width of the label 5450 */ 5451 getLabelSize: function () { 5452 var label = this.label; 5453 return label ? 5454 ((this.labelBBox = label.getBBox()))[horiz ? 'height' : 'width'] : 5455 0; 5456 }, 5457 5458 /** 5459 * Find how far the labels extend to the right and left of the tick's x position. Used for anti-collision 5460 * detection with overflow logic. 5461 */ 5462 getLabelSides: function () { 5463 var bBox = this.labelBBox, // assume getLabelSize has run at this point 5464 labelOptions = options.labels, 5465 width = bBox.width, 5466 leftSide = width * { left: 0, center: 0.5, right: 1 }[labelOptions.align] - labelOptions.x; 5467 5468 return [-leftSide, width - leftSide]; 5469 }, 5470 5471 /** 5472 * Handle the label overflow by adjusting the labels to the left and right edge, or 5473 * hide them if they collide into the neighbour label. 5474 */ 5475 handleOverflow: function (index) { 5476 var show = true, 5477 isFirst = this.isFirst, 5478 isLast = this.isLast, 5479 label = this.label, 5480 x = label.x; 5481 5482 if (isFirst || isLast) { 5483 5484 var sides = this.getLabelSides(), 5485 leftSide = sides[0], 5486 rightSide = sides[1], 5487 plotLeft = chart.plotLeft, 5488 plotRight = plotLeft + axis.len, 5489 neighbour = ticks[tickPositions[index + (isFirst ? 1 : -1)]], 5490 neighbourEdge = neighbour && neighbour.label.x + neighbour.getLabelSides()[isFirst ? 0 : 1]; 5491 5492 if ((isFirst && !reversed) || (isLast && reversed)) { 5493 // Is the label spilling out to the left of the plot area? 5494 if (x + leftSide < plotLeft) { 5495 5496 // Align it to plot left 5497 x = plotLeft - leftSide; 5498 5499 // Hide it if it now overlaps the neighbour label 5500 if (neighbour && x + rightSide > neighbourEdge) { 5501 show = false;
5502 } 5503 } 5504 5505 } else { 5506 // Is the label spilling out to the right of the plot area? 5507 if (x + rightSide > plotRight) { 5508 5509 // Align it to plot right 5510 x = plotRight - rightSide; 5511 5512 // Hide it if it now overlaps the neighbour label 5513 if (neighbour && x + leftSide < neighbourEdge) { 5514 show = false; 5515 } 5516 5517 } 5518 } 5519 5520 // Set the modified x position of the label 5521 label.x = x; 5522 } 5523 return show; 5524 }, 5525 5526 /** 5527 * Put everything in place 5528 * 5529 * @param index {Number} 5530 * @param old {Boolean} Use old coordinates to prepare an animation into new position 5531 */ 5532 render: function (index, old) { 5533 var tick = this, 5534 type = tick.type, 5535 label = tick.label, 5536 pos = tick.pos, 5537 labelOptions = options.labels, 5538 gridLine = tick.gridLine, 5539 gridPrefix = type ? type + 'Grid' : 'grid', 5540 tickPrefix = type ? type + 'Tick' : 'tick', 5541 gridLineWidth = options[gridPrefix + 'LineWidth'], 5542 gridLineColor = options[gridPrefix + 'LineColor'], 5543 dashStyle = options[gridPrefix + 'LineDashStyle'], 5544 tickLength = options[tickPrefix + 'Length'], 5545 tickWidth = options[tickPrefix + 'Width'] || 0, 5546 tickColor = options[tickPrefix + 'Color'], 5547 tickPosition = options[tickPrefix + 'Position'], 5548 gridLinePath, 5549 mark = tick.mark, 5550 markPath, 5551 step = labelOptions.step, 5552 cHeight = (old && oldChartHeight) || chartHeight, 5553 attribs, 5554 show = true, 5555 x, 5556 y; 5557 5558 // get x and y position for ticks and labels 5559 x = horiz ? 5560 translate(pos + tickmarkOffset, null, null, old) + transB : 5561 axisLeft + offset + (opposite ? ((old && oldChartWidth) || chartWidth) - axisRight - axisLeft : 0); 5562 5563 y = horiz ? 5564 cHeight - axisBottom + offset - (opposite ? axisHeight : 0) : 5565 cHeight - translate(pos + tickmarkOffset, null, null, old) - transB; 5566 5567 // create the grid line 5568 if (gridLineWidth) { 5569 gridLinePath = getPlotLinePath(pos + tickmarkOffset, gridLineWidth, old); 5570 5571 if (gridLine === UNDEFINED) { 5572 attribs = { 5573 stroke: gridLineColor, 5574 'stroke-width': gridLineWidth 5575 }; 5576 if (dashStyle) { 5577 attribs.dashstyle = dashStyle; 5578 } 5579 if (!type) { 5580 attribs.zIndex = 1; 5581 } 5582 tick.gridLine = gridLine = 5583 gridLineWidth ? 5584 renderer.path(gridLinePath) 5585 .attr(attribs).add(gridGroup) : 5586 null; 5587 } 5588 5589 // If the parameter 'old' is set, the current call will be followed 5590 // by another call, therefore do not do any animations this time 5591 if (!old && gridLine && gridLinePath) { 5592 gridLine.animate({ 5593 d: gridLinePath 5594 }); 5595 } 5596 } 5597 5598 // create the tick mark 5599 if (tickWidth) { 5600 5601 // negate the length 5602 if (tickPosition === 'inside') { 5603 tickLength = -tickLength; 5604 } 5605 if (opposite) { 5606 tickLength = -tickLength; 5607 } 5608 5609 markPath = renderer.crispLine([ 5610 M, 5611 x, 5612 y, 5613 L, 5614 x + (horiz ? 0 : -tickLength), 5615 y + (horiz ? tickLength : 0) 5616 ], tickWidth); 5617 5618 if (mark) { // updating 5619 mark.animate({ 5620 d: markPath 5621 }); 5622 } else { // first time 5623 tick.mark = renderer.path( 5624 markPath 5625 ).attr({ 5626 stroke: tickColor, 5627 'stroke-width': tickWidth 5628 }).add(axisGroup); 5629 } 5630 } 5631 5632 // the label is created on init - now move it into place 5633 if (label && !isNaN(x)) { 5634 x = x + labelOptions.x - (tickmarkOffset && horiz ? 5635 tickmarkOffset * transA * (reversed ? -1 : 1) : 0); 5636 y = y + labelOptions.y - (tickmarkOffset && !horiz ? 5637 tickmarkOffset * transA * (reversed ? 1 : -1) : 0); 5638 5639 // vertically centered 5640 if (!defined(labelOptions.y)) { 5641 y += pInt(label.styles.lineHeight) * 0.9 - label.getBBox().height / 2; 5642 } 5643 5644 5645 // correct for staggered labels 5646 if (staggerLines) { 5647 y += (index / (step || 1) % staggerLines) * 16; 5648 } 5649 5650 // Cache x and y to be able to read final position before animation 5651 label.x = x; 5652 label.y = y; 5653 5654 // apply show first and show last 5655 if ((tick.isFirst && !pick(options.showFirstLabel, 1)) || 5656 (tick.isLast && !pick(options.showLastLabel, 1))) { 5657 show = false;
5658 5659 // Handle label overflow and show or hide accordingly 5660 } else if (!staggerLines && horiz && labelOptions.overflow === 'justify' && !tick.handleOverflow(index)) { 5661 show = false; 5662 } 5663 5664 // apply step 5665 if (step && index % step) { 5666 // show those indices dividable by step 5667 show = false; 5668 } 5669 5670 // Set the new position, and show or hide 5671 if (show) { 5672 label[tick.isNew ? 'attr' : 'animate']({ 5673 x: label.x, 5674 y: label.y 5675 }); 5676 label.show(); 5677 tick.isNew = false; 5678 } else { 5679 label.hide(); 5680 } 5681 } 5682 5683 5684 }, 5685 5686 /** 5687 * Destructor for the tick prototype 5688 */ 5689 destroy: function () { 5690 destroyObjectProperties(this); 5691 } 5692 }; 5693 5694 /** 5695 * The object wrapper for plot lines and plot bands 5696 * @param {Object} options 5697 */ 5698 function PlotLineOrBand(options) { 5699 var plotLine = this; 5700 if (options) { 5701 plotLine.options = options; 5702 plotLine.id = options.id; 5703 } 5704 5705 //plotLine.render() 5706 return plotLine; 5707 } 5708 5709 PlotLineOrBand.prototype = { 5710 5711 /** 5712 * Render the plot line or plot band. If it is already existing, 5713 * move it. 5714 */ 5715 render: function () { 5716 var plotLine = this, 5717 halfPointRange = (axis.pointRange || 0) / 2, 5718 options = plotLine.options, 5719 optionsLabel = options.label, 5720 label = plotLine.label, 5721 width = options.width, 5722 to = options.to, 5723 from = options.from, 5724 value = options.value, 5725 toPath, // bands only 5726 dashStyle = options.dashStyle, 5727 svgElem = plotLine.svgElem, 5728 path = [], 5729 addEvent, 5730 eventType, 5731 xs, 5732 ys, 5733 x, 5734 y, 5735 color = options.color, 5736 zIndex = options.zIndex, 5737 events = options.events, 5738 attribs; 5739 5740 // logarithmic conversion 5741 if (isLog) { 5742 from = log2lin(from); 5743 to = log2lin(to); 5744 value = log2lin(value); 5745 } 5746 5747 // plot line 5748 if (width) { 5749 path = getPlotLinePath(value, width); 5750 attribs = { 5751 stroke: color, 5752 'stroke-width': width 5753 }; 5754 if (dashStyle) { 5755 attribs.dashstyle = dashStyle; 5756 } 5757 } else if (defined(from) && defined(to)) { // plot band 5758 // keep within plot area 5759 from = mathMax(from, min - halfPointRange); 5760 to = mathMin(to, max + halfPointRange); 5761 5762 toPath = getPlotLinePath(to); 5763 path = getPlotLinePath(from); 5764 if (path && toPath) { 5765 path.push( 5766 toPath[4], 5767 toPath[5], 5768 toPath[1], 5769 toPath[2] 5770 ); 5771 } else { // outside the axis area 5772 path = null; 5773 } 5774 attribs = { 5775 fill: color 5776 }; 5777 } else { 5778 return; 5779 } 5780 // zIndex 5781 if (defined(zIndex)) { 5782 attribs.zIndex = zIndex; 5783 } 5784 5785 // common for lines and bands 5786 if (svgElem) { 5787 if (path) { 5788 svgElem.animate({ 5789 d: path 5790 }, null, svgElem.onGetPath); 5791 } else { 5792 svgElem.hide(); 5793 svgElem.onGetPath = function () { 5794 svgElem.show(); 5795 }; 5796 } 5797 } else if (path && path.length) { 5798 plotLine.svgElem = svgElem = renderer.path(path) 5799 .attr(attribs).add(); 5800 5801 // events 5802 if (events) { 5803 addEvent = function (eventType) { 5804 svgElem.on(eventType, function (e) { 5805 events[eventType].apply(plotLine, [e]); 5806 }); 5807 }; 5808 for (eventType in events) { 5809 addEvent(eventType); 5810 } 5811 } 5812 } 5813 5814 // the plot band/line label 5815 if (optionsLabel && defined(optionsLabel.text) && path && path.length && axisWidth > 0 && axisHeight > 0) { 5816 // apply defaults 5817 optionsLabel = merge({ 5818 align: horiz && toPath && 'center', 5819 x: horiz ? !toPath && 4 : 10, 5820 verticalAlign : !horiz && toPath && 'middle', 5821 y: horiz ? toPath ? 16 : 10 : toPath ? 6 : -4, 5822 rotation: horiz && !toPath && 90 5823 }, optionsLabel); 5824 5825 // add the SVG element 5826 if (!label) { 5827 plotLine.label = label = renderer.text( 5828 optionsLabel.text, 5829 0, 5830 0 5831 ) 5832 .attr({ 5833 align: optionsLabel.textAlign || optionsLabel.align, 5834 rotation: optionsLabel.rotation, 5835 zIndex: zIndex 5836 }) 5837 .css(optionsLabel.style) 5838 .add(); 5839 } 5840 5841 // get the bounding box and align the label 5842 xs = [path[1], path[4], pick(path[6], path[1])]; 5843 ys = [path[2], path[5], pick(path[7], path[2])]; 5844 x = arrayMin(xs); 5845 y = arrayMin(ys); 5846 5847 label.align(optionsLabel, false, { 5848 x: x, 5849 y: y, 5850 width: arrayMax(xs) - x, 5851 height: arrayMax(ys) - y 5852 }); 5853 label.show(); 5854 5855 } else if (label) { // move out of sight 5856 label.hide(); 5857 } 5858 5859 // chainable 5860 return plotLine; 5861 }, 5862 5863 /** 5864 * Remove the plot line or band 5865 */ 5866 destroy: function () { 5867 var obj = this; 5868 5869 destroyObjectProperties(obj); 5870 5871 // remove it from the lookup 5872 erase(plotLinesAndBands, obj); 5873 } 5874 }; 5875 5876 /**
5877 * The class for stack items 5878 */ 5879 function StackItem(options, isNegative, x, stackOption) { 5880 var stackItem = this; 5881 5882 // Tells if the stack is negative 5883 stackItem.isNegative = isNegative; 5884 5885 // Save the options to be able to style the label 5886 stackItem.options = options; 5887 5888 // Save the x value to be able to position the label later 5889 stackItem.x = x; 5890 5891 // Save the stack option on the series configuration object 5892 stackItem.stack = stackOption; 5893 5894 // The align options and text align varies on whether the stack is negative and 5895 // if the chart is inverted or not. 5896 // First test the user supplied value, then use the dynamic. 5897 stackItem.alignOptions = { 5898 align: options.align || (inverted ? (isNegative ? 'left' : 'right') : 'center'), 5899 verticalAlign: options.verticalAlign || (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')), 5900 y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)), 5901 x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0) 5902 }; 5903 5904 stackItem.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center'); 5905 } 5906 5907 StackItem.prototype = { 5908 destroy: function () { 5909 destroyObjectProperties(this); 5910 }, 5911 5912 /** 5913 * Sets the total of this stack. Should be called when a serie is hidden or shown 5914 * since that will affect the total of other stacks. 5915 */ 5916 setTotal: function (total) { 5917 this.total = total; 5918 this.cum = total; 5919 }, 5920 5921 /** 5922 * Renders the stack total label and adds it to the stack label group. 5923 */ 5924 render: function (group) { 5925 var stackItem = this, // aliased this 5926 str = stackItem.options.formatter.call(stackItem); // format the text in the label 5927 5928 // Change the text to reflect the new total and set visibility to hidden in case the serie is hidden 5929 if (stackItem.label) { 5930 stackItem.label.attr({text: str, visibility: HIDDEN}); 5931 // Create new label 5932 } else { 5933 stackItem.label = 5934 chart.renderer.text(str, 0, 0) // dummy positions, actual position updated with setOffset method in columnseries 5935 .css(stackItem.options.style) // apply style 5936 .attr({align: stackItem.textAlign, // fix the text-anchor 5937 rotation: stackItem.options.rotation, // rotation 5938 visibility: HIDDEN }) // hidden until setOffset is called 5939 .add(group); // add to the labels-group 5940 } 5941 }, 5942 5943 /** 5944 * Sets the offset that the stack has from the x value and repositions the label. 5945 */ 5946 setOffset: function (xOffset, xWidth) { 5947 var stackItem = this, // aliased this 5948 neg = stackItem.isNegative, // special treatment is needed for negative stacks 5949 y = axis.translate(stackItem.total, 0, 0, 0, 1), // stack value translated mapped to chart coordinates 5950 yZero = axis.translate(0), // stack origin 5951 h = mathAbs(y - yZero), // stack height 5952 x = chart.xAxis[0].translate(stackItem.x) + xOffset, // stack x position 5953 plotHeight = chart.plotHeight, 5954 stackBox = { // this is the box for the complete stack 5955 x: inverted ? (neg ? y : y - h) : x, 5956 y: inverted ? plotHeight - x - xWidth : (neg ? (plotHeight - y - h) : plotHeight - y), 5957 width: inverted ? h : xWidth, 5958 height: inverted ? xWidth : h 5959 }; 5960 5961 if (stackItem.label) { 5962 stackItem.label 5963 .align(stackItem.alignOptions, null, stackBox) // align the label to the box 5964 .attr({visibility: VISIBLE}); // set visibility 5965 } 5966 } 5967 }; 5968 5969 /** 5970 * Get the minimum and maximum for the series of each axis 5971 */ 5972 function getSeriesExtremes() { 5973 var posStack = [], 5974 negStack = [], 5975 i; 5976 5977 // reset dataMin and dataMax in case we're redrawing 5978 dataMin = dataMax = null; 5979 5980 // loop through this axis' series 5981 each(axis.series, function (series) { 5982 5983 if (series.visible || !optionsChart.ignoreHiddenSeries) { 5984 5985 var seriesOptions = series.options, 5986 stacking, 5987 posPointStack, 5988 negPointStack, 5989 stackKey, 5990 stackOption, 5991 negKey, 5992 xData, 5993 yData, 5994 x, 5995 y, 5996 threshold = seriesOptions.threshold, 5997 yDataLength, 5998 activeYData = [], 5999 activeCounter = 0; 6000 6001 // Validate threshold in logarithmic axes 6002 if (isLog && threshold <= 0) { 6003 threshold = seriesOptions.threshold = null; 6004 } 6005 6006 // Get dataMin and dataMax for X axes 6007 if (isXAxis) { 6008 xData = series.xData; 6009 if (xData.length) { 6010 dataMin = mathMin(pick(dataMin, xData[0]), arrayMin(xData)); 6011 dataMax = mathMax(pick(dataMax, xData[0]), arrayMax(xData)); 6012 } 6013 6014 // Get dataMin and dataMax for Y axes, as well as handle stacking and processed data 6015 } else { 6016 var isNegative, 6017 pointStack, 6018 key, 6019 cropped = series.cropped, 6020 xExtremes = series.xAxis.getExtremes(), 6021 //findPointRange, 6022 //pointRange, 6023 j, 6024 hasModifyValue = !!series.modifyValue; 6025 6026 6027 // Handle stacking 6028 stacking = seriesOptions.stacking; 6029 usePercentage = stacking === 'percent'; 6030 6031 // create a stack for this particular series type 6032 if (stacking) { 6033 stackOption = seriesOptions.stack; 6034 stackKey = series.type + pick(stackOption, ''); 6035 negKey = '-' + stackKey; 6036 series.stackKey = stackKey; // used in translate 6037 6038 posPointStack = posStack[stackKey] || []; // contains the total values for each x 6039 posStack[stackKey] = posPointStack; 6040 6041 negPointStack = negStack[negKey] || []; 6042 negStack[negKey] = negPointStack; 6043 } 6044 if (usePercentage) { 6045 dataMin = 0; 6046 dataMax = 99; 6047 } 6048 6049 6050 // processData can alter series.pointRange, so this goes after 6051 //findPointRange = series.pointRange === null; 6052 6053 xData = series.processedXData; 6054 yData = series.processedYData; 6055 yDataLength = yData.length; 6056 6057 // loop over the non-null y values and read them into a local array 6058 for (i = 0; i < yDataLength; i++) { 6059 x = xData[i]; 6060 y = yData[i]; 6061 if (y !== null && y !== UNDEFINED) { 6062 6063 // read stacked values into a stack based on the x value, 6064 // the sign of y and the stack key 6065 if (stacking) { 6066 isNegative = y < threshold; 6067 pointStack = isNegative ? negPointStack : posPointStack; 6068 key = isNegative ? negKey : stackKey; 6069 6070 y = pointStack[x] =
6071 defined(pointStack[x]) ? 6072 pointStack[x] + y : y; 6073 6074 6075 // add the series 6076 if (!stacks[key]) { 6077 stacks[key] = {}; 6078 } 6079 6080 // If the StackItem is there, just update the values, 6081 // if not, create one first 6082 if (!stacks[key][x]) { 6083 stacks[key][x] = new StackItem(options.stackLabels, isNegative, x, stackOption); 6084 } 6085 stacks[key][x].setTotal(y); 6086 6087 6088 // general hook, used for Highstock compare values feature 6089 } else if (hasModifyValue) { 6090 y = series.modifyValue(y); 6091 } 6092 6093 // get the smallest distance between points 6094 /*if (i) { 6095 distance = mathAbs(xData[i] - xData[i - 1]); 6096 pointRange = pointRange === UNDEFINED ? distance : mathMin(distance, pointRange); 6097 }*/ 6098 6099 // for points within the visible range, including the first point outside the 6100 // visible range, consider y extremes 6101 if (cropped || ((xData[i + 1] || x) >= xExtremes.min && (xData[i - 1] || x) <= xExtremes.max)) { 6102 6103 j = y.length; 6104 if (j) { // array, like ohlc data 6105 while (j--) { 6106 if (y[j] !== null) { 6107 activeYData[activeCounter++] = y[j]; 6108 } 6109 } 6110 } else { 6111 activeYData[activeCounter++] = y; 6112 } 6113 } 6114 } 6115 } 6116 6117 // record the least unit distance 6118 /*if (findPointRange) { 6119 series.pointRange = pointRange || 1; 6120 } 6121 series.closestPointRange = pointRange;*/ 6122 6123 // Get the dataMin and dataMax so far. If percentage is used, the min and max are 6124 // always 0 and 100. If the length of activeYData is 0, continue with null values. 6125 if (!usePercentage && activeYData.length) { 6126 dataMin = mathMin(pick(dataMin, activeYData[0]), arrayMin(activeYData)); 6127 dataMax = mathMax(pick(dataMax, activeYData[0]), arrayMax(activeYData)); 6128 } 6129 6130 // Adjust to threshold 6131 if (defined(threshold)) { 6132 if (dataMin >= threshold) { 6133 dataMin = threshold; 6134 ignoreMinPadding = true; 6135 } else if (dataMax < threshold) { 6136 dataMax = threshold; 6137 ignoreMaxPadding = true; 6138 } 6139 } 6140 } 6141 } 6142 }); 6143 6144 } 6145 6146 /** 6147 * Translate from axis value to pixel position on the chart, or back 6148 * 6149 */ 6150 translate = function (val, backwards, cvsCoord, old, handleLog) { 6151 6152 var sign = 1, 6153 cvsOffset = 0, 6154 localA = old ? oldTransA : transA, 6155 localMin = old ? oldMin : min, 6156 returnValue, 6157 postTranslate = options.ordinal || (isLog && handleLog); 6158 6159 if (!localA) { 6160 localA = transA; 6161 } 6162 6163 if (cvsCoord) { 6164 sign *= -1; // canvas coordinates inverts the value 6165 cvsOffset = axisLength; 6166 } 6167 if (reversed) { // reversed axis 6168 sign *= -1; 6169 cvsOffset -= sign * axisLength; 6170 } 6171 6172 if (backwards) { // reverse translation 6173 if (reversed) { 6174 val = axisLength - val; 6175 } 6176 returnValue = val / localA + localMin; // from chart pixel to value 6177 if (postTranslate) { // log and ordinal axes 6178 returnValue = axis.lin2val(returnValue); 6179 } 6180 6181 } else { // normal translation, from axis value to pixel, relative to plot 6182 if (postTranslate) { // log and ordinal axes 6183 val = axis.val2lin(val); 6184 } 6185 6186 returnValue = sign * (val - localMin) * localA + cvsOffset + (sign * minPixelPadding); 6187 } 6188 6189 return returnValue; 6190 }; 6191 6192 /** 6193 * Create the path for a plot line that goes from the given value on 6194 * this axis, across the plot to the opposite side 6195 * @param {Number} value 6196 * @param {Number} lineWidth Used for calculation crisp line 6197 * @param {Number] old Use old coordinates (for resizing and rescaling) 6198 */ 6199 getPlotLinePath = function (value, lineWidth, old) { 6200 var x1, 6201 y1, 6202 x2, 6203 y2, 6204 translatedValue = translate(value, null, null, old), 6205 cHeight = (old && oldChartHeight) || chartHeight, 6206 cWidth = (old && oldChartWidth) || chartWidth, 6207 skip; 6208 6209 x1 = x2 = mathRound(translatedValue + transB); 6210 y1 = y2 = mathRound(cHeight - translatedValue - transB); 6211 6212 if (isNaN(translatedValue)) { // no min or max 6213 skip = true; 6214 6215 } else if (horiz) {
6216 y1 = axisTop; 6217 y2 = cHeight - axisBottom; 6218 if (x1 < axisLeft || x1 > axisLeft + axisWidth) { 6219 skip = true; 6220 } 6221 } else { 6222 x1 = axisLeft; 6223 x2 = cWidth - axisRight; 6224 6225 if (y1 < axisTop || y1 > axisTop + axisHeight) { 6226 skip = true; 6227 } 6228 } 6229 return skip ? 6230 null : 6231 renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 0); 6232 }; 6233 6234 /** 6235 * Set the tick positions of a linear axis to round values like whole tens or every five. 6236 */ 6237 function getLinearTickPositions(tickInterval, min, max) { 6238 6239 var pos, 6240 lastPos, 6241 roundedMin = correctFloat(mathFloor(min / tickInterval) * tickInterval), 6242 roundedMax = correctFloat(mathCeil(max / tickInterval) * tickInterval), 6243 tickPositions = []; 6244 6245 // Populate the intermediate values 6246 pos = roundedMin; 6247 while (pos <= roundedMax) { 6248 6249 // Place the tick on the rounded value 6250 tickPositions.push(pos); 6251 6252 // Always add the raw tickInterval, not the corrected one. 6253 pos = correctFloat(pos + tickInterval); 6254 6255 // If the interval is not big enough in the current min - max range to actually increase 6256 // the loop variable, we need to break out to prevent endless loop. Issue #619 6257 if (pos === lastPos) { 6258 break; 6259 } 6260 6261 // Record the last value 6262 lastPos = pos; 6263 } 6264 return tickPositions; 6265 } 6266 6267 /** 6268 * Set the tick positions of a logarithmic axis 6269 */ 6270 function getLogTickPositions(interval, min, max, minor) { 6271 6272 // Since we use this method for both major and minor ticks, 6273 // use a local variable and return the result 6274 var positions = []; 6275 6276 // Reset 6277 if (!minor) { 6278 axis._minorAutoInterval = null; 6279 } 6280 6281 // First case: All ticks fall on whole logarithms: 1, 10, 100 etc. 6282 if (interval >= 0.5) { 6283 interval = mathRound(interval); 6284 positions = getLinearTickPositions(interval, min, max); 6285 6286 // Second case: We need intermediary ticks. For example 6287 // 1, 2, 4, 6, 8, 10, 20, 40 etc. 6288 } else if (interval >= 0.08) { 6289 var roundedMin = mathFloor(min), 6290 intermediate, 6291 i, 6292 j, 6293 len, 6294 pos, 6295 lastPos, 6296 break2; 6297 6298 if (interval > 0.3) { 6299 intermediate = [1, 2, 4]; 6300 } else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc 6301 intermediate = [1, 2, 4, 6, 8]; 6302 } else { // 0.1 equals ten minor ticks per 1, 10, 100 etc 6303 intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9]; 6304 } 6305 6306 for (i = roundedMin; i < max + 1 && !break2; i++) { 6307 len = intermediate.length; 6308 for (j = 0; j < len && !break2; j++) { 6309 pos = log2lin(lin2log(i) * intermediate[j]); 6310 6311 if (pos > min) { 6312 positions.push(lastPos); 6313 } 6314 6315 if (lastPos > max) { 6316 break2 = true; 6317 } 6318 lastPos = pos; 6319 } 6320 } 6321 6322 // Third case: We are so deep in between whole logarithmic values that 6323 // we might as well handle the tick positions like a linear axis. For 6324 // example 1.01, 1.02, 1.03, 1.04. 6325 } else { 6326 var realMin = lin2log(min), 6327 realMax = lin2log(max), 6328 tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'], 6329 filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption, 6330 tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1), 6331 totalPixelLength = minor ? axisLength / tickPositions.length : axisLength; 6332 6333 interval = pick( 6334 filteredTickIntervalOption, 6335 axis._minorAutoInterval, 6336 (realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1) 6337 ); 6338 6339 interval = normalizeTickInterval( 6340 interval, 6341 null, 6342 math.pow(10, mathFloor(math.log(interval) / math.LN10)) 6343 ); 6344 6345 positions = map(getLinearTickPositions( 6346 interval, 6347 realMin, 6348 realMax 6349 ), log2lin); 6350 6351 if (!minor) { 6352 axis._minorAutoInterval = interval / 5; 6353 } 6354 } 6355 6356 // Set the axis-level tickInterval variable 6357 if (!minor) { 6358 tickInterval = interval; 6359 } 6360 return positions; 6361 } 6362 6363 /** 6364 * Return the minor tick positions. For logarithmic axes, reuse the same logic 6365 * as for major ticks.
6366 */ 6367 function getMinorTickPositions() { 6368 var minorTickPositions = [], 6369 pos, 6370 i, 6371 len; 6372 6373 if (isLog) { 6374 len = tickPositions.length; 6375 for (i = 1; i < len; i++) { 6376 minorTickPositions = minorTickPositions.concat( 6377 getLogTickPositions(minorTickInterval, tickPositions[i - 1], tickPositions[i], true) 6378 ); 6379 } 6380 6381 } else { 6382 for (pos = min + (tickPositions[0] - min) % minorTickInterval; pos <= max; pos += minorTickInterval) { 6383 minorTickPositions.push(pos); 6384 } 6385 } 6386 6387 return minorTickPositions; 6388 } 6389 6390 /** 6391 * Adjust the min and max for the minimum range. Keep in mind that the series data is 6392 * not yet processed, so we don't have information on data cropping and grouping, or 6393 * updated axis.pointRange or series.pointRange. The data can't be processed until 6394 * we have finally established min and max. 6395 */ 6396 function adjustForMinRange() { 6397 var zoomOffset, 6398 spaceAvailable = dataMax - dataMin >= minRange, 6399 closestDataRange, 6400 i, 6401 distance, 6402 xData, 6403 loopLength, 6404 minArgs, 6405 maxArgs; 6406 6407 // Set the automatic minimum range based on the closest point distance 6408 if (isXAxis && minRange === UNDEFINED && !isLog) { 6409 6410 if (defined(options.min) || defined(options.max)) { 6411 minRange = null; // don't do this again 6412 6413 } else { 6414 6415 // Find the closest distance between raw data points, as opposed to 6416 // closestPointRange that applies to processed points (cropped and grouped) 6417 each(axis.series, function (series) { 6418 xData = series.xData; 6419 loopLength = series.xIncrement ? 1 : xData.length - 1; 6420 for (i = loopLength; i > 0; i--) { 6421 distance = xData[i] - xData[i - 1]; 6422 if (closestDataRange === UNDEFINED || distance < closestDataRange) { 6423 closestDataRange = distance; 6424 } 6425 } 6426 }); 6427 minRange = mathMin(closestDataRange * 5, dataMax - dataMin); 6428 } 6429 } 6430 6431 // if minRange is exceeded, adjust 6432 if (max - min < minRange) { 6433 6434 zoomOffset = (minRange - max + min) / 2; 6435 6436 // if min and max options have been set, don't go beyond it 6437 minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)]; 6438 if (spaceAvailable) { // if space is available, stay within the data range 6439 minArgs[2] = dataMin; 6440 } 6441 min = arrayMax(minArgs); 6442 6443 maxArgs = [min + minRange, pick(options.max, min + minRange)]; 6444 if (spaceAvailable) { // if space is availabe, stay within the data range 6445 maxArgs[2] = dataMax; 6446 } 6447 6448 max = arrayMin(maxArgs); 6449 6450 // now if the max is adjusted, adjust the min back 6451 if (max - min < minRange) { 6452 minArgs[0] = max - minRange; 6453 minArgs[1] = pick(options.min, max - minRange); 6454 min = arrayMax(minArgs); 6455 } 6456 } 6457 } 6458 6459 /** 6460 * Set the tick positions to round values and optionally extend the extremes 6461 * to the nearest tick 6462 */ 6463 function setTickPositions(secondPass) { 6464 6465 var length, 6466 linkedParentExtremes, 6467 tickIntervalOption = options.tickInterval, 6468 tickPixelIntervalOption = options.tickPixelInterval; 6469 6470 // linked axis gets the extremes from the parent axis 6471 if (isLinked) { 6472 linkedParent = chart[isXAxis ? 'xAxis' : 'yAxis'][options.linkedTo]; 6473 linkedParentExtremes = linkedParent.getExtremes(); 6474 min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin); 6475 max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax); 6476 if (options.type !== linkedParent.options.type) { 6477 error(11, 1); // Can't link axes of different type 6478 } 6479 } else { // initial min and max from the extreme data values 6480 min = pick(userMin, options.min, dataMin); 6481 max = pick(userMax, options.max, dataMax); 6482 } 6483 6484 if (isLog) { 6485 if (!secondPass && mathMin(min, dataMin) <= 0) { 6486 error(10, 1); // Can't plot negative values on log axis 6487 } 6488 min = log2lin(min); 6489 max = log2lin(max); 6490 } 6491 6492 // handle zoomed range 6493 if (range) { 6494 userMin = min = mathMax(min, max - range); // #618 6495 userMax = max; 6496 if (secondPass) { 6497 range = null; // don't use it when running setExtremes 6498 } 6499 } 6500 6501 // adjust min and max for the minimum range
6502 adjustForMinRange(); 6503 6504 // pad the values to get clear of the chart's edges 6505 if (!categories && !usePercentage && !isLinked && defined(min) && defined(max)) { 6506 length = (max - min) || 1; 6507 if (!defined(options.min) && !defined(userMin) && minPadding && (dataMin < 0 || !ignoreMinPadding)) { 6508 min -= length * minPadding; 6509 } 6510 if (!defined(options.max) && !defined(userMax) && maxPadding && (dataMax > 0 || !ignoreMaxPadding)) { 6511 max += length * maxPadding; 6512 } 6513 } 6514 6515 // get tickInterval 6516 if (min === max || min === undefined || max === undefined) { 6517 tickInterval = 1; 6518 } else if (isLinked && !tickIntervalOption && 6519 tickPixelIntervalOption === linkedParent.options.tickPixelInterval) { 6520 tickInterval = linkedParent.tickInterval; 6521 } else { 6522 tickInterval = pick( 6523 tickIntervalOption, 6524 categories ? // for categoried axis, 1 is default, for linear axis use tickPix 6525 1 : 6526 (max - min) * tickPixelIntervalOption / (axisLength || 1) 6527 ); 6528 } 6529 6530 // Now we're finished detecting min and max, crop and group series data. This 6531 // is in turn needed in order to find tick positions in ordinal axes. 6532 if (isXAxis && !secondPass) { 6533 each(axis.series, function (series) { 6534 series.processData(min !== oldMin || max !== oldMax); 6535 }); 6536 } 6537 6538 // set the translation factor used in translate function 6539 setAxisTranslation(); 6540 6541 // hook for ordinal axes. To do: merge with below
6542 if (axis.beforeSetTickPositions) { 6543 axis.beforeSetTickPositions(); 6544 } 6545 6546 // hook for extensions, used in Highstock ordinal axes 6547 if (axis.postProcessTickInterval) { 6548 tickInterval = axis.postProcessTickInterval(tickInterval); 6549 } 6550 6551 // for linear axes, get magnitude and normalize the interval 6552 if (!isDatetimeAxis && !isLog) { // linear 6553 magnitude = math.pow(10, mathFloor(math.log(tickInterval) / math.LN10)); 6554 if (!defined(options.tickInterval)) { 6555 tickInterval = normalizeTickInterval(tickInterval, null, magnitude, options); 6556 } 6557 } 6558 6559 // record the tick interval for linked axis 6560 axis.tickInterval = tickInterval; 6561 6562 // get minorTickInterval 6563 minorTickInterval = options.minorTickInterval === 'auto' && tickInterval ? 6564 tickInterval / 5 : options.minorTickInterval; 6565 6566 // find the tick positions 6567 tickPositions = options.tickPositions || (tickPositioner && tickPositioner.apply(axis, [min, max])); 6568 if (!tickPositions) { 6569 if (isDatetimeAxis) { 6570 tickPositions = (axis.getNonLinearTimeTicks || getTimeTicks)( 6571 normalizeTimeTickInterval(tickInterval, options.units), 6572 min, 6573 max, 6574 options.startOfWeek, 6575 axis.ordinalPositions, 6576 axis.closestPointRange, 6577 true 6578 ); 6579 } else if (isLog) { 6580 tickPositions = getLogTickPositions(tickInterval, min, max); 6581 } else { 6582 tickPositions = getLinearTickPositions(tickInterval, min, max); 6583 } 6584 } 6585 6586 if (!isLinked) { 6587 6588 // reset min/max or remove extremes based on start/end on tick 6589 var roundedMin = tickPositions[0], 6590 roundedMax = tickPositions[tickPositions.length - 1]; 6591 6592 if (options.startOnTick) { 6593 min = roundedMin; 6594 } else if (min > roundedMin) { 6595 tickPositions.shift(); 6596 } 6597 6598 if (options.endOnTick) { 6599 max = roundedMax; 6600 } else if (max < roundedMax) { 6601 tickPositions.pop(); 6602 } 6603 6604 // record the greatest number of ticks for multi axis 6605 if (!maxTicks) { // first call, or maxTicks have been reset after a zoom operation 6606 maxTicks = { 6607 x: 0, 6608 y: 0 6609 }; 6610 } 6611 6612 if (!isDatetimeAxis && tickPositions.length > maxTicks[xOrY] && options.alignTicks !== false) { 6613 maxTicks[xOrY] = tickPositions.length; 6614 } 6615 } 6616 } 6617 6618 /** 6619 * When using multiple axes, adjust the number of ticks to match the highest 6620 * number of ticks in that group 6621 */ 6622 function adjustTickAmount() { 6623 6624 if (maxTicks && maxTicks[xOrY] && !isDatetimeAxis && !categories && !isLinked && options.alignTicks !== false) { // only apply to linear scale 6625 var oldTickAmount = tickAmount, 6626 calculatedTickAmount = tickPositions.length; 6627 6628 // set the axis-level tickAmount to use below 6629 tickAmount = maxTicks[xOrY]; 6630 6631 if (calculatedTickAmount < tickAmount) { 6632 while (tickPositions.length < tickAmount) { 6633 tickPositions.push(correctFloat( 6634 tickPositions[tickPositions.length - 1] + tickInterval 6635 )); 6636 } 6637 transA *= (calculatedTickAmount - 1) / (tickAmount - 1); 6638 max = tickPositions[tickPositions.length - 1]; 6639 6640 } 6641 if (defined(oldTickAmount) && tickAmount !== oldTickAmount) { 6642 axis.isDirty = true; 6643 } 6644 } 6645 6646 6647 } 6648 6649 /** 6650 * Set the scale based on data min and max, user set min and max or options 6651 * 6652 */ 6653 function setScale() { 6654 var type, 6655 i, 6656 isDirtyData, 6657 isDirtyAxisLength; 6658 6659 oldMin = min; 6660 oldMax = max; 6661 oldAxisLength = axisLength; 6662 6663 // set the new axisLength 6664 axisLength = horiz ? axisWidth : axisHeight; 6665 isDirtyAxisLength = axisLength !== oldAxisLength; 6666 6667 // is there new data? 6668 each(axis.series, function (series) { 6669 if (series.isDirtyData || series.isDirty || 6670 series.xAxis.isDirty) { // when x axis is dirty, we need new data extremes for y as well 6671 isDirtyData = true; 6672 } 6673 }); 6674 6675 // do we really need to go through all this? 6676 if (isDirtyAxisLength || isDirtyData || isLinked || 6677 userMin !== oldUserMin || userMax !== oldUserMax) { 6678 6679 // get data extremes if needed 6680 getSeriesExtremes(); 6681 6682 // get fixed positions based on tickInterval 6683 setTickPositions(); 6684 6685 // record old values to decide whether a rescale is necessary later on (#540) 6686 oldUserMin = userMin; 6687 oldUserMax = userMax; 6688 6689 // reset stacks 6690 if (!isXAxis) { 6691 for (type in stacks) { 6692 for (i in stacks[type]) { 6693 stacks[type][i].cum = stacks[type][i].total; 6694 } 6695 } 6696 } 6697 6698 // Mark as dirty if it is not already set to dirty and extremes have changed. #595. 6699 if (!axis.isDirty) { 6700 axis.isDirty = isDirtyAxisLength || min !== oldMin || max !== oldMax; 6701 } 6702 } 6703 } 6704 6705 /** 6706 * Set the extremes and optionally redraw 6707 * @param {Number} newMin 6708 * @param {Number} newMax 6709 * @param {Boolean} redraw 6710 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation 6711 * configuration 6712 * @param {Object} eventArguments 6713 * 6714 */ 6715 function setExtremes(newMin, newMax, redraw, animation, eventArguments) { 6716 6717 redraw = pick(redraw, true); // defaults to true 6718 6719 // Extend the arguments with min and max 6720 eventArguments = extend(eventArguments, { 6721 min: newMin, 6722 max: newMax 6723 }); 6724 6725 // Fire the event 6726 fireEvent(axis, 'setExtremes', eventArguments, function () { // the default event handler 6727 6728 userMin = newMin; 6729 userMax = newMax; 6730 6731 // Mark for running afterSetExtremes 6732 axis.isDirtyExtremes = true; 6733 6734 // redraw 6735 if (redraw) { 6736 chart.redraw(animation); 6737 } 6738 }); 6739 } 6740 6741 /** 6742 * Update translation information 6743 */ 6744 setAxisTranslation = function () { 6745 var range = max - min, 6746 pointRange = 0, 6747 closestPointRange, 6748 seriesClosestPointRange; 6749 6750 // adjust translation for padding 6751 if (isXAxis) { 6752 if (isLinked) { 6753 pointRange = linkedParent.pointRange; 6754 } else { 6755 each(axis.series, function (series) { 6756 pointRange = mathMax(pointRange, series.pointRange); 6757 seriesClosestPointRange = series.closestPointRange; 6758 if (!series.noSharedTooltip && defined(seriesClosestPointRange)) { 6759 closestPointRange = defined(closestPointRange) ? 6760 mathMin(closestPointRange, seriesClosestPointRange) : 6761 seriesClosestPointRange; 6762 } 6763 }); 6764 } 6765 6766 // pointRange means the width reserved for each point, like in a column chart 6767 axis.pointRange = pointRange; 6768 6769 // closestPointRange means the closest distance between points. In columns 6770 // it is mostly equal to pointRange, but in lines pointRange is 0 while closestPointRange 6771 // is some other value 6772 axis.closestPointRange = closestPointRange; 6773 } 6774 6775 // secondary values 6776 oldTransA = transA; 6777 axis.translationSlope = transA = axisLength / ((range + pointRange) || 1); 6778 transB = horiz ? axisLeft : axisBottom; // translation addend 6779 minPixelPadding = transA * (pointRange / 2); 6780 }; 6781 6782 /** 6783 * Update the axis metrics 6784 */ 6785 function setAxisSize() { 6786 6787 var offsetLeft = options.offsetLeft || 0, 6788 offsetRight = options.offsetRight || 0; 6789 6790 // basic values 6791 axisLeft = pick(options.left, plotLeft + offsetLeft); 6792 axisTop = pick(options.top, plotTop); 6793 axisWidth = pick(options.width, plotWidth - offsetLeft + offsetRight); 6794 axisHeight = pick(options.height, plotHeight);
6795 axisBottom = chartHeight - axisHeight - axisTop; 6796 axisRight = chartWidth - axisWidth - axisLeft; 6797 axisLength = horiz ? axisWidth : axisHeight; 6798 6799 // expose to use in Series object and navigator 6800 axis.left = axisLeft; 6801 axis.top = axisTop; 6802 axis.len = axisLength; 6803 6804 } 6805 6806 /** 6807 * Get the actual axis extremes 6808 */ 6809 function getExtremes() { 6810 return { 6811 min: isLog ? correctFloat(lin2log(min)) : min, 6812 max: isLog ? correctFloat(lin2log(max)) : max, 6813 dataMin: dataMin, 6814 dataMax: dataMax, 6815 userMin: userMin, 6816 userMax: userMax 6817 }; 6818 } 6819 6820 /** 6821 * Get the zero plane either based on zero or on the min or max value. 6822 * Used in bar and area plots 6823 */ 6824 function getThreshold(threshold) { 6825 var realMin = isLog ? lin2log(min) : min, 6826 realMax = isLog ? lin2log(max) : max; 6827 6828 if (realMin > threshold || threshold === null) { 6829 threshold = realMin; 6830 } else if (realMax < threshold) { 6831 threshold = realMax; 6832 } 6833 6834 return translate(threshold, 0, 1, 0, 1); 6835 } 6836 6837 /** 6838 * Add a plot band or plot line after render time 6839 * 6840 * @param options {Object} The plotBand or plotLine configuration object 6841 */ 6842 function addPlotBandOrLine(options) { 6843 var obj = new PlotLineOrBand(options).render(); 6844 plotLinesAndBands.push(obj); 6845 return obj; 6846 } 6847 6848 /** 6849 * Render the tick labels to a preliminary position to get their sizes 6850 */ 6851 function getOffset() { 6852 6853 var hasData = axis.series.length && defined(min) && defined(max), 6854 showAxis = hasData || pick(options.showEmpty, true), 6855 titleOffset = 0, 6856 titleOffsetOption, 6857 titleMargin = 0, 6858 axisTitleOptions = options.title, 6859 labelOptions = options.labels, 6860 directionFactor = [-1, 1, 1, -1][side], 6861 n; 6862 6863 if (!axisGroup) { 6864 axisGroup = renderer.g('axis') 6865 .attr({ zIndex: 7 }) 6866 .add(); 6867 gridGroup = renderer.g('grid') 6868 .attr({ zIndex: options.gridZIndex || 1 }) 6869 .add(); 6870 } 6871 6872 labelOffset = 0; // reset 6873 6874 if (hasData || isLinked) { 6875 each(tickPositions, function (pos) { 6876 if (!ticks[pos]) { 6877 ticks[pos] = new Tick(pos); 6878 } else { 6879 ticks[pos].addLabel(); // update labels depending on tick interval 6880 } 6881 6882 }); 6883 6884 each(tickPositions, function (pos) { 6885 // left side must be align: right and right side must have align: left for labels 6886 if (side === 0 || side === 2 || { 1: 'left', 3: 'right' }[side] === labelOptions.align) { 6887 6888 // get the highest offset 6889 labelOffset = mathMax( 6890 ticks[pos].getLabelSize(), 6891 labelOffset 6892 ); 6893 } 6894 6895 }); 6896 6897 if (staggerLines) { 6898 labelOffset += (staggerLines - 1) * 16; 6899 } 6900 6901 } else { // doesn't have data 6902 for (n in ticks) { 6903 ticks[n].destroy(); 6904 delete ticks[n]; 6905 } 6906 } 6907 6908 if (axisTitleOptions && axisTitleOptions.text) { 6909 if (!axisTitle) { 6910 axisTitle = axis.axisTitle = renderer.text( 6911 axisTitleOptions.text, 6912 0, 6913 0, 6914 axisTitleOptions.useHTML 6915 ) 6916 .attr({ 6917 zIndex: 7, 6918 rotation: axisTitleOptions.rotation || 0, 6919 align: 6920 axisTitleOptions.textAlign || 6921 { low: 'left', middle: 'center', high: 'right' }[axisTitleOptions.align] 6922 }) 6923 .css(axisTitleOptions.style) 6924 .add(); 6925 axisTitle.isNew = true; 6926 } 6927 6928 if (showAxis) { 6929 titleOffset = axisTitle.getBBox()[horiz ? 'height' : 'width']; 6930 titleMargin = pick(axisTitleOptions.margin, horiz ? 5 : 10); 6931 titleOffsetOption = axisTitleOptions.offset; 6932 } 6933 6934 // hide or show the title depending on whether showEmpty is set 6935 axisTitle[showAxis ? 'show' : 'hide'](); 6936 6937 6938 } 6939 6940 // handle automatic or user set offset 6941 offset = directionFactor * pick(options.offset, axisOffset[side]); 6942 6943 axisTitleMargin = 6944 pick(titleOffsetOption, 6945 labelOffset + titleMargin + 6946 (side !== 2 && labelOffset && directionFactor * options.labels[horiz ? 'y' : 'x']) 6947 ); 6948 6949 axisOffset[side] = mathMax( 6950 axisOffset[side], 6951 axisTitleMargin + titleOffset + directionFactor * offset 6952 ); 6953 6954 } 6955 6956 /** 6957 * Render the axis 6958 */ 6959 function render() { 6960 var axisTitleOptions = options.title, 6961 stackLabelOptions = options.stackLabels, 6962 alternateGridColor = options.alternateGridColor, 6963 lineWidth = options.lineWidth, 6964 lineLeft, 6965 lineTop, 6966 linePath, 6967 hasRendered = chart.hasRendered, 6968 slideInTicks = hasRendered && defined(oldMin) && !isNaN(oldMin), 6969 hasData = axis.series.length && defined(min) && defined(max), 6970 showAxis = hasData || pick(options.showEmpty, true), 6971 from, 6972 to; 6973 6974 // If the series has data draw the ticks. Else only the line and title 6975 if (hasData || isLinked) { 6976 6977 // minor ticks 6978 if (minorTickInterval && !categories) { 6979 each(getMinorTickPositions(), function (pos) { 6980 if (!minorTicks[pos]) { 6981 minorTicks[pos] = new Tick(pos, 'minor'); 6982 } 6983 6984 // render new ticks in old position 6985 if (slideInTicks && minorTicks[pos].isNew) { 6986 minorTicks[pos].render(null, true); 6987 } 6988 6989 6990 minorTicks[pos].isActive = true; 6991 minorTicks[pos].render(); 6992 }); 6993 } 6994 6995 // Major ticks. Pull out the first item and render it last so that 6996 // we can get the position of the neighbour label. #808. 6997 each(tickPositions.slice(1).concat([tickPositions[0]]), function (pos, i) { 6998 6999 // Reorganize the indices 7000 i = (i === tickPositions.length - 1) ? 0 : i + 1; 7001 7002 // linked axes need an extra check to find out if 7003 if (!isLinked || (pos >= min && pos <= max)) { 7004 7005 if (!ticks[pos]) { 7006 ticks[pos] = new Tick(pos); 7007 } 7008 7009 // render new ticks in old position 7010 if (slideInTicks && ticks[pos].isNew) { 7011 ticks[pos].render(i, true); 7012 } 7013 7014 ticks[pos].isActive = true; 7015 ticks[pos].render(i); 7016 } 7017 7018 }); 7019 7020 // alternate grid color 7021 if (alternateGridColor) { 7022 each(tickPositions, function (pos, i) { 7023 if (i % 2 === 0 && pos < max) { 7024 if (!alternateBands[pos]) { 7025 alternateBands[pos] = new PlotLineOrBand(); 7026 } 7027 from = pos; 7028 to = tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] : max; 7029 alternateBands[pos].options = { 7030 from: isLog ? lin2log(from) : from, 7031 to: isLog ? lin2log(to) : to, 7032 color: alternateGridColor 7033 }; 7034 alternateBands[pos].render(); 7035 alternateBands[pos].isActive = true; 7036 } 7037 }); 7038 } 7039 7040 // custom plot lines and bands 7041 if (!axis._addedPlotLB) { // only first time 7042 each((options.plotLines || []).concat(options.plotBands || []), function (plotLineOptions) { 7043 //plotLinesAndBands.push(new PlotLineOrBand(plotLineOptions).render()); 7044 addPlotBandOrLine(plotLineOptions); 7045 }); 7046 axis._addedPlotLB = true; 7047 } 7048 7049 7050 7051 } // end if hasData 7052 7053 // remove inactive ticks 7054 each([ticks, minorTicks, alternateBands], function (coll) { 7055 var pos; 7056 for (pos in coll) { 7057 if (!coll[pos].isActive) { 7058 coll[pos].destroy(); 7059 delete coll[pos]; 7060 } else { 7061 coll[pos].isActive = false; // reset 7062 } 7063 } 7064 }); 7065 7066 7067 7068 7069 // Static items. As the axis group is cleared on subsequent calls 7070 // to render, these items are added outside the group. 7071 // axis line 7072 if (lineWidth) { 7073 lineLeft = axisLeft + (opposite ? axisWidth : 0) + offset; 7074 lineTop = chartHeight - axisBottom - (opposite ? axisHeight : 0) + offset; 7075 7076 linePath = renderer.crispLine([ 7077 M, 7078 horiz ? 7079 axisLeft : 7080 lineLeft, 7081 horiz ? 7082 lineTop : 7083 axisTop, 7084 L, 7085 horiz ? 7086 chartWidth - axisRight : 7087 lineLeft, 7088 horiz ? 7089 lineTop :
7090 chartHeight - axisBottom 7091 ], lineWidth); 7092 if (!axisLine) { 7093 axisLine = renderer.path(linePath) 7094 .attr({ 7095 stroke: options.lineColor, 7096 'stroke-width': lineWidth, 7097 zIndex: 7 7098 }) 7099 .add(); 7100 } else { 7101 axisLine.animate({ d: linePath }); 7102 } 7103 7104 // show or hide the line depending on options.showEmpty 7105 axisLine[showAxis ? 'show' : 'hide'](); 7106 7107 } 7108 7109 if (axisTitle && showAxis) { 7110 // compute anchor points for each of the title align options 7111 var margin = horiz ? axisLeft : axisTop, 7112 fontSize = pInt(axisTitleOptions.style.fontSize || 12), 7113 // the position in the length direction of the axis 7114 alongAxis = { 7115 low: margin + (horiz ? 0 : axisLength), 7116 middle: margin + axisLength / 2, 7117 high: margin + (horiz ? axisLength : 0) 7118 }[axisTitleOptions.align], 7119 7120 // the position in the perpendicular direction of the axis 7121 offAxis = (horiz ? axisTop + axisHeight : axisLeft) + 7122 (horiz ? 1 : -1) * // horizontal axis reverses the margin 7123 (opposite ? -1 : 1) * // so does opposite axes 7124 axisTitleMargin + 7125 (side === 2 ? fontSize : 0); 7126 7127 axisTitle[axisTitle.isNew ? 'attr' : 'animate']({ 7128 x: horiz ? 7129 alongAxis : 7130 offAxis + (opposite ? axisWidth : 0) + offset + 7131 (axisTitleOptions.x || 0), // x 7132 y: horiz ? 7133 offAxis - (opposite ? axisHeight : 0) + offset : 7134 alongAxis + (axisTitleOptions.y || 0) // y 7135 }); 7136 axisTitle.isNew = false; 7137 } 7138 7139 // Stacked totals: 7140 if (stackLabelOptions && stackLabelOptions.enabled) { 7141 var stackKey, oneStack, stackCategory, 7142 stackTotalGroup = axis.stackTotalGroup; 7143 7144 // Create a separate group for the stack total labels 7145 if (!stackTotalGroup) { 7146 axis.stackTotalGroup = stackTotalGroup = 7147 renderer.g('stack-labels') 7148 .attr({ 7149 visibility: VISIBLE, 7150 zIndex: 6 7151 }) 7152 .translate(plotLeft, plotTop) 7153 .add(); 7154 } 7155 7156 // Render each stack total 7157 for (stackKey in stacks) { 7158 oneStack = stacks[stackKey]; 7159 for (stackCategory in oneStack) { 7160 oneStack[stackCategory].render(stackTotalGroup); 7161 } 7162 } 7163 } 7164 // End stacked totals 7165 7166 axis.isDirty = false; 7167 } 7168 7169 /** 7170 * Remove a plot band or plot line from the chart by id 7171 * @param {Object} id 7172 */ 7173 function removePlotBandOrLine(id) { 7174 var i = plotLinesAndBands.length; 7175 while (i--) { 7176 if (plotLinesAndBands[i].id === id) { 7177 plotLinesAndBands[i].destroy(); 7178 } 7179 } 7180 } 7181 7182 /** 7183 * Update the axis title by options 7184 */ 7185 function setTitle(newTitleOptions, redraw) { 7186 options.title = merge(options.title, newTitleOptions); 7187 7188 axisTitle = axisTitle.destroy(); 7189 axis.isDirty = true; 7190 7191 if (pick(redraw, true)) { 7192 chart.redraw(); 7193 } 7194 } 7195 7196 /** 7197 * Redraw the axis to reflect changes in the data or axis extremes 7198 */ 7199 function redraw() { 7200 7201 // hide tooltip and hover states 7202 if (tracker.resetTracker) { 7203 tracker.resetTracker(); 7204 } 7205 7206 // render the axis 7207 render(); 7208 7209 // move plot lines and bands 7210 each(plotLinesAndBands, function (plotLine) { 7211 plotLine.render(); 7212 }); 7213 7214 // mark associated series as dirty and ready for redraw 7215 each(axis.series, function (series) { 7216 series.isDirty = true; 7217 }); 7218 7219 } 7220 7221 /** 7222 * Set new axis categories and optionally redraw 7223 * @param {Array} newCategories 7224 * @param {Boolean} doRedraw 7225 */ 7226 function setCategories(newCategories, doRedraw) { 7227 // set the categories 7228 axis.categories = userOptions.categories = categories = newCategories; 7229 7230 // force reindexing tooltips 7231 each(axis.series, function (series) { 7232 series.translate(); 7233 series.setTooltipPoints(true); 7234 }); 7235 7236 7237 // optionally redraw 7238 axis.isDirty = true; 7239 7240 if (pick(doRedraw, true)) { 7241 chart.redraw(); 7242 } 7243 } 7244 7245 /** 7246 * Destroys an Axis instance. 7247 */ 7248 function destroy() { 7249 var stackKey; 7250 7251 // Remove the events 7252 removeEvent(axis); 7253 7254 // Destroy each stack total 7255 for (stackKey in stacks) { 7256 destroyObjectProperties(stacks[stackKey]); 7257 7258 stacks[stackKey] = null; 7259 } 7260 7261 // Destroy stack total group
7262 if (axis.stackTotalGroup) { 7263 axis.stackTotalGroup = axis.stackTotalGroup.destroy(); 7264 } 7265 7266 // Destroy collections 7267 each([ticks, minorTicks, alternateBands, plotLinesAndBands], function (coll) { 7268 destroyObjectProperties(coll); 7269 }); 7270 7271 // Destroy local variables 7272 each([axisLine, axisGroup, gridGroup, axisTitle], function (obj) { 7273 if (obj) { 7274 obj.destroy(); 7275 } 7276 }); 7277 axisLine = axisGroup = gridGroup = axisTitle = null; 7278 } 7279 7280 7281 // Run Axis 7282 7283 // Register 7284 axes.push(axis); 7285 chart[isXAxis ? 'xAxis' : 'yAxis'].push(axis); 7286 7287 // inverted charts have reversed xAxes as default 7288 if (inverted && isXAxis && reversed === UNDEFINED) { 7289 reversed = true; 7290 } 7291 7292 7293 // expose some variables 7294 extend(axis, { 7295 addPlotBand: addPlotBandOrLine, 7296 addPlotLine: addPlotBandOrLine, 7297 adjustTickAmount: adjustTickAmount, 7298 categories: categories, 7299 getExtremes: getExtremes, 7300 getPlotLinePath: getPlotLinePath, 7301 getThreshold: getThreshold, 7302 isXAxis: isXAxis, 7303 options: options, 7304 plotLinesAndBands: plotLinesAndBands, 7305 getOffset: getOffset, 7306 render: render, 7307 setAxisSize: setAxisSize, 7308 setAxisTranslation: setAxisTranslation, 7309 setCategories: setCategories, 7310 setExtremes: setExtremes, 7311 setScale: setScale, 7312 setTickPositions: setTickPositions, 7313 translate: translate, 7314 redraw: redraw, 7315 removePlotBand: removePlotBandOrLine, 7316 removePlotLine: removePlotBandOrLine, 7317 reversed: reversed, 7318 setTitle: setTitle, 7319 series: [], // populated by Series 7320 stacks: stacks, 7321 destroy: destroy 7322 }); 7323 7324 // register event listeners 7325 for (eventType in events) { 7326 addEvent(axis, eventType, events[eventType]); 7327 } 7328 7329 // extend logarithmic axis 7330 if (isLog) { 7331 axis.val2lin = log2lin; 7332 axis.lin2val = lin2log; 7333 } 7334 7335 } // end Axis 7336 7337 7338 /** 7339 * The tooltip object 7340 * @param {Object} options Tooltip options 7341 */ 7342 function Tooltip(options) { 7343 var currentSeries, 7344 borderWidth = options.borderWidth, 7345 crosshairsOptions = options.crosshairs, 7346 crosshairs = [], 7347 style = options.style, 7348 shared = options.shared, 7349 padding = pInt(style.padding), 7350 tooltipIsHidden = true, 7351 currentX = 0, 7352 currentY = 0; 7353 7354 // remove padding CSS and apply padding on box instead 7355 style.padding = 0; 7356 7357 // create the label 7358 var label = renderer.label('', 0, 0, null, null, null, options.useHTML) 7359 .attr({ 7360 padding: padding, 7361 fill: options.backgroundColor, 7362 'stroke-width': borderWidth, 7363 r: options.borderRadius, 7364 zIndex: 8 7365 }) 7366 .css(style) 7367 .hide() 7368 .add(); 7369 7370 // When using canVG the shadow shows up as a gray circle 7371 // even if the tooltip is hidden. 7372 if (!useCanVG) { 7373 label.shadow(options.shadow); 7374 } 7375 7376 /** 7377 * Destroy the tooltip and its elements. 7378 */ 7379 function destroy() { 7380 each(crosshairs, function (crosshair) { 7381 if (crosshair) {
7382 crosshair.destroy(); 7383 } 7384 }); 7385 7386 // Destroy and clear local variables 7387 if (label) { 7388 label = label.destroy(); 7389 } 7390 } 7391 7392 /** 7393 * In case no user defined formatter is given, this will be used 7394 */ 7395 function defaultFormatter() { 7396 var pThis = this, 7397 items = pThis.points || splat(pThis), 7398 series = items[0].series, 7399 s; 7400 7401 // build the header 7402 s = [series.tooltipHeaderFormatter(items[0].key)]; 7403 7404 // build the values 7405 each(items, function (item) { 7406 series = item.series; 7407 s.push((series.tooltipFormatter && series.tooltipFormatter(item)) || 7408 item.point.tooltipFormatter(series.tooltipOptions.pointFormat)); 7409 }); 7410 7411 // footer 7412 s.push(options.footerFormat || ''); 7413 7414 return s.join(''); 7415 } 7416 7417 /** 7418 * Provide a soft movement for the tooltip 7419 * 7420 * @param {Number} finalX 7421 * @param {Number} finalY 7422 */ 7423 function move(finalX, finalY) { 7424 7425 // get intermediate values for animation 7426 currentX = tooltipIsHidden ? finalX : (2 * currentX + finalX) / 3; 7427 currentY = tooltipIsHidden ? finalY : (currentY + finalY) / 2; 7428 7429 // move to the intermediate value 7430 label.attr({ x: currentX, y: currentY }); 7431 7432 // run on next tick of the mouse tracker 7433 if (mathAbs(finalX - currentX) > 1 || mathAbs(finalY - currentY) > 1) { 7434 tooltipTick = function () { 7435 move(finalX, finalY); 7436 }; 7437 } else { 7438 tooltipTick = null; 7439 } 7440 } 7441 7442 /** 7443 * Hide the tooltip 7444 */ 7445 function hide() { 7446 if (!tooltipIsHidden) { 7447 var hoverPoints = chart.hoverPoints; 7448 7449 label.hide(); 7450 7451 // hide previous hoverPoints and set new 7452 if (hoverPoints) { 7453 each(hoverPoints, function (point) { 7454 point.setState(); 7455 }); 7456 } 7457 chart.hoverPoints = null; 7458 7459 7460 tooltipIsHidden = true; 7461 } 7462 7463 } 7464 7465 /** 7466 * Hide the crosshairs 7467 */ 7468 function hideCrosshairs() { 7469 each(crosshairs, function (crosshair) { 7470 if (crosshair) { 7471 crosshair.hide(); 7472 } 7473 }); 7474 } 7475 7476 /** 7477 * Refresh the tooltip's text and position. 7478 * @param {Object} point 7479 * 7480 */ 7481 function refresh(point) { 7482 var x, 7483 y, 7484 show, 7485 plotX, 7486 plotY, 7487 textConfig = {}, 7488 text, 7489 pointConfig = [], 7490 tooltipPos = point.tooltipPos, 7491 formatter = options.formatter || defaultFormatter, 7492 hoverPoints = chart.hoverPoints, 7493 placedTooltipPoint, 7494 borderColor; 7495 7496 // shared tooltip, array is sent over 7497 if (shared && !(point.series && point.series.noSharedTooltip)) { 7498 plotY = 0; 7499 7500 // hide previous hoverPoints and set new 7501 if (hoverPoints) { 7502 each(hoverPoints, function (point) { 7503 point.setState(); 7504 }); 7505 } 7506 chart.hoverPoints = point; 7507 7508 each(point, function (item) { 7509 item.setState(HOVER_STATE); 7510 plotY += item.plotY; // for average 7511 7512 pointConfig.push(item.getLabelConfig()); 7513 }); 7514 7515 plotX = point[0].plotX; 7516 plotY = mathRound(plotY) / point.length; // mathRound because Opera 10 has problems here 7517 7518 textConfig = { 7519 x: point[0].category 7520 }; 7521 textConfig.points = pointConfig; 7522 point = point[0]; 7523 7524 // single point tooltip 7525 } else { 7526 textConfig = point.getLabelConfig(); 7527 } 7528 text = formatter.call(textConfig); 7529 7530 // register the current series 7531 currentSeries = point.series; 7532 7533 // get the reference point coordinates (pie charts use tooltipPos) 7534 plotX = pick(plotX, point.plotX); 7535 plotY = pick(plotY, point.plotY); 7536 7537 x = mathRound(tooltipPos ? tooltipPos[0] : (inverted ? plotWidth - plotY : plotX)); 7538 y = mathRound(tooltipPos ? tooltipPos[1] : (inverted ? plotHeight - plotX : plotY)); 7539 7540 7541 // For line type series, hide tooltip if the point falls outside the plot 7542 show = shared || !currentSeries.isCartesian || currentSeries.tooltipOutsidePlot || isInsidePlot(x, y); 7543 7544 // update the inner HTML 7545 if (text === false || !show) { 7546 hide(); 7547 } else { 7548 7549 // show it 7550 if (tooltipIsHidden) { 7551 label.show(); 7552 tooltipIsHidden = false; 7553 } 7554 7555 // update text 7556 label.attr({ 7557 text: text 7558 }); 7559 7560 // set the stroke color of the box 7561 borderColor = options.borderColor || point.color || currentSeries.color || '#606060'; 7562 label.attr({ 7563 stroke: borderColor 7564 }); 7565 7566 placedTooltipPoint = placeBox( 7567 label.width, 7568 label.height, 7569 plotLeft, 7570 plotTop, 7571 plotWidth, 7572 plotHeight, 7573 {x: x, y: y}, 7574 pick(options.distance, 12), 7575 inverted 7576 ); 7577 7578 // do the move 7579 move(mathRound(placedTooltipPoint.x), mathRound(placedTooltipPoint.y)); 7580 } 7581 7582 7583 // crosshairs 7584 if (crosshairsOptions) { 7585 crosshairsOptions = splat(crosshairsOptions); // [x, y] 7586 7587 var path, 7588 i = crosshairsOptions.length, 7589 attribs, 7590 axis; 7591 7592 while (i--) { 7593 axis = point.series[i ? 'yAxis' : 'xAxis']; 7594 if (crosshairsOptions[i] && axis) { 7595 path = axis.getPlotLinePath( 7596 i ? pick(point.stackY, point.y) : point.x, // #814 7597 1 7598 ); 7599 if (crosshairs[i]) { 7600 crosshairs[i].attr({ d: path, visibility: VISIBLE }); 7601 7602 } else { 7603 attribs = { 7604 'stroke-width': crosshairsOptions[i].width || 1, 7605 stroke: crosshairsOptions[i].color || '#C0C0C0', 7606 zIndex: crosshairsOptions[i].zIndex || 2 7607 }; 7608 if (crosshairsOptions[i].dashStyle) { 7609 attribs.dashstyle = crosshairsOptions[i].dashStyle; 7610 } 7611 crosshairs[i] = renderer.path(path) 7612 .attr(attribs) 7613 .add(); 7614 } 7615 } 7616 } 7617 } 7618 fireEvent(chart, 'tooltipRefresh', { 7619 text: text, 7620 x: x + plotLeft, 7621 y: y + plotTop, 7622 borderColor: borderColor 7623 }); 7624 } 7625 7626 7627 7628 // public members 7629 return { 7630 shared: shared, 7631 refresh: refresh, 7632 hide: hide, 7633 hideCrosshairs: hideCrosshairs, 7634 destroy: destroy 7635 }; 7636 } 7637 7638 /** 7639 * The mouse tracker object 7640 * @param {Object} options 7641 */ 7642 function MouseTracker(options) { 7643 7644 7645 var mouseDownX, 7646 mouseDownY, 7647 hasDragged, 7648 selectionMarker, 7649 zoomType = useCanVG ? '' : optionsChart.zoomType, 7650 zoomX = /x/.test(zoomType), 7651 zoomY = /y/.test(zoomType), 7652 zoomHor = (zoomX && !inverted) || (zoomY && inverted), 7653 zoomVert = (zoomY && !inverted) || (zoomX && inverted); 7654 7655 /** 7656 * Add crossbrowser support for chartX and chartY 7657 * @param {Object} e The event object in standard browsers 7658 */ 7659 function normalizeMouseEvent(e) { 7660 var ePos, 7661 chartPosLeft, 7662 chartPosTop, 7663 chartX, 7664 chartY; 7665 7666 // common IE normalizing 7667 e = e || win.event; 7668 if (!e.target) { 7669 e.target = e.srcElement; 7670 } 7671 7672 // jQuery only copies over some properties. IE needs e.x and iOS needs touches. 7673 if (e.originalEvent) { 7674 e = e.originalEvent; 7675 } 7676 7677 // The same for MooTools. It renames e.pageX to e.page.x. #445. 7678 if (e.event) { 7679 e = e.event; 7680 } 7681 7682 // iOS 7683 ePos = e.touches ? e.touches.item(0) : e; 7684 7685 // get mouse position 7686 chartPosition = offset(container); 7687 chartPosLeft = chartPosition.left; 7688 chartPosTop = chartPosition.top; 7689 7690 // chartX and chartY 7691 if (isIE) { // IE including IE9 that has pageX but in a different meaning 7692 chartX = e.x; 7693 chartY = e.y; 7694 } else {
7695 chartX = ePos.pageX - chartPosLeft; 7696 chartY = ePos.pageY - chartPosTop; 7697 } 7698 7699 return extend(e, { 7700 chartX: mathRound(chartX), 7701 chartY: mathRound(chartY) 7702 }); 7703 } 7704 7705 /** 7706 * Get the click position in terms of axis values. 7707 * 7708 * @param {Object} e A mouse event 7709 */ 7710 function getMouseCoordinates(e) { 7711 var coordinates = { 7712 xAxis: [], 7713 yAxis: [] 7714 }; 7715 each(axes, function (axis) { 7716 var translate = axis.translate, 7717 isXAxis = axis.isXAxis, 7718 isHorizontal = inverted ? !isXAxis : isXAxis; 7719 7720 coordinates[isXAxis ? 'xAxis' : 'yAxis'].push({ 7721 axis: axis, 7722 value: translate( 7723 isHorizontal ? 7724 e.chartX - plotLeft : 7725 plotHeight - e.chartY + plotTop, 7726 true 7727 ) 7728 }); 7729 }); 7730 return coordinates; 7731 } 7732 7733 /** 7734 * With line type charts with a single tracker, get the point closest to the mouse 7735 */ 7736 function onmousemove(e) { 7737 var point, 7738 points, 7739 hoverPoint = chart.hoverPoint, 7740 hoverSeries = chart.hoverSeries, 7741 i, 7742 j, 7743 distance = chartWidth, 7744 index = inverted ? e.chartY : e.chartX - plotLeft; // wtf? 7745 7746 // shared tooltip 7747 if (tooltip && options.shared && !(hoverSeries && hoverSeries.noSharedTooltip)) { 7748 points = []; 7749 7750 // loop over all series and find the ones with points closest to the mouse 7751 i = series.length; 7752 for (j = 0; j < i; j++) { 7753 if (series[j].visible && 7754 series[j].options.enableMouseTracking !== false && 7755 !series[j].noSharedTooltip && series[j].tooltipPoints.length) { 7756 point = series[j].tooltipPoints[index]; 7757 point._dist = mathAbs(index - point.plotX); 7758 distance = mathMin(distance, point._dist); 7759 points.push(point); 7760 } 7761 } 7762 // remove furthest points 7763 i = points.length; 7764 while (i--) { 7765 if (points[i]._dist > distance) { 7766 points.splice(i, 1); 7767 } 7768 } 7769 // refresh the tooltip if necessary 7770 if (points.length && (points[0].plotX !== hoverX)) { 7771 tooltip.refresh(points); 7772 hoverX = points[0].plotX; 7773 } 7774 } 7775 7776 // separate tooltip and general mouse events 7777 if (hoverSeries && hoverSeries.tracker) { // only use for line-type series with common tracker 7778 7779 // get the point 7780 point = hoverSeries.tooltipPoints[index]; 7781 7782 // a new point is hovered, refresh the tooltip 7783 if (point && point !== hoverPoint) { 7784 7785 // trigger the events 7786 point.onMouseOver(); 7787 7788 } 7789 } 7790 } 7791 7792 7793 7794 /** 7795 * Reset the tracking by hiding the tooltip, the hover series state and the hover point 7796 */ 7797 function resetTracker() { 7798 var hoverSeries = chart.hoverSeries, 7799 hoverPoint = chart.hoverPoint; 7800 7801 if (hoverPoint) { 7802 hoverPoint.onMouseOut(); 7803 } 7804 7805 if (hoverSeries) { 7806 hoverSeries.onMouseOut(); 7807 } 7808 7809 if (tooltip) { 7810 tooltip.hide(); 7811 tooltip.hideCrosshairs(); 7812 } 7813 7814 hoverX = null; 7815 } 7816 7817 /** 7818 * Mouse up or outside the plot area 7819 */ 7820 function drop() { 7821 if (selectionMarker) { 7822 var selectionData = { 7823 xAxis: [], 7824 yAxis: [] 7825 }, 7826 selectionBox = selectionMarker.getBBox(), 7827 selectionLeft = selectionBox.x - plotLeft, 7828 selectionTop = selectionBox.y - plotTop; 7829 7830 7831 // a selection has been made 7832 if (hasDragged) { 7833 7834 // record each axis' min and max 7835 each(axes, function (axis) { 7836 if (axis.options.zoomEnabled !== false) { 7837 var translate = axis.translate, 7838 isXAxis = axis.isXAxis, 7839 isHorizontal = inverted ? !isXAxis : isXAxis, 7840 selectionMin = translate( 7841 isHorizontal ? 7842 selectionLeft : 7843 plotHeight - selectionTop - selectionBox.height, 7844 true, 7845 0, 7846 0, 7847 1 7848 ), 7849 selectionMax = translate( 7850 isHorizontal ? 7851 selectionLeft + selectionBox.width : 7852 plotHeight - selectionTop, 7853 true, 7854 0, 7855 0, 7856 1 7857 ); 7858 7859 selectionData[isXAxis ? 'xAxis' : 'yAxis'].push({ 7860 axis: axis, 7861 min: mathMin(selectionMin, selectionMax), // for reversed axes, 7862 max: mathMax(selectionMin, selectionMax) 7863 }); 7864 } 7865 }); 7866 fireEvent(chart, 'selection', selectionData, zoom); 7867 7868 } 7869 selectionMarker = selectionMarker.destroy(); 7870 } 7871 7872 css(container, { cursor: 'auto' }); 7873 7874 chart.mouseIsDown = mouseIsDown = hasDragged = false;
7875 removeEvent(doc, hasTouch ? 'touchend' : 'mouseup', drop); 7876 7877 } 7878 7879 /** 7880 * Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea. 7881 */ 7882 function hideTooltipOnMouseMove(e) { 7883 var pageX = defined(e.pageX) ? e.pageX : e.page.x, // In mootools the event is wrapped and the page x/y position is named e.page.x 7884 pageY = defined(e.pageX) ? e.pageY : e.page.y; // Ref: http://mootools.net/docs/core/Types/DOMEvent 7885 7886 if (chartPosition && 7887 !isInsidePlot(pageX - chartPosition.left - plotLeft, 7888 pageY - chartPosition.top - plotTop)) { 7889 resetTracker(); 7890 } 7891 } 7892 7893 /** 7894 * When mouse leaves the container, hide the tooltip. 7895 */ 7896 function hideTooltipOnMouseLeave() { 7897 resetTracker(); 7898 chartPosition = null; // also reset the chart position, used in #149 fix 7899 } 7900 7901 /** 7902 * Set the JS events on the container element 7903 */ 7904 function setDOMEvents() { 7905 var lastWasOutsidePlot = true; 7906 /* 7907 * Record the starting position of a dragoperation 7908 */ 7909 container.onmousedown = function (e) { 7910 e = normalizeMouseEvent(e); 7911 7912 // issue #295, dragging not always working in Firefox 7913 if (!hasTouch && e.preventDefault) { 7914 e.preventDefault(); 7915 } 7916 7917 // record the start position 7918 chart.mouseIsDown = mouseIsDown = true; 7919 chart.mouseDownX = mouseDownX = e.chartX; 7920 mouseDownY = e.chartY; 7921 7922 addEvent(doc, hasTouch ? 'touchend' : 'mouseup', drop); 7923 }; 7924 7925 // The mousemove, touchmove and touchstart event handler 7926 var mouseMove = function (e) { 7927 7928 // let the system handle multitouch operations like two finger scroll 7929 // and pinching 7930 if (e && e.touches && e.touches.length > 1) { 7931 return; 7932 } 7933 7934 // normalize 7935 e = normalizeMouseEvent(e); 7936 if (!hasTouch) { // not for touch devices 7937 e.returnValue = false; 7938 } 7939 7940 var chartX = e.chartX, 7941 chartY = e.chartY, 7942 isOutsidePlot = !isInsidePlot(chartX - plotLeft, chartY - plotTop); 7943 7944 // on touch devices, only trigger click if a handler is defined 7945 if (hasTouch && e.type === 'touchstart') { 7946 if (attr(e.target, 'isTracker')) { 7947 if (!chart.runTrackerClick) { 7948 e.preventDefault(); 7949 } 7950 } else if (!runChartClick && !isOutsidePlot) { 7951 e.preventDefault(); 7952 } 7953 } 7954 7955 // cancel on mouse outside 7956 if (isOutsidePlot) { 7957 7958 /*if (!lastWasOutsidePlot) { 7959 // reset the tracker 7960 resetTracker(); 7961 }*/ 7962 7963 // drop the selection if any and reset mouseIsDown and hasDragged 7964 //drop(); 7965 if (chartX < plotLeft) { 7966 chartX = plotLeft; 7967 } else if (chartX > plotLeft + plotWidth) { 7968 chartX = plotLeft + plotWidth; 7969 } 7970 7971 if (chartY < plotTop) { 7972 chartY = plotTop; 7973 } else if (chartY > plotTop + plotHeight) { 7974 chartY = plotTop + plotHeight; 7975 } 7976 7977 } 7978 7979 if (mouseIsDown && e.type !== 'touchstart') { // make selection 7980 7981 // determine if the mouse has moved more than 10px 7982 hasDragged = Math.sqrt( 7983 Math.pow(mouseDownX - chartX, 2) + 7984 Math.pow(mouseDownY - chartY, 2) 7985 ); 7986 if (hasDragged > 10) { 7987 var clickedInside = isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop); 7988 7989 // make a selection 7990 if (hasCartesianSeries && (zoomX || zoomY) && clickedInside) { 7991 if (!selectionMarker) { 7992 selectionMarker = renderer.rect( 7993 plotLeft, 7994 plotTop, 7995 zoomHor ? 1 : plotWidth, 7996 zoomVert ? 1 : plotHeight, 7997 0 7998 ) 7999 .attr({ 8000 fill: optionsChart.selectionMarkerFill || 'rgba(69,114,167,0.25)', 8001 zIndex: 7 8002 }) 8003 .add(); 8004 } 8005 } 8006 8007 // adjust the width of the selection marker 8008 if (selectionMarker && zoomHor) { 8009 var xSize = chartX - mouseDownX; 8010 selectionMarker.attr({ 8011 width: mathAbs(xSize), 8012 x: (xSize > 0 ? 0 : xSize) + mouseDownX 8013 }); 8014 } 8015 // adjust the height of the selection marker 8016 if (selectionMarker && zoomVert) { 8017 var ySize = chartY - mouseDownY; 8018 selectionMarker.attr({ 8019 height: mathAbs(ySize), 8020 y: (ySize > 0 ? 0 : ySize) + mouseDownY 8021 }); 8022 } 8023 8024 // panning 8025 if (clickedInside && !selectionMarker && optionsChart.panning) { 8026 chart.pan(chartX); 8027 } 8028 } 8029 8030 } else if (!isOutsidePlot) { 8031 // show the tooltip 8032 onmousemove(e); 8033 } 8034 8035 lastWasOutsidePlot = isOutsidePlot; 8036 8037 // when outside plot, allow touch-drag by returning true 8038 return isOutsidePlot || !hasCartesianSeries; 8039 }; 8040 8041 /* 8042 * When the mouse enters the container, run mouseMove 8043 */ 8044 container.onmousemove = mouseMove; 8045 8046 /* 8047 * When the mouse leaves the container, hide the tracking (tooltip). 8048 */ 8049 addEvent(container, 'mouseleave', hideTooltipOnMouseLeave); 8050 8051 // issue #149 workaround 8052 // The mouseleave event above does not always fire. Whenever the mouse is moving 8053 // outside the plotarea, hide the tooltip 8054 addEvent(doc, 'mousemove', hideTooltipOnMouseMove); 8055 8056 container.ontouchstart = function (e) { 8057 // For touch devices, use touchmove to zoom 8058 if (zoomX || zoomY) { 8059 container.onmousedown(e); 8060 }
8061 // Show tooltip and prevent the lower mouse pseudo event 8062 mouseMove(e); 8063 }; 8064 8065 /* 8066 * Allow dragging the finger over the chart to read the values on touch 8067 * devices 8068 */ 8069 container.ontouchmove = mouseMove; 8070 8071 /* 8072 * Allow dragging the finger over the chart to read the values on touch 8073 * devices 8074 */ 8075 container.ontouchend = function () { 8076 if (hasDragged) { 8077 resetTracker(); 8078 } 8079 }; 8080 8081 8082 // MooTools 1.2.3 doesn't fire this in IE when using addEvent 8083 container.onclick = function (e) { 8084 var hoverPoint = chart.hoverPoint; 8085 e = normalizeMouseEvent(e); 8086 8087 e.cancelBubble = true; // IE specific 8088 8089 8090 if (!hasDragged) { 8091 8092 // Detect clicks on trackers or tracker groups, #783 8093 if (hoverPoint && (attr(e.target, 'isTracker') || attr(e.target.parentNode, 'isTracker'))) { 8094 var plotX = hoverPoint.plotX, 8095 plotY = hoverPoint.plotY; 8096 8097 // add page position info 8098 extend(hoverPoint, { 8099 pageX: chartPosition.left + plotLeft + 8100 (inverted ? plotWidth - plotY : plotX), 8101 pageY: chartPosition.top + plotTop + 8102 (inverted ? plotHeight - plotX : plotY) 8103 }); 8104 8105 // the series click event 8106 fireEvent(hoverPoint.series, 'click', extend(e, { 8107 point: hoverPoint 8108 })); 8109 8110 // the point click event 8111 hoverPoint.firePointEvent('click', e); 8112 8113 } else { 8114 extend(e, getMouseCoordinates(e)); 8115 8116 // fire a click event in the chart 8117 if (isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) { 8118 fireEvent(chart, 'click', e); 8119 } 8120 } 8121 8122 8123 } 8124 // reset mouseIsDown and hasDragged 8125 hasDragged = false; 8126 }; 8127 8128 } 8129 8130 /** 8131 * Destroys the MouseTracker object and disconnects DOM events. 8132 */ 8133 function destroy() { 8134 // Destroy the tracker group element 8135 if (chart.trackerGroup) { 8136 chart.trackerGroup = trackerGroup = chart.trackerGroup.destroy(); 8137 } 8138 8139 removeEvent(container, 'mouseleave', hideTooltipOnMouseLeave); 8140 removeEvent(doc, 'mousemove', hideTooltipOnMouseMove); 8141 container.onclick = container.onmousedown = container.onmousemove = container.ontouchstart = container.ontouchend = container.ontouchmove = null; 8142 } 8143 8144 8145 // Run MouseTracker 8146 8147 if (!trackerGroup) { 8148 chart.trackerGroup = trackerGroup = renderer.g('tracker') 8149 .attr({ zIndex: 9 }) 8150 .add(); 8151 } 8152 8153 if (options.enabled) { 8154 chart.tooltip = tooltip = Tooltip(options); 8155 8156 // set the fixed interval ticking for the smooth tooltip 8157 tooltipInterval = setInterval(function () { 8158 if (tooltipTick) { 8159 tooltipTick(); 8160 } 8161 }, 32); 8162 } 8163 8164 setDOMEvents(); 8165 8166 // expose properties 8167 extend(this, { 8168 zoomX: zoomX, 8169 zoomY: zoomY, 8170 resetTracker: resetTracker, 8171 normalizeMouseEvent: normalizeMouseEvent, 8172 destroy: destroy 8173 }); 8174 } 8175 8176 8177 8178 /** 8179 * The overview of the chart's series 8180 */ 8181 var Legend = function () { 8182 8183 var options = chart.options.legend; 8184 8185 if (!options.enabled) { 8186 return; 8187 } 8188 8189 var horizontal = options.layout === 'horizontal', 8190 symbolWidth = options.symbolWidth, 8191 symbolPadding = options.symbolPadding, 8192 allItems, 8193 style = options.style, 8194 itemStyle = options.itemStyle, 8195 itemHoverStyle = options.itemHoverStyle, 8196 itemHiddenStyle = merge(itemStyle, options.itemHiddenStyle), 8197 padding = options.padding || pInt(style.padding), 8198 ltr = !options.rtl, 8199 itemMarginTop = options.itemMarginTop || 0, 8200 itemMarginBottom = options.itemMarginBottom || 0, 8201 y = 18, 8202 maxItemWidth = 0, 8203 initialItemX = 4 + padding + symbolWidth + symbolPadding, 8204 initialItemY = padding + itemMarginTop + y - 5, // 5 is the number of pixels above the text 8205 itemX, 8206 itemY, 8207 lastItemY, 8208 itemHeight = 0, 8209 box, 8210 legendBorderWidth = options.borderWidth, 8211 legendBackgroundColor = options.backgroundColor, 8212 legendGroup, 8213 offsetWidth, 8214 widthOption = options.width, 8215 series = chart.series, 8216 reversedLegend = options.reversed; 8217 8218 8219 8220 /** 8221 * Set the colors for the legend item 8222 * @param {Object} item A Series or Point instance 8223 * @param {Object} visible Dimmed or colored 8224 */ 8225 function colorizeItem(item, visible) { 8226 var legendItem = item.legendItem, 8227 legendLine = item.legendLine, 8228 legendSymbol = item.legendSymbol, 8229 hiddenColor = itemHiddenStyle.color, 8230 textColor = visible ? options.itemStyle.color : hiddenColor, 8231 symbolColor = visible ? item.color : hiddenColor; 8232 8233 if (legendItem) { 8234 legendItem.css({ fill: textColor }); 8235 } 8236 if (legendLine) { 8237 legendLine.attr({ stroke: symbolColor }); 8238 } 8239 if (legendSymbol) { 8240 legendSymbol.attr({ 8241 stroke: symbolColor, 8242 fill: symbolColor 8243 }); 8244 } 8245 } 8246 8247 /** 8248 * Position the legend item 8249 * @param {Object} item A Series or Point instance 8250 * @param {Object} visible Dimmed or colored 8251 */ 8252 function positionItem(item) { 8253 var legendItem = item.legendItem, 8254 legendLine = item.legendLine, 8255 legendItemPos = item._legendItemPos, 8256 itemX = legendItemPos[0], 8257 itemY = legendItemPos[1], 8258 legendSymbol = item.legendSymbol, 8259 symbolX, 8260 checkbox = item.checkbox; 8261 8262 if (legendItem) { 8263 legendItem.attr({ 8264 x: ltr ? itemX : legendWidth - itemX, 8265 y: itemY 8266 }); 8267 } 8268 if (legendLine) { 8269 legendLine.translate( 8270 ltr ? itemX : legendWidth - itemX, 8271 itemY - 4 8272 ); 8273 } 8274 if (legendSymbol) { 8275 symbolX = itemX + legendSymbol.xOff; 8276 legendSymbol.attr({ 8277 x: ltr ? symbolX : legendWidth - symbolX, 8278 y: itemY + legendSymbol.yOff 8279 }); 8280 } 8281 if (checkbox) { 8282 checkbox.x = itemX; 8283 checkbox.y = itemY; 8284 } 8285 } 8286 8287 /** 8288 * Destroy a single legend item 8289 * @param {Object} item The series or point 8290 */ 8291 function destroyItem(item) { 8292 var checkbox = item.checkbox; 8293 8294 // destroy SVG elements 8295 each(['legendItem', 'legendLine', 'legendSymbol'], function (key) { 8296 if (item[key]) { 8297 item[key].destroy(); 8298 } 8299 }); 8300 8301 if (checkbox) { 8302 discardElement(item.checkbox); 8303 } 8304 8305 8306 } 8307 8308 /** 8309 * Destroys the legend. 8310 */ 8311 function destroy() { 8312 if (box) { 8313 box = box.destroy(); 8314 } 8315 8316 if (legendGroup) { 8317 legendGroup = legendGroup.destroy(); 8318 } 8319 } 8320 8321 /** 8322 * Position the checkboxes after the width is determined 8323 */ 8324 function positionCheckboxes() { 8325 each(allItems, function (item) { 8326 var checkbox = item.checkbox,
8327 alignAttr = legendGroup.alignAttr; 8328 if (checkbox) { 8329 css(checkbox, { 8330 left: (alignAttr.translateX + item.legendItemWidth + checkbox.x - 40) + PX, 8331 top: (alignAttr.translateY + checkbox.y - 11) + PX 8332 }); 8333 } 8334 }); 8335 } 8336 8337 /** 8338 * Render a single specific legend item 8339 * @param {Object} item A series or point 8340 */ 8341 function renderItem(item) { 8342 var bBox, 8343 itemWidth, 8344 legendSymbol, 8345 symbolX, 8346 symbolY, 8347 simpleSymbol, 8348 radius, 8349 li = item.legendItem, 8350 series = item.series || item, 8351 itemOptions = series.options, 8352 strokeWidth = (itemOptions && itemOptions.borderWidth) || 0; 8353 8354 8355 if (!li) { // generate it once, later move it 8356 8357 // let these series types use a simple symbol 8358 simpleSymbol = /^(bar|pie|area|column)$/.test(series.type); 8359 8360 // generate the list item text 8361 item.legendItem = li = renderer.text( 8362 options.labelFormatter.call(item), 8363 0, 8364 0, 8365 options.useHTML 8366 ) 8367 .css(item.visible ? itemStyle : itemHiddenStyle) 8368 .on('mouseover', function () { 8369 item.setState(HOVER_STATE); 8370 li.css(itemHoverStyle); 8371 }) 8372 .on('mouseout', function () { 8373 li.css(item.visible ? itemStyle : itemHiddenStyle); 8374 item.setState(); 8375 }) 8376 .on('click', function () { 8377 var strLegendItemClick = 'legendItemClick', 8378 fnLegendItemClick = function () { 8379 item.setVisible(); 8380 }; 8381 8382 // click the name or symbol 8383 if (item.firePointEvent) { // point 8384 item.firePointEvent(strLegendItemClick, null, fnLegendItemClick); 8385 } else { 8386 fireEvent(item, strLegendItemClick, null, fnLegendItemClick); 8387 } 8388 }) 8389 .attr({ 8390 align: ltr ? 'left' : 'right', 8391 zIndex: 2 8392 }) 8393 .add(legendGroup); 8394 8395 // draw the line 8396 if (!simpleSymbol && itemOptions && itemOptions.lineWidth) { 8397 var attrs = { 8398 'stroke-width': itemOptions.lineWidth, 8399 zIndex: 2 8400 }; 8401 if (itemOptions.dashStyle) { 8402 attrs.dashstyle = itemOptions.dashStyle; 8403 } 8404 item.legendLine = renderer.path([ 8405 M, 8406 (-symbolWidth - symbolPadding) * (ltr ? 1 : -1), 8407 0, 8408 L, 8409 (-symbolPadding) * (ltr ? 1 : -1), 8410 0 8411 ]) 8412 .attr(attrs) 8413 .add(legendGroup); 8414 } 8415 8416 // draw a simple symbol 8417 if (simpleSymbol) { // bar|pie|area|column 8418 8419 legendSymbol = renderer.rect( 8420 (symbolX = -symbolWidth - symbolPadding), 8421 (symbolY = -11), 8422 symbolWidth, 8423 12, 8424 2 8425 ).attr({ 8426 //'stroke-width': 0, 8427 zIndex: 3 8428 }).add(legendGroup); 8429 8430 if (!ltr) { 8431 symbolX += symbolWidth; 8432 } 8433 8434 } else if (itemOptions && itemOptions.marker && itemOptions.marker.enabled) { // draw the marker 8435 radius = itemOptions.marker.radius; 8436 legendSymbol = renderer.symbol( 8437 item.symbol, 8438 (symbolX = -symbolWidth / 2 - symbolPadding - radius), 8439 (symbolY = -4 - radius), 8440 2 * radius, 8441 2 * radius 8442 ) 8443 .attr(item.pointAttr[NORMAL_STATE]) 8444 .attr({ zIndex: 3 }) 8445 .add(legendGroup); 8446 8447 if (!ltr) { 8448 symbolX += symbolWidth / 2; 8449 } 8450 8451 } 8452 if (legendSymbol) { 8453 8454 legendSymbol.xOff = symbolX + (strokeWidth % 2 / 2); 8455 legendSymbol.yOff = symbolY + (strokeWidth % 2 / 2); 8456 } 8457 8458 item.legendSymbol = legendSymbol; 8459 8460 // colorize the items 8461 colorizeItem(item, item.visible); 8462 8463 8464 // add the HTML checkbox on top 8465 if (itemOptions && itemOptions.showCheckbox) { 8466 item.checkbox = createElement('input', { 8467 type: 'checkbox', 8468 checked: item.selected, 8469 defaultChecked: item.selected // required by IE7 8470 }, options.itemCheckboxStyle, container); 8471 8472 addEvent(item.checkbox, 'click', function (event) { 8473 var target = event.target; 8474 fireEvent(item, 'checkboxClick', { 8475 checked: target.checked 8476 }, 8477 function () { 8478 item.select(); 8479 } 8480 ); 8481 }); 8482 } 8483 } 8484 8485 8486 // calculate the positions for the next line 8487 bBox = li.getBBox(); 8488 8489 itemWidth = item.legendItemWidth = 8490 options.itemWidth || symbolWidth + symbolPadding + bBox.width + padding; 8491 itemHeight = bBox.height; 8492 8493 // if the item exceeds the width, start a new line 8494 if (horizontal && itemX - initialItemX + itemWidth > 8495 (widthOption || (chartWidth - 2 * padding - initialItemX))) { 8496 itemX = initialItemX; 8497 itemY += itemMarginTop + itemHeight + itemMarginBottom; 8498 } 8499 8500 // If the item exceeds the height, start a new column 8501 if (!horizontal && itemY + options.y + itemHeight > chartHeight - spacingTop - spacingBottom) { 8502 itemY = initialItemY; 8503 itemX += maxItemWidth; 8504 maxItemWidth = 0; 8505 } 8506 8507 // Set the edge positions 8508 maxItemWidth = mathMax(maxItemWidth, itemWidth); 8509 lastItemY = mathMax(lastItemY, itemY + itemMarginBottom); 8510 8511 // cache the position of the newly generated or reordered items 8512 item._legendItemPos = [itemX, itemY]; 8513 8514 // advance 8515 if (horizontal) { 8516 itemX += itemWidth; 8517 } else { 8518 itemY += itemMarginTop + itemHeight + itemMarginBottom; 8519 } 8520 8521 // the width of the widest item 8522 offsetWidth = widthOption || mathMax( 8523 (itemX - initialItemX) + (horizontal ? 0 : itemWidth), 8524 offsetWidth 8525 ); 8526 8527 } 8528 8529 /** 8530 * Render the legend. This method can be called both before and after 8531 * chart.render. If called after, it will only rearrange items instead 8532 * of creating new ones. 8533 */ 8534 function renderLegend() { 8535 itemX = initialItemX; 8536 itemY = initialItemY; 8537 offsetWidth = 0; 8538 lastItemY = 0; 8539 8540 if (!legendGroup) { 8541 legendGroup = renderer.g('legend') 8542 // #414, #759. Trackers will be drawn above the legend, but we have 8543 // to sacrifice that because tooltips need to be above the legend 8544 // and trackers above tooltips 8545 .attr({ zIndex: 7 }) 8546 .add(); 8547 } 8548 8549 8550 // add each series or point 8551 allItems = []; 8552 each(series, function (serie) { 8553 var seriesOptions = serie.options; 8554
8555 if (!seriesOptions.showInLegend) { 8556 return; 8557 } 8558 8559 // use points or series for the legend item depending on legendType 8560 allItems = allItems.concat( 8561 serie.legendItems || 8562 (seriesOptions.legendType === 'point' ? 8563 serie.data : 8564 serie) 8565 ); 8566 8567 }); 8568 8569 // sort by legendIndex 8570 stableSort(allItems, function (a, b) { 8571 return (a.options.legendIndex || 0) - (b.options.legendIndex || 0); 8572 }); 8573 8574 // reversed legend 8575 if (reversedLegend) { 8576 allItems.reverse(); 8577 } 8578 8579 // render the items 8580 each(allItems, renderItem); 8581 8582 8583 // Draw the border 8584 legendWidth = widthOption || offsetWidth; 8585 legendHeight = lastItemY - y + itemHeight; 8586 8587 if (legendBorderWidth || legendBackgroundColor) { 8588 legendWidth += 2 * padding; 8589 legendHeight += 2 * padding; 8590 8591 if (!box) { 8592 box = renderer.rect( 8593 0, 8594 0, 8595 legendWidth, 8596 legendHeight, 8597 options.borderRadius, 8598 legendBorderWidth || 0 8599 ).attr({ 8600 stroke: options.borderColor, 8601 'stroke-width': legendBorderWidth || 0, 8602 fill: legendBackgroundColor || NONE 8603 }) 8604 .add(legendGroup) 8605 .shadow(options.shadow); 8606 box.isNew = true; 8607 8608 } else if (legendWidth > 0 && legendHeight > 0) { 8609 box[box.isNew ? 'attr' : 'animate']( 8610 box.crisp(null, null, null, legendWidth, legendHeight) 8611 ); 8612 box.isNew = false; 8613 } 8614 8615 // hide the border if no items 8616 box[allItems.length ? 'show' : 'hide'](); 8617 } 8618 8619 // Now that the legend width and height are extablished, put the items in the 8620 // final position 8621 each(allItems, positionItem); 8622 8623 // 1.x compatibility: positioning based on style 8624 var props = ['left', 'right', 'top', 'bottom'], 8625 prop, 8626 i = 4; 8627 while (i--) { 8628 prop = props[i]; 8629 if (style[prop] && style[prop] !== 'auto') { 8630 options[i < 2 ? 'align' : 'verticalAlign'] = prop; 8631 options[i < 2 ? 'x' : 'y'] = pInt(style[prop]) * (i % 2 ? -1 : 1); 8632 } 8633 } 8634 8635 if (allItems.length) { 8636 legendGroup.align(extend(options, { 8637 width: legendWidth, 8638 height: legendHeight 8639 }), true, spacingBox); 8640 } 8641 8642 if (!isResizing) { 8643 positionCheckboxes(); 8644 } 8645 } 8646 8647 8648 // run legend 8649 renderLegend(); 8650 8651 // move checkboxes 8652 addEvent(chart, 'endResize', positionCheckboxes); 8653 8654 // expose 8655 return { 8656 colorizeItem: colorizeItem, 8657 destroyItem: destroyItem, 8658 renderLegend: renderLegend, 8659 destroy: destroy 8660 }; 8661 }; 8662 8663 8664 8665 8666 8667 8668 /** 8669 * Initialize an individual series, called internally before render time 8670 */ 8671 function initSeries(options) { 8672 var type = options.type || optionsChart.type || optionsChart.defaultSeriesType, 8673 typeClass = seriesTypes[type], 8674 serie, 8675 hasRendered = chart.hasRendered; 8676 8677 // an inverted chart can't take a column series and vice versa 8678 if (hasRendered) { 8679 if (inverted && type === 'column') { 8680 typeClass = seriesTypes.bar; 8681 } else if (!inverted && type === 'bar') { 8682 typeClass = seriesTypes.column; 8683 } 8684 } 8685 8686 serie = new typeClass(); 8687 8688 serie.init(chart, options); 8689 8690 // set internal chart properties 8691 if (!hasRendered && serie.inverted) { 8692 inverted = true; 8693 } 8694 if (serie.isCartesian) { 8695 hasCartesianSeries = serie.isCartesian; 8696 } 8697 8698 series.push(serie); 8699 8700 return serie; 8701 } 8702 8703 /** 8704 * Add a series dynamically after time 8705 * 8706 * @param {Object} options The config options 8707 * @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true. 8708 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation 8709 * configuration 8710 * 8711 * @return {Object} series The newly created series object 8712 */ 8713 function addSeries(options, redraw, animation) { 8714 var series; 8715 8716 if (options) { 8717 setAnimation(animation, chart); 8718 redraw = pick(redraw, true); // defaults to true 8719 8720 fireEvent(chart, 'addSeries', { options: options }, function () { 8721 series = initSeries(options); 8722 series.isDirty = true; 8723 8724 chart.isDirtyLegend = true; // the series array is out of sync with the display 8725 if (redraw) { 8726 chart.redraw(); 8727 } 8728 }); 8729 } 8730 8731 return series; 8732 } 8733 8734 /** 8735 * Check whether a given point is within the plot area 8736 * 8737 * @param {Number} x Pixel x relative to the plot area 8738 * @param {Number} y Pixel y relative to the plot area 8739 */ 8740 isInsidePlot = function (x, y) { 8741 return x >= 0 && 8742 x <= plotWidth && 8743 y >= 0 && 8744 y <= plotHeight; 8745 }; 8746 8747 /** 8748 * Adjust all axes tick amounts 8749 */ 8750 function adjustTickAmounts() { 8751 if (optionsChart.alignTicks !== false) { 8752 each(axes, function (axis) { 8753 axis.adjustTickAmount(); 8754 }); 8755 } 8756 maxTicks = null; 8757 } 8758 8759 /** 8760 * Redraw legend, axes or series based on updated data 8761 * 8762 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation 8763 * configuration 8764 */ 8765 function redraw(animation) { 8766 var redrawLegend = chart.isDirtyLegend, 8767 hasStackedSeries, 8768 isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed? 8769 seriesLength = series.length, 8770 i = seriesLength,
8771 clipRect = chart.clipRect, 8772 serie; 8773 8774 setAnimation(animation, chart); 8775 8776 // link stacked series 8777 while (i--) { 8778 serie = series[i]; 8779 if (serie.isDirty && serie.options.stacking) { 8780 hasStackedSeries = true; 8781 break; 8782 } 8783 } 8784 if (hasStackedSeries) { // mark others as dirty 8785 i = seriesLength; 8786 while (i--) { 8787 serie = series[i]; 8788 if (serie.options.stacking) { 8789 serie.isDirty = true; 8790 } 8791 } 8792 } 8793 8794 // handle updated data in the series 8795 each(series, function (serie) { 8796 if (serie.isDirty) { // prepare the data so axis can read it 8797 if (serie.options.legendType === 'point') { 8798 redrawLegend = true; 8799 } 8800 } 8801 }); 8802 8803 // handle added or removed series 8804 if (redrawLegend && legend.renderLegend) { // series or pie points are added or removed 8805 // draw legend graphics 8806 legend.renderLegend(); 8807 8808 chart.isDirtyLegend = false; 8809 } 8810 8811 8812 if (hasCartesianSeries) { 8813 if (!isResizing) { 8814 8815 // reset maxTicks 8816 maxTicks = null; 8817 8818 // set axes scales 8819 each(axes, function (axis) { 8820 axis.setScale(); 8821 }); 8822 } 8823 adjustTickAmounts(); 8824 getMargins(); 8825 8826 // redraw axes 8827 each(axes, function (axis) { 8828 8829 // Fire 'afterSetExtremes' only if extremes are set 8830 if (axis.isDirtyExtremes) { // #821 8831 axis.isDirtyExtremes = false; 8832 fireEvent(axis, 'afterSetExtremes', axis.getExtremes()); // #747, #751 8833 } 8834 8835 if (axis.isDirty || isDirtyBox) { 8836 axis.redraw(); 8837 isDirtyBox = true; // #792 8838 } 8839 }); 8840 8841 8842 } 8843 8844 // the plot areas size has changed 8845 if (isDirtyBox) { 8846 drawChartBox(); 8847 8848 // move clip rect 8849 if (clipRect) { 8850 stop(clipRect); 8851 clipRect.animate({ // for chart resize 8852 width: chart.plotSizeX, 8853 height: chart.plotSizeY + 1 8854 }); 8855 } 8856 8857 } 8858 8859 8860 // redraw affected series 8861 each(series, function (serie) { 8862 if (serie.isDirty && serie.visible && 8863 (!serie.isCartesian || serie.xAxis)) { // issue #153 8864 serie.redraw(); 8865 } 8866 }); 8867 8868 8869 // hide tooltip and hover states 8870 if (tracker && tracker.resetTracker) { 8871 tracker.resetTracker(); 8872 } 8873 8874 // redraw if canvas 8875 renderer.draw(); 8876 8877 // fire the event 8878 fireEvent(chart, 'redraw'); // jQuery breaks this when calling it from addEvent. Overwrites chart.redraw 8879 } 8880 8881 8882 8883 /** 8884 * Dim the chart and show a loading text or symbol 8885 * @param {String} str An optional text to show in the loading label instead of the default one 8886 */ 8887 function showLoading(str) { 8888 var loadingOptions = options.loading; 8889 8890 // create the layer at the first call 8891 if (!loadingDiv) { 8892 loadingDiv = createElement(DIV, { 8893 className: PREFIX + 'loading' 8894 }, extend(loadingOptions.style, { 8895 left: plotLeft + PX, 8896 top: plotTop + PX, 8897 width: plotWidth + PX, 8898 height: plotHeight + PX, 8899 zIndex: 10, 8900 display: NONE 8901 }), container); 8902 8903 loadingSpan = createElement( 8904 'span', 8905 null, 8906 loadingOptions.labelStyle, 8907 loadingDiv 8908 ); 8909 8910 } 8911 8912 // update text 8913 loadingSpan.innerHTML = str || options.lang.loading; 8914 8915 // show it 8916 if (!loadingShown) { 8917 css(loadingDiv, { opacity: 0, display: '' }); 8918 animate(loadingDiv, { 8919 opacity: loadingOptions.style.opacity 8920 }, { 8921 duration: loadingOptions.showDuration || 0 8922 }); 8923 loadingShown = true; 8924 } 8925 } 8926 /** 8927 * Hide the loading layer 8928 */ 8929 function hideLoading() { 8930 if (loadingDiv) { 8931 animate(loadingDiv, { 8932 opacity: 0 8933 }, { 8934 duration: options.loading.hideDuration || 100, 8935 complete: function () { 8936 css(loadingDiv, { display: NONE }); 8937 } 8938 }); 8939 } 8940 loadingShown = false; 8941 } 8942 8943 /** 8944 * Get an axis, series or point object by id. 8945 * @param id {String} The id as given in the configuration options 8946 */ 8947 function get(id) { 8948 var i, 8949 j, 8950 points; 8951 8952 // search axes 8953 for (i = 0; i < axes.length; i++) { 8954 if (axes[i].options.id === id) { 8955 return axes[i]; 8956 } 8957 } 8958 8959 // search series 8960 for (i = 0; i < series.length; i++) { 8961 if (series[i].options.id === id) { 8962 return series[i]; 8963 } 8964 } 8965 8966 // search points 8967 for (i = 0; i < series.length; i++) { 8968 points = series[i].points || []; 8969 for (j = 0; j < points.length; j++) { 8970 if (points[j].id === id) { 8971 return points[j]; 8972 } 8973 } 8974 } 8975 return null; 8976 } 8977 8978 /** 8979 * Create the Axis instances based on the config options 8980 */ 8981 function getAxes() { 8982 var xAxisOptions = options.xAxis || {}, 8983 yAxisOptions = options.yAxis || {}, 8984 optionsArray, 8985 axis; 8986 8987 // make sure the options are arrays and add some members 8988 xAxisOptions = splat(xAxisOptions); 8989 each(xAxisOptions, function (axis, i) { 8990 axis.index = i; 8991 axis.isX = true; 8992 }); 8993 8994 yAxisOptions = splat(yAxisOptions); 8995 each(yAxisOptions, function (axis, i) { 8996 axis.index = i; 8997 }); 8998 8999 // concatenate all axis options into one array 9000 optionsArray = xAxisOptions.concat(yAxisOptions); 9001 9002 each(optionsArray, function (axisOptions) { 9003 axis = new Axis(axisOptions); 9004 }); 9005 9006 adjustTickAmounts(); 9007 } 9008 9009 9010 /** 9011 * Get the currently selected points from all series 9012 */ 9013 function getSelectedPoints() { 9014 var points = []; 9015 each(series, function (serie) { 9016 points = points.concat(grep(serie.points, function (point) { 9017 return point.selected; 9018 })); 9019 }); 9020 return points; 9021 } 9022 9023 /** 9024 * Get the currently selected series 9025 */ 9026 function getSelectedSeries() { 9027 return grep(series, function (serie) { 9028 return serie.selected; 9029 }); 9030 } 9031 9032 /** 9033 * Display the zoom button 9034 */ 9035 function showResetZoom() { 9036 var lang = defaultOptions.lang,
9037 btnOptions = optionsChart.resetZoomButton, 9038 theme = btnOptions.theme, 9039 states = theme.states, 9040 box = btnOptions.relativeTo === 'chart' ? null : { 9041 x: plotLeft, 9042 y: plotTop, 9043 width: plotWidth, 9044 height: plotHeight 9045 }; 9046 chart.resetZoomButton = renderer.button(lang.resetZoom, null, null, zoomOut, theme, states && states.hover) 9047 .attr({ 9048 align: btnOptions.position.align, 9049 title: lang.resetZoomTitle 9050 }) 9051 .add() 9052 .align(btnOptions.position, false, box); 9053 } 9054 9055 /** 9056 * Zoom out to 1:1 9057 */ 9058 zoomOut = function () { 9059 var resetZoomButton = chart.resetZoomButton; 9060 9061 fireEvent(chart, 'selection', { resetSelection: true }, zoom); 9062 if (resetZoomButton) { 9063 chart.resetZoomButton = resetZoomButton.destroy(); 9064 } 9065 }; 9066 /** 9067 * Zoom into a given portion of the chart given by axis coordinates 9068 * @param {Object} event 9069 */ 9070 zoom = function (event) { 9071 9072 // add button to reset selection 9073 var hasZoomed; 9074 9075 if (chart.resetZoomEnabled !== false && !chart.resetZoomButton) { // hook for Stock charts etc. 9076 showResetZoom(); 9077 } 9078 9079 // if zoom is called with no arguments, reset the axes 9080 if (!event || event.resetSelection) { 9081 each(axes, function (axis) { 9082 if (axis.options.zoomEnabled !== false) { 9083 axis.setExtremes(null, null, false); 9084 hasZoomed = true; 9085 } 9086 }); 9087 } else { // else, zoom in on all axes 9088 each(event.xAxis.concat(event.yAxis), function (axisData) { 9089 var axis = axisData.axis; 9090 9091 // don't zoom more than minRange 9092 if (chart.tracker[axis.isXAxis ? 'zoomX' : 'zoomY']) { 9093 axis.setExtremes(axisData.min, axisData.max, false); 9094 hasZoomed = true; 9095 } 9096 }); 9097 } 9098 9099 // Redraw 9100 if (hasZoomed) { 9101 redraw( 9102 pick(optionsChart.animation, chart.pointCount < 100) // animation 9103 ); 9104 } 9105 }; 9106 9107 /** 9108 * Pan the chart by dragging the mouse across the pane. This function is called 9109 * on mouse move, and the distance to pan is computed from chartX compared to 9110 * the first chartX position in the dragging operation. 9111 */ 9112 chart.pan = function (chartX) { 9113 9114 var xAxis = chart.xAxis[0], 9115 mouseDownX = chart.mouseDownX, 9116 halfPointRange = xAxis.pointRange / 2, 9117 extremes = xAxis.getExtremes(), 9118 newMin = xAxis.translate(mouseDownX - chartX, true) + halfPointRange, 9119 newMax = xAxis.translate(mouseDownX + plotWidth - chartX, true) - halfPointRange, 9120 hoverPoints = chart.hoverPoints; 9121 9122 // remove active points for shared tooltip 9123 if (hoverPoints) { 9124 each(hoverPoints, function (point) { 9125 point.setState(); 9126 }); 9127 } 9128 9129 if (newMin > mathMin(extremes.dataMin, extremes.min) && newMax < mathMax(extremes.dataMax, extremes.max)) { 9130 xAxis.setExtremes(newMin, newMax, true, false); 9131 } 9132 9133 chart.mouseDownX = chartX; // set new reference for next run 9134 css(container, { cursor: 'move' }); 9135 }; 9136 9137 /** 9138 * Show the title and subtitle of the chart 9139 * 9140 * @param titleOptions {Object} New title options 9141 * @param subtitleOptions {Object} New subtitle options 9142 * 9143 */ 9144 function setTitle(titleOptions, subtitleOptions) { 9145 9146 chartTitleOptions = merge(options.title, titleOptions); 9147 chartSubtitleOptions = merge(options.subtitle, subtitleOptions); 9148 9149 // add title and subtitle 9150 each([ 9151 ['title', titleOptions, chartTitleOptions], 9152 ['subtitle', subtitleOptions, chartSubtitleOptions] 9153 ], function (arr) { 9154 var name = arr[0], 9155 title = chart[name], 9156 titleOptions = arr[1], 9157 chartTitleOptions = arr[2]; 9158 9159 if (title && titleOptions) { 9160 title = title.destroy(); // remove old 9161 } 9162 if (chartTitleOptions && chartTitleOptions.text && !title) { 9163 chart[name] = renderer.text( 9164 chartTitleOptions.text, 9165 0, 9166 0, 9167 chartTitleOptions.useHTML 9168 ) 9169 .attr({ 9170 align: chartTitleOptions.align, 9171 'class': PREFIX + name, 9172 zIndex: chartTitleOptions.zIndex || 4 9173 }) 9174 .css(chartTitleOptions.style) 9175 .add() 9176 .align(chartTitleOptions, false, spacingBox); 9177 } 9178 }); 9179 9180 } 9181 9182 /** 9183 * Get chart width and height according to options and container size 9184 */ 9185 function getChartSize() { 9186 9187 containerWidth = (renderToClone || renderTo).offsetWidth; 9188 containerHeight = (renderToClone || renderTo).offsetHeight; 9189 chart.chartWidth = chartWidth = optionsChart.width || containerWidth || 600; 9190 chart.chartHeight = chartHeight = optionsChart.height || 9191 // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7: 9192 (containerHeight > 19 ? containerHeight : 400); 9193 } 9194 9195 9196 /** 9197 * Get the containing element, determine the size and create the inner container 9198 * div to hold the chart 9199 */ 9200 function getContainer() { 9201 renderTo = optionsChart.renderTo; 9202 containerId = PREFIX + idCounter++; 9203 9204 if (isString(renderTo)) { 9205 renderTo = doc.getElementById(renderTo); 9206 } 9207 9208 // Display an error if the renderTo is wrong 9209 if (!renderTo) { 9210 error(13, true); 9211 } 9212 9213 // remove previous chart 9214 renderTo.innerHTML = ''; 9215 9216 // If the container doesn't have an offsetWidth, it has or is a child of a node 9217 // that has display:none. We need to temporarily move it out to a visible 9218 // state to determine the size, else the legend and tooltips won't render 9219 // properly 9220 if (!renderTo.offsetWidth) { 9221 renderToClone = renderTo.cloneNode(0); 9222 css(renderToClone, { 9223 position: ABSOLUTE, 9224 top: '-9999px', 9225 display: '' 9226 }); 9227 doc.body.appendChild(renderToClone); 9228 } 9229 9230 // get the width and height 9231 getChartSize(); 9232 9233 // create the inner container 9234 chart.container = container = createElement(DIV, { 9235 className: PREFIX + 'container' + 9236 (optionsChart.className ? ' ' + optionsChart.className : ''), 9237 id: containerId 9238 }, extend({ 9239 position: RELATIVE,
9240 overflow: HIDDEN, // needed for context menu (avoid scrollbars) and 9241 // content overflow in IE 9242 width: chartWidth + PX, 9243 height: chartHeight + PX, 9244 textAlign: 'left', 9245 lineHeight: 'normal' // #427 9246 }, optionsChart.style), 9247 renderToClone || renderTo 9248 ); 9249 9250 chart.renderer = renderer = 9251 optionsChart.forExport ? // force SVG, used for SVG export 9252 new SVGRenderer(container, chartWidth, chartHeight, true) : 9253 new Renderer(container, chartWidth, chartHeight); 9254 9255 if (useCanVG) { 9256 // If we need canvg library, extend and configure the renderer 9257 // to get the tracker for translating mouse events 9258 renderer.create(chart, container, chartWidth, chartHeight); 9259 } 9260 9261 // Issue 110 workaround: 9262 // In Firefox, if a div is positioned by percentage, its pixel position may land 9263 // between pixels. The container itself doesn't display this, but an SVG element 9264 // inside this container will be drawn at subpixel precision. In order to draw 9265 // sharp lines, this must be compensated for. This doesn't seem to work inside 9266 // iframes though (like in jsFiddle). 9267 var subPixelFix, rect; 9268 if (isFirefox && container.getBoundingClientRect) { 9269 subPixelFix = function () { 9270 css(container, { left: 0, top: 0 }); 9271 rect = container.getBoundingClientRect(); 9272 css(container, { 9273 left: (-(rect.left - pInt(rect.left))) + PX, 9274 top: (-(rect.top - pInt(rect.top))) + PX 9275 }); 9276 }; 9277 9278 // run the fix now 9279 subPixelFix(); 9280 9281 // run it on resize 9282 addEvent(win, 'resize', subPixelFix); 9283 9284 // remove it on chart destroy 9285 addEvent(chart, 'destroy', function () { 9286 removeEvent(win, 'resize', subPixelFix); 9287 }); 9288 } 9289 } 9290 9291 /** 9292 * Calculate margins by rendering axis labels in a preliminary position. Title, 9293 * subtitle and legend have already been rendered at this stage, but will be 9294 * moved into their final positions 9295 */ 9296 getMargins = function () { 9297 var legendOptions = options.legend, 9298 legendMargin = pick(legendOptions.margin, 10), 9299 legendX = legendOptions.x, 9300 legendY = legendOptions.y, 9301 align = legendOptions.align, 9302 verticalAlign = legendOptions.verticalAlign, 9303 titleOffset; 9304 9305 resetMargins(); 9306 9307 // adjust for title and subtitle 9308 if ((chart.title || chart.subtitle) && !defined(optionsMarginTop)) { 9309 titleOffset = mathMax( 9310 (chart.title && !chartTitleOptions.floating && !chartTitleOptions.verticalAlign && chartTitleOptions.y) || 0, 9311 (chart.subtitle && !chartSubtitleOptions.floating && !chartSubtitleOptions.verticalAlign && chartSubtitleOptions.y) || 0 9312 ); 9313 if (titleOffset) { 9314 plotTop = mathMax(plotTop, titleOffset + pick(chartTitleOptions.margin, 15) + spacingTop); 9315 } 9316 } 9317 // adjust for legend 9318 if (legendOptions.enabled && !legendOptions.floating) { 9319 if (align === 'right') { // horizontal alignment handled first 9320 if (!defined(optionsMarginRight)) { 9321 marginRight = mathMax( 9322 marginRight, 9323 legendWidth - legendX + legendMargin + spacingRight 9324 ); 9325 } 9326 } else if (align === 'left') { 9327 if (!defined(optionsMarginLeft)) { 9328 plotLeft = mathMax( 9329 plotLeft, 9330 legendWidth + legendX + legendMargin + spacingLeft 9331 ); 9332 } 9333 9334 } else if (verticalAlign === 'top') { 9335 if (!defined(optionsMarginTop)) { 9336 plotTop = mathMax( 9337 plotTop, 9338 legendHeight + legendY + legendMargin + spacingTop 9339 ); 9340 } 9341 9342 } else if (verticalAlign === 'bottom') { 9343 if (!defined(optionsMarginBottom)) { 9344 marginBottom = mathMax( 9345 marginBottom, 9346 legendHeight - legendY + legendMargin + spacingBottom 9347 ); 9348 } 9349 } 9350 } 9351 9352 // adjust for scroller 9353 if (chart.extraBottomMargin) { 9354 marginBottom += chart.extraBottomMargin; 9355 } 9356 if (chart.extraTopMargin) { 9357 plotTop += chart.extraTopMargin; 9358 } 9359 9360 // pre-render axes to get labels offset width 9361 if (hasCartesianSeries) { 9362 each(axes, function (axis) { 9363 axis.getOffset(); 9364 }); 9365 } 9366 9367 if (!defined(optionsMarginLeft)) { 9368 plotLeft += axisOffset[3]; 9369 } 9370 if (!defined(optionsMarginTop)) { 9371 plotTop += axisOffset[0]; 9372 } 9373 if (!defined(optionsMarginBottom)) { 9374 marginBottom += axisOffset[2]; 9375 } 9376 if (!defined(optionsMarginRight)) { 9377 marginRight += axisOffset[1]; 9378 } 9379 9380 setChartSize(); 9381 9382 }; 9383 9384 /** 9385 * Add the event handlers necessary for auto resizing 9386 * 9387 */ 9388 function initReflow() { 9389 var reflowTimeout; 9390 function reflow(e) { 9391 var width = optionsChart.width || renderTo.offsetWidth, 9392 height = optionsChart.height || renderTo.offsetHeight, 9393 target = e ? e.target : win; // #805 - MooTools doesn't supply e 9394 9395 // Width and height checks for display:none. Target is doc in IE8 and Opera, 9396 // win in Firefox, Chrome and IE9. 9397 if (width && height && (target === win || target === doc)) { 9398 9399 if (width !== containerWidth || height !== containerHeight) { 9400 clearTimeout(reflowTimeout); 9401 reflowTimeout = setTimeout(function () { 9402 resize(width, height, false); 9403 }, 100); 9404 } 9405 containerWidth = width; 9406 containerHeight = height; 9407 } 9408 } 9409 addEvent(win, 'resize', reflow); 9410 addEvent(chart, 'destroy', function () { 9411 removeEvent(win, 'resize', reflow); 9412 }); 9413 } 9414 9415 /** 9416 * Fires endResize event on chart instance. 9417 */ 9418 function fireEndResize() { 9419 if (chart) { 9420 fireEvent(chart, 'endResize', null, function () { 9421 isResizing -= 1; 9422 }); 9423 } 9424 } 9425
9426 /** 9427 * Resize the chart to a given width and height 9428 * @param {Number} width 9429 * @param {Number} height 9430 * @param {Object|Boolean} animation 9431 */ 9432 resize = function (width, height, animation) { 9433 var chartTitle = chart.title, 9434 chartSubtitle = chart.subtitle; 9435 9436 isResizing += 1; 9437 9438 // set the animation for the current process 9439 setAnimation(animation, chart); 9440 9441 oldChartHeight = chartHeight; 9442 oldChartWidth = chartWidth; 9443 if (defined(width)) { 9444 chart.chartWidth = chartWidth = mathRound(width); 9445 } 9446 if (defined(height)) { 9447 chart.chartHeight = chartHeight = mathRound(height); 9448 } 9449 9450 css(container, { 9451 width: chartWidth + PX, 9452 height: chartHeight + PX 9453 }); 9454 renderer.setSize(chartWidth, chartHeight, animation); 9455 9456 // update axis lengths for more correct tick intervals: 9457 plotWidth = chartWidth - plotLeft - marginRight; 9458 plotHeight = chartHeight - plotTop - marginBottom; 9459 9460 // handle axes 9461 maxTicks = null; 9462 each(axes, function (axis) { 9463 axis.isDirty = true; 9464 axis.setScale(); 9465 }); 9466 9467 // make sure non-cartesian series are also handled 9468 each(series, function (serie) { 9469 serie.isDirty = true; 9470 }); 9471 9472 chart.isDirtyLegend = true; // force legend redraw 9473 chart.isDirtyBox = true; // force redraw of plot and chart border 9474 9475 getMargins(); 9476 9477 // move titles 9478 if (chartTitle) { 9479 chartTitle.align(null, null, spacingBox); 9480 } 9481 if (chartSubtitle) { 9482 chartSubtitle.align(null, null, spacingBox); 9483 } 9484 9485 redraw(animation); 9486 9487 9488 oldChartHeight = null; 9489 fireEvent(chart, 'resize'); 9490 9491 // fire endResize and set isResizing back 9492 // If animation is disabled, fire without delay 9493 if (globalAnimation === false) { 9494 fireEndResize(); 9495 } else { // else set a timeout with the animation duration 9496 setTimeout(fireEndResize, (globalAnimation && globalAnimation.duration) || 500); 9497 } 9498 }; 9499 9500 /** 9501 * Set the public chart properties. This is done before and after the pre-render 9502 * to determine margin sizes 9503 */ 9504 setChartSize = function () { 9505 9506 chart.plotLeft = plotLeft = mathRound(plotLeft); 9507 chart.plotTop = plotTop = mathRound(plotTop); 9508 chart.plotWidth = plotWidth = mathRound(chartWidth - plotLeft - marginRight); 9509 chart.plotHeight = plotHeight = mathRound(chartHeight - plotTop - marginBottom); 9510 9511 chart.plotSizeX = inverted ? plotHeight : plotWidth; 9512 chart.plotSizeY = inverted ? plotWidth : plotHeight; 9513 9514 spacingBox = { 9515 x: spacingLeft, 9516 y: spacingTop, 9517 width: chartWidth - spacingLeft - spacingRight, 9518 height: chartHeight - spacingTop - spacingBottom 9519 }; 9520 9521 each(axes, function (axis) { 9522 axis.setAxisSize(); 9523 axis.setAxisTranslation(); 9524 }); 9525 }; 9526 9527 /** 9528 * Initial margins before auto size margins are applied 9529 */ 9530 resetMargins = function () { 9531 plotTop = pick(optionsMarginTop, spacingTop); 9532 marginRight = pick(optionsMarginRight, spacingRight); 9533 marginBottom = pick(optionsMarginBottom, spacingBottom); 9534 plotLeft = pick(optionsMarginLeft, spacingLeft); 9535 axisOffset = [0, 0, 0, 0]; // top, right, bottom, left 9536 }; 9537 9538 /** 9539 * Draw the borders and backgrounds for chart and plot area 9540 */ 9541 drawChartBox = function () { 9542 var chartBorderWidth = optionsChart.borderWidth || 0, 9543 chartBackgroundColor = optionsChart.backgroundColor,
9544 plotBackgroundColor = optionsChart.plotBackgroundColor, 9545 plotBackgroundImage = optionsChart.plotBackgroundImage, 9546 mgn, 9547 plotSize = { 9548 x: plotLeft, 9549 y: plotTop, 9550 width: plotWidth, 9551 height: plotHeight 9552 }; 9553 9554 // Chart area 9555 mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0); 9556 9557 if (chartBorderWidth || chartBackgroundColor) { 9558 if (!chartBackground) { 9559 chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn, 9560 optionsChart.borderRadius, chartBorderWidth) 9561 .attr({ 9562 stroke: optionsChart.borderColor, 9563 'stroke-width': chartBorderWidth, 9564 fill: chartBackgroundColor || NONE 9565 }) 9566 .add() 9567 .shadow(optionsChart.shadow); 9568 } else { // resize 9569 chartBackground.animate( 9570 chartBackground.crisp(null, null, null, chartWidth - mgn, chartHeight - mgn) 9571 ); 9572 } 9573 } 9574 9575 9576 // Plot background 9577 if (plotBackgroundColor) { 9578 if (!plotBackground) { 9579 plotBackground = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0) 9580 .attr({
9581 fill: plotBackgroundColor 9582 }) 9583 .add() 9584 .shadow(optionsChart.plotShadow); 9585 } else { 9586 plotBackground.animate(plotSize); 9587 } 9588 } 9589 if (plotBackgroundImage) { 9590 if (!plotBGImage) { 9591 plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight) 9592 .add(); 9593 } else { 9594 plotBGImage.animate(plotSize); 9595 } 9596 } 9597 9598 // Plot area border 9599 if (optionsChart.plotBorderWidth) { 9600 if (!plotBorder) { 9601 plotBorder = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0, optionsChart.plotBorderWidth) 9602 .attr({ 9603 stroke: optionsChart.plotBorderColor, 9604 'stroke-width': optionsChart.plotBorderWidth, 9605 zIndex: 4 9606 }) 9607 .add(); 9608 } else { 9609 plotBorder.animate( 9610 plotBorder.crisp(null, plotLeft, plotTop, plotWidth, plotHeight) 9611 ); 9612 } 9613 } 9614 9615 // reset 9616 chart.isDirtyBox = false; 9617 }; 9618 9619 /** 9620 * Detect whether the chart is inverted, either by setting the chart.inverted option 9621 * or adding a bar series to the configuration options 9622 */ 9623 function setInverted() { 9624 var BAR = 'bar', 9625 isInverted = ( 9626 inverted || // it is set before 9627 optionsChart.inverted || 9628 optionsChart.type === BAR || // default series type 9629 optionsChart.defaultSeriesType === BAR // backwards compatible 9630 ), 9631 seriesOptions = options.series, 9632 i = seriesOptions && seriesOptions.length; 9633 9634 // check if a bar series is present in the config options 9635 while (!isInverted && i--) { 9636 if (seriesOptions[i].type === BAR) { 9637 isInverted = true; 9638 } 9639 } 9640 9641 // set the chart property and the chart scope variable 9642 chart.inverted = inverted = isInverted; 9643 } 9644 9645 /** 9646 * Render all graphics for the chart 9647 */ 9648 function render() { 9649 var labels = options.labels, 9650 credits = options.credits, 9651 creditsHref; 9652 9653 // Title 9654 setTitle(); 9655 9656 9657 // Legend 9658 legend = chart.legend = new Legend(); 9659 9660 // Get margins by pre-rendering axes 9661 // set axes scales 9662 each(axes, function (axis) { 9663 axis.setScale(); 9664 }); 9665 getMargins(); 9666 each(axes, function (axis) { 9667 axis.setTickPositions(true); // update to reflect the new margins 9668 }); 9669 adjustTickAmounts(); 9670 getMargins(); // second pass to check for new labels 9671 9672 9673 // Draw the borders and backgrounds 9674 drawChartBox(); 9675 9676 // Axes 9677 if (hasCartesianSeries) { 9678 each(axes, function (axis) { 9679 axis.render(); 9680 }); 9681 } 9682 9683 9684 // The series 9685 if (!chart.seriesGroup) { 9686 chart.seriesGroup = renderer.g('series-group') 9687 .attr({ zIndex: 3 }) 9688 .add(); 9689 } 9690 each(series, function (serie) { 9691 serie.translate(); 9692 serie.setTooltipPoints(); 9693 serie.render(); 9694 }); 9695 9696 9697 // Labels 9698 if (labels.items) { 9699 each(labels.items, function () { 9700 var style = extend(labels.style, this.style), 9701 x = pInt(style.left) + plotLeft, 9702 y = pInt(style.top) + plotTop + 12; 9703 9704 // delete to prevent rewriting in IE 9705 delete style.left; 9706 delete style.top; 9707 9708 renderer.text( 9709 this.html, 9710 x, 9711 y 9712 ) 9713 .attr({ zIndex: 2 }) 9714 .css(style) 9715 .add(); 9716 9717 }); 9718 } 9719 9720 // Credits 9721 if (credits.enabled && !chart.credits) { 9722 creditsHref = credits.href; 9723 chart.credits = renderer.text( 9724 credits.text, 9725 0, 9726 0 9727 ) 9728 .on('click', function () { 9729 if (creditsHref) { 9730 location.href = creditsHref; 9731 } 9732 }) 9733 .attr({ 9734 align: credits.position.align, 9735 zIndex: 8 9736 }) 9737 .css(credits.style) 9738 .add() 9739 .align(credits.position); 9740 } 9741 9742 // Set flag 9743 chart.hasRendered = true; 9744 9745 } 9746 9747 /** 9748 * Clean up memory usage 9749 */ 9750 function destroy() { 9751 var i, 9752 parentNode = container && container.parentNode; 9753 9754 // If the chart is destroyed already, do nothing. 9755 // This will happen if if a script invokes chart.destroy and 9756 // then it will be called again on win.unload 9757 if (chart === null) { 9758 return; 9759 } 9760 9761 // fire the chart.destoy event 9762 fireEvent(chart, 'destroy'); 9763 9764 // remove events 9765 removeEvent(chart); 9766 9767 // ==== Destroy collections: 9768 // Destroy axes 9769 i = axes.length; 9770 while (i--) { 9771 axes[i] = axes[i].destroy(); 9772 } 9773 9774 // Destroy each series 9775 i = series.length; 9776 while (i--) { 9777 series[i] = series[i].destroy(); 9778 } 9779 9780 // ==== Destroy chart properties:
9781 each(['title', 'subtitle', 'seriesGroup', 'clipRect', 'credits', 'tracker', 'scroller', 'rangeSelector'], function (name) { 9782 var prop = chart[name]; 9783 9784 if (prop) { 9785 chart[name] = prop.destroy(); 9786 } 9787 }); 9788 9789 // ==== Destroy local variables: 9790 each([chartBackground, plotBorder, plotBackground, legend, tooltip, renderer, tracker], function (obj) { 9791 if (obj && obj.destroy) { 9792 obj.destroy(); 9793 } 9794 }); 9795 chartBackground = plotBorder = plotBackground = legend = tooltip = renderer = tracker = null; 9796 9797 // remove container and all SVG 9798 if (container) { // can break in IE when destroyed before finished loading 9799 container.innerHTML = ''; 9800 removeEvent(container); 9801 if (parentNode) { 9802 discardElement(container); 9803 } 9804 9805 // IE6 leak 9806 container = null; 9807 } 9808 9809 // memory and CPU leak 9810 clearInterval(tooltipInterval); 9811 9812 // clean it all up 9813 for (i in chart) { 9814 delete chart[i]; 9815 } 9816 9817 chart = null; 9818 options = null; 9819 } 9820 /** 9821 * Prepare for first rendering after all data are loaded 9822 */ 9823 function firstRender() { 9824 // VML namespaces can't be added until after complete. Listening 9825 // for Perini's doScroll hack is not enough. 9826 var ONREADYSTATECHANGE = 'onreadystatechange', 9827 COMPLETE = 'complete'; 9828 // Note: in spite of JSLint's complaints, win == win.top is required 9829 /*jslint eqeq: true*/ 9830 if ((!hasSVG && (win == win.top && doc.readyState !== COMPLETE)) || (useCanVG && !win.canvg)) { 9831 /*jslint eqeq: false*/ 9832 if (useCanVG) { 9833 // Delay rendering until canvg library is downloaded and ready 9834 CanVGController.push(firstRender, options.global.canvasToolsURL); 9835 } else { 9836 doc.attachEvent(ONREADYSTATECHANGE, function () { 9837 doc.detachEvent(ONREADYSTATECHANGE, firstRender); 9838 if (doc.readyState === COMPLETE) { 9839 firstRender(); 9840 } 9841 }); 9842 } 9843 return; 9844 } 9845 9846 // create the container 9847 getContainer(); 9848 9849 // Run an early event after the container and renderer are established 9850 fireEvent(chart, 'init'); 9851 9852 // Initialize range selector for stock charts 9853 if (Highcharts.RangeSelector && options.rangeSelector.enabled) { 9854 chart.rangeSelector = new Highcharts.RangeSelector(chart); 9855 } 9856 9857 resetMargins(); 9858 setChartSize(); 9859 9860 // Set the common inversion and transformation for inverted series after initSeries 9861 setInverted(); 9862 9863 // get axes 9864 getAxes(); 9865 9866 // Initialize the series 9867 each(options.series || [], function (serieOptions) { 9868 initSeries(serieOptions); 9869 }); 9870 9871 // Run an event where series and axes can be added 9872 //fireEvent(chart, 'beforeRender'); 9873 9874 // Initialize scroller for stock charts 9875 if (Highcharts.Scroller && (options.navigator.enabled || options.scrollbar.enabled)) { 9876 chart.scroller = new Highcharts.Scroller(chart); 9877 } 9878 9879 chart.render = render; 9880 9881 // depends on inverted and on margins being set 9882 chart.tracker = tracker = new MouseTracker(options.tooltip); 9883 9884 9885 render(); 9886 9887 // add canvas 9888 renderer.draw(); 9889 // run callbacks 9890 if (callback) { 9891 callback.apply(chart, [chart]); 9892 } 9893 each(chart.callbacks, function (fn) { 9894 fn.apply(chart, [chart]); 9895 }); 9896 9897 9898 // If the chart was rendered outside the top container, put it back in 9899 if (renderToClone) { 9900 renderTo.appendChild(container); 9901 discardElement(renderToClone); 9902 } 9903 9904 fireEvent(chart, 'load'); 9905 9906 } 9907 9908 // Run chart 9909 9910 // Set up auto resize 9911 if (optionsChart.reflow !== false) { 9912 addEvent(chart, 'load', initReflow); 9913 } 9914 9915 // Chart event handlers 9916 if (chartEvents) { 9917 for (eventType in chartEvents) { 9918 addEvent(chart, eventType, chartEvents[eventType]); 9919 } 9920 } 9921 9922 9923 chart.options = options; 9924 chart.series = series; 9925 9926 9927 chart.xAxis = []; 9928 chart.yAxis = []; 9929 9930 9931 9932 9933 // Expose methods and variables 9934 chart.addSeries = addSeries; 9935 chart.animation = useCanVG ? false : pick(optionsChart.animation, true); 9936 chart.Axis = Axis; 9937 chart.destroy = destroy; 9938 chart.get = get; 9939 chart.getSelectedPoints = getSelectedPoints; 9940 chart.getSelectedSeries = getSelectedSeries; 9941 chart.hideLoading = hideLoading; 9942 chart.initSeries = initSeries; 9943 chart.isInsidePlot = isInsidePlot; 9944 chart.redraw = redraw; 9945 chart.setSize = resize; 9946 chart.setTitle = setTitle; 9947 chart.showLoading = showLoading; 9948 chart.pointCount = 0; 9949 chart.counters = new ChartCounters(); 9950 /* 9951 if ($) $(function () { 9952 $container = $('#container'); 9953 var origChartWidth,
9954 origChartHeight; 9955 if ($container) { 9956 $('<button>+</button>') 9957 .insertBefore($container) 9958 .click(function () { 9959 if (origChartWidth === UNDEFINED) { 9960 origChartWidth = chartWidth; 9961 origChartHeight = chartHeight; 9962 } 9963 chart.resize(chartWidth *= 1.1, chartHeight *= 1.1); 9964 }); 9965 $('<button>-</button>') 9966 .insertBefore($container) 9967 .click(function () { 9968 if (origChartWidth === UNDEFINED) { 9969 origChartWidth = chartWidth; 9970 origChartHeight = chartHeight; 9971 } 9972 chart.resize(chartWidth *= 0.9, chartHeight *= 0.9); 9973 }); 9974 $('<button>1:1</button>') 9975 .insertBefore($container) 9976 .click(function () { 9977 if (origChartWidth === UNDEFINED) { 9978 origChartWidth = chartWidth; 9979 origChartHeight = chartHeight; 9980 } 9981 chart.resize(origChartWidth, origChartHeight); 9982 }); 9983 } 9984 }) 9985 */ 9986 9987 9988 9989 9990 firstRender(); 9991 9992 9993} // end Chart 9994 9995// Hook for exporting module 9996Chart.prototype.callbacks = []; 9997/** 9998 * The Point object and prototype. Inheritable and used as base for PiePoint 9999 */ 10000var Point = function () {}; 10001Point.prototype = { 10002 10003 /** 10004 * Initialize the point 10005 * @param {Object} series The series object containing this point 10006 * @param {Object} options The data in either number, array or object format 10007 */ 10008 init: function (series, options, x) { 10009 var point = this, 10010 counters = series.chart.counters, 10011 defaultColors; 10012 point.series = series; 10013 point.applyOptions(options, x); 10014 point.pointAttr = {}; 10015 10016 if (series.options.colorByPoint) { 10017 defaultColors = series.chart.options.colors; 10018 if (!point.options) { 10019 point.options = {}; 10020 } 10021 point.color = point.options.color = point.color || defaultColors[counters.color++]; 10022 10023 // loop back to zero 10024 counters.wrapColor(defaultColors.length); 10025 } 10026 10027 series.chart.pointCount++; 10028 return point; 10029 }, 10030 /** 10031 * Apply the options containing the x and y data and possible some extra properties. 10032 * This is called on point init or from point.update. 10033 * 10034 * @param {Object} options 10035 */ 10036 applyOptions: function (options, x) { 10037 var point = this, 10038 series = point.series, 10039 optionsType = typeof options; 10040 10041 point.config = options; 10042 10043 // onedimensional array input 10044 if (optionsType === 'number' || options === null) { 10045 point.y = options; 10046 } else if (typeof options[0] === 'number') { // two-dimentional array 10047 point.x = options[0]; 10048 point.y = options[1]; 10049 } else if (optionsType === 'object' && typeof options.length !== 'number') { // object input 10050 // copy options directly to point 10051 extend(point, options); 10052 point.options = options; 10053 10054 // This is the fastest way to detect if there are individual point dataLabels that need 10055 // to be considered in drawDataLabels. These can only occur in object configs. 10056 if (options.dataLabels) { 10057 series._hasPointLabels = true; 10058 } 10059 } else if (typeof options[0] === 'string') { // categorized data with name in first position 10060 point.name = options[0]; 10061 point.y = options[1]; 10062 } 10063 10064 /* 10065 * If no x is set by now, get auto incremented value. All points must have an 10066 * x value, however the y value can be null to create a gap in the series 10067 */ 10068 // todo: skip this? It is only used in applyOptions, in translate it should not be used 10069 if (point.x === UNDEFINED) { 10070 point.x = x === UNDEFINED ? series.autoIncrement() : x; 10071 } 10072 10073 10074 10075 }, 10076 10077 /** 10078 * Destroy a point to clear memory. Its reference still stays in series.data. 10079 */ 10080 destroy: function () { 10081 var point = this, 10082 series = point.series, 10083 hoverPoints = series.chart.hoverPoints, 10084 prop; 10085 10086 series.chart.pointCount--; 10087 10088 if (hoverPoints) { 10089 point.setState(); 10090 erase(hoverPoints, point); 10091 } 10092 if (point === series.chart.hoverPoint) { 10093 point.onMouseOut(); 10094 } 10095 series.chart.hoverPoints = null; 10096 10097 // remove all events 10098 if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive 10099 removeEvent(point); 10100 point.destroyElements(); 10101 } 10102 10103 if (point.legendItem) { // pies have legend items 10104 point.series.chart.legend.destroyItem(point); 10105 } 10106 10107 for (prop in point) { 10108 point[prop] = null; 10109 } 10110 10111 10112 }, 10113 10114 /** 10115 * Destroy SVG elements associated with the point 10116 */ 10117 destroyElements: function () { 10118 var point = this, 10119 props = ['graphic', 'tracker', 'dataLabel', 'group', 'connector', 'shadowGroup'], 10120 prop, 10121 i = 6; 10122 while (i--) { 10123 prop = props[i]; 10124 if (point[prop]) { 10125 point[prop] = point[prop].destroy(); 10126 } 10127 } 10128 }, 10129 10130 /** 10131 * Return the configuration hash needed for the data label and tooltip formatters 10132 */ 10133 getLabelConfig: function () { 10134 var point = this; 10135 return { 10136 x: point.category, 10137 y: point.y, 10138 key: point.name || point.category, 10139 series: point.series, 10140 point: point, 10141 percentage: point.percentage, 10142 total: point.total || point.stackTotal 10143 }; 10144 }, 10145 10146 /** 10147 * Toggle the selection status of a point 10148 * @param {Boolean} selected Whether to select or unselect the point. 10149 * @param {Boolean} accumulate Whether to add to the previous selection. By default, 10150 * this happens if the control key (Cmd on Mac) was pressed during clicking. 10151 */ 10152 select: function (selected, accumulate) { 10153 var point = this, 10154 series = point.series, 10155 chart = series.chart; 10156 10157 selected = pick(selected, !point.selected); 10158 10159 // fire the event with the defalut handler 10160 point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () { 10161 point.selected = selected; 10162 point.setState(selected && SELECT_STATE); 10163 10164 // unselect all other points unless Ctrl or Cmd + click 10165 if (!accumulate) { 10166 each(chart.getSelectedPoints(), function (loopPoint) { 10167 if (loopPoint.selected && loopPoint !== point) { 10168 loopPoint.selected = false;
10169 loopPoint.setState(NORMAL_STATE); 10170 loopPoint.firePointEvent('unselect'); 10171 } 10172 }); 10173 } 10174 }); 10175 }, 10176 10177 onMouseOver: function () { 10178 var point = this, 10179 series = point.series, 10180 chart = series.chart, 10181 tooltip = chart.tooltip, 10182 hoverPoint = chart.hoverPoint; 10183 10184 // set normal state to previous series 10185 if (hoverPoint && hoverPoint !== point) { 10186 hoverPoint.onMouseOut(); 10187 } 10188 10189 // trigger the event 10190 point.firePointEvent('mouseOver'); 10191 10192 // update the tooltip 10193 if (tooltip && (!tooltip.shared || series.noSharedTooltip)) { 10194 tooltip.refresh(point); 10195 } 10196 10197 // hover this 10198 point.setState(HOVER_STATE); 10199 chart.hoverPoint = point; 10200 }, 10201 10202 onMouseOut: function () { 10203 var point = this; 10204 point.firePointEvent('mouseOut'); 10205 10206 point.setState(); 10207 point.series.chart.hoverPoint = null; 10208 }, 10209 10210 /** 10211 * Extendable method for formatting each point's tooltip line 10212 * 10213 * @return {String} A string to be concatenated in to the common tooltip text 10214 */ 10215 tooltipFormatter: function (pointFormat) { 10216 var point = this, 10217 series = point.series, 10218 seriesTooltipOptions = series.tooltipOptions, 10219 split = String(point.y).split('.'), 10220 originalDecimals = split[1] ? split[1].length : 0, 10221 match = pointFormat.match(/\{(series|point)\.[a-zA-Z]+\}/g), 10222 splitter = /[{\.}]/, 10223 obj, 10224 key, 10225 replacement, 10226 parts, 10227 prop, 10228 i; 10229 10230 // loop over the variables defined on the form {series.name}, {point.y} etc 10231 for (i in match) { 10232 key = match[i]; 10233 if (isString(key) && key !== pointFormat) { // IE matches more than just the variables 10234 10235 // Split it further into parts 10236 parts = (' ' + key).split(splitter); // add empty string because IE and the rest handles it differently 10237 obj = { 'point': point, 'series': series }[parts[1]]; 10238 prop = parts[2]; 10239 10240 // Add some preformatting 10241 if (obj === point && (prop === 'y' || prop === 'open' || prop === 'high' || 10242 prop === 'low' || prop === 'close')) { 10243 replacement = (seriesTooltipOptions.valuePrefix || seriesTooltipOptions.yPrefix || '') + 10244 numberFormat(point[prop], pick(seriesTooltipOptions.valueDecimals, seriesTooltipOptions.yDecimals, originalDecimals)) + 10245 (seriesTooltipOptions.valueSuffix || seriesTooltipOptions.ySuffix || ''); 10246 10247 // Automatic replacement 10248 } else { 10249 replacement = obj[prop]; 10250 } 10251 10252 pointFormat = pointFormat.replace(key, replacement); 10253 } 10254 } 10255 10256 return pointFormat; 10257 }, 10258 10259 /** 10260 * Update the point with new options (typically x/y data) and optionally redraw the series. 10261 * 10262 * @param {Object} options Point options as defined in the series.data array 10263 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call 10264 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation 10265 * configuration 10266 * 10267 */ 10268 update: function (options, redraw, animation) { 10269 var point = this, 10270 series = point.series, 10271 graphic = point.graphic, 10272 i, 10273 data = series.data, 10274 dataLength = data.length, 10275 chart = series.chart; 10276 10277 redraw = pick(redraw, true); 10278 10279 // fire the event with a default handler of doing the update 10280 point.firePointEvent('update', { options: options }, function () { 10281 10282 point.applyOptions(options); 10283 10284 // update visuals 10285 if (isObject(options)) { 10286 series.getAttribs(); 10287 if (graphic) { 10288 graphic.attr(point.pointAttr[series.state]); 10289 } 10290 } 10291 10292 // record changes in the parallel arrays 10293 for (i = 0; i < dataLength; i++) { 10294 if (data[i] === point) { 10295 series.xData[i] = point.x; 10296 series.yData[i] = point.y; 10297 series.options.data[i] = options; 10298 break; 10299 } 10300 } 10301 10302 // redraw 10303 series.isDirty = true; 10304 series.isDirtyData = true; 10305 if (redraw) { 10306 chart.redraw(animation); 10307 } 10308 }); 10309 }, 10310 10311 /** 10312 * Remove a point and optionally redraw the series and if necessary the axes 10313 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call 10314 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation 10315 * configuration 10316 */ 10317 remove: function (redraw, animation) { 10318 var point = this, 10319 series = point.series, 10320 chart = series.chart, 10321 i, 10322 data = series.data, 10323 dataLength = data.length; 10324 10325 setAnimation(animation, chart); 10326 redraw = pick(redraw, true); 10327 10328 // fire the event with a default handler of removing the point 10329 point.firePointEvent('remove', null, function () { 10330 10331 //erase(series.data, point); 10332 10333 for (i = 0; i < dataLength; i++) { 10334 if (data[i] === point) { 10335 10336 // splice all the parallel arrays 10337 data.splice(i, 1); 10338 series.options.data.splice(i, 1); 10339 series.xData.splice(i, 1); 10340 series.yData.splice(i, 1); 10341 break; 10342 } 10343 } 10344 10345 point.destroy(); 10346 10347 10348 // redraw 10349 series.isDirty = true; 10350 series.isDirtyData = true; 10351 if (redraw) { 10352 chart.redraw(); 10353 } 10354 }); 10355 10356 10357 }, 10358 10359 /** 10360 * Fire an event on the Point object. Must not be renamed to fireEvent, as this 10361 * causes a name clash in MooTools 10362 * @param {String} eventType 10363 * @param {Object} eventArgs Additional event arguments 10364 * @param {Function} defaultFunction Default event handler 10365 */ 10366 firePointEvent: function (eventType, eventArgs, defaultFunction) { 10367 var point = this, 10368 series = this.series, 10369 seriesOptions = series.options; 10370 10371 // load event handlers on demand to save time on mouseover/out 10372 if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) { 10373 this.importEvents(); 10374 } 10375 10376 // add default handler if in selection mode 10377 if (eventType === 'click' && seriesOptions.allowPointSelect) { 10378 defaultFunction = function (event) { 10379 // Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera 10380 point.select(null, event.ctrlKey || event.metaKey || event.shiftKey); 10381 }; 10382 } 10383 10384 fireEvent(this, eventType, eventArgs, defaultFunction); 10385 }, 10386 /** 10387 * Import events from the series' and point's options. Only do it on 10388 * demand, to save processing time on hovering. 10389 */ 10390 importEvents: function () { 10391 if (!this.hasImportedEvents) { 10392 var point = this, 10393 options = merge(point.series.options.point, point.options), 10394 events = options.events, 10395 eventType; 10396 10397 point.events = events; 10398 10399 for (eventType in events) { 10400 addEvent(point, eventType, events[eventType]); 10401 } 10402 this.hasImportedEvents = true; 10403 10404 } 10405 }, 10406 10407 /** 10408 * Set the point's state 10409 * @param {String} state 10410 */ 10411 setState: function (state) { 10412 var point = this, 10413 plotX = point.plotX, 10414 plotY = point.plotY, 10415 series = point.series, 10416 stateOptions = series.options.states, 10417 markerOptions = defaultPlotOptions[series.type].marker && series.options.marker, 10418 normalDisabled = markerOptions && !markerOptions.enabled, 10419 markerStateOptions = markerOptions && markerOptions.states[state], 10420 stateDisabled = markerStateOptions && markerStateOptions.enabled === false, 10421 stateMarkerGraphic = series.stateMarkerGraphic, 10422 chart = series.chart, 10423 radius, 10424 pointAttr = point.pointAttr; 10425 10426 state = state || NORMAL_STATE; // empty string 10427 10428 if ( 10429 // already has this state 10430 state === point.state || 10431 // selected points don't respond to hover 10432 (point.selected && state !== SELECT_STATE) || 10433 // series' state options is disabled 10434 (stateOptions[state] && stateOptions[state].enabled === false) || 10435 // point marker's state options is disabled 10436 (state && (stateDisabled || (normalDisabled && !markerStateOptions.enabled))) 10437 10438 ) { 10439 return; 10440 } 10441 10442 // apply hover styles to the existing point 10443 if (point.graphic) { 10444 radius = markerOptions && point.graphic.symbolName && pointAttr[state].r; 10445 point.graphic.attr(merge( 10446 pointAttr[state], 10447 radius ? { // new symbol attributes (#507, #612)
10448 x: plotX - radius, 10449 y: plotY - radius, 10450 width: 2 * radius, 10451 height: 2 * radius 10452 } : {} 10453 )); 10454 } else { 10455 // if a graphic is not applied to each point in the normal state, create a shared 10456 // graphic for the hover state 10457 if (state) { 10458 if (!stateMarkerGraphic) { 10459 radius = markerOptions.radius; 10460 series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol( 10461 series.symbol, 10462 -radius, 10463 -radius, 10464 2 * radius, 10465 2 * radius 10466 ) 10467 .attr(pointAttr[state]) 10468 .add(series.group); 10469 } 10470 10471 stateMarkerGraphic.translate( 10472 plotX, 10473 plotY 10474 ); 10475 } 10476 10477 if (stateMarkerGraphic) { 10478 stateMarkerGraphic[state ? 'show' : 'hide'](); 10479 } 10480 } 10481 10482 point.state = state; 10483 } 10484}; 10485 10486/** 10487 * @classDescription The base function which all other series types inherit from. The data in the series is stored 10488 * in various arrays. 10489 * 10490 * - First, series.options.data contains all the original config options for 10491 * each point whether added by options or methods like series.addPoint. 10492 * - Next, series.data contains those values converted to points, but in case the series data length 10493 * exceeds the cropThreshold, or if the data is grouped, series.data doesn't contain all the points. It 10494 * only contains the points that have been created on demand. 10495 * - Then there's series.points that contains all currently visible point objects. In case of cropping, 10496 * the cropped-away points are not part of this array. The series.points array starts at series.cropStart 10497 * compared to series.data and series.options.data. If however the series data is grouped, these can't 10498 * be correlated one to one. 10499 * - series.xData and series.processedXData contain clean x values, equivalent to series.data and series.points. 10500 * - series.yData and series.processedYData contain clean x values, equivalent to series.data and series.points. 10501 * 10502 * @param {Object} chart 10503 * @param {Object} options 10504 */ 10505var Series = function () {}; 10506 10507Series.prototype = { 10508 10509 isCartesian: true, 10510 type: 'line', 10511 pointClass: Point, 10512 sorted: true, // requires the data to be sorted 10513 pointAttrToOptions: { // mapping between SVG attributes and the corresponding options 10514 stroke: 'lineColor', 10515 'stroke-width': 'lineWidth', 10516 fill: 'fillColor', 10517 r: 'radius' 10518 }, 10519 init: function (chart, options) { 10520 var series = this, 10521 eventType, 10522 events, 10523 //pointEvent, 10524 index = chart.series.length; 10525 10526 series.chart = chart; 10527 series.options = options = series.setOptions(options); // merge with plotOptions 10528 10529 // bind the axes 10530 series.bindAxes(); 10531 10532 // set some variables 10533 extend(series, { 10534 index: index, 10535 name: options.name || 'Series ' + (index + 1), 10536 state: NORMAL_STATE, 10537 pointAttr: {}, 10538 visible: options.visible !== false, // true by default 10539 selected: options.selected === true // false by default 10540 }); 10541 10542 // special 10543 if (useCanVG) { 10544 options.animation = false; 10545 } 10546 10547 // register event listeners 10548 events = options.events; 10549 for (eventType in events) { 10550 addEvent(series, eventType, events[eventType]); 10551 } 10552 if ( 10553 (events && events.click) || 10554 (options.point && options.point.events && options.point.events.click) || 10555 options.allowPointSelect 10556 ) { 10557 chart.runTrackerClick = true; 10558 } 10559 10560 series.getColor(); 10561 series.getSymbol(); 10562 10563 // set the data 10564 series.setData(options.data, false); 10565 10566 }, 10567 10568 10569 10570 /** 10571 * Set the xAxis and yAxis properties of cartesian series, and register the series 10572 * in the axis.series array 10573 */ 10574 bindAxes: function () { 10575 var series = this, 10576 seriesOptions = series.options, 10577 chart = series.chart, 10578 axisOptions; 10579 10580 if (series.isCartesian) { 10581 10582 each(['xAxis', 'yAxis'], function (AXIS) { // repeat for xAxis and yAxis 10583 10584 each(chart[AXIS], function (axis) { // loop through the chart's axis objects 10585 10586 axisOptions = axis.options; 10587 10588 // apply if the series xAxis or yAxis option mathches the number of the 10589 // axis, or if undefined, use the first axis 10590 if ((seriesOptions[AXIS] === axisOptions.index) || 10591 (seriesOptions[AXIS] === UNDEFINED && axisOptions.index === 0)) { 10592 10593 // register this series in the axis.series lookup
10594 axis.series.push(series); 10595 10596 // set this series.xAxis or series.yAxis reference 10597 series[AXIS] = axis; 10598 10599 // mark dirty for redraw 10600 axis.isDirty = true; 10601 } 10602 }); 10603 10604 }); 10605 } 10606 }, 10607 10608 10609 /** 10610 * Return an auto incremented x value based on the pointStart and pointInterval options. 10611 * This is only used if an x value is not given for the point that calls autoIncrement. 10612 */ 10613 autoIncrement: function () { 10614 var series = this, 10615 options = series.options, 10616 xIncrement = series.xIncrement; 10617 10618 xIncrement = pick(xIncrement, options.pointStart, 0); 10619 10620 series.pointInterval = pick(series.pointInterval, options.pointInterval, 1); 10621 10622 series.xIncrement = xIncrement + series.pointInterval; 10623 return xIncrement; 10624 }, 10625 10626 /** 10627 * Divide the series data into segments divided by null values. 10628 */ 10629 getSegments: function () { 10630 var series = this, 10631 lastNull = -1, 10632 segments = [], 10633 i, 10634 points = series.points, 10635 pointsLength = points.length; 10636 10637 if (pointsLength) { // no action required for [] 10638 10639 // if connect nulls, just remove null points 10640 if (series.options.connectNulls) { 10641 i = pointsLength; 10642 while (i--) { 10643 if (points[i].y === null) { 10644 points.splice(i, 1); 10645 } 10646 } 10647 if (points.length) { 10648 segments = [points]; 10649 } 10650 10651 // else, split on null points 10652 } else { 10653 each(points, function (point, i) { 10654 if (point.y === null) { 10655 if (i > lastNull + 1) { 10656 segments.push(points.slice(lastNull + 1, i)); 10657 } 10658 lastNull = i; 10659 } else if (i === pointsLength - 1) { // last value 10660 segments.push(points.slice(lastNull + 1, i + 1)); 10661 } 10662 }); 10663 } 10664 } 10665 10666 // register it 10667 series.segments = segments; 10668 }, 10669 /** 10670 * Set the series options by merging from the options tree 10671 * @param {Object} itemOptions 10672 */ 10673 setOptions: function (itemOptions) { 10674 var series = this, 10675 chart = series.chart, 10676 chartOptions = chart.options, 10677 plotOptions = chartOptions.plotOptions, 10678 data = itemOptions.data, 10679 options; 10680 10681 itemOptions.data = null; // remove from merge to prevent looping over the data set 10682 10683 options = merge( 10684 plotOptions[this.type], 10685 plotOptions.series, 10686 itemOptions 10687 ); 10688 10689 // Re-insert the data array to the options and the original config (#717) 10690 options.data = itemOptions.data = data; 10691 10692 // the tooltip options are merged between global and series specific options 10693 series.tooltipOptions = merge(chartOptions.tooltip, options.tooltip); 10694 10695 return options; 10696 10697 }, 10698 /** 10699 * Get the series' color 10700 */ 10701 getColor: function () { 10702 var defaultColors = this.chart.options.colors, 10703 counters = this.chart.counters; 10704 this.color = this.options.color || defaultColors[counters.color++] || '#0000ff'; 10705 counters.wrapColor(defaultColors.length); 10706 }, 10707 /** 10708 * Get the series' symbol 10709 */ 10710 getSymbol: function () { 10711 var series = this, 10712 seriesMarkerOption = series.options.marker, 10713 chart = series.chart, 10714 defaultSymbols = chart.options.symbols, 10715 counters = chart.counters; 10716 series.symbol = seriesMarkerOption.symbol || defaultSymbols[counters.symbol++]; 10717 10718 // don't substract radius in image symbols (#604) 10719 if (/^url/.test(series.symbol)) { 10720 seriesMarkerOption.radius = 0; 10721 } 10722 counters.wrapSymbol(defaultSymbols.length); 10723 }, 10724 10725 /** 10726 * Add a point dynamically after chart load time 10727 * @param {Object} options Point options as given in series.data 10728 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call 10729 * @param {Boolean} shift If shift is true, a point is shifted off the start 10730 * of the series as one is appended to the end. 10731 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation 10732 * configuration 10733 */ 10734 addPoint: function (options, redraw, shift, animation) { 10735 var series = this, 10736 data = series.data, 10737 graph = series.graph, 10738 area = series.area,
10739 chart = series.chart, 10740 xData = series.xData, 10741 yData = series.yData, 10742 currentShift = (graph && graph.shift) || 0, 10743 dataOptions = series.options.data, 10744 point; 10745 //point = (new series.pointClass()).init(series, options); 10746 10747 setAnimation(animation, chart); 10748 10749 // Make graph animate sideways 10750 if (graph && shift) { 10751 graph.shift = currentShift + 1; 10752 } 10753 if (area) { 10754 if (shift) { // #780 10755 area.shift = currentShift + 1; 10756 } 10757 area.isArea = true; // needed in animation, both with and without shift 10758 } 10759 10760 // Optional redraw, defaults to true 10761 redraw = pick(redraw, true); 10762 10763 // Get options and push the point to xData, yData and series.options. In series.generatePoints 10764 // the Point instance will be created on demand and pushed to the series.data array. 10765 point = { series: series }; 10766 series.pointClass.prototype.applyOptions.apply(point, [options]); 10767 xData.push(point.x); 10768 yData.push(series.valueCount === 4 ? [point.open, point.high, point.low, point.close] : point.y); 10769 dataOptions.push(options); 10770 10771 10772 // Shift the first point off the parallel arrays 10773 // todo: consider series.removePoint(i) method 10774 if (shift) { 10775 if (data[0]) { 10776 data[0].remove(false); 10777 } else { 10778 data.shift(); 10779 xData.shift(); 10780 yData.shift(); 10781 dataOptions.shift(); 10782 } 10783 } 10784 series.getAttribs(); 10785 10786 // redraw 10787 series.isDirty = true; 10788 series.isDirtyData = true; 10789 if (redraw) { 10790 chart.redraw(); 10791 } 10792 }, 10793 10794 /** 10795 * Replace the series data with a new set of data 10796 * @param {Object} data 10797 * @param {Object} redraw 10798 */ 10799 setData: function (data, redraw) { 10800 var series = this, 10801 oldData = series.points, 10802 options = series.options, 10803 initialColor = series.initialColor, 10804 chart = series.chart, 10805 firstPoint = null, 10806 i; 10807 10808 // reset properties 10809 series.xIncrement = null; 10810 series.pointRange = (series.xAxis && series.xAxis.categories && 1) || options.pointRange; 10811 10812 if (defined(initialColor)) { // reset colors for pie 10813 chart.counters.color = initialColor; 10814 } 10815 10816 // parallel arrays 10817 var xData = [], 10818 yData = [], 10819 dataLength = data ? data.length : [], 10820 turboThreshold = options.turboThreshold || 1000, 10821 pt, 10822 ohlc = series.valueCount === 4; 10823 10824 // In turbo mode, only one- or twodimensional arrays of numbers are allowed. The 10825 // first value is tested, and we assume that all the rest are defined the same 10826 // way. Although the 'for' loops are similar, they are repeated inside each 10827 // if-else conditional for max performance. 10828 if (dataLength > turboThreshold) { 10829 10830 // find the first non-null point 10831 i = 0; 10832 while (firstPoint === null && i < dataLength) { 10833 firstPoint = data[i]; 10834 i++; 10835 } 10836 10837 10838 if (isNumber(firstPoint)) { // assume all points are numbers 10839 var x = pick(options.pointStart, 0), 10840 pointInterval = pick(options.pointInterval, 1); 10841 10842 for (i = 0; i < dataLength; i++) { 10843 xData[i] = x; 10844 yData[i] = data[i]; 10845 x += pointInterval; 10846 } 10847 series.xIncrement = x; 10848 } else if (isArray(firstPoint)) { // assume all points are arrays 10849 if (ohlc) { // [x, o, h, l, c] 10850 for (i = 0; i < dataLength; i++) { 10851 pt = data[i]; 10852 xData[i] = pt[0]; 10853 yData[i] = pt.slice(1, 5); 10854 } 10855 } else { // [x, y] 10856 for (i = 0; i < dataLength; i++) { 10857 pt = data[i]; 10858 xData[i] = pt[0]; 10859 yData[i] = pt[1]; 10860 } 10861 } 10862 } /* else { 10863 error(12); // Highcharts expects configs to be numbers or arrays in turbo mode 10864 }*/ 10865 } else { 10866 for (i = 0; i < dataLength; i++) { 10867 pt = { series: series }; 10868 series.pointClass.prototype.applyOptions.apply(pt, [data[i]]); 10869 xData[i] = pt.x; 10870 yData[i] = ohlc ? [pt.open, pt.high, pt.low, pt.close] : pt.y; 10871 } 10872 } 10873 10874 series.data = []; 10875 series.options.data = data; 10876 series.xData = xData; 10877 series.yData = yData; 10878 10879 // destroy old points 10880 i = (oldData && oldData.length) || 0; 10881 while (i--) { 10882 if (oldData[i] && oldData[i].destroy) { 10883 oldData[i].destroy(); 10884 } 10885 } 10886 10887 // redraw 10888 series.isDirty = series.isDirtyData = chart.isDirtyBox = true; 10889 if (pick(redraw, true)) { 10890 chart.redraw(false); 10891 } 10892 }, 10893 10894 /** 10895 * Remove a series and optionally redraw the chart 10896 * 10897 * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call 10898 * @param {Boolean|Object} animation Whether to apply animation, and optionally animation 10899 * configuration 10900 */ 10901 10902 remove: function (redraw, animation) { 10903 var series = this, 10904 chart = series.chart; 10905 redraw = pick(redraw, true); 10906 10907 if (!series.isRemoving) { /* prevent triggering native event in jQuery 10908 (calling the remove function from the remove event) */ 10909 series.isRemoving = true; 10910 10911 // fire the event with a default handler of removing the point 10912 fireEvent(series, 'remove', null, function () { 10913 10914 10915 // destroy elements 10916 series.destroy(); 10917 10918 10919 // redraw 10920 chart.isDirtyLegend = chart.isDirtyBox = true; 10921 if (redraw) { 10922 chart.redraw(animation); 10923 } 10924 }); 10925 10926 } 10927 series.isRemoving = false;
10928 }, 10929 10930 /** 10931 * Process the data by cropping away unused data points if the series is longer 10932 * than the crop threshold. This saves computing time for lage series. 10933 */ 10934 processData: function (force) { 10935 var series = this, 10936 processedXData = series.xData, // copied during slice operation below 10937 processedYData = series.yData, 10938 dataLength = processedXData.length, 10939 cropStart = 0, 10940 cropEnd = dataLength, 10941 cropped, 10942 distance, 10943 closestPointRange, 10944 xAxis = series.xAxis, 10945 i, // loop variable 10946 options = series.options, 10947 cropThreshold = options.cropThreshold, 10948 isCartesian = series.isCartesian; 10949 10950 // If the series data or axes haven't changed, don't go through this. Return false to pass 10951 // the message on to override methods like in data grouping. 10952 if (isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) { 10953 return false; 10954 } 10955 10956 // optionally filter out points outside the plot area 10957 if (isCartesian && series.sorted && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) { 10958 var extremes = xAxis.getExtremes(), 10959 min = extremes.min, 10960 max = extremes.max; 10961 10962 // it's outside current extremes 10963 if (processedXData[dataLength - 1] < min || processedXData[0] > max) { 10964 processedXData = []; 10965 processedYData = []; 10966 10967 // only crop if it's actually spilling out 10968 } else if (processedXData[0] < min || processedXData[dataLength - 1] > max) { 10969 10970 // iterate up to find slice start 10971 for (i = 0; i < dataLength; i++) { 10972 if (processedXData[i] >= min) { 10973 cropStart = mathMax(0, i - 1); 10974 break; 10975 } 10976 } 10977 // proceed to find slice end 10978 for (; i < dataLength; i++) { 10979 if (processedXData[i] > max) { 10980 cropEnd = i + 1; 10981 break; 10982 } 10983 10984 } 10985 processedXData = processedXData.slice(cropStart, cropEnd); 10986 processedYData = processedYData.slice(cropStart, cropEnd); 10987 cropped = true; 10988 } 10989 } 10990 10991 10992 // Find the closest distance between processed points 10993 for (i = processedXData.length - 1; i > 0; i--) { 10994 distance = processedXData[i] - processedXData[i - 1]; 10995 if (distance > 0 && (closestPointRange === UNDEFINED || distance < closestPointRange)) { 10996 closestPointRange = distance; 10997 } 10998 } 10999 11000 // Record the properties 11001 series.cropped = cropped; // undefined or true 11002 series.cropStart = cropStart; 11003 series.processedXData = processedXData; 11004 series.processedYData = processedYData; 11005 11006 if (options.pointRange === null) { // null means auto, as for columns, candlesticks and OHLC 11007 series.pointRange = closestPointRange || 1; 11008 } 11009 series.closestPointRange = closestPointRange; 11010 11011 }, 11012 11013 /** 11014 * Generate the data point after the data has been processed by cropping away 11015 * unused points and optionally grouped in Highcharts Stock. 11016 */ 11017 generatePoints: function () { 11018 var series = this, 11019 options = series.options, 11020 dataOptions = options.data, 11021 data = series.data, 11022 dataLength, 11023 processedXData = series.processedXData, 11024 processedYData = series.processedYData, 11025 pointClass = series.pointClass, 11026 processedDataLength = processedXData.length, 11027 cropStart = series.cropStart || 0, 11028 cursor, 11029 hasGroupedData = series.hasGroupedData, 11030 point, 11031 points = [], 11032 i; 11033 11034 if (!data && !hasGroupedData) { 11035 var arr = []; 11036 arr.length = dataOptions.length; 11037 data = series.data = arr; 11038 } 11039 11040 for (i = 0; i < processedDataLength; i++) { 11041 cursor = cropStart + i; 11042 if (!hasGroupedData) { 11043 if (data[cursor]) { 11044 point = data[cursor]; 11045 } else { 11046 data[cursor] = point = (new pointClass()).init(series, dataOptions[cursor], processedXData[i]); 11047 } 11048 points[i] = point; 11049 } else { 11050 // splat the y data in case of ohlc data array 11051 points[i] = (new pointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i]))); 11052 } 11053 } 11054 11055 // Hide cropped-away points - this only runs when the number of points is above cropThreshold, or when 11056 // swithching view from non-grouped data to grouped data (#637) 11057 if (data && (processedDataLength !== (dataLength = data.length) || hasGroupedData)) { 11058 for (i = 0; i < dataLength; i++) { 11059 if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points 11060 i += processedDataLength; 11061 } 11062 if (data[i]) { 11063 data[i].destroyElements(); 11064 } 11065 } 11066 } 11067 11068 series.data = data; 11069 series.points = points; 11070 }, 11071 11072 /** 11073 * Translate data points from raw data values to chart specific positioning data 11074 * needed later in drawPoints, drawGraph and drawTracker. 11075 */ 11076 translate: function () { 11077 if (!this.processedXData) { // hidden series 11078 this.processData(); 11079 } 11080 this.generatePoints(); 11081 var series = this, 11082 chart = series.chart, 11083 options = series.options, 11084 stacking = options.stacking, 11085 xAxis = series.xAxis, 11086 categories = xAxis.categories, 11087 yAxis = series.yAxis, 11088 points = series.points, 11089 dataLength = points.length, 11090 hasModifyValue = !!series.modifyValue, 11091 isLastSeries, 11092 allStackSeries = yAxis.series, 11093 i = allStackSeries.length; 11094 11095 // Is it the last visible series? 11096 while (i--) { 11097 if (allStackSeries[i].visible) { 11098 if (i === series.index) { 11099 isLastSeries = true; 11100 } 11101 break; 11102 } 11103 } 11104 11105 // Translate each point 11106 for (i = 0; i < dataLength; i++) { 11107 var point = points[i], 11108 xValue = point.x, 11109 yValue = point.y, 11110 yBottom = point.low, 11111 stack = yAxis.stacks[(yValue < options.threshold ? '-' : '') + series.stackKey], 11112 pointStack, 11113 pointStackTotal; 11114 11115 // get the plotX translation 11116 point.plotX = mathRound(xAxis.translate(xValue, 0, 0, 0, 1) * 10) / 10; // Math.round fixes #591 11117 11118 // calculate the bottom y value for stacked series 11119 if (stacking && series.visible && stack && stack[xValue]) { 11120 pointStack = stack[xValue]; 11121 pointStackTotal = pointStack.total; 11122 pointStack.cum = yBottom = pointStack.cum - yValue; // start from top 11123 yValue = yBottom + yValue; 11124 11125 if (isLastSeries) { 11126 yBottom = options.threshold; 11127 } 11128 11129 if (stacking === 'percent') { 11130 yBottom = pointStackTotal ? yBottom * 100 / pointStackTotal : 0; 11131 yValue = pointStackTotal ? yValue * 100 / pointStackTotal : 0; 11132 } 11133 11134 point.percentage = pointStackTotal ? point.y * 100 / pointStackTotal : 0; 11135 point.stackTotal = pointStackTotal; 11136 point.stackY = yValue; 11137 } 11138 11139 // Set translated yBottom or remove it 11140 point.yBottom = defined(yBottom) ? 11141 yAxis.translate(yBottom, 0, 1, 0, 1) : 11142 null; 11143 11144 // general hook, used for Highstock compare mode 11145 if (hasModifyValue) { 11146 yValue = series.modifyValue(yValue, point); 11147 } 11148 11149 // Set the the plotY value, reset it for redraws 11150 point.plotY = (typeof yValue === 'number') ? 11151 mathRound(yAxis.translate(yValue, 0, 1, 0, 1) * 10) / 10 : // Math.round fixes #591 11152 UNDEFINED; 11153 11154 // set client related positions for mouse tracking 11155 point.clientX = chart.inverted ? 11156 chart.plotHeight - point.plotX : 11157 point.plotX; // for mouse tracking 11158 11159 // some API data 11160 point.category = categories && categories[point.x] !== UNDEFINED ? 11161 categories[point.x] : point.x; 11162 11163 11164 } 11165 11166 // now that we have the cropped data, build the segments 11167 series.getSegments(); 11168 }, 11169 /** 11170 * Memoize tooltip texts and positions 11171 */
11172 setTooltipPoints: function (renew) { 11173 var series = this, 11174 chart = series.chart, 11175 inverted = chart.inverted, 11176 points = [], 11177 pointsLength, 11178 plotSize = mathRound((inverted ? chart.plotTop : chart.plotLeft) + chart.plotSizeX), 11179 low, 11180 high, 11181 xAxis = series.xAxis, 11182 point, 11183 i, 11184 tooltipPoints = []; // a lookup array for each pixel in the x dimension 11185 11186 // don't waste resources if tracker is disabled 11187 if (series.options.enableMouseTracking === false) { 11188 return; 11189 } 11190 11191 // renew 11192 if (renew) { 11193 series.tooltipPoints = null; 11194 } 11195 11196 // concat segments to overcome null values 11197 each(series.segments || series.points, function (segment) { 11198 points = points.concat(segment); 11199 }); 11200 11201 // loop the concatenated points and apply each point to all the closest 11202 // pixel positions 11203 if (xAxis && xAxis.reversed) { 11204 points = points.reverse();//reverseArray(points); 11205 } 11206 11207 //each(points, function (point, i) { 11208 pointsLength = points.length; 11209 for (i = 0; i < pointsLength; i++) { 11210 point = points[i]; 11211 low = points[i - 1] ? points[i - 1]._high + 1 : 0; 11212 high = point._high = points[i + 1] ? 11213 (mathFloor((point.plotX + (points[i + 1] ? points[i + 1].plotX : plotSize)) / 2)) : 11214 plotSize; 11215 11216 while (low <= high) { 11217 tooltipPoints[inverted ? plotSize - low++ : low++] = point; 11218 } 11219 } 11220 series.tooltipPoints = tooltipPoints; 11221 }, 11222 11223 /** 11224 * Format the header of the tooltip 11225 */ 11226 tooltipHeaderFormatter: function (key) { 11227 var series = this, 11228 tooltipOptions = series.tooltipOptions, 11229 xDateFormat = tooltipOptions.xDateFormat || '%A, %b %e, %Y', 11230 xAxis = series.xAxis, 11231 isDateTime = xAxis && xAxis.options.type === 'datetime'; 11232 11233 return tooltipOptions.headerFormat 11234 .replace('{point.key}', isDateTime ? dateFormat(xDateFormat, key) : key) 11235 .replace('{series.name}', series.name) 11236 .replace('{series.color}', series.color); 11237 }, 11238 11239 /** 11240 * Series mouse over handler 11241 */ 11242 onMouseOver: function () { 11243 var series = this, 11244 chart = series.chart, 11245 hoverSeries = chart.hoverSeries; 11246 11247 if (!hasTouch && chart.mouseIsDown) { 11248 return; 11249 } 11250 11251 // set normal state to previous series 11252 if (hoverSeries && hoverSeries !== series) { 11253 hoverSeries.onMouseOut(); 11254 } 11255 11256 // trigger the event, but to save processing time, 11257 // only if defined 11258 if (series.options.events.mouseOver) { 11259 fireEvent(series, 'mouseOver'); 11260 } 11261 11262 // hover this 11263 series.setState(HOVER_STATE); 11264 chart.hoverSeries = series; 11265 }, 11266 11267 /** 11268 * Series mouse out handler 11269 */ 11270 onMouseOut: function () { 11271 // trigger the event only if listeners exist 11272 var series = this, 11273 options = series.options, 11274 chart = series.chart, 11275 tooltip = chart.tooltip, 11276 hoverPoint = chart.hoverPoint; 11277 11278 // trigger mouse out on the point, which must be in this series 11279 if (hoverPoint) { 11280 hoverPoint.onMouseOut(); 11281 } 11282 11283 // fire the mouse out event 11284 if (series && options.events.mouseOut) { 11285 fireEvent(series, 'mouseOut'); 11286 } 11287 11288 11289 // hide the tooltip 11290 if (tooltip && !options.stickyTracking && !tooltip.shared) { 11291 tooltip.hide(); 11292 } 11293 11294 // set normal state 11295 series.setState(); 11296 chart.hoverSeries = null; 11297 }, 11298 11299 /** 11300 * Animate in the series 11301 */ 11302 animate: function (init) { 11303 var series = this, 11304 chart = series.chart, 11305 clipRect = series.clipRect, 11306 animation = series.options.animation; 11307 11308 if (animation && !isObject(animation)) { 11309 animation = {}; 11310 } 11311 11312 if (init) { // initialize the animation 11313 if (!clipRect.isAnimating) { // apply it only for one of the series 11314 clipRect.attr('width', 0); 11315 clipRect.isAnimating = true; 11316 } 11317 11318 } else { // run the animation 11319 clipRect.animate({ 11320 width: chart.plotSizeX 11321 }, animation); 11322 11323 // delete this function to allow it only once 11324 this.animate = null; 11325 } 11326 }, 11327 11328 11329 /** 11330 * Draw the markers 11331 */ 11332 drawPoints: function () { 11333 var series = this, 11334 pointAttr, 11335 points = series.points, 11336 chart = series.chart, 11337 plotX, 11338 plotY, 11339 i, 11340 point, 11341 radius, 11342 symbol, 11343 isImage, 11344 graphic; 11345 11346 if (series.options.marker.enabled) { 11347 i = points.length; 11348 while (i--) { 11349 point = points[i]; 11350 plotX = point.plotX; 11351 plotY = point.plotY; 11352 graphic = point.graphic; 11353 11354 // only draw the point if y is defined 11355 if (plotY !== UNDEFINED && !isNaN(plotY)) { 11356 11357 // shortcuts 11358 pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]; 11359 radius = pointAttr.r; 11360 symbol = pick(point.marker && point.marker.symbol, series.symbol); 11361 isImage = symbol.indexOf('url') === 0; 11362 11363 if (graphic) { // update 11364 graphic.animate(extend({
11365 x: plotX - radius, 11366 y: plotY - radius 11367 }, graphic.symbolName ? { // don't apply to image symbols #507 11368 width: 2 * radius, 11369 height: 2 * radius 11370 } : {})); 11371 } else if (radius > 0 || isImage) { 11372 point.graphic = chart.renderer.symbol( 11373 symbol, 11374 plotX - radius, 11375 plotY - radius, 11376 2 * radius, 11377 2 * radius 11378 ) 11379 .attr(pointAttr) 11380 .add(series.group); 11381 } 11382 } 11383 } 11384 } 11385 11386 }, 11387 11388 /** 11389 * Convert state properties from API naming conventions to SVG attributes 11390 * 11391 * @param {Object} options API options object 11392 * @param {Object} base1 SVG attribute object to inherit from 11393 * @param {Object} base2 Second level SVG attribute object to inherit from 11394 */ 11395 convertAttribs: function (options, base1, base2, base3) { 11396 var conversion = this.pointAttrToOptions, 11397 attr, 11398 option, 11399 obj = {}; 11400 11401 options = options || {}; 11402 base1 = base1 || {}; 11403 base2 = base2 || {}; 11404 base3 = base3 || {}; 11405 11406 for (attr in conversion) { 11407 option = conversion[attr]; 11408 obj[attr] = pick(options[option], base1[attr], base2[attr], base3[attr]); 11409 } 11410 return obj; 11411 }, 11412 11413 /** 11414 * Get the state attributes. Each series type has its own set of attributes 11415 * that are allowed to change on a point's state change. Series wide attributes are stored for 11416 * all series, and additionally point specific attributes are stored for all 11417 * points with individual marker options. If such options are not defined for the point, 11418 * a reference to the series wide attributes is stored in point.pointAttr. 11419 */ 11420 getAttribs: function () { 11421 var series = this, 11422 normalOptions = defaultPlotOptions[series.type].marker ? series.options.marker : series.options, 11423 stateOptions = normalOptions.states, 11424 stateOptionsHover = stateOptions[HOVER_STATE], 11425 pointStateOptionsHover, 11426 seriesColor = series.color, 11427 normalDefaults = { 11428 stroke: seriesColor, 11429 fill: seriesColor 11430 }, 11431 points = series.points, 11432 i, 11433 point, 11434 seriesPointAttr = [], 11435 pointAttr, 11436 pointAttrToOptions = series.pointAttrToOptions, 11437 hasPointSpecificOptions, 11438 key; 11439 11440 // series type specific modifications 11441 if (series.options.marker) { // line, spline, area, areaspline, scatter 11442 11443 // if no hover radius is given, default to normal radius + 2 11444 stateOptionsHover.radius = stateOptionsHover.radius || normalOptions.radius + 2; 11445 stateOptionsHover.lineWidth = stateOptionsHover.lineWidth || normalOptions.lineWidth + 1; 11446 11447 } else { // column, bar, pie 11448 11449 // if no hover color is given, brighten the normal color 11450 stateOptionsHover.color = stateOptionsHover.color || 11451 Color(stateOptionsHover.color || seriesColor) 11452 .brighten(stateOptionsHover.brightness).get(); 11453 } 11454 11455 // general point attributes for the series normal state 11456 seriesPointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, normalDefaults); 11457 11458 // HOVER_STATE and SELECT_STATE states inherit from normal state except the default radius 11459 each([HOVER_STATE, SELECT_STATE], function (state) { 11460 seriesPointAttr[state] = 11461 series.convertAttribs(stateOptions[state], seriesPointAttr[NORMAL_STATE]); 11462 }); 11463 11464 // set it 11465 series.pointAttr = seriesPointAttr; 11466 11467 11468 // Generate the point-specific attribute collections if specific point 11469 // options are given. If not, create a referance to the series wide point 11470 // attributes 11471 i = points.length; 11472 while (i--) { 11473 point = points[i]; 11474 normalOptions = (point.options && point.options.marker) || point.options; 11475 if (normalOptions && normalOptions.enabled === false) { 11476 normalOptions.radius = 0; 11477 } 11478 hasPointSpecificOptions = false; 11479 11480 // check if the point has specific visual options 11481 if (point.options) { 11482 for (key in pointAttrToOptions) { 11483 if (defined(normalOptions[pointAttrToOptions[key]])) { 11484 hasPointSpecificOptions = true; 11485 } 11486 } 11487 } 11488 11489 11490 11491 // a specific marker config object is defined for the individual point: 11492 // create it's own attribute collection 11493 if (hasPointSpecificOptions) { 11494 11495 pointAttr = []; 11496 stateOptions = normalOptions.states || {}; // reassign for individual point 11497 pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {}; 11498 11499 // if no hover color is given, brighten the normal color 11500 if (!series.options.marker) { // column, bar, point 11501 pointStateOptionsHover.color = 11502 Color(pointStateOptionsHover.color || point.options.color) 11503 .brighten(pointStateOptionsHover.brightness || 11504 stateOptionsHover.brightness).get(); 11505 11506 } 11507 11508 // normal point state inherits series wide normal state 11509 pointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, seriesPointAttr[NORMAL_STATE]); 11510 11511 // inherit from point normal and series hover 11512 pointAttr[HOVER_STATE] = series.convertAttribs( 11513 stateOptions[HOVER_STATE], 11514 seriesPointAttr[HOVER_STATE], 11515 pointAttr[NORMAL_STATE] 11516 ); 11517 // inherit from point normal and series hover 11518 pointAttr[SELECT_STATE] = series.convertAttribs( 11519 stateOptions[SELECT_STATE], 11520 seriesPointAttr[SELECT_STATE], 11521 pointAttr[NORMAL_STATE] 11522 ); 11523 11524 11525 11526 // no marker config object is created: copy a reference to the series-wide 11527 // attribute collection 11528 } else { 11529 pointAttr = seriesPointAttr; 11530 } 11531 11532 point.pointAttr = pointAttr; 11533 11534 } 11535 11536 }, 11537 11538 11539 /** 11540 * Clear DOM objects and free up memory 11541 */ 11542 destroy: function () { 11543 var series = this, 11544 chart = series.chart, 11545 seriesClipRect = series.clipRect,
11546 issue134 = /AppleWebKit\/533/.test(userAgent), 11547 destroy, 11548 i, 11549 data = series.data || [], 11550 point, 11551 prop, 11552 axis; 11553 11554 // add event hook 11555 fireEvent(series, 'destroy'); 11556 11557 // remove all events 11558 removeEvent(series); 11559 11560 // erase from axes 11561 each(['xAxis', 'yAxis'], function (AXIS) { 11562 axis = series[AXIS]; 11563 if (axis) { 11564 erase(axis.series, series); 11565 axis.isDirty = true; 11566 } 11567 }); 11568 11569 // remove legend items 11570 if (series.legendItem) { 11571 series.chart.legend.destroyItem(series); 11572 } 11573 11574 // destroy all points with their elements 11575 i = data.length; 11576 while (i--) { 11577 point = data[i]; 11578 if (point && point.destroy) { 11579 point.destroy(); 11580 } 11581 } 11582 series.points = null; 11583 11584 // If this series clipRect is not the global one (which is removed on chart.destroy) we 11585 // destroy it here. 11586 if (seriesClipRect && seriesClipRect !== chart.clipRect) { 11587 series.clipRect = seriesClipRect.destroy(); 11588 } 11589 11590 // destroy all SVGElements associated to the series 11591 each(['area', 'graph', 'dataLabelsGroup', 'group', 'tracker'], function (prop) { 11592 if (series[prop]) { 11593 11594 // issue 134 workaround 11595 destroy = issue134 && prop === 'group' ? 11596 'hide' : 11597 'destroy'; 11598 11599 series[prop][destroy](); 11600 } 11601 }); 11602 11603 // remove from hoverSeries 11604 if (chart.hoverSeries === series) { 11605 chart.hoverSeries = null; 11606 } 11607 erase(chart.series, series); 11608 11609 // clear all members 11610 for (prop in series) { 11611 delete series[prop]; 11612 } 11613 }, 11614 11615 /** 11616 * Draw the data labels 11617 */ 11618 drawDataLabels: function () { 11619 11620 var series = this, 11621 seriesOptions = series.options, 11622 options = seriesOptions.dataLabels; 11623 11624 if (options.enabled || series._hasPointLabels) { 11625 var x, 11626 y, 11627 points = series.points, 11628 pointOptions, 11629 generalOptions, 11630 str, 11631 dataLabelsGroup = series.dataLabelsGroup, 11632 chart = series.chart, 11633 xAxis = series.xAxis, 11634 groupLeft = xAxis ? xAxis.left : chart.plotLeft, 11635 yAxis = series.yAxis, 11636 groupTop = yAxis ? yAxis.top : chart.plotTop, 11637 renderer = chart.renderer, 11638 inverted = chart.inverted, 11639 seriesType = series.type, 11640 stacking = seriesOptions.stacking, 11641 isBarLike = seriesType === 'column' || seriesType === 'bar', 11642 vAlignIsNull = options.verticalAlign === null, 11643 yIsNull = options.y === null, 11644 fontMetrics = renderer.fontMetrics(options.style.fontSize), // height and baseline 11645 fontLineHeight = fontMetrics.h, 11646 fontBaseline = fontMetrics.b, 11647 dataLabel, 11648 enabled; 11649 11650 if (isBarLike) { 11651 var defaultYs = { 11652 top: fontBaseline, 11653 middle: fontBaseline - fontLineHeight / 2, 11654 bottom: -fontLineHeight + fontBaseline 11655 }; 11656 if (stacking) { 11657 // In stacked series the default label placement is inside the bars 11658 if (vAlignIsNull) { 11659 options = merge(options, {verticalAlign: 'middle'}); 11660 } 11661 11662 // If no y delta is specified, try to create a good default 11663 if (yIsNull) { 11664 options = merge(options, { y: defaultYs[options.verticalAlign]}); 11665 } 11666 } else { 11667 // In non stacked series the default label placement is on top of the bars 11668 if (vAlignIsNull) { 11669 options = merge(options, {verticalAlign: 'top'}); 11670 11671 // If no y delta is specified, try to create a good default (like default bar) 11672 } else if (yIsNull) { 11673 options = merge(options, { y: defaultYs[options.verticalAlign]}); 11674 } 11675 11676 } 11677 } 11678 11679 11680 // create a separate group for the data labels to avoid rotation 11681 if (!dataLabelsGroup) { 11682 dataLabelsGroup = series.dataLabelsGroup = 11683 renderer.g('data-labels') 11684 .attr({ 11685 visibility: series.visible ? VISIBLE : HIDDEN, 11686 zIndex: 6 11687 }) 11688 .translate(groupLeft, groupTop) 11689 .add(); 11690 } else { 11691 dataLabelsGroup.translate(groupLeft, groupTop); 11692 } 11693 11694 // make the labels for each point 11695 generalOptions = options; 11696 each(points, function (point) { 11697 11698 dataLabel = point.dataLabel; 11699 11700 // Merge in individual options from point 11701 options = generalOptions; // reset changes from previous points 11702 pointOptions = point.options; 11703 if (pointOptions && pointOptions.dataLabels) { 11704 options = merge(options, pointOptions.dataLabels); 11705 } 11706 enabled = options.enabled; 11707 11708 // Get the positions 11709 if (enabled) { 11710 var plotX = (point.barX && point.barX + point.barW / 2) || pick(point.plotX, -999), 11711 plotY = pick(point.plotY, -999), 11712 11713 // if options.y is null, which happens by default on column charts, set the position 11714 // above or below the column depending on the threshold 11715 individualYDelta = options.y === null ? 11716 (point.y >= seriesOptions.threshold ? 11717 -fontLineHeight + fontBaseline : // below the threshold 11718 fontBaseline) : // above the threshold 11719 options.y; 11720 11721 x = (inverted ? chart.plotWidth - plotY : plotX) + options.x; 11722 y = mathRound((inverted ? chart.plotHeight - plotX : plotY) + individualYDelta); 11723 11724 } 11725 11726 // If the point is outside the plot area, destroy it. #678, #820 11727 if (dataLabel && series.isCartesian && (!chart.isInsidePlot(x, y) || !enabled)) { 11728 point.dataLabel = dataLabel.destroy(); 11729 11730 // Individual labels are disabled if the are explicitly disabled 11731 // in the point options, or if they fall outside the plot area. 11732 } else if (enabled) { 11733 11734 var align = options.align; 11735 11736 // Get the string 11737 str = options.formatter.call(point.getLabelConfig(), options); 11738 11739 // in columns, align the string to the column 11740 if (seriesType === 'column') { 11741 x += { left: -1, right: 1 }[align] * point.barW / 2 || 0; 11742 } 11743 11744 if (!stacking && inverted && point.y < 0) { 11745 align = 'right'; 11746 x -= 10; 11747 } 11748 11749 // Determine the color 11750 options.style.color = pick(options.color, options.style.color, series.color, 'black'); 11751 11752 11753 // update existing label 11754 if (dataLabel) { 11755 // vertically centered 11756 dataLabel 11757 .attr({ 11758 text: str 11759 }).animate({ 11760 x: x, 11761 y: y 11762 }); 11763 // create new label 11764 } else if (defined(str)) { 11765 dataLabel = point.dataLabel = renderer[options.rotation ? 'text' : 'label']( // labels don't support rotation 11766 str, 11767 x, 11768 y, 11769 null, 11770 null, 11771 null, 11772 options.useHTML, 11773 true // baseline for backwards compat 11774 ) 11775 .attr({ 11776 align: align, 11777 fill: options.backgroundColor, 11778 stroke: options.borderColor, 11779 'stroke-width': options.borderWidth, 11780 r: options.borderRadius, 11781 rotation: options.rotation, 11782 padding: options.padding, 11783 zIndex: 1 11784 }) 11785 .css(options.style) 11786 .add(dataLabelsGroup) 11787 .shadow(options.shadow); 11788 } 11789 11790 if (isBarLike && seriesOptions.stacking && dataLabel) { 11791 var barX = point.barX, 11792 barY = point.barY, 11793 barW = point.barW, 11794 barH = point.barH; 11795 11796 dataLabel.align(options, null, 11797 { 11798 x: inverted ? chart.plotWidth - barY - barH : barX, 11799 y: inverted ? chart.plotHeight - barX - barW : barY, 11800 width: inverted ? barH : barW, 11801 height: inverted ? barW : barH 11802 }); 11803 } 11804 11805 11806 } 11807 }); 11808 } 11809 }, 11810 11811 /** 11812 * Draw the actual graph 11813 */ 11814 drawGraph: function () { 11815 var series = this, 11816 options = series.options, 11817 chart = series.chart, 11818 graph = series.graph, 11819 graphPath = [], 11820 fillColor, 11821 area = series.area,
11822 group = series.group, 11823 color = options.lineColor || series.color, 11824 lineWidth = options.lineWidth, 11825 dashStyle = options.dashStyle, 11826 segmentPath, 11827 renderer = chart.renderer, 11828 translatedThreshold = series.yAxis.getThreshold(options.threshold), 11829 useArea = /^area/.test(series.type), 11830 singlePoints = [], // used in drawTracker 11831 areaPath = [], 11832 attribs; 11833 11834 11835 // divide into segments and build graph and area paths 11836 each(series.segments, function (segment) { 11837 segmentPath = []; 11838 11839 // build the segment line 11840 each(segment, function (point, i) { 11841 11842 if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object 11843 segmentPath.push.apply(segmentPath, series.getPointSpline(segment, point, i)); 11844 11845 } else { 11846 11847 // moveTo or lineTo 11848 segmentPath.push(i ? L : M); 11849 11850 // step line? 11851 if (i && options.step) { 11852 var lastPoint = segment[i - 1]; 11853 segmentPath.push( 11854 point.plotX, 11855 lastPoint.plotY 11856 ); 11857 } 11858 11859 // normal line to next point 11860 segmentPath.push( 11861 point.plotX, 11862 point.plotY 11863 ); 11864 } 11865 }); 11866 11867 // add the segment to the graph, or a single point for tracking 11868 if (segment.length > 1) { 11869 graphPath = graphPath.concat(segmentPath); 11870 } else { 11871 singlePoints.push(segment[0]); 11872 } 11873 11874 // build the area 11875 if (useArea) { 11876 var areaSegmentPath = [], 11877 i, 11878 segLength = segmentPath.length; 11879 for (i = 0; i < segLength; i++) { 11880 areaSegmentPath.push(segmentPath[i]); 11881 } 11882 if (segLength === 3) { // for animation from 1 to two points 11883 areaSegmentPath.push(L, segmentPath[1], segmentPath[2]); 11884 } 11885 if (options.stacking && series.type !== 'areaspline') { 11886 11887 // Follow stack back. Todo: implement areaspline. A general solution could be to 11888 // reverse the entire graphPath of the previous series, though may be hard with 11889 // splines and with series with different extremes 11890 for (i = segment.length - 1; i >= 0; i--) { 11891 11892 // step line? 11893 if (i < segment.length - 1 && options.step) { 11894 areaSegmentPath.push(segment[i + 1].plotX, segment[i].yBottom); 11895 } 11896 11897 areaSegmentPath.push(segment[i].plotX, segment[i].yBottom); 11898 } 11899 11900 } else { // follow zero line back 11901 areaSegmentPath.push( 11902 L, 11903 segment[segment.length - 1].plotX, 11904 translatedThreshold, 11905 L, 11906 segment[0].plotX, 11907 translatedThreshold 11908 ); 11909 } 11910 areaPath = areaPath.concat(areaSegmentPath); 11911 } 11912 }); 11913 11914 // used in drawTracker: 11915 series.graphPath = graphPath; 11916 series.singlePoints = singlePoints; 11917 11918 // draw the area if area series or areaspline 11919 if (useArea) { 11920 fillColor = pick( 11921 options.fillColor, 11922 Color(series.color).setOpacity(options.fillOpacity || 0.75).get() 11923 ); 11924 if (area) { 11925 area.animate({ d: areaPath }); 11926 11927 } else { 11928 // draw the area 11929 series.area = series.chart.renderer.path(areaPath) 11930 .attr({ 11931 fill: fillColor 11932 }).add(group); 11933 } 11934 } 11935 11936 // draw the graph 11937 if (graph) { 11938 stop(graph); // cancel running animations, #459 11939 graph.animate({ d: graphPath }); 11940 11941 } else { 11942 if (lineWidth) { 11943 attribs = { 11944 'stroke': color, 11945 'stroke-width': lineWidth 11946 }; 11947 if (dashStyle) { 11948 attribs.dashstyle = dashStyle; 11949 } 11950 11951 series.graph = renderer.path(graphPath) 11952 .attr(attribs).add(group).shadow(options.shadow); 11953 } 11954 } 11955 }, 11956 11957 /** 11958 * Initialize and perform group inversion on series.group and series.trackerGroup 11959 */ 11960 invertGroups: function () { 11961 var series = this, 11962 group = series.group, 11963 trackerGroup = series.trackerGroup, 11964 chart = series.chart; 11965 11966 // A fixed size is needed for inversion to work 11967 function setInvert() { 11968 var size = { 11969 width: series.yAxis.len, 11970 height: series.xAxis.len 11971 }; 11972 11973 // Set the series.group size 11974 group.attr(size).invert(); 11975 11976 // Set the tracker group size 11977 if (trackerGroup) { 11978 trackerGroup.attr(size).invert(); 11979 } 11980 } 11981 11982 addEvent(chart, 'resize', setInvert); // do it on resize 11983 addEvent(series, 'destroy', function () { 11984 removeEvent(chart, 'resize', setInvert); 11985 }); 11986 11987 // Do it now 11988 setInvert(); // do it now 11989 11990 // On subsequent render and redraw, just do setInvert without setting up events again 11991 series.invertGroups = setInvert; 11992 }, 11993 11994 /** 11995 * Render the graph and markers 11996 */ 11997 render: function () { 11998 var series = this, 11999 chart = series.chart, 12000 group, 12001 options = series.options, 12002 doClip = options.clip !== false, 12003 animation = options.animation, 12004 doAnimation = animation && series.animate, 12005 duration = doAnimation ? (animation && animation.duration) || 500 : 0, 12006 clipRect = series.clipRect, 12007 renderer = chart.renderer; 12008 12009 12010 // Add plot area clipping rectangle. If this is before chart.hasRendered, 12011 // create one shared clipRect. 12012 12013 // Todo: since creating the clip property, the clipRect is created but
12014 // never used when clip is false. A better way would be that the animation 12015 // would run, then the clipRect destroyed. 12016 if (!clipRect) { 12017 clipRect = series.clipRect = !chart.hasRendered && chart.clipRect ? 12018 chart.clipRect : 12019 renderer.clipRect(0, 0, chart.plotSizeX, chart.plotSizeY + 1); 12020 if (!chart.clipRect) { 12021 chart.clipRect = clipRect; 12022 } 12023 } 12024 12025 12026 // the group 12027 if (!series.group) { 12028 group = series.group = renderer.g('series'); 12029 12030 group.attr({ 12031 visibility: series.visible ? VISIBLE : HIDDEN, 12032 zIndex: options.zIndex 12033 }) 12034 .translate(series.xAxis.left, series.yAxis.top) 12035 .add(chart.seriesGroup); 12036 } 12037 12038 series.drawDataLabels(); 12039 12040 // initiate the animation 12041 if (doAnimation) { 12042 series.animate(true); 12043 } 12044 12045 // cache attributes for shapes 12046 series.getAttribs(); 12047 12048 // draw the graph if any 12049 if (series.drawGraph) { 12050 series.drawGraph(); 12051 } 12052 12053 // draw the points 12054 series.drawPoints(); 12055 12056 // draw the mouse tracking area 12057 if (series.options.enableMouseTracking !== false) { 12058 series.drawTracker(); 12059 } 12060 12061 // Handle inverted series and tracker groups 12062 if (chart.inverted) { 12063 series.invertGroups(); 12064 } 12065 12066 // Do the initial clipping. This must be done after inverting for VML. 12067 if (doClip && !series.hasRendered) { 12068 group.clip(clipRect); 12069 if (series.trackerGroup) { 12070 series.trackerGroup.clip(chart.clipRect); 12071 } 12072 } 12073 12074 12075 // run the animation 12076 if (doAnimation) { 12077 series.animate(); 12078 } 12079 12080 // finish the individual clipRect 12081 setTimeout(function () { 12082 clipRect.isAnimating = false; 12083 group = series.group; // can be destroyed during the timeout 12084 if (group && clipRect !== chart.clipRect && clipRect.renderer) { 12085 if (doClip) { 12086 group.clip((series.clipRect = chart.clipRect)); 12087 } 12088 clipRect.destroy(); 12089 } 12090 }, duration); 12091 12092 series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see 12093 // (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see 12094 series.hasRendered = true; 12095 }, 12096 12097 /** 12098 * Redraw the series after an update in the axes. 12099 */ 12100 redraw: function () { 12101 var series = this, 12102 chart = series.chart, 12103 wasDirtyData = series.isDirtyData, // cache it here as it is set to false in render, but used after 12104 group = series.group; 12105 12106 // reposition on resize 12107 if (group) { 12108 if (chart.inverted) { 12109 group.attr({ 12110 width: chart.plotWidth, 12111 height: chart.plotHeight 12112 }); 12113 } 12114 12115 group.animate({ 12116 translateX: series.xAxis.left, 12117 translateY: series.yAxis.top 12118 }); 12119 } 12120 12121 series.translate(); 12122 series.setTooltipPoints(true); 12123 12124 series.render(); 12125 if (wasDirtyData) { 12126 fireEvent(series, 'updatedData'); 12127 } 12128 }, 12129 12130 /** 12131 * Set the state of the graph 12132 */ 12133 setState: function (state) { 12134 var series = this, 12135 options = series.options, 12136 graph = series.graph, 12137 stateOptions = options.states, 12138 lineWidth = options.lineWidth; 12139 12140 state = state || NORMAL_STATE; 12141 12142 if (series.state !== state) { 12143 series.state = state; 12144 12145 if (stateOptions[state] && stateOptions[state].enabled === false) { 12146 return; 12147 } 12148 12149 if (state) { 12150 lineWidth = stateOptions[state].lineWidth || lineWidth + 1; 12151 } 12152 12153 if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML 12154 graph.attr({ // use attr because animate will cause any other animation on the graph to stop 12155 'stroke-width': lineWidth 12156 }, state ? 0 : 500); 12157 } 12158 } 12159 }, 12160 12161 /** 12162 * Set the visibility of the graph 12163 * 12164 * @param vis {Boolean} True to show the series, false to hide. If UNDEFINED, 12165 * the visibility is toggled. 12166 */ 12167 setVisible: function (vis, redraw) { 12168 var series = this, 12169 chart = series.chart, 12170 legendItem = series.legendItem, 12171 seriesGroup = series.group, 12172 seriesTracker = series.tracker, 12173 dataLabelsGroup = series.dataLabelsGroup, 12174 showOrHide, 12175 i, 12176 points = series.points, 12177 point, 12178 ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries, 12179 oldVisibility = series.visible; 12180 12181 // if called without an argument, toggle visibility 12182 series.visible = vis = vis === UNDEFINED ? !oldVisibility : vis; 12183 showOrHide = vis ? 'show' : 'hide'; 12184 12185 // show or hide series 12186 if (seriesGroup) { // pies don't have one 12187 seriesGroup[showOrHide](); 12188 } 12189 12190 // show or hide trackers 12191 if (seriesTracker) {
12192 seriesTracker[showOrHide](); 12193 } else if (points) { 12194 i = points.length; 12195 while (i--) { 12196 point = points[i]; 12197 if (point.tracker) { 12198 point.tracker[showOrHide](); 12199 } 12200 } 12201 } 12202 12203 12204 if (dataLabelsGroup) { 12205 dataLabelsGroup[showOrHide](); 12206 } 12207 12208 if (legendItem) { 12209 chart.legend.colorizeItem(series, vis); 12210 } 12211 12212 12213 // rescale or adapt to resized chart 12214 series.isDirty = true; 12215 // in a stack, all other series are affected 12216 if (series.options.stacking) { 12217 each(chart.series, function (otherSeries) { 12218 if (otherSeries.options.stacking && otherSeries.visible) { 12219 otherSeries.isDirty = true; 12220 } 12221 }); 12222 } 12223 12224 if (ignoreHiddenSeries) { 12225 chart.isDirtyBox = true; 12226 } 12227 if (redraw !== false) { 12228 chart.redraw(); 12229 } 12230 12231 fireEvent(series, showOrHide); 12232 }, 12233 12234 /** 12235 * Show the graph 12236 */ 12237 show: function () { 12238 this.setVisible(true); 12239 }, 12240 12241 /** 12242 * Hide the graph 12243 */ 12244 hide: function () { 12245 this.setVisible(false); 12246 }, 12247 12248 12249 /** 12250 * Set the selected state of the graph 12251 * 12252 * @param selected {Boolean} True to select the series, false to unselect. If 12253 * UNDEFINED, the selection state is toggled. 12254 */ 12255 select: function (selected) { 12256 var series = this; 12257 // if called without an argument, toggle 12258 series.selected = selected = (selected === UNDEFINED) ? !series.selected : selected; 12259 12260 if (series.checkbox) { 12261 series.checkbox.checked = selected; 12262 } 12263 12264 fireEvent(series, selected ? 'select' : 'unselect'); 12265 }, 12266 12267 /** 12268 * Create a group that holds the tracking object or objects. This allows for 12269 * individual clipping and placement of each series tracker. 12270 */ 12271 drawTrackerGroup: function () { 12272 var trackerGroup = this.trackerGroup, 12273 chart = this.chart; 12274 12275 if (this.isCartesian) { 12276 12277 // Generate it on first call 12278 if (!trackerGroup) { 12279 this.trackerGroup = trackerGroup = chart.renderer.g() 12280 .attr({ 12281 zIndex: this.options.zIndex || 1 12282 }) 12283 .add(chart.trackerGroup); 12284 12285 } 12286 // Place it on first and subsequent (redraw) calls 12287 trackerGroup.translate(this.xAxis.left, this.yAxis.top); 12288 12289 } 12290 12291 return trackerGroup; 12292 }, 12293 12294 /** 12295 * Draw the tracker object that sits above all data labels and markers to 12296 * track mouse events on the graph or points. For the line type charts 12297 * the tracker uses the same graphPath, but with a greater stroke width 12298 * for better control. 12299 */ 12300 drawTracker: function () { 12301 var series = this, 12302 options = series.options, 12303 trackerPath = [].concat(series.graphPath), 12304 trackerPathLength = trackerPath.length, 12305 chart = series.chart, 12306 renderer = chart.renderer, 12307 snap = chart.options.tooltip.snap, 12308 tracker = series.tracker, 12309 cursor = options.cursor, 12310 css = cursor && { cursor: cursor }, 12311 singlePoints = series.singlePoints, 12312 trackerGroup = series.drawTrackerGroup(), 12313 singlePoint, 12314 i; 12315 12316 // Extend end points. A better way would be to use round linecaps, 12317 // but those are not clickable in VML. 12318 if (trackerPathLength) { 12319 i = trackerPathLength + 1; 12320 while (i--) { 12321 if (trackerPath[i] === M) { // extend left side 12322 trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], L); 12323 } 12324 if ((i && trackerPath[i] === M) || i === trackerPathLength) { // extend right side 12325 trackerPath.splice(i, 0, L, trackerPath[i - 2] + snap, trackerPath[i - 1]); 12326 } 12327 } 12328 } 12329 12330 // handle single points 12331 for (i = 0; i < singlePoints.length; i++) { 12332 singlePoint = singlePoints[i]; 12333 trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY, 12334 L, singlePoint.plotX + snap, singlePoint.plotY); 12335 } 12336 12337 12338 12339 // draw the tracker 12340 if (tracker) { 12341 tracker.attr({ d: trackerPath }); 12342 12343 } else { // create 12344 12345 series.tracker = renderer.path(trackerPath) 12346 .attr({ 12347 isTracker: true, 12348 stroke: TRACKER_FILL, 12349 fill: NONE, 12350 'stroke-linejoin': 'bevel', 12351 'stroke-width' : options.lineWidth + 2 * snap, 12352 visibility: series.visible ? VISIBLE : HIDDEN 12353 }) 12354 .on(hasTouch ? 'touchstart' : 'mouseover', function () { 12355 if (chart.hoverSeries !== series) { 12356 series.onMouseOver(); 12357 } 12358 }) 12359 .on('mouseout', function () { 12360 if (!options.stickyTracking) { 12361 series.onMouseOut(); 12362 } 12363 }) 12364 .css(css) 12365 .add(trackerGroup); 12366 } 12367 12368 } 12369 12370}; // end Series prototype 12371 12372 12373/** 12374 * LineSeries object 12375 */
12376var LineSeries = extendClass(Series); 12377seriesTypes.line = LineSeries; 12378 12379/** 12380 * AreaSeries object 12381 */ 12382var AreaSeries = extendClass(Series, { 12383 type: 'area' 12384}); 12385seriesTypes.area = AreaSeries; 12386 12387 12388 12389 12390/** 12391 * SplineSeries object 12392 */ 12393var SplineSeries = extendClass(Series, { 12394 type: 'spline', 12395 12396 /** 12397 * Draw the actual graph 12398 */ 12399 getPointSpline: function (segment, point, i) { 12400 var smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc 12401 denom = smoothing + 1, 12402 plotX = point.plotX, 12403 plotY = point.plotY, 12404 lastPoint = segment[i - 1], 12405 nextPoint = segment[i + 1], 12406 leftContX, 12407 leftContY, 12408 rightContX, 12409 rightContY, 12410 ret; 12411 12412 // find control points 12413 if (i && i < segment.length - 1) { 12414 var lastX = lastPoint.plotX, 12415 lastY = lastPoint.plotY, 12416 nextX = nextPoint.plotX, 12417 nextY = nextPoint.plotY, 12418 correction; 12419 12420 leftContX = (smoothing * plotX + lastX) / denom; 12421 leftContY = (smoothing * plotY + lastY) / denom; 12422 rightContX = (smoothing * plotX + nextX) / denom; 12423 rightContY = (smoothing * plotY + nextY) / denom; 12424 12425 // have the two control points make a straight line through main point 12426 correction = ((rightContY - leftContY) * (rightContX - plotX)) / 12427 (rightContX - leftContX) + plotY - rightContY; 12428 12429 leftContY += correction; 12430 rightContY += correction; 12431 12432 // to prevent false extremes, check that control points are between 12433 // neighbouring points' y values 12434 if (leftContY > lastY && leftContY > plotY) { 12435 leftContY = mathMax(lastY, plotY); 12436 rightContY = 2 * plotY - leftContY; // mirror of left control point 12437 } else if (leftContY < lastY && leftContY < plotY) { 12438 leftContY = mathMin(lastY, plotY); 12439 rightContY = 2 * plotY - leftContY; 12440 } 12441 if (rightContY > nextY && rightContY > plotY) { 12442 rightContY = mathMax(nextY, plotY); 12443 leftContY = 2 * plotY - rightContY; 12444 } else if (rightContY < nextY && rightContY < plotY) { 12445 rightContY = mathMin(nextY, plotY); 12446 leftContY = 2 * plotY - rightContY; 12447 } 12448 12449 // record for drawing in next point 12450 point.rightContX = rightContX; 12451 point.rightContY = rightContY; 12452 12453 } 12454 12455 // moveTo or lineTo 12456 if (!i) { 12457 ret = [M, plotX, plotY]; 12458 } else { // curve from last point to this 12459 ret = [ 12460 'C', 12461 lastPoint.rightContX || lastPoint.plotX, 12462 lastPoint.rightContY || lastPoint.plotY, 12463 leftContX || plotX, 12464 leftContY || plotY, 12465 plotX, 12466 plotY 12467 ]; 12468 lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later 12469 } 12470 return ret; 12471 } 12472}); 12473seriesTypes.spline = SplineSeries; 12474 12475 12476 12477/** 12478 * AreaSplineSeries object 12479 */ 12480var AreaSplineSeries = extendClass(SplineSeries, { 12481 type: 'areaspline' 12482}); 12483seriesTypes.areaspline = AreaSplineSeries; 12484 12485/** 12486 * ColumnSeries object 12487 */ 12488var ColumnSeries = extendClass(Series, { 12489 type: 'column', 12490 tooltipOutsidePlot: true, 12491 pointAttrToOptions: { // mapping between SVG attributes and the corresponding options 12492 stroke: 'borderColor', 12493 'stroke-width': 'borderWidth', 12494 fill: 'color', 12495 r: 'borderRadius' 12496 }, 12497 init: function () { 12498 Series.prototype.init.apply(this, arguments); 12499 12500 var series = this, 12501 chart = series.chart; 12502 12503 // if the series is added dynamically, force redraw of other 12504 // series affected by a new column 12505 if (chart.hasRendered) { 12506 each(chart.series, function (otherSeries) { 12507 if (otherSeries.type === series.type) { 12508 otherSeries.isDirty = true; 12509 } 12510 }); 12511 } 12512 }, 12513 12514 /** 12515 * Translate each point to the plot area coordinate system and find shape positions 12516 */ 12517 translate: function () { 12518 var series = this, 12519 chart = series.chart, 12520 options = series.options, 12521 stacking = options.stacking, 12522 borderWidth = options.borderWidth,
12523 columnCount = 0, 12524 xAxis = series.xAxis, 12525 reversedXAxis = xAxis.reversed, 12526 stackGroups = {}, 12527 stackKey, 12528 columnIndex; 12529 12530 Series.prototype.translate.apply(series); 12531 12532 // Get the total number of column type series. 12533 // This is called on every series. Consider moving this logic to a 12534 // chart.orderStacks() function and call it on init, addSeries and removeSeries 12535 each(chart.series, function (otherSeries) { 12536 if (otherSeries.type === series.type && otherSeries.visible && 12537 series.options.group === otherSeries.options.group) { // used in Stock charts navigator series 12538 if (otherSeries.options.stacking) { 12539 stackKey = otherSeries.stackKey; 12540 if (stackGroups[stackKey] === UNDEFINED) { 12541 stackGroups[stackKey] = columnCount++; 12542 } 12543 columnIndex = stackGroups[stackKey]; 12544 } else { 12545 columnIndex = columnCount++; 12546 } 12547 otherSeries.columnIndex = columnIndex; 12548 } 12549 }); 12550 12551 // calculate the width and position of each column based on 12552 // the number of column series in the plot, the groupPadding 12553 // and the pointPadding options 12554 var points = series.points, 12555 categoryWidth = mathAbs(xAxis.translationSlope) * (xAxis.ordinalSlope || xAxis.closestPointRange || 1), 12556 groupPadding = categoryWidth * options.groupPadding, 12557 groupWidth = categoryWidth - 2 * groupPadding, 12558 pointOffsetWidth = groupWidth / columnCount, 12559 optionPointWidth = options.pointWidth, 12560 pointPadding = defined(optionPointWidth) ? (pointOffsetWidth - optionPointWidth) / 2 : 12561 pointOffsetWidth * options.pointPadding, 12562 pointWidth = mathCeil(mathMax(pick(optionPointWidth, pointOffsetWidth - 2 * pointPadding), 1 + 2 * borderWidth)), 12563 colIndex = (reversedXAxis ? columnCount - 12564 series.columnIndex : series.columnIndex) || 0, 12565 pointXOffset = pointPadding + (groupPadding + colIndex * 12566 pointOffsetWidth - (categoryWidth / 2)) * 12567 (reversedXAxis ? -1 : 1), 12568 threshold = options.threshold, 12569 translatedThreshold = series.yAxis.getThreshold(threshold), 12570 minPointLength = pick(options.minPointLength, 5); 12571 12572 // record the new values 12573 each(points, function (point) { 12574 var plotY = point.plotY, 12575 yBottom = pick(point.yBottom, translatedThreshold), 12576 barX = point.plotX + pointXOffset, 12577 barY = mathCeil(mathMin(plotY, yBottom)), 12578 barH = mathCeil(mathMax(plotY, yBottom) - barY), 12579 stack = series.yAxis.stacks[(point.y < 0 ? '-' : '') + series.stackKey], 12580 shapeArgs; 12581 12582 // Record the offset'ed position and width of the bar to be able to align the stacking total correctly 12583 if (stacking && series.visible && stack && stack[point.x]) { 12584 stack[point.x].setOffset(pointXOffset, pointWidth); 12585 } 12586 12587 // handle options.minPointLength 12588 if (mathAbs(barH) < minPointLength) { 12589 if (minPointLength) { 12590 barH = minPointLength; 12591 barY = 12592 mathAbs(barY - translatedThreshold) > minPointLength ? // stacked 12593 yBottom - minPointLength : // keep position 12594 translatedThreshold - (plotY <= translatedThreshold ? minPointLength : 0); 12595 } 12596 } 12597 12598 extend(point, { 12599 barX: barX, 12600 barY: barY, 12601 barW: pointWidth, 12602 barH: barH 12603 }); 12604 12605 // create shape type and shape args that are reused in drawPoints and drawTracker 12606 point.shapeType = 'rect'; 12607 shapeArgs = { 12608 x: barX, 12609 y: barY, 12610 width: pointWidth, 12611 height: barH, 12612 r: options.borderRadius, 12613 strokeWidth: borderWidth 12614 }; 12615 12616 if (borderWidth % 2) { // correct for shorting in crisp method, visible in stacked columns with 1px border 12617 shapeArgs.y -= 1; 12618 shapeArgs.height += 1; 12619 } 12620 point.shapeArgs = shapeArgs; 12621 12622 // make small columns responsive to mouse 12623 point.trackerArgs = mathAbs(barH) < 3 && merge(point.shapeArgs, { 12624 height: 6, 12625 y: barY - 3 12626 }); 12627 }); 12628 12629 }, 12630 12631 getSymbol: function () { 12632 }, 12633 12634 /** 12635 * Columns have no graph 12636 */ 12637 drawGraph: function () {}, 12638 12639 /** 12640 * Draw the columns. For bars, the series.group is rotated, so the same coordinates 12641 * apply for columns and bars. This method is inherited by scatter series. 12642 * 12643 */ 12644 drawPoints: function () { 12645 var series = this, 12646 options = series.options, 12647 renderer = series.chart.renderer, 12648 graphic, 12649 shapeArgs; 12650 12651 12652 // draw the columns 12653 each(series.points, function (point) { 12654 var plotY = point.plotY; 12655 if (plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) { 12656 graphic = point.graphic; 12657 shapeArgs = point.shapeArgs; 12658 if (graphic) { // update 12659 stop(graphic); 12660 graphic.animate(renderer.Element.prototype.crisp.apply({}, [ 12661 shapeArgs.strokeWidth, 12662 shapeArgs.x, 12663 shapeArgs.y, 12664 shapeArgs.width, 12665 shapeArgs.height 12666 ])); 12667 12668 } else { 12669 point.graphic = graphic = renderer[point.shapeType](shapeArgs) 12670 .attr(point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]) 12671 .add(series.group) 12672 .shadow(options.shadow); 12673 12674 } 12675 12676 } 12677 }); 12678 }, 12679 /** 12680 * Draw the individual tracker elements. 12681 * This method is inherited by scatter and pie charts too. 12682 */ 12683 drawTracker: function () { 12684 var series = this, 12685 chart = series.chart, 12686 renderer = chart.renderer, 12687 shapeArgs, 12688 tracker, 12689 trackerLabel = +new Date(), 12690 options = series.options, 12691 cursor = options.cursor,
12692 css = cursor && { cursor: cursor }, 12693 trackerGroup = series.drawTrackerGroup(), 12694 rel; 12695 12696 each(series.points, function (point) { 12697 tracker = point.tracker; 12698 shapeArgs = point.trackerArgs || point.shapeArgs; 12699 delete shapeArgs.strokeWidth; 12700 if (point.y !== null) { 12701 if (tracker) {// update 12702 tracker.attr(shapeArgs); 12703 12704 } else { 12705 point.tracker = 12706 renderer[point.shapeType](shapeArgs) 12707 .attr({ 12708 isTracker: trackerLabel, 12709 fill: TRACKER_FILL, 12710 visibility: series.visible ? VISIBLE : HIDDEN 12711 }) 12712 .on(hasTouch ? 'touchstart' : 'mouseover', function (event) { 12713 rel = event.relatedTarget || event.fromElement; 12714 if (chart.hoverSeries !== series && attr(rel, 'isTracker') !== trackerLabel) { 12715 series.onMouseOver(); 12716 } 12717 point.onMouseOver(); 12718 12719 }) 12720 .on('mouseout', function (event) { 12721 if (!options.stickyTracking) { 12722 rel = event.relatedTarget || event.toElement; 12723 if (attr(rel, 'isTracker') !== trackerLabel) { 12724 series.onMouseOut(); 12725 } 12726 } 12727 }) 12728 .css(css) 12729 .add(point.group || trackerGroup); // pies have point group - see issue #118 12730 } 12731 } 12732 }); 12733 }, 12734 12735 12736 /** 12737 * Animate the column heights one by one from zero 12738 * @param {Boolean} init Whether to initialize the animation or run it 12739 */ 12740 animate: function (init) { 12741 var series = this, 12742 points = series.points, 12743 options = series.options; 12744 12745 if (!init) { // run the animation 12746 /* 12747 * Note: Ideally the animation should be initialized by calling 12748 * series.group.hide(), and then calling series.group.show() 12749 * after the animation was started. But this rendered the shadows 12750 * invisible in IE8 standards mode. If the columns flicker on large 12751 * datasets, this is the cause. 12752 */ 12753 12754 each(points, function (point) { 12755 var graphic = point.graphic, 12756 shapeArgs = point.shapeArgs, 12757 yAxis = series.yAxis, 12758 threshold = options.threshold; 12759 12760 if (graphic) { 12761 // start values 12762 graphic.attr({ 12763 height: 0, 12764 y: defined(threshold) ? 12765 yAxis.getThreshold(threshold) : 12766 yAxis.translate(yAxis.getExtremes().min, 0, 1, 0, 1) 12767 }); 12768 12769 // animate 12770 graphic.animate({ 12771 height: shapeArgs.height, 12772 y: shapeArgs.y 12773 }, options.animation); 12774 } 12775 }); 12776 12777 12778 // delete this function to allow it only once 12779 series.animate = null; 12780 } 12781 12782 }, 12783 /** 12784 * Remove this series from the chart 12785 */ 12786 remove: function () { 12787 var series = this, 12788 chart = series.chart; 12789 12790 // column and bar series affects other series of the same type 12791 // as they are either stacked or grouped 12792 if (chart.hasRendered) { 12793 each(chart.series, function (otherSeries) { 12794 if (otherSeries.type === series.type) { 12795 otherSeries.isDirty = true; 12796 } 12797 }); 12798 } 12799 12800 Series.prototype.remove.apply(series, arguments); 12801 } 12802}); 12803seriesTypes.column = ColumnSeries; 12804 12805var BarSeries = extendClass(ColumnSeries, { 12806 type: 'bar', 12807 init: function () { 12808 this.inverted = true; 12809 ColumnSeries.prototype.init.apply(this, arguments); 12810 } 12811}); 12812seriesTypes.bar = BarSeries; 12813 12814/** 12815 * The scatter series class 12816 */ 12817var ScatterSeries = extendClass(Series, { 12818 type: 'scatter', 12819 sorted: false, 12820 /** 12821 * Extend the base Series' translate method by adding shape type and 12822 * arguments for the point trackers 12823 */ 12824 translate: function () { 12825 var series = this; 12826 12827 Series.prototype.translate.apply(series); 12828 12829 each(series.points, function (point) { 12830 point.shapeType = 'circle'; 12831 point.shapeArgs = { 12832 x: point.plotX, 12833 y: point.plotY, 12834 r: series.chart.options.tooltip.snap 12835 }; 12836 }); 12837 }, 12838 12839 /** 12840 * Add tracking event listener to the series group, so the point graphics 12841 * themselves act as trackers 12842 */ 12843 drawTracker: function () { 12844 var series = this, 12845 cursor = series.options.cursor, 12846 css = cursor && { cursor: cursor }, 12847 points = series.points, 12848 i = points.length, 12849 graphic; 12850 12851 // Set an expando property for the point index, used below 12852 while (i--) { 12853 graphic = points[i].graphic; 12854 if (graphic) { // doesn't exist for null points 12855 graphic.element._i = i; 12856 } 12857 } 12858 12859 // Add the event listeners, we need to do this only once 12860 if (!series._hasTracking) { 12861 series.group 12862 .attr({ 12863 isTracker: true 12864 }) 12865 .on(hasTouch ? 'touchstart' : 'mouseover', function (e) { 12866 series.onMouseOver(); 12867 if (e.target._i !== UNDEFINED) { // undefined on graph in scatterchart 12868 points[e.target._i].onMouseOver(); 12869 } 12870 }) 12871 .on('mouseout', function () { 12872 if (!series.options.stickyTracking) { 12873 series.onMouseOut(); 12874 } 12875 }) 12876 .css(css); 12877 } else { 12878 series._hasTracking = true; 12879 } 12880 12881 } 12882}); 12883seriesTypes.scatter = ScatterSeries; 12884 12885/** 12886 * Extended point object for pies 12887 */ 12888var PiePoint = extendClass(Point, { 12889 /** 12890 * Initiate the pie slice 12891 */ 12892 init: function () { 12893
12894 Point.prototype.init.apply(this, arguments); 12895 12896 var point = this, 12897 toggleSlice; 12898 12899 //visible: options.visible !== false, 12900 extend(point, { 12901 visible: point.visible !== false, 12902 name: pick(point.name, 'Slice') 12903 }); 12904 12905 // add event listener for select 12906 toggleSlice = function () { 12907 point.slice(); 12908 }; 12909 addEvent(point, 'select', toggleSlice); 12910 addEvent(point, 'unselect', toggleSlice); 12911 12912 return point; 12913 }, 12914 12915 /** 12916 * Toggle the visibility of the pie slice 12917 * @param {Boolean} vis Whether to show the slice or not. If undefined, the 12918 * visibility is toggled 12919 */ 12920 setVisible: function (vis) { 12921 var point = this, 12922 chart = point.series.chart, 12923 tracker = point.tracker, 12924 dataLabel = point.dataLabel, 12925 connector = point.connector, 12926 shadowGroup = point.shadowGroup, 12927 method; 12928 12929 // if called without an argument, toggle visibility 12930 point.visible = vis = vis === UNDEFINED ? !point.visible : vis; 12931 12932 method = vis ? 'show' : 'hide'; 12933 12934 point.group[method](); 12935 if (tracker) { 12936 tracker[method](); 12937 } 12938 if (dataLabel) { 12939 dataLabel[method](); 12940 } 12941 if (connector) { 12942 connector[method](); 12943 } 12944 if (shadowGroup) { 12945 shadowGroup[method](); 12946 } 12947 if (point.legendItem) { 12948 chart.legend.colorizeItem(point, vis); 12949 } 12950 }, 12951 12952 /** 12953 * Set or toggle whether the slice is cut out from the pie 12954 * @param {Boolean} sliced When undefined, the slice state is toggled 12955 * @param {Boolean} redraw Whether to redraw the chart. True by default. 12956 */ 12957 slice: function (sliced, redraw, animation) { 12958 var point = this, 12959 series = point.series, 12960 chart = series.chart, 12961 slicedTranslation = point.slicedTranslation, 12962 translation; 12963 12964 setAnimation(animation, chart); 12965 12966 // redraw is true by default 12967 redraw = pick(redraw, true); 12968 12969 // if called without an argument, toggle 12970 sliced = point.sliced = defined(sliced) ? sliced : !point.sliced; 12971 12972 translation = { 12973 translateX: (sliced ? slicedTranslation[0] : chart.plotLeft), 12974 translateY: (sliced ? slicedTranslation[1] : chart.plotTop) 12975 }; 12976 point.group.animate(translation); 12977 if (point.shadowGroup) { 12978 point.shadowGroup.animate(translation); 12979 } 12980 12981 } 12982}); 12983 12984/** 12985 * The Pie series class 12986 */ 12987var PieSeries = extendClass(Series, { 12988 type: 'pie', 12989 isCartesian: false, 12990 pointClass: PiePoint, 12991 pointAttrToOptions: { // mapping between SVG attributes and the corresponding options 12992 stroke: 'borderColor', 12993 'stroke-width': 'borderWidth', 12994 fill: 'color' 12995 }, 12996 12997 /** 12998 * Pies have one color each point 12999 */ 13000 getColor: function () { 13001 // record first color for use in setData 13002 this.initialColor = this.chart.counters.color; 13003 }, 13004 13005 /** 13006 * Animate the column heights one by one from zero 13007 */ 13008 animate: function () { 13009 var series = this, 13010 points = series.points; 13011 13012 each(points, function (point) { 13013 var graphic = point.graphic, 13014 args = point.shapeArgs, 13015 up = -mathPI / 2; 13016 13017 if (graphic) { 13018 // start values 13019 graphic.attr({ 13020 r: 0, 13021 start: up, 13022 end: up 13023 }); 13024 13025 // animate 13026 graphic.animate({ 13027 r: args.r, 13028 start: args.start, 13029 end: args.end 13030 }, series.options.animation); 13031 } 13032 }); 13033 13034 // delete this function to allow it only once 13035 series.animate = null; 13036 13037 }, 13038 13039 /** 13040 * Extend the basic setData method by running processData and generatePoints immediately, 13041 * in order to access the points from the legend. 13042 */ 13043 setData: function () { 13044 Series.prototype.setData.apply(this, arguments); 13045 this.processData(); 13046 this.generatePoints(); 13047 }, 13048 /** 13049 * Do translation for pie slices 13050 */ 13051 translate: function () { 13052 this.generatePoints(); 13053 13054 var total = 0, 13055 series = this, 13056 cumulative = -0.25, // start at top 13057 precision = 1000, // issue #172 13058 options = series.options, 13059 slicedOffset = options.slicedOffset, 13060 connectorOffset = slicedOffset + options.borderWidth, 13061 positions = options.center.concat([options.size, options.innerSize || 0]), 13062 chart = series.chart, 13063 plotWidth = chart.plotWidth, 13064 plotHeight = chart.plotHeight, 13065 start, 13066 end, 13067 angle, 13068 points = series.points, 13069 circ = 2 * mathPI, 13070 fraction, 13071 smallestSize = mathMin(plotWidth, plotHeight), 13072 isPercent, 13073 radiusX, // the x component of the radius vector for a given point 13074 radiusY, 13075 labelDistance = options.dataLabels.distance; 13076 13077 // get positions - either an integer or a percentage string must be given 13078 positions = map(positions, function (length, i) { 13079 13080 isPercent = /%$/.test(length); 13081 return isPercent ? 13082 // i == 0: centerX, relative to width 13083 // i == 1: centerY, relative to height 13084 // i == 2: size, relative to smallestSize 13085 // i == 4: innerSize, relative to smallestSize 13086 [plotWidth, plotHeight, smallestSize, smallestSize][i] * 13087 pInt(length) / 100 : 13088 length; 13089 }); 13090 13091 // utility for getting the x value from a given y, used for anticollision logic in data labels 13092 series.getX = function (y, left) { 13093 13094 angle = math.asin((y - positions[1]) / (positions[2] / 2 + labelDistance)); 13095 13096 return positions[0] + 13097 (left ? -1 : 1) * 13098 (mathCos(angle) * (positions[2] / 2 + labelDistance)); 13099 }; 13100 13101 // set center for later use 13102 series.center = positions; 13103 13104 // get the total sum 13105 each(points, function (point) { 13106 total += point.y; 13107 }); 13108 13109 each(points, function (point) { 13110 // set start and end angle 13111 fraction = total ? point.y / total : 0; 13112 start = mathRound(cumulative * circ * precision) / precision; 13113 cumulative += fraction; 13114 end = mathRound(cumulative * circ * precision) / precision; 13115 13116 // set the shape 13117 point.shapeType = 'arc'; 13118 point.shapeArgs = { 13119 x: positions[0], 13120 y: positions[1], 13121 r: positions[2] / 2, 13122 innerR: positions[3] / 2, 13123 start: start, 13124 end: end 13125 }; 13126 13127 // center for the sliced out slice 13128 angle = (end + start) / 2; 13129 point.slicedTranslation = map([ 13130 mathCos(angle) * slicedOffset + chart.plotLeft, 13131 mathSin(angle) * slicedOffset + chart.plotTop 13132 ], mathRound); 13133
13134 // set the anchor point for tooltips 13135 radiusX = mathCos(angle) * positions[2] / 2; 13136 radiusY = mathSin(angle) * positions[2] / 2; 13137 point.tooltipPos = [ 13138 positions[0] + radiusX * 0.7, 13139 positions[1] + radiusY * 0.7 13140 ]; 13141 13142 // set the anchor point for data labels 13143 point.labelPos = [ 13144 positions[0] + radiusX + mathCos(angle) * labelDistance, // first break of connector 13145 positions[1] + radiusY + mathSin(angle) * labelDistance, // a/a 13146 positions[0] + radiusX + mathCos(angle) * connectorOffset, // second break, right outside pie 13147 positions[1] + radiusY + mathSin(angle) * connectorOffset, // a/a 13148 positions[0] + radiusX, // landing point for connector 13149 positions[1] + radiusY, // a/a 13150 labelDistance < 0 ? // alignment 13151 'center' : 13152 angle < circ / 4 ? 'left' : 'right', // alignment 13153 angle // center angle 13154 ]; 13155 13156 // API properties 13157 point.percentage = fraction * 100; 13158 point.total = total; 13159 13160 }); 13161 13162 13163 this.setTooltipPoints(); 13164 }, 13165 13166 /** 13167 * Render the slices 13168 */ 13169 render: function () { 13170 var series = this; 13171 13172 // cache attributes for shapes 13173 series.getAttribs(); 13174 13175 this.drawPoints(); 13176 13177 // draw the mouse tracking area 13178 if (series.options.enableMouseTracking !== false) { 13179 series.drawTracker(); 13180 } 13181 13182 this.drawDataLabels(); 13183 13184 if (series.options.animation && series.animate) { 13185 series.animate(); 13186 } 13187 13188 // (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see 13189 series.isDirty = false; // means data is in accordance with what you see 13190 }, 13191 13192 /** 13193 * Draw the data points 13194 */ 13195 drawPoints: function () { 13196 var series = this, 13197 chart = series.chart, 13198 renderer = chart.renderer, 13199 groupTranslation, 13200 //center, 13201 graphic, 13202 group, 13203 shadow = series.options.shadow, 13204 shadowGroup, 13205 shapeArgs; 13206 13207 // draw the slices 13208 each(series.points, function (point) { 13209 graphic = point.graphic; 13210 shapeArgs = point.shapeArgs; 13211 group = point.group; 13212 shadowGroup = point.shadowGroup; 13213 13214 // put the shadow behind all points 13215 if (shadow && !shadowGroup) { 13216 shadowGroup = point.shadowGroup = renderer.g('shadow') 13217 .attr({ zIndex: 4 }) 13218 .add(); 13219 } 13220 13221 // create the group the first time 13222 if (!group) { 13223 group = point.group = renderer.g('point') 13224 .attr({ zIndex: 5 }) 13225 .add(); 13226 } 13227 13228 // if the point is sliced, use special translation, else use plot area traslation 13229 groupTranslation = point.sliced ? point.slicedTranslation : [chart.plotLeft, chart.plotTop]; 13230 group.translate(groupTranslation[0], groupTranslation[1]); 13231 if (shadowGroup) { 13232 shadowGroup.translate(groupTranslation[0], groupTranslation[1]); 13233 } 13234 13235 // draw the slice 13236 if (graphic) { 13237 graphic.animate(shapeArgs); 13238 } else { 13239 point.graphic = 13240 renderer.arc(shapeArgs) 13241 .attr(extend( 13242 point.pointAttr[NORMAL_STATE], 13243 { 'stroke-linejoin': 'round' } 13244 )) 13245 .add(point.group) 13246 .shadow(shadow, shadowGroup); 13247 } 13248 13249 // detect point specific visibility 13250 if (point.visible === false) { 13251 point.setVisible(false); 13252 } 13253 13254 }); 13255 13256 }, 13257 13258 /** 13259 * Override the base drawDataLabels method by pie specific functionality 13260 */ 13261 drawDataLabels: function () { 13262 var series = this, 13263 data = series.data, 13264 point, 13265 chart = series.chart, 13266 options = series.options.dataLabels, 13267 connectorPadding = pick(options.connectorPadding, 10), 13268 connectorWidth = pick(options.connectorWidth, 1), 13269 connector, 13270 connectorPath, 13271 softConnector = pick(options.softConnector, true), 13272 distanceOption = options.distance, 13273 seriesCenter = series.center, 13274 radius = seriesCenter[2] / 2, 13275 centerY = seriesCenter[1], 13276 outside = distanceOption > 0, 13277 dataLabel, 13278 labelPos, 13279 labelHeight, 13280 halves = [// divide the points into right and left halves for anti collision 13281 [], // right 13282 [] // left 13283 ], 13284 x, 13285 y, 13286 visibility, 13287 rankArr, 13288 sort, 13289 i = 2, 13290 j; 13291 13292 // get out if not enabled 13293 if (!options.enabled) { 13294 return; 13295 } 13296 13297 // run parent method 13298 Series.prototype.drawDataLabels.apply(series); 13299 13300 // arrange points for detection collision 13301 each(data, function (point) { 13302 if (point.dataLabel) { // it may have been cancelled in the base method (#407) 13303 halves[ 13304 point.labelPos[7] < mathPI / 2 ? 0 : 1 13305 ].push(point); 13306 } 13307 }); 13308 halves[1].reverse(); 13309 13310 // define the sorting algorithm 13311 sort = function (a, b) { 13312 return b.y - a.y; 13313 }; 13314 13315 // assume equal label heights 13316 labelHeight = halves[0][0] && halves[0][0].dataLabel && halves[0][0].dataLabel.getBBox().height; 13317 13318 /* Loop over the points in each quartile, starting from the top and bottom 13319 * of the pie to detect overlapping labels. 13320 */ 13321 while (i--) { 13322 13323 var slots = [], 13324 slotsLength,
13325 usedSlots = [], 13326 points = halves[i], 13327 pos, 13328 length = points.length, 13329 slotIndex; 13330 13331 13332 // build the slots 13333 for (pos = centerY - radius - distanceOption; pos <= centerY + radius + distanceOption; pos += labelHeight) { 13334 slots.push(pos); 13335 // visualize the slot 13336 /* 13337 var slotX = series.getX(pos, i) + chart.plotLeft - (i ? 100 : 0), 13338 slotY = pos + chart.plotTop; 13339 if (!isNaN(slotX)) { 13340 chart.renderer.rect(slotX, slotY - 7, 100, labelHeight) 13341 .attr({ 13342 'stroke-width': 1, 13343 stroke: 'silver' 13344 }) 13345 .add(); 13346 chart.renderer.text('Slot '+ (slots.length - 1), slotX, slotY + 4) 13347 .attr({ 13348 fill: 'silver' 13349 }).add(); 13350 } 13351 // */ 13352 } 13353 slotsLength = slots.length; 13354 13355 // if there are more values than available slots, remove lowest values 13356 if (length > slotsLength) { 13357 // create an array for sorting and ranking the points within each quarter 13358 rankArr = [].concat(points); 13359 rankArr.sort(sort); 13360 j = length; 13361 while (j--) { 13362 rankArr[j].rank = j; 13363 } 13364 j = length; 13365 while (j--) { 13366 if (points[j].rank >= slotsLength) { 13367 points.splice(j, 1); 13368 } 13369 } 13370 length = points.length; 13371 } 13372 13373 // The label goes to the nearest open slot, but not closer to the edge than 13374 // the label's index. 13375 for (j = 0; j < length; j++) { 13376 13377 point = points[j]; 13378 labelPos = point.labelPos; 13379 13380 var closest = 9999, 13381 distance, 13382 slotI; 13383 13384 // find the closest slot index 13385 for (slotI = 0; slotI < slotsLength; slotI++) { 13386 distance = mathAbs(slots[slotI] - labelPos[1]); 13387 if (distance < closest) { 13388 closest = distance; 13389 slotIndex = slotI; 13390 } 13391 } 13392 13393 // if that slot index is closer to the edges of the slots, move it 13394 // to the closest appropriate slot 13395 if (slotIndex < j && slots[j] !== null) { // cluster at the top 13396 slotIndex = j; 13397 } else if (slotsLength < length - j + slotIndex && slots[j] !== null) { // cluster at the bottom 13398 slotIndex = slotsLength - length + j; 13399 while (slots[slotIndex] === null) { // make sure it is not taken 13400 slotIndex++; 13401 } 13402 } else { 13403 // Slot is taken, find next free slot below. In the next run, the next slice will find the 13404 // slot above these, because it is the closest one 13405 while (slots[slotIndex] === null) { // make sure it is not taken 13406 slotIndex++; 13407 } 13408 } 13409 13410 usedSlots.push({ i: slotIndex, y: slots[slotIndex] }); 13411 slots[slotIndex] = null; // mark as taken 13412 } 13413 // sort them in order to fill in from the top 13414 usedSlots.sort(sort); 13415 13416 13417 // now the used slots are sorted, fill them up sequentially 13418 for (j = 0; j < length; j++) { 13419 13420 point = points[j]; 13421 labelPos = point.labelPos; 13422 dataLabel = point.dataLabel; 13423 var slot = usedSlots.pop(), 13424 naturalY = labelPos[1]; 13425 13426 visibility = point.visible === false ? HIDDEN : VISIBLE; 13427 slotIndex = slot.i; 13428 13429 // if the slot next to currrent slot is free, the y value is allowed 13430 // to fall back to the natural position 13431 y = slot.y; 13432 if ((naturalY > y && slots[slotIndex + 1] !== null) || 13433 (naturalY < y && slots[slotIndex - 1] !== null)) { 13434 y = naturalY; 13435 } 13436 13437 // get the x - use the natural x position for first and last slot, to prevent the top 13438 // and botton slice connectors from touching each other on either side 13439 x = series.getX(slotIndex === 0 || slotIndex === slots.length - 1 ? naturalY : y, i); 13440 13441 // move or place the data label 13442 dataLabel 13443 .attr({ 13444 visibility: visibility, 13445 align: labelPos[6] 13446 })[dataLabel.moved ? 'animate' : 'attr']({ 13447 x: x + options.x + 13448 ({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0), 13449 y: y + options.y 13450 }); 13451 dataLabel.moved = true; 13452 13453 // draw the connector 13454 if (outside && connectorWidth) { 13455 connector = point.connector; 13456 13457 connectorPath = softConnector ? [ 13458 M, 13459 x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label 13460 'C', 13461 x, y, // first break, next to the label 13462 2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5], 13463 labelPos[2], labelPos[3], // second break 13464 L, 13465 labelPos[4], labelPos[5] // base 13466 ] : [ 13467 M, 13468 x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label 13469 L, 13470 labelPos[2], labelPos[3], // second break 13471 L, 13472 labelPos[4], labelPos[5] // base 13473 ]; 13474 13475 if (connector) { 13476 connector.animate({ d: connectorPath }); 13477 connector.attr('visibility', visibility); 13478 13479 } else { 13480 point.connector = connector = series.chart.renderer.path(connectorPath).attr({ 13481 'stroke-width': connectorWidth, 13482 stroke: options.connectorColor || point.color || '#606060', 13483 visibility: visibility, 13484 zIndex: 3 13485 }) 13486 .translate(chart.plotLeft, chart.plotTop) 13487 .add(); 13488 } 13489 } 13490 } 13491 } 13492 }, 13493 13494 /** 13495 * Draw point specific tracker objects. Inherit directly from column series. 13496 */ 13497 drawTracker: ColumnSeries.prototype.drawTracker, 13498 13499 /** 13500 * Pies don't have point marker symbols 13501 */ 13502 getSymbol: function () {} 13503 13504}); 13505seriesTypes.pie = PieSeries; 13506 13507 13508// global variables 13509extend(Highcharts, { 13510 Chart: Chart, 13511 dateFormat: dateFormat, 13512 pathAnim: pathAnim, 13513 getOptions: getOptions, 13514 hasBidiBug: hasBidiBug, 13515 numberFormat: numberFormat, 13516 Point: Point, 13517 Color: Color, 13518 Renderer: Renderer, 13519 SVGRenderer: SVGRenderer, 13520 VMLRenderer: VMLRenderer, 13521 CanVGRenderer: CanVGRenderer, 13522 seriesTypes: seriesTypes, 13523 setOptions: setOptions, 13524 Series: Series, 13525 13526 // Expose utility funcitons for modules 13527 addEvent: addEvent, 13528 removeEvent: removeEvent, 13529 createElement: createElement, 13530 discardElement: discardElement, 13531 css: css, 13532 each: each, 13533 extend: extend, 13534 map: map, 13535 merge: merge, 13536 pick: pick, 13537 splat: splat, 13538 extendClass: extendClass, 13539 placeBox: placeBox, 13540 product: 'Highcharts', 13541 version: '2.2.1' 13542}); 13543}());
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.