// ==ClosureCompiler== // @compilation_level SIMPLE_OPTIMIZATIONS /** * @license Highcharts JS v1.2.5 (2010-04-13) * * (c) 2010 Torstein Hønsi * * License: www.highcharts.com/license */ /* * Roadmap * * 2.0 (summer 2010) * - Rewritten rendering engine to SVG/VML * - Save as image * - Print chart - open in a popup and call window.print. * - Improvements to the toolbar object to allow the two above * * 2.1 (summer/autumn 2010) * - Logarithmic axis * - Built-in table parser * - Base value for column, bar and area series * - Automatic margin size based on axis label size and legend. Anti-collision logic for labels. * - Floating columns and bars * - Candlestick charts * - Stock charts? * - Radar charts? * * 2.2 (autumn 2010) * - Improve pies: shadow, better dataLabels, 3D view. * */ (function() { // encapsulated variables var // abstracts to make compiled code smaller undefined, doc = document, win = window, math = Math, mathRound = math.round, mathFloor = math.floor, mathMax = math.max, mathAbs = math.abs, mathCos = math.cos, mathSin = math.sin, // some variables userAgent = navigator.userAgent, isIE = /msie/i.test(userAgent) && !win.opera, isWebKit = /AppleWebKit/.test(userAgent), styleTag, canvasCounter = 0, colorCounter, symbolCounter, symbolSizes = {}, idCounter = 0, timeFactor = 1, // 1 = JavaScript time, 1000 = Unix time garbageBin, // some constants for frequently used strings DIV = 'div', ABSOLUTE = 'absolute', RELATIVE = 'relative', HIDDEN = 'hidden', HIGHCHARTS_HIDDEN = 'highcharts-' + HIDDEN, VISIBLE = 'visible', PX = 'px', // time methods, changed based on whether or not UTC is used makeTime, getMinutes, getHours, getDay, getDate, getMonth, getFullYear, setMinutes, setHours, setDate, setMonth, setFullYear, // check for a custom HighchartsAdapter defined prior to this file globalAdapter = win.HighchartsAdapter, adapter = globalAdapter || {}, // Utility functions. If the HighchartsAdapter is not defined, adapter is an empty object // and all the utility functions will be null. In that case they are populated by the // default adapters below. each = adapter.each, grep = adapter.grep, map = adapter.map, merge = adapter.merge, hyphenate = adapter.hyphenate, addEvent = adapter.addEvent, fireEvent = adapter.fireEvent, animate = adapter.animate, getAjax = adapter.getAjax, // lookup over the types and the associated classes seriesTypes = {}; // the jQuery adapter if (!globalAdapter && win.jQuery) { var jQ = jQuery; each = function(arr, fn) { for (var i = 0, len = arr.length; i < len; i++) { if (fn.call(arr[i], arr[i], i, arr) === false) { return i; } } }; grep = jQ.grep; map = function(arr, fn){ //return jQuery.map(arr, fn); var results = []; for (var i = 0, len = arr.length; i < len; i++) { results[i] = fn.call(arr[i], arr[i], i, arr); } return results; } merge = function(){ var args = arguments; return jQ.extend(true, null, args[0], args[1], args[2], args[3]); } hyphenate = function (str){ return str.replace(/([A-Z])/g, function(a, b){ return '-'+ b.toLowerCase() }); } addEvent = function (el, event, fn){ jQ(el).bind(event, fn); } fireEvent = function(el, type, eventArguments, defaultFunction) { var event = jQ.Event(type), detachedType = 'detached'+ type; extend(event, eventArguments); // Prevent jQuery from triggering the object method that is named the // same as the event. For example, if the event is 'select', jQuery // attempts calling el.select and it goes into a loop. if (el[type]) { el[detachedType] = el[type]; el[type] = null; } // trigger it jQ(el).trigger(event); // attach the method if (el[detachedType]) { el[type] = el[detachedType]; el[detachedType] = null; } if (defaultFunction && !event.isDefaultPrevented()) { defaultFunction(event); } } animate = function (el, params, options) { jQ(el).animate(params, options); } getAjax = function (url, callback) { jQ.get(url, null, callback); } jQ.extend( jQ.easing, { easeOutQuad: function (x, t, b, c, d) { return -c *(t/=d)*(t-2) + b; } }); // the MooTools adapter } else if (!globalAdapter && win.MooTools) { each = $each; map = function (arr, fn){ return arr.map(fn); } grep = function(arr, fn) { return arr.filter(fn) } merge = $merge; hyphenate = function (str){ return str.hyphenate(); } addEvent = function (el, type, fn) { // if the addEvent method is not defined, el is a custom Highcharts object // like series or point if (!el.addEvent) { if (el.nodeName) el = $(el); // a dynamically generated node else extend(el, new Events()); // a custom object } el.addEvent(type, fn); } fireEvent = function(el, event, eventArguments, defaultFunction) { // create an event object that keeps all functions event = new Event({ type: event, target: el }); event = extend (event, eventArguments); // override the preventDefault function to be able to use // this for custom events event.preventDefault = function() { defaultFunction = null; } // if fireEvent is not available on the object, there hasn't been added // any events to it above if (el.fireEvent) el.fireEvent(event.type, event); // fire the default if it is passed and it is not prevented above if (defaultFunction) defaultFunction(event); } animate = function (el, params, options){ var myEffect = new Fx.Morph($(el), extend(options, { transition: Fx.Transitions.Quad.easeInOut })); myEffect.start(params); } getAjax = function (url, callback) { (new Request({ url: url, method: 'get', onSuccess: callback })).send(); } } /** * Check if an element is an array, and if not, make it into an array. Like * MooTools' $.splat. */ function splat(obj) { if (!obj || obj.constructor != Array) obj = [obj]; return obj; } /** * Returns true if the object is not null or undefined. Like MooTools' $.defined. * @param {Object} obj */ function defined (obj) { return obj !== undefined && obj !== null; } /** * Return the first value that is defined. Like MooTools' $.pick. */ function pick() { var args = arguments, i, arg; for (i = 0; i < args.length; i++) { arg = args[i]; if (defined(arg)) return arg; }; } /** * Dynamically add a CSS rule to the page * @param {String} selector * @param {Object} declaration * @param {Boolean} print Whether to add the styles only to print media. IE only. */ function addCSSRule(selector, declaration, print) { var key, serialized = '', styleSheets, last, media = print ? 'print' : '', createStyleTag = function(print) { return createElement( 'style', { type: 'text/css', media: print ? 'print' : '' }, null, doc.getElementsByTagName('HEAD')[0] ); }; // add the style tag the first time if (!styleTag) styleTag = createStyleTag(); // serialize the declaration for (key in declaration) serialized += hyphenate(key) +':'+ declaration[key] + ';'; if (!isIE) { // create a text node in the style tag styleTag.appendChild( doc.createTextNode( selector + " {" + serialized + "}\n" ) ); } else { // get the last stylesheet and add rules var styleSheets = doc.styleSheets, index, styleSheet; if (print) { // only in IE for now createStyleTag(true); } index = styleSheets.length - 1; while (index >= 0 && styleSheets[index].media != media) index--; styleSheet = styleSheets[index]; styleSheet.addRule(selector, serialized); } } /** * Extend an object with the members of another * @param {Object} a The object to be extended * @param {Object} b The object to add to the first one */ function extend(a, b) { if (!a) a = {}; for (var n in b) a[n] = b[n]; return a; } /** * Merge the default options with custom options and return the new options structure * @param {Object} options The new custom options */ function setOptions(options) { defaultOptions = merge(defaultOptions, options); // apply UTC setTimeMethods(); return defaultOptions; } /** * Discard an element by moving it to the bin and delete * @param {Object} The HTML node to discard */ function discardElement(element) { // create a garbage bin element, not part of the DOM if (!garbageBin) garbageBin = createElement(DIV); // move the node and empty bin if (element) garbageBin.appendChild(element); garbageBin.innerHTML = ''; } var defaultFont = 'normal 12px "Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', defaultLabelOptions = { enabled: true, // rotation: 0, align: 'center', x: 0, y: 15, /*formatter: function() { return this.value; },*/ style: { color: '#666', font: defaultFont.replace('12px', '11px') //'10px bold "Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif' } }, defaultOptions = { colors: ['#4572A7', '#AA4643', '#89A54E', '#80699B', '#3D96AE', '#DB843D', '#92A8CD', '#A47D7C', '#B5CA92'], symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'], lang: { loading: 'Loading...', months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], decimalPoint: '.', resetZoom: 'Reset zoom', resetZoomTitle: 'Reset zoom level 1:1', thousandsSep: ',' }, global: { useUTC: true }, chart: { //alignTicks: false, //className: null, /*events: { * load, * selection * }, */ margin: [50, 50, 60, 80], borderColor: '#4572A7', //borderWidth: 0, borderRadius: 5, defaultSeriesType: 'line', ignoreHiddenSeries: true, //inverted: false, //shadow: false, //style: {}, //backgroundColor: null, //plotBackgroundColor: null, plotBorderColor: '#C0C0C0' //plotBorderWidth: 0, //plotShadow: false, //zoomType: '' }, title: { text: 'Chart title', style: { textAlign: 'center', color: '#3E576F', font: defaultFont.replace('12px', '16px'), margin: '10px 0 0 0' } }, subtitle: { text: '', style: { textAlign: 'center', color: '#6D869F', font: defaultFont, margin: 0 } }, plotOptions: { line: { // base series options allowPointSelect: false, //allowDrag: false, // point dragging - not yet implemented //dragType: 'y', showCheckbox: false, animation: true, //cursor: 'default', //enableMouseTracking: true, events: {}, lineWidth: 2, shadow: true, // stacking: null, marker: { enabled: true, symbol: 'auto', lineWidth: 0, radius: 4, lineColor: '#FFFFFF', fillColor: 'auto', states: { // states for a single point hover: { //radius: base + 2 }, select: { fillColor: '#FFFFFF', lineColor: 'auto', lineWidth: 2 } } }, point: { events: {} }, dataLabels: merge(defaultLabelOptions, { enabled: false, y: -6, formatter: function() { return this.y; } }), //pointStart: 0, //pointInterval: 1, showInLegend: true, states: { // states for the entire series hover: { //enabled: false, lineWidth: 3, marker: { // lineWidth: base + 1, // radius: base + 1 } }, select: { marker: {} } } } }, labels: { //items: [], style: { position: ABSOLUTE, color: '#3E576F', font: defaultFont } }, legend: { enabled: true, layout: 'horizontal', labelFormatter: function() { return this.name }, //borderWidth: 0, borderColor: '#909090', borderRadius: 5, shadow: true, //backgroundColor: null, style: { bottom: '10px', left: '80px', padding: '5px' }, itemStyle: { listStyle: 'none', margin: 0, padding: '0 2em 0 0', // make room for the checkbox font: defaultFont, cursor: 'pointer', color: '#3E576F', position: RELATIVE // to allow absolute placement of the checkboxes }, itemHoverStyle: { color: '#000' }, itemHiddenStyle: { color: '#CCC' }, itemCheckboxStyle: { position: ABSOLUTE, right: 0 }, //reversed: false, // docs symbolWidth: 16, symbolPadding: 5 }, loading: { hideDuration: 100, labelStyle: { font: defaultFont.replace('normal', 'bold'), position: RELATIVE, top: '1em' }, showDuration: 100, style: { position: ABSOLUTE, backgroundColor: 'white', opacity: 0.5, textAlign: 'center' } }, tooltip: { enabled: true, formatter: function() { var pThis = this, series = pThis.series, xAxis = series.xAxis, x = pThis.x; return ''+ (pThis.point.name || series.name) +'
'+ (defined(x) ? 'X value: '+ (xAxis && xAxis.options.type == 'datetime' ? dateFormat('%Y-%m-%d %H:%M:%S', x) : x) +'
': '')+ 'Y value: '+ pThis.y; }, backgroundColor: 'rgba(255, 255, 255, .85)', borderWidth: 2, borderRadius: 5, shadow: true, snap: 10, style: { color: '#333333', font: defaultFont, fontSize: '9pt', padding: '5px', whiteSpace: 'nowrap' } }, toolbar: { itemStyle: { color: '#4572A7', cursor: 'pointer', margin: '20px', font: defaultFont } }, credits: { enabled: true, text: 'Highcharts.com', href: 'http://www.highcharts.com', style: { position: ABSOLUTE, right: '10px', bottom: '5px', color: '#999', textDecoration: 'none', font: defaultFont.replace('12px', '10px') }, target: '_self' } }; // Axis defaults //defaultOptions.xAxis = merge(defaultOptions.axis); var defaultXAxisOptions = { // alternateGridColor: null, // categories: [], dateTimeLabelFormats: { second: '%H:%M:%S', minute: '%H:%M', hour: '%H:%M', day: '%e. %b', week: '%e. %b', month: '%b \'%y', year: '%Y' }, endOnTick: false, gridLineColor: '#C0C0C0', // gridLineWidth: 0, // reversed: false, labels: defaultLabelOptions, lineColor: '#C0D0E0', lineWidth: 1, max: null, min: null, maxZoom: null, minorGridLineColor: '#E0E0E0', minorGridLineWidth: 1, minorTickColor: '#A0A0A0', //minorTickInterval: null, minorTickLength: 2, minorTickPosition: 'outside', // inside or outside minorTickWidth: 1, //plotBands: [], //plotLines: [], //reversed: false, showFirstLabel: true, showLastLabel: false, startOfWeek: 1, startOnTick: false, tickColor: '#C0D0E0', tickInterval: 'auto', tickLength: 5, tickmarkPlacement: 'between', // on or between tickPixelInterval: 100, tickPosition: 'outside', tickWidth: 1, title: { enabled: false, text: 'X-values', align: 'middle', // low, middle or high margin: 35, //rotation: 0, //side: 'outside', style: { color: '#6D869F', font: defaultFont.replace('normal', 'bold') } }, type: 'linear' // linear or datetime }, defaultYAxisOptions = merge(defaultXAxisOptions, { endOnTick: true, gridLineWidth: 1, tickPixelInterval: 72, showLastLabel: true, labels: { align: 'right', x: -8, y: 3 }, lineWidth: 0, maxPadding: 0.05, minPadding: 0.05, startOnTick: true, tickWidth: 0, title: { enabled: true, margin: 40, rotation: 270, text: 'Y-values' } }), defaultLeftAxisOptions = { labels: { align: 'right', x: -8, y: 3 }, title: { rotation: 270 } }, defaultRightAxisOptions = { labels: { align: 'left', x: 8, y: 3 }, title: { rotation: 90 } }, defaultBottomAxisOptions = { // horizontal axis labels: { align: 'center', x: 0, y: 14 }, title: { rotation: 0 } }, defaultTopAxisOptions = merge(defaultBottomAxisOptions, { labels: { y: -5 } }); // Series defaults var defaultPlotOptions = defaultOptions.plotOptions, defaultSeriesOptions = defaultPlotOptions.line; //defaultPlotOptions.line = merge(defaultSeriesOptions); defaultPlotOptions.spline = merge(defaultSeriesOptions); defaultPlotOptions.scatter = merge(defaultSeriesOptions, { //dragType: 'xy', // n/a lineWidth: 0, states: { hover: { lineWidth: 0 } } }); defaultPlotOptions.area = merge(defaultSeriesOptions, { // lineColor: null, // overrides color, but lets fillColor be unaltered // fillOpacity: .75, fillColor: 'auto' }); defaultPlotOptions.areaspline = merge(defaultPlotOptions.area); defaultPlotOptions.column = merge(defaultSeriesOptions, { borderColor: '#FFFFFF', borderWidth: 1, borderRadius: 0, groupPadding: 0.2, pointPadding: 0.1, states: { hover: { brightness: 0.1, shadow: false }, select: { color: '#C0C0C0', borderColor: '#000000', shadow: false } } }); defaultPlotOptions.bar = merge(defaultPlotOptions.column, { dataLabels: { align: 'left', x: 5, y: 0 } }); defaultPlotOptions.pie = merge(defaultSeriesOptions, { //dragType: '', // n/a borderColor: '#FFFFFF', borderWidth: 1, center: ['50%', '50%'], legendType: 'point', size: '90%', slicedOffset: 10, states: { hover: { brightness: 0.1, shadow: false } } }); // set the default time methods setTimeMethods(); // class-like inheritance function extendClass(parent, members) { var object = function(){}; object.prototype = new parent(); extend(object.prototype, members); return object; } /* function reverseArray(arr) { var reversed = []; for (var i = arr.length - 1; i >= 0; i--) reversed.push( arr[i]); return reversed; } */ // return a deep value without throwing an error /*function deepStructure(obj, path) { // split the path into an array path = path.split('.'), i = 0; // recursively set obj to the path while (path[i] && obj) obj = obj[path[i++]]; if (i == path.length) return obj; }*/ /** * Create a color from a string or configuration object * @param {Object} val */ function setColor(val, ctx) { if (typeof val == 'string') { return val; } else if (val.linearGradient) { var gradient = ctx.createLinearGradient.apply(ctx, val.linearGradient); each (val.stops, function(stop) { gradient.addColorStop(stop[0], stop[1]); }); return gradient; } } var Color = function(input) { var rgba = [], result; function parse(input) { // rgba if((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))) rgba = [parseInt(result[1]), parseInt(result[2]), parseInt(result[3]), parseFloat(result[4])]; // hex else if((result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(input))) rgba = [parseInt(result[1],16), parseInt(result[2],16), parseInt(result[3],16), 1]; } function get() { // it's NaN if gradient colors on a column chart if (rgba && !isNaN(rgba[0])) return 'rgba('+ rgba.join(',') +')'; else return input; } function brighten(alpha) { if (typeof alpha == 'number' && alpha != 0) { for (var i = 0; i < 3; i++) { rgba[i] += parseInt(alpha * 255); if (rgba[i] < 0) rgba[i] = 0; if (rgba[i] > 255) rgba[i] = 255; } } return this; } function setOpacity(alpha) { rgba[3] = alpha; return this; } parse(input); // public methods return { get: get, brighten: brighten, setOpacity: setOpacity }; }; //defaultMarkers = ['circle']; function createElement (tag, attribs, styles, parent, nopad) { var el = doc.createElement(tag); if (attribs) extend(el, attribs); if (nopad) setStyles(el, {padding: 0, border: 'none', margin: 0}); if (styles) setStyles(el, styles); if (parent) parent.appendChild(el); return el; }; function setStyles (el, styles) { //for (var x in styles) el.style[x] = styles[x]; if (isIE) { if (styles.opacity !== undefined) styles.filter = 'alpha(opacity='+ (styles.opacity * 100) +')'; } extend(el.style, styles); }; /** * Format a number and return a string based on input settings * @param {Number} number The input number to format * @param {Number} decimals The amount of decimals * @param {String} decPoint The decimal point, defaults to the one given in the lang options * @param {String} thousandsSep The thousands separator, defaults to the one given in the lang options */ function numberFormat (number, decimals, decPoint, thousandsSep) { var lang = defaultOptions.lang, // http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_phps_number_format/ n = number, c = isNaN(decimals = mathAbs(decimals)) ? 2 : decimals, d = decPoint === undefined ? lang.decimalPoint : decPoint, t = thousandsSep === undefined ? lang.thousandsSep : thousandsSep, s = n < 0 ? "-" : "", i = parseInt(n = mathAbs(+n || 0).toFixed(c)) + "", j = (j = i.length) > 3 ? j % 3 : 0; return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + (c ? d + mathAbs(n - i).toFixed(c).slice(2) : ""); }; /** * Based on http://www.php.net/manual/en/function.strftime.php * @param {String} format * @param {Number} timestamp * @param {Boolean} capitalize */ function dateFormat(format, timestamp, capitalize) { function pad (number) { return number.toString().replace(/^([0-9])$/, '0$1'); } if (!defined(timestamp)) return 'Invalid date'; var date = new Date(timestamp * timeFactor), // get the basic time values hours = date[getHours](), day = date[getDay](), dayOfMonth = date[getDate](), month = date[getMonth](), fullYear = date[getFullYear](), lang = defaultOptions.lang, langWeekdays = lang.weekdays, langMonths = lang.months, // list all format keys replacements = { // Day 'a': langWeekdays[day].substr(0, 3), // Short weekday, like 'Mon' 'A': langWeekdays[day], // Long weekday, like 'Monday' 'd': pad(dayOfMonth), // Two digit day of the month, 01 to 31 'e': dayOfMonth, // Day of the month, 1 through 31 // Week (none implemented) // Month 'b': langMonths[month].substr(0, 3), // Short month, like 'Jan' 'B': langMonths[month], // Long month, like 'January' 'm': pad(month + 1), // Two digit month number, 01 through 12 // Year 'y': fullYear.toString().substr(2, 2), // Two digits year, like 09 for 2009 'Y': fullYear, // Four digits year, like 2009 // Time 'H': pad(hours), // Two digits hours in 24h format, 00 through 23 'I': pad((hours % 12) || 12), // Two digits hours in 12h format, 00 through 11 'l': (hours % 12) || 12, // Hours in 12h format, 1 through 12 'M': pad(date[getMinutes]()), // Two digits minutes, 00 through 59 'p': hours < 12 ? 'AM' : 'PM', // Upper case AM or PM 'P': hours < 12 ? 'am' : 'pm', // Lower case AM or PM 'S': pad(date.getSeconds()) // Two digits seconds, 00 through 59 }; // do the replaces for (var key in replacements) format = format.replace('%'+ key, replacements[key]); // Optionally capitalize the string and return return capitalize ? format.substr(0, 1).toUpperCase() + format.substr(1) : format; }; /** * Set the time methods globally based on the useUTC option. Time method can be either * local time or UTC (default). */ function setTimeMethods() { var useUTC = defaultOptions.global.useUTC; makeTime = useUTC ? Date.UTC : function(year, month, date, hours, minutes, seconds) { return new Date( year, month, pick(date, 1), pick(hours, 0), pick(minutes, 0), pick(seconds, 0) ).getTime(); }; getMinutes = useUTC ? 'getUTCMinutes' : 'getMinutes'; getHours = useUTC ? 'getUTCHours' : 'getHours'; getDay = useUTC ? 'getUTCDay' : 'getDay'; getDate = useUTC ? 'getUTCDate' : 'getDate'; getMonth = useUTC ? 'getUTCMonth' : 'getMonth'; getFullYear = useUTC ? 'getUTCFullYear' : 'getFullYear'; setMinutes = useUTC ? 'setUTCMinutes' : 'setMinutes'; setHours = useUTC ? 'setUTCHours' : 'setHours'; setDate = useUTC ? 'setUTCDate' : 'setDate'; setMonth = useUTC ? 'setUTCMonth' : 'setMonth'; setFullYear = useUTC ? 'setUTCFullYear' : 'setFullYear'; }; /** * Return the absolute page position of an element * @param {Object} el */ function getPosition (el) { var p = { x: el.offsetLeft, y: el.offsetTop }; while (el.offsetParent) { el = el.offsetParent; p.x += el.offsetLeft; p.y += el.offsetTop; if (el != doc.body && el != doc.documentElement) { p.x -= el.scrollLeft; p.y -= el.scrollTop; } } return p; } var Layer = function (name, appendTo, props, styles) { var layer = this, div, appendToStyle = appendTo.style; props = extend({ className: 'highcharts-'+ name }, props); styles = extend({ width: appendToStyle.width, //appendTo.offsetWidth + PX, height: appendToStyle.height, //appendTo.offsetHeight + PX, position: ABSOLUTE, top: 0, left: 0, margin: 0, padding: 0, border: 'none' }, styles); div = createElement(DIV, props, styles, appendTo); extend(layer, { div: div, width: parseInt(styles.width), height: parseInt(styles.height) }); // initial SVG string layer.svg = isIE ? '' : ''+ ''; // save it for later layer.basicSvg = layer.svg; } Layer.prototype = { getCtx: function() { if (!this.ctx) { var cvs = createElement('canvas', { id: 'highcharts-canvas-' + idCounter++, width: this.width, height: this.height }, { position: ABSOLUTE }, this.div); if (isIE) { G_vmlCanvasManager.initElement(cvs); cvs = doc.getElementById(cvs.id); } this.ctx = cvs.getContext('2d'); } return this.ctx; }, getSvg: function() { if (!this.svgObject) { var layer = this, div = layer.div, width = layer.width, height = layer.height; if (isIE) { // create xmlns if excanvas hasn't done it if (!doc.namespaces["g_vml_"]) { doc.namespaces.add("g_vml_", "urn:schemas-microsoft-com:vml"); // setup default css doc.createStyleSheet().cssText = "g_vml_\\:*{behavior:url(#default#VML)}"; } this.svgObject = createElement(DIV, null, { width: width + PX, height: height + PX, position: ABSOLUTE }, div); } else { // create an object and inject SVG into it this.svgObject = createElement('object', { width: width, height: height, type: 'image/svg+xml' }, { position : ABSOLUTE, left: 0, top: 0 }, div); } } return this.svgObject; }, drawLine: function(x1, y1, x2, y2, color, width) { var ctx = this.getCtx(), xBefore = x1; // normalize to a crisp line if (x1 == x2) x1 = x2 = mathRound(x1) + (width % 2 / 2); if (y1 == y2) y1 = y2 = mathRound(y1) + (width % 2 / 2); // draw path ctx.lineWidth = width; ctx.lineCap = 'round'; /* If this is not set, plotBands appear like 'square' in Firefox/Win. Reason unknown. */ ctx.beginPath(); ctx.moveTo(x1, y1); ctx.strokeStyle = color; ctx.lineTo(x2, y2); ctx.closePath(); ctx.stroke(); }, drawPolyLine: function(points, color, width, shadow, fillColor) { var ctx = this.getCtx(), shadowLine = []; // the shadow if (shadow && width) { each (points, function(point) { // add 1px offset shadowLine.push(point === undefined ? point : point + 1); }); for (var i = 1; i <= 3; i++) // three lines of differing thickness and opacity this.drawPolyLine(shadowLine, 'rgba(0, 0, 0, '+ (0.05 * i) +')', 6 - 2 * i); } // the line path ctx.beginPath(); for (i = 0; i < points.length; i += 2) ctx[i == 0 ? 'moveTo' : 'lineTo'](points[i], points[i + 1]); // common properties extend(ctx, { lineWidth: width, lineJoin: 'round' }); // stroke if (color && width) { ctx.strokeStyle = setColor(color, ctx); ctx.stroke(); } // fill if (fillColor) { ctx.fillStyle = setColor(fillColor, ctx); ctx.fill(); } }, drawRect: function(x, y, w, h, color, width, radius, fill, shadow, image) { // must (?) be done twice to apply both stroke and fill in excanvas var drawPath = function() { var ret; if (w > 0 && h > 0) { // zero or negative dimensions break Opera 10 ctx.beginPath(); if (!radius) { ctx.rect(x, y, w, h); } else { ctx.moveTo(x, y + radius); ctx.lineTo(x, y + h - radius); ctx.quadraticCurveTo(x, y + h, x + radius, y + h); // change: use bezier ctx.lineTo(x + w - radius, y + h); ctx.quadraticCurveTo(x + w, y + h, x + w, y + h - radius); ctx.lineTo(x + w, y + radius); ctx.quadraticCurveTo(x + w, y , x + w - radius, y); ctx.lineTo(x + radius, y); ctx.quadraticCurveTo(x , y, x, y + radius); } ctx.closePath(); ret = true; } return ret; }; var ctx = this.getCtx(), normalizer = (width || 0) % 2 / 2; // normalize for sharp edges x = mathRound(x) + normalizer; y = mathRound(y) + normalizer; w = mathRound(w - 2 * normalizer); h = mathRound(h - 2 * normalizer); // apply the drop shadow if (shadow) for (var i = 1; i <= 3; i++) { this.drawRect(x + 1, y + 1, w, h, 'rgba(0, 0, 0, '+ (0.05 * i) +')', 6 - 2 * i, radius); } // apply the background image behind everything if (image) ctx.drawImage(image, x, y, w, h); if (drawPath()) { if (fill) { ctx.fillStyle = setColor(fill, ctx); ctx.fill(); // draw path again if (win.G_vmlCanvasManager) drawPath(); } if (width) { ctx.strokeStyle = setColor(color, ctx); ctx.lineWidth = width; ctx.stroke(); } } }, drawSymbol: function(symbol, x, y, radius, lineWidth, lineColor, fillColor) { var ctx = this.getCtx(), imageRegex = /^url\((.*?)\)$/; ctx.beginPath(); if (symbol == 'square') { var len = 0.707 * radius; ctx.moveTo(x - len, y - len); ctx.lineTo(x + len, y - len); ctx.lineTo(x + len, y + len); ctx.lineTo(x - len, y + len); ctx.lineTo(x - len, y - len); } else if (symbol == 'triangle') { y++; ctx.moveTo(x, y - 1.33 * radius); ctx.lineTo(x + radius, y + 0.67 * radius); ctx.lineTo(x - radius, y + 0.67 * radius); ctx.lineTo(x, y - 1.33 * radius); } else if (symbol == 'triangle-down') { y--; ctx.moveTo(x, y + 1.33 * radius); ctx.lineTo(x - radius, y - 0.67 * radius); ctx.lineTo(x + radius, y - 0.67 * radius); ctx.lineTo(x, y + 1.33 * radius); } else if (symbol == 'diamond') { ctx.moveTo(x, y - radius); ctx.lineTo(x + radius, y); ctx.lineTo(x, y + radius); ctx.lineTo(x - radius, y); ctx.lineTo(x, y - radius); } else if (imageRegex.test(symbol)) { createElement('img', { onload: function() { var img = this, size = symbolSizes[img.src] || [img.width, img.height]; setStyles(img, { left: mathRound(x - size[0] / 2) + PX, top: mathRound(y - size[1] / 2) + PX, visibility: VISIBLE }) // Bug workaround: Opera (10.01) fails to get size the second time symbolSizes[img.src] = size; }, src: symbol.match(imageRegex)[1] }, { position: ABSOLUTE, visibility: isIE ? VISIBLE : HIDDEN // hide until left and top are set in Gecko }, this.div); } else { // default: circle ctx.arc(x, y, radius, 0, 2*math.PI, true); } if (fillColor) { ctx.fillStyle = fillColor; ctx.fill(); // draw path again //if (isIE) ctx.arc(x, y, radius, 0, 2*Math.PI, true); } if (lineColor && lineWidth) { ctx.strokeStyle = lineColor || "rgb(100, 100, 255)"; ctx.lineWidth = lineWidth || 2; ctx.stroke(); } }, drawHtml: function(html, attributes, styles) { createElement( DIV, extend(attributes, { innerHTML: html }), extend(styles, { position: ABSOLUTE}), this.div ); }, /** * Add text and draw it. For those browsers adding the text to an SVG object, * it is better for performance to add all strings before the object * is created. This function takes the same arguments as addText. * * @param {string} str * @param {number} x * @param {number} y * @param {object} style * @param {number} rotation * @param {string} align */ drawText: function() { this.addText.apply(this, arguments); this.strokeText(); }, addText: function(str, x, y, style, rotation, align) { if (str || str === 0) { // declare variables var layer = this, hasObject, div = layer.div, CSStransform, css = '', style = style || {}, fill = style.color || '#000000', align = align || 'left', fontSize = parseInt(style.fontSize || style.font.replace(/^[a-z ]+/, '')), span, spanWidth, transformOriginX; // prepare style for (var key in style) css += hyphenate(key) +':'+ style[key] +';'; // what transform property is supported? each (['MozTransform', 'WebkitTransform', 'transform'], function(str) { if (str in div.style) CSStransform = str; }); // if the text is not rotated, or if the browser supports CSS transform, // write a simple span if (!rotation || CSStransform) { span = createElement('span', { innerHTML: str }, extend(style, { position: ABSOLUTE, left: x + PX, whiteSpace: 'nowrap', bottom: mathRound(layer.height - y - fontSize * 0.25) + PX, color: fill }), div); // fix the position according to align and rotation spanWidth = span.offsetWidth; if (align == 'right') setStyles(span, { left: (x - spanWidth) + PX }); else if (align == 'center') setStyles(span, { left: mathRound(x - spanWidth / 2) + PX }); if (rotation) { // ... and CSStransform transformOriginX = { left: 0, center: 50, right: 100 }[align] span.style[CSStransform] = 'rotate('+ rotation +'deg)'; span.style[CSStransform +'Origin'] = transformOriginX +'% 100%'; } } else if (isIE) { // to achieve rotated text, the ie text is drawn on a vector line that // is extrapolated to the left or right or both depending on the // alignment of the text hasObject = true; var radians = (rotation || 0) * math.PI * 2 / 360, // deg to rad costheta = mathCos(radians), sintheta = mathSin(radians), length = layer.width, // the text is not likely to be longer than this baselineCorrection = fontSize / 3 || 3, left = align == 'left', right = align == 'right', x1 = left ? x : x - length * costheta, x2 = right ? x : x + length * costheta, y1 = left ? y : y - length * sintheta, y2 = right ? y : y + length * sintheta; // IE seems to always draw the text with v-text-align middle, so we need // to correct for that by moving the path x1 += baselineCorrection * sintheta; x2 += baselineCorrection * sintheta; y1 -= baselineCorrection * costheta; y2 -= baselineCorrection * costheta; if (mathAbs(x1 - x2) < 0.1) x1 += 0.1; // strange IE painting bug if (mathAbs(y1 - y2) < 0.1) y1 += 0.1; // strange IE painting bug layer.svg += ''+ ''+ ''+ ''+ ''; // svg browsers } else { hasObject = true; layer.svg += ''+ ''+ str+ ''+ ''; } layer.hasObject = hasObject; } }, /* Experimental text rendering using canvas text. Speed and possibly weight are the advantages. Excanvas trunk supports canvas text, but not current version (2009-11-03). Older Gecko and Webkit browsers and Opera needs SVG approach, so all in all there is not much weight spared by this one. Furthermore, CanvasText looks crappy in Firefox, but on the other hand, SVG object make the tooltip animation slow. 2009-11-07: Preliminary conclusion: - IE: Use VML on a textpath. The con is that IE8 renders all text as bold italic. Possibly experiment with CSS text rotation for whole 90 degrees if that's supported by IE8. - Firefox >= 3.5 (Gecko 1.9.1 was it?) and Webkit > ?: Use CSS transforms to rotate the text. Canvas and textFill would be a better and faster alternative, but the text is very badly drawn in Firefox. When that's fixed in future versions, go for Canvas and textFill instead. - Opera, older Gecko and older Webkit: Use SVG. It is slow, and all the text has to be added before written to the SVG object. Hence the addText and strokeText functions. When Opera starts supporting textFill or text rotate, use that instead. _addText: function(str, x, y, style, rotation, anchor) { if (str || str === 0) { // declare variables var css = '', style = style || {}, fill = style.color || '#000000', anchor = anchor || 'start', ctx, span, font = (style.font || '') +' '+ (style.fontSize || '') +' '+ (style.fontWeight || '') +' '+ (style.fontFamily || ''), align = { start: 'left', middle: 'center', end: 'right' }[anchor], rotation = (rotation || 0) * math.PI * 2 / 360, // deg to rad fontSize = parseInt(style.fontSize || font); // prepare style for (var key in style) css += hyphenate(key) +':'+ style[key] +';'; if (rotation) { var ctx = this.getCtx(); ctx.font = font; ctx.fillStyle = fill; ctx.textAlign = align; ctx.translate(x, y); ctx.rotate(rotation); ctx.fillText(str, 0, 0); ctx.rotate(-rotation); ctx.translate(-x, -y); } else { } } },*/ strokeText: function() { if (this.hasObject) { var svgObject = this.getSvg(), svg = this.svg; if (isIE) { svgObject.innerHTML = svg; } else { svgObject.data = 'data:image/svg+xml,'+ svg +''; // append it again for Chrome to update if (isWebKit) this.div.appendChild(svgObject); } } }, clear: function() { var layer = this, div = this.div, childNodes = div.childNodes, node; if (layer.ctx) layer.ctx.clearRect(0, 0, layer.width, layer.height); if (layer.svgObject) { discardElement(layer.svgObject); layer.svgObject = null; layer.svg = layer.basicSvg; } // remove all spans for (var i = childNodes.length - 1; i >= 0; i--) { node = childNodes[i]; if (/(SPAN|IMG)/.test(node.tagName)) discardElement(node); } }, hide: function() { setStyles (this.div, { display: 'none' }) //jQuery(this.div).fadeOut(250); // A possible way to fade out in IE would be to get all the shapes // in the layer and change the opacities of their FillColor and StrokeColor /*var shapes = this.div.getElementsByTagName('shape'); each (shapes, function(shape) { //shape.style.filter = 'alpha(opacity=59)'; }) var layer = this; setTimeout(function() { layer.div.style.visibility = HIDDEN }, 2000);*/ }, show: function() { setStyles (this.div, { display: '' }) //jQuery(this.div).fadeIn(50); }, /** * Discard layer DOM elements and null the reference */ destroy: function() { discardElement(this.div); return null; } }; function Chart (options) { /** * Add a series dynamically after time * * @param {Object} options The config options * @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true. * * @return {Object} series The newly created series object */ function addSeries(options, redraw) { var series; redraw = pick(redraw, true); // defaults to true fireEvent(chart, 'addSeries', { options: options }, function() { series = initSeries(options); series.isDirty = true; chart.isDirty = true; // the series array is out of sync with the display if (redraw) chart.redraw(); }); return series; }; /** * Redraw legend, axes or series based on updated data */ function redraw() { var redrawLegend = chart.isDirty; // handle updated data in the series each (series, function(serie) { if (serie.isDirty) { // prepare the data so axis can read it serie.cleanData(); serie.getSegments(); if (serie.options.legendType == 'point') redrawLegend = true; } }); // reset maxTicks maxTicks = null; if (hasCartesianSeries) { // set axes scales each (axes, function(axis) { axis.setScale(); }) adjustTickAmounts(); // redraw axes each (axes, function(axis) { if (axis.isDirty) axis.redraw(); }) } // redraw affected series each (series, function(serie) { if (serie.isDirty && serie.visible) serie.redraw(); }); // handle added or removed series if (redrawLegend) { // series or pie points are added or removed // draw legend graphics if (legend && legend.renderHTML) { legend.renderHTML(true); legend.drawGraphics(true); } chart.isDirty = false; } // hide tooltip and hover states if (tracker && tracker.resetTracker) tracker.resetTracker(); // fire the event fireEvent(chart, 'redraw'); } /** * Initialize an individual series, called internally before render time */ function initSeries(options) { var type = options.type || optionsChart.defaultSeriesType, typeClass = seriesTypes[type], serie, hasRendered = chart.hasRendered; // an inverted chart can't take a column series and vice versa if (hasRendered) { if (inverted && type == 'column') typeClass = BarSeries; else if (!inverted && type == 'bar') typeClass = ColumnSeries; } serie = new typeClass(); serie.init(chart, options); // set internal chart properties if (!hasRendered && serie.inverted) inverted = true; if (serie.isCartesian) hasCartesianSeries = serie.isCartesian; series.push(serie); return serie; } /** * Dim the chart and show a loading text or symbol */ function showLoading() { var loadingOptions = options.loading; // create the layer at the first call if (!loadingLayer) { loadingLayer = createElement(DIV, { className: 'highcharts-loading' }, extend(loadingOptions.style, { left: marginLeft + PX, top: marginTop + PX, width: plotWidth + PX, height: plotHeight + PX, zIndex: 10, display: 'none' }), container); createElement('span', { innerHTML: options.lang.loading }, loadingOptions.labelStyle, loadingLayer); } // show it setStyles(loadingLayer, { display: '' }); animate(loadingLayer, { opacity: loadingOptions.style.opacity }, { duration: loadingOptions.showDuration }); } /** * Hide the loading layer */ function hideLoading() { animate(loadingLayer, { opacity: 0 }, { duration: options.loading.hideDuration, complete: function() { setStyles(loadingLayer, { display: 'none' }); } }); } /** * Get an axis, series or point object by id. * @param id {String} The id as given in the configuration options */ function get(id) { var i, j, match, data; // search axes for (i = 0; i < axes.length; i++) { if (axes[i].options.id == id) return axes[i]; } // search series for (i = 0; i < series.length; i++) { if (series[i].options.id == id) return series[i]; } // search points for (i = 0; i < series.length; i++) { data = series[i].data; for (j = 0; j < data.length; j++) { if (data[j].id == id) return data[j]; } } return null; } /** * Update the chart's position after it has been moved, to match * the mouse positions with the chart */ function updatePosition() { var container = doc.getElementById(containerId); if (container) { position = getPosition(container); } } /** * Create the Axis instances based on the config options */ function getAxes() { var xAxisOptions = options.xAxis || {}, yAxisOptions = options.yAxis || {}, axis; // make sure the options are arrays and add some members xAxisOptions = splat(xAxisOptions); each(xAxisOptions, function(axis, i) { axis.index = i; axis.isX = true; }); yAxisOptions = splat(yAxisOptions); each(yAxisOptions, function(axis, i) { axis.index = i; }); // concatenate all axis options into one array axes = xAxisOptions.concat(yAxisOptions); // loop the options and construct axis objects chart.xAxis = []; chart.yAxis = []; axes = map (axes, function(axisOptions) { axis = new Axis(chart, axisOptions); chart[axis.isXAxis ? 'xAxis' : 'yAxis'].push(axis); return axis; }); adjustTickAmounts(); }; /** * Adjust all axes tick amounts */ function adjustTickAmounts() { if (optionsChart.alignTicks !== false) each (axes, function(axis) { axis.adjustTickAmount(); }); } /** * Get the currently selected points from all series */ function getSelectedPoints() { var points = []; each(series, function(serie) { points = points.concat( grep( serie.data, function(point) { return point.selected; })); }); return points; }; /** * Get the currently selected series */ function getSelectedSeries() { return grep (series, function (serie) { return serie.selected; }); } /** * Zoom into a given portion of the chart given by axis coordinates * @param {Object} event */ function zoom(event) { var lang = defaultOptions.lang; // add button to reset selection chart.toolbar.add('zoom', lang.resetZoom, lang.resetZoomTitle, function() { //zoom(false); fireEvent(chart, 'selection', { resetSelection: true }, zoom); chart.toolbar.remove('zoom'); }); // if zoom is called with no arguments, reset the axes if (!event || event.resetSelection) each(axes, function(axis) { axis.setExtremes(null, null, false); }); // else, zoom in on all axes else { each (event.xAxis.concat(event.yAxis), function(axisData) { var axis = axisData.axis; // don't zoom more than maxZoom if (chart.tracker[axis.isXAxis ? 'zoomX' : 'zoomY']) axis.setExtremes(axisData.min, axisData.max, false); }); } // redraw chart redraw(); } /** * Function: (private) showTitle * * Show the title and subtitle of the chart */ function showTitle () { var title = options.title, subtitle = options.subtitle; if (!chart.titleLayer) { var titleLayer = new Layer('title-layer', container, null, { zIndex: 2 }); // title if (title && title.text) createElement('h2', { className: 'highcharts-title', innerHTML: title.text }, title.style, titleLayer.div); // subtitle if (subtitle && subtitle.text) createElement('h3', { className: 'highcharts-subtitle', innerHTML: subtitle.text }, subtitle.style, titleLayer.div); chart.titleLayer = titleLayer; } } /** * Load graphics and data required to draw the chart */ function checkResources() { var allLoaded = true; for (var n in chart.resources) { if (!chart.resources[n]) allLoaded = false; } if (allLoaded) resourcesLoaded(); }; /** * Prepare for first rendering after all data are loaded */ function resourcesLoaded() { getAxes(); // Prepare for the axis sizes each(series, function(serie) { serie.translate(); serie.setTooltipPoints(); /*if (options.tooltip.enabled) */ serie.createArea(); }); chart.render = render; setTimeout(function() { // IE(7) needs timeout render(); fireEvent(chart, 'load'); }, 0); } /** * Get the containing element, determine the size and create the inner container * div to hold the chart */ function getContainer() { renderTo = optionsChart.renderTo; containerId = 'highcharts-'+ idCounter++; if (typeof renderTo == 'string') { renderTo = doc.getElementById(renderTo); } // remove previous chart renderTo.innerHTML = ''; // If the container doesn't have an offsetWidth, it has or is a child of a node // that has display:none. We need to temporarily move it out to a visible // state to determine the size, else the legend and tooltips won't render // properly if (!renderTo.offsetWidth) { renderToClone = renderTo.cloneNode(0); setStyles(renderToClone, { position: ABSOLUTE, top: '-9999px', display: '' }); doc.body.appendChild(renderToClone); } // get the width and height var renderToOffsetHeight = (renderToClone || renderTo).offsetHeight; chartWidth = optionsChart.width || (renderToClone || renderTo).offsetWidth || 600; chartHeight = optionsChart.height || // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7: (renderToOffsetHeight > marginTop + marginBottom ? renderToOffsetHeight : 0) || 400; // create the inner container container = createElement(DIV, { className: 'highcharts-container' + (optionsChart.className ? ' '+ optionsChart.className : ''), id: containerId }, extend({ position: RELATIVE, overflow: HIDDEN, width: chartWidth + PX, height: chartHeight + PX, textAlign: 'left' }, optionsChart.style), renderToClone || renderTo ); } /** * Render all graphics for the chart */ function render () { var mgn, div, i, labels = options.labels, credits = options.credits; // Chart area mgn = 2 * (optionsChart.borderWidth || 0) + (optionsChart.shadow ? 8 : 0); backgroundLayer.drawRect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn, optionsChart.borderColor, optionsChart.borderWidth, optionsChart.borderRadius, optionsChart.backgroundColor, optionsChart.shadow); // Plot background backgroundLayer.drawRect( marginLeft, marginTop, plotWidth, plotHeight, null, null, null, optionsChart.plotBackgroundColor, null, plotBackground ); // Plot area border (new Layer('plot-border', container, null, { zIndex: 4 // in front of grid lines and graphs, behind axis lines })).drawRect( marginLeft, marginTop, plotWidth, plotHeight, optionsChart.plotBorderColor, optionsChart.plotBorderWidth, null, null, optionsChart.plotShadow ); // Printing CSS for IE if (isIE) addCSSRule('.highcharts-image-map', { display: 'none' }, 'print'); // Axes if (hasCartesianSeries) each(axes, function(axis) { axis.render(); }); // Title showTitle(); // Labels if (labels.items) each (labels.items, function () { var attributes = extend({ className: 'highcharts-label' }, this.attributes); plotLayer.drawHtml(this.html, attributes, extend(labels.style, this.style)); }); // The series each (series, function(serie) { serie.render(); }); // Legend legend = chart.legend = new Legend(chart); // Toolbar (don't redraw) if (!chart.toolbar) chart.toolbar = Toolbar(chart); // Credits if (credits.enabled && !chart.credits) chart.credits = createElement('a', { className: 'highcharts-credits', href: credits.href, innerHTML: credits.text, target: credits.target }, extend(credits.style, { zIndex: 8 }), container); // Set flag chart.hasRendered = true; // If the chart was rendered outside the top container, put it back in if (renderToClone) { renderTo.appendChild(container); discardElement(renderToClone); updatePosition() } }; /** * Clean up memory usage */ function destroy() { /** * Clear certain attributes from the element * @param {Object} d */ function purge(d) { var a = d.attributes, i, l, n; if (a) { l = a.length; for (i = l - 1; i >= 0; i -= 1) { n = a[i].name; try { //if (typeof d[n] != 'object' && !/^(width|height)$/.test(n)) { if (typeof d[n] == 'function') { d[n] = null; } } catch (e) { // IE/excanvas produces errors on some of the properties } } } a = d.childNodes; if (a) { l = a.length; for (i = l - 1; i >= 0; i--) { var node = d.childNodes[i]; purge(node); if (!node.childNodes.length) discardElement(node); } } } // destroy each series each (series, function(serie) { serie.destroy(); }); series = []; purge(container); }; /** * Create a new axis object * @param {Object} chart * @param {Object} options */ function Axis (chart, options) { /** * Set the options by merging the inheritance line */ function setOptions() { options = merge( isXAxis ? defaultXAxisOptions : defaultYAxisOptions, //defaultOptions[isXAxis ? 'xAxis' : 'yAxis'], horiz ? (opposite ? defaultTopAxisOptions : defaultBottomAxisOptions) : (opposite ? defaultRightAxisOptions : defaultLeftAxisOptions), options ); }; /** * Get the minimum and maximum for the series of each axis */ function getSeriesExtremes() { var stack = [], run; // reset dataMin and dataMax in case we're redrawing dataMin = dataMax = null; // get an overview of what series are associated with this axis associatedSeries = []; each(series, function(serie) { run = false; // match this axis against the series' given or implicated axis each(['xAxis', 'yAxis'], function(strAxis) { if ( // we're in the right x or y dimension, and... (strAxis == 'xAxis' && isXAxis || strAxis == 'yAxis' && !isXAxis) && ( // the axis number is given in the options and matches this axis index, or (serie.options[strAxis] == options.index) || // the axis index is not given (serie.options[strAxis] === undefined && options.index == 0) ) ) { serie[strAxis] = axis; associatedSeries.push(serie); // the series is visible, run the min/max detection run = true; } }); // ignore hidden series if opted if (!serie.visible && optionsChart.ignoreHiddenSeries) run = false; if (run) { var stacking; if (!isXAxis) { stacking = serie.options.stacking; usePercentage = stacking == 'percent'; // create a stack for this particular series type if (stacking) { var typeStack = stack[serie.type] || []; stack[serie.type] = typeStack; } if (usePercentage) { dataMin = 0; dataMax = 99; } } if (serie.isCartesian) { // line, column etc. need axes, pie doesn't each(serie.data, function(point, i) { var pointX = point.x, pointY = point.y; // initial values if (dataMin === null) { // start out with the first point dataMin = dataMax = point[xOrY]; } // x axis if (isXAxis) { if (pointX > dataMax) dataMax = pointX; else if (pointX < dataMin) dataMin = pointX; } // y axis else if (defined(pointY)) { if (stacking) typeStack[pointX] = typeStack[pointX] ? typeStack[pointX] + pointY : pointY; var stackedPoint = typeStack ? typeStack[pointX] : pointY; if (!usePercentage) { if (stackedPoint > dataMax) dataMax = stackedPoint; else if (stackedPoint < dataMin) dataMin = stackedPoint; } if (stacking) stacks[serie.type][pointX] = { total: stackedPoint, cum: stackedPoint }; } }); // For column, areas and bars, set the minimum automatically to zero // and prevent that minPadding is added in setScale if (!isXAxis && /(area|column|bar)/.test(serie.type)) { if (dataMin >= 0) { dataMin = 0; ignoreMinPadding = true; } else if (dataMax < 0) { dataMax = 0; ignoreMaxPadding = true; } } } } }); }; /** * Translate from axis value to pixel position on the chart, or back * */ function translate(val, backwards, cvsCoord) { var sign = 1, cvsOffset = 0, returnValue; if (cvsCoord) { sign *= -1; // canvas coordinates inverts the value cvsOffset = axisLength; } if (reversed) { // reversed axis sign *= -1; cvsOffset -= sign * axisLength; } if (backwards) { // reverse translation if (reversed) val = axisLength - val; returnValue = val / transA + min; // from chart pixel to value } else { // normal translation returnValue = sign * (val - min) * transA + cvsOffset; // from value to chart pixel } return returnValue; }; /** * Add a single line across the plot */ function drawPlotLine(value, color, width) { if (width) { var x1, y1, x2, y2, translatedValue = translate(value), skip; x1 = x2 = translatedValue + transB; y1 = y2 = chartHeight - translatedValue - transB; if (horiz) { // horizontal axis, vertical plot line y1 = marginTop; y2 = chartHeight - marginBottom; if (x1 < marginLeft || x1 > marginLeft + plotWidth) skip = true; } else { // vertical axis, horizontal plot line x1 = marginLeft; x2 = chartWidth - marginRight; if (y1 < marginTop || y1 > marginTop + plotHeight) skip = true; } if (!skip) gridLayer.drawLine(x1, y1, x2, y2, color, width); } }; /** * Add a masked band across the plot * @param {Number} from chart axis value * @param {Number} to chart axis value * @param {String} color */ function drawPlotBand(from, to, color) { // keep within plot area from = mathMax(from, min); to = Math.min(to, max); var width = (to - from) * transA; drawPlotLine(from + (to - from) / 2, color, width); } /** * Add a tick mark an a label */ function addTick(pos, tickPos, color, width, len, withLabel, index) { var x1, y1, x2, y2, str, labelOptions = options.labels; // negate the length if (tickPos == 'inside') len = -len; if (opposite) len = -len; // set the initial positions x1 = x2 = translate(pos + tickmarkOffset) + transB; y1 = y2 = chartHeight - translate(pos + tickmarkOffset) - transB; if (horiz) { y1 = chartHeight - marginBottom - (opposite ? plotHeight : 0) + offset; y2 = y1 + len; } else { x1 = marginLeft + (opposite ? plotWidth : 0) + offset; x2 = x1 - len; } if (width) axisLayer.drawLine(x1, y1, x2, y2, color, width); // write the label if (withLabel && labelOptions.enabled) { str = labelFormatter.call({ index: index, isFirst: pos == tickPositions[0], isLast: pos == tickPositions[tickPositions.length - 1], value: (categories && categories[pos] ? categories[pos] : pos) }); if (str || str === 0) axisLayer.addText( str, x1 + labelOptions.x - (tickmarkOffset && horiz ? tickmarkOffset * transA * (reversed ? -1 : 1) : 0), y1 + labelOptions.y - (tickmarkOffset && !horiz ? tickmarkOffset * transA * (reversed ? 1 : -1) : 0), labelOptions.style, labelOptions.rotation, labelOptions.align ); } }; /** * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5 * @param {Number} interval */ function normalizeTickInterval(interval, multiples) { var normalized, allowDecimals = pick(options.allowDecimals, true); // round to a tenfold of 1, 2, 2.5 or 5 magnitude = multiples ? 1 : math.pow(10, mathFloor(math.log(interval) / math.LN10)); normalized = interval / magnitude; // multiples for a linear scale if (!multiples) multiples = [1, 2, 2.5, 5, 10]; // normalize the interval to the nearest multiple for (var i = 0; i < multiples.length; i++) { interval = multiples[i]; if (normalized <= (multiples[i] + (multiples[i+1] || multiples[i])) / 2) { break; } } // multiply back to the correct magnitude interval *= magnitude; return interval; }; /** * Set the tick positions to a time unit that makes sense, for example * on the first of each month or on every Monday. */ function setDateTimeTickPositions() { tickPositions = []; var useUTC = defaultOptions.global.useUTC, oneSecond = 1000 / timeFactor, oneMinute = 60000 / timeFactor, oneHour = 3600000 / timeFactor, oneDay = 24 * 3600000 / timeFactor, oneWeek = 7 * 24 * 3600000 / timeFactor, oneMonth = 30 * 24 * 3600000 / timeFactor, oneYear = 31556952000 / timeFactor, units = [[ 'second', // unit name oneSecond, // fixed incremental unit [1, 2, 5, 10, 15, 30] // allowed multiples ], [ 'minute', // unit name oneMinute, // fixed incremental unit [1, 2, 5, 10, 15, 30] // allowed multiples ], [ 'hour', // unit name oneHour, // fixed incremental unit [1, 2, 3, 4, 6, 8, 12] // allowed multiples ], [ 'day', // unit name oneDay, // fixed incremental unit [1, 2] // allowed multiples ], [ 'week', // unit name oneWeek, // fixed incremental unit [1, 2] // allowed multiples ], [ 'month', oneMonth, [1, 2, 3, 4, 6] ], [ 'year', oneYear, null ]], unit = units[6], // default unit is years interval = unit[1], multiples = unit[2]; // loop through the units to find the one that best fits the tickInterval for (var i = 0; i < units.length; i++) { unit = units[i]; interval = unit[1]; multiples = unit[2]; if (units[i+1]) { // lessThan is in the middle between the highest multiple and the next unit. var lessThan = (interval * multiples[multiples.length - 1] + units[i + 1][1]) / 2; // break and keep the current unit if (tickInterval <= lessThan) break; } } // prevent 2.5 years intervals, though 25, 250 etc. are allowed if (interval == oneYear && tickInterval < 5 * interval) multiples = [1, 2, 5]; // get the minimum value by flooring the date var multitude = normalizeTickInterval(tickInterval / interval, multiples), minYear, // used in months and years as a basis for Date.UTC() minDate = new Date(min * timeFactor); minDate.setMilliseconds(0); if (interval >= oneSecond) // second minDate.setSeconds(interval >= oneMinute ? 0 : multitude * mathFloor(minDate.getSeconds() / multitude)); if (interval >= oneMinute) // minute minDate[setMinutes](interval >= oneHour ? 0 : multitude * mathFloor(minDate[getMinutes]() / multitude)); if (interval >= oneHour) // hour minDate[setHours](interval >= oneDay ? 0 : multitude * mathFloor(minDate[getHours]() / multitude)); if (interval >= oneDay) // day minDate[setDate](interval >= oneMonth ? 1 : multitude * mathFloor(minDate[getDate]() / multitude)); if (interval >= oneMonth) { // month minDate[setMonth](interval >= oneYear ? 0 : multitude * mathFloor(minDate[getMonth]() / multitude)); minYear = minDate[getFullYear](); } if (interval >= oneYear) { // year minYear -= minYear % multitude; minDate[setFullYear](minYear); } // week is a special case that runs outside the hierarchy if (interval == oneWeek) { // get start of current week, independent of multitude minDate[setDate](minDate[getDate]() - minDate[getDay]() + options.startOfWeek); } // get tick positions var i = 1, // prevent crash just in case time = minDate.getTime() / timeFactor, minYear = minDate[getFullYear](), minMonth = minDate[getMonth](), minDateDate = minDate[getDate](); // iterate and add tick positions at appropriate values while (time < max && i < plotWidth) { tickPositions.push(time); // if the interval is years, use Date.UTC to increase years if (interval == oneYear) { time = makeTime(minYear + i * multitude, 0) / timeFactor; // if the interval is months, use Date.UTC to increase months } else if (interval == oneMonth) { time = makeTime(minYear, minMonth + i * multitude) / timeFactor; // if we're using global time, the interval is not fixed as it jumps // one hour at the DST crossover } else if (!useUTC && (interval == oneDay || interval == oneWeek)) { time = makeTime(minYear, minMonth, minDateDate + i * multitude * (interval == oneDay ? 1 : 7)); // else, the interval is fixed and we use simple addition } else { time += interval * multitude; } i++; } // push the last time tickPositions.push(time); // dynamic label formatter if (!options.labels.formatter) labelFormatter = function() { return dateFormat(options.dateTimeLabelFormats[unit[0]], this.value, 1); } } /** * Set the tick positions of a linear axis to round values like whole tens or every five. */ function setLinearTickPositions() { var correctFloat = function(num) { // JS round off float errors var invMag = (magnitude < 1 ? mathRound(1 / magnitude) : 1) * 10; return mathRound(num * invMag) / invMag }, i, roundedMin = mathFloor(min / tickInterval) * tickInterval, roundedMax = math.ceil(max / tickInterval) * tickInterval; // default extreme ticks when axis does not start and end on a tick //firstTickPosition = roundedMin + tickInterval, //lastTickPosition = roundedMax - tickInterval, //invMag = (magnitude < 1 ? 1 / magnitude : 1) * 10; // round off JS float errors; tickPositions = []; // populate the intermediate values // todo: round off float errors occur here! i = correctFloat(roundedMin); while (i <= roundedMax) { //for (i = roundedMin; i <= roundedMax; i += tickInterval) { //i = mathRound(i * invMag) / invMag tickPositions.push(i); i = correctFloat(i + tickInterval); } // pad categorised axis to nearest half unit if (categories) { min -= 0.5; max += 0.5; } // dynamic label formatter if (!labelFormatter) labelFormatter = function() { return this.value; } }; /** * Set the tick positions to round values and optionally extend the extremes * to the nearest tick */ function setTickPositions() { if (isDatetimeAxis) setDateTimeTickPositions(); else setLinearTickPositions(); // reset min/max or remove extremes based on start/end on tick var roundedMin = tickPositions[0], roundedMax = tickPositions[tickPositions.length - 1]; if (options.startOnTick) { min = roundedMin; } else if (min > roundedMin) { tickPositions.shift(); } if (options.endOnTick) { max = roundedMax; } else if (max < roundedMax) { tickPositions.pop(); } } /** * When using multiple axes, adjust the number of ticks to match the highest * number of ticks in that group */ function adjustTickAmount() { if (!isDatetimeAxis && !categories) { // only apply to linear scale var oldTickAmount = tickAmount, calculatedTickAmount = tickPositions.length; // set the axis-level tickAmount to use below tickAmount = maxTicks[xOrY]; if (calculatedTickAmount < tickAmount) { while (tickPositions.length < tickAmount) tickPositions.push(tickPositions[tickPositions.length - 1] + tickInterval); transA *= (calculatedTickAmount - 1) / (tickAmount - 1); } if (defined(oldTickAmount) && tickAmount != oldTickAmount) axis.isDirty = true; } }; /** * Set the scale based on data min and max, user set min and max or options */ function setScale() { var length, type, i, total, oldMin = min, oldMax = max, maxZoom = options.maxZoom, zoomOffset; // get data extremes if needed getSeriesExtremes(); // initial min and max from the extreme data values min = pick(userSetMin, options.min, dataMin); max = pick(userSetMax, options.max, dataMax); // maxZoom exceeded, just center the selection if (max - min < maxZoom) { zoomOffset = (maxZoom - max + min) / 2; // if min and max options have been set, don't go beyond it min = mathMax(min - zoomOffset, pick(options.min, min - zoomOffset)); max = math.min(min + maxZoom, pick(options.max, min + maxZoom)); } // pad the values to get clear of the chart's edges if (!categories && !usePercentage) { length = (max - min) || 1; if (!defined(options.min) && minPadding && (dataMin < 0 || !ignoreMinPadding)) min -= length * minPadding; if (!defined(options.max) && maxPadding && (dataMax > 0 || !ignoreMaxPadding)) max += length * maxPadding; } // tickInterval if (categories || min == max) tickInterval = 1; else tickInterval = options.tickInterval == 'auto' ? (max - min) * options.tickPixelInterval / axisLength : options.tickInterval; if (!isDatetimeAxis && options.tickInterval == 'auto') // linear tickInterval = normalizeTickInterval(tickInterval); // minorTickInterval minorTickInterval = (options.minorTickInterval == 'auto' && tickInterval) ? tickInterval / 5 : options.minorTickInterval; // get fixed positions based on tickInterval setTickPositions(); // the translation factor used in translate function transA = axisLength / ((max - min) || 1); // record the greatest number of ticks for multi axis if (!maxTicks) maxTicks = { // first call, or maxTicks have been reset after a zoom operation x: 0, y: 0 }; if (!isDatetimeAxis && tickPositions.length > maxTicks[xOrY]) maxTicks[xOrY] = tickPositions.length; // reset stacks //if (!isXAxis) for (type in stacks) each (stacks[type], function(stack, i) { if (!isXAxis) for (type in stacks) for (i in stacks[type]) { stacks[type][i].cum = stacks[type][i].total; } // mark as dirty axis.isDirty = (min != oldMin || max != oldMax); }; /** * Set the extremes and optionally redraw * @param {Number} newMin * @param {Number} newMax * @param {Boolean} redraw * */ function setExtremes(newMin, newMax, redraw) { redraw = pick(redraw, true); // defaults to true fireEvent(axis, 'setExtremes', { // fire an event to enable syncing of multiple charts min: newMin, max: newMax }, function() { // the default event handler // make sure categorized axes are not exceeded if (categories) { if (newMin < 0) newMin = 0; if (newMax > categories.length - 1) newMax = categories.length - 1; } // set the new values //userSetMin = pick(newMin, min); //userSetMax = pick(newMax, max); //if (defined(newMin)) userSetMin = newMin; //if (defined(newMax)) userSetMax = newMax; // this fails on zooming when a series is hidden and ignoreHiddenSeries is true //userSetMin = pick(newMin, options.min, dataMin); //userSetMax = pick(newMax, options.max, dataMax); userSetMin = newMin; userSetMax = newMax; // redraw if (redraw) chart.redraw(); }); }; /** * Set new axis categories and optionally redraw * @param {Array} newCategories * @param {Boolean} doRedraw */ function setCategories(newCategories, doRedraw) { categories = newCategories; if (pick(doRedraw, true)) redraw(); }; /* * * Reset min and max and set the scale again from data min and max * / function reset() { //min = max = tickInterval = minorTickInterval = tickPositions = null; setScale(); }*/ /** * Get the actual axis extremes */ function getExtremes() { return { min: min, max: max, dataMin: dataMin, dataMax: dataMax } } /** * Add a plot band or plot line after render time * * @param item {Object} The plotBand or plotLine configuration object */ function addPlotBandOrLine(item) { var isLine = item.width, collection = isLine ? plotLines : plotBands; collection.push(item); if (isLine) drawPlotLine(item.value, item.color, item.width); else drawPlotBand(item.from, item.to, item.color); } /** * Remove a plot band or plot line from the chart by id * @param {Object} id */ function removePlotBandOrLine(id) { each ([plotBands, plotLines], function(collection) { for (var i = 0; i < collection.length; i++) { if (collection[i].id == id) { collection.splice(i, 1); break; } } }); render(); } /** * Redraw the axis to reflect changes in the data or axis extremes */ function redraw() { // hide tooltip and hover states if (tracker.resetTracker) tracker.resetTracker(); // render the axis render(); // mark associated series as dirty and ready for redraw each (associatedSeries, function(series) { series.isDirty = true; }); } function render() { var axisTitle = options.title, alternateGridColor = options.alternateGridColor, minorTickWidth = options.minorTickWidth, lineWidth = options.lineWidth, lineLeft, lineTop, tickmarkPos, hasData = associatedSeries.length && defined(min) && defined(max); // clear the axis layers before new grid and ticks are drawn axisLayer.clear(); gridLayer.clear(); // return if there's no series on this axis //if (!associatedSeries.length || !defined(min) || !defined(max)) return; // If the series has data draw the ticks. Else only the line and title if (hasData) { // alternate grid color if (alternateGridColor) { each(tickPositions, function(pos, i) { if (i % 2 == 0 && pos < max) { drawPlotBand( pos, tickPositions[i + 1] !== undefined ? tickPositions[i + 1] : max, alternateGridColor ); } }); } // custom plot bands (behind grid lines) each (plotBands, function(plotBand) { drawPlotBand(plotBand.from, plotBand.to, plotBand.color); }); // minor grid lines if (minorTickInterval && !categories) for (var i = min; i <= max; i += minorTickInterval) { drawPlotLine(i, options.minorGridLineColor, options.minorGridLineWidth); if (minorTickWidth) addTick( i, options.minorTickPosition, options.minorTickColor, minorTickWidth, options.minorTickLength ); } // grid lines and tick marks each(tickPositions, function(pos, index) { tickmarkPos = pos + tickmarkOffset; // add the grid line drawPlotLine(tickmarkPos, options.gridLineColor, options.gridLineWidth); // add the tick mark addTick( pos, options.tickPosition, options.tickColor, options.tickWidth, options.tickLength, !((pos == min && !options.showFirstLabel) || (pos == max && !options.showLastLabel)), index ); }); // custom plot lines (in front of grid lines) each (plotLines, function(plotLine) { drawPlotLine(plotLine.value, plotLine.color, plotLine.width); }); } // end if hasData // axis line if (lineWidth) { lineLeft = marginLeft + (opposite ? plotWidth : 0) + offset; lineTop = chartHeight - marginBottom - (opposite ? plotHeight : 0) + offset; axisLayer.drawLine( horiz ? marginLeft: lineLeft, horiz ? lineTop: marginTop, horiz ? chartWidth - marginRight : lineLeft, horiz ? lineTop: chartHeight - marginBottom, options.lineColor, lineWidth ); } // title if (axisTitle && axisTitle.enabled && axisTitle.text) { // compute anchor points for each of the title align options var margin = horiz ? marginLeft : marginTop, length = horiz ? plotWidth : plotHeight; // the position in the length direction of the axis var alongAxis = { low: margin + (horiz ? 0 : length), middle: margin + length / 2, high: margin + (horiz ? length : 0) }[axisTitle.align]; // the position in the perpendicular direction of the axis var offAxis = (horiz ? marginTop + plotHeight : marginLeft) + (horiz ? 1 : -1) * // horizontal axis reverses the margin (opposite ? -1 : 1) * // so does opposite axes axisTitle.margin - (isIE ? parseInt( axisTitle.style.fontSize || axisTitle.style.font.replace(/^[a-z ]+/, '') ) / 3 : 0); // preliminary fix for vml's centerline axisLayer.addText( axisTitle.text, horiz ? alongAxis: offAxis + (opposite ? plotWidth : 0) + offset, // x horiz ? offAxis - (opposite ? plotHeight : 0) + offset: alongAxis, // y axisTitle.style, axisTitle.rotation || 0, { low: 'left', middle: 'center', high: 'right' }[axisTitle.align] ); } // stroke tick labels and title axisLayer.strokeText(); axis.isDirty = false; }; // Run Axis var isXAxis = options.isX, opposite = options.opposite, // needed in setOptions horiz = inverted ? !isXAxis : isXAxis, stacks = { bar: {}, column: {}, area: {}, areaspline: {} }; setOptions(); // do the merging var axis = this, isDatetimeAxis = options.type == 'datetime', offset = options.offset || 0, xOrY = isXAxis ? 'x' : 'y', axisLength = horiz ? plotWidth : plotHeight, transA, // translation factor transB = horiz ? marginLeft : marginBottom, // translation addend axisLayer = new Layer('axis-layer', container, null, { zIndex: 7}), /* The axis layer is in front of the series because the axis line must hide graphs and bars. Grid lines are drawn on the grid layer. */ gridLayer = new Layer('grid-layer', container, null, { zIndex: 1 }), dataMin, dataMax, associatedSeries, userSetMin, userSetMax, max = null, min = null, minPadding = options.minPadding, maxPadding = options.maxPadding, ignoreMinPadding, // can be set to true by a column or bar series ignoreMaxPadding, usePercentage, events = options.events, eventType, plotBands = options.plotBands || [], plotLines = options.plotLines || [], tickInterval, minorTickInterval, magnitude, tickPositions, // array containing predefined positions tickAmount, zoom = 1, //var axisLabelsLayer = new Layer((horiz ? 'x' : 'y') +'-axis-labels'); labelFormatter = options.labels.formatter, // can be overwritten by dynamic format // column plots are always categorized categories = options.categories || (isXAxis && chart.columnCount), reversed = options.reversed, tickmarkOffset = (categories && options.tickmarkPlacement == 'between') ? 0.5 : 0; //var hasWrittenTitle; // inverted charts have reversed xAxes as default if (inverted && isXAxis && reversed === undefined) reversed = true; // negate offset if (!opposite) offset *= -1; if (horiz) offset *= -1; // expose some variables extend (axis, { addPlotBand: addPlotBandOrLine, addPlotLine: addPlotBandOrLine, adjustTickAmount: adjustTickAmount, categories: categories, getExtremes: getExtremes, isXAxis: isXAxis, options: options, render: render, setExtremes: setExtremes, setScale: setScale, setCategories: setCategories, translate: translate, redraw: redraw, removePlotBand: removePlotBandOrLine, removePlotLine: removePlotBandOrLine, //reset: reset, reversed: reversed, stacks: stacks }); // register event listeners for (eventType in events) { addEvent(axis, eventType, events[eventType]); } // set min and max setScale(); }; // end Axis function Toolbar(chart) { var toolbarLayer, buttons = {}; toolbarLayer = new Layer('toolbar', container, null, { zIndex: 1004, width: 'auto', height: 'auto' }); function add(id, text, title, fn) { if (!buttons[id]) { var button = createElement(DIV, { innerHTML: text, title: title, onclick: fn }, extend(options.toolbar.itemStyle, { zIndex: 1003 }), toolbarLayer.div); buttons[id] = button; } } function remove(id) { discardElement(buttons[id]); buttons[id] = null; } // public return { add: add, remove: remove } }; function MouseTracker (chart, options) { /** * Get the currently hovered point */ function getActivePoint() { return activePoint; }; /** * Add IE support for pageX and pageY * @param {Object} e The event object in standard browsers */ function normalizeMouseEvent(e) { // common IE normalizing e = e || win.event; if (!e.target) e.target = e.srcElement; // pageX and pageY if (!e.pageX) e.pageX = e.clientX + (doc.documentElement.scrollLeft || doc.body.scrollLeft); if (!e.pageY) e.pageY = e.clientY + (doc.documentElement.scrollTop || doc.body.scrollTop); return e; } /** * Get the click position in terms of axis values. * * @param {Object} e A mouse event */ function getMouseCoordinates(e) { var coordinates = { xAxis: [], yAxis: [] }; each (axes, function(axis, i) { var translate = axis.translate, isXAxis = axis.isXAxis, isHorizontal = inverted ? !isXAxis : isXAxis; coordinates[isXAxis ? 'xAxis' : 'yAxis'].push({ axis: axis, value: translate( isHorizontal ? e.pageX - position.x - marginLeft : plotHeight - e.pageY + position.y + marginTop , true ) }) }); return coordinates; } /* * * Drop a point after dragging to change it's value * * @todo: * - x dimension * / function dropDragPoint() { if (hasDragged && dragPoint) { var yAxis = dragPointCoordinates.yAxis, i = 0; // identify the point's yAxis for (i; i < yAxis.length; i++) { if (yAxis[i].axis == dragPoint.series.yAxis) { break; } } // update the point dragPoint.update(yAxis[i].value); dragPoint = null; } } */ /** * Set the JS events on the container element */ function setDOMEvents () { imagemap.onmousedown = function(e) { e = normalizeMouseEvent(e); // record the start position if (e.preventDefault) e.preventDefault(); chart.mouseIsDown = mouseIsDown = true; mouseDownX = e.pageX; mouseDownY = e.pageY; // make a selection if (hasCartesianSeries && (zoomX || zoomY)) { if (!selectionMarker) selectionMarker = createElement(DIV, null, { position: ABSOLUTE, border: 'none', background: '#4572A7', opacity: 0.25, width: zoomHor ? 0 : plotWidth + PX, height: zoomVert ? 0 : plotHeight + PX }); plotLayer.div.appendChild(selectionMarker); } // drag a point /* else if (activePoint && e.target.tagName == 'AREA') { var seriesOptions = activePoint.series.options, dragType = seriesOptions.dragType; if (seriesOptions.allowDrag) { allowXDrag = /x/.test(dragType); allowYDrag = /y/.test(dragType); } // define the point if (allowXDrag || allowYDrag) dragPoint = activePoint; } */ }; // Use native browser event for this one. It's faster, and MooTools // doesn't use clientX and clientY. imagemap.onmousemove = function(e) { e = normalizeMouseEvent(e); e.returnValue = false; if (mouseIsDown) { // make selection // determine if the mouse has moved more than 10px hasDragged = Math.sqrt( Math.pow(mouseDownX - e.pageX, 2) + Math.pow(mouseDownY - e.pageY, 2) ) > 10; // adjust the width of the selection marker if (zoomHor) { var xSize = e.pageX - mouseDownX; setStyles(selectionMarker, { width: mathAbs(xSize) + PX, left: ((xSize > 0 ? 0 : xSize) + mouseDownX - position.x - marginLeft) + PX }); } // adjust the height of the selection marker if (zoomVert) { var ySize = e.pageY - mouseDownY; setStyles(selectionMarker, { height: mathAbs(ySize) + PX, top: ((ySize > 0 ? 0 : ySize) + + mouseDownY - position.y - marginTop) + PX }); } /* Removed to prevent bloating. Can be added as a separate component later. // drag a point if (hasDragged && dragPoint) { // draw the hover point dragPoint.series.drawPointState(dragPoint, 'hover'); // record the coordinates dragPointCoordinates = getMouseCoordinates(e); if (allowXDrag) { // update the plot coordinates dragPoint.plotX = e.pageX - position.x - marginLeft; // get the tooltip text and refresh the tooltip dragPoint.x = dragPoint.series.xAxis.translate( dragPoint.plotX, true ); } if (allowYDrag) { // update the plot coordinates dragPoint.plotY = e.pageY - position.y - marginTop; // get the tooltip text and refresh the tooltip dragPoint.y = dragPoint.series.yAxis.translate( plotHeight - dragPoint.plotY, true ); } // adjust height for columns dragPoint.h = (dragPoint.yBottom || dragPoint.y0) - dragPoint.plotY; dragPoint.setTooltipText(); tooltip.refresh(dragPoint, dragPoint.series); } */ } else { // show the tooltip onmousemove(e); } return false; }; imagemap.onmouseup = function() { var selectionIsMade; if (selectionMarker) { var selectionData = { xAxis: [], yAxis: [] }, selectionLeft = selectionMarker.offsetLeft, selectionTop = selectionMarker.offsetTop, selectionWidth = selectionMarker.offsetWidth, selectionHeight = selectionMarker.offsetHeight; // a selection has been made if (hasDragged) { // record each axis' min and max each (axes, function(axis, i) { var translate = axis.translate, isXAxis = axis.isXAxis, isHorizontal = inverted ? !isXAxis : isXAxis, selectionMin = translate( isHorizontal ? selectionLeft : plotHeight - selectionTop - selectionHeight, true ), selectionMax = translate( isHorizontal ? selectionLeft + selectionWidth : plotHeight - selectionTop, true ); selectionData[isXAxis ? 'xAxis' : 'yAxis'].push({ axis: axis, min: math.min(selectionMin, selectionMax), // for reversed axes max: mathMax(selectionMin, selectionMax) }); }); fireEvent(chart, 'selection', selectionData, zoom); selectionIsMade = true; } discardElement(selectionMarker); selectionMarker = null; } chart.mouseIsDown = mouseIsDown = hasDragged = false; /* else { dropDragPoint(); }*/ }; // MooTools 1.2.4 doesn't handle this 'mouseleave' in IE imagemap.onmouseout = function(e) { e = e || win.event; var related = e.relatedTarget || e.toElement; // check that the mouse has really left the imagemap if (related && related != trackerImage && related.tagName != 'AREA') { // reset the tracker resetTracker(); // if the user is pushing a point, drop it //dropDragPoint(); // reset mouseIsDown and hasDragged chart.mouseIsDown = mouseIsDown = hasDragged = false; } } // MooTools 1.2.3 doesn't fire this in IE when using addEvent imagemap.onclick = function(e) { e = normalizeMouseEvent(e); e.cancelBubble = true; // IE specific if (!hasDragged) { if (activePoint && e.target.tagName == 'AREA') { var plotX = activePoint.plotX, plotY = activePoint.plotY; // add page position info extend(activePoint, { pageX: position.x + marginLeft + (inverted ? plotWidth - plotY : plotX), pageY: position.y + marginTop + (inverted ? plotHeight - plotX : plotY) }); // the series click event fireEvent(chart.hoverSeries, 'click', extend(e, { point: activePoint })); // the point click event activePoint.firePointEvent('click', e); } else { extend (e, getMouseCoordinates(e)); // fire a click event in the chart fireEvent(chart, 'click', e); } } // reset mouseIsDown and hasDragged //chart.mouseIsDown = mouseIsDown = hasDragged = false; hasDragged = false; }; }; /** * Refresh the tooltip on mouse move */ function onmousemove (e) { var point = chart.hoverPoint, series = chart.hoverSeries; if (series) { // get the point if (!point) point = series.tooltipPoints[ inverted ? e.pageY - position.y - marginTop : e.pageX - position.x - marginLeft ]; // a new point is hovered, refresh the tooltip if (point && point != activePoint) { // trigger the events if (activePoint) activePoint.firePointEvent('mouseOut'); point.firePointEvent('mouseOver'); // refresh the tooltip if (tooltip) tooltip.refresh(point); activePoint = point; } } }; /** * Create the image map that listens for mouseovers */ function createImageMap () { var id = 'highchartsMap'+ canvasCounter++; chart.imagemap = imagemap = createElement('map', { name: id, id: id, className: 'highcharts-image-map' }, null, container); // Append the image to the image map to allow events to // bubble up trackerImage = createElement('img', { useMap: '#'+ id }, { width: plotWidth + PX, height: plotHeight + PX, left: marginLeft + PX, top: marginTop + PX, opacity: 0, border: 'none', position: ABSOLUTE, // Workaround: if not clipped, the left axis will flicker in // IE8 when hovering the chart clip: 'rect(1px,'+ plotWidth +'px,'+ plotHeight +'px,1px)', zIndex: 9 }, imagemap); // Blank image for modern browsers. IE doesn't need a valid // image for the image map to work, and fails in SSL mode // if it's present. if (!isIE) trackerImage.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='; }; /** * Reset the tracking by hiding the tooltip, the hover series state and the hover point */ function resetTracker() { // hide the tooltip if (tooltip) tooltip.hide(); // hide the hovered series and point if (chart.hoverSeries) { chart.hoverSeries.setState(); chart.hoverSeries = null; activePoint = null; } } /** * Bring a specific area to the front so that the user can follow a line. The * legend area should always stay on top. Series tracker areas are brought to the * top after the legend area. * @param {Object} area The area DOM element */ function insertAtFront(area) { var before = 0, i, childNodes = imagemap.childNodes; for (i = 0; i < childNodes.length; i++) { if (childNodes[i].isLegendArea) { before = i + 1; break; } } imagemap.insertBefore(area, childNodes[before]); } //if (!options.enabled) return; // Run MouseTracker var activePoint, mouseDownX, mouseDownY, hasDragged, selectionMarker, /*dragPoint, dragPointCoordinates, allowXDrag, allowYDrag,*/ zoomType = optionsChart.zoomType, zoomX = /x/.test(zoomType), zoomY = /y/.test(zoomType), zoomHor = zoomX && !inverted || zoomY && inverted, zoomVert = zoomY && !inverted || zoomX && inverted; // public createImageMap(); if (options.enabled) chart.tooltip = tooltip = Tooltip(options); setDOMEvents(); // set the fixed interval ticking for the smooth tooltip setInterval(function() { if (tooltipTick) tooltipTick(); }, 32); // expose properties extend (this, { insertAtFront: insertAtFront, zoomX: zoomX, zoomY: zoomY, resetTracker: resetTracker }); }; /** * The overview of the chart's series * @param {Object} chart */ var Legend = function(chart) { // already existing //if (chart.legend) return; var options = chart.options.legend; if (!options.enabled) return; var li, layout = options.layout, symbolWidth = options.symbolWidth, dom, topRule = '#'+ container.id +' .highcharts-legend li', // apply once for each chart allItems = [], legendLayer = new Layer('legend', container, null, { zIndex: 7 }), legendArea, series = chart.series, reversedLegend = options.reversed; // Don't use Layer prototype because this needs to sit above the chart in zIndex this.dom = dom = createElement(DIV, { className: 'highcharts-legend highcharts-legend-'+ layout, innerHTML: '' }, extend({ position: ABSOLUTE, zIndex: 7 }, options.style), container); // Add the CSS for all states addCSSRule(topRule, extend(options.itemStyle, { paddingLeft: (symbolWidth + options.symbolPadding) + PX, 'float': layout == 'horizontal' ? 'left' : 'none' })); addCSSRule(topRule +':hover', options.itemHoverStyle); addCSSRule(topRule +'.'+ HIGHCHARTS_HIDDEN, options.itemHiddenStyle); addCSSRule('.highcharts-legend-horizontal li', { 'float': 'left' }); renderHTML(); drawGraphics(); function renderHTML(clear) { if (clear) { each (allItems, function(item) { discardElement(item.legendItem); }); allItems = []; } // add HTML for each series if (reversedLegend) { series.reverse(); } each(series, function(serie) { if (!serie.options.showInLegend) return; // use points or series for the legend item depending on legendType var items = (serie.options.legendType == 'point') ? serie.data : [serie]; each(items, function(item) { // let these series types use a simple symbol item.simpleSymbol = /(bar|pie|area|column)/.test(serie.type); // generate the list item item.legendItem = li = createElement('li', { innerHTML: options.labelFormatter.call(item), className: item.visible ? '' : HIGHCHARTS_HIDDEN }, null, //item.visible ? style : merge(style, options.itemHiddenStyle), dom.firstChild ); // add the checkbox if (item.options && item.options.showCheckbox) { item.checkbox = createElement('input', { type: 'checkbox', checked: item.selected, defaultChecked: item.selected // required by IE7 }, options.itemCheckboxStyle, li); } // add the events addEvent(li, 'mouseover', function() { item.setState('hover'); }); addEvent(li, 'mouseout', function() { item.setState(); }); addEvent(li, 'click', function(event) { var target = event.target, strLegendItemClick = 'legendItemClick', fnLegendItemClick = function() { item.setVisible(); }; // click the input if (target.tagName == 'INPUT') { fireEvent (item, 'checkboxClick', { checked: target.checked }, function() { item.select(); } ); // click the name or symbol } else if (item.firePointEvent) { // point item.firePointEvent (strLegendItemClick, null, fnLegendItemClick); } else { fireEvent (item, strLegendItemClick, null, fnLegendItemClick); } }); // add it all to an array to use below allItems.push(item); }); }); if (reversedLegend) { series.reverse(); } } /** * Draw the box behind the legend and the symbols * @param {Boolean} clear Whether to clear out previous graphics */ function drawGraphics(clear) { if (clear) { legendLayer.clear(); discardElement(legendArea); legendArea = null; } if (series.length) { // draw the box around the legend if (options.borderWidth || options.backgroundColor) legendLayer.drawRect( dom.offsetLeft, dom.offsetTop, dom.offsetWidth, dom.offsetHeight, options.borderColor, options.borderWidth, options.borderRadius, options.backgroundColor, options.shadow ); // Add the symbol after the list is complete. each(allItems, function(item) { if (!item.legendItem) return; var li = item.legendItem, symbolX = dom.offsetLeft + li.offsetLeft, symbolY = dom.offsetTop + li.offsetTop + li.offsetHeight / 2, markerOptions, isHidden = item.legendItem.className == HIGHCHARTS_HIDDEN, color = isHidden ? options.itemHiddenStyle.color : item.color; // draw the line if (!item.simpleSymbol && item.options && item.options.lineWidth) legendLayer.drawLine( symbolX, symbolY, symbolX + symbolWidth, symbolY, color, item.options.lineWidth ); // draw a simple symbol if (item.simpleSymbol) // bar|pie|area|column legendLayer.drawRect( symbolX, symbolY - 6, 16, 12, null, 0, 2, color ); // draw the marker else if (item.options && item.options.marker && item.options.marker.enabled) item.drawMarker( legendLayer, symbolX + symbolWidth / 2, symbolY, merge(item.options.marker, isHidden ? { fillColor: color, lineColor: color }: null) ); }); // Add an area that detects mouseovers and puts the legend in front so it can be clicked if (imagemap) { legendArea = createElement('area', { shape: 'rect', isLegendArea: true, coords: [ dom.offsetLeft - marginLeft, dom.offsetTop - marginTop, dom.offsetLeft + dom.offsetWidth - marginLeft, dom.offsetTop + dom.offsetHeight - marginTop ].join(',') }); // insert at the top tracker.insertAtFront(legendArea); // note: using addEvent and mouseleave, mouseenter doesn't work with Moo in IE legendArea.onmouseover = function(e) { e = e || win.event; var relatedTarget = e.relatedTarget || e.fromElement; if (relatedTarget != dom && !mouseIsDown) { if (tooltip) tooltip.hide(); setStyles(dom, { zIndex: 10 }); } } dom.onmouseout = legendArea.onmouseout = function(e) { e = e || win.event; var relatedTarget = e.relatedTarget || e.toElement; if (relatedTarget && (relatedTarget == trackerImage || (relatedTarget.tagName == 'AREA' && relatedTarget != legendArea))) setStyles(dom, { zIndex: 7 }); } } } // if series.length } // expose redrawGraphics return { renderHTML: renderHTML, drawGraphics: drawGraphics }; }; function Tooltip (options) { var currentSeries, innerDiv, borderWidth = options.borderWidth, boxLayer; tooltipDiv = createElement(DIV, null, { position: ABSOLUTE, visibility: HIDDEN, overflow: HIDDEN, padding: '0 50px 5px 0', zIndex: 8 }, container); // the rounded corner box boxLayer = new Layer('tooltip-box', tooltipDiv, null, { width: chartWidth + PX, height: chartHeight + PX }); // an inner element for the contents innerDiv = createElement(DIV, { className: "highcharts-tooltip" }, extend(options.style, { maxWidth: (chartWidth - 40) + PX, //overflow: HIDDEN, interferes with the text width detection textOverflow: 'ellipsis', position: RELATIVE, zIndex: 2 }), tooltipDiv ); /** * Refresh the tooltip's text and position. * @param {Object} point * */ function refresh(point, series) { var tooltipPos = point.tooltipPos, series = point.series, //chartOptions = chart.options, borderColor = options.borderColor || point.color || series.color || '#606060', //categories = series.xAxis ? series.xAxis.categories : null, inverted = chart.inverted,//chart.options.chart.inverted, x, y, boxX, boxY, boxWidth, boxHeight, oldInnerDivHeight = innerDiv.offsetHeight, show, text = point.tooltipText; // register the current series currentSeries = series; // get the reference point coordinates (pie charts use tooltipPos) x = tooltipPos ? tooltipPos[0] : (inverted ? plotWidth - point.plotY : point.plotX); y = tooltipPos ? tooltipPos[1] : (inverted ? plotHeight - point.plotX : point.plotY); // hide tooltip if the point falls outside the plot if (x >= 0 && x <= plotWidth && y >= 0 && y <= plotHeight) { show = true; } // update the inner HTML if (text === false || !show) { hide(); } else { innerDiv.innerHTML = text; // Draw a rounded border. Draw the border with 20px extra width to minimize // the need to redraw it later. Next time, only redraw if the width of the // box is more than 20px wider or smaller than the old box. setStyles(innerDiv, { overflow: VISIBLE }); boxWidth = innerDiv.offsetWidth - borderWidth; boxHeight = innerDiv.offsetHeight - borderWidth; setStyles(innerDiv, { overflow: HIDDEN }); if (boxWidth > (boxLayer.w || 0) + 20 || boxWidth < (boxLayer.w || 0) - 20 || boxHeight > boxLayer.h || boxLayer.c != borderColor || oldInnerDivHeight != innerDiv.offsetHeight ) { boxLayer.clear(); boxLayer.drawRect( borderWidth / 2, borderWidth / 2, boxWidth + 20, boxHeight, borderColor, borderWidth, options.borderRadius, options.backgroundColor, options.shadow ); // register size extend(boxLayer, { w: boxWidth, h: boxHeight, c: borderColor }); } // keep the box within the chart area boxX = x - boxLayer.w + marginLeft - 35; boxY = y - boxLayer.h + 10 + marginTop; // it is too far to the left, and there is space to the right /*if ((inverted || boxX < 5) && x + marginLeft + boxLayer.w < chartWidth - 100) boxX = x + marginLeft + 15; // right align*/ // it is too far to the left, lift it up if (boxX < 5) { boxX = 5; boxY -= 20; } if (boxY < 5) boxY = 5; // above else if (boxY + boxLayer.h > chartHeight) boxY = chartHeight - boxLayer.h - 5; // below // do the move move(mathRound(boxX), mathRound(boxY)); // show the hover mark series.drawPointState(point, 'hover'); tooltipDiv.style.visibility = VISIBLE; } }; // Provide a soft movement of the tooltip function move(finalX, finalY) { var hidden = (tooltipDiv.style.visibility == HIDDEN), x = hidden ? finalX : (tooltipDiv.offsetLeft + finalX) / 2, y = hidden ? finalY : (tooltipDiv.offsetTop + finalY) / 2; setStyles( tooltipDiv, { left: x + PX, top: y + PX }); // run on next tick of the mouse tracker if (mathAbs(finalX - x) > 1 || mathAbs(finalY - y) > 1) { tooltipTick = function() { move(finalX, finalY); }; } else { tooltipTick = null; } }; function hide() { if (tooltipDiv) tooltipDiv.style.visibility = HIDDEN; if (currentSeries) currentSeries.drawPointState(); }; // public members return { refresh: refresh, hide: hide } }; //--- Run Chart --- // Override defaults for inverted axis /*if (options.chart && options.chart.inverted) defaultOptions = merge(defaultOptions, invertedDefaultOptions);*/ // Make sure the VML canvas manager is initialized. It initializes on doc.ready by default, // which in some cases is too late for Highcharts. if (win.G_vmlCanvasManager) { win.G_vmlCanvasManager.init_(document); } // Pull out the axis options to enable multiple axes defaultXAxisOptions = merge(defaultXAxisOptions, defaultOptions.xAxis); defaultYAxisOptions = merge(defaultYAxisOptions, defaultOptions.yAxis); defaultOptions.xAxis = defaultOptions.yAxis = null; // Handle regular options options = merge(defaultOptions, options); var optionsChart = options.chart; // handle margins var optionsMargin = optionsChart.margin, margin = typeof optionsMargin == 'number' ? [optionsMargin, optionsMargin, optionsMargin, optionsMargin] : optionsMargin, marginTop = margin[0], marginRight = margin[1], marginBottom = margin[2], marginLeft = margin[3], renderTo, renderToClone, container, containerId, chartWidth, chartHeight; // create the container // todo: move this to render and don't render anything to the container before that getContainer(); var chart = this, //container = doc.getElementById(optionsChart.renderTo), //container, chartEvents = optionsChart.events, eventType, imagemap, tooltip, mouseIsDown, backgroundLayer = new Layer('chart-background', container), //chartHeight, //chartWidth, loadingLayer, plotLayer, plotHeight, plotWidth, //ctx, tracker, trackerImage, legend, //xAxis, //yAxis, position = getPosition(container), hasCartesianSeries = optionsChart.showAxes, axes = [], maxTicks, // handle the greatest amount of ticks on grouped axes series = [], resourcesLoaded, plotBackground, inverted, tooltipTick, tooltipDiv; // Set to zero for each new chart colorCounter = 0; symbolCounter = 0; // Update position on resize and scroll addEvent(win, 'resize', updatePosition); // Destroy the chart and free up memory addEvent(win, 'unload', destroy); // Chart event handlers if (chartEvents) for (eventType in chartEvents) { addEvent(chart, eventType, chartEvents[eventType]); } // Chart member functions chart.addLoading = function (loadingId) { chart.resources[loadingId] = false; } chart.clearLoading = function (loadingId) { chart.resources[loadingId] = true; checkResources(); } chart.options = options; chart.series = series; chart.container = container; chart.resources = {}; chart.inverted = inverted = options.chart.inverted chart.chartWidth = chartWidth; chart.chartHeight = chartHeight; chart.plotWidth = plotWidth = chartWidth - marginLeft - marginRight; chart.plotHeight = plotHeight = chartHeight - marginTop - marginBottom; chart.plotLeft = marginLeft; chart.plotTop = marginTop; // API methods chart.redraw = redraw; chart.addSeries = addSeries; chart.getSelectedPoints = getSelectedPoints; chart.getSelectedSeries = getSelectedSeries; chart.showLoading = showLoading; chart.hideLoading = hideLoading; chart.get = get; chart.destroy = destroy; chart.updatePosition = updatePosition; // docs chart.plotLayer = plotLayer = new Layer('plot', container, null, { position: ABSOLUTE, width: plotWidth + PX, height: plotHeight + PX, left: marginLeft + PX, top: marginTop + PX, overflow: HIDDEN, zIndex: 3 }); // Wait for loading of plot area background if (optionsChart.plotBackgroundImage) { chart.addLoading('plotBack'); plotBackground = createElement('img'); plotBackground.onload = function() { chart.clearLoading('plotBack'); } plotBackground.src = optionsChart.plotBackgroundImage; } // Initialize the series //initSeries(); each (options.series || [], function(serieOptions) { initSeries(serieOptions); }); // Mouse tracker must be initiated after series because it depends on inverted chart.tracker = tracker = new MouseTracker(chart, options.tooltip); checkResources(); }; /** * The Point object and prototype. Inheritable and used as base for PiePoint */ var Point = function() {}; Point.prototype = { /** * Initialize the point * @param {Object} series The series object containing this point * @param {Object} options The data in either number, array or object format */ init: function(series, options) { var point = this; point.series = series; point.applyOptions(options); return point; }, /** * Apply the options containing the x and y data and possible some extra properties. * This is called on point init or from point.update. * * @param {Object} options */ applyOptions: function(options) { var point = this, series = point.series, n; // onedimensional array input if (typeof options == 'number' || options === null) { //point.x = i; point.y = options; } // object input else if (typeof options == 'object' && typeof options.length != 'number') { // copy options directly to point //for (n in options) point[n] = options[n]; extend(point, options); point.options = options; // set x and y //point.x = options.x; //point.y = options.y; } // categorized data with name in first position else if (typeof options[0] == 'string') { point.name = options[0]; //point.x = i; point.y = options[1]; } // two-dimentional array else if (typeof options[0] == 'number') { point.x = options[0]; point.y = options[1]; } /* * If no x is set by now, get auto incremented value. All points must have an * x value, however the y value can be null to create a gap in the series */ if (point.x === undefined) point.x = series.autoIncrement(); }, /** * Clear memory */ destroy: function() { var point = this; if (point.stateLayer) point.stateLayer.destroy(); for (prop in point) point[prop] = null; }, /** * Toggle the selection status of a point * @param {Boolean} selected Whether to select or unselect the point. * @param {Boolean} accumulate Whether to add to the previous selection. By default, * this happens if the control key (Cmd on Mac) was pressed during clicking. */ select: function(selected, accumulate) { var point = this, series = point.series, chart = series.chart, stateLayers, state, singlePointLayer = pick(point.stateLayer, series.singlePointLayer, chart.singlePointLayer); //point.selected = !point.selected; // if called without an argument, toggle //series.selected = selected = (selected === undefined) ? !series.selected : selected; point.selected = selected = pick(selected, !point.selected); series.isDirty = true; point.firePointEvent(selected ? 'select' : 'unselect'); // remove the hover marker so the user can see the underlying marker changes to selected if (singlePointLayer) singlePointLayer.clear(); each (chart.series, function (series) { stateLayers = series.stateLayers; // unselect all other points unless Ctrl or Cmd + click if (!accumulate) each (series.data, function(loopPoint) { if (loopPoint.selected && loopPoint != point) { loopPoint.selected = false; fireEvent(loopPoint, 'unselect'); series.isDirty = true; } }); // Just render the series, not the entire chart. Also, don't redraw // with new translation and all. if (series.isDirty) { for (state in stateLayers) { stateLayers[state].clear(); } series.render(); } }) }, /** * Update the point with new options (typically x/y data) and optionally redraw the series. * * @param {Object} options Point options as defined in the series.data array * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call * */ update: function(options, redraw) { var point = this, series = point.series; redraw = pick(redraw, true); // fire the event with a default handler of doing the update point.firePointEvent('update', { options: options }, function() { point.applyOptions(options); // redraw series.isDirty = true; if (redraw) series.chart.redraw(); }); }, /** * Remove a point and optionally redraw the series and if necessary the axes * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call */ remove: function(redraw) { var point = this, series = point.series, chart = series.chart, data = series.data; redraw = pick(redraw, true); // fire the event with a default handler of removing the point point.firePointEvent('remove', null, function() { // loop through the data to locate the point and remove it each(data, function(existingPoint, i) { if (existingPoint == point) { data.splice(i, 1); } }) // pies have separate point layers and legend items if (point.layer) point.layer = point.layer.destroy(); if (point.legendItem) { discardElement(point.legendItem); point.legendItem = null; chart.isDirty = true; } // redraw series.isDirty = true; if (redraw) chart.redraw(); }) }, /** * Fire an event on the Point object. Must not be renamed to fireEvent, as this * causes a name clash in MooTools * @param {String} eventType * @param {Object} eventArgs Additional event arguments * @param {Function} defaultFunction Default event handler */ firePointEvent: function(eventType, eventArgs, defaultFunction) { var point = this, series = this.series, seriesOptions = series.options; // load event handlers on demand to save time on mouseover/out if (seriesOptions.point.events[eventType] || ( point.options && point.options.events && point.options.events[eventType])) this.importEvents(); // add default handler if in selection mode if (eventType == 'click' && seriesOptions.allowPointSelect) defaultFunction = function (event) { // Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera point.select(null, event.ctrlKey || event.metaKey || event.shiftKey); } fireEvent(this, eventType, eventArgs, defaultFunction); }, /** * Import events from the series' and point's options. Only do it on * demand, to save processing time on hovering. */ importEvents: function() { if (!this.hasImportedEvents) { var point = this, options = merge (point.series.options.point, point.options), events = options.events, eventType; point.events = events; for (eventType in events) { addEvent(point, eventType, events[eventType]); } this.hasImportedEvents = true; } }, setTooltipText: function() { var point = this; point.tooltipText = point.series.chart.options.tooltip.formatter.call({ series: point.series, point: point, x: point.category, y: point.y, percentage: point.percentage, total: point.stackTotal }); } }; /** * The base function which all other series types inherit from * @param {Object} chart * @param {Object} options */ var Series = function() { this.isCartesian = true; this.type = 'line'; this.pointClass = Point; }; Series.prototype = { init: function(chart, options) { var series = this, eventType, events, pointEvent, index = chart.series.length; series.chart = chart; options = series.setOptions(options); // merge with plotOptions // set some variables extend (series, { index: index, options: options, name: options.name || 'Series '+ (index + 1), state: '', visible: options.visible !== false, // true by default selected: options.selected == true // false by default }); // register event listeners events = options.events; for (eventType in events) { addEvent(series, eventType, events[eventType]); } series.getColor(); series.getSymbol(); // get the data and go on when the data is loaded series.getData(options); }, getData: function(options) { var series = this, chart = series.chart, loadingId = 'series'+ idCounter++; // Ajax loaded data if (!options.data && options.dataURL) { chart.addLoading(loadingId); getAjax(options.dataURL, function(data) { series.dataLoaded(data); chart.clearLoading(loadingId); }); } else { series.dataLoaded(options.data); } }, dataLoaded: function(data) { var series = this, chart = series.chart, options = series.options, enabledStates = [''], //data = series.data, dataParser = options.dataParser, stateLayers = {}, layerGroup, point, //pointInterval = options.pointInterval || 1, x; // if no dataParser is defined for ajax loaded data, assume JSON and eval the code if (options.dataURL && !dataParser) dataParser = function(data){ return eval(data); } // dataParser is defined, run parsing if (dataParser) data = dataParser.call(series, data); // create the group layer (TODO: move to render?) series.layerGroup = layerGroup = new Layer('series-group', chart.plotLayer.div, null, { zIndex: 2 // labels are underneath }); if (options.states.hover.enabled) enabledStates.push('hover'); each(enabledStates, function(state) { // create the state layers stateLayers[state] = new Layer('state-'+ state, layerGroup.div); }); series.stateLayers = stateLayers; series.setData(data, false); }, /** * Return an auto incremented x value based on the pointStart and pointInterval options. * This is only used if an x value is not given for the point that calls autoIncrement. */ autoIncrement: function() { var series = this, options = series.options, xIncrement = series.xIncrement; xIncrement = pick(xIncrement, options.pointStart, 0); series.pointInterval = pick(series.pointInterval, options.pointInterval, 1); series.xIncrement = xIncrement + series.pointInterval; return xIncrement; }, /** * Sort the data and remove duplicates * * @todo: For reversed x axis, reverse the data once and for all here */ cleanData: function() { var series = this, data = series.data, i; //smallestInterval, //closestPoints, //interval; // sort the data points data.sort(function(a, b){ return (a.x - b.x); }); // remove points with equal x values // record the closest distance for calculation of column widths for (i = data.length - 1; i >= 0; i--) { if (data[i - 1]) { if (data[i - 1].x == data[i].x) data.splice(i - 1, 1); // remove the duplicate /*interval = data[i].x - data[i - 1].x if (smallestInterval === undefined || interval < smallestInterval) { smallestInterval = interval; closestPoints = i; }*/ } } //series.closestPoints = closestPoints; }, /** * Divide the series data into segments divided by null values. Also sort * the data points and delete duplicate values. */ getSegments: function() { var lastNull = -1, segments = [], data = this.data; // create the segments each (data, function(point, i) { if (point.y === null) { if (i > lastNull + 1) segments.push(data.slice(lastNull + 1, i)); lastNull = i; } else if (i == data.length - 1) { // last value segments.push(data.slice(lastNull + 1, i + 1)); } }); this.segments = segments; }, /** * Set the series options by merging from the options tree * @param {Object} options */ setOptions: function(options) { var plotOptions = this.chart.options.plotOptions, options = merge( plotOptions[this.type], plotOptions.series, options ), normalSeriesMarkerOptions = options.marker, hoverSeriesMarkerOptions = options.states.hover.marker; // default hover values are dynamic based on basic state //var stateOptions = seriesOptions.states[state].marker; if (hoverSeriesMarkerOptions.lineWidth === undefined) hoverSeriesMarkerOptions.lineWidth = normalSeriesMarkerOptions.lineWidth + 1; if (hoverSeriesMarkerOptions.radius === undefined) hoverSeriesMarkerOptions.radius = normalSeriesMarkerOptions.radius + 1; //markerOptions = merge(markerOptions, stateOptions); return options; }, getColor: function(){ var defaultColors = this.chart.options.colors; this.color = this.options.color || defaultColors[colorCounter++] || '#0000ff'; if (colorCounter >= defaultColors.length) colorCounter = 0; }, getSymbol: function(){ var defaultSymbols = this.chart.options.symbols, symbol = this.options.marker.symbol || 'auto'; if (symbol == 'auto') symbol = defaultSymbols[symbolCounter++]; this.symbol = symbol; if (symbolCounter >= defaultSymbols.length) symbolCounter = 0; }, /** * Add a point dynamically after chart load time * @param {Object} options Point options as given in series.data * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call * @param {Boolean} shift If shift is true, a point is shifted off the start * of the series as one is appended to the end. */ addPoint: function(options, redraw, shift) { var series = this, data = series.data, point = (new series.pointClass).init(series, options); redraw = pick(redraw, true); data.push(point); if (shift) data.shift(); // redraw series.isDirty = true; if (redraw) series.chart.redraw(); }, /** * Replace the series data with a new set of data * @param {Object} data * @param {Object} redraw */ setData: function(data, redraw) { var series = this; // data.push(point); // if (shift) data.shift(); // generate the point objects //x = options.pointStart || 0; series.xIncrement = null; // reset for new data data = map(splat(data), function(pointOptions) { return (new series.pointClass).init(series, pointOptions); //return new PiePoint(series, pointOptions); //x += pointInterval; //return point; }); // set the data series.data = data; series.cleanData(); series.getSegments(); // redraw series.isDirty = true; if (pick(redraw, true)) series.chart.redraw(); }, /** * Remove a series and optionally redraw the chart * * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call */ remove: function(redraw) { var series = this, chart = series.chart; redraw = pick(redraw, true); if (!series.isRemoving) { /* prevent triggering native event in jQuery (calling the remove function from the remove event) */ series.isRemoving = true; // fire the event with a default handler of removing the point fireEvent(series, 'remove', null, function() { // remove the layer group discardElement(series.layerGroup.div); // remove the area each (series.areas, function(area) { discardElement(area); }); // remove legend item discardElement(series.legendItem); series.legendItem = null; // loop through the chart series to locate the series and remove it each(chart.series, function(existingSeries, i) { if (existingSeries == series) { chart.series.splice(i, 1); } }) // redraw chart.isDirty = true; if (redraw) chart.redraw(); }) } series.isRemoving = false }, /** * Translate data points from raw values 0 and 1 to x and y. */ translate: function() { var chart = this.chart, series = this, stacking = series.options.stacking, categories = series.xAxis.categories, yAxis = series.yAxis, stack = yAxis.stacks[series.type]; // do the translation each(this.data, function(point) { var xValue = point.x, yValue = point.y, yBottom, pointStack, pointStackTotal; point.plotX = series.xAxis.translate(point.x); // calculate the bottom y value for stacked series if (stacking && series.visible && stack[xValue]) { pointStack = stack[xValue]; pointStackTotal = pointStack.total; pointStack.cum = yBottom = pointStack.cum - yValue; // start from top yValue = yBottom + yValue; if (stacking == 'percent') { yBottom = pointStackTotal ? yBottom * 100 / pointStackTotal : 0; yValue = pointStackTotal ? yValue * 100 / pointStackTotal : 0; } point.percentage = pointStackTotal ? point.y * 100 / pointStackTotal : 0; point.stackTotal = pointStackTotal; point.yBottom = yAxis.translate(yBottom, 0, 1); } // set the y value if (yValue !== null) point.plotY = yAxis.translate(yValue, 0, 1); // set client related positions for mouse tracking point.clientX = chart.inverted ? chart.plotHeight - point.plotX + chart.plotTop : point.plotX + chart.plotLeft; // for mouse tracking // some API data point.category = categories && categories[point.x] !== undefined ? categories[point.x] : point.x; }); }, /** * Memoize tooltip texts and positions */ setTooltipPoints: function (renew) { var series = this, chart = series.chart, inverted = chart.inverted, //concatenated = [], data = [], plotSize = inverted ? chart.plotHeight : chart.plotWidth, low, high, tooltipPoints = []; // a lookup array for each pixel in the x dimension // renew if (renew) series.tooltipPoints = null; // concat segments to overcome null values each (series.segments, function(segment){ data = data.concat(segment); }); // loop the concatenated data and apply each point to all the closest // pixel positions if (series.xAxis.reversed) data = data.reverse();//reverseArray(data); each (data, function(point, i) { if (!series.tooltipPoints) // only create the text the first time, not on zoom point.setTooltipText(); low = data[i - 1] ? data [i - 1].high + 1 : 0; high = point.high = data[i + 1] ? ( mathFloor((point.plotX + (data[i + 1] ? data[i + 1].plotX : plotSize)) / 2)) : plotSize; while (low <= high) { tooltipPoints[inverted ? plotSize - low++ : low++] = point; } }); series.tooltipPoints = tooltipPoints; }, /** * Draw the actual graph */ drawLine: function(state) { var i, j, series = this, options = series.options, chart = series.chart, doAnimation = options.animation && series.animate, layer = series.stateLayers[state], data = series.data, color = options.lineColor || series.color, fillColor = options.fillColor == 'auto' ? Color(series.color).setOpacity(options.fillOpacity || 0.75).get() : options.fillColor, inverted = chart.inverted, y0 = (inverted ? 0 : chart.plotHeight) - series.yAxis.translate(0); // get state options if (state) options = merge(options, options.states[state]); // initiate the animation if (doAnimation) series.animate(true); // divide into segments each(series.segments, function(segment){ var line = [], area = []; // get the points each(segment, function(point, i) { if (i && options.step) { var lastPoint = segment[i - 1]; line.push ( inverted ? chart.plotWidth - lastPoint.plotY : point.plotX, inverted ? chart.plotHeight - point.plotX : lastPoint.plotY ); } line.push( inverted ? chart.plotWidth - point.plotY : point.plotX, inverted ? chart.plotHeight - point.plotX : point.plotY ); }); // draw the area if (/area/.test(series.type)) { for (i = 0; i < line.length; i++) area.push(line[i]); if (options.stacking && series.type != 'areaspline') { // follow stack back. Todo: implement areaspline for (i = segment.length - 1; i >= 0; i--) area.push(segment[i].plotX, segment[i].yBottom); } else { // follow zero line back area.push( inverted ? y0 : segment[segment.length - 1].plotX, inverted ? chart.plotHeight - segment[segment.length - 1].plotX : y0, inverted ? y0 : segment[0].plotX, inverted ? chart.plotHeight - segment[0].plotX : y0 ); } layer.drawPolyLine(area, null, null, options.shadow, fillColor); } // draw the line if (options.lineWidth) layer.drawPolyLine(line, color, options.lineWidth, options.shadow); }); // experimental animation if (doAnimation) series.animate(); }, /** * Experimental animation */ animate: function(init) { var series = this, chart = series.chart, inverted = chart.inverted, div = series.layerGroup.div; if (series.visible) { if (init) { // initialize the animation setStyles ( div, extend( { overflow: HIDDEN }, inverted ? { height: 0 } : { width: 0 } ) ); } else { // run the animation animate( div, inverted ? { height: chart.plotHeight + PX } : { width: chart.plotWidth + PX }, { duration: 1000 } ); // delete this function to allow it only once this.animate = null; } } }, /** * Draw the markers */ drawPoints: function(state){ var series = this, i, //state = series.state, layer = series.stateLayers[state], seriesOptions = series.options, markerOptions = seriesOptions.marker, data = series.data, chart = series.chart, inverted = chart.inverted; /*if (state) { // default hover values are dynamic based on basic state var stateOptions = seriesOptions.states[state].marker; if (stateOptions.lineWidth === undefined) stateOptions.lineWidth = markerOptions.lineWidth + 1; if (stateOptions.radius === undefined) stateOptions.radius = markerOptions.radius + 1; markerOptions = merge(markerOptions, stateOptions); }*/ if (markerOptions.enabled) { each(data, function(point){ if (point.plotY !== undefined) series.drawMarker( layer, inverted ? chart.plotWidth - point.plotY : point.plotX, inverted ? chart.plotHeight - point.plotX : point.plotY, merge(markerOptions, point.marker) ); // draw the selected mode marker on top of the default one if (point.selected) series.drawPointState(point, 'select', layer); }); } }, /** * Some config objects, like marker, have a state value that depends on the base value * @param {Object} props */ /*getDynamicStateValues: function(base, state, props) { each (props, function(value, key) { if (state[key] === undefined) state[key] = base[key] + value; }); return state; },*/ /** * Draw a single marker into a given layer and position */ drawMarker: function(layer, x, y, options) { if (options.lineColor == 'auto') options.lineColor = this.color; if (options.fillColor == 'auto') options.fillColor = this.color; if (options.symbol == 'auto') options.symbol = this.symbol; layer.drawSymbol( options.symbol, x, y, options.radius, options.lineWidth, options.lineColor, options.fillColor ); }, /** * Draw the data labels */ drawDataLabels: function() { if (this.options.dataLabels.enabled) { var series = this, i, x, y, data = series.data, options = series.options.dataLabels, color, str, dataLabelsLayer = series.dataLabelsLayer, chart = series.chart, inverted = chart.inverted, seriesType = series.type, isPie = (seriesType == 'pie'), align; // create a separate layer for the data labels if (dataLabelsLayer) { dataLabelsLayer.clear(); } else { series.dataLabelsLayer = dataLabelsLayer = new Layer('data-labels', series.layerGroup.div, null, { zIndex: 1 }); } // determine the color options.style.color = options.color == 'auto' ? series.color : options.color; // make the labels for each point each(data, function(point){ var plotX = point.plotX, plotY = point.plotY, tooltipPos = point.tooltipPos; str = options.formatter.call({ x: point.x, y: point.y, series: series, point: point }); x = (inverted ? chart.plotWidth - plotY : plotX) + options.x; y = (inverted ? chart.plotHeight - plotX : plotY) + options.y; // special case for pies if (tooltipPos) { x = tooltipPos[0] + options.x; y = tooltipPos[1] + options.y; } // special for pies if (isPie) { if (!point.dataLabelsLayer) point.dataLabelsLayer = new Layer('data-labels', point.layer.div, null, { zIndex: 3} ); dataLabelsLayer = point.dataLabelsLayer; } // in columns, align the string to the column align = options.align; if (seriesType == 'column') x += { center: point.w / 2, right: point.w }[align] || 0; if (str) dataLabelsLayer[isPie ? 'drawText' : 'addText']( str, x, y, options.style, options.rotation, align ); }); if (!isPie) dataLabelsLayer.strokeText(); // only draw once - todo: different labels in different states and single point label? //series.hasDrawnDataLabels = true; } }, /** * Draw a single point in a specific state */ drawPointState: function(point, state, layer){ var chart = this.chart, inverted = chart.inverted, isHoverState = state == 'hover', layer = layer || chart.singlePointLayer, options = this.options, stateOptions; // a specific layer for the currently active point if (isHoverState) { if (!layer) layer = chart.singlePointLayer = new Layer( 'single-point', chart.plotLayer.div, null, { zIndex: 3 } ); layer.clear(); } if (state) { // merge series hover marker and marker hover marker var seriesStateOptions = options.states[state].marker, pointStateOptions = options.marker.states[state]; // the default for hover points is two more than normal radius if (isHoverState && pointStateOptions.radius === undefined) pointStateOptions.radius = seriesStateOptions.radius + 2; // merge all options stateOptions = merge( options.marker, point.marker, seriesStateOptions, pointStateOptions ); // draw the marker if (stateOptions && stateOptions.enabled) this.drawMarker( layer, inverted ? chart.plotWidth - point.plotY : point.plotX, inverted ? chart.plotHeight - point.plotX : point.plotY, stateOptions ); } }, /** * Clear DOM objects and free up memory */ destroy: function() { var series = this, prop; each (series.data, function(point) { point.destroy(); }); for (prop in series) series[prop] = null; }, /** * Render the graph and markers */ render: function() { var series = this, state, stateLayers = series.stateLayers; series.drawDataLabels(); if (series.visible) for (state in stateLayers) { series.drawLine(state); series.drawPoints(state); } else series.setVisible(false, false); // initially hide other states than normal if (!series.hasRendered && stateLayers.hover) { stateLayers.hover.hide(); hasRendered = true; } series.isDirty = false; // means data is in accordance with what you see }, /** * Redraw the series after an update in the axes. */ redraw: function() { var series = this; series.translate(); series.setTooltipPoints(true); /*if (series.chart.options.tooltip.enabled) */series.createArea(); series.clear(); series.render(); }, /** * Clear all graphics and HTML from the series layer group */ clear: function(){ var stateLayers = this.stateLayers; for (var state in stateLayers) { stateLayers[state].clear(); stateLayers[state].cleared = true; } if (this.dataLabelsLayer) { this.dataLabelsLayer.clear(); this.hasDrawnDataLabels = false; } }, /** * Set the state of the graph and redraw */ setState: function(state) { state = state || ''; if (this.state != state) { var series = this, stateLayers = series.stateLayers, newStateLayer = stateLayers[state], oldStateLayer = stateLayers[series.state], singlePointLayer = series.singlePointLayer || series.chart.singlePointLayer; series.state = state; if (newStateLayer) { // if not, hover state is disabled if (state) newStateLayer.show(); else { if (oldStateLayer) oldStateLayer.hide(); if (singlePointLayer) singlePointLayer.clear(); } } } }, /** * Set the visibility of the graph * * @param vis {Boolean} True to show the series, false to hide. If undefined, * the visibility is toggled. */ setVisible: function(vis, redraw) { var series = this, chart = series.chart, //imagemap = chart.imagemap, layerGroup = series.layerGroup, legendItem = series.legendItem, areas = series.areas, oldVisibility = series.visible; // if called without an argument, toggle visibility series.visible = vis = vis === undefined ? !oldVisibility : vis; if (vis) { series.isDirty = true; // when series is initially hidden layerGroup.show(); } else { layerGroup.hide(); } if (legendItem) { legendItem.className = vis ? '' : HIGHCHARTS_HIDDEN; chart.legend.drawGraphics(true); } // hide or show areas if (areas) each (areas, function(area) { if (vis) //imagemap.insertBefore(area, imagemap.childNodes[1]); chart.tracker.insertAtFront(area); else discardElement(area); }); // rescale if (chart.options.chart.ignoreHiddenSeries) { // in a stack, all other series are affected if (series.options.stacking) each (chart.series, function(otherSeries) { if (otherSeries.options.stacking && otherSeries.visible) otherSeries.isDirty = true; }); } if (redraw !== false) chart.redraw(); fireEvent(series, vis ? 'show' : 'hide'); }, /** * Show the graph */ show: function() { this.setVisible(true); }, /** * Hide the graph */ hide: function() { this.setVisible(false); }, /** * Set the selected state of the graph * * @param selected {Boolean} True to select the series, false to unselect. If * undefined, the selection state is toggled. */ select: function(selected) { var series = this; // if called without an argument, toggle series.selected = selected = (selected === undefined) ? !series.selected : selected; if (series.checkbox) series.checkbox.checked = selected; fireEvent(series, selected ? 'select' : 'unselect'); }, /** * Calculate the mouseover area coordinates for a given data series */ getAreaCoords: function(){ var data = this.data, series = this, datas = [], chart = this.chart, inverted = chart.inverted, plotWidth = chart.plotWidth, plotHeight = chart.plotHeight, reversedXAxis = series.xAxis.reversed, reversedData, snap = chart.options.tooltip.snap, dataIsReverse, i = 0, ret = []; each(series.splinedata || series.segments, function(data, i) { //if (reversedXAxis) data.reverse();//reverseArray(data); // Reverse the data in case of a reversed x axis. Spline data // is already reversed at this point, so we need to actually // inspect the data x values. reversedData = data.length > 1 && data[0].x > data[1].x; if (reversedData && !reversedXAxis || reversedXAxis && !reversedData) { data = data.reverse(); } var coords = [], outlineTop = [], outlineBottom = []; each([outlineTop, outlineBottom], function(outline){ var last = 0, i = 0, extreme, slice, peaks = [data[0]], // add the first point at init sign = outline == outlineTop ? 1 : -1, intersects, num, x, y, lastX, lastY, x1, y1, x2, y2, dX, dY, pX, pY, l, factor, p1, p2, mA, mB, iX, iY, area; // pull out the highest and lowest peaks in slices of {snap} width, // push these peaks into the peaks array. while (data[i]) { if (data[i].plotX > data[last].plotX + snap || i == data.length - 1) { extreme = data[i]; slice = data.slice(last, i - 1); each(slice, function(point) { if (sign * point.plotY < sign * extreme.plotY) { extreme = point; } }); // push it only if we have moved on along the x axis if (mathRound(data[last].plotX) < mathRound(extreme.plotX) || data[i].plotX > data[last].plotX + snap) { peaks.push(extreme); } last = i; } i++; } // push the last point if (peaks[peaks.length - 1] != data[data.length-1]) peaks.push(data[data.length - 1]); // loop through the peaks and calculate rectangles {snap} pixels // away from the peaks. //peaks = data; for (i = 0; i < peaks.length; i++) { // clickable area if (i > 0) { // vector from last point to this x = peaks[i].plotX; y = peaks[i].plotY; lastX = peaks[i - 1].plotX; lastY = peaks[i - 1].plotY; dX = x - peaks[i - 1].plotX; dY = y - peaks[i - 1].plotY; // perpendicular vector pX = dY; pY = -dX; // length of the perpendicular vector l = math.sqrt(math.pow(pX, 2) + math.pow(pY, 2)); // extend the line by {snap} pixels if (i == 1) { lastX -= (snap / l) * dX; lastY -= (snap / l) * dY; } else if (i == peaks.length - 1) { x += (snap / l) * dX; y += (snap / l) * dY; } // factors compared to snap factor = sign * snap / l; // incremental calculation of the top and bottom line // the new upper parallel vector x1 = mathRound(lastX + factor * pX); y1 = mathRound(lastY + factor * pY); x2 = mathRound(x + factor * pX); y2 = mathRound(y + factor * pY); // loop back until this line intersects a previous line if (outline[outline.length - 1] && outline[outline.length - 1][0] > x1) { intersects = false; while (!intersects) { p2 = outline.pop(); p1 = outline[outline.length - 1]; if (!p1) break; // get intersection point // http://www.ultrashock.com/forums/showthread.php?t=81785 mA = (y1 - y2) / (x1 - x2); mB = (p1[1] - p2[1]) / (p1[0] - p2[0]); iX = ((-mB * p1[0]) + p1[1] + (mA * x1) - y1) / (mA - mB); iY = (mA * (iX - x1)) + y1; if (iX > p1[0]) { outline.push([mathRound(iX), mathRound(iY), 1]); intersects = true; } } } else { if (!isNaN(x1)) outline.push([x1, y1]); } if (outline[outline.length - 1] && outline[outline.length - 1][0] < x2) outline.push([x2, y2]); } } }); // area for detecting moveover for (i = 0; i < outlineTop.length; i++) { // top of the area coords.push( inverted ? plotWidth - outlineTop[i][1] : outlineTop[i][0], inverted ? plotHeight - outlineTop[i][0] : outlineTop[i][1] ); } for (i = outlineBottom.length - 1; i >= 0; i--) { // bottom of the area coords.push( inverted ? plotWidth - outlineBottom[i][1] : outlineBottom[i][0], inverted ? plotHeight - outlineBottom[i][0] : outlineBottom[i][1] ); } // single point: make circle if (!coords.length) { coords.push(mathRound(data[0].plotX), mathRound(data[0].plotY)); } // visualize //series.stateLayers[''].drawPolyLine(coords, '#afaf00', 1); if (coords.length) ret.push([coords.join(',')]); // undo reverse //if (reversedXAxis) data.reverse(); }); return ret; }, createArea: function() { if (this.options.enableMouseTracking === false) return; var area, series = this, options = series.options, chart = series.chart, inverted = chart.inverted, tracker = chart.tracker, //cursor = series.options.cursor, coordsArray = series.getAreaCoords(), firstArea, seriesAreas = [], existingAreas = series.areas, isCircle; // remove old areas in case of updating if (existingAreas) each (existingAreas, function(area) { discardElement(area); }) // create each area each(coordsArray, function(coords) { isCircle = /^[0-9]+,[0-9]+$/.test(coords[0]); area = createElement('area', { shape: isCircle ? 'circle' : 'poly', chart: chart, coords: coords[0] + (isCircle ? ','+ chart.options.tooltip.snap : ''), onmouseover: function(e) { if (!series.visible || chart.mouseIsDown) return; var hoverSeries = chart.hoverSeries; // for column/scatterplots, register that we entered a new column // coords[1] contains the reference to a point - if // no such reference is given, the area refers to // a series chart.hoverPoint = coords[1]; // trigger the event, but to save processing time, // only if defined if (options.events.mouseOver) { fireEvent(series, 'mouseOver', { point: chart.hoverPoint }); } // set normal state to previous series if (hoverSeries && hoverSeries != series) hoverSeries.setState(); // bring to front if (!/(column|bar|pie)/.test(series.type)) { //imagemap.insertBefore(this, imagemap.childNodes[1]); tracker.insertAtFront(area); } // hover this series.setState('hover'); chart.hoverSeries = series; }, onmouseout: function() { // trigger the event only if listeners exist var hoverSeries = chart.hoverSeries; if (hoverSeries && options.events.mouseOut) { fireEvent(hoverSeries, 'mouseOut'); } } }); // add a href to make the cursor appear - simply adding // the style is not enough for IE. if (options.cursor == 'pointer') { area.href = 'javascript:;'; /*if (options.allowDrag) { setStyles(area, { cursor: { 'x': inverted ? 'ns-resize' : 'ew-resize', 'xy': 'move', 'y': inverted ? 'ew-resize' : 'ns-resize' }[options.dragType] }); }*/ } // insert latest on top tracker.insertAtFront(area); seriesAreas.push(area); }); series.areas = seriesAreas; } }; // end Series prototype /** * LineSeries object */ var LineSeries = extendClass(Series); seriesTypes.line = LineSeries; /** * AreaSeries object */ var AreaSeries = extendClass(Series, { type: 'area' }); seriesTypes.area = AreaSeries; /** * SplineSeries object */ var SplineSeries = extendClass( Series, { type: 'spline', /** * Translate the points and get the spline data */ translate: function() { var series = this; // do the partent translate Series.prototype.translate.apply(series, arguments); // get the spline data series.splinedata = series.getSplineData(); }, /** * Draw the actual spline line with interpolated values * @param {Object} state */ drawLine: function(state) { var series = this, realSegments = series.segments; // temporarily set the segments to reflect the spline series.segments = series.splinedata;// || series.getSplineData(); // draw the line Series.prototype.drawLine.apply(series, arguments); // reset the segments series.segments = realSegments; }, /** * Get interpolated spline values */ getSplineData: function() { var series = this, chart = series.chart, //data = this.data, splinedata = [], num; each (series.segments, function(data) { if (series.xAxis.reversed) data = data.reverse();//reverseArray(data); var croppedData = [], nextUp, nextDown; // to save calculations, only add data within the plot each (data, function(point, i) { nextUp = data[i+2] || data[i+1] || point; nextDown = data[i-2] || data[i-1] || point; if (nextUp.plotX > 0 && nextDown.plotY < chart.plotWidth) { croppedData.push(point); } }); // 3px intervals: if (croppedData.length > 1) { num = mathRound(mathMax(chart.plotWidth, croppedData[croppedData.length-1].clientX - croppedData[0].clientX) / 3); } splinedata.push ( data.length > 1 ? // if the data.length is one, it's a single point so we can't spline it num ? (new SplineHelper(croppedData)).get(num) : [] : data ); }); series.splinedata = splinedata; return splinedata; } }); seriesTypes.spline = SplineSeries; /** * Calculate the spine interpolation. * * @todo: Implement true Bezier curves like shown at http://www.math.ucla.edu/~baker/java/hoefer/Spline.htm */ function SplineHelper (data) { var xdata = []; var ydata = []; for (var i = 0; i < data.length; i++) { xdata[i] = data[i].plotX; ydata[i] = data[i].plotY; } this.xdata = xdata; this.ydata = ydata; var delta = []; this.y2 = []; var n = ydata.length; this.n = n; // Natural spline 2:derivate == 0 at endpoints this.y2[0] = 0.0; this.y2[n-1] = 0.0; delta[0] = 0.0; // Calculate 2:nd derivate for(var i=1; i < n-1; i++) { var d = (xdata[i+1]-xdata[i-1]); /*if( d == 0 ) { error: ('Invalid input data for spline. Two or more consecutive input X-values are equal. Each input X-value must differ since from a mathematical point of view it must be a one-to-one mapping, i.e. each X-value must correspond to exactly one Y-value.'); }*/ var s = (xdata[i]-xdata[i-1])/d; var p = s*this.y2[i-1]+2.0; this.y2[i] = (s-1.0)/p; delta[i] = (ydata[i+1]-ydata[i])/(xdata[i+1]-xdata[i]) - (ydata[i]-ydata[i-1])/(xdata[i]-xdata[i-1]); delta[i] = (6.0*delta[i]/(xdata[i+1]-xdata[i-1])-s*delta[i-1])/p; } // Backward substitution for(var j=n-2; j >= 0; j-- ) { this.y2[j] = this.y2[j]*this.y2[j+1] + delta[j]; } }; SplineHelper.prototype = { // Return the two new data vectors get: function(num) { if (!num) num = 50; var n = this.n ; var step = (this.xdata[n-1]-this.xdata[0]) / (num-1); var xnew=[]; var ynew=[]; xnew[0] = this.xdata[0]; ynew[0] = this.ydata[0]; var data = [{ plotX: xnew[0], plotY: ynew[0] }];//[[xnew[0], ynew[0]]]; for(var j = 1; j < num; j++ ) { xnew[j] = xnew[0]+j*step; ynew[j] = this.interpolate(xnew[j]); data[j] = { plotX: xnew[j], plotY: ynew[j] };//[xnew[j], ynew[j]]; } return data; }, // Return a single interpolated Y-value from an x value interpolate: function(xpoint) { var max = this.n-1; var min = 0; // Binary search to find interval while( max-min > 1 ) { var k = (max+min) / 2; if( this.xdata[mathFloor(k)] > xpoint ) max=k; else min=k; } var intMax = mathFloor(max), intMin = mathFloor(min); // Each interval is interpolated by a 3:degree polynom function var h = this.xdata[intMax]-this.xdata[intMin]; /*if( h == 0 ) { error: ('Invalid input data for spline. Two or more consecutive input X-values are equal. Each input X-value must differ since from a mathematical point of view it must be a one-to-one mapping, i.e. each X-value must correspond to exactly one Y-value.'); }*/ var a = (this.xdata[intMax]-xpoint)/h; var b = (xpoint-this.xdata[intMin])/h; return a*this.ydata[intMin]+b*this.ydata[intMax]+ ((a*a*a-a)*this.y2[intMin]+(b*b*b-b)*this.y2[intMax])*(h*h)/6.0; } }; /** * AreaSplineSeries object */ var AreaSplineSeries = extendClass(SplineSeries, { type: 'areaspline' }); seriesTypes.areaspline = AreaSplineSeries /** * ColumnSeries object */ var ColumnSeries = extendClass(Series, { type: 'column', init: function() { Series.prototype.init.apply(this, arguments); var series = this, chart = series.chart; // record number of column series to calculate column width //if (!series.options.stacking) series.countColumn = true; // if the series is added dynamically, force redraw of other // series affected by a new column if (chart.hasRendered) each (chart.series, function(otherSeries) { if (otherSeries.type == series.type) otherSeries.isDirty = true; }); }, translate: function() { var series = this, chart = series.chart, columnCount = 0, stackedIndex; // the index of the first column in a stack Series.prototype.translate.apply(series); // Get the total number of column type series. // This is called on every series. Consider moving this logic to a // chart.orderStacks() function and call it on init, addSeries and removeSeries each (chart.series, function(otherSeries) { if (otherSeries.type == series.type) { if (!otherSeries.options.stacking) { otherSeries.columnIndex = columnCount++; } else { if (!defined(stackedIndex)) stackedIndex = columnCount++; otherSeries.columnIndex = stackedIndex; } } }); // calculate the width and position of each column based on // the number of column series in the plot, the groupPadding // and the pointPadding options var options = series.options, data = series.data, inverted = chart.inverted, plotWidth = chart.plotWidth, plotHeight = chart.plotHeight, closestPoints = series.closestPoints, categoryWidth = mathAbs( data[1] ? data[closestPoints].plotX - data[closestPoints - 1].plotX : (inverted ? plotHeight : plotWidth) ), groupPadding = categoryWidth * options.groupPadding, groupWidth = categoryWidth - 2 * groupPadding, pointOffsetWidth = groupWidth / columnCount, optionPointWidth = options.pointWidth, pointPadding = defined(optionPointWidth) ? (pointOffsetWidth - optionPointWidth) / 2 : pointOffsetWidth * options.pointPadding, pointWidth = pick(optionPointWidth, pointOffsetWidth - 2 * pointPadding), columnIndex = (chart.options.xAxis.reversed ? columnCount - series.columnIndex : series.columnIndex) || 0, pointX = -(categoryWidth / 2) + groupPadding + columnIndex * pointOffsetWidth + pointPadding, //pointY0 = plotWidth - chart.xAxis.translate(0), translatedY0 = series.yAxis.translate(0); // record the new values each (data, function(point) { point.plotX += pointX; point.w = pointWidth; point.y0 = (inverted ? plotWidth : plotHeight) - translatedY0; point.h = (point.yBottom || point.y0) - point.plotY; }); }, drawLine: function() { }, getSymbol: function(){ }, drawPoints: function(state) { var series = this, options = series.options, chart = series.chart, doAnimation = options.animation && series.animate, plot = chart.plot, inverted = chart.inverted, data = series.data, layer = series.stateLayers[state]; // make ready for animation if (doAnimation) this.animate(true); // draw the columns // todo: record the rectangle coordinates and reuse them for the mouseover area each (data, function(point) { if (point.plotY !== undefined) layer.drawRect( inverted ? (point.h >= 0 ? chart.plotWidth - point.plotY - point.h : chart.plotWidth - point.plotY) : point.plotX, inverted ? chart.plotHeight - point.plotX - point.w : (point.h >= 0 ? point.plotY : point.plotY + point.h), // for negative bars, subtract h (Opera) inverted ? mathAbs(point.h) : point.w, inverted ? point.w : mathAbs(point.h), options.borderColor, options.borderWidth, options.borderRadius, point.color || series.color, options.shadow ); // draw the selected mode marker on top of the default one if (point.selected) series.drawPointState(point, 'select', layer); }); if (doAnimation) series.animate(); }, /** * Draw a single point in hover state */ drawPointState: function(point, state, layer) { // local vars var series = this, chart = series.chart, seriesOptions = series.options, pointOptions = point ? point.options : null, plot = chart.plot, inverted = chart.inverted, //singlePointLayer = series.singlePointLayer; layer = layer || series.singlePointLayer; // use one layer each series as opposed to the chartwide singlePointLayer for line-type series. /*if (!singlePointLayer) singlePointLayer = series.singlePointLayer = new Layer( 'single-point-layer', series.layerGroup.div ); singlePointLayer.clear();*/ // a specific layer for the currently active point if (state == 'hover') { if (!layer) layer = series.singlePointLayer = new Layer( 'single-point', series.layerGroup.div ); layer.clear(); } // draw the column if (state && this.options.states[state]) { var options = merge( seriesOptions, seriesOptions.states[state], pointOptions ); layer.drawRect( inverted ? chart.plotWidth - point.plotY - point.h : point.plotX, inverted ? chart.plotHeight - point.plotX - point.w : point.plotY, inverted ? point.h : point.w, inverted ? point.w : point.h, options.borderColor, options.borderWidth, options.borderRadius, Color(options.color || this.color).brighten(options.brightness).get(), options.shadow ) } }, getAreaCoords: function() { var areas = [], chart = this.chart, inverted = chart.inverted; each (this.data, function(point) { var pointH = mathMax(mathAbs(point.h), 3) * (point.h < 0 ? -1 : 1), x1 = inverted ? chart.plotWidth - point.plotY - pointH : point.plotX, y2 = inverted ? chart.plotHeight - point.plotX - point.w : point.plotY, y1 = y2 + (inverted ? point.w : pointH), x2 = x1 + (inverted ? pointH : point.w); // make sure tightly packed colums can receive mouseover if (!inverted && mathAbs(x2 - x1) < 1) x2 = x1 + 1; else if (inverted && mathAbs(y2 - y1) < 1) y2 = y1 + 1; // push an array containing the coordinates and the point areas.push([ map([ x1, y1, x1, y2, x2, y2, x2, y1 ], mathRound).join(','), point ]); }); return areas; }, cleanData: function() { var series = this, data = series.data, interval, smallestInterval, closestPoints, i; // apply the parent method Series.prototype.cleanData.apply(series); // find the closes pair of points for (i = data.length - 1; i >= 0; i--) { if (data[i - 1]) { interval = data[i].x - data[i - 1].x; if (smallestInterval === undefined || interval < smallestInterval) { smallestInterval = interval; closestPoints = i; } } } series.closestPoints = closestPoints; }, animate: function(init) { var series = this, chart = series.chart, inverted = chart.inverted, div = series.layerGroup.div, dataLabelsLayer = series.dataLabelsLayer; if (init) { // initialize the animation div.style[inverted ? 'left' : 'top'] = (inverted ? -chart.plotWidth : chart.plotHeight) + PX; } else { // run the animation animate(div, chart.inverted ? { left: 0 } : { top: 0 }); // delete this function to allow it only once series.animate = null; } }, /** * Remove this series from the chart */ remove: function() { var series = this, chart = series.chart; // column and bar series affects other series of the same type // as they are either stacked or grouped if (chart.hasRendered) each (chart.series, function(otherSeries) { if (otherSeries.type == series.type) otherSeries.isDirty = true; }); Series.prototype.remove.apply(series, arguments); } }); seriesTypes.column = ColumnSeries; var BarSeries = extendClass(ColumnSeries, { type: 'bar', init: function(chart) { chart.inverted = this.inverted = true; ColumnSeries.prototype.init.apply(this, arguments); } }); seriesTypes.bar = BarSeries; /** * The scatter series class */ var ScatterSeries = extendClass(Series, { type: 'scatter', /** * Calculate the mouseover area coordinates for a given data series */ getAreaCoords: function () { var data = this.data, coords, ret = []; each (data, function(point) { // create a circle for each point ret.push([[mathRound(point.plotX), mathRound(point.plotY)].join(','), point]); }); return ret; }, /** * Cleaning the data is not necessary in a scatter plot */ cleanData: function() {} }); seriesTypes.scatter = ScatterSeries; /** * Extended point object for pies */ var PiePoint = extendClass(Point, { setState: function(state) { this.series.drawPointState(this, state); }, init: function () { Point.prototype.init.apply(this, arguments); var point = this, series = point.series, defaultColors = series.chart.options.colors, toggleSlice; //visible: options.visible !== false, extend(point, { visible: point.visible !== false, name: pick(point.name, 'Slice'), color: point.color || defaultColors[colorCounter++] }); // loop back to zero if (colorCounter >= defaultColors.length) colorCounter = 0; // create an individual layer if (!point.layer) point.layer = new Layer('pie', series.layerGroup.div); // add event listener for select toggleSlice = function() { point.slice(); } addEvent(point, 'select', toggleSlice); addEvent(point, 'unselect', toggleSlice); return point; }, setVisible: function(vis) { var point = this, layer = point.layer, legendItem = point.legendItem; // if called without an argument, toggle visibility point.visible = vis = vis === undefined ? !point.visible : vis; if (vis) layer.show(); else layer.hide(); if (legendItem) { legendItem.className = vis ? '' : HIGHCHARTS_HIDDEN; point.series.chart.legend.drawGraphics(true); } }, /** * Set or toggle whether the slice is cut out from the pie * @param {Boolean} sliced When undefined, the slice state is toggled * @param {Boolean} redraw Whether to redraw the chart. True by default. */ slice: function(sliced, redraw) { var point = this, series = point.series; // redraw is true by default redraw = pick(redraw, true); // if called without an argument, toggle point.sliced = defined(sliced) ? sliced : !point.sliced; series.isDirty = true; if (redraw) series.chart.redraw(); } }); /** * The Pie series class */ var PieSeries = extendClass(Series, { type: 'pie', isCartesian: false, pointClass: PiePoint, getColor: function() { // pie charts have a color each point }, translate: function() { var sum = 0, series = this, cumulative = -0.25, // start at top options = series.options, slicedOffset = options.slicedOffset, positions = options.center, size = options.size, chart = series.chart, data = series.data, circ = 2 * math.PI, fraction; // get positions - either an integer or a percentage string must be given positions.push(options.size); positions = map (positions, function(length, i) { return /%$/.test(length) ? // i == 0: centerX, relative to width // i == 1: centerY, relative to height // i == 2: size, relative to height chart['plot' + (i ? 'Height' : 'Width')] * parseInt(length) / 100: length; }); // get the total sum each (data, function(point) { sum += point.y; }); each (data, function(point) { // set start and end angle fraction = sum ? point.y / sum : 0 point.start = cumulative * circ; cumulative += fraction; point.end = cumulative * circ; point.percentage = fraction * 100; // set size and positions point.center = [positions[0], positions[1]]; point.size = positions[2]; // center for the sliced out slice var angle = (point.end + point.start) / 2; point.centerSliced = map([ mathCos(angle) * slicedOffset + positions[0], mathSin(angle) * slicedOffset + positions[1] ], mathRound); }); this.setTooltipPoints(); }, /** * Render the graph and markers */ render: function() { //if (!this.pointsDrawn) this.drawPoints(); this.drawDataLabels(); }, /** * Draw the data points * @param {Object} state The state of this graph */ drawPoints: function(state) { var series = this; // draw the slices each (this.data, function(point) { series.drawPoint(point, point.layer.getCtx(), point.color); if (point.visible === false) point.setVisible(false); // draw the selected mode marker on top of the default one if (point.selected) series.drawPointState(point, 'select', point.layer); //if (point.sliced) this.slice(point); }); //series.pointsDrawn = true; }, getSymbol: function(){ }, /** * Draw a single point in hover state */ drawPointState: function(point, state, layer) { var series = this, seriesOptions = series.options; if (point) { // drawPointState can be called without arguments to clear states // create a special state layer nested in this point's main layer /*stateLayer = point.stateLayer; if (!stateLayer) stateLayer = point.stateLayer = new Layer('state-layer', point.layer.div); stateLayer.clear();*/ // a specific layer for the currently active point layer = layer || point.stateLayer; if (state == 'hover') { if (!layer) layer = point.stateLayer = new Layer( 'single-point', point.layer.div ); layer.clear(); } // draw the point if (state && series.options.states[state]) { var options = merge(seriesOptions, seriesOptions.states[state]); this.drawPoint( point, layer.getCtx(), options.color || point.color, options.brightness ); } } // clear the old point on register the new one if (series.hoverPoint && series.hoverPoint.stateLayer) series.hoverPoint.stateLayer.clear(); series.hoverPoint = point; }, /** * Draw a single point (pie slice) * @param {Object} point The point object * @param {Object} ctx The canvas context to draw it into * @param {Object} color The color of the point * @param {Object} brightness The brightness relative to the color */ drawPoint: function(point, ctx, color, brightness) { var options = this.options, center = point.sliced ? point.centerSliced : point.center, centerX = center[0], centerY = center[1], size = point.size, borderWidth = options.borderWidth, /* IE7 and IE6 fail to render a full circle unless start and end points are equal: */ end = isIE && point.percentage == 100 ? point.start : point.end; // Todo: make Layer.prototype.drawArc method if (point.y > 0) { // drawing 0 will draw a full disc in IE ctx.fillStyle = setColor(Color(color).brighten(brightness).get(ctx), ctx); ctx.strokeStyle = options.borderColor; ctx.lineWidth = borderWidth; ctx.beginPath(); ctx.moveTo(centerX, centerY); ctx.arc(centerX, centerY, size / 2, point.start, end, false); ctx.lineTo(centerX, centerY); ctx.closePath(); ctx.fill(); if (borderWidth) ctx.stroke(); // in WebKit, a zero width line will display as 1 if stroked } }, /** * Pull the slice out from the pie * @param {Object} point */ /*slice: function(point) { var centerSliced = point.centerSliced; setStyles(point.layer, { left: centerSliced[0] + PX, top: centerSliced[1] + PX }); },*/ /** * Get the coordinates for the mouse tracker area */ getAreaCoords: function() { var areas = []; var series = this; each (this.data, function(point) { var centerX = point.center[0], centerY = point.center[1], radius = point.size / 2, start = point.start, end = point.end, coords = []; // start building the coordinates from the start point // with .25 radians (~15 degrees) increments the coordinates for (var angle = start; angle; angle += 0.25) { if (angle >= end) angle = end; coords = coords.concat([ centerX + mathCos(angle) * radius, centerY + mathSin(angle) * radius ]) if (angle >= end) break; } // wrap it up with the center point coords = coords.concat([ centerX, centerY ]); // set tooltip position in the center of the sector point.tooltipPos = [ centerX + 2 * mathCos((start + end) / 2) * radius / 3, centerY + 2 * mathSin((start + end) / 2) * radius / 3 ]; // push an array containing the coordinates and the point areas.push([ map(coords, mathRound).join(','), point ]) }); return areas; }, /** * Extend the default setData method by first removing the old points */ setData: function() { // destroy old points, since the pie has one layer each point var series = this, data = series.data, i; if (data) { for (i = data.length - 1; i >= 0; i--) { data[i].remove(); } } Series.prototype.setData.apply(series, arguments); }, /** * Clear all graphics and HTML from the series layer group */ clear: function() { /*var stateLayers = this.stateLayers; for (var state in stateLayers) { stateLayers[state].clear(); stateLayers[state].cleared = true; } if (this.dataLabelsLayer) { this.dataLabelsLayer.clear(); this.hasDrawnDataLabels = false; }*/ // pies have separate layers per point each (this.data, function(point) { point.layer.clear(); if (point.dataLabelsLayer) point.dataLabelsLayer.clear(); if (point.stateLayer) point.stateLayer.clear(); }); } }); seriesTypes.pie = PieSeries; // global variables Highcharts = { /*'load': function(arr) { each (arr, function(url) { getAjax(url, function(script) { eval(script) }) }) },*/ numberFormat: numberFormat, dateFormat: dateFormat, defaultOptions: defaultOptions, setOptions: setOptions, Chart: Chart, extendClass: extendClass, seriesTypes: seriesTypes, Layer: Layer } })();