test/dummy/tmp/cache/assets/test/sprockets/2f5173deea6c795b8fdde723bb4b63af in contour-2.0.0.beta.9 vs test/dummy/tmp/cache/assets/test/sprockets/2f5173deea6c795b8fdde723bb4b63af in contour-2.0.0.rc

- old
+ new

@@ -1,8 +1,8 @@ {I" class:ETI"BundledAsset;FI"logical_path;TI"application.js;TI" pathname;TI"0$root/app/assets/javascripts/application.js;FI"content_type;TI"application/javascript;TI" -mtime;Tl+QQI" length;Ti I" digest;TI"%baa99b268e03816f3f4b91cdda749503;FI" source;TI" /*! +mtime;Tl+SQI" length;Tiv&I" digest;TI"%f46777d77ba3d1a354cb94a6c76ea962;FI" source;TI"v&/*! * jQuery JavaScript Library v1.10.2 * http://jquery.com/ * * Includes Sizzle.js * http://sizzlejs.com/ @@ -10190,11 +10190,11 @@ * Copyright (c) 2013 jQuery Foundation and other contributors Licensed MIT */ (function(e,t){function i(t,n){var r,i,o,u=t.nodeName.toLowerCase();return"area"===u?(r=t.parentNode,i=r.name,!t.href||!i||r.nodeName.toLowerCase()!=="map"?!1:(o=e("img[usemap=#"+i+"]")[0],!!o&&s(o))):(/input|select|textarea|button|object/.test(u)?!t.disabled:"a"===u?t.href||n:n)&&s(t)}function s(t){return e.expr.filters.visible(t)&&!e(t).parents().addBack().filter(function(){return e.css(this,"visibility")==="hidden"}).length}var n=0,r=/^ui-id-\d+$/;e.ui=e.ui||{};if(e.ui.version)return;e.extend(e.ui,{version:"1.10.0",keyCode:{BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38}}),e.fn.extend({_focus:e.fn.focus,focus:function(t,n){return typeof t=="number"?this.each(function(){var r=this;setTimeout(function(){e(r).focus(),n&&n.call(r)},t)}):this._focus.apply(this,arguments)},scrollParent:function(){var t;return e.ui.ie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?t=this.parents().filter(function(){return/(relative|absolute|fixed)/.test(e.css(this,"position"))&&/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0):t=this.parents().filter(function(){return/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0),/fixed/.test(this.css("position"))||!t.length?e(document):t},zIndex:function(n){if(n!==t)return this.css("zIndex",n);if(this.length){var r=e(this[0]),i,s;while(r.length&&r[0]!==document){i=r.css("position");if(i==="absolute"||i==="relative"||i==="fixed"){s=parseInt(r.css("zIndex"),10);if(!isNaN(s)&&s!==0)return s}r=r.parent()}}return 0},uniqueId:function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++n)})},removeUniqueId:function(){return this.each(function(){r.test(this.id)&&e(this).removeAttr("id")})}}),e.extend(e.expr[":"],{data:e.expr.createPseudo?e.expr.createPseudo(function(t){return function(n){return!!e.data(n,t)}}):function(t,n,r){return!!e.data(t,r[3])},focusable:function(t){return i(t,!isNaN(e.attr(t,"tabindex")))},tabbable:function(t){var n=e.attr(t,"tabindex"),r=isNaN(n);return(r||n>=0)&&i(t,!r)}}),e("<a>").outerWidth(1).jquery||e.each(["Width","Height"],function(n,r){function u(t,n,r,s){return e.each(i,function(){n-=parseFloat(e.css(t,"padding"+this))||0,r&&(n-=parseFloat(e.css(t,"border"+this+"Width"))||0),s&&(n-=parseFloat(e.css(t,"margin"+this))||0)}),n}var i=r==="Width"?["Left","Right"]:["Top","Bottom"],s=r.toLowerCase(),o={innerWidth:e.fn.innerWidth,innerHeight:e.fn.innerHeight,outerWidth:e.fn.outerWidth,outerHeight:e.fn.outerHeight};e.fn["inner"+r]=function(n){return n===t?o["inner"+r].call(this):this.each(function(){e(this).css(s,u(this,n)+"px")})},e.fn["outer"+r]=function(t,n){return typeof t!="number"?o["outer"+r].call(this,t):this.each(function(){e(this).css(s,u(this,t,!0,n)+"px")})}}),e.fn.addBack||(e.fn.addBack=function(e){return this.add(e==null?this.prevObject:this.prevObject.filter(e))}),e("<a>").data("a-b","a").removeData("a-b").data("a-b")&&(e.fn.removeData=function(t){return function(n){return arguments.length?t.call(this,e.camelCase(n)):t.call(this)}}(e.fn.removeData)),e.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()),e.support.selectstart="onselectstart"in document.createElement("div"),e.fn.extend({disableSelection:function(){return this.bind((e.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(e){e.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),e.extend(e.ui,{plugin:{add:function(t,n,r){var i,s=e.ui[t].prototype;for(i in r)s.plugins[i]=s.plugins[i]||[],s.plugins[i].push([n,r[i]])},call:function(e,t,n){var r,i=e.plugins[t];if(!i||!e.element[0].parentNode||e.element[0].parentNode.nodeType===11)return;for(r=0;r<i.length;r++)e.options[i[r][0]]&&i[r][1].apply(e.element,n)}},hasScroll:function(t,n){if(e(t).css("overflow")==="hidden")return!1;var r=n&&n==="left"?"scrollLeft":"scrollTop",i=!1;return t[r]>0?!0:(t[r]=1,i=t[r]>0,t[r]=0,i)}})})(jQuery);(function(e,t){var n=0,r=Array.prototype.slice,i=e.cleanData;e.cleanData=function(t){for(var n=0,r;(r=t[n])!=null;n++)try{e(r).triggerHandler("remove")}catch(s){}i(t)},e.widget=function(t,n,r){var i,s,o,u,a={},f=t.split(".")[0];t=t.split(".")[1],i=f+"-"+t,r||(r=n,n=e.Widget),e.expr[":"][i.toLowerCase()]=function(t){return!!e.data(t,i)},e[f]=e[f]||{},s=e[f][t],o=e[f][t]=function(e,t){if(!this._createWidget)return new o(e,t);arguments.length&&this._createWidget(e,t)},e.extend(o,s,{version:r.version,_proto:e.extend({},r),_childConstructors:[]}),u=new n,u.options=e.widget.extend({},u.options),e.each(r,function(t,r){if(!e.isFunction(r)){a[t]=r;return}a[t]=function(){var e=function(){return n.prototype[t].apply(this,arguments)},i=function(e){return n.prototype[t].apply(this,e)};return function(){var t=this._super,n=this._superApply,s;return this._super=e,this._superApply=i,s=r.apply(this,arguments),this._super=t,this._superApply=n,s}}()}),o.prototype=e.widget.extend(u,{widgetEventPrefix:s?u.widgetEventPrefix:t},a,{constructor:o,namespace:f,widgetName:t,widgetFullName:i}),s?(e.each(s._childConstructors,function(t,n){var r=n.prototype;e.widget(r.namespace+"."+r.widgetName,o,n._proto)}),delete s._childConstructors):n._childConstructors.push(o),e.widget.bridge(t,o)},e.widget.extend=function(n){var i=r.call(arguments,1),s=0,o=i.length,u,a;for(;s<o;s++)for(u in i[s])a=i[s][u],i[s].hasOwnProperty(u)&&a!==t&&(e.isPlainObject(a)?n[u]=e.isPlainObject(n[u])?e.widget.extend({},n[u],a):e.widget.extend({},a):n[u]=a);return n},e.widget.bridge=function(n,i){var s=i.prototype.widgetFullName||n;e.fn[n]=function(o){var u=typeof o=="string",a=r.call(arguments,1),f=this;return o=!u&&a.length?e.widget.extend.apply(null,[o].concat(a)):o,u?this.each(function(){var r,i=e.data(this,s);if(!i)return e.error("cannot call methods on "+n+" prior to initialization; "+"attempted to call method '"+o+"'");if(!e.isFunction(i[o])||o.charAt(0)==="_")return e.error("no such method '"+o+"' for "+n+" widget instance");r=i[o].apply(i,a);if(r!==i&&r!==t)return f=r&&r.jquery?f.pushStack(r.get()):r,!1}):this.each(function(){var t=e.data(this,s);t?t.option(o||{})._init():e.data(this,s,new i(o,this))}),f}},e.Widget=function(){},e.Widget._childConstructors=[],e.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"<div>",options:{disabled:!1,create:null},_createWidget:function(t,r){r=e(r||this.defaultElement||this)[0],this.element=e(r),this.uuid=n++,this.eventNamespace="."+this.widgetName+this.uuid,this.options=e.widget.extend({},this.options,this._getCreateOptions(),t),this.bindings=e(),this.hoverable=e(),this.focusable=e(),r!==this&&(e.data(r,this.widgetFullName,this),this._on(!0,this.element,{remove:function(e){e.target===r&&this.destroy()}}),this.document=e(r.style?r.ownerDocument:r.document||r),this.window=e(this.document[0].defaultView||this.document[0].parentWindow)),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:e.noop,_getCreateEventData:e.noop,_create:e.noop,_init:e.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetName).removeData(this.widgetFullName).removeData(e.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:e.noop,widget:function(){return this.element},option:function(n,r){var i=n,s,o,u;if(arguments.length===0)return e.widget.extend({},this.options);if(typeof n=="string"){i={},s=n.split("."),n=s.shift();if(s.length){o=i[n]=e.widget.extend({},this.options[n]);for(u=0;u<s.length-1;u++)o[s[u]]=o[s[u]]||{},o=o[s[u]];n=s.pop();if(r===t)return o[n]===t?null:o[n];o[n]=r}else{if(r===t)return this.options[n]===t?null:this.options[n];i[n]=r}}return this._setOptions(i),this},_setOptions:function(e){var t;for(t in e)this._setOption(t,e[t]);return this},_setOption:function(e,t){return this.options[e]=t,e==="disabled"&&(this.widget().toggleClass(this.widgetFullName+"-disabled ui-state-disabled",!!t).attr("aria-disabled",t),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")),this},enable:function(){return this._setOption("disabled",!1)},disable:function(){return this._setOption("disabled",!0)},_on:function(t,n,r){var i,s=this;typeof t!="boolean"&&(r=n,n=t,t=!1),r?(n=i=e(n),this.bindings=this.bindings.add(n)):(r=n,n=this.element,i=this.widget()),e.each(r,function(r,o){function u(){if(!t&&(s.options.disabled===!0||e(this).hasClass("ui-state-disabled")))return;return(typeof o=="string"?s[o]:o).apply(s,arguments)}typeof o!="string"&&(u.guid=o.guid=o.guid||u.guid||e.guid++);var a=r.match(/^(\w+)\s*(.*)$/),f=a[1]+s.eventNamespace,l=a[2];l?i.delegate(l,f,u):n.bind(f,u)})},_off:function(e,t){t=(t||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,e.unbind(t).undelegate(t)},_delay:function(e,t){function n(){return(typeof e=="string"?r[e]:e).apply(r,arguments)}var r=this;return setTimeout(n,t||0)},_hoverable:function(t){this.hoverable=this.hoverable.add(t),this._on(t,{mouseenter:function(t){e(t.currentTarget).addClass("ui-state-hover")},mouseleave:function(t){e(t.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(t){this.focusable=this.focusable.add(t),this._on(t,{focusin:function(t){e(t.currentTarget).addClass("ui-state-focus")},focusout:function(t){e(t.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(t,n,r){var i,s,o=this.options[t];r=r||{},n=e.Event(n),n.type=(t===this.widgetEventPrefix?t:this.widgetEventPrefix+t).toLowerCase(),n.target=this.element[0],s=n.originalEvent;if(s)for(i in s)i in n||(n[i]=s[i]);return this.element.trigger(n,r),!(e.isFunction(o)&&o.apply(this.element[0],[n].concat(r))===!1||n.isDefaultPrevented())}},e.each({show:"fadeIn",hide:"fadeOut"},function(t,n){e.Widget.prototype["_"+t]=function(r,i,s){typeof i=="string"&&(i={effect:i});var o,u=i?i===!0||typeof i=="number"?n:i.effect||n:t;i=i||{},typeof i=="number"&&(i={duration:i}),o=!e.isEmptyObject(i),i.complete=s,i.delay&&r.delay(i.delay),o&&e.effects&&e.effects.effect[u]?r[t](i):u!==t&&r[u]?r[u](i.duration,i.easing,s):r.queue(function(n){e(this)[t](),s&&s.call(r[0]),n()})}})})(jQuery);(function(e,t){var n=!1;e(document).mouseup(function(){n=!1}),e.widget("ui.mouse",{version:"1.10.0",options:{cancel:"input,textarea,button,select,option",distance:1,delay:0},_mouseInit:function(){var t=this;this.element.bind("mousedown."+this.widgetName,function(e){return t._mouseDown(e)}).bind("click."+this.widgetName,function(n){if(!0===e.data(n.target,t.widgetName+".preventClickEvent"))return e.removeData(n.target,t.widgetName+".preventClickEvent"),n.stopImmediatePropagation(),!1}),this.started=!1},_mouseDestroy:function(){this.element.unbind("."+this.widgetName),this._mouseMoveDelegate&&e(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(t){if(n)return;this._mouseStarted&&this._mouseUp(t),this._mouseDownEvent=t;var r=this,i=t.which===1,s=typeof this.options.cancel=="string"&&t.target.nodeName?e(t.target).closest(this.options.cancel).length:!1;if(!i||s||!this._mouseCapture(t))return!0;this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){r.mouseDelayMet=!0},this.options.delay));if(this._mouseDistanceMet(t)&&this._mouseDelayMet(t)){this._mouseStarted=this._mouseStart(t)!==!1;if(!this._mouseStarted)return t.preventDefault(),!0}return!0===e.data(t.target,this.widgetName+".preventClickEvent")&&e.removeData(t.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(e){return r._mouseMove(e)},this._mouseUpDelegate=function(e){return r._mouseUp(e)},e(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate),t.preventDefault(),n=!0,!0},_mouseMove:function(t){return e.ui.ie&&(!document.documentMode||document.documentMode<9)&&!t.button?this._mouseUp(t):this._mouseStarted?(this._mouseDrag(t),t.preventDefault()):(this._mouseDistanceMet(t)&&this._mouseDelayMet(t)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,t)!==!1,this._mouseStarted?this._mouseDrag(t):this._mouseUp(t)),!this._mouseStarted)},_mouseUp:function(t){return e(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,t.target===this._mouseDownEvent.target&&e.data(t.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(t)),!1},_mouseDistanceMet:function(e){return Math.max(Math.abs(this._mouseDownEvent.pageX-e.pageX),Math.abs(this._mouseDownEvent.pageY-e.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}})})(jQuery);(function(e,t){e.widget("ui.draggable",e.ui.mouse,{version:"1.10.0",widgetEventPrefix:"drag",options:{addClasses:!0,appendTo:"parent",axis:!1,connectToSortable:!1,containment:!1,cursor:"auto",cursorAt:!1,grid:!1,handle:!1,helper:"original",iframeFix:!1,opacity:!1,refreshPositions:!1,revert:!1,revertDuration:500,scope:"default",scroll:!0,scrollSensitivity:20,scrollSpeed:20,snap:!1,snapMode:"both",snapTolerance:20,stack:!1,zIndex:!1,drag:null,start:null,stop:null},_create:function(){this.options.helper==="original"&&!/^(?:r|a|f)/.test(this.element.css("position"))&&(this.element[0].style.position="relative"),this.options.addClasses&&this.element.addClass("ui-draggable"),this.options.disabled&&this.element.addClass("ui-draggable-disabled"),this._mouseInit()},_destroy:function(){this.element.removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled"),this._mouseDestroy()},_mouseCapture:function(t){var n=this.options;return this.helper||n.disabled||e(t.target).closest(".ui-resizable-handle").length>0?!1:(this.handle=this._getHandle(t),this.handle?(e(n.iframeFix===!0?"iframe":n.iframeFix).each(function(){e("<div class='ui-draggable-iframeFix' style='background: #fff;'></div>").css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1e3}).css(e(this).offset()).appendTo("body")}),!0):!1)},_mouseStart:function(t){var n=this.options;return this.helper=this._createHelper(t),this.helper.addClass("ui-draggable-dragging"),this._cacheHelperProportions(),e.ui.ddmanager&&(e.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(),this.offset=this.positionAbs=this.element.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},e.extend(this.offset,{click:{left:t.pageX-this.offset.left,top:t.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.originalPosition=this.position=this._generatePosition(t),this.originalPageX=t.pageX,this.originalPageY=t.pageY,n.cursorAt&&this._adjustOffsetFromHelper(n.cursorAt),n.containment&&this._setContainment(),this._trigger("start",t)===!1?(this._clear(),!1):(this._cacheHelperProportions(),e.ui.ddmanager&&!n.dropBehaviour&&e.ui.ddmanager.prepareOffsets(this,t),this._mouseDrag(t,!0),e.ui.ddmanager&&e.ui.ddmanager.dragStart(this,t),!0)},_mouseDrag:function(t,n){this.position=this._generatePosition(t),this.positionAbs=this._convertPositionTo("absolute");if(!n){var r=this._uiHash();if(this._trigger("drag",t,r)===!1)return this._mouseUp({}),!1;this.position=r.position}if(!this.options.axis||this.options.axis!=="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis||this.options.axis!=="x")this.helper[0].style.top=this.position.top+"px";return e.ui.ddmanager&&e.ui.ddmanager.drag(this,t),!1},_mouseStop:function(t){var n,r=this,i=!1,s=!1;e.ui.ddmanager&&!this.options.dropBehaviour&&(s=e.ui.ddmanager.drop(this,t)),this.dropped&&(s=this.dropped,this.dropped=!1),n=this.element[0];while(n&&(n=n.parentNode))n===document&&(i=!0);return!i&&this.options.helper==="original"?!1:(this.options.revert==="invalid"&&!s||this.options.revert==="valid"&&s||this.options.revert===!0||e.isFunction(this.options.revert)&&this.options.revert.call(this.element,s)?e(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){r._trigger("stop",t)!==!1&&r._clear()}):this._trigger("stop",t)!==!1&&this._clear(),!1)},_mouseUp:function(t){return e("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)}),e.ui.ddmanager&&e.ui.ddmanager.dragStop(this,t),e.ui.mouse.prototype._mouseUp.call(this,t)},cancel:function(){return this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear(),this},_getHandle:function(t){var n=!this.options.handle||!e(this.options.handle,this.element).length?!0:!1;return e(this.options.handle,this.element).find("*").addBack().each(function(){this===t.target&&(n=!0)}),n},_createHelper:function(t){var n=this.options,r=e.isFunction(n.helper)?e(n.helper.apply(this.element[0],[t])):n.helper==="clone"?this.element.clone().removeAttr("id"):this.element;return r.parents("body").length||r.appendTo(n.appendTo==="parent"?this.element[0].parentNode:n.appendTo),r[0]!==this.element[0]&&!/(fixed|absolute)/.test(r.css("position"))&&r.css("position","absolute"),r},_adjustOffsetFromHelper:function(t){typeof t=="string"&&(t=t.split(" ")),e.isArray(t)&&(t={left:+t[0],top:+t[1]||0}),"left"in t&&(this.offset.click.left=t.left+this.margins.left),"right"in t&&(this.offset.click.left=this.helperProportions.width-t.right+this.margins.left),"top"in t&&(this.offset.click.top=t.top+this.margins.top),"bottom"in t&&(this.offset.click.top=this.helperProportions.height-t.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var t=this.offsetParent.offset();this.cssPosition==="absolute"&&this.scrollParent[0]!==document&&e.contains(this.scrollParent[0],this.offsetParent[0])&&(t.left+=this.scrollParent.scrollLeft(),t.top+=this.scrollParent.scrollTop());if(this.offsetParent[0]===document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()==="html"&&e.ui.ie)t={top:0,left:0};return{top:t.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:t.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition==="relative"){var e=this.element.position();return{top:e.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:e.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var t,n,r,i=this.options;i.containment==="parent"&&(i.containment=this.helper[0].parentNode);if(i.containment==="document"||i.containment==="window")this.containment=[i.containment==="document"?0:e(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,i.containment==="document"?0:e(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,(i.containment==="document"?0:e(window).scrollLeft())+e(i.containment==="document"?document:window).width()-this.helperProportions.width-this.margins.left,(i.containment==="document"?0:e(window).scrollTop())+(e(i.containment==="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(i.containment)&&i.containment.constructor!==Array){n=e(i.containment),r=n[0];if(!r)return;t=e(r).css("overflow")!=="hidden",this.containment=[(parseInt(e(r).css("borderLeftWidth"),10)||0)+(parseInt(e(r).css("paddingLeft"),10)||0),(parseInt(e(r).css("borderTopWidth"),10)||0)+(parseInt(e(r).css("paddingTop"),10)||0),(t?Math.max(r.scrollWidth,r.offsetWidth):r.offsetWidth)-(parseInt(e(r).css("borderLeftWidth"),10)||0)-(parseInt(e(r).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(t?Math.max(r.scrollHeight,r.offsetHeight):r.offsetHeight)-(parseInt(e(r).css("borderTopWidth"),10)||0)-(parseInt(e(r).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom],this.relative_container=n}else i.containment.constructor===Array&&(this.containment=i.containment)},_convertPositionTo:function(t,n){n||(n=this.position);var r=t==="absolute"?1:-1,i=this.cssPosition!=="absolute"||this.scrollParent[0]!==document&&!!e.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,s=/(html|body)/i.test(i[0].tagName);return{top:n.top+this.offset.relative.top*r+this.offset.parent.top*r-(this.cssPosition==="fixed"?-this.scrollParent.scrollTop():s?0:i.scrollTop())*r,left:n.left+this.offset.relative.left*r+this.offset.parent.left*r-(this.cssPosition==="fixed"?-this.scrollParent.scrollLeft():s?0:i.scrollLeft())*r}},_generatePosition:function(t){var n,r,i,s,o=this.options,u=this.cssPosition!=="absolute"||this.scrollParent[0]!==document&&!!e.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,a=/(html|body)/i.test(u[0].tagName),f=t.pageX,l=t.pageY;return this.originalPosition&&(this.containment&&(this.relative_container?(r=this.relative_container.offset(),n=[this.containment[0]+r.left,this.containment[1]+r.top,this.containment[2]+r.left,this.containment[3]+r.top]):n=this.containment,t.pageX-this.offset.click.left<n[0]&&(f=n[0]+this.offset.click.left),t.pageY-this.offset.click.top<n[1]&&(l=n[1]+this.offset.click.top),t.pageX-this.offset.click.left>n[2]&&(f=n[2]+this.offset.click.left),t.pageY-this.offset.click.top>n[3]&&(l=n[3]+this.offset.click.top)),o.grid&&(i=o.grid[1]?this.originalPageY+Math.round((l-this.originalPageY)/o.grid[1])*o.grid[1]:this.originalPageY,l=n?i-this.offset.click.top>=n[1]||i-this.offset.click.top>n[3]?i:i-this.offset.click.top>=n[1]?i-o.grid[1]:i+o.grid[1]:i,s=o.grid[0]?this.originalPageX+Math.round((f-this.originalPageX)/o.grid[0])*o.grid[0]:this.originalPageX,f=n?s-this.offset.click.left>=n[0]||s-this.offset.click.left>n[2]?s:s-this.offset.click.left>=n[0]?s-o.grid[0]:s+o.grid[0]:s)),{top:l-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(this.cssPosition==="fixed"?-this.scrollParent.scrollTop():a?0:u.scrollTop()),left:f-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+(this.cssPosition==="fixed"?-this.scrollParent.scrollLeft():a?0:u.scrollLeft())}},_clear:function(){this.helper.removeClass("ui-draggable-dragging"),this.helper[0]!==this.element[0]&&!this.cancelHelperRemoval&&this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1},_trigger:function(t,n,r){return r=r||this._uiHash(),e.ui.plugin.call(this,t,[n,r]),t==="drag"&&(this.positionAbs=this._convertPositionTo("absolute")),e.Widget.prototype._trigger.call(this,t,n,r)},plugins:{},_uiHash:function(){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}}),e.ui.plugin.add("draggable","connectToSortable",{start:function(t,n){var r=e(this).data("ui-draggable"),i=r.options,s=e.extend({},n,{item:r.element});r.sortables=[],e(i.connectToSortable).each(function(){var n=e.data(this,"ui-sortable");n&&!n.options.disabled&&(r.sortables.push({instance:n,shouldRevert:n.options.revert}),n.refreshPositions(),n._trigger("activate",t,s))})},stop:function(t,n){var r=e(this).data("ui-draggable"),i=e.extend({},n,{item:r.element});e.each(r.sortables,function(){this.instance.isOver?(this.instance.isOver=0,r.cancelHelperRemoval=!0,this.instance.cancelHelperRemoval=!1,this.shouldRevert&&(this.instance.options.revert=!0),this.instance._mouseStop(t),this.instance.options.helper=this.instance.options._helper,r.options.helper==="original"&&this.instance.currentItem.css({top:"auto",left:"auto"})):(this.instance.cancelHelperRemoval=!1,this.instance._trigger("deactivate",t,i))})},drag:function(t,n){var r=e(this).data("ui-draggable"),i=this;e.each(r.sortables,function(){var s=!1,o=this;this.instance.positionAbs=r.positionAbs,this.instance.helperProportions=r.helperProportions,this.instance.offset.click=r.offset.click,this.instance._intersectsWith(this.instance.containerCache)&&(s=!0,e.each(r.sortables,function(){return this.instance.positionAbs=r.positionAbs,this.instance.helperProportions=r.helperProportions,this.instance.offset.click=r.offset.click,this!==o&&this.instance._intersectsWith(this.instance.containerCache)&&e.ui.contains(o.instance.element[0],this.instance.element[0])&&(s=!1),s})),s?(this.instance.isOver||(this.instance.isOver=1,this.instance.currentItem=e(i).clone().removeAttr("id").appendTo(this.instance.element).data("ui-sortable-item",!0),this.instance.options._helper=this.instance.options.helper,this.instance.options.helper=function(){return n.helper[0]},t.target=this.instance.currentItem[0],this.instance._mouseCapture(t,!0),this.instance._mouseStart(t,!0,!0),this.instance.offset.click.top=r.offset.click.top,this.instance.offset.click.left=r.offset.click.left,this.instance.offset.parent.left-=r.offset.parent.left-this.instance.offset.parent.left,this.instance.offset.parent.top-=r.offset.parent.top-this.instance.offset.parent.top,r._trigger("toSortable",t),r.dropped=this.instance.element,r.currentItem=r.element,this.instance.fromOutside=r),this.instance.currentItem&&this.instance._mouseDrag(t)):this.instance.isOver&&(this.instance.isOver=0,this.instance.cancelHelperRemoval=!0,this.instance.options.revert=!1,this.instance._trigger("out",t,this.instance._uiHash(this.instance)),this.instance._mouseStop(t,!0),this.instance.options.helper=this.instance.options._helper,this.instance.currentItem.remove(),this.instance.placeholder&&this.instance.placeholder.remove(),r._trigger("fromSortable",t),r.dropped=!1)})}}),e.ui.plugin.add("draggable","cursor",{start:function(){var t=e("body"),n=e(this).data("ui-draggable").options;t.css("cursor")&&(n._cursor=t.css("cursor")),t.css("cursor",n.cursor)},stop:function(){var t=e(this).data("ui-draggable").options;t._cursor&&e("body").css("cursor",t._cursor)}}),e.ui.plugin.add("draggable","opacity",{start:function(t,n){var r=e(n.helper),i=e(this).data("ui-draggable").options;r.css("opacity")&&(i._opacity=r.css("opacity")),r.css("opacity",i.opacity)},stop:function(t,n){var r=e(this).data("ui-draggable").options;r._opacity&&e(n.helper).css("opacity",r._opacity)}}),e.ui.plugin.add("draggable","scroll",{start:function(){var t=e(this).data("ui-draggable");t.scrollParent[0]!==document&&t.scrollParent[0].tagName!=="HTML"&&(t.overflowOffset=t.scrollParent.offset())},drag:function(t){var n=e(this).data("ui-draggable"),r=n.options,i=!1;if(n.scrollParent[0]!==document&&n.scrollParent[0].tagName!=="HTML"){if(!r.axis||r.axis!=="x")n.overflowOffset.top+n.scrollParent[0].offsetHeight-t.pageY<r.scrollSensitivity?n.scrollParent[0].scrollTop=i=n.scrollParent[0].scrollTop+r.scrollSpeed:t.pageY-n.overflowOffset.top<r.scrollSensitivity&&(n.scrollParent[0].scrollTop=i=n.scrollParent[0].scrollTop-r.scrollSpeed);if(!r.axis||r.axis!=="y")n.overflowOffset.left+n.scrollParent[0].offsetWidth-t.pageX<r.scrollSensitivity?n.scrollParent[0].scrollLeft=i=n.scrollParent[0].scrollLeft+r.scrollSpeed:t.pageX-n.overflowOffset.left<r.scrollSensitivity&&(n.scrollParent[0].scrollLeft=i=n.scrollParent[0].scrollLeft-r.scrollSpeed)}else{if(!r.axis||r.axis!=="x")t.pageY-e(document).scrollTop()<r.scrollSensitivity?i=e(document).scrollTop(e(document).scrollTop()-r.scrollSpeed):e(window).height()-(t.pageY-e(document).scrollTop())<r.scrollSensitivity&&(i=e(document).scrollTop(e(document).scrollTop()+r.scrollSpeed));if(!r.axis||r.axis!=="y")t.pageX-e(document).scrollLeft()<r.scrollSensitivity?i=e(document).scrollLeft(e(document).scrollLeft()-r.scrollSpeed):e(window).width()-(t.pageX-e(document).scrollLeft())<r.scrollSensitivity&&(i=e(document).scrollLeft(e(document).scrollLeft()+r.scrollSpeed))}i!==!1&&e.ui.ddmanager&&!r.dropBehaviour&&e.ui.ddmanager.prepareOffsets(n,t)}}),e.ui.plugin.add("draggable","snap",{start:function(){var t=e(this).data("ui-draggable"),n=t.options;t.snapElements=[],e(n.snap.constructor!==String?n.snap.items||":data(ui-draggable)":n.snap).each(function(){var n=e(this),r=n.offset();this!==t.element[0]&&t.snapElements.push({item:this,width:n.outerWidth(),height:n.outerHeight(),top:r.top,left:r.left})})},drag:function(t,n){var r,i,s,o,u,a,f,l,c,h,p=e(this).data("ui-draggable"),d=p.options,v=d.snapTolerance,m=n.offset.left,g=m+p.helperProportions.width,y=n.offset.top,b=y+p.helperProportions.height;for(c=p.snapElements.length-1;c>=0;c--){u=p.snapElements[c].left,a=u+p.snapElements[c].width,f=p.snapElements[c].top,l=f+p.snapElements[c].height;if(!(u-v<m&&m<a+v&&f-v<y&&y<l+v||u-v<m&&m<a+v&&f-v<b&&b<l+v||u-v<g&&g<a+v&&f-v<y&&y<l+v||u-v<g&&g<a+v&&f-v<b&&b<l+v)){p.snapElements[c].snapping&&p.options.snap.release&&p.options.snap.release.call(p.element,t,e.extend(p._uiHash(),{snapItem:p.snapElements[c].item})),p.snapElements[c].snapping=!1;continue}d.snapMode!=="inner"&&(r=Math.abs(f-b)<=v,i=Math.abs(l-y)<=v,s=Math.abs(u-g)<=v,o=Math.abs(a-m)<=v,r&&(n.position.top=p._convertPositionTo("relative",{top:f-p.helperProportions.height,left:0}).top-p.margins.top),i&&(n.position.top=p._convertPositionTo("relative",{top:l,left:0}).top-p.margins.top),s&&(n.position.left=p._convertPositionTo("relative",{top:0,left:u-p.helperProportions.width}).left-p.margins.left),o&&(n.position.left=p._convertPositionTo("relative",{top:0,left:a}).left-p.margins.left)),h=r||i||s||o,d.snapMode!=="outer"&&(r=Math.abs(f-y)<=v,i=Math.abs(l-b)<=v,s=Math.abs(u-m)<=v,o=Math.abs(a-g)<=v,r&&(n.position.top=p._convertPositionTo("relative",{top:f,left:0}).top-p.margins.top),i&&(n.position.top=p._convertPositionTo("relative",{top:l-p.helperProportions.height,left:0}).top-p.margins.top),s&&(n.position.left=p._convertPositionTo("relative",{top:0,left:u}).left-p.margins.left),o&&(n.position.left=p._convertPositionTo("relative",{top:0,left:a-p.helperProportions.width}).left-p.margins.left)),!p.snapElements[c].snapping&&(r||i||s||o||h)&&p.options.snap.snap&&p.options.snap.snap.call(p.element,t,e.extend(p._uiHash(),{snapItem:p.snapElements[c].item})),p.snapElements[c].snapping=r||i||s||o||h}}}),e.ui.plugin.add("draggable","stack",{start:function(){var t,n=e(this).data("ui-draggable").options,r=e.makeArray(e(n.stack)).sort(function(t,n){return(parseInt(e(t).css("zIndex"),10)||0)-(parseInt(e(n).css("zIndex"),10)||0)});if(!r.length)return;t=parseInt(r[0].style.zIndex,10)||0,e(r).each(function(e){this.style.zIndex=t+e}),this[0].style.zIndex=t+r.length}}),e.ui.plugin.add("draggable","zIndex",{start:function(t,n){var r=e(n.helper),i=e(this).data("ui-draggable").options;r.css("zIndex")&&(i._zIndex=r.css("zIndex")),r.css("zIndex",i.zIndex)},stop:function(t,n){var r=e(this).data("ui-draggable").options;r._zIndex&&e(n.helper).css("zIndex",r._zIndex)}})})(jQuery);(function(e,t){function n(e,t,n){return e>t&&e<t+n}e.widget("ui.droppable",{version:"1.10.0",widgetEventPrefix:"drop",options:{accept:"*",activeClass:!1,addClasses:!0,greedy:!1,hoverClass:!1,scope:"default",tolerance:"intersect",activate:null,deactivate:null,drop:null,out:null,over:null},_create:function(){var t=this.options,n=t.accept;this.isover=!1,this.isout=!0,this.accept=e.isFunction(n)?n:function(e){return e.is(n)},this.proportions={width:this.element[0].offsetWidth,height:this.element[0].offsetHeight},e.ui.ddmanager.droppables[t.scope]=e.ui.ddmanager.droppables[t.scope]||[],e.ui.ddmanager.droppables[t.scope].push(this),t.addClasses&&this.element.addClass("ui-droppable")},_destroy:function(){var t=0,n=e.ui.ddmanager.droppables[this.options.scope];for(;t<n.length;t++)n[t]===this&&n.splice(t,1);this.element.removeClass("ui-droppable ui-droppable-disabled")},_setOption:function(t,n){t==="accept"&&(this.accept=e.isFunction(n)?n:function(e){return e.is(n)}),e.Widget.prototype._setOption.apply(this,arguments)},_activate:function(t){var n=e.ui.ddmanager.current;this.options.activeClass&&this.element.addClass(this.options.activeClass),n&&this._trigger("activate",t,this.ui(n))},_deactivate:function(t){var n=e.ui.ddmanager.current;this.options.activeClass&&this.element.removeClass(this.options.activeClass),n&&this._trigger("deactivate",t,this.ui(n))},_over:function(t){var n=e.ui.ddmanager.current;if(!n||(n.currentItem||n.element)[0]===this.element[0])return;this.accept.call(this.element[0],n.currentItem||n.element)&&(this.options.hoverClass&&this.element.addClass(this.options.hoverClass),this._trigger("over",t,this.ui(n)))},_out:function(t){var n=e.ui.ddmanager.current;if(!n||(n.currentItem||n.element)[0]===this.element[0])return;this.accept.call(this.element[0],n.currentItem||n.element)&&(this.options.hoverClass&&this.element.removeClass(this.options.hoverClass),this._trigger("out",t,this.ui(n)))},_drop:function(t,n){var r=n||e.ui.ddmanager.current,i=!1;return!r||(r.currentItem||r.element)[0]===this.element[0]?!1:(this.element.find(":data(ui-droppable)").not(".ui-draggable-dragging").each(function(){var t=e.data(this,"ui-droppable");if(t.options.greedy&&!t.options.disabled&&t.options.scope===r.options.scope&&t.accept.call(t.element[0],r.currentItem||r.element)&&e.ui.intersect(r,e.extend(t,{offset:t.element.offset()}),t.options.tolerance))return i=!0,!1}),i?!1:this.accept.call(this.element[0],r.currentItem||r.element)?(this.options.activeClass&&this.element.removeClass(this.options.activeClass),this.options.hoverClass&&this.element.removeClass(this.options.hoverClass),this._trigger("drop",t,this.ui(r)),this.element):!1)},ui:function(e){return{draggable:e.currentItem||e.element,helper:e.helper,position:e.position,offset:e.positionAbs}}}),e.ui.intersect=function(e,t,r){if(!t.offset)return!1;var i,s,o=(e.positionAbs||e.position.absolute).left,u=o+e.helperProportions.width,a=(e.positionAbs||e.position.absolute).top,f=a+e.helperProportions.height,l=t.offset.left,c=l+t.proportions.width,h=t.offset.top,p=h+t.proportions.height;switch(r){case"fit":return l<=o&&u<=c&&h<=a&&f<=p;case"intersect":return l<o+e.helperProportions.width/2&&u-e.helperProportions.width/2<c&&h<a+e.helperProportions.height/2&&f-e.helperProportions.height/2<p;case"pointer":return i=(e.positionAbs||e.position.absolute).left+(e.clickOffset||e.offset.click).left,s=(e.positionAbs||e.position.absolute).top+(e.clickOffset||e.offset.click).top,n(s,h,t.proportions.height)&&n(i,l,t.proportions.width);case"touch":return(a>=h&&a<=p||f>=h&&f<=p||a<h&&f>p)&&(o>=l&&o<=c||u>=l&&u<=c||o<l&&u>c);default:return!1}},e.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(t,n){var r,i,s=e.ui.ddmanager.droppables[t.options.scope]||[],o=n?n.type:null,u=(t.currentItem||t.element).find(":data(ui-droppable)").addBack();e:for(r=0;r<s.length;r++){if(s[r].options.disabled||t&&!s[r].accept.call(s[r].element[0],t.currentItem||t.element))continue;for(i=0;i<u.length;i++)if(u[i]===s[r].element[0]){s[r].proportions.height=0;continue e}s[r].visible=s[r].element.css("display")!=="none";if(!s[r].visible)continue;o==="mousedown"&&s[r]._activate.call(s[r],n),s[r].offset=s[r].element.offset(),s[r].proportions={width:s[r].element[0].offsetWidth,height:s[r].element[0].offsetHeight}}},drop:function(t,n){var r=!1;return e.each(e.ui.ddmanager.droppables[t.options.scope]||[],function(){if(!this.options)return;!this.options.disabled&&this.visible&&e.ui.intersect(t,this,this.options.tolerance)&&(r=this._drop.call(this,n)||r),!this.options.disabled&&this.visible&&this.accept.call(this.element[0],t.currentItem||t.element)&&(this.isout=!0,this.isover=!1,this._deactivate.call(this,n))}),r},dragStart:function(t,n){t.element.parentsUntil("body").bind("scroll.droppable",function(){t.options.refreshPositions||e.ui.ddmanager.prepareOffsets(t,n)})},drag:function(t,n){t.options.refreshPositions&&e.ui.ddmanager.prepareOffsets(t,n),e.each(e.ui.ddmanager.droppables[t.options.scope]||[],function(){if(this.options.disabled||this.greedyChild||!this.visible)return;var r,i,s,o=e.ui.intersect(t,this,this.options.tolerance),u=!o&&this.isover?"isout":o&&!this.isover?"isover":null;if(!u)return;this.options.greedy&&(i=this.options.scope,s=this.element.parents(":data(ui-droppable)").filter(function(){return e.data(this,"ui-droppable").options.scope===i}),s.length&&(r=e.data(s[0],"ui-droppable"),r.greedyChild=u==="isover")),r&&u==="isover"&&(r.isover=!1,r.isout=!0,r._out.call(r,n)),this[u]=!0,this[u==="isout"?"isover":"isout"]=!1,this[u==="isover"?"_over":"_out"].call(this,n),r&&u==="isout"&&(r.isout=!1,r.isover=!0,r._over.call(r,n))})},dragStop:function(t,n){t.element.parentsUntil("body").unbind("scroll.droppable"),t.options.refreshPositions||e.ui.ddmanager.prepareOffsets(t,n)}}})(jQuery);(function(e,t){function n(e,t,n){return e>t&&e<t+n}e.widget("ui.sortable",e.ui.mouse,{version:"1.10.0",widgetEventPrefix:"sort",ready:!1,options:{appendTo:"parent",axis:!1,connectWith:!1,containment:!1,cursor:"auto",cursorAt:!1,dropOnEmpty:!0,forcePlaceholderSize:!1,forceHelperSize:!1,grid:!1,handle:!1,helper:"original",items:"> *",opacity:!1,placeholder:!1,revert:!1,scroll:!0,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1e3,activate:null,beforeStop:null,change:null,deactivate:null,out:null,over:null,receive:null,remove:null,sort:null,start:null,stop:null,update:null},_create:function(){var e=this.options;this.containerCache={},this.element.addClass("ui-sortable"),this.refresh(),this.floating=this.items.length?e.axis==="x"||/left|right/.test(this.items[0].item.css("float"))||/inline|table-cell/.test(this.items[0].item.css("display")):!1,this.offset=this.element.offset(),this._mouseInit(),this.ready=!0},_destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled"),this._mouseDestroy();for(var e=this.items.length-1;e>=0;e--)this.items[e].item.removeData(this.widgetName+"-item");return this},_setOption:function(t,n){t==="disabled"?(this.options[t]=n,this.widget().toggleClass("ui-sortable-disabled",!!n)):e.Widget.prototype._setOption.apply(this,arguments)},_mouseCapture:function(t,n){var r=null,i=!1,s=this;if(this.reverting)return!1;if(this.options.disabled||this.options.type==="static")return!1;this._refreshItems(t),e(t.target).parents().each(function(){if(e.data(this,s.widgetName+"-item")===s)return r=e(this),!1}),e.data(t.target,s.widgetName+"-item")===s&&(r=e(t.target));if(!r)return!1;if(this.options.handle&&!n){e(this.options.handle,r).find("*").addBack().each(function(){this===t.target&&(i=!0)});if(!i)return!1}return this.currentItem=r,this._removeCurrentsFromItems(),!0},_mouseStart:function(t,n,r){var i,s=this.options;this.currentContainer=this,this.refreshPositions(),this.helper=this._createHelper(t),this._cacheHelperProportions(),this._cacheMargins(),this.scrollParent=this.helper.scrollParent(),this.offset=this.currentItem.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},e.extend(this.offset,{click:{left:t.pageX-this.offset.left,top:t.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.helper.css("position","absolute"),this.cssPosition=this.helper.css("position"),this.originalPosition=this._generatePosition(t),this.originalPageX=t.pageX,this.originalPageY=t.pageY,s.cursorAt&&this._adjustOffsetFromHelper(s.cursorAt),this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]},this.helper[0]!==this.currentItem[0]&&this.currentItem.hide(),this._createPlaceholder(),s.containment&&this._setContainment(),s.cursor&&(e("body").css("cursor")&&(this._storedCursor=e("body").css("cursor")),e("body").css("cursor",s.cursor)),s.opacity&&(this.helper.css("opacity")&&(this._storedOpacity=this.helper.css("opacity")),this.helper.css("opacity",s.opacity)),s.zIndex&&(this.helper.css("zIndex")&&(this._storedZIndex=this.helper.css("zIndex")),this.helper.css("zIndex",s.zIndex)),this.scrollParent[0]!==document&&this.scrollParent[0].tagName!=="HTML"&&(this.overflowOffset=this.scrollParent.offset()),this._trigger("start",t,this._uiHash()),this._preserveHelperProportions||this._cacheHelperProportions();if(!r)for(i=this.containers.length-1;i>=0;i--)this.containers[i]._trigger("activate",t,this._uiHash(this));return e.ui.ddmanager&&(e.ui.ddmanager.current=this),e.ui.ddmanager&&!s.dropBehaviour&&e.ui.ddmanager.prepareOffsets(this,t),this.dragging=!0,this.helper.addClass("ui-sortable-helper"),this._mouseDrag(t),!0},_mouseDrag:function(t){var n,r,i,s,o=this.options,u=!1;this.position=this._generatePosition(t),this.positionAbs=this._convertPositionTo("absolute"),this.lastPositionAbs||(this.lastPositionAbs=this.positionAbs),this.options.scroll&&(this.scrollParent[0]!==document&&this.scrollParent[0].tagName!=="HTML"?(this.overflowOffset.top+this.scrollParent[0].offsetHeight-t.pageY<o.scrollSensitivity?this.scrollParent[0].scrollTop=u=this.scrollParent[0].scrollTop+o.scrollSpeed:t.pageY-this.overflowOffset.top<o.scrollSensitivity&&(this.scrollParent[0].scrollTop=u=this.scrollParent[0].scrollTop-o.scrollSpeed),this.overflowOffset.left+this.scrollParent[0].offsetWidth-t.pageX<o.scrollSensitivity?this.scrollParent[0].scrollLeft=u=this.scrollParent[0].scrollLeft+o.scrollSpeed:t.pageX-this.overflowOffset.left<o.scrollSensitivity&&(this.scrollParent[0].scrollLeft=u=this.scrollParent[0].scrollLeft-o.scrollSpeed)):(t.pageY-e(document).scrollTop()<o.scrollSensitivity?u=e(document).scrollTop(e(document).scrollTop()-o.scrollSpeed):e(window).height()-(t.pageY-e(document).scrollTop())<o.scrollSensitivity&&(u=e(document).scrollTop(e(document).scrollTop()+o.scrollSpeed)),t.pageX-e(document).scrollLeft()<o.scrollSensitivity?u=e(document).scrollLeft(e(document).scrollLeft()-o.scrollSpeed):e(window).width()-(t.pageX-e(document).scrollLeft())<o.scrollSensitivity&&(u=e(document).scrollLeft(e(document).scrollLeft()+o.scrollSpeed))),u!==!1&&e.ui.ddmanager&&!o.dropBehaviour&&e.ui.ddmanager.prepareOffsets(this,t)),this.positionAbs=this._convertPositionTo("absolute");if(!this.options.axis||this.options.axis!=="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis||this.options.axis!=="x")this.helper[0].style.top=this.position.top+"px";for(n=this.items.length-1;n>=0;n--){r=this.items[n],i=r.item[0],s=this._intersectsWithPointer(r);if(!s)continue;if(r.instance!==this.currentContainer)continue;if(i!==this.currentItem[0]&&this.placeholder[s===1?"next":"prev"]()[0]!==i&&!e.contains(this.placeholder[0],i)&&(this.options.type==="semi-dynamic"?!e.contains(this.element[0],i):!0)){this.direction=s===1?"down":"up";if(this.options.tolerance!=="pointer"&&!this._intersectsWithSides(r))break;this._rearrange(t,r),this._trigger("change",t,this._uiHash());break}}return this._contactContainers(t),e.ui.ddmanager&&e.ui.ddmanager.drag(this,t),this._trigger("sort",t,this._uiHash()),this.lastPositionAbs=this.positionAbs,!1},_mouseStop:function(t,n){if(!t)return;e.ui.ddmanager&&!this.options.dropBehaviour&&e.ui.ddmanager.drop(this,t);if(this.options.revert){var r=this,i=this.placeholder.offset();this.reverting=!0,e(this.helper).animate({left:i.left-this.offset.parent.left-this.margins.left+(this.offsetParent[0]===document.body?0:this.offsetParent[0].scrollLeft),top:i.top-this.offset.parent.top-this.margins.top+(this.offsetParent[0]===document.body?0:this.offsetParent[0].scrollTop)},parseInt(this.options.revert,10)||500,function(){r._clear(t)})}else this._clear(t,n);return!1},cancel:function(){if(this.dragging){this._mouseUp({target:null}),this.options.helper==="original"?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"):this.currentItem.show();for(var t=this.containers.length-1;t>=0;t--)this.containers[t]._trigger("deactivate",null,this._uiHash(this)),this.containers[t].containerCache.over&&(this.containers[t]._trigger("out",null,this._uiHash(this)),this.containers[t].containerCache.over=0)}return this.placeholder&&(this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.options.helper!=="original"&&this.helper&&this.helper[0].parentNode&&this.helper.remove(),e.extend(this,{helper:null,dragging:!1,reverting:!1,_noFinalSort:null}),this.domPosition.prev?e(this.domPosition.prev).after(this.currentItem):e(this.domPosition.parent).prepend(this.currentItem)),this},serialize:function(t){var n=this._getItemsAsjQuery(t&&t.connected),r=[];return t=t||{},e(n).each(function(){var n=(e(t.item||this).attr(t.attribute||"id")||"").match(t.expression||/(.+)[\-=_](.+)/);n&&r.push((t.key||n[1]+"[]")+"="+(t.key&&t.expression?n[1]:n[2]))}),!r.length&&t.key&&r.push(t.key+"="),r.join("&")},toArray:function(t){var n=this._getItemsAsjQuery(t&&t.connected),r=[];return t=t||{},n.each(function(){r.push(e(t.item||this).attr(t.attribute||"id")||"")}),r},_intersectsWith:function(e){var t=this.positionAbs.left,n=t+this.helperProportions.width,r=this.positionAbs.top,i=r+this.helperProportions.height,s=e.left,o=s+e.width,u=e.top,a=u+e.height,f=this.offset.click.top,l=this.offset.click.left,c=r+f>u&&r+f<a&&t+l>s&&t+l<o;return this.options.tolerance==="pointer"||this.options.forcePointerForContainers||this.options.tolerance!=="pointer"&&this.helperProportions[this.floating?"width":"height"]>e[this.floating?"width":"height"]?c:s<t+this.helperProportions.width/2&&n-this.helperProportions.width/2<o&&u<r+this.helperProportions.height/2&&i-this.helperProportions.height/2<a},_intersectsWithPointer:function(e){var t=this.options.axis==="x"||n(this.positionAbs.top+this.offset.click.top,e.top,e.height),r=this.options.axis==="y"||n(this.positionAbs.left+this.offset.click.left,e.left,e.width),i=t&&r,s=this._getDragVerticalDirection(),o=this._getDragHorizontalDirection();return i?this.floating?o&&o==="right"||s==="down"?2:1:s&&(s==="down"?2:1):!1},_intersectsWithSides:function(e){var t=n(this.positionAbs.top+this.offset.click.top,e.top+e.height/2,e.height),r=n(this.positionAbs.left+this.offset.click.left,e.left+e.width/2,e.width),i=this._getDragVerticalDirection(),s=this._getDragHorizontalDirection();return this.floating&&s?s==="right"&&r||s==="left"&&!r:i&&(i==="down"&&t||i==="up"&&!t)},_getDragVerticalDirection:function(){var e=this.positionAbs.top-this.lastPositionAbs.top;return e!==0&&(e>0?"down":"up")},_getDragHorizontalDirection:function(){var e=this.positionAbs.left-this.lastPositionAbs.left;return e!==0&&(e>0?"right":"left")},refresh:function(e){return this._refreshItems(e),this.refreshPositions(),this},_connectWith:function(){var e=this.options;return e.connectWith.constructor===String?[e.connectWith]:e.connectWith},_getItemsAsjQuery:function(t){var n,r,i,s,o=[],u=[],a=this._connectWith();if(a&&t)for(n=a.length-1;n>=0;n--){i=e(a[n]);for(r=i.length-1;r>=0;r--)s=e.data(i[r],this.widgetFullName),s&&s!==this&&!s.options.disabled&&u.push([e.isFunction(s.options.items)?s.options.items.call(s.element):e(s.options.items,s.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),s])}u.push([e.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):e(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]);for(n=u.length-1;n>=0;n--)u[n][0].each(function(){o.push(this)});return e(o)},_removeCurrentsFromItems:function(){var t=this.currentItem.find(":data("+this.widgetName+"-item)");this.items=e.grep(this.items,function(e){for(var n=0;n<t.length;n++)if(t[n]===e.item[0])return!1;return!0})},_refreshItems:function(t){this.items=[],this.containers=[this];var n,r,i,s,o,u,a,f,l=this.items,c=[[e.isFunction(this.options.items)?this.options.items.call(this.element[0],t,{item:this.currentItem}):e(this.options.items,this.element),this]],h=this._connectWith();if(h&&this.ready)for(n=h.length-1;n>=0;n--){i=e(h[n]);for(r=i.length-1;r>=0;r--)s=e.data(i[r],this.widgetFullName),s&&s!==this&&!s.options.disabled&&(c.push([e.isFunction(s.options.items)?s.options.items.call(s.element[0],t,{item:this.currentItem}):e(s.options.items,s.element),s]),this.containers.push(s))}for(n=c.length-1;n>=0;n--){o=c[n][1],u=c[n][0];for(r=0,f=u.length;r<f;r++)a=e(u[r]),a.data(this.widgetName+"-item",o),l.push({item:a,instance:o,width:0,height:0,left:0,top:0})}},refreshPositions:function(t){this.offsetParent&&this.helper&&(this.offset.parent=this._getParentOffset());var n,r,i,s;for(n=this.items.length-1;n>=0;n--){r=this.items[n];if(r.instance!==this.currentContainer&&this.currentContainer&&r.item[0]!==this.currentItem[0])continue;i=this.options.toleranceElement?e(this.options.toleranceElement,r.item):r.item,t||(r.width=i.outerWidth(),r.height=i.outerHeight()),s=i.offset(),r.left=s.left,r.top=s.top}if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(n=this.containers.length-1;n>=0;n--)s=this.containers[n].element.offset(),this.containers[n].containerCache.left=s.left,this.containers[n].containerCache.top=s.top,this.containers[n].containerCache.width=this.containers[n].element.outerWidth(),this.containers[n].containerCache.height=this.containers[n].element.outerHeight();return this},_createPlaceholder:function(t){t=t||this;var n,r=t.options;if(!r.placeholder||r.placeholder.constructor===String)n=r.placeholder,r.placeholder={element:function(){var r=e(document.createElement(t.currentItem[0].nodeName)).addClass(n||t.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper")[0];return n||(r.style.visibility="hidden"),r},update:function(e,i){if(n&&!r.forcePlaceholderSize)return;i.height()||i.height(t.currentItem.innerHeight()-parseInt(t.currentItem.css("paddingTop")||0,10)-parseInt(t.currentItem.css("paddingBottom")||0,10)),i.width()||i.width(t.currentItem.innerWidth()-parseInt(t.currentItem.css("paddingLeft")||0,10)-parseInt(t.currentItem.css("paddingRight")||0,10))}};t.placeholder=e(r.placeholder.element.call(t.element,t.currentItem)),t.currentItem.after(t.placeholder),r.placeholder.update(t,t.placeholder)},_contactContainers:function(t){var n,r,i,s,o,u,a,f,l,c=null,h=null;for(n=this.containers.length-1;n>=0;n--){if(e.contains(this.currentItem[0],this.containers[n].element[0]))continue;if(this._intersectsWith(this.containers[n].containerCache)){if(c&&e.contains(this.containers[n].element[0],c.element[0]))continue;c=this.containers[n],h=n}else this.containers[n].containerCache.over&&(this.containers[n]._trigger("out",t,this._uiHash(this)),this.containers[n].containerCache.over=0)}if(!c)return;if(this.containers.length===1)this.containers[h]._trigger("over",t,this._uiHash(this)),this.containers[h].containerCache.over=1;else{i=1e4,s=null,o=this.containers[h].floating?"left":"top",u=this.containers[h].floating?"width":"height",a=this.positionAbs[o]+this.offset.click[o];for(r=this.items.length-1;r>=0;r--){if(!e.contains(this.containers[h].element[0],this.items[r].item[0]))continue;if(this.items[r].item[0]===this.currentItem[0])continue;f=this.items[r].item.offset()[o],l=!1,Math.abs(f-a)>Math.abs(f+this.items[r][u]-a)&&(l=!0,f+=this.items[r][u]),Math.abs(f-a)<i&&(i=Math.abs(f-a),s=this.items[r],this.direction=l?"up":"down")}if(!s&&!this.options.dropOnEmpty)return;this.currentContainer=this.containers[h],s?this._rearrange(t,s,null,!0):this._rearrange(t,null,this.containers[h].element,!0),this._trigger("change",t,this._uiHash()),this.containers[h]._trigger("change",t,this._uiHash(this)),this.options.placeholder.update(this.currentContainer,this.placeholder),this.containers[h]._trigger("over",t,this._uiHash(this)),this.containers[h].containerCache.over=1}},_createHelper:function(t){var n=this.options,r=e.isFunction(n.helper)?e(n.helper.apply(this.element[0],[t,this.currentItem])):n.helper==="clone"?this.currentItem.clone():this.currentItem;return r.parents("body").length||e(n.appendTo!=="parent"?n.appendTo:this.currentItem[0].parentNode)[0].appendChild(r[0]),r[0]===this.currentItem[0]&&(this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")}),(!r[0].style.width||n.forceHelperSize)&&r.width(this.currentItem.width()),(!r[0].style.height||n.forceHelperSize)&&r.height(this.currentItem.height()),r},_adjustOffsetFromHelper:function(t){typeof t=="string"&&(t=t.split(" ")),e.isArray(t)&&(t={left:+t[0],top:+t[1]||0}),"left"in t&&(this.offset.click.left=t.left+this.margins.left),"right"in t&&(this.offset.click.left=this.helperProportions.width-t.right+this.margins.left),"top"in t&&(this.offset.click.top=t.top+this.margins.top),"bottom"in t&&(this.offset.click.top=this.helperProportions.height-t.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var t=this.offsetParent.offset();this.cssPosition==="absolute"&&this.scrollParent[0]!==document&&e.contains(this.scrollParent[0],this.offsetParent[0])&&(t.left+=this.scrollParent.scrollLeft(),t.top+=this.scrollParent.scrollTop());if(this.offsetParent[0]===document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()==="html"&&e.ui.ie)t={top:0,left:0};return{top:t.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:t.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition==="relative"){var e=this.currentItem.position();return{top:e.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:e.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"),10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var t,n,r,i=this.options;i.containment==="parent"&&(i.containment=this.helper[0].parentNode);if(i.containment==="document"||i.containment==="window")this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,e(i.containment==="document"?document:window).width()-this.helperProportions.width-this.margins.left,(e(i.containment==="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];/^(document|window|parent)$/.test(i.containment)||(t=e(i.containment)[0],n=e(i.containment).offset(),r=e(t).css("overflow")!=="hidden",this.containment=[n.left+(parseInt(e(t).css("borderLeftWidth"),10)||0)+(parseInt(e(t).css("paddingLeft"),10)||0)-this.margins.left,n.top+(parseInt(e(t).css("borderTopWidth"),10)||0)+(parseInt(e(t).css("paddingTop"),10)||0)-this.margins.top,n.left+(r?Math.max(t.scrollWidth,t.offsetWidth):t.offsetWidth)-(parseInt(e(t).css("borderLeftWidth"),10)||0)-(parseInt(e(t).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,n.top+(r?Math.max(t.scrollHeight,t.offsetHeight):t.offsetHeight)-(parseInt(e(t).css("borderTopWidth"),10)||0)-(parseInt(e(t).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top])},_convertPositionTo:function(t,n){n||(n=this.position);var r=t==="absolute"?1:-1,i=this.cssPosition!=="absolute"||this.scrollParent[0]!==document&&!!e.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,s=/(html|body)/i.test(i[0].tagName);return{top:n.top+this.offset.relative.top*r+this.offset.parent.top*r-(this.cssPosition==="fixed"?-this.scrollParent.scrollTop():s?0:i.scrollTop())*r,left:n.left+this.offset.relative.left*r+this.offset.parent.left*r-(this.cssPosition==="fixed"?-this.scrollParent.scrollLeft():s?0:i.scrollLeft())*r}},_generatePosition:function(t){var n,r,i=this.options,s=t.pageX,o=t.pageY,u=this.cssPosition!=="absolute"||this.scrollParent[0]!==document&&!!e.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,a=/(html|body)/i.test(u[0].tagName);return this.cssPosition==="relative"&&(this.scrollParent[0]===document||this.scrollParent[0]===this.offsetParent[0])&&(this.offset.relative=this._getRelativeOffset()),this.originalPosition&&(this.containment&&(t.pageX-this.offset.click.left<this.containment[0]&&(s=this.containment[0]+this.offset.click.left),t.pageY-this.offset.click.top<this.containment[1]&&(o=this.containment[1]+this.offset.click.top),t.pageX-this.offset.click.left>this.containment[2]&&(s=this.containment[2]+this.offset.click.left),t.pageY-this.offset.click.top>this.containment[3]&&(o=this.containment[3]+this.offset.click.top)),i.grid&&(n=this.originalPageY+Math.round((o-this.originalPageY)/i.grid[1])*i.grid[1],o=this.containment?n-this.offset.click.top>=this.containment[1]&&n-this.offset.click.top<=this.containment[3]?n:n-this.offset.click.top>=this.containment[1]?n-i.grid[1]:n+i.grid[1]:n,r=this.originalPageX+Math.round((s-this.originalPageX)/i.grid[0])*i.grid[0],s=this.containment?r-this.offset.click.left>=this.containment[0]&&r-this.offset.click.left<=this.containment[2]?r:r-this.offset.click.left>=this.containment[0]?r-i.grid[0]:r+i.grid[0]:r)),{top:o-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(this.cssPosition==="fixed"?-this.scrollParent.scrollTop():a?0:u.scrollTop()),left:s-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+(this.cssPosition==="fixed"?-this.scrollParent.scrollLeft():a?0:u.scrollLeft())}},_rearrange:function(e,t,n,r){n?n[0].appendChild(this.placeholder[0]):t.item[0].parentNode.insertBefore(this.placeholder[0],this.direction==="down"?t.item[0]:t.item[0].nextSibling),this.counter=this.counter?++this.counter:1;var i=this.counter;this._delay(function(){i===this.counter&&this.refreshPositions(!r)})},_clear:function(t,n){this.reverting=!1;var r,i=[];!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem),this._noFinalSort=null;if(this.helper[0]===this.currentItem[0]){for(r in this._storedCSS)if(this._storedCSS[r]==="auto"||this._storedCSS[r]==="static")this._storedCSS[r]="";this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else this.currentItem.show();this.fromOutside&&!n&&i.push(function(e){this._trigger("receive",e,this._uiHash(this.fromOutside))}),(this.fromOutside||this.domPosition.prev!==this.currentItem.prev().not(".ui-sortable-helper")[0]||this.domPosition.parent!==this.currentItem.parent()[0])&&!n&&i.push(function(e){this._trigger("update",e,this._uiHash())}),this!==this.currentContainer&&(n||(i.push(function(e){this._trigger("remove",e,this._uiHash())}),i.push(function(e){return function(t){e._trigger("receive",t,this._uiHash(this))}}.call(this,this.currentContainer)),i.push(function(e){return function(t){e._trigger("update",t,this._uiHash(this))}}.call(this,this.currentContainer))));for(r=this.containers.length-1;r>=0;r--)n||i.push(function(e){return function(t){e._trigger("deactivate",t,this._uiHash(this))}}.call(this,this.containers[r])),this.containers[r].containerCache.over&&(i.push(function(e){return function(t){e._trigger("out",t,this._uiHash(this))}}.call(this,this.containers[r])),this.containers[r].containerCache.over=0);this._storedCursor&&e("body").css("cursor",this._storedCursor),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex",this._storedZIndex==="auto"?"":this._storedZIndex),this.dragging=!1;if(this.cancelHelperRemoval){if(!n){this._trigger("beforeStop",t,this._uiHash());for(r=0;r<i.length;r++)i[r].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!1}n||this._trigger("beforeStop",t,this._uiHash()),this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.helper[0]!==this.currentItem[0]&&this.helper.remove(),this.helper=null;if(!n){for(r=0;r<i.length;r++)i[r].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!0},_trigger:function(){e.Widget.prototype._trigger.apply(this,arguments)===!1&&this.cancel()},_uiHash:function(t){var n=t||this;return{helper:n.helper,placeholder:n.placeholder||e([]),position:n.position,originalPosition:n.originalPosition,offset:n.positionAbs,item:n.currentItem,sender:t?t.element:null}}})})(jQuery);jQuery.effects||function(e,t){var n="ui-effects-";e.effects={effect:{}},function(e,t){function h(e,t,n){var r=u[t.type]||{};return e==null?n||!t.def?null:t.def:(e=r.floor?~~e:parseFloat(e),isNaN(e)?t.def:r.mod?(e+r.mod)%r.mod:0>e?0:r.max<e?r.max:e)}function p(t){var n=s(),r=n._rgba=[];return t=t.toLowerCase(),c(i,function(e,i){var s,u=i.re.exec(t),a=u&&i.parse(u),f=i.space||"rgba";if(a)return s=n[f](a),n[o[f].cache]=s[o[f].cache],r=n._rgba=s._rgba,!1}),r.length?(r.join()==="0,0,0,0"&&e.extend(r,l.transparent),n):l[t]}function d(e,t,n){return n=(n+1)%1,n*6<1?e+(t-e)*n*6:n*2<1?t:n*3<2?e+(t-e)*(2/3-n)*6:e}var n="backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor",r=/^([\-+])=\s*(\d+\.?\d*)/,i=[{re:/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(e){return[e[1],e[2],e[3],e[4]]}},{re:/rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(e){return[e[1]*2.55,e[2]*2.55,e[3]*2.55,e[4]]}},{re:/#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/,parse:function(e){return[parseInt(e[1],16),parseInt(e[2],16),parseInt(e[3],16)]}},{re:/#([a-f0-9])([a-f0-9])([a-f0-9])/,parse:function(e){return[parseInt(e[1]+e[1],16),parseInt(e[2]+e[2],16),parseInt(e[3]+e[3],16)]}},{re:/hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,space:"hsla",parse:function(e){return[e[1],e[2]/100,e[3]/100,e[4]]}}],s=e.Color=function(t,n,r,i){return new e.Color.fn.parse(t,n,r,i)},o={rgba:{props:{red:{idx:0,type:"byte"},green:{idx:1,type:"byte"},blue:{idx:2,type:"byte"}}},hsla:{props:{hue:{idx:0,type:"degrees"},saturation:{idx:1,type:"percent"},lightness:{idx:2,type:"percent"}}}},u={"byte":{floor:!0,max:255},percent:{max:1},degrees:{mod:360,floor:!0}},a=s.support={},f=e("<p>")[0],l,c=e.each;f.style.cssText="background-color:rgba(1,1,1,.5)",a.rgba=f.style.backgroundColor.indexOf("rgba")>-1,c(o,function(e,t){t.cache="_"+e,t.props.alpha={idx:3,type:"percent",def:1}}),s.fn=e.extend(s.prototype,{parse:function(n,r,i,u){if(n===t)return this._rgba=[null,null,null,null],this;if(n.jquery||n.nodeType)n=e(n).css(r),r=t;var a=this,f=e.type(n),d=this._rgba=[];r!==t&&(n=[n,r,i,u],f="array");if(f==="string")return this.parse(p(n)||l._default);if(f==="array")return c(o.rgba.props,function(e,t){d[t.idx]=h(n[t.idx],t)}),this;if(f==="object")return n instanceof s?c(o,function(e,t){n[t.cache]&&(a[t.cache]=n[t.cache].slice())}):c(o,function(t,r){var i=r.cache;c(r.props,function(e,t){if(!a[i]&&r.to){if(e==="alpha"||n[e]==null)return;a[i]=r.to(a._rgba)}a[i][t.idx]=h(n[e],t,!0)}),a[i]&&e.inArray(null,a[i].slice(0,3))<0&&(a[i][3]=1,r.from&&(a._rgba=r.from(a[i])))}),this},is:function(e){var t=s(e),n=!0,r=this;return c(o,function(e,i){var s,o=t[i.cache];return o&&(s=r[i.cache]||i.to&&i.to(r._rgba)||[],c(i.props,function(e,t){if(o[t.idx]!=null)return n=o[t.idx]===s[t.idx],n})),n}),n},_space:function(){var e=[],t=this;return c(o,function(n,r){t[r.cache]&&e.push(n)}),e.pop()},transition:function(e,t){var n=s(e),r=n._space(),i=o[r],a=this.alpha()===0?s("transparent"):this,f=a[i.cache]||i.to(a._rgba),l=f.slice();return n=n[i.cache],c(i.props,function(e,r){var i=r.idx,s=f[i],o=n[i],a=u[r.type]||{};if(o===null)return;s===null?l[i]=o:(a.mod&&(o-s>a.mod/2?s+=a.mod:s-o>a.mod/2&&(s-=a.mod)),l[i]=h((o-s)*t+s,r))}),this[r](l)},blend:function(t){if(this._rgba[3]===1)return this;var n=this._rgba.slice(),r=n.pop(),i=s(t)._rgba;return s(e.map(n,function(e,t){return(1-r)*i[t]+r*e}))},toRgbaString:function(){var t="rgba(",n=e.map(this._rgba,function(e,t){return e==null?t>2?1:0:e});return n[3]===1&&(n.pop(),t="rgb("),t+n.join()+")"},toHslaString:function(){var t="hsla(",n=e.map(this.hsla(),function(e,t){return e==null&&(e=t>2?1:0),t&&t<3&&(e=Math.round(e*100)+"%"),e});return n[3]===1&&(n.pop(),t="hsl("),t+n.join()+")"},toHexString:function(t){var n=this._rgba.slice(),r=n.pop();return t&&n.push(~~(r*255)),"#"+e.map(n,function(e){return e=(e||0).toString(16),e.length===1?"0"+e:e}).join("")},toString:function(){return this._rgba[3]===0?"transparent":this.toRgbaString()}}),s.fn.parse.prototype=s.fn,o.hsla.to=function(e){if(e[0]==null||e[1]==null||e[2]==null)return[null,null,null,e[3]];var t=e[0]/255,n=e[1]/255,r=e[2]/255,i=e[3],s=Math.max(t,n,r),o=Math.min(t,n,r),u=s-o,a=s+o,f=a*.5,l,c;return o===s?l=0:t===s?l=60*(n-r)/u+360:n===s?l=60*(r-t)/u+120:l=60*(t-n)/u+240,u===0?c=0:f<=.5?c=u/a:c=u/(2-a),[Math.round(l)%360,c,f,i==null?1:i]},o.hsla.from=function(e){if(e[0]==null||e[1]==null||e[2]==null)return[null,null,null,e[3]];var t=e[0]/360,n=e[1],r=e[2],i=e[3],s=r<=.5?r*(1+n):r+n-r*n,o=2*r-s;return[Math.round(d(o,s,t+1/3)*255),Math.round(d(o,s,t)*255),Math.round(d(o,s,t-1/3)*255),i]},c(o,function(n,i){var o=i.props,u=i.cache,a=i.to,f=i.from;s.fn[n]=function(n){a&&!this[u]&&(this[u]=a(this._rgba));if(n===t)return this[u].slice();var r,i=e.type(n),l=i==="array"||i==="object"?n:arguments,p=this[u].slice();return c(o,function(e,t){var n=l[i==="object"?e:t.idx];n==null&&(n=p[t.idx]),p[t.idx]=h(n,t)}),f?(r=s(f(p)),r[u]=p,r):s(p)},c(o,function(t,i){if(s.fn[t])return;s.fn[t]=function(s){var o=e.type(s),u=t==="alpha"?this._hsla?"hsla":"rgba":n,a=this[u](),f=a[i.idx],l;return o==="undefined"?f:(o==="function"&&(s=s.call(this,f),o=e.type(s)),s==null&&i.empty?this:(o==="string"&&(l=r.exec(s),l&&(s=f+parseFloat(l[2])*(l[1]==="+"?1:-1))),a[i.idx]=s,this[u](a)))}})}),s.hook=function(t){var n=t.split(" ");c(n,function(t,n){e.cssHooks[n]={set:function(t,r){var i,o,u="";if(r!=="transparent"&&(e.type(r)!=="string"||(i=p(r)))){r=s(i||r);if(!a.rgba&&r._rgba[3]!==1){o=n==="backgroundColor"?t.parentNode:t;while((u===""||u==="transparent")&&o&&o.style)try{u=e.css(o,"backgroundColor"),o=o.parentNode}catch(f){}r=r.blend(u&&u!=="transparent"?u:"_default")}r=r.toRgbaString()}try{t.style[n]=r}catch(f){}}},e.fx.step[n]=function(t){t.colorInit||(t.start=s(t.elem,n),t.end=s(t.end),t.colorInit=!0),e.cssHooks[n].set(t.elem,t.start.transition(t.end,t.pos))}})},s.hook(n),e.cssHooks.borderColor={expand:function(e){var t={};return c(["Top","Right","Bottom","Left"],function(n,r){t["border"+r+"Color"]=e}),t}},l=e.Color.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00",transparent:[null,null,null,0],_default:"#ffffff"}}(jQuery),function(){function i(t){var n,r,i=t.ownerDocument.defaultView?t.ownerDocument.defaultView.getComputedStyle(t,null):t.currentStyle,s={};if(i&&i.length&&i[0]&&i[i[0]]){r=i.length;while(r--)n=i[r],typeof i[n]=="string"&&(s[e.camelCase(n)]=i[n])}else for(n in i)typeof i[n]=="string"&&(s[n]=i[n]);return s}function s(t,n){var i={},s,o;for(s in n)o=n[s],t[s]!==o&&!r[s]&&(e.fx.step[s]||!isNaN(parseFloat(o)))&&(i[s]=o);return i}var n=["add","remove","toggle"],r={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};e.each(["borderLeftStyle","borderRightStyle","borderBottomStyle","borderTopStyle"],function(t,n){e.fx.step[n]=function(e){if(e.end!=="none"&&!e.setAttr||e.pos===1&&!e.setAttr)jQuery.style(e.elem,n,e.end),e.setAttr=!0}}),e.fn.addBack||(e.fn.addBack=function(e){return this.add(e==null?this.prevObject:this.prevObject.filter(e))}),e.effects.animateClass=function(t,r,o,u){var a=e.speed(r,o,u);return this.queue(function(){var r=e(this),o=r.attr("class")||"",u,f=a.children?r.find("*").addBack():r;f=f.map(function(){var t=e(this);return{el:t,start:i(this)}}),u=function(){e.each(n,function(e,n){t[n]&&r[n+"Class"](t[n])})},u(),f=f.map(function(){return this.end=i(this.el[0]),this.diff=s(this.start,this.end),this}),r.attr("class",o),f=f.map(function(){var t=this,n=e.Deferred(),r=e.extend({},a,{queue:!1,complete:function(){n.resolve(t)}});return this.el.animate(this.diff,r),n.promise()}),e.when.apply(e,f.get()).done(function(){u(),e.each(arguments,function(){var t=this.el;e.each(this.diff,function(e){t.css(e,"")})}),a.complete.call(r[0])})})},e.fn.extend({_addClass:e.fn.addClass,addClass:function(t,n,r,i){return n?e.effects.animateClass.call(this,{add:t},n,r,i):this._addClass(t)},_removeClass:e.fn.removeClass,removeClass:function(t,n,r,i){return n?e.effects.animateClass.call(this,{remove:t},n,r,i):this._removeClass(t)},_toggleClass:e.fn.toggleClass,toggleClass:function(n,r,i,s,o){return typeof r=="boolean"||r===t?i?e.effects.animateClass.call(this,r?{add:n}:{remove:n},i,s,o):this._toggleClass(n,r):e.effects.animateClass.call(this,{toggle:n},r,i,s)},switchClass:function(t,n,r,i,s){return e.effects.animateClass.call(this,{add:n,remove:t},r,i,s)}})}(),function(){function r(t,n,r,i){e.isPlainObject(t)&&(n=t,t=t.effect),t={effect:t},n==null&&(n={}),e.isFunction(n)&&(i=n,r=null,n={});if(typeof n=="number"||e.fx.speeds[n])i=r,r=n,n={};return e.isFunction(r)&&(i=r,r=null),n&&e.extend(t,n),r=r||n.duration,t.duration=e.fx.off?0:typeof r=="number"?r:r in e.fx.speeds?e.fx.speeds[r]:e.fx.speeds._default,t.complete=i||n.complete,t}function i(t){return!t||typeof t=="number"||e.fx.speeds[t]?!0:typeof t=="string"&&!e.effects.effect[t]}e.extend(e.effects,{version:"1.10.0",save:function(e,t){for(var r=0;r<t.length;r++)t[r]!==null&&e.data(n+t[r],e[0].style[t[r]])},restore:function(e,r){var i,s;for(s=0;s<r.length;s++)r[s]!==null&&(i=e.data(n+r[s]),i===t&&(i=""),e.css(r[s],i))},setMode:function(e,t){return t==="toggle"&&(t=e.is(":hidden")?"show":"hide"),t},getBaseline:function(e,t){var n,r;switch(e[0]){case"top":n=0;break;case"middle":n=.5;break;case"bottom":n=1;break;default:n=e[0]/t.height}switch(e[1]){case"left":r=0;break;case"center":r=.5;break;case"right":r=1;break;default:r=e[1]/t.width}return{x:r,y:n}},createWrapper:function(t){if(t.parent().is(".ui-effects-wrapper"))return t.parent();var n={width:t.outerWidth(!0),height:t.outerHeight(!0),"float":t.css("float")},r=e("<div></div>").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),i={width:t.width(),height:t.height()},s=document.activeElement;try{s.id}catch(o){s=document.body}return t.wrap(r),(t[0]===s||e.contains(t[0],s))&&e(s).focus(),r=t.parent(),t.css("position")==="static"?(r.css({position:"relative"}),t.css({position:"relative"})):(e.extend(n,{position:t.css("position"),zIndex:t.css("z-index")}),e.each(["top","left","bottom","right"],function(e,r){n[r]=t.css(r),isNaN(parseInt(n[r],10))&&(n[r]="auto")}),t.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),t.css(i),r.css(n).show()},removeWrapper:function(t){var n=document.activeElement;return t.parent().is(".ui-effects-wrapper")&&(t.parent().replaceWith(t),(t[0]===n||e.contains(t[0],n))&&e(n).focus()),t},setTransition:function(t,n,r,i){return i=i||{},e.each(n,function(e,n){var s=t.cssUnit(n);s[0]>0&&(i[n]=s[0]*r+s[1])}),i}}),e.fn.extend({effect:function(){function o(n){function u(){e.isFunction(i)&&i.call(r[0]),e.isFunction(n)&&n()}var r=e(this),i=t.complete,o=t.mode;(r.is(":hidden")?o==="hide":o==="show")?u():s.call(r[0],t,u)}var t=r.apply(this,arguments),n=t.mode,i=t.queue,s=e.effects.effect[t.effect];return e.fx.off||!s?n?this[n](t.duration,t.complete):this.each(function(){t.complete&&t.complete.call(this)}):i===!1?this.each(o):this.queue(i||"fx",o)},_show:e.fn.show,show:function(e){if(i(e))return this._show.apply(this,arguments);var t=r.apply(this,arguments);return t.mode="show",this.effect.call(this,t)},_hide:e.fn.hide,hide:function(e){if(i(e))return this._hide.apply(this,arguments);var t=r.apply(this,arguments);return t.mode="hide",this.effect.call(this,t)},__toggle:e.fn.toggle,toggle:function(t){if(i(t)||typeof t=="boolean"||e.isFunction(t))return this.__toggle.apply(this,arguments);var n=r.apply(this,arguments);return n.mode="toggle",this.effect.call(this,n)},cssUnit:function(t){var n=this.css(t),r=[];return e.each(["em","px","%","pt"],function(e,t){n.indexOf(t)>0&&(r=[parseFloat(n),t])}),r}})}(),function(){var t={};e.each(["Quad","Cubic","Quart","Quint","Expo"],function(e,n){t[n]=function(t){return Math.pow(t,e+2)}}),e.extend(t,{Sine:function(e){return 1-Math.cos(e*Math.PI/2)},Circ:function(e){return 1-Math.sqrt(1-e*e)},Elastic:function(e){return e===0||e===1?e:-Math.pow(2,8*(e-1))*Math.sin(((e-1)*80-7.5)*Math.PI/15)},Back:function(e){return e*e*(3*e-2)},Bounce:function(e){var t,n=4;while(e<((t=Math.pow(2,--n))-1)/11);return 1/Math.pow(4,3-n)-7.5625*Math.pow((t*3-2)/22-e,2)}}),e.each(t,function(t,n){e.easing["easeIn"+t]=n,e.easing["easeOut"+t]=function(e){return 1-n(1-e)},e.easing["easeInOut"+t]=function(e){return e<.5?n(e*2)/2:1-n(e*-2+2)/2}})}()}(jQuery);(function(e,t){e.effects.effect.fade=function(t,n){var r=e(this),i=e.effects.setMode(r,t.mode||"toggle");r.animate({opacity:i},{queue:!1,duration:t.duration,easing:t.easing,complete:n})}})(jQuery);(function(e,t){e.effects.effect.highlight=function(t,n){var r=e(this),i=["backgroundImage","backgroundColor","opacity"],s=e.effects.setMode(r,t.mode||"show"),o={backgroundColor:r.css("backgroundColor")};s==="hide"&&(o.opacity=0),e.effects.save(r,i),r.show().css({backgroundImage:"none",backgroundColor:t.color||"#ffff99"}).animate(o,{queue:!1,duration:t.duration,easing:t.easing,complete:function(){s==="hide"&&r.hide(),e.effects.restore(r,i),n()}})}})(jQuery); /* =================================================== - * bootstrap-transition.js v2.3.0 + * bootstrap-transition.js v2.3.2 * http://twitter.github.com/bootstrap/javascript.html#transitions * =================================================== * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -10250,11 +10250,11 @@ })() }) }(window.jQuery);/* ========================================================== - * bootstrap-alert.js v2.3.0 + * bootstrap-alert.js v2.3.2 * http://twitter.github.com/bootstrap/javascript.html#alerts * ========================================================== * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -10348,11 +10348,11 @@ * ============== */ $(document).on('click.alert.data-api', dismiss, Alert.prototype.close) }(window.jQuery);/* ============================================================ - * bootstrap-button.js v2.3.0 + * bootstrap-button.js v2.3.2 * http://twitter.github.com/bootstrap/javascript.html#buttons * ============================================================ * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -10452,11 +10452,11 @@ if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') $btn.button('toggle') }) }(window.jQuery);/* ========================================================== - * bootstrap-carousel.js v2.3.0 + * bootstrap-carousel.js v2.3.2 * http://twitter.github.com/bootstrap/javascript.html#carousel * ========================================================== * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -10528,11 +10528,11 @@ , pause: function (e) { if (!e) this.paused = true if (this.$element.find('.next, .prev').length && $.support.transition.end) { this.$element.trigger($.support.transition.end) - this.cycle() + this.cycle(true) } clearInterval(this.interval) this.interval = null return this } @@ -10658,11 +10658,11 @@ e.preventDefault() }) }(window.jQuery);/* ============================================================= - * bootstrap-collapse.js v2.3.0 + * bootstrap-collapse.js v2.3.2 * http://twitter.github.com/bootstrap/javascript.html#collapse * ============================================================= * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -10824,11 +10824,11 @@ $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') $(target).collapse(option) }) }(window.jQuery);/* ============================================================ - * bootstrap-dropdown.js v2.3.0 + * bootstrap-dropdown.js v2.3.2 * http://twitter.github.com/bootstrap/javascript.html#dropdowns * ============================================================ * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -10877,10 +10877,14 @@ isActive = $parent.hasClass('open') clearMenus() if (!isActive) { + if ('ontouchstart' in document.documentElement) { + // if mobile we we use a backdrop because click events don't delegate + $('<div class="dropdown-backdrop"/>').insertBefore($(this)).on('click', clearMenus) + } $parent.toggleClass('open') } $this.focus() @@ -10929,10 +10933,11 @@ } } function clearMenus() { + $('.dropdown-backdrop').remove() $(toggle).each(function () { getParent($(this)).removeClass('open') }) } @@ -10983,17 +10988,16 @@ * =================================== */ $(document) .on('click.dropdown.data-api', clearMenus) .on('click.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) - .on('.dropdown-menu', function (e) { e.stopPropagation() }) .on('click.dropdown.data-api' , toggle, Dropdown.prototype.toggle) .on('keydown.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) }(window.jQuery); /* ========================================================= - * bootstrap-modal.js v2.3.0 + * bootstrap-modal.js v2.3.2 * http://twitter.github.com/bootstrap/javascript.html#modals * ========================================================= * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -11138,11 +11142,11 @@ that.$element.trigger('hidden') }) } , removeBackdrop: function () { - this.$backdrop.remove() + this.$backdrop && this.$backdrop.remove() this.$backdrop = null } , backdrop: function (callback) { var that = this @@ -11236,11 +11240,11 @@ }) }) }(window.jQuery); /* =========================================================== - * bootstrap-tooltip.js v2.3.0 + * bootstrap-tooltip.js v2.3.2 * http://twitter.github.com/bootstrap/javascript.html#tooltips * Inspired by the original jQuery.tipsy by Jason Frame * =========================================================== * Copyright 2012 Twitter, Inc. * @@ -11317,20 +11321,20 @@ return options } , enter: function (e) { - // Added by Contour - // https://github.com/twitter/bootstrap/issues/6232#issuecomment-13458685 - if (this.options.trigger == 'hover' && 'ontouchstart' in document.documentElement) { - // Block tooltip event on touch devices since it swallows click events - return; - } - // End of Added by Contour + var defaults = $.fn[this.type].defaults + , options = {} + , self - var self = $(e.currentTarget)[this.type](this._options).data(this.type) + this._options && $.each(this._options, function (key, value) { + if (defaults[key] != value) options[key] = value + }, this) + self = $(e.currentTarget)[this.type](options).data(this.type) + if (!self.options.delay || !self.options.delay.show) return self.show() clearTimeout(this.timeout) self.hoverState = 'in' this.timeout = setTimeout(function() { @@ -11597,11 +11601,11 @@ return this } }(window.jQuery); /* =========================================================== - * bootstrap-popover.js v2.3.0 + * bootstrap-popover.js v2.3.2 * http://twitter.github.com/bootstrap/javascript.html#popovers * =========================================================== * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -11711,11 +11715,11 @@ return this } }(window.jQuery); /* ============================================================= - * bootstrap-scrollspy.js v2.3.0 + * bootstrap-scrollspy.js v2.3.2 * http://twitter.github.com/bootstrap/javascript.html#scrollspy * ============================================================= * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -11872,11 +11876,11 @@ $spy.scrollspy($spy.data()) }) }) }(window.jQuery);/* ======================================================== - * bootstrap-tab.js v2.3.0 + * bootstrap-tab.js v2.3.2 * http://twitter.github.com/bootstrap/javascript.html#tabs * ======================================================== * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -12015,11 +12019,11 @@ e.preventDefault() $(this).tab('show') }) }(window.jQuery);/* ============================================================= - * bootstrap-typeahead.js v2.3.0 + * bootstrap-typeahead.js v2.3.2 * http://twitter.github.com/bootstrap/javascript.html#typeahead * ============================================================= * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -12350,11 +12354,11 @@ $this.typeahead($this.data()) }) }(window.jQuery); /* ========================================================== - * bootstrap-affix.js v2.3.0 + * bootstrap-affix.js v2.3.2 * http://twitter.github.com/bootstrap/javascript.html#affix * ========================================================== * Copyright 2012 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -12756,19 +12760,19 @@ }).call(this); // ==ClosureCompiler== // @compilation_level SIMPLE_OPTIMIZATIONS /** - * @license Highcharts JS v2.3.3 (2012-10-04) + * @license Highcharts JS v3.0.2 (2013-06-05) * - * (c) 2009-2011 Torstein Hønsi + * (c) 2009-2013 Torstein Hønsi * * License: www.highcharts.com/license */ // JSLint options: -/*global Highcharts, document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $, console */ +/*global Highcharts, document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $, console, each, grep */ (function () { // encapsulated variables var UNDEFINED, @@ -12792,10 +12796,11 @@ isOpera = win.opera, isIE = /msie/i.test(userAgent) && !isOpera, docMode8 = doc.documentMode === 8, isWebKit = /AppleWebKit/.test(userAgent), isFirefox = /Firefox/.test(userAgent), + isTouchDevice = /(Mobile|Android|Windows Phone)/.test(userAgent), SVG_NS = 'http://www.w3.org/2000/svg', hasSVG = !!doc.createElementNS && !!doc.createElementNS(SVG_NS, 'svg').createSVGRect, hasBidiBug = isFirefox && parseInt(userAgent.split('Firefox/')[1], 10) < 4, // issue #38 useCanVG = !hasSVG && !isIE && !!doc.createElement('canvas').getContext, Renderer, @@ -12807,10 +12812,13 @@ dateFormat, // function globalAnimation, pathAnim, timeUnits, noop = function () {}, + charts = [], + PRODUCT = 'Highcharts', + VERSION = '3.0.2', // some constants for frequently used strings DIV = 'div', ABSOLUTE = 'absolute', RELATIVE = 'relative', @@ -12825,16 +12833,17 @@ * Empirical lowest possible opacities for TRACKER_FILL * IE6: 0.002 * IE7: 0.002 * IE8: 0.002 * IE9: 0.00000000001 (unlimited) + * IE10: 0.0001 (exporting only) * FF: 0.00000000001 (unlimited) * Chrome: 0.000001 * Safari: 0.000001 * Opera: 0.00000000001 (unlimited) */ - TRACKER_FILL = 'rgba(192,192,192,' + (hasSVG ? 0.000001 : 0.002) + ')', // invisible but clickable + TRACKER_FILL = 'rgba(192,192,192,' + (hasSVG ? 0.0001 : 0.002) + ')', // invisible but clickable //TRACKER_FILL = 'rgba(192,192,192,0.5)', NORMAL_STATE = '', HOVER_STATE = 'hover', SELECT_STATE = 'select', MILLISECOND = 'millisecond', @@ -12845,14 +12854,12 @@ WEEK = 'week', MONTH = 'month', YEAR = 'year', // constants for attributes - FILL = 'fill', LINEAR_GRADIENT = 'linearGradient', STOPS = 'stops', - STROKE = 'stroke', STROKE_WIDTH = 'stroke-width', // time methods, changed based on whether or not UTC is used makeTime, getMinutes, @@ -12870,11 +12877,11 @@ // lookup over the types and the associated classes seriesTypes = {}; // The Highcharts namespace -win.Highcharts = {}; +win.Highcharts = win.Highcharts ? error(16, true) : {}; /** * 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 @@ -12887,11 +12894,55 @@ for (n in b) { a[n] = b[n]; } return a; } + +/** + * Deep merge two or more objects and return a third object. + * Previously this function redirected to jQuery.extend(true), but this had two limitations. + * First, it deep merged arrays, which lead to workarounds in Highcharts. Second, + * it copied properties from extended prototypes. + */ +function merge() { + var i, + len = arguments.length, + ret = {}, + doCopy = function (copy, original) { + var value, key; + for (key in original) { + if (original.hasOwnProperty(key)) { + value = original[key]; + + // An object is replacing a primitive + if (typeof copy !== 'object') { + copy = {}; + } + + // Copy the contents of objects, but not arrays or DOM nodes + if (value && typeof value === 'object' && Object.prototype.toString.call(value) !== '[object Array]' + && typeof value.nodeType !== 'number') { + copy[key] = doCopy(copy[key] || {}, value); + + // Primitives and arrays are copied over directly + } else { + copy[key] = original[key]; + } + } + } + return copy; + }; + + // For each argument, extend the return + for (i = 0; i < len; i++) { + ret = doCopy(ret, arguments[i]); + } + + return ret; +} + /** * Take an array and turn into a hash with even number arguments as keys and odd numbers as * values. Allows creating constants for commonly used style properties, attributes etc. * Avoid it in performance critical situations like looping */ @@ -13086,22 +13137,10 @@ extend(object.prototype, members); return object; } /** - * How many decimals are there in a number - */ -function getDecimals(number) { - - number = (number || 0).toString(); - - return number.indexOf('.') > -1 ? - number.split('.')[1].length : - 0; -} - -/** * 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 @@ -13109,11 +13148,11 @@ 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 = decimals === -1 ? - getDecimals(number) : + ((n || 0).toString().split('.')[1] || '').length : // preserve decimals (isNaN(decimals = mathAbs(decimals)) ? 2 : decimals), d = decPoint === undefined ? lang.decimalPoint : decPoint, t = thousandsSep === undefined ? lang.thousandsSep : thousandsSep, s = n < 0 ? "-" : "", i = String(pInt(n = mathAbs(+n || 0).toFixed(c))), @@ -13170,23 +13209,13 @@ dayOfMonth = date[getDate](), month = date[getMonth](), fullYear = date[getFullYear](), lang = defaultOptions.lang, langWeekdays = lang.weekdays, - /* // uncomment this and the 'W' format key below to enable week numbers - weekNumber = function () { - var clone = new Date(date.valueOf()), - day = clone[getDay]() == 0 ? 7 : clone[getDay](), - dayNumber; - clone.setDate(clone[getDate]() + 4 - day); - dayNumber = mathFloor((clone.getTime() - new Date(clone[getFullYear](), 0, 1, -6)) / 86400000); - return 1 + mathFloor(dayNumber / 7); - }, - */ - // list all format keys - replacements = { + // List all format keys. Custom formats can be added from the outside. + replacements = extend({ // 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 @@ -13211,23 +13240,99 @@ '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 'L': pad(mathRound(timestamp % 1000), 3) // Milliseconds (naming from Ruby) - }; + }, Highcharts.dateFormats); // do the replaces for (key in replacements) { - format = format.replace('%' + key, replacements[key]); + while (format.indexOf('%' + key) !== -1) { // regex would do it in one line, but this is faster + format = format.replace('%' + key, typeof replacements[key] === 'function' ? replacements[key](timestamp) : replacements[key]); + } } // Optionally capitalize the string and return return capitalize ? format.substr(0, 1).toUpperCase() + format.substr(1) : format; }; +/** + * Format a single variable. Similar to sprintf, without the % prefix. + */ +function formatSingle(format, val) { + var floatRegex = /f$/, + decRegex = /\.([0-9])/, + lang = defaultOptions.lang, + decimals; + + if (floatRegex.test(format)) { // float + decimals = format.match(decRegex); + decimals = decimals ? decimals[1] : -1; + val = numberFormat( + val, + decimals, + lang.decimalPoint, + format.indexOf(',') > -1 ? lang.thousandsSep : '' + ); + } else { + val = dateFormat(format, val); + } + return val; +} + /** + * Format a string according to a subset of the rules of Python's String.format method. + */ +function format(str, ctx) { + var splitter = '{', + isInside = false, + segment, + valueAndFormat, + path, + i, + len, + ret = [], + val, + index; + + while ((index = str.indexOf(splitter)) !== -1) { + + segment = str.slice(0, index); + if (isInside) { // we're on the closing bracket looking back + + valueAndFormat = segment.split(':'); + path = valueAndFormat.shift().split('.'); // get first and leave format + len = path.length; + val = ctx; + + // Assign deeper paths + for (i = 0; i < len; i++) { + val = val[path[i]]; + } + + // Format the replacement + if (valueAndFormat.length) { + val = formatSingle(valueAndFormat.join(':'), val); + } + + // Push the result and advance the cursor + ret.push(val); + + } else { + ret.push(segment); + + } + str = str.slice(index + 1); // the rest + isInside = !isInside; // toggle + splitter = isInside ? '}' : '{'; // now look for next matching bracket + } + ret.push(str); + return ret.join(''); +} + +/** * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5 * @param {Number} interval * @param {Array} multiples * @param {Number} magnitude * @param {Object} options @@ -13365,96 +13470,100 @@ minYear, // used in months and years as a basis for Date.UTC() minDate = new Date(min), interval = normalizedInterval.unitRange, count = normalizedInterval.count; + if (defined(min)) { // #1300 + if (interval >= timeUnits[SECOND]) { // second + minDate.setMilliseconds(0); + minDate.setSeconds(interval >= timeUnits[MINUTE] ? 0 : + count * mathFloor(minDate.getSeconds() / count)); + } - - if (interval >= timeUnits[SECOND]) { // second - minDate.setMilliseconds(0); - minDate.setSeconds(interval >= timeUnits[MINUTE] ? 0 : - count * mathFloor(minDate.getSeconds() / count)); - } - - if (interval >= timeUnits[MINUTE]) { // minute - minDate[setMinutes](interval >= timeUnits[HOUR] ? 0 : - count * mathFloor(minDate[getMinutes]() / count)); - } - - if (interval >= timeUnits[HOUR]) { // hour - minDate[setHours](interval >= timeUnits[DAY] ? 0 : - count * mathFloor(minDate[getHours]() / count)); - } - - if (interval >= timeUnits[DAY]) { // day - minDate[setDate](interval >= timeUnits[MONTH] ? 1 : - count * mathFloor(minDate[getDate]() / count)); - } - - if (interval >= timeUnits[MONTH]) { // month - minDate[setMonth](interval >= timeUnits[YEAR] ? 0 : - count * mathFloor(minDate[getMonth]() / count)); + if (interval >= timeUnits[MINUTE]) { // minute + minDate[setMinutes](interval >= timeUnits[HOUR] ? 0 : + count * mathFloor(minDate[getMinutes]() / count)); + } + + if (interval >= timeUnits[HOUR]) { // hour + minDate[setHours](interval >= timeUnits[DAY] ? 0 : + count * mathFloor(minDate[getHours]() / count)); + } + + if (interval >= timeUnits[DAY]) { // day + minDate[setDate](interval >= timeUnits[MONTH] ? 1 : + count * mathFloor(minDate[getDate]() / count)); + } + + if (interval >= timeUnits[MONTH]) { // month + minDate[setMonth](interval >= timeUnits[YEAR] ? 0 : + count * mathFloor(minDate[getMonth]() / count)); + minYear = minDate[getFullYear](); + } + + if (interval >= timeUnits[YEAR]) { // year + minYear -= minYear % count; + minDate[setFullYear](minYear); + } + + // week is a special case that runs outside the hierarchy + if (interval === timeUnits[WEEK]) { + // get start of current week, independent of count + minDate[setDate](minDate[getDate]() - minDate[getDay]() + + pick(startOfWeek, 1)); + } + + + // get tick positions + i = 1; minYear = minDate[getFullYear](); - } - - if (interval >= timeUnits[YEAR]) { // year - minYear -= minYear % count; - minDate[setFullYear](minYear); - } - - // week is a special case that runs outside the hierarchy - if (interval === timeUnits[WEEK]) { - // get start of current week, independent of count - minDate[setDate](minDate[getDate]() - minDate[getDay]() + - pick(startOfWeek, 1)); - } - - - // get tick positions - i = 1; - minYear = minDate[getFullYear](); - var time = minDate.getTime(), - minMonth = minDate[getMonth](), - minDateDate = minDate[getDate](), - timezoneOffset = useUTC ? - 0 : - (24 * 3600 * 1000 + minDate.getTimezoneOffset() * 60 * 1000) % (24 * 3600 * 1000); // #950 - - // iterate and add tick positions at appropriate values - while (time < max) { - tickPositions.push(time); - - // if the interval is years, use Date.UTC to increase years - if (interval === timeUnits[YEAR]) { - time = makeTime(minYear + i * count, 0); - - // if the interval is months, use Date.UTC to increase months - } else if (interval === timeUnits[MONTH]) { - time = makeTime(minYear, minMonth + i * count); - - // if we're using global time, the interval is not fixed as it jumps - // one hour at the DST crossover - } else if (!useUTC && (interval === timeUnits[DAY] || interval === timeUnits[WEEK])) { - time = makeTime(minYear, minMonth, minDateDate + - i * count * (interval === timeUnits[DAY] ? 1 : 7)); - - // else, the interval is fixed and we use simple addition - } else { - time += interval * count; - - // mark new days if the time is dividable by day - if (interval <= timeUnits[HOUR] && time % timeUnits[DAY] === timezoneOffset) { - higherRanks[time] = DAY; + var time = minDate.getTime(), + minMonth = minDate[getMonth](), + minDateDate = minDate[getDate](), + timezoneOffset = useUTC ? + 0 : + (24 * 3600 * 1000 + minDate.getTimezoneOffset() * 60 * 1000) % (24 * 3600 * 1000); // #950 + + // iterate and add tick positions at appropriate values + while (time < max) { + tickPositions.push(time); + + // if the interval is years, use Date.UTC to increase years + if (interval === timeUnits[YEAR]) { + time = makeTime(minYear + i * count, 0); + + // if the interval is months, use Date.UTC to increase months + } else if (interval === timeUnits[MONTH]) { + time = makeTime(minYear, minMonth + i * count); + + // if we're using global time, the interval is not fixed as it jumps + // one hour at the DST crossover + } else if (!useUTC && (interval === timeUnits[DAY] || interval === timeUnits[WEEK])) { + time = makeTime(minYear, minMonth, minDateDate + + i * count * (interval === timeUnits[DAY] ? 1 : 7)); + + // else, the interval is fixed and we use simple addition + } else { + time += interval * count; } + + i++; } + + // push the last time + tickPositions.push(time); - i++; + + // mark new days if the time is dividible by day (#1649, #1760) + each(grep(tickPositions, function (time) { + return interval <= timeUnits[HOUR] && time % timeUnits[DAY] === timezoneOffset; + }), function (time) { + higherRanks[time] = DAY; + }); } - - // push the last time - tickPositions.push(time); + // record information on the chosen unit - for dynamic label formatter tickPositions.info = extend(normalizedInterval, { higherRanks: higherRanks, totalRange: interval * count }); @@ -13630,11 +13739,11 @@ SECOND, 1000, MINUTE, 60000, HOUR, 3600000, DAY, 24 * 3600000, WEEK, 7 * 24 * 3600000, - MONTH, 30 * 24 * 3600000, + MONTH, 31 * 24 * 3600000, YEAR, 31556952000 ); /*jslint white: false*/ /** * Path interpolation algorithm used across adapters @@ -13746,23 +13855,23 @@ // extend the animate function to allow SVG animations var Fx = $.fx, Step = Fx.step, dSetter, Tween = $.Tween, - propHooks = Tween && Tween.propHooks; + propHooks = Tween && Tween.propHooks, + opacityHook = $.cssHooks.opacity; /*jslint unparam: true*//* allow unused param x in this function */ $.extend($.easing, { easeOutQuad: function (x, t, b, c, d) { return -c * (t /= d) * (t - 2) + b; } }); /*jslint unparam: false*/ - // extend some methods to check for elem.attr, which means it is a Highcharts SVG object - $.each(['cur', '_default', 'width', 'height'], function (i, fn) { + $.each(['cur', '_default', 'width', 'height', 'opacity'], function (i, fn) { var obj = Step, base, elem; // Handle different parent objects @@ -13793,10 +13902,15 @@ elem.attr(fx.prop, fn === 'cur' ? UNDEFINED : fx.now) : // apply the SVG wrapper's method base.apply(this, arguments); // use jQuery's built-in method }; } }); + + // Extend the opacity getter, needed for fading opacity with IE9 and jQuery 1.10+ + wrap(opacityHook, 'get', function (proceed, elem, computed) { + return elem.attr ? (elem.opacity || 0) : proceed.call(this, elem, computed); + }); // Define the setter function for d (path definitions) dSetter = function (fx) { var elem = fx.elem, @@ -13846,20 +13960,47 @@ return i; } } }; - // Register Highcharts as a jQuery plugin - // TODO: MooTools and prototype as well? - // TODO: StockChart - /*$.fn.highcharts = function(options, callback) { - options.chart = merge(options.chart, { renderTo: this[0] }); - this.chart = new Chart(options, callback); - return this; - };*/ + /** + * Register Highcharts as a plugin in the respective framework + */ + $.fn.highcharts = function () { + var constr = 'Chart', // default constructor + args = arguments, + options, + ret, + chart; + + if (isString(args[0])) { + constr = args[0]; + args = Array.prototype.slice.call(args, 1); + } + options = args[0]; + + // Create the chart + if (options !== UNDEFINED) { + /*jslint unused:false*/ + options.chart = options.chart || {}; + options.chart.renderTo = this[0]; + chart = new Highcharts[constr](options, args[1]); + ret = this; + /*jslint unused:true*/ + } + + // When called without parameters or with the return argument, get a predefined chart + if (options === UNDEFINED) { + ret = charts[attr(this[0], 'data-highcharts-chart')]; + } + + return ret; + }; + }, - + + /** * Downloads a script and executes a callback when done. * @param {String} scriptLocation * @param {Function} callback */ @@ -13900,18 +14041,10 @@ return results; }, /** - * Deep merge two objects and return a third object - */ - merge: function () { - var args = arguments; - return $.extend(true, null, args[0], args[1], args[2], args[3]); - }, - - /** * Get the position of an element relative to the top left of the page */ offset: function (el) { return $(el).offset(); }, @@ -13934,11 +14067,11 @@ */ removeEvent: function (el, eventType, handler) { // workaround for jQuery issue with unbinding custom events: // http://forum.jQuery.com/topic/javascript-error-when-unbinding-a-custom-event-using-jQuery-1-4-2 var func = doc.removeEventListener ? 'removeEventListener' : 'detachEvent'; - if (doc[func] && !el[func]) { + if (doc[func] && el && !el[func]) { el[func] = function () {}; } $(el).unbind(eventType, handler); }, @@ -14029,10 +14162,13 @@ * @param {Object} params * @param {Object} options jQuery-like animation options: duration, easing, callback */ animate: function (el, params, options) { var $el = $(el); + if (!el.style) { + el.style = {}; // #1881 + } if (params.d) { el.toD = params.d; // keep the array form for paths, used in $.fx.step.d params.d = 1; // because in jQuery, animating to an array has a different meaning } @@ -14058,21 +14194,20 @@ if (globalAdapter) { globalAdapter.init.call(globalAdapter, pathAnim); } - // 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. +// 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. var adapterRun = adapter.adapterRun, getScript = adapter.getScript, inArray = adapter.inArray, each = adapter.each, grep = adapter.grep, offset = adapter.offset, map = adapter.map, - merge = adapter.merge, addEvent = adapter.addEvent, removeEvent = adapter.removeEvent, fireEvent = adapter.fireEvent, washMouseEvent = adapter.washMouseEvent, animate = adapter.animate, @@ -14094,18 +14229,19 @@ /*formatter: function () { return this.value; },*/ style: { color: '#666', + cursor: 'default', fontSize: '11px', lineHeight: '14px' } }; defaultOptions = { - colors: ['#4572A7', '#AA4643', '#89A54E', '#80699B', '#3D96AE', - '#DB843D', '#92A8CD', '#A47D7C', '#B5CA92'], + colors: ['#2f7ed8', '#0d233a', '#8bbc21', '#910000', '#1aadce', '#492970', + '#f28f43', '#77a1e5', '#c42525', '#a6c96a'], symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'], lang: { loading: 'Loading...', months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], @@ -14117,12 +14253,12 @@ resetZoomTitle: 'Reset zoom level 1:1', thousandsSep: ',' }, global: { useUTC: true, - canvasToolsURL: 'http://code.highcharts.com/2.3.3/modules/canvas-tools.js', - VMLRadialGradientURL: 'http://code.highcharts.com/2.3.3/gfx/vml-radial-gradient.png' + canvasToolsURL: 'http://code.highcharts.com/3.0.2/modules/canvas-tools.js', + VMLRadialGradientURL: 'http://code.highcharts.com/3.0.2/gfx/vml-radial-gradient.png' }, chart: { //animation: true, //alignTicks: false, //reflow: true, @@ -14174,11 +14310,11 @@ // margin: 15, // x: 0, // verticalAlign: 'top', y: 15, style: { - color: '#3E576F', + color: '#274b6d',//#3E576F', fontSize: '16px' } }, subtitle: { @@ -14187,11 +14323,11 @@ // floating: false // x: 0, // verticalAlign: 'top', y: 30, style: { - color: '#6D869F' + color: '#4d759e' } }, plotOptions: { line: { // base series options @@ -14206,11 +14342,11 @@ //dashStyle: null, //enableMouseTracking: true, events: {}, //legendIndex: 0, lineWidth: 2, - shadow: true, + //shadow: false, // stacking: null, marker: { enabled: true, //symbol: null, lineWidth: 0, @@ -14233,11 +14369,11 @@ events: {} }, dataLabels: merge(defaultLabelOptions, { enabled: false, formatter: function () { - return this.y; + return numberFormat(this.y, -1); }, verticalAlign: 'bottom', // above singular point y: 0 // backgroundColor: undefined, // borderColor: undefined, @@ -14295,11 +14431,11 @@ borderWidth: 1, borderColor: '#909090', borderRadius: 5, navigation: { // animation: true, - activeColor: '#3E576F', + activeColor: '#274b6d', // arrowSize: 12 inactiveColor: '#CCC' // style: {} // text styles }, // margin: 10, @@ -14309,11 +14445,11 @@ /*style: { padding: '5px' },*/ itemStyle: { cursor: 'pointer', - color: '#3E576F', + color: '#274b6d', fontSize: '12px' }, itemHoverStyle: { //cursor: 'pointer', removed as of #601 color: '#000' @@ -14330,11 +14466,17 @@ symbolWidth: 16, symbolPadding: 5, verticalAlign: 'bottom', // width: undefined, x: 0, - y: 0 + y: 0, + title: { + //text: null, + style: { + fontWeight: 'bold' + } + } }, loading: { // hideDuration: 100, labelStyle: { @@ -14351,14 +14493,15 @@ } }, tooltip: { enabled: true, + animation: hasSVG, //crosshairs: null, backgroundColor: 'rgba(255, 255, 255, .85)', - borderWidth: 2, - borderRadius: 5, + borderWidth: 1, + borderRadius: 3, dateTimeLabelFormats: { millisecond: '%A, %b %e, %H:%M:%S.%L', second: '%A, %b %e, %H:%M:%S', minute: '%A, %b %e, %H:%M', hour: '%A, %b %e, %H:%M', @@ -14369,16 +14512,17 @@ }, //formatter: defaultFormatter, headerFormat: '<span style="font-size: 10px">{point.key}</span><br/>', pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b><br/>', shadow: true, - shared: useCanVG, - snap: hasTouch ? 25 : 10, + //shared: false, + snap: isTouchDevice ? 25 : 10, style: { color: '#333333', + cursor: 'default', fontSize: '12px', - padding: '5px', + padding: '8px', whiteSpace: 'nowrap' } //xDateFormat: '%A, %b %e, %Y', //valueDecimals: null, //valuePrefix: '', @@ -14396,11 +14540,11 @@ y: -5 }, style: { cursor: 'pointer', color: '#909090', - fontSize: '10px' + fontSize: '9px' } } }; @@ -14475,46 +14619,68 @@ function getOptions() { return defaultOptions; } - /** * Handle color operations. The object methods are chainable. * @param {String} input The input color in either rbga or hex format */ var Color = function (input) { // declare variables - var rgba = [], result; + var rgba = [], result, stops; /** * Parse the input color to rgba array * @param {String} input */ function init(input) { - // rgba - 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); - if (result) { - rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)]; - } else { // hex - result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(input); + // Gradients + if (input && input.stops) { + stops = map(input.stops, function (stop) { + return Color(stop[1]); + }); + + // Solid colors + } else { + // rgba + 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); if (result) { - rgba = [pInt(result[1], 16), pInt(result[2], 16), pInt(result[3], 16), 1]; + rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)]; + } else { + // hex + result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(input); + if (result) { + rgba = [pInt(result[1], 16), pInt(result[2], 16), pInt(result[3], 16), 1]; + } else { + // rgb + result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(input); + if (result) { + rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), 1]; + } + } } - } + } } /** * Return the color a specified format * @param {String} format */ function get(format) { var ret; + if (stops) { + ret = merge(input); + ret.stops = [].concat(ret.stops); + each(stops, function (stop, i) { + ret.stops[i] = [ret.stops[i][0], stop.get(format)]; + }); + // it's NaN if gradient colors on a column chart - if (rgba && !isNaN(rgba[0])) { + } else if (rgba && !isNaN(rgba[0])) { if (format === 'rgb') { ret = 'rgb(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ')'; } else if (format === 'a') { ret = rgba[3]; } else { @@ -14529,11 +14695,16 @@ /** * Brighten the color * @param {Number} alpha */ function brighten(alpha) { - if (isNumber(alpha) && alpha !== 0) { + if (stops) { + each(stops, function (stop) { + stop.brighten(alpha); + }); + + } else if (isNumber(alpha) && alpha !== 0) { var i; for (i = 0; i < 3; i++) { rgba[i] += pInt(alpha * 255); if (rgba[i] < 0) { @@ -14560,10 +14731,11 @@ // public methods return { get: get, brighten: brighten, + rgba: rgba, setOpacity: setOpacity }; }; @@ -14591,10 +14763,14 @@ * Renderer.label. */ wrapper.attrSetters = {}; }, /** + * Default base for animation + */ + opacity: 1, + /** * Animate a given attribute * @param {Object} params * @param {Number} options The same options as in jQuery animation * @param {Function} complete Function to perform at the end of animation */ @@ -14651,11 +14827,10 @@ key = { x: 'cx', y: 'cy' }[key] || key; } else if (key === 'strokeWidth') { key = 'stroke-width'; } ret = attr(element, key) || wrapper[key] || 0; - if (key !== 'd' && key !== 'visibility') { // 'd' is string in animation step ret = parseFloat(ret); } // setter @@ -14671,10 +14846,11 @@ if (result !== false) { if (result !== UNDEFINED) { value = result; // the attribute setter has returned a new value to set } + // paths if (key === 'd') { if (value && value.join) { // join path value = value.join(' '); } @@ -14692,14 +14868,12 @@ //child.setAttribute('x', value); attr(child, 'x', value); } } - if (wrapper.rotation) { - attr(element, 'transform', 'rotate(' + wrapper.rotation + ' ' + value + ' ' + - pInt(hash.y || attr(element, 'y')) + ')'); - } + } else if (wrapper.rotation && (key === 'x' || key === 'y')) { + doTransform = true; // apply gradients } else if (key === 'fill') { value = renderer.color(value, element, key); @@ -14714,11 +14888,12 @@ ry: value }); skipAttr = true; // translation and text rotation - } else if (key === 'translateX' || key === 'translateY' || key === 'rotation' || key === 'verticalAlign') { + } else if (key === 'translateX' || key === 'translateY' || key === 'rotation' || + key === 'verticalAlign' || key === 'scaleX' || key === 'scaleY') { doTransform = true; skipAttr = true; // apply opacity as subnode (required by legacy WebKit and Batik) } else if (key === 'stroke') { @@ -14747,14 +14922,10 @@ value[i] = pInt(value[i]) * hash['stroke-width']; } value = value.join(','); } - // special - } else if (key === 'isTracker') { - wrapper[key] = value; - // IE9/MooTools combo: MooTools returns objects instead of numbers and IE9 Beta 2 // is unable to cast them. Test again with final IE9. } else if (key === 'width') { value = pInt(value); @@ -14776,13 +14947,24 @@ // jQuery animate changes case if (key === 'strokeWidth') { key = 'stroke-width'; } - // Chrome/Win < 6 bug (http://code.google.com/p/chromium/issues/detail?id=15461) - if (isWebKit && key === 'stroke-width' && value === 0) { - value = 0.000001; + // In Chrome/Win < 6 as well as Batik, the stroke attribute can't be set when the stroke- + // width is 0. #1369 + if (key === 'stroke-width' || key === 'stroke') { + wrapper[key] = value; + // Only apply the stroke attribute if the stroke width is defined and larger than 0 + if (wrapper.stroke && wrapper['stroke-width']) { + attr(element, 'stroke', wrapper.stroke); + attr(element, 'stroke-width', wrapper['stroke-width']); + wrapper.hasStroke = true; + } else if (key === 'stroke-width' && value === 0 && wrapper.hasStroke) { + element.removeAttribute('stroke'); + wrapper.hasStroke = false; + } + skipAttr = true; } // symbols if (wrapper.symbolName && /^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)/.test(key)) { @@ -14814,16 +14996,11 @@ } // Record for animation and quick access without polling the DOM wrapper[key] = value; - // Update transform - if (doTransform) { - wrapper.updateTransform(); - } - - + if (key === 'text') { // Delete bBox memo when the text changes if (value !== wrapper.textStr) { delete wrapper.bBox; } @@ -14837,16 +15014,40 @@ } } + // Update transform. Do this outside the loop to prevent redundant updating for batch setting + // of attributes. + if (doTransform) { + wrapper.updateTransform(); + } + } return ret; }, + /** + * Add a class name to an element + */ + addClass: function (className) { + attr(this.element, 'class', attr(this.element, 'class') + ' ' + className); + return this; + }, + /* hasClass and removeClass are not (yet) needed + hasClass: function (className) { + return attr(this.element, 'class').indexOf(className) !== -1; + }, + removeClass: function (className) { + attr(this.element, 'class', attr(this.element, 'class').replace(className, '')); + return this; + }, + */ + + /** * If one of the symbol size affecting parameters are changed, * check all the others only once for each call to an element's * .attr() method * @param {Object} hash */ @@ -14856,11 +15057,17 @@ each(['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR', 'anchorX', 'anchorY'], function (key) { wrapper[key] = pick(hash[key], wrapper[key]); }); wrapper.attr({ - d: wrapper.renderer.symbols[wrapper.symbolName](wrapper.x, wrapper.y, wrapper.width, wrapper.height, wrapper) + d: wrapper.renderer.symbols[wrapper.symbolName]( + wrapper.x, + wrapper.y, + wrapper.width, + wrapper.height, + wrapper + ) }); }, /** * Apply a clipping path to this object @@ -14948,13 +15155,11 @@ css(elemWrapper.element, styles); } else { for (n in styles) { serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':' + styles[n] + ';'; } - elemWrapper.attr({ - style: serializedCss - }); + attr(elem, 'style', serializedCss); // #1881 } // re-build text if (textWidth && elemWrapper.added) { @@ -14968,21 +15173,19 @@ * Add an event listener * @param {String} eventType * @param {Function} handler */ on: function (eventType, handler) { - var fn = handler; // touch if (hasTouch && eventType === 'click') { - eventType = 'touchstart'; - fn = function (e) { + this.element.ontouchstart = function (e) { e.preventDefault(); handler(); }; } // simplest possible event model for internal use - this.element['on' + eventType] = fn; + this.element['on' + eventType] = handler; return this; }, /** * Set the coordinates needed to draw a consistent radial gradient across @@ -15092,23 +15295,21 @@ alignCorrection = { left: 0, center: 0.5, right: 1 }[align], nonLeft = align && align !== 'left', shadows = wrapper.shadows; // apply translate - if (translateX || translateY) { - css(elem, { - marginLeft: translateX, - marginTop: translateY - }); - if (shadows) { // used in labels/tooltip - each(shadows, function (shadow) { - css(shadow, { - marginLeft: translateX + 1, - marginTop: translateY + 1 - }); + css(elem, { + marginLeft: translateX, + marginTop: translateY + }); + if (shadows) { // used in labels/tooltip + each(shadows, function (shadow) { + css(shadow, { + marginLeft: translateX + 1, + marginTop: translateY + 1 }); - } + }); } // apply inversion if (wrapper.inverted) { // wrapper is a group each(elem.childNodes, function (child) { @@ -15159,11 +15360,11 @@ width = pick(wrapper.elemWidth, elem.offsetWidth); height = pick(wrapper.elemHeight, elem.offsetHeight); // update textWidth - if (width > textWidth && /[ \-]/.test(elem.innerText)) { // #983 + if (width > textWidth && /[ \-]/.test(elem.textContent || elem.innerText)) { // #983, #1254 css(elem, { width: textWidth + PX, display: 'block', whiteSpace: 'normal' }); @@ -15199,10 +15400,15 @@ // apply position with correction css(elem, { left: (x + xCorr) + PX, top: (y + yCorr) + PX }); + + // force reflow in webkit to apply the left and top on useHTML element (#1249) + if (isWebKit) { + height = elem.offsetHeight; // assigned to height for JSLint purpose + } // record current text transform wrapper.cTT = currentTextTransform; } }, @@ -15213,32 +15419,38 @@ */ updateTransform: function () { var wrapper = this, translateX = wrapper.translateX || 0, translateY = wrapper.translateY || 0, + scaleX = wrapper.scaleX, + scaleY = wrapper.scaleY, inverted = wrapper.inverted, rotation = wrapper.rotation, - transform = []; + transform; // flipping affects translate as adjustment for flipping around the group's axis if (inverted) { translateX += wrapper.attr('width'); translateY += wrapper.attr('height'); } - // apply translate - if (translateX || translateY) { - transform.push('translate(' + translateX + ',' + translateY + ')'); - } + // Apply translate. Nearly all transformed elements have translation, so instead + // of checking for translate = 0, do it always (#1767, #1846). + transform = ['translate(' + translateX + ',' + translateY + ')']; // apply rotation if (inverted) { transform.push('rotate(90) scale(-1,1)'); } else if (rotation) { // text rotation transform.push('rotate(' + rotation + ' ' + (wrapper.x || 0) + ' ' + (wrapper.y || 0) + ')'); } + // apply scale + if (defined(scaleX) || defined(scaleY)) { + transform.push('scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')'); + } + if (transform.length) { attr(wrapper.element, 'transform', transform.join(' ')); } }, /** @@ -15255,58 +15467,74 @@ * Break down alignment options like align, verticalAlign, x and y * to x and y relative to the chart. * * @param {Object} alignOptions * @param {Boolean} alignByTranslate - * @param {Object} box The box to align to, needs a width and height + * @param {String[Object} box The box to align to, needs a width and height. When the + * box is a string, it refers to an object in the Renderer. For example, when + * box is 'spacingBox', it refers to Renderer.spacingBox which holds width, height + * x and y properties. * */ align: function (alignOptions, alignByTranslate, box) { - var elemWrapper = this; + var align, + vAlign, + x, + y, + attribs = {}, + alignTo, + renderer = this.renderer, + alignedObjects = renderer.alignedObjects; - if (!alignOptions) { // called on resize - alignOptions = elemWrapper.alignOptions; - alignByTranslate = elemWrapper.alignByTranslate; - } else { // first call on instanciate - elemWrapper.alignOptions = alignOptions; - elemWrapper.alignByTranslate = alignByTranslate; - if (!box) { // boxes other than renderer handle this internally - elemWrapper.renderer.alignedObjects.push(elemWrapper); + // First call on instanciate + if (alignOptions) { + this.alignOptions = alignOptions; + this.alignByTranslate = alignByTranslate; + if (!box || isString(box)) { // boxes other than renderer handle this internally + this.alignTo = alignTo = box || 'renderer'; + erase(alignedObjects, this); // prevent duplicates, like legendGroup after resize + alignedObjects.push(this); + box = null; // reassign it below } + + // When called on resize, no arguments are supplied + } else { + alignOptions = this.alignOptions; + alignByTranslate = this.alignByTranslate; + alignTo = this.alignTo; } - box = pick(box, elemWrapper.renderer); + box = pick(box, renderer[alignTo], renderer); - var align = alignOptions.align, - vAlign = alignOptions.verticalAlign, - x = (box.x || 0) + (alignOptions.x || 0), // default: left align - y = (box.y || 0) + (alignOptions.y || 0), // default: top align - attribs = {}; + // Assign variables + align = alignOptions.align; + vAlign = alignOptions.verticalAlign; + x = (box.x || 0) + (alignOptions.x || 0); // default: left align + y = (box.y || 0) + (alignOptions.y || 0); // default: top align - - // align + // Align if (align === 'right' || align === 'center') { x += (box.width - (alignOptions.width || 0)) / { right: 1, center: 2 }[align]; } attribs[alignByTranslate ? 'translateX' : 'x'] = mathRound(x); - // vertical align + // Vertical align if (vAlign === 'bottom' || vAlign === 'middle') { y += (box.height - (alignOptions.height || 0)) / ({ bottom: 1, middle: 2 }[vAlign] || 1); } attribs[alignByTranslate ? 'translateY' : 'y'] = mathRound(y); - // animate only if already placed - elemWrapper[elemWrapper.placed ? 'animate' : 'attr'](attribs); - elemWrapper.placed = true; - elemWrapper.alignAttr = attribs; + // Animate only if already placed + this[this.placed ? 'animate' : 'attr'](attribs); + this.placed = true; + this.alignAttr = attribs; - return elemWrapper; + return this; }, /** * Get the bounding box (width, height, x and y) for the element */ @@ -15318,11 +15546,11 @@ height, rotation = wrapper.rotation, element = wrapper.element, styles = wrapper.styles, rad = rotation * deg2rad; - + if (!bBox) { // SVG elements if (element.namespaceURI === SVG_NS || renderer.forExport) { try { // Fails in Firefox if the container has display: none. @@ -15354,23 +15582,23 @@ // True SVG elements as well as HTML elements in modern browsers using the .useHTML option // need to compensated for rotation if (renderer.isSVG) { width = bBox.width; height = bBox.height; - + + // Workaround for wrong bounding box in IE9 and IE10 (#1101, #1505, #1669) + if (isIE && styles && styles.fontSize === '11px' && height.toPrecision(3) === '22.7') { + bBox.height = height = 14; + } + // Adjust for rotated text if (rotation) { bBox.width = mathAbs(height * mathSin(rad)) + mathAbs(width * mathCos(rad)); bBox.height = mathAbs(height * mathCos(rad)) + mathAbs(width * mathSin(rad)); } } - // Workaround for wrong bounding box in IE9 and IE10 (#1101) - if (isIE && styles && styles.fontSize === '11px' && height === 22.700000762939453) { - bBox.height = 14; - } - wrapper.bBox = bBox; } return bBox; }, @@ -15386,10 +15614,22 @@ */ hide: function () { return this.attr({ visibility: HIDDEN }); }, + fadeOut: function (duration) { + var elemWrapper = this; + elemWrapper.animate({ + opacity: 0 + }, { + duration: duration || 150, + complete: function () { + elemWrapper.hide(); + } + }); + }, + /** * Add the element * @param {Object|Undefined} parent Can be an element, an element wrapper or undefined * to append the element to the renderer.box. */ @@ -15477,11 +15717,11 @@ shadows = wrapper.shadows, key, i; // remove events - element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = null; + element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = element.point = null; stop(wrapper); // stop running animations if (wrapper.clipPath) { wrapper.clipPath = wrapper.clipPath.destroy(); } @@ -15503,33 +15743,22 @@ wrapper.safeRemoveChild(shadow); }); } // remove from alignObjects - erase(wrapper.renderer.alignedObjects, wrapper); + if (wrapper.alignTo) { + erase(wrapper.renderer.alignedObjects, wrapper); + } for (key in wrapper) { delete wrapper[key]; } return null; }, /** - * Empty a group element - */ - empty: function () { - var element = this.element, - childNodes = element.childNodes, - i = childNodes.length; - - while (i--) { - element.removeChild(childNodes[i]); - } - }, - - /** * Add a shadow to the element. Must be done after the element is added to the DOM * @param {Boolean|Object} shadowOptions */ shadow: function (shadowOptions, group, cutOff) { var shadows = [], @@ -15600,11 +15829,12 @@ * @param {Boolean} forExport */ init: function (container, width, height, forExport) { var renderer = this, loc = location, - boxWrapper; + boxWrapper, + desc; boxWrapper = renderer.createElement('svg') .attr({ xmlns: SVG_NS, version: '1.1' @@ -15623,10 +15853,15 @@ .replace(/#.*?$/, '') // remove the hash .replace(/([\('\)])/g, '\\$1') // escape parantheses and quotes .replace(/ /g, '%20') : // replace spaces (needed for Safari only) ''; + // Add description + desc = this.createElement('desc').add(); + desc.element.appendChild(doc.createTextNode('Created with ' + PRODUCT + ' ' + VERSION)); + + renderer.defs = this.createElement('defs').add(); renderer.forExport = forExport; renderer.gradients = {}; // Object where gradient SvgElements are stored renderer.setSize(width, height, false); @@ -15717,10 +15952,12 @@ * * @param {Object} textNode The parent text SVG node */ buildText: function (wrapper) { var textNode = wrapper.element, + renderer = this, + forExport = renderer.forExport, lines = pick(wrapper.textStr, '').toString() .replace(/<(b|strong)>/g, '<span style="font-weight:bold">') .replace(/<(i|em)>/g, '<span style="font-style:italic">') .replace(/<a/g, '<span') .replace(/<\/(b|strong|i|em|a)>/g, '</span>') @@ -15730,24 +15967,13 @@ hrefRegex = /href="([^"]+)"/, parentX = attr(textNode, 'x'), textStyles = wrapper.styles, width = textStyles && textStyles.width && pInt(textStyles.width), textLineHeight = textStyles && textStyles.lineHeight, - lastLine, - GET_COMPUTED_STYLE = 'getComputedStyle', - i = childNodes.length, - linePositions = []; - - // Needed in IE9 because it doesn't report tspan's offsetHeight (#893) - function getLineHeightByBBox(lineNo) { - linePositions[lineNo] = textNode.getBBox ? - textNode.getBBox().height : - wrapper.renderer.fontMetrics(textNode.style.fontSize).h; // #990 - return mathRound(linePositions[lineNo] - (linePositions[lineNo - 1] || 0)); - } + i = childNodes.length; - // remove old text + /// remove old text while (i--) { textNode.removeChild(childNodes[i]); } if (width && !wrapper.added) { @@ -15759,11 +15985,11 @@ lines.pop(); } // build the lines each(lines, function (line, lineNo) { - var spans, spanNo = 0, lineHeight; + var spans, spanNo = 0; line = line.replace(/<span/g, '|||<span').replace(/<\/span>/g, '</span>|||'); spans = line.split('|||'); each(spans, function (span) { @@ -15773,65 +15999,56 @@ spanStyle; // #390 if (styleRegex.test(span)) { spanStyle = span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2'); attr(tspan, 'style', spanStyle); } - if (hrefRegex.test(span)) { + if (hrefRegex.test(span) && !forExport) { // Not for export - #1529 attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"'); css(tspan, { cursor: 'pointer' }); } span = (span.replace(/<(.|\n)*?>/g, '') || ' ') .replace(/&lt;/g, '<') .replace(/&gt;/g, '>'); - // issue #38 workaround. - /*if (reverse) { - arr = []; - i = span.length; - while (i--) { - arr.push(span.charAt(i)); - } - span = arr.join(''); - }*/ - // add the text node tspan.appendChild(doc.createTextNode(span)); if (!spanNo) { // first span in a line, align it to the left attributes.x = parentX; } else { - // Firefox ignores spaces at the front or end of the tspan - attributes.dx = 3; // space + attributes.dx = 0; // #16 } + // add attributes + attr(tspan, attributes); + // first span on subsequent line, add the line height - if (!spanNo) { - if (lineNo) { + if (!spanNo && lineNo) { - // allow getting the right offset height in exporting in IE - if (!hasSVG && wrapper.renderer.forExport) { - css(tspan, { display: 'block' }); - } - - // Webkit and opera sometimes return 'normal' as the line height. In that - // case, webkit uses offsetHeight, while Opera falls back to 18 - lineHeight = win[GET_COMPUTED_STYLE] && - pInt(win[GET_COMPUTED_STYLE](lastLine, null).getPropertyValue('line-height')); - - if (!lineHeight || isNaN(lineHeight)) { - lineHeight = textLineHeight || lastLine.offsetHeight || getLineHeightByBBox(lineNo) || 18; - } - attr(tspan, 'dy', lineHeight); + // allow getting the right offset height in exporting in IE + if (!hasSVG && forExport) { + css(tspan, { display: 'block' }); } - lastLine = tspan; // record for use in next line + + // Set the line height based on the font size of either + // the text element or the tspan element + attr( + tspan, + 'dy', + textLineHeight || renderer.fontMetrics( + /px$/.test(tspan.style.fontSize) ? + tspan.style.fontSize : + textStyles.fontSize + ).h, + // Safari 6.0.2 - too optimized for its own good (#1539) + // TODO: revisit this with future versions of Safari + isWebKit && tspan.offsetHeight + ); } - // add attributes - attr(tspan, attributes); - - // append it + // Append it textNode.appendChild(tspan); spanNo++; // check width and apply soft breaks @@ -15886,69 +16103,65 @@ * @param {Object} normalState * @param {Object} hoverState * @param {Object} pressedState */ button: function (text, x, y, callback, normalState, hoverState, pressedState) { - var label = this.label(text, x, y), + var label = this.label(text, x, y, null, null, null, null, null, 'button'), curState = 0, stateOptions, stateStyle, normalStyle, hoverStyle, pressedStyle, STYLE = 'style', verticalGradient = { x1: 0, y1: 0, x2: 0, y2: 1 }; - // prepare the attributes - /*jslint white: true*/ - normalState = merge(hash( - STROKE_WIDTH, 1, - STROKE, '#999', - FILL, hash( - LINEAR_GRADIENT, verticalGradient, - STOPS, [ - [0, '#FFF'], - [1, '#DDD'] + // Normal state - prepare the attributes + normalState = merge({ + 'stroke-width': 1, + stroke: '#CCCCCC', + fill: { + linearGradient: verticalGradient, + stops: [ + [0, '#FEFEFE'], + [1, '#F6F6F6'] ] - ), - 'r', 3, - 'padding', 3, - STYLE, hash( - 'color', 'black' - ) - ), normalState); - /*jslint white: false*/ + }, + r: 2, + padding: 5, + style: { + color: 'black' + } + }, normalState); normalStyle = normalState[STYLE]; delete normalState[STYLE]; - /*jslint white: true*/ - hoverState = merge(normalState, hash( - STROKE, '#68A', - FILL, hash( - LINEAR_GRADIENT, verticalGradient, - STOPS, [ + // Hover state + hoverState = merge(normalState, { + stroke: '#68A', + fill: { + linearGradient: verticalGradient, + stops: [ [0, '#FFF'], [1, '#ACF'] ] - ) - ), hoverState); - /*jslint white: false*/ + } + }, hoverState); hoverStyle = hoverState[STYLE]; delete hoverState[STYLE]; - /*jslint white: true*/ - pressedState = merge(normalState, hash( - STROKE, '#68A', - FILL, hash( - LINEAR_GRADIENT, verticalGradient, - STOPS, [ + // Pressed state + pressedState = merge(normalState, { + stroke: '#68A', + fill: { + linearGradient: verticalGradient, + stops: [ [0, '#9BD'], [1, '#CDF'] ] - ) - ), pressedState); - /*jslint white: false*/ + } + }, pressedState); pressedStyle = pressedState[STYLE]; delete pressedState[STYLE]; // add the events addEvent(label.element, 'mouseenter', function () { @@ -16185,10 +16398,11 @@ width, height, options ), + imageElement, imageRegex = /^url\((.*?)\)$/, imageSrc, imageSize, centerImage; @@ -16211,45 +16425,48 @@ // image symbols } else if (imageRegex.test(symbol)) { // On image load, set the size and position centerImage = function (img, size) { - img.attr({ - width: size[0], - height: size[1] - }); + if (img.element) { // it may be destroyed in the meantime (#1390) + img.attr({ + width: size[0], + height: size[1] + }); - if (!img.alignByTranslate) { // #185 - img.translate( - -mathRound(size[0] / 2), - -mathRound(size[1] / 2) - ); + if (!img.alignByTranslate) { // #185 + img.translate( + mathRound((width - size[0]) / 2), // #1378 + mathRound((height - size[1]) / 2) + ); + } } }; imageSrc = symbol.match(imageRegex)[1]; imageSize = symbolSizes[imageSrc]; - // create the image synchronously, add attribs async + // Ireate the image synchronously, add attribs async obj = this.image(imageSrc) .attr({ x: x, y: y }); + obj.isImg = true; if (imageSize) { centerImage(obj, imageSize); } else { - // initialize image to be 0 size so export will still function if there's no cached sizes + // Initialize image to be 0 size so export will still function if there's no cached sizes. + // obj.attr({ width: 0, height: 0 }); - // create a dummy JavaScript image to get the width and height - createElement('img', { + // Create a dummy JavaScript image to get the width and height. Due to a bug in IE < 8, + // the created element must be assigned to a variable in order to load (#292). + imageElement = createElement('img', { onload: function () { - var img = this; - - centerImage(obj, symbolSizes[imageSrc] = [img.width, img.height]); + centerImage(obj, symbolSizes[imageSrc] = [this.width, this.height]); }, src: imageSrc }); } } @@ -16308,11 +16525,11 @@ ]; }, 'arc': function (x, y, w, h, options) { var start = options.start, radius = options.r || w || h, - end = options.end - 0.000001, // to prevent cos and sin of start and end from becoming equal on 360 arcs + end = options.end - 0.001, // to prevent cos and sin of start and end from becoming equal on 360 arcs (related: #1561) innerRadius = options.innerR, open = options.open, cosStart = mathCos(start), sinStart = mathSin(start), cosEnd = mathCos(end), @@ -16384,61 +16601,83 @@ */ color: function (color, elem, prop) { var renderer = this, colorObject, regexRgba = /^rgba/, - gradName; + gradName, + gradAttr, + gradients, + gradientObject, + stops, + stopColor, + stopOpacity, + radialReference, + n, + id, + key = []; // Apply linear or radial gradients if (color && color.linearGradient) { gradName = 'linearGradient'; } else if (color && color.radialGradient) { gradName = 'radialGradient'; } if (gradName) { - var gradAttr = color[gradName], - gradients = renderer.gradients, - gradientObject, - stopColor, - stopOpacity, - radialReference = elem.radialReference; + gradAttr = color[gradName]; + gradients = renderer.gradients; + stops = color.stops; + radialReference = elem.radialReference; + // Keep < 2.2 kompatibility + if (isArray(gradAttr)) { + color[gradName] = gradAttr = { + x1: gradAttr[0], + y1: gradAttr[1], + x2: gradAttr[2], + y2: gradAttr[3], + gradientUnits: 'userSpaceOnUse' + }; + } + + // Correct the radial gradient for the radial reference system + if (gradName === 'radialGradient' && radialReference && !defined(gradAttr.gradientUnits)) { + gradAttr = merge(gradAttr, { + cx: (radialReference[0] - radialReference[2] / 2) + gradAttr.cx * radialReference[2], + cy: (radialReference[1] - radialReference[2] / 2) + gradAttr.cy * radialReference[2], + r: gradAttr.r * radialReference[2], + gradientUnits: 'userSpaceOnUse' + }); + } + + // Build the unique key to detect whether we need to create a new element (#1282) + for (n in gradAttr) { + if (n !== 'id') { + key.push(n, gradAttr[n]); + } + } + for (n in stops) { + key.push(stops[n]); + } + key = key.join(','); + // Check if a gradient object with the same config object is created within this renderer - if (!gradAttr.id || !gradients[gradAttr.id]) { + if (gradients[key]) { + id = gradients[key].id; - // Keep < 2.2 kompatibility - if (isArray(gradAttr)) { - color[gradName] = gradAttr = { - x1: gradAttr[0], - y1: gradAttr[1], - x2: gradAttr[2], - y2: gradAttr[3], - gradientUnits: 'userSpaceOnUse' - }; - } - - // Correct the radial gradient for the radial reference system - if (gradName === 'radialGradient' && radialReference && !defined(gradAttr.gradientUnits)) { - extend(gradAttr, { - cx: (radialReference[0] - radialReference[2] / 2) + gradAttr.cx * radialReference[2], - cy: (radialReference[1] - radialReference[2] / 2) + gradAttr.cy * radialReference[2], - r: gradAttr.r * radialReference[2], - gradientUnits: 'userSpaceOnUse' - }); - } + } else { // Set the id and create the element - gradAttr.id = PREFIX + idCounter++; - gradients[gradAttr.id] = gradientObject = renderer.createElement(gradName) + gradAttr.id = id = PREFIX + idCounter++; + gradients[key] = gradientObject = renderer.createElement(gradName) .attr(gradAttr) .add(renderer.defs); // The gradient needs to keep a list of stops to be able to destroy them gradientObject.stops = []; - each(color.stops, function (stop) { + each(stops, function (stop) { var stopObject; if (regexRgba.test(stop[1])) { colorObject = Color(stop[1]); stopColor = colorObject.get('rgb'); stopOpacity = colorObject.get('a'); @@ -16456,11 +16695,11 @@ gradientObject.stops.push(stopObject); }); } // Return the reference to the gradient object - return 'url(' + renderer.url + '#' + gradAttr.id + ')'; + return 'url(' + renderer.url + '#' + id + ')'; // Webkit and Batik can't show rgba. } else if (regexRgba.test(color)) { colorObject = Color(color); attr(elem, prop + '-opacity', colorObject.get('a')); @@ -16691,62 +16930,70 @@ //.add(wrapper), box, bBox, alignFactor = 0, padding = 3, + paddingLeft = 0, width, height, wrapperX, wrapperY, crispAdjust = 0, deferredAttr = {}, baselineOffset, - attrSetters = wrapper.attrSetters; + attrSetters = wrapper.attrSetters, + needsBox; /** * This function runs after the label is added to the DOM (when the bounding box is * available), and after the text of the label is updated to detect the new bounding * box and reflect it in the border box. */ function updateBoxSize() { - var boxY, + var boxX, + boxY, style = text.element.style; bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) && text.getBBox(); - wrapper.width = (width || bBox.width || 0) + 2 * padding; + wrapper.width = (width || bBox.width || 0) + 2 * padding + paddingLeft; wrapper.height = (height || bBox.height || 0) + 2 * padding; // update the label-scoped y offset baselineOffset = padding + renderer.fontMetrics(style && style.fontSize).b; - - - // create the border box if it is not already present - if (!box) { - boxY = baseline ? -baselineOffset : 0; - - wrapper.box = box = shape ? - renderer.symbol(shape, -alignFactor * padding, boxY, wrapper.width, wrapper.height) : - renderer.rect(-alignFactor * padding, boxY, wrapper.width, wrapper.height, 0, deferredAttr[STROKE_WIDTH]); - box.add(wrapper); + + if (needsBox) { + + // create the border box if it is not already present + if (!box) { + boxX = mathRound(-alignFactor * padding); + boxY = baseline ? -baselineOffset : 0; + + wrapper.box = box = shape ? + renderer.symbol(shape, boxX, boxY, wrapper.width, wrapper.height) : + renderer.rect(boxX, boxY, wrapper.width, wrapper.height, 0, deferredAttr[STROKE_WIDTH]); + box.add(wrapper); + } + + // apply the box attributes + if (!box.isImg) { // #1630 + box.attr(merge({ + width: wrapper.width, + height: wrapper.height + }, deferredAttr)); + } + deferredAttr = null; } - - // apply the box attributes - box.attr(merge({ - width: wrapper.width, - height: wrapper.height - }, deferredAttr)); - deferredAttr = null; } /** * This function runs after setting text or padding, but only if padding is changed */ function updateTextPadding() { var styles = wrapper.styles, textAlign = styles && styles.textAlign, - x = padding * (1 - alignFactor), + x = paddingLeft + padding * (1 - alignFactor), y; // determin y based on the baseline y = baseline ? 0 : baselineOffset; @@ -16787,11 +17034,11 @@ text: str, // alignment is available now x: x, y: y }); - if (defined(anchorX)) { + if (box && defined(anchorX)) { wrapper.attr({ anchorX: anchorX, anchorY: anchorY }); } @@ -16814,18 +17061,25 @@ }; attrSetters.height = function (value) { height = value; return false; }; - attrSetters.padding = function (value) { + attrSetters.padding = function (value) { if (defined(value) && value !== padding) { padding = value; updateTextPadding(); } - return false; }; + attrSetters.paddingLeft = function (value) { + if (defined(value) && value !== paddingLeft) { + paddingLeft = value; + updateTextPadding(); + } + return false; + }; + // change local variable and set attribue as well attrSetters.align = function (value) { alignFactor = { left: 0, center: 0.5, right: 1 }[value]; return false; // prevent setting text-anchor on the group @@ -16839,15 +17093,19 @@ return false; }; // apply these to the box but not to the text attrSetters[STROKE_WIDTH] = function (value, key) { + needsBox = true; crispAdjust = value % 2 / 2; boxAttr(key, value); return false; }; attrSetters.stroke = attrSetters.fill = attrSetters.r = function (value, key) { + if (key === 'fill') { + needsBox = true; + } boxAttr(key, value); return false; }; attrSetters.anchorX = function (value, key) { anchorX = value; @@ -16869,11 +17127,11 @@ wrapper.attr('translateX', wrapperX); return false; }; attrSetters.y = function (value) { wrapperY = wrapper.y = mathRound(value); - wrapper.attr('translateY', value); + wrapper.attr('translateY', wrapperY); return false; }; // Redirect certain methods to either the box or the text var baseCss = wrapper.css; @@ -16882,12 +17140,12 @@ * Pick up some properties and apply them to the text instead of the wrapper */ css: function (styles) { if (styles) { var textStyles = {}; - styles = merge({}, styles); // create a copy to avoid altering the original object (#537) - each(['fontSize', 'fontWeight', 'fontFamily', 'color', 'lineHeight', 'width'], function (prop) { + styles = merge(styles); // create a copy to avoid altering the original object (#537) + each(['fontSize', 'fontWeight', 'fontFamily', 'color', 'lineHeight', 'width', 'textDecoration'], function (prop) { if (styles[prop] !== UNDEFINED) { textStyles[prop] = styles[prop]; delete styles[prop]; } }); @@ -16897,17 +17155,24 @@ }, /** * Return the bounding box of the box, not the group */ getBBox: function () { - return box.getBBox(); + return { + width: bBox.width + 2 * padding, + height: bBox.height + 2 * padding, + x: bBox.x - padding, + y: bBox.y - padding + }; }, /** * Apply the shadow to the box */ shadow: function (b) { - box.shadow(b); + if (box) { + box.shadow(b); + } return wrapper; }, /** * Destroy and release memory. */ @@ -16924,10 +17189,13 @@ if (box) { box = box.destroy(); } // Call base implementation to destroy the rest SVGElement.prototype.destroy.call(wrapper); + + // Release local pointers (#1298) + wrapper = renderer = updateBoxSize = updateTextPadding = boxAttr = getSizeAfterAdd = null; } }); } }; // end SVGRenderer @@ -16946,42 +17214,41 @@ *****************************************************************************/ /** * @constructor */ -var VMLRenderer; +var VMLRenderer, VMLElement; if (!hasSVG && !useCanVG) { /** * The VML element wrapper. */ -var VMLElement = { +Highcharts.VMLElement = VMLElement = { /** * Initialize a new VML element wrapper. It builds the markup as a string * to minimize DOM traffic. * @param {Object} renderer * @param {Object} nodeName */ init: function (renderer, nodeName) { var wrapper = this, markup = ['<', nodeName, ' filled="f" stroked="f"'], - style = ['position: ', ABSOLUTE, ';']; + style = ['position: ', ABSOLUTE, ';'], + isDiv = nodeName === DIV; // divs and shapes need size - if (nodeName === 'shape' || nodeName === DIV) { + if (nodeName === 'shape' || isDiv) { style.push('left:0;top:0;width:1px;height:1px;'); } - if (docMode8) { - style.push('visibility: ', nodeName === DIV ? HIDDEN : VISIBLE); - } - + style.push('visibility: ', isDiv ? HIDDEN : VISIBLE); + markup.push(' style="', style.join(''), '"/>'); // create element with default attributes and style if (nodeName) { - markup = nodeName === DIV || nodeName === 'span' || nodeName === 'img' ? + markup = isDiv || nodeName === 'span' || nodeName === 'img' ? markup.join('') : renderer.prepVML(markup); wrapper.element = createElement(markup); } @@ -17017,11 +17284,11 @@ // align text after adding to be able to read offset wrapper.added = true; if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) { wrapper.updateTransform(); } - + // fire an event for internal hooks fireEvent(wrapper, 'add'); return wrapper; }, @@ -17100,11 +17367,12 @@ value = value || []; wrapper.d = value.join(' '); // used in getter for animation // convert paths i = value.length; - var convertedPath = []; + var convertedPath = [], + clockwise; while (i--) { // Multiply by 10 to allow subpixel precision. // Substracting half a pixel seems to make the coordinates // align with SVG, but this hasn't been tested thoroughly @@ -17112,10 +17380,24 @@ convertedPath[i] = mathRound(value[i] * 10) - 5; } else if (value[i] === 'Z') { // close the path convertedPath[i] = 'x'; } else { convertedPath[i] = value[i]; + + // When the start X and end X coordinates of an arc are too close, + // they are rounded to the same value above. In this case, substract 1 from the end X + // position. #760, #1371. + if (value.isArc && (value[i] === 'wa' || value[i] === 'at')) { + clockwise = value[i] === 'wa' ? 1 : -1; // #1642 + if (convertedPath[i + 5] === convertedPath[i + 7]) { + convertedPath[i + 7] -= clockwise; + } + // Start and end Y (#1410) + if (convertedPath[i + 6] === convertedPath[i + 8]) { + convertedPath[i + 8] -= clockwise; + } + } } } value = convertedPath.join(' ') || 'x'; element.path = value; @@ -17142,13 +17424,19 @@ // Instead of toggling the visibility CSS property, move the div out of the viewport. // This works around #61 and #586 if (nodeName === 'DIV') { value = value === HIDDEN ? '-999em' : 0; + + // In order to redraw, IE7 needs the div to be visible when tucked away + // outside the viewport. So the visibility is actually opposite of + // the expected value. This applies to the tooltip only. + if (!docMode8) { + elemStyle[key] = value ? VISIBLE : HIDDEN; + } key = 'top'; } - elemStyle[key] = value; skipAttr = true; // directly mapped to css } else if (key === 'zIndex') { @@ -17156,37 +17444,36 @@ if (value) { elemStyle[key] = value; } skipAttr = true; - // width and height - } else if (key === 'width' || key === 'height') { + // x, y, width, height + } else if (inArray(key, ['x', 'y', 'width', 'height']) !== -1) { - value = mathMax(0, value); // don't set width or height below zero (#311) + wrapper[key] = value; // used in getter - this[key] = value; // used in getter - + if (key === 'x' || key === 'y') { + key = { x: 'left', y: 'top' }[key]; + } else { + value = mathMax(0, value); // don't set width or height below zero (#311) + } + // clipping rectangle special if (wrapper.updateClipping) { - wrapper[key] = value; + wrapper[key] = value; // the key is now 'left' or 'top' for 'x' and 'y' wrapper.updateClipping(); } else { // normal elemStyle[key] = value; } - skipAttr = true; + skipAttr = true; - // x and y - } else if (key === 'x' || key === 'y') { - wrapper[key] = value; // used in getter - elemStyle[{ x: 'left', y: 'top' }[key]] = value; - // class name - } else if (key === 'class') { + } else if (key === 'class' && nodeName === 'DIV') { // IE8 Standards mode has problems retrieving the className - element.className = value; + element.className = value; // stroke } else if (key === 'stroke') { value = renderer.color(value, element, key); @@ -17214,17 +17501,24 @@ // fill } else if (key === 'fill') { if (nodeName === 'SPAN') { // text color elemStyle.color = value; - } else { + } else if (nodeName !== 'IMG') { // #1336 element.filled = value !== NONE ? true : false; value = renderer.color(value, element, key, wrapper); key = 'fillcolor'; } + + // opacity: don't bother - animation is too slow and filters introduce artifacts + } else if (key === 'opacity') { + /*css(element, { + opacity: value + });*/ + skipAttr = true; // rotation on VML elements } else if (nodeName === 'shape' && key === 'rotation') { wrapper[key] = value; // Correction for the 1x1 size of the shape container. Used in gauge needles. @@ -17241,12 +17535,13 @@ // text for rotated and non-rotated elements } else if (key === 'text') { this.bBox = null; element.innerHTML = value; skipAttr = true; - } + } + if (!skipAttr) { if (docMode8) { // IE8 setAttribute bug element[key] = value; } else { attr(element, key, value); @@ -17265,24 +17560,19 @@ * @param {String} id The id of the clip rectangle */ clip: function (clipRect) { var wrapper = this, clipMembers, - element = wrapper.element, - parentNode = element.parentNode, cssRet; if (clipRect) { clipMembers = clipRect.members; + erase(clipMembers, wrapper); // Ensure unique list of elements (#1258) clipMembers.push(wrapper); wrapper.destroyClip = function () { erase(clipMembers, wrapper); }; - // Issue #863 workaround - related to #140, #61, #74 - if (parentNode && parentNode.className === 'highcharts-tracker' && !docMode8) { - css(element, { visibility: HIDDEN }); - } cssRet = clipRect.getCSS(wrapper); } else { if (wrapper.destroyClip) { wrapper.destroyClip(); @@ -17322,25 +17612,10 @@ return SVGElement.prototype.destroy.apply(this); }, /** - * Remove all child nodes of a group, except the v:group element - */ - empty: function () { - var element = this.element, - childNodes = element.childNodes, - i = childNodes.length, - node; - - while (i--) { - node = childNodes[i]; - node.parentNode.removeChild(node); - } - }, - - /** * Add an event listener. VML override for normalizing event parameters. * @param {String} eventType * @param {Function} handler */ on: function (eventType, handler) { @@ -17471,10 +17746,11 @@ box.style.position = RELATIVE; // for freeform drawing using renderer directly container.appendChild(boxWrapper.element); // generate the containing box + renderer.isVML = true; renderer.box = box; renderer.boxWrapper = boxWrapper; renderer.setSize(width, height, false); @@ -17524,13 +17800,16 @@ left: isObj ? x.x : x, top: isObj ? x.y : y, width: isObj ? x.width : width, height: isObj ? x.height : height, getCSS: function (wrapper) { - var inverted = wrapper.inverted, + var element = wrapper.element, + nodeName = element.nodeName, + isShape = nodeName === 'shape', + inverted = wrapper.inverted, rect = this, - top = rect.top, + top = rect.top - (isShape ? element.offsetTop : 0), left = rect.left, right = left + rect.width, bottom = top + rect.height, ret = { clip: 'rect(' + @@ -17539,17 +17818,16 @@ mathRound(inverted ? right : bottom) + 'px,' + mathRound(inverted ? top : left) + 'px)' }; // issue 74 workaround - if (!inverted && docMode8 && wrapper.element.nodeName !== 'IMG') { + if (!inverted && docMode8 && nodeName === 'DIV') { extend(ret, { width: right + PX, height: bottom + PX }); } - return ret; }, // used in attr and animation to update the clipping of all members updateClipping: function () { @@ -17722,13 +18000,14 @@ ret = colorObject.get('rgb'); } else { - var strokeNodes = elem.getElementsByTagName(prop); - if (strokeNodes.length) { - strokeNodes[0].opacity = 1; + var propNodes = elem.getElementsByTagName(prop); // 'stroke' or 'fill' node + if (propNodes.length) { + propNodes[0].opacity = 1; + propNodes[0].type = 'solid'; } ret = color; } return ret; @@ -17791,11 +18070,18 @@ * @param {Number} x * @param {Number} y * @param {Number} r */ circle: function (x, y, r) { - return this.symbol('circle').attr({ x: x - r, y: y - r, width: 2 * r, height: 2 * r }); + var circle = this.symbol('circle'); + if (isObject(x)) { + r = x.r; + y = x.y; + x = x.x; + } + circle.isCircle = true; // Causes x and y to mean center (#1682) + return circle.attr({ x: x, y: y, width: 2 * r, height: 2 * r }); }, /** * Create a group using an outer div and an inner v:group to allow rotating * and flipping. A simple v:group would have problems with positioning @@ -17882,27 +18168,19 @@ // VML specific arc function arc: function (x, y, w, h, options) { var start = options.start, end = options.end, radius = options.r || w || h, + innerRadius = options.innerR, cosStart = mathCos(start), sinStart = mathSin(start), cosEnd = mathCos(end), sinEnd = mathSin(end), - innerRadius = options.innerR, - circleCorrection = 0.08 / radius, // #760 - innerCorrection = (innerRadius && 0.1 / innerRadius) || 0, ret; if (end - start === 0) { // no angle, don't show it. return ['x']; - - } else if (2 * mathPI - end + start < circleCorrection) { // full circle - // empirical correction found by trying out the limits for different radii - cosEnd = -circleCorrection; - } else if (end - start < innerCorrection) { // issue #186, another mysterious VML arc problem - cosEnd = mathCos(start + innerCorrection); } ret = [ 'wa', // clockwise arc to x - radius, // left @@ -17936,16 +18214,23 @@ y + innerRadius * sinStart, // end y 'x', // finish path 'e' // close ); + ret.isArc = true; return ret; }, // Add circle symbol path. This performs significantly faster than v:oval. - circle: function (x, y, w, h) { + circle: function (x, y, w, h, wrapper) { + // Center correction, #1682 + if (wrapper && wrapper.isCircle) { + x -= w / 2; + y -= h / 2; + } + // Return the path return [ 'wa', // clockwisearcto x, // left y, // top x + w, // right @@ -18026,11 +18311,11 @@ } return ret; } } }; -VMLRenderer = function () { +Highcharts.VMLRenderer = VMLRenderer = function () { this.init.apply(this, arguments); }; VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension); // general renderer @@ -18055,11 +18340,11 @@ /** * The CanVGRenderer is empty from start to keep the source footprint small. * When requested, the CanVGController downloads the rest of the source packaged * together with the canvg library. */ - CanVGRenderer = function () { + Highcharts.CanVGRenderer = CanVGRenderer = function () { // Override the global SVG namespace to fake SVG/HTML that accepts CSS SVG_NS = 'http://www.w3.org/1999/xhtml'; }; /** @@ -18099,32 +18384,30 @@ // Register render call deferredRenderCalls.push(func); } }; }()); + + Renderer = CanVGRenderer; } // end CanVGRenderer /* **************************************************************************** * * * END OF ANDROID < 3 SPECIFIC CODE * * * *****************************************************************************/ /** - * General renderer - */ -Renderer = VMLRenderer || CanVGRenderer || SVGRenderer; -/** * The Tick class */ -function Tick(axis, pos, type) { +function Tick(axis, pos, type, noLabel) { this.axis = axis; this.pos = pos; this.type = type || ''; this.isNew = true; - if (!type) { + if (!type && !noLabel) { this.addLabel(); } } Tick.prototype = { @@ -18136,24 +18419,27 @@ axis = tick.axis, options = axis.options, chart = axis.chart, horiz = axis.horiz, categories = axis.categories, + names = axis.series[0] && axis.series[0].names, pos = tick.pos, labelOptions = options.labels, str, tickPositions = axis.tickPositions, - width = (categories && horiz && categories.length && + width = (horiz && categories && !labelOptions.step && !labelOptions.staggerLines && !labelOptions.rotation && chart.plotWidth / tickPositions.length) || - (!horiz && chart.plotWidth / 2), + (!horiz && (chart.optionsMarginLeft || chart.plotWidth / 2)), // #1580 isFirst = pos === tickPositions[0], isLast = pos === tickPositions[tickPositions.length - 1], css, attr, - value = categories && defined(categories[pos]) ? categories[pos] : pos, + value = categories ? + pick(categories[pos], names && names[pos], pos) : + pos, label = tick.label, tickPositionInfo = tickPositions.info, dateTimeLabelFormat; // Set the datetime label format. If a higher rank is set for this position, use that. If not, @@ -18363,11 +18649,11 @@ * Put everything in place * * @param index {Number} * @param old {Boolean} Use old coordinates to prepare an animation into new position */ - render: function (index, old) { + render: function (index, old, opacity) { var tick = this, axis = tick.axis, options = axis.options, chart = axis.chart, renderer = chart.renderer, @@ -18394,15 +18680,18 @@ show = true, tickmarkOffset = axis.tickmarkOffset, xy = tick.getPosition(horiz, pos, tickmarkOffset, old), x = xy.x, y = xy.y, + reverseCrisp = ((horiz && x === axis.pos) || (!horiz && y === axis.pos + axis.len)) ? -1 : 1, // #1480 staggerLines = axis.staggerLines; + + this.isActive = true; // create the grid line if (gridLineWidth) { - gridLinePath = axis.getPlotLinePath(pos + tickmarkOffset, gridLineWidth, old); + gridLinePath = axis.getPlotLinePath(pos + tickmarkOffset, gridLineWidth * reverseCrisp, old, true); if (gridLine === UNDEFINED) { attribs = { stroke: gridLineColor, 'stroke-width': gridLineWidth @@ -18411,10 +18700,13 @@ attribs.dashstyle = dashStyle; } if (!type) { attribs.zIndex = 1; } + if (old) { + attribs.opacity = 0; + } tick.gridLine = gridLine = gridLineWidth ? renderer.path(gridLinePath) .attr(attribs).add(axis.gridGroup) : null; @@ -18422,11 +18714,12 @@ // If the parameter 'old' is set, the current call will be followed // by another call, therefore do not do any animations this time if (!old && gridLine && gridLinePath) { gridLine[tick.isNew ? 'attr' : 'animate']({ - d: gridLinePath + d: gridLinePath, + opacity: opacity }); } } // create the tick mark @@ -18438,22 +18731,24 @@ } if (axis.opposite) { tickLength = -tickLength; } - markPath = tick.getMarkPath(x, y, tickLength, tickWidth, horiz, renderer); + markPath = tick.getMarkPath(x, y, tickLength, tickWidth * reverseCrisp, horiz, renderer); if (mark) { // updating mark.animate({ - d: markPath + d: markPath, + opacity: opacity }); } else { // first time tick.mark = renderer.path( markPath ).attr({ stroke: tickColor, - 'stroke-width': tickWidth + 'stroke-width': tickWidth, + opacity: opacity }).add(axis.axisGroup); } } // the label is created on init - now move it into place @@ -18475,16 +18770,16 @@ // show those indices dividable by step show = false; } // Set the new position, and show or hide - if (show) { + if (show && !isNaN(xy.y)) { + xy.opacity = opacity; label[tick.isNew ? 'attr' : 'animate'](xy); - label.show(); tick.isNew = false; } else { - label.hide(); + label.attr('y', -9999); // #1338 } } }, /** @@ -18504,13 +18799,10 @@ if (options) { this.options = options; this.id = options.id; } - - //plotLine.render() - return this; } PlotLineOrBand.prototype = { /** @@ -18726,23 +19018,27 @@ /** * Renders the stack total label and adds it to the stack label group. */ render: function (group) { - var str = this.options.formatter.call(this); // format the text in the label + var options = this.options, + formatOption = options.format, // docs: added stackLabel.format option + str = formatOption ? + format(formatOption, this) : + options.formatter.call(this); // format the text in the label // Change the text to reflect the new total and set visibility to hidden in case the serie is hidden if (this.label) { this.label.attr({text: str, visibility: HIDDEN}); // Create new label } else { this.label = - this.axis.chart.renderer.text(str, 0, 0) // dummy positions, actual position updated with setOffset method in columnseries - .css(this.options.style) // apply style + this.axis.chart.renderer.text(str, 0, 0, options.useHTML) // dummy positions, actual position updated with setOffset method in columnseries + .css(options.style) // apply style .attr({ align: this.textAlign, // fix the text-anchor - rotation: this.options.rotation, // rotation + rotation: options.rotation, // rotation visibility: HIDDEN // hidden until setOffset is called }) .add(group); // add to the labels-group } }, @@ -18865,11 +19161,11 @@ align: 'middle', // low, middle or high //margin: 0 for horizontal, 10 for vertical axes, //rotation: 0, //side: 'outside', style: { - color: '#6D869F', + color: '#4d759e', //font: defaultFont.replace('normal', 'bold') fontWeight: 'bold' } //x: 0, //y: 0 @@ -18895,22 +19191,22 @@ minPadding: 0.05, startOnTick: true, tickWidth: 0, title: { rotation: 270, - text: 'Y-values' + text: 'Values' }, stackLabels: { enabled: false, //align: dynamic, //y: dynamic, //x: dynamic, //verticalAlign: dynamic, //textAlign: dynamic, //rotation: 0, formatter: function () { - return this.total; + return numberFormat(this.total, -1); }, style: defaultLabelOptions.style } }, @@ -19014,30 +19310,30 @@ //axis.ignoreMinPadding = UNDEFINED; // can be set to true by a column or bar series //axis.ignoreMaxPadding = UNDEFINED; axis.chart = chart; axis.reversed = options.reversed; + axis.zoomEnabled = options.zoomEnabled !== false; // Initial categories - axis.categories = options.categories; + axis.categories = options.categories || type === 'category'; // Elements //axis.axisGroup = UNDEFINED; //axis.gridGroup = UNDEFINED; //axis.axisTitle = UNDEFINED; //axis.axisLine = UNDEFINED; - // Flag if type === logarithmic + // Shorthand types axis.isLog = type === 'logarithmic'; + axis.isDatetimeAxis = isDatetimeAxis; // Flag, if axis is linked to another axis axis.isLinked = defined(options.linkedTo); // Linked axis. //axis.linkedParent = UNDEFINED; - // Flag if type === datetime - axis.isDatetimeAxis = isDatetimeAxis; // Flag if percentage mode //axis.usePercentage = UNDEFINED; @@ -19045,11 +19341,11 @@ //axis.tickPositions = UNDEFINED; // array containing predefined positions // Tick intervals //axis.tickInterval = UNDEFINED; //axis.minorTickInterval = UNDEFINED; - axis.tickmarkOffset = (options.categories && options.tickmarkPlacement === 'between') ? 0.5 : 0; + axis.tickmarkOffset = (axis.categories && options.tickmarkPlacement === 'between') ? 0.5 : 0; // Major ticks axis.ticks = {}; // Minor ticks axis.minorTicks = {}; @@ -19082,10 +19378,11 @@ axis.offset = options.offset || 0; // Dictionary for stacks axis.stacks = {}; + axis._stacksTouched = 0; // Min and max in the data //axis.dataMin = UNDEFINED, //axis.dataMax = UNDEFINED, @@ -19101,24 +19398,24 @@ var eventType, events = axis.options.events; // Register - chart.axes.push(axis); - chart[isXAxis ? 'xAxis' : 'yAxis'].push(axis); + if (inArray(axis, chart.axes) === -1) { // don't add it again on Axis.update() + chart.axes.push(axis); + chart[isXAxis ? 'xAxis' : 'yAxis'].push(axis); + } - axis.series = []; // populated by Series + axis.series = axis.series || []; // populated by Series // inverted charts have reversed xAxes as default if (chart.inverted && isXAxis && axis.reversed === UNDEFINED) { axis.reversed = true; } axis.removePlotBand = axis.removePlotBandOrLine; axis.removePlotLine = axis.removePlotBandOrLine; - axis.addPlotBand = axis.addPlotBandOrLine; - axis.addPlotLine = axis.addPlotBandOrLine; // register event listeners for (eventType in events) { addEvent(axis, eventType, events[eventType]); @@ -19144,11 +19441,56 @@ defaultOptions[this.isXAxis ? 'xAxis' : 'yAxis'], // if set in setOptions (#1053) userOptions ) ); }, + + /** + * Update the axis with a new options structure + */ + update: function (newOptions, redraw) { + var chart = this.chart; + + newOptions = chart.options[this.xOrY + 'Axis'][this.options.index] = merge(this.userOptions, newOptions); + + this.destroy(); + this._addedPlotLB = false; // #1611 + + this.init(chart, newOptions); + + chart.isDirtyBox = true; + if (pick(redraw, true)) { + chart.redraw(); + } + }, + /** + * Remove the axis from the chart + */ + remove: function (redraw) { + var chart = this.chart, + key = this.xOrY + 'Axis'; // xAxis or yAxis + + // Remove associated series + each(this.series, function (series) { + series.remove(false); + }); + + // Remove the axis + erase(chart.axes, this); + erase(chart[key], this); + chart.options[key].splice(this.options.index, 1); + each(chart[key], function (axis, i) { // Re-index, #1706 + axis.options.index = i; + }); + this.destroy(); + chart.isDirtyBox = true; + + if (pick(redraw, true)) { + chart.redraw(); + } + }, /** * The default label formatter. The context is a special config object for the label. */ defaultLabelFormatter: function () { @@ -19158,15 +19500,19 @@ dateTimeLabelFormat = this.dateTimeLabelFormat, numericSymbols = defaultOptions.lang.numericSymbols, i = numericSymbols && numericSymbols.length, multi, ret, + formatOption = axis.options.labels.format, // make sure the same symbol is added for all labels on a linear axis numericSymbolDetector = axis.isLog ? value : axis.tickInterval; - if (categories) { + if (formatOption) { + ret = format(formatOption, this); + + } else if (categories) { ret = value; } else if (dateTimeLabelFormat) { // datetime axis ret = dateFormat(dateTimeLabelFormat, value); @@ -19201,10 +19547,12 @@ var axis = this, chart = axis.chart, stacks = axis.stacks, posStack = [], negStack = [], + stacksTouched = axis._stacksTouched = axis._stacksTouched + 1, + type, i; axis.hasVisibleSeries = false; // reset dataMin and dataMax in case we're redrawing @@ -19227,10 +19575,12 @@ x, y, threshold = seriesOptions.threshold, yDataLength, activeYData = [], + seriesDataMin, + seriesDataMax, activeCounter = 0; axis.hasVisibleSeries = true; // Validate threshold in logarithmic axes @@ -19256,11 +19606,10 @@ //findPointRange, //pointRange, j, hasModifyValue = !!series.modifyValue; - // Handle stacking stacking = seriesOptions.stacking; axis.usePercentage = stacking === 'percent'; // create a stack for this particular series type @@ -19298,15 +19647,18 @@ if (stacking) { isNegative = y < threshold; pointStack = isNegative ? negPointStack : posPointStack; key = isNegative ? negKey : stackKey; - y = pointStack[x] = - defined(pointStack[x]) ? - correctFloat(pointStack[x] + y) : - y; + // Set the stack value and y for extremes + if (defined(pointStack[x])) { // we're adding to the stack + pointStack[x] = correctFloat(pointStack[x] + y); + y = [y, pointStack[x]]; // consider both the actual value and the stack (#1376) + } else { // it's the first point in the stack + pointStack[x] = y; + } // add the series if (!stacks[key]) { stacks[key] = {}; } @@ -19314,24 +19666,26 @@ // If the StackItem is there, just update the values, // if not, create one first if (!stacks[key][x]) { stacks[key][x] = new StackItem(axis, axis.options.stackLabels, isNegative, x, stackOption, stacking); } - stacks[key][x].setTotal(y); + stacks[key][x].setTotal(pointStack[x]); + stacks[key][x].touched = stacksTouched; } // Handle non null values - if (y !== null && y !== UNDEFINED) { + if (y !== null && y !== UNDEFINED && (!axis.isLog || (y.length || y > 0))) { // general hook, used for Highstock compare values feature if (hasModifyValue) { y = series.modifyValue(y); } - // for points within the visible range, including the first point outside the + // For points within the visible range, including the first point outside the // visible range, consider y extremes - if (cropped || ((xData[i + 1] || x) >= xExtremes.min && (xData[i - 1] || x) <= xExtremes.max)) { + if (series.getExtremesFromAll || cropped || ((xData[i + 1] || x) >= xExtremes.min && + (xData[i - 1] || x) <= xExtremes.max)) { j = y.length; if (j) { // array, like ohlc or range data while (j--) { if (y[j] !== null) { @@ -19343,21 +19697,17 @@ } } } } - // record the least unit distance - /*if (findPointRange) { - series.pointRange = pointRange || 1; - } - series.closestPointRange = pointRange;*/ - // Get the dataMin and dataMax so far. If percentage is used, the min and max are // always 0 and 100. If the length of activeYData is 0, continue with null values. if (!axis.usePercentage && activeYData.length) { - axis.dataMin = mathMin(pick(axis.dataMin, activeYData[0]), arrayMin(activeYData)); - axis.dataMax = mathMax(pick(axis.dataMax, activeYData[0]), arrayMax(activeYData)); + series.dataMin = seriesDataMin = arrayMin(activeYData); + series.dataMax = seriesDataMax = arrayMax(activeYData); + axis.dataMin = mathMin(pick(axis.dataMin, seriesDataMin), seriesDataMin); + axis.dataMax = mathMax(pick(axis.dataMax, seriesDataMax), seriesDataMax); } // Adjust to threshold if (defined(threshold)) { if (axis.dataMin >= threshold) { @@ -19369,10 +19719,20 @@ } } } } }); + + // Destroy unused stacks (#1044) + for (type in stacks) { + for (i in stacks[type]) { + if (stacks[type][i].touched < stacksTouched) { + stacks[type][i].destroy(); + delete stacks[type][i]; + } + } + } }, /** * Translate from axis value to pixel position on the chart, or back @@ -19384,54 +19744,81 @@ sign = 1, cvsOffset = 0, localA = old ? axis.oldTransA : axis.transA, localMin = old ? axis.oldMin : axis.min, returnValue, - postTranslate = axis.options.ordinal || (axis.isLog && handleLog); + minPixelPadding = axis.minPixelPadding, + postTranslate = (axis.options.ordinal || (axis.isLog && handleLog)) && axis.lin2val; if (!localA) { localA = axis.transA; } + // In vertical axes, the canvas coordinates start from 0 at the top like in + // SVG. if (cvsCoord) { sign *= -1; // canvas coordinates inverts the value cvsOffset = axisLength; } - if (axis.reversed) { // reversed axis + + // Handle reversed axis + if (axis.reversed) { sign *= -1; cvsOffset -= sign * axisLength; } + // From pixels to value if (backwards) { // reverse translation - if (axis.reversed) { - val = axisLength - val; - } + + val = val * sign + cvsOffset; + val -= minPixelPadding; returnValue = val / localA + localMin; // from chart pixel to value if (postTranslate) { // log and ordinal axes returnValue = axis.lin2val(returnValue); } - } else { // normal translation, from axis value to pixel, relative to plot + // From value to pixels + } else { if (postTranslate) { // log and ordinal axes val = axis.val2lin(val); } - returnValue = sign * (val - localMin) * localA + cvsOffset + (sign * axis.minPixelPadding) + + returnValue = sign * (val - localMin) * localA + cvsOffset + (sign * minPixelPadding) + (pointPlacementBetween ? localA * axis.pointRange / 2 : 0); } return returnValue; }, /** + * Utility method to translate an axis value to pixel position. + * @param {Number} value A value in terms of axis units + * @param {Boolean} paneCoordinates Whether to return the pixel coordinate relative to the chart + * or just the axis/pane itself. + */ + toPixels: function (value, paneCoordinates) { + return this.translate(value, false, !this.horiz, null, true) + (paneCoordinates ? 0 : this.pos); + }, + + /* + * Utility method to translate a pixel position in to an axis value + * @param {Number} pixel The pixel value coordinate + * @param {Boolean} paneCoordiantes Whether the input pixel is relative to the chart or just the + * axis/pane itself. + */ + toValue: function (pixel, paneCoordinates) { + return this.translate(pixel - (paneCoordinates ? 0 : this.pos), true, !this.horiz, null, true); + }, + + /** * Create the path for a plot line that goes from the given value on * this axis, across the plot to the opposite side * @param {Number} value * @param {Number} lineWidth Used for calculation crisp line * @param {Number] old Use old coordinates (for resizing and rescaling) */ - getPlotLinePath: function (value, lineWidth, old) { + getPlotLinePath: function (value, lineWidth, old, force) { var axis = this, chart = axis.chart, axisLeft = axis.left, axisTop = axis.top, x1, @@ -19462,11 +19849,11 @@ if (y1 < axisTop || y1 > axisTop + axis.height) { skip = true; } } - return skip ? + return skip && !force ? null : chart.renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 0); }, /** @@ -19527,15 +19914,14 @@ * Set the tick positions of a logarithmic axis */ getLogTickPositions: function (interval, min, max, minor) { var axis = this, options = axis.options, - axisLength = axis.len; - - // Since we use this method for both major and minor ticks, - // use a local variable and return the result - var positions = []; + axisLength = axis.len, + // Since we use this method for both major and minor ticks, + // use a local variable and return the result + positions = []; // Reset if (!minor) { axis._minorAutoInterval = null; } @@ -19568,11 +19954,11 @@ for (i = roundedMin; i < max + 1 && !break2; i++) { len = intermediate.length; for (j = 0; j < len && !break2; j++) { pos = log2lin(lin2log(i) * intermediate[j]); - if (pos > min) { + if (pos > min && (!minor || lastPos <= max)) { // #1670 positions.push(lastPos); } if (lastPos > max) { break2 = true; @@ -19626,14 +20012,14 @@ * Return the minor tick positions. For logarithmic axes, reuse the same logic * as for major ticks. */ getMinorTickPositions: function () { var axis = this, + options = axis.options, tickPositions = axis.tickPositions, - minorTickInterval = axis.minorTickInterval; - - var minorTickPositions = [], + minorTickInterval = axis.minorTickInterval, + minorTickPositions = [], pos, i, len; if (axis.isLog) { @@ -19641,17 +20027,27 @@ for (i = 1; i < len; i++) { minorTickPositions = minorTickPositions.concat( axis.getLogTickPositions(minorTickInterval, tickPositions[i - 1], tickPositions[i], true) ); } - + } else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314 + minorTickPositions = minorTickPositions.concat( + getTimeTicks( + normalizeTimeTickInterval(minorTickInterval), + axis.min, + axis.max, + options.startOfWeek + ) + ); + if (minorTickPositions[0] < axis.min) { + minorTickPositions.shift(); + } } else { for (pos = axis.min + (tickPositions[0] - axis.min) % minorTickInterval; pos <= axis.max; pos += minorTickInterval) { - minorTickPositions.push(pos); + minorTickPositions.push(pos); } } - return minorTickPositions; }, /** * Adjust the min and max for the minimum range. Keep in mind that the series data is @@ -19731,18 +20127,19 @@ }, /** * Update translation information */ - setAxisTranslation: function () { + setAxisTranslation: function (saveOld) { var axis = this, range = axis.max - axis.min, pointRange = 0, closestPointRange, minPointOffset = 0, pointRangePadding = 0, linkedParent = axis.linkedParent, + ordinalCorrection, transA = axis.transA; // adjust translation for padding if (axis.isXAxis) { if (linkedParent) { @@ -19753,10 +20150,13 @@ each(axis.series, function (series) { var seriesPointRange = series.pointRange, pointPlacement = series.options.pointPlacement, seriesClosestPointRange = series.closestPointRange; + if (seriesPointRange > range) { // #1446 + seriesPointRange = 0; + } pointRange = mathMax(pointRange, seriesPointRange); // minPointOffset is the value padding to the left of the axis in order to make // room for points with a pointRange, typically columns. When the pointPlacement option // is 'between' or 'on', this padding does not apply. @@ -19780,25 +20180,27 @@ } }); } // Record minPointOffset and pointRangePadding - axis.minPointOffset = minPointOffset; - axis.pointRangePadding = pointRangePadding; + ordinalCorrection = axis.ordinalSlope && closestPointRange ? axis.ordinalSlope / closestPointRange : 1; // #988, #1853 + axis.minPointOffset = minPointOffset = minPointOffset * ordinalCorrection; + axis.pointRangePadding = pointRangePadding = pointRangePadding * ordinalCorrection; // pointRange means the width reserved for each point, like in a column chart - axis.pointRange = pointRange; + axis.pointRange = mathMin(pointRange, range); // closestPointRange means the closest distance between points. In columns // it is mostly equal to pointRange, but in lines pointRange is 0 while closestPointRange // is some other value axis.closestPointRange = closestPointRange; } - // secondary values - axis.oldTransA = transA; - //axis.translationSlope = axis.transA = transA = axis.len / ((range + (2 * minPointOffset)) || 1); + // Secondary values + if (saveOld) { + axis.oldTransA = transA; + } axis.translationSlope = axis.transA = transA = axis.len / ((range + pointRangePadding) || 1); axis.transB = axis.horiz ? axis.left : axis.bottom; // translation addend axis.minPixelPadding = transA * minPointOffset; }, @@ -19854,23 +20256,31 @@ axis.userMax = axis.max; if (secondPass) { axis.range = null; // don't use it when running setExtremes } } + + // Hook for adjusting this.min and this.max. Used by bubble series. + if (axis.beforePadding) { + axis.beforePadding(); + } // adjust min and max for the minimum range axis.adjustForMinRange(); - - // pad the values to get clear of the chart's edges + + // Pad the values to get clear of the chart's edges. To avoid tickInterval taking the padding + // into account, we do this after computing tick interval (#1337). if (!categories && !axis.usePercentage && !isLinked && defined(axis.min) && defined(axis.max)) { - length = (axis.max - axis.min) || 1; - if (!defined(options.min) && !defined(axis.userMin) && minPadding && (axis.dataMin < 0 || !axis.ignoreMinPadding)) { - axis.min -= length * minPadding; + length = axis.max - axis.min; + if (length) { + if (!defined(options.min) && !defined(axis.userMin) && minPadding && (axis.dataMin < 0 || !axis.ignoreMinPadding)) { + axis.min -= length * minPadding; + } + if (!defined(options.max) && !defined(axis.userMax) && maxPadding && (axis.dataMax > 0 || !axis.ignoreMaxPadding)) { + axis.max += length * maxPadding; + } } - if (!defined(options.max) && !defined(axis.userMax) && maxPadding && (axis.dataMax > 0 || !axis.ignoreMaxPadding)) { - axis.max += length * maxPadding; - } } // get tickInterval if (axis.min === axis.max || axis.min === undefined || axis.max === undefined) { axis.tickInterval = 1; @@ -19893,11 +20303,11 @@ series.processData(axis.min !== axis.oldMin || axis.max !== axis.oldMax); }); } // set the translation factor used in translate function - axis.setAxisTranslation(secondPass); + axis.setAxisTranslation(true); // hook for ordinal axes and radial axes if (axis.beforeSetTickPositions) { axis.beforeSetTickPositions(); } @@ -19923,11 +20333,13 @@ // get minorTickInterval axis.minorTickInterval = options.minorTickInterval === 'auto' && axis.tickInterval ? axis.tickInterval / 5 : options.minorTickInterval; // find the tick positions - axis.tickPositions = tickPositions = options.tickPositions || (tickPositioner && tickPositioner.apply(axis, [axis.min, axis.max])); + axis.tickPositions = tickPositions = options.tickPositions ? + [].concat(options.tickPositions) : // Work on a copy (#1565) + (tickPositioner && tickPositioner.apply(axis, [axis.min, axis.max])); if (!tickPositions) { if (isDatetimeAxis) { tickPositions = (axis.getNonLinearTimeTicks || getTimeTicks)( normalizeTimeTickInterval(axis.tickInterval, options.units), axis.min, @@ -19948,11 +20360,12 @@ if (!isLinked) { // reset min/max or remove extremes based on start/end on tick var roundedMin = tickPositions[0], roundedMax = tickPositions[tickPositions.length - 1], - minPointOffset = axis.minPointOffset || 0; + minPointOffset = axis.minPointOffset || 0, + singlePad; if (options.startOnTick) { axis.min = roundedMin; } else if (axis.min - minPointOffset > roundedMin) { tickPositions.shift(); @@ -19962,54 +20375,55 @@ axis.max = roundedMax; } else if (axis.max + minPointOffset < roundedMax) { tickPositions.pop(); } + // When there is only one point, or all points have the same value on this axis, then min + // and max are equal and tickPositions.length is 1. In this case, add some padding + // in order to center the point, but leave it with one tick. #1337. + if (tickPositions.length === 1) { + singlePad = 0.001; // The lowest possible number to avoid extra padding on columns + axis.min -= singlePad; + axis.max += singlePad; + } } }, /** * Set the max ticks of either the x and y axis collection */ setMaxTicks: function () { var chart = this.chart, - maxTicks = chart.maxTicks, + maxTicks = chart.maxTicks || {}, tickPositions = this.tickPositions, - xOrY = this.xOrY; + key = this._maxTicksKey = [this.xOrY, this.pos, this.len].join('-'); - if (!maxTicks) { // first call, or maxTicks have been reset after a zoom operation - maxTicks = { - x: 0, - y: 0 - }; + if (!this.isLinked && !this.isDatetimeAxis && tickPositions && tickPositions.length > (maxTicks[key] || 0) && this.options.alignTicks !== false) { + maxTicks[key] = tickPositions.length; } - - if (!this.isLinked && !this.isDatetimeAxis && tickPositions.length > maxTicks[xOrY] && this.options.alignTicks !== false) { - maxTicks[xOrY] = tickPositions.length; - } chart.maxTicks = maxTicks; }, /** * When using multiple axes, adjust the number of ticks to match the highest * number of ticks in that group */ adjustTickAmount: function () { var axis = this, chart = axis.chart, - xOrY = axis.xOrY, + key = axis._maxTicksKey, tickPositions = axis.tickPositions, maxTicks = chart.maxTicks; - if (maxTicks && maxTicks[xOrY] && !axis.isDatetimeAxis && !axis.categories && !axis.isLinked && axis.options.alignTicks !== false) { // only apply to linear scale + if (maxTicks && maxTicks[key] && !axis.isDatetimeAxis && !axis.categories && !axis.isLinked && axis.options.alignTicks !== false) { // only apply to linear scale var oldTickAmount = axis.tickAmount, calculatedTickAmount = tickPositions.length, tickAmount; // set the axis-level tickAmount to use below - axis.tickAmount = tickAmount = maxTicks[xOrY]; + axis.tickAmount = tickAmount = maxTicks[key]; if (calculatedTickAmount < tickAmount) { while (tickPositions.length < tickAmount) { tickPositions.push(correctFloat( tickPositions[tickPositions.length - 1] + axis.tickInterval @@ -20053,13 +20467,15 @@ isDirtyData = true; } }); // do we really need to go through all this? - if (isDirtyAxisLength || isDirtyData || axis.isLinked || + if (isDirtyAxisLength || isDirtyData || axis.isLinked || axis.forceRedraw || axis.userMin !== axis.oldUserMin || axis.userMax !== axis.oldUserMax) { + axis.forceRedraw = false; + // get data extremes if needed axis.getSeriesExtremes(); // get fixed positions based on tickInterval axis.setTickPositions(); @@ -20129,34 +20545,60 @@ /** * Overridable method for zooming chart. Pulled out in a separate method to allow overriding * in stock charts. */ zoom: function (newMin, newMax) { - this.setExtremes(newMin, newMax, false, UNDEFINED, { trigger: 'zoom' }); + + // Prevent pinch zooming out of range + if (!this.allowZoomOutside) { + if (newMin <= this.dataMin) { + newMin = UNDEFINED; + } + if (newMax >= this.dataMax) { + newMax = UNDEFINED; + } + } + + // In full view, displaying the reset zoom button is not required + this.displayBtn = newMin !== UNDEFINED || newMax !== UNDEFINED; + + // Do it + this.setExtremes( + newMin, + newMax, + false, + UNDEFINED, + { trigger: 'zoom' } + ); return true; }, /** * Update the axis metrics */ setAxisSize: function () { - var axis = this, - chart = axis.chart, - options = axis.options; + var chart = this.chart, + options = this.options, + offsetLeft = options.offsetLeft || 0, + offsetRight = options.offsetRight || 0, + horiz = this.horiz, + width, + height, + top, + left; - var offsetLeft = options.offsetLeft || 0, - offsetRight = options.offsetRight || 0; + // Expose basic values to use in Series object and navigator + this.left = left = pick(options.left, chart.plotLeft + offsetLeft); + this.top = top = pick(options.top, chart.plotTop); + this.width = width = pick(options.width, chart.plotWidth - offsetLeft + offsetRight); + this.height = height = pick(options.height, chart.plotHeight); + this.bottom = chart.chartHeight - height - top; + this.right = chart.chartWidth - width - left; - // basic values - // expose to use in Series object and navigator - axis.left = pick(options.left, chart.plotLeft + offsetLeft); - axis.top = pick(options.top, chart.plotTop); - axis.width = pick(options.width, chart.plotWidth - offsetLeft + offsetRight); - axis.height = pick(options.height, chart.plotHeight); - axis.bottom = chart.chartHeight - axis.height - axis.top; - axis.right = chart.chartWidth - axis.width - axis.left; - axis.len = mathMax(axis.horiz ? axis.width : axis.height, 0); // mathMax fixes #905 + // Direction agnostic properties + this.len = mathMax(horiz ? width : height, 0); // mathMax fixes #905 + this.pos = horiz ? left : top; // distance from SVG origin }, /** * Get the actual axis extremes */ @@ -20192,18 +20634,35 @@ } return axis.translate(threshold, 0, 1, 0, 1); }, + addPlotBand: function (options) { + this.addPlotBandOrLine(options, 'plotBands'); + }, + + addPlotLine: function (options) { + this.addPlotBandOrLine(options, 'plotLines'); + }, + /** * Add a plot band or plot line after render time * * @param options {Object} The plotBand or plotLine configuration object */ - addPlotBandOrLine: function (options) { - var obj = new PlotLineOrBand(this, options).render(); - this.plotLinesAndBands.push(obj); + addPlotBandOrLine: function (options, coll) { + var obj = new PlotLineOrBand(this, options).render(), + userOptions = this.userOptions; + + // Add it to the user options for exporting and Axis.update + if (coll) { + userOptions[coll] = userOptions[coll] || []; + userOptions[coll].push(options); + } + + this.plotLinesAndBands.push(obj); + return obj; }, /** * Render the tick labels to a preliminary position to get their sizes @@ -20215,28 +20674,28 @@ options = axis.options, tickPositions = axis.tickPositions, ticks = axis.ticks, horiz = axis.horiz, side = axis.side, + invertedSide = chart.inverted ? [1, 0, 3, 2][side] : side, hasData, showAxis, titleOffset = 0, titleOffsetOption, titleMargin = 0, axisTitleOptions = options.title, labelOptions = options.labels, labelOffset = 0, // reset axisOffset = chart.axisOffset, + clipOffset = chart.clipOffset, directionFactor = [-1, 1, 1, -1][side], n; - // For reuse in Axis.render axis.hasData = hasData = (axis.hasVisibleSeries || (defined(axis.min) && defined(axis.max) && !!tickPositions)); axis.showAxis = showAxis = hasData || pick(options.showEmpty, true); - // Create the axisGroup and gridGroup elements on first iteration if (!axis.axisGroup) { axis.gridGroup = renderer.g('grid') .attr({ zIndex: options.gridZIndex || 1 }) .add(); @@ -20280,11 +20739,11 @@ ticks[n].destroy(); delete ticks[n]; } } - if (axisTitleOptions && axisTitleOptions.text) { + if (axisTitleOptions && axisTitleOptions.text && axisTitleOptions.enabled !== false) { if (!axis.axisTitle) { axis.axisTitle = renderer.text( axisTitleOptions.text, 0, 0, @@ -20313,21 +20772,21 @@ } // handle automatic or user set offset axis.offset = directionFactor * pick(options.offset, axisOffset[side]); - axis.axisTitleMargin = pick(titleOffsetOption, labelOffset + titleMargin + (side !== 2 && labelOffset && directionFactor * options.labels[horiz ? 'y' : 'x']) ); axisOffset[side] = mathMax( axisOffset[side], axis.axisTitleMargin + titleOffset + directionFactor * axis.offset ); + clipOffset[invertedSide] = mathMax(clipOffset[invertedSide], options.lineWidth); }, /** * Get the path for the axis line @@ -20339,10 +20798,13 @@ horiz = this.horiz, lineLeft = this.left + (opposite ? this.width : 0) + offset, lineTop = chart.chartHeight - this.bottom - (opposite ? this.height : 0) + offset; this.lineTop = lineTop; // used by flag series + if (!opposite) { + lineWidth *= -1; // crispify the other way - #1480 + } return chart.renderer.crispLine([ M, horiz ? this.left : @@ -20426,10 +20888,18 @@ hasData = axis.hasData, showAxis = axis.showAxis, from, to; + // Mark all elements inActive before we go over and mark the active ones + each([ticks, minorTicks, alternateBands], function (coll) { + var pos; + for (pos in coll) { + coll[pos].isActive = false; + } + }); + // If the series has data draw the ticks. Else only the line and title if (hasData || isLinked) { // minor ticks if (axis.minorTickInterval && !axis.categories) { @@ -20441,41 +20911,49 @@ // render new ticks in old position if (slideInTicks && minorTicks[pos].isNew) { minorTicks[pos].render(null, true); } - - minorTicks[pos].isActive = true; - minorTicks[pos].render(); + minorTicks[pos].render(null, false, 1); }); } // Major ticks. Pull out the first item and render it last so that // we can get the position of the neighbour label. #808. - each(tickPositions.slice(1).concat([tickPositions[0]]), function (pos, i) { - - // Reorganize the indices - i = (i === tickPositions.length - 1) ? 0 : i + 1; - - // linked axes need an extra check to find out if - if (!isLinked || (pos >= axis.min && pos <= axis.max)) { - - if (!ticks[pos]) { - ticks[pos] = new Tick(axis, pos); + if (tickPositions.length) { // #1300 + each(tickPositions.slice(1).concat([tickPositions[0]]), function (pos, i) { + + // Reorganize the indices + i = (i === tickPositions.length - 1) ? 0 : i + 1; + + // linked axes need an extra check to find out if + if (!isLinked || (pos >= axis.min && pos <= axis.max)) { + + if (!ticks[pos]) { + ticks[pos] = new Tick(axis, pos); + } + + // render new ticks in old position + if (slideInTicks && ticks[pos].isNew) { + ticks[pos].render(i, true); + } + + ticks[pos].render(i, false, 1); } - - // render new ticks in old position - if (slideInTicks && ticks[pos].isNew) { - ticks[pos].render(i, true); + + }); + // In a categorized axis, the tick marks are displayed between labels. So + // we need to add a tick mark and grid line at the left edge of the X axis. + if (tickmarkOffset && axis.min === 0) { + if (!ticks[-1]) { + ticks[-1] = new Tick(axis, -1, null, true); } - - ticks[pos].isActive = true; - ticks[pos].render(i); + ticks[-1].render(-1); } + + } - }); - // alternate grid color if (alternateGridColor) { each(tickPositions, function (pos, i) { if (i % 2 === 0 && pos < axis.max) { if (!alternateBands[pos]) { @@ -20495,29 +20973,52 @@ } // custom plot lines and bands if (!axis._addedPlotLB) { // only first time each((options.plotLines || []).concat(options.plotBands || []), function (plotLineOptions) { - //plotLinesAndBands.push(new PlotLineOrBand(plotLineOptions).render()); axis.addPlotBandOrLine(plotLineOptions); }); axis._addedPlotLB = true; } } // end if hasData - // remove inactive ticks + // Remove inactive ticks each([ticks, minorTicks, alternateBands], function (coll) { - var pos; + var pos, + i, + forDestruction = [], + delay = globalAnimation ? globalAnimation.duration || 500 : 0, + destroyInactiveItems = function () { + i = forDestruction.length; + while (i--) { + // When resizing rapidly, the same items may be destroyed in different timeouts, + // or the may be reactivated + if (coll[forDestruction[i]] && !coll[forDestruction[i]].isActive) { + coll[forDestruction[i]].destroy(); + delete coll[forDestruction[i]]; + } + } + + }; + for (pos in coll) { + if (!coll[pos].isActive) { - coll[pos].destroy(); - delete coll[pos]; - } else { - coll[pos].isActive = false; // reset + // Render to zero opacity + coll[pos].render(pos, false, 0); + coll[pos].isActive = false; + forDestruction.push(pos); } } + + // When the objects are finished fading out, destroy them + if (coll === alternateBands || !chart.hasRendered || !delay) { + destroyInactiveItems(); + } else if (delay) { + setTimeout(destroyInactiveItems, delay); + } }); // Static items. As the axis group is cleared on subsequent calls // to render, these items are added outside the group. // axis line @@ -20596,34 +21097,24 @@ /** * Update the axis title by options */ setTitle: function (newTitleOptions, redraw) { - var chart = this.chart, - options = this.options, - axisTitle = this.axisTitle; - - options.title = merge(options.title, newTitleOptions); - - this.axisTitle = axisTitle && axisTitle.destroy(); // #922 - this.isDirty = true; - - if (pick(redraw, true)) { - chart.redraw(); - } + this.update({ title: newTitleOptions }, redraw); }, /** * Redraw the axis to reflect changes in the data or axis extremes */ redraw: function () { var axis = this, - chart = axis.chart; + chart = axis.chart, + pointer = chart.pointer; // hide tooltip and hover states - if (chart.tracker.resetTracker) { - chart.tracker.resetTracker(true); + if (pointer.reset) { + pointer.reset(true); } // render the axis axis.render(); @@ -20639,33 +21130,15 @@ }, /** * Set new axis categories and optionally redraw - * @param {Array} newCategories - * @param {Boolean} doRedraw + * @param {Array} categories + * @param {Boolean} redraw */ - setCategories: function (newCategories, doRedraw) { - var axis = this, - chart = axis.chart; - - // set the categories - axis.categories = axis.userOptions.categories = newCategories; - - // force reindexing tooltips - each(axis.series, function (series) { - series.translate(); - series.setTooltipPoints(true); - }); - - - // optionally redraw - axis.isDirty = true; - - if (pick(doRedraw, true)) { - chart.redraw(); - } + setCategories: function (categories, redraw) { + this.update({ categories: categories }, redraw); }, /** * Destroys an Axis instance. */ @@ -20703,56 +21176,63 @@ /** * The tooltip object * @param {Object} chart The chart instance * @param {Object} options Tooltip options */ -function Tooltip(chart, options) { - var borderWidth = options.borderWidth, - style = options.style, - padding = pInt(style.padding); +function Tooltip() { + this.init.apply(this, arguments); +} - // Save the chart and options - this.chart = chart; - this.options = options; +Tooltip.prototype = { - // Keep track of the current series - //this.currentSeries = UNDEFINED; + init: function (chart, options) { - // List of crosshairs - this.crosshairs = []; + var borderWidth = options.borderWidth, + style = options.style, + padding = pInt(style.padding); - // Current values of x and y when animating - this.now = { x: 0, y: 0 }; + // Save the chart and options + this.chart = chart; + this.options = options; - // The tooltip is initially hidden - this.isHidden = true; + // Keep track of the current series + //this.currentSeries = UNDEFINED; - // create the label - this.label = chart.renderer.label('', 0, 0, options.shape, null, null, options.useHTML, null, 'tooltip') - .attr({ - padding: padding, - fill: options.backgroundColor, - 'stroke-width': borderWidth, - r: options.borderRadius, - zIndex: 8 - }) - .css(style) - .css({ padding: 0 }) // Remove it from VML, the padding is applied as an attribute instead (#1117) - .hide() - .add(); + // List of crosshairs + this.crosshairs = []; - // When using canVG the shadow shows up as a gray circle - // even if the tooltip is hidden. - if (!useCanVG) { - this.label.shadow(options.shadow); - } + // Current values of x and y when animating + this.now = { x: 0, y: 0 }; - // Public property for getting the shared state. - this.shared = options.shared; -} + // The tooltip is initially hidden + this.isHidden = true; -Tooltip.prototype = { + + // create the label + this.label = chart.renderer.label('', 0, 0, options.shape, null, null, options.useHTML, null, 'tooltip') + .attr({ + padding: padding, + fill: options.backgroundColor, + 'stroke-width': borderWidth, + r: options.borderRadius, + zIndex: 8 + }) + .css(style) + .css({ padding: 0 }) // Remove it from VML, the padding is applied as an attribute instead (#1117) + .hide() + .add(); + + // When using canVG the shadow shows up as a gray circle + // even if the tooltip is hidden. + if (!useCanVG) { + this.label.shadow(options.shadow); + } + + // Public property for getting the shared state. + this.shared = options.shared; + }, + /** * Destroy the tooltip and its elements. */ destroy: function () { each(this.crosshairs, function (crosshair) { @@ -20763,10 +21243,12 @@ // Destroy and clear local variables if (this.label) { this.label = this.label.destroy(); } + clearTimeout(this.hideTimer); + clearTimeout(this.tooltipTimeout); }, /** * Provide a soft movement for the tooltip * @@ -20810,24 +21292,30 @@ /** * Hide the tooltip */ hide: function () { + var tooltip = this, + hoverPoints; + + clearTimeout(this.hideTimer); // disallow duplicate timers (#1728, #1766) if (!this.isHidden) { - var hoverPoints = this.chart.hoverPoints; + hoverPoints = this.chart.hoverPoints; - this.label.hide(); + this.hideTimer = setTimeout(function () { + tooltip.label.fadeOut(); + tooltip.isHidden = true; + }, pick(this.options.hideDelay, 500)); // hide previous hoverPoints and set new if (hoverPoints) { each(hoverPoints, function (point) { point.setState(); }); } this.chart.hoverPoints = null; - this.isHidden = true; } }, /** * Hide the crosshairs @@ -20846,35 +21334,46 @@ */ getAnchor: function (points, mouseEvent) { var ret, chart = this.chart, inverted = chart.inverted, + plotTop = chart.plotTop, plotX = 0, plotY = 0, yAxis; points = splat(points); // Pie uses a special tooltipPos ret = points[0].tooltipPos; + // When tooltip follows mouse, relate the position to the mouse + if (this.followPointer && mouseEvent) { + if (mouseEvent.chartX === UNDEFINED) { + mouseEvent = chart.pointer.normalize(mouseEvent); + } + ret = [ + mouseEvent.chartX - chart.plotLeft, + mouseEvent.chartY - plotTop + ]; + } // When shared, use the average position if (!ret) { each(points, function (point) { yAxis = point.series.yAxis; plotX += point.plotX; plotY += (point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY) + - (!inverted && yAxis ? yAxis.top - chart.plotTop : 0); // #1151 + (!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151 }); plotX /= points.length; plotY /= points.length; ret = [ inverted ? chart.plotWidth - plotY : plotX, this.shared && !inverted && points.length > 1 && mouseEvent ? - mouseEvent.chartY - chart.plotTop : // place shared tooltip next to the mouse (#424) + mouseEvent.chartY - plotTop : // place shared tooltip next to the mouse (#424) inverted ? chart.plotHeight - plotX : plotY ]; } return map(ret, mathRound); @@ -20930,60 +21429,61 @@ return {x: x, y: y}; }, /** + * In case no user defined formatter is given, this will be used. Note that the context + * here is an object holding point, series, x, y etc. + */ + defaultFormatter: function (tooltip) { + var items = this.points || splat(this), + series = items[0].series, + s; + + // build the header + s = [series.tooltipHeaderFormatter(items[0])]; + + // build the values + each(items, function (item) { + series = item.series; + s.push((series.tooltipFormatter && series.tooltipFormatter(item)) || + item.point.tooltipFormatter(series.tooltipOptions.pointFormat)); + }); + + // footer + s.push(tooltip.options.footerFormat || ''); + + return s.join(''); + }, + + /** * Refresh the tooltip's text and position. * @param {Object} point */ refresh: function (point, mouseEvent) { var tooltip = this, chart = tooltip.chart, label = tooltip.label, - options = tooltip.options; - - /** - * In case no user defined formatter is given, this will be used - */ - function defaultFormatter() { - var pThis = this, - items = pThis.points || splat(pThis), - series = items[0].series, - s; - - // build the header - s = [series.tooltipHeaderFormatter(items[0].key)]; - - // build the values - each(items, function (item) { - series = item.series; - s.push((series.tooltipFormatter && series.tooltipFormatter(item)) || - item.point.tooltipFormatter(series.tooltipOptions.pointFormat)); - }); - - // footer - s.push(options.footerFormat || ''); - - return s.join(''); - } - - var x, + options = tooltip.options, + x, y, show, anchor, textConfig = {}, text, pointConfig = [], - formatter = options.formatter || defaultFormatter, + formatter = options.formatter || tooltip.defaultFormatter, hoverPoints = chart.hoverPoints, - placedTooltipPoint, borderColor, crosshairsOptions = options.crosshairs, shared = tooltip.shared, currentSeries; + clearTimeout(this.hideTimer); + // get the reference point coordinates (pie charts use tooltipPos) + tooltip.followPointer = splat(point)[0].series.tooltipOptions.followPointer; anchor = tooltip.getAnchor(point, mouseEvent); x = anchor[0]; y = anchor[1]; // shared tooltip, array is sent over @@ -21013,11 +21513,11 @@ // single point tooltip } else { textConfig = point.getLabelConfig(); } - text = formatter.call(textConfig); + text = formatter.call(textConfig, tooltip); // register the current series currentSeries = point.series; @@ -21029,11 +21529,12 @@ this.hide(); } else { // show it if (tooltip.isHidden) { - label.show(); + stop(label); + label.attr('opacity', 1).show(); } // update text label.attr({ text: text @@ -21042,45 +21543,36 @@ // set the stroke color of the box borderColor = options.borderColor || point.color || currentSeries.color || '#606060'; label.attr({ stroke: borderColor }); - - placedTooltipPoint = (options.positioner || tooltip.getPosition).call( - tooltip, - label.width, - label.height, - { plotX: x, plotY: y } - ); - - // do the move - tooltip.move( - mathRound(placedTooltipPoint.x), - mathRound(placedTooltipPoint.y), - x + chart.plotLeft, - y + chart.plotTop - ); - - tooltip.isHidden = false; + tooltip.updatePosition({ plotX: x, plotY: y }); + + this.isHidden = false; } // crosshairs if (crosshairsOptions) { crosshairsOptions = splat(crosshairsOptions); // [x, y] var path, i = crosshairsOptions.length, attribs, - axis; + axis, + val; while (i--) { axis = point.series[i ? 'yAxis' : 'xAxis']; if (crosshairsOptions[i] && axis) { + val = i ? pick(point.stackY, point.y) : point.x; // #814 + if (axis.isLog) { // #1671 + val = log2lin(val); + } path = axis.getPlotLinePath( - i ? pick(point.stackY, point.y) : point.x, // #814 + val, 1 ); if (tooltip.crosshairs[i]) { tooltip.crosshairs[i].attr({ d: path, visibility: VISIBLE }); @@ -21104,54 +21596,80 @@ text: text, x: x + chart.plotLeft, y: y + chart.plotTop, borderColor: borderColor }); + }, + + /** + * Find the new position and perform the move + */ + updatePosition: function (point) { + var chart = this.chart, + label = this.label, + pos = (this.options.positioner || this.getPosition).call( + this, + label.width, + label.height, + point + ); + + // do the move + this.move( + mathRound(pos.x), + mathRound(pos.y), + point.plotX + chart.plotLeft, + point.plotY + chart.plotTop + ); } }; /** - * The mouse tracker object + * The mouse tracker object. All methods starting with "on" are primary DOM event handlers. + * Subsequent methods should be named differently from what they are doing. * @param {Object} chart The Chart instance * @param {Object} options The root options object */ -function MouseTracker(chart, options) { - var zoomType = useCanVG ? '' : options.chart.zoomType; +function Pointer(chart, options) { + this.init(chart, options); +} - // Zoom status - this.zoomX = /x/.test(zoomType); - this.zoomY = /y/.test(zoomType); +Pointer.prototype = { + /** + * Initialize Pointer + */ + init: function (chart, options) { + + var zoomType = useCanVG ? '' : options.chart.zoomType, + inverted = chart.inverted, + zoomX, + zoomY; - // Store reference to options - this.options = options; + // Store references + this.options = options; + this.chart = chart; + + // Zoom status + this.zoomX = zoomX = /x/.test(zoomType); + this.zoomY = zoomY = /y/.test(zoomType); + this.zoomHor = (zoomX && !inverted) || (zoomY && inverted); + this.zoomVert = (zoomY && !inverted) || (zoomX && inverted); - // Reference to the chart - this.chart = chart; + this.pinchDown = []; + this.lastValidTouch = {}; - // The interval id - //this.tooltipTimeout = UNDEFINED; + if (options.tooltip.enabled) { + chart.tooltip = new Tooltip(chart, options.tooltip); + } - // The cached x hover position - //this.hoverX = UNDEFINED; + this.setDOMEvents(); + }, - // The chart position - //this.chartPosition = UNDEFINED; - - // The selection marker element - //this.selectionMarker = UNDEFINED; - - // False or a value > 0 if a dragging operation - //this.mouseDownX = UNDEFINED; - //this.mouseDownY = UNDEFINED; - this.init(chart, options.tooltip); -} - -MouseTracker.prototype = { /** * Add crossbrowser support for chartX and chartY * @param {Object} e The event object in standard browsers */ - normalizeMouseEvent: function (e) { + normalize: function (e) { var chartPosition, chartX, chartY, ePos; @@ -21186,31 +21704,22 @@ }, /** * Get the click position in terms of axis values. * - * @param {Object} e A mouse event + * @param {Object} e A pointer event */ - getMouseCoordinates: function (e) { + getCoordinates: function (e) { var coordinates = { xAxis: [], yAxis: [] - }, - chart = this.chart; + }; - each(chart.axes, function (axis) { - var isXAxis = axis.isXAxis, - isHorizontal = chart.inverted ? !isXAxis : isXAxis; - - coordinates[isXAxis ? 'xAxis' : 'yAxis'].push({ + each(this.chart.axes, function (axis) { + coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({ axis: axis, - value: axis.translate( - (isHorizontal ? - e.chartX - chart.plotLeft : - axis.top + axis.len - e.chartY) - axis.minPixelPadding, // #1051 - true - ) + value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY']) }); }); return coordinates; }, @@ -21224,53 +21733,57 @@ chart.plotHeight + chart.plotTop - e.chartY : e.chartX - chart.plotLeft; }, /** - * With line type charts with a single tracker, get the point closest to the mouse + * With line type charts with a single tracker, get the point closest to the mouse. + * Run Point.onMouseOver and display tooltip for the point or points. */ - onmousemove: function (e) { - var mouseTracker = this, - chart = mouseTracker.chart, + runPointActions: function (e) { + var pointer = this, + chart = pointer.chart, series = chart.series, tooltip = chart.tooltip, point, points, hoverPoint = chart.hoverPoint, hoverSeries = chart.hoverSeries, i, j, distance = chart.chartWidth, - index = mouseTracker.getIndex(e); + index = pointer.getIndex(e), + anchor; // shared tooltip - if (tooltip && mouseTracker.options.tooltip.shared && !(hoverSeries && hoverSeries.noSharedTooltip)) { + if (tooltip && pointer.options.tooltip.shared && !(hoverSeries && hoverSeries.noSharedTooltip)) { points = []; // loop over all series and find the ones with points closest to the mouse i = series.length; for (j = 0; j < i; j++) { if (series[j].visible && series[j].options.enableMouseTracking !== false && !series[j].noSharedTooltip && series[j].tooltipPoints.length) { point = series[j].tooltipPoints[index]; - point._dist = mathAbs(index - point[series[j].xAxis.tooltipPosName || 'plotX']); - distance = mathMin(distance, point._dist); - points.push(point); + if (point.series) { // not a dummy point, #1544 + point._dist = mathAbs(index - point.clientX); + distance = mathMin(distance, point._dist); + points.push(point); + } } } // remove furthest points i = points.length; while (i--) { if (points[i]._dist > distance) { points.splice(i, 1); } } // refresh the tooltip if necessary - if (points.length && (points[0].plotX !== mouseTracker.hoverX)) { + if (points.length && (points[0].clientX !== pointer.hoverX)) { tooltip.refresh(points, e); - mouseTracker.hoverX = points[0].plotX; + pointer.hoverX = points[0].clientX; } } // separate tooltip and general mouse events if (hoverSeries && hoverSeries.tracker) { // only use for line-type series with common tracker @@ -21280,26 +21793,30 @@ // a new point is hovered, refresh the tooltip if (point && point !== hoverPoint) { // trigger the events - point.onMouseOver(); + point.onMouseOver(e); } + + } else if (tooltip && tooltip.followPointer && !tooltip.isHidden) { + anchor = tooltip.getAnchor([{}], e); + tooltip.updatePosition({ plotX: anchor[0], plotY: anchor[1] }); } }, /** * Reset the tracking by hiding the tooltip, the hover series state and the hover point * * @param allowMove {Boolean} Instead of destroying the tooltip altogether, allow moving it if possible */ - resetTracker: function (allowMove) { - var mouseTracker = this, - chart = mouseTracker.chart, + reset: function (allowMove) { + var pointer = this, + chart = pointer.chart, hoverSeries = chart.hoverSeries, hoverPoint = chart.hoverPoint, tooltip = chart.tooltip, tooltipPoints = tooltip && tooltip.shared ? chart.hoverPoints : hoverPoint; @@ -21329,445 +21846,663 @@ if (tooltip) { tooltip.hide(); tooltip.hideCrosshairs(); } - mouseTracker.hoverX = null; + pointer.hoverX = null; } }, /** - * Set the JS events on the container element + * Scale series groups to a certain scale and translation */ - setDOMEvents: function () { - var lastWasOutsidePlot = true, - mouseTracker = this, - chart = mouseTracker.chart, - container = chart.container, - hasDragged, - zoomHor = (mouseTracker.zoomX && !chart.inverted) || (mouseTracker.zoomY && chart.inverted), - zoomVert = (mouseTracker.zoomY && !chart.inverted) || (mouseTracker.zoomX && chart.inverted); + scaleGroups: function (attribs, clip) { - /** - * Mouse up or outside the plot area - */ - function drop() { - if (mouseTracker.selectionMarker) { - var selectionData = { - xAxis: [], - yAxis: [] - }, - selectionBox = mouseTracker.selectionMarker.getBBox(), - selectionLeft = selectionBox.x - chart.plotLeft, - selectionTop = selectionBox.y - chart.plotTop, - runZoom; + var chart = this.chart; - // a selection has been made - if (hasDragged) { - - // record each axis' min and max - each(chart.axes, function (axis) { - if (axis.options.zoomEnabled !== false) { - var isXAxis = axis.isXAxis, - isHorizontal = chart.inverted ? !isXAxis : isXAxis, - selectionMin = axis.translate( - isHorizontal ? - selectionLeft : - chart.plotHeight - selectionTop - selectionBox.height, - true, - 0, - 0, - 1 - ), - selectionMax = axis.translate( - (isHorizontal ? - selectionLeft + selectionBox.width : - chart.plotHeight - selectionTop) - - 2 * axis.minPixelPadding, // #875 - true, - 0, - 0, - 1 - ); - - if (!isNaN(selectionMin) && !isNaN(selectionMax)) { // #859 - selectionData[isXAxis ? 'xAxis' : 'yAxis'].push({ - axis: axis, - min: mathMin(selectionMin, selectionMax), // for reversed axes, - max: mathMax(selectionMin, selectionMax) - }); - runZoom = true; - } - } - }); - if (runZoom) { - fireEvent(chart, 'selection', selectionData, function (args) { chart.zoom(args); }); - } - + // Scale each series + each(chart.series, function (series) { + if (series.xAxis && series.xAxis.zoomEnabled) { + series.group.attr(attribs); + if (series.markerGroup) { + series.markerGroup.attr(attribs); + series.markerGroup.clip(clip ? chart.clipRect : null); } - mouseTracker.selectionMarker = mouseTracker.selectionMarker.destroy(); + if (series.dataLabelsGroup) { + series.dataLabelsGroup.attr(attribs); + } } + }); + + // Clip + chart.clipRect.attr(clip || chart.clipBox); + }, - if (chart) { // it may be destroyed on mouse up - #877 - css(container, { cursor: 'auto' }); - chart.cancelClick = hasDragged; // #370 - chart.mouseIsDown = hasDragged = false; - } + /** + * Run translation operations for each direction (horizontal and vertical) independently + */ + pinchTranslateDirection: function (horiz, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) { + var chart = this.chart, + xy = horiz ? 'x' : 'y', + XY = horiz ? 'X' : 'Y', + sChartXY = 'chart' + XY, + wh = horiz ? 'width' : 'height', + plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')], + selectionWH, + selectionXY, + clipXY, + scale = 1, + inverted = chart.inverted, + bounds = chart.bounds[horiz ? 'h' : 'v'], + singleTouch = pinchDown.length === 1, + touch0Start = pinchDown[0][sChartXY], + touch0Now = touches[0][sChartXY], + touch1Start = !singleTouch && pinchDown[1][sChartXY], + touch1Now = !singleTouch && touches[1][sChartXY], + outOfBounds, + transformScale, + scaleKey, + setScale = function () { + if (!singleTouch && mathAbs(touch0Start - touch1Start) > 20) { // Don't zoom if fingers are too close on this axis + scale = mathAbs(touch0Now - touch1Now) / mathAbs(touch0Start - touch1Start); + } + + clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start; + selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale; + }; - removeEvent(doc, hasTouch ? 'touchend' : 'mouseup', drop); - } + // Set the scale, first pass + setScale(); - /** - * Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea. - */ - mouseTracker.hideTooltipOnMouseMove = function (e) { + selectionXY = clipXY; // the clip position (x or y) is altered if out of bounds, the selection position is not - // Get e.pageX and e.pageY back in MooTools - e = washMouseEvent(e); + // Out of bounds + if (selectionXY < bounds.min) { + selectionXY = bounds.min; + outOfBounds = true; + } else if (selectionXY + selectionWH > bounds.max) { + selectionXY = bounds.max - selectionWH; + outOfBounds = true; + } + + // Is the chart dragged off its bounds, determined by dataMin and dataMax? + if (outOfBounds) { - // If we're outside, hide the tooltip - if (mouseTracker.chartPosition && chart.hoverSeries && chart.hoverSeries.isCartesian && - !chart.isInsidePlot(e.pageX - mouseTracker.chartPosition.left - chart.plotLeft, - e.pageY - mouseTracker.chartPosition.top - chart.plotTop)) { - mouseTracker.resetTracker(); + // Modify the touchNow position in order to create an elastic drag movement. This indicates + // to the user that the chart is responsive but can't be dragged further. + touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]); + if (!singleTouch) { + touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]); } - }; - /** - * When mouse leaves the container, hide the tooltip. - */ - mouseTracker.hideTooltipOnMouseLeave = function () { - mouseTracker.resetTracker(); - mouseTracker.chartPosition = null; // also reset the chart position, used in #149 fix - }; + // Set the scale, second pass to adapt to the modified touchNow positions + setScale(); + } else { + lastValidTouch[xy] = [touch0Now, touch1Now]; + } - /* - * Record the starting position of a dragoperation - */ - container.onmousedown = function (e) { - e = mouseTracker.normalizeMouseEvent(e); + + // Set geometry for clipping, selection and transformation + if (!inverted) { // TODO: implement clipping for inverted charts + clip[xy] = clipXY - plotLeftTop; + clip[wh] = selectionWH; + } + scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY; + transformScale = inverted ? 1 / scale : scale; - // issue #295, dragging not always working in Firefox - if (!hasTouch && e.preventDefault) { + selectionMarker[wh] = selectionWH; + selectionMarker[xy] = selectionXY; + transform[scaleKey] = scale; + transform['translate' + XY] = (transformScale * plotLeftTop) + (touch0Now - (transformScale * touch0Start)); + }, + + /** + * Handle touch events with two touches + */ + pinch: function (e) { + + var self = this, + chart = self.chart, + pinchDown = self.pinchDown, + followTouchMove = chart.tooltip && chart.tooltip.options.followTouchMove, + touches = e.touches, + touchesLength = touches.length, + lastValidTouch = self.lastValidTouch, + zoomHor = self.zoomHor || self.pinchHor, + zoomVert = self.zoomVert || self.pinchVert, + hasZoom = zoomHor || zoomVert, + selectionMarker = self.selectionMarker, + transform = {}, + clip = {}; + + // On touch devices, only proceed to trigger click if a handler is defined + if (e.type === 'touchstart') { + if (followTouchMove || hasZoom) { e.preventDefault(); } + } + + // Normalize each touch + map(touches, function (e) { + return self.normalize(e); + }); + + // Register the touch start position + if (e.type === 'touchstart') { + each(touches, function (e, i) { + pinchDown[i] = { chartX: e.chartX, chartY: e.chartY }; + }); + lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && pinchDown[1].chartX]; + lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && pinchDown[1].chartY]; - // record the start position - chart.mouseIsDown = true; - chart.cancelClick = false; - chart.mouseDownX = mouseTracker.mouseDownX = e.chartX; - mouseTracker.mouseDownY = e.chartY; + // Identify the data bounds in pixels + each(chart.axes, function (axis) { + if (axis.zoomEnabled) { + var bounds = chart.bounds[axis.horiz ? 'h' : 'v'], + minPixelPadding = axis.minPixelPadding, + min = axis.toPixels(axis.dataMin), + max = axis.toPixels(axis.dataMax), + absMin = mathMin(min, max), + absMax = mathMax(min, max); - addEvent(doc, hasTouch ? 'touchend' : 'mouseup', drop); - }; + // Store the bounds for use in the touchmove handler + bounds.min = mathMin(axis.pos, absMin - minPixelPadding); + bounds.max = mathMax(axis.pos + axis.len, absMax + minPixelPadding); + } + }); + + // Event type is touchmove, handle panning and pinching + } else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first + - // The mousemove, touchmove and touchstart event handler - var mouseMove = function (e) { - // let the system handle multitouch operations like two finger scroll - // and pinching - if (e && e.touches && e.touches.length > 1) { - return; + // Set the marker + if (!selectionMarker) { + self.selectionMarker = selectionMarker = extend({ + destroy: noop + }, chart.plotBox); } - // normalize - e = mouseTracker.normalizeMouseEvent(e); - if (!hasTouch) { // not for touch devices - e.returnValue = false; + + + if (zoomHor) { + self.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); } + if (zoomVert) { + self.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); + } - var chartX = e.chartX, - chartY = e.chartY, - isOutsidePlot = !chart.isInsidePlot(chartX - chart.plotLeft, chartY - chart.plotTop); + self.hasPinched = hasZoom; - // on touch devices, only trigger click if a handler is defined - if (hasTouch && e.type === 'touchstart') { - if (attr(e.target, 'isTracker')) { - if (!chart.runTrackerClick) { - e.preventDefault(); - } - } else if (!chart.runChartClick && !isOutsidePlot) { - e.preventDefault(); - } + // Scale and translate the groups to provide visual feedback during pinching + self.scaleGroups(transform, clip); + + // Optionally move the tooltip on touchmove + if (!hasZoom && followTouchMove && touchesLength === 1) { + this.runPointActions(self.normalize(e)); } + } + }, - // cancel on mouse outside - if (isOutsidePlot) { + /** + * Start a drag operation + */ + dragStart: function (e) { + var chart = this.chart; - /*if (!lastWasOutsidePlot) { - // reset the tracker - resetTracker(); - }*/ + // Record the start position + chart.mouseIsDown = e.type; + chart.cancelClick = false; + chart.mouseDownX = this.mouseDownX = e.chartX; + this.mouseDownY = e.chartY; + }, - // drop the selection if any and reset mouseIsDown and hasDragged - //drop(); - if (chartX < chart.plotLeft) { - chartX = chart.plotLeft; - } else if (chartX > chart.plotLeft + chart.plotWidth) { - chartX = chart.plotLeft + chart.plotWidth; - } + /** + * Perform a drag operation in response to a mousemove event while the mouse is down + */ + drag: function (e) { - if (chartY < chart.plotTop) { - chartY = chart.plotTop; - } else if (chartY > chart.plotTop + chart.plotHeight) { - chartY = chart.plotTop + chart.plotHeight; + var chart = this.chart, + chartOptions = chart.options.chart, + chartX = e.chartX, + chartY = e.chartY, + zoomHor = this.zoomHor, + zoomVert = this.zoomVert, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + clickedInside, + size, + mouseDownX = this.mouseDownX, + mouseDownY = this.mouseDownY; + + // If the mouse is outside the plot area, adjust to cooordinates + // inside to prevent the selection marker from going outside + if (chartX < plotLeft) { + chartX = plotLeft; + } else if (chartX > plotLeft + plotWidth) { + chartX = plotLeft + plotWidth; + } + + if (chartY < plotTop) { + chartY = plotTop; + } else if (chartY > plotTop + plotHeight) { + chartY = plotTop + plotHeight; + } + + // determine if the mouse has moved more than 10px + this.hasDragged = Math.sqrt( + Math.pow(mouseDownX - chartX, 2) + + Math.pow(mouseDownY - chartY, 2) + ); + if (this.hasDragged > 10) { + clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop); + + // make a selection + if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside) { + if (!this.selectionMarker) { + this.selectionMarker = chart.renderer.rect( + plotLeft, + plotTop, + zoomHor ? 1 : plotWidth, + zoomVert ? 1 : plotHeight, + 0 + ) + .attr({ + fill: chartOptions.selectionMarkerFill || 'rgba(69,114,167,0.25)', + zIndex: 7 + }) + .add(); } } - if (chart.mouseIsDown && e.type !== 'touchstart') { // make selection + // adjust the width of the selection marker + if (this.selectionMarker && zoomHor) { + size = chartX - mouseDownX; + this.selectionMarker.attr({ + width: mathAbs(size), + x: (size > 0 ? 0 : size) + mouseDownX + }); + } + // adjust the height of the selection marker + if (this.selectionMarker && zoomVert) { + size = chartY - mouseDownY; + this.selectionMarker.attr({ + height: mathAbs(size), + y: (size > 0 ? 0 : size) + mouseDownY + }); + } - // determine if the mouse has moved more than 10px - hasDragged = Math.sqrt( - Math.pow(mouseTracker.mouseDownX - chartX, 2) + - Math.pow(mouseTracker.mouseDownY - chartY, 2) - ); - if (hasDragged > 10) { - var clickedInside = chart.isInsidePlot(mouseTracker.mouseDownX - chart.plotLeft, mouseTracker.mouseDownY - chart.plotTop); + // panning + if (clickedInside && !this.selectionMarker && chartOptions.panning) { + chart.pan(chartX); + } + } + }, - // make a selection - if (chart.hasCartesianSeries && (mouseTracker.zoomX || mouseTracker.zoomY) && clickedInside) { - if (!mouseTracker.selectionMarker) { - mouseTracker.selectionMarker = chart.renderer.rect( - chart.plotLeft, - chart.plotTop, - zoomHor ? 1 : chart.plotWidth, - zoomVert ? 1 : chart.plotHeight, - 0 - ) - .attr({ - fill: mouseTracker.options.chart.selectionMarkerFill || 'rgba(69,114,167,0.25)', - zIndex: 7 - }) - .add(); - } - } + /** + * On mouse up or touch end across the entire document, drop the selection. + */ + drop: function (e) { + var chart = this.chart, + hasPinched = this.hasPinched; - // adjust the width of the selection marker - if (mouseTracker.selectionMarker && zoomHor) { - var xSize = chartX - mouseTracker.mouseDownX; - mouseTracker.selectionMarker.attr({ - width: mathAbs(xSize), - x: (xSize > 0 ? 0 : xSize) + mouseTracker.mouseDownX - }); - } - // adjust the height of the selection marker - if (mouseTracker.selectionMarker && zoomVert) { - var ySize = chartY - mouseTracker.mouseDownY; - mouseTracker.selectionMarker.attr({ - height: mathAbs(ySize), - y: (ySize > 0 ? 0 : ySize) + mouseTracker.mouseDownY - }); - } + if (this.selectionMarker) { + var selectionData = { + xAxis: [], + yAxis: [], + originalEvent: e.originalEvent || e + }, + selectionBox = this.selectionMarker, + selectionLeft = selectionBox.x, + selectionTop = selectionBox.y, + runZoom; + // a selection has been made + if (this.hasDragged || hasPinched) { - // panning - if (clickedInside && !mouseTracker.selectionMarker && mouseTracker.options.chart.panning) { - chart.pan(chartX); + // record each axis' min and max + each(chart.axes, function (axis) { + if (axis.zoomEnabled) { + var horiz = axis.horiz, + selectionMin = axis.toValue((horiz ? selectionLeft : selectionTop)), + selectionMax = axis.toValue((horiz ? selectionLeft + selectionBox.width : selectionTop + selectionBox.height)); + + if (!isNaN(selectionMin) && !isNaN(selectionMax)) { // #859 + selectionData[axis.xOrY + 'Axis'].push({ + axis: axis, + min: mathMin(selectionMin, selectionMax), // for reversed axes, + max: mathMax(selectionMin, selectionMax) + }); + runZoom = true; + } } + }); + if (runZoom) { + fireEvent(chart, 'selection', selectionData, function (args) { + chart.zoom(extend(args, hasPinched ? { animation: false } : null)); + }); } - } - - // Show the tooltip and run mouse over events (#977) - if (!isOutsidePlot) { - mouseTracker.onmousemove(e); } + this.selectionMarker = this.selectionMarker.destroy(); - lastWasOutsidePlot = isOutsidePlot; + // Reset scaling preview + if (hasPinched) { + this.scaleGroups({ + translateX: chart.plotLeft, + translateY: chart.plotTop, + scaleX: 1, + scaleY: 1 + }); + } + } - // when outside plot, allow touch-drag by returning true - return isOutsidePlot || !chart.hasCartesianSeries; - }; + // Reset all + if (chart) { // it may be destroyed on mouse up - #877 + css(chart.container, { cursor: chart._cursor }); + chart.cancelClick = this.hasDragged > 10; // #370 + chart.mouseIsDown = this.hasDragged = this.hasPinched = false; + this.pinchDown = []; + } + }, - /* - * When the mouse enters the container, run mouseMove - */ - container.onmousemove = mouseMove; + onContainerMouseDown: function (e) { - /* - * When the mouse leaves the container, hide the tracking (tooltip). - */ - addEvent(container, 'mouseleave', mouseTracker.hideTooltipOnMouseLeave); + e = this.normalize(e); - // issue #149 workaround - // The mouseleave event above does not always fire. Whenever the mouse is moving - // outside the plotarea, hide the tooltip - addEvent(doc, 'mousemove', mouseTracker.hideTooltipOnMouseMove); + // issue #295, dragging not always working in Firefox + if (e.preventDefault) { + e.preventDefault(); + } + + this.dragStart(e); + }, - container.ontouchstart = function (e) { - // For touch devices, use touchmove to zoom - if (mouseTracker.zoomX || mouseTracker.zoomY) { - container.onmousedown(e); - } - // Show tooltip and prevent the lower mouse pseudo event - mouseMove(e); - }; + - /* - * Allow dragging the finger over the chart to read the values on touch - * devices - */ - container.ontouchmove = mouseMove; + onDocumentMouseUp: function (e) { + this.drop(e); + }, - /* - * Allow dragging the finger over the chart to read the values on touch - * devices - */ - container.ontouchend = function () { - if (hasDragged) { - mouseTracker.resetTracker(); - } - }; + /** + * Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea. + * Issue #149 workaround. The mouseleave event does not always fire. + */ + onDocumentMouseMove: function (e) { + var chart = this.chart, + chartPosition = this.chartPosition, + hoverSeries = chart.hoverSeries; + // Get e.pageX and e.pageY back in MooTools + e = washMouseEvent(e); - // MooTools 1.2.3 doesn't fire this in IE when using addEvent - container.onclick = function (e) { - var hoverPoint = chart.hoverPoint, - plotX, - plotY; - e = mouseTracker.normalizeMouseEvent(e); + // If we're outside, hide the tooltip + if (chartPosition && hoverSeries && hoverSeries.isCartesian && + !chart.isInsidePlot(e.pageX - chartPosition.left - chart.plotLeft, + e.pageY - chartPosition.top - chart.plotTop)) { + this.reset(); + } + }, - e.cancelBubble = true; // IE specific + /** + * When mouse leaves the container, hide the tooltip. + */ + onContainerMouseLeave: function () { + this.reset(); + this.chartPosition = null; // also reset the chart position, used in #149 fix + }, + // The mousemove, touchmove and touchstart event handler + onContainerMouseMove: function (e) { - if (!chart.cancelClick) { - // Detect clicks on trackers or tracker groups, #783 - if (hoverPoint && (attr(e.target, 'isTracker') || attr(e.target.parentNode, 'isTracker'))) { - plotX = hoverPoint.plotX; - plotY = hoverPoint.plotY; + var chart = this.chart; - // add page position info - extend(hoverPoint, { - pageX: mouseTracker.chartPosition.left + chart.plotLeft + - (chart.inverted ? chart.plotWidth - plotY : plotX), - pageY: mouseTracker.chartPosition.top + chart.plotTop + - (chart.inverted ? chart.plotHeight - plotX : plotY) - }); + // normalize + e = this.normalize(e); - // the series click event - fireEvent(hoverPoint.series, 'click', extend(e, { - point: hoverPoint - })); + // #295 + e.returnValue = false; + + + if (chart.mouseIsDown === 'mousedown') { + this.drag(e); + } + + // Show the tooltip and run mouse over events (#977) + if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop) && !chart.openMenu) { + this.runPointActions(e); + } + }, - // the point click event + /** + * Utility to detect whether an element has, or has a parent with, a specific + * class name. Used on detection of tracker objects and on deciding whether + * hovering the tooltip should cause the active series to mouse out. + */ + inClass: function (element, className) { + var elemClassName; + while (element) { + elemClassName = attr(element, 'class'); + if (elemClassName) { + if (elemClassName.indexOf(className) !== -1) { + return true; + } else if (elemClassName.indexOf(PREFIX + 'container') !== -1) { + return false; + } + } + element = element.parentNode; + } + }, + + onTrackerMouseOut: function (e) { + var series = this.chart.hoverSeries; + if (series && !series.options.stickyTracking && !this.inClass(e.toElement || e.relatedTarget, PREFIX + 'tooltip')) { + series.onMouseOut(); + } + }, + + onContainerClick: function (e) { + var chart = this.chart, + hoverPoint = chart.hoverPoint, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + inverted = chart.inverted, + chartPosition, + plotX, + plotY; + + e = this.normalize(e); + e.cancelBubble = true; // IE specific + + if (!chart.cancelClick) { + + // On tracker click, fire the series and point events. #783, #1583 + if (hoverPoint && this.inClass(e.target, PREFIX + 'tracker')) { + chartPosition = this.chartPosition; + plotX = hoverPoint.plotX; + plotY = hoverPoint.plotY; + + // add page position info + extend(hoverPoint, { + pageX: chartPosition.left + plotLeft + + (inverted ? chart.plotWidth - plotY : plotX), + pageY: chartPosition.top + plotTop + + (inverted ? chart.plotHeight - plotX : plotY) + }); + + // the series click event + fireEvent(hoverPoint.series, 'click', extend(e, { + point: hoverPoint + })); + + // the point click event + if (chart.hoverPoint) { // it may be destroyed (#1844) hoverPoint.firePointEvent('click', e); + } - } else { - extend(e, mouseTracker.getMouseCoordinates(e)); + // When clicking outside a tracker, fire a chart event + } else { + extend(e, this.getCoordinates(e)); - // fire a click event in the chart - if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { - fireEvent(chart, 'click', e); - } + // fire a click event in the chart + if (chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) { + fireEvent(chart, 'click', e); } + } + } + }, + + onContainerTouchStart: function (e) { + var chart = this.chart; + + if (e.touches.length === 1) { + + e = this.normalize(e); + + if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { + + // Prevent the click pseudo event from firing unless it is set in the options + /*if (!chart.runChartClick) { + e.preventDefault(); + }*/ + + // Run mouse events and display tooltip etc + this.runPointActions(e); + + this.pinch(e); } - }; + } else if (e.touches.length === 2) { + this.pinch(e); + } }, + onContainerTouchMove: function (e) { + if (e.touches.length === 1 || e.touches.length === 2) { + this.pinch(e); + } + }, + + onDocumentTouchEnd: function (e) { + this.drop(e); + }, + /** - * Destroys the MouseTracker object and disconnects DOM events. + * Set the JS DOM events on the container and document. This method should contain + * a one-to-one assignment between methods and their handlers. Any advanced logic should + * be moved to the handler reflecting the event's name. */ - destroy: function () { - var mouseTracker = this, - chart = mouseTracker.chart, - container = chart.container; + setDOMEvents: function () { - // Destroy the tracker group element - if (chart.trackerGroup) { - chart.trackerGroup = chart.trackerGroup.destroy(); + var pointer = this, + container = pointer.chart.container, + events; + + this._events = events = [ + [container, 'onmousedown', 'onContainerMouseDown'], + [container, 'onmousemove', 'onContainerMouseMove'], + [container, 'onclick', 'onContainerClick'], + [container, 'mouseleave', 'onContainerMouseLeave'], + [doc, 'mousemove', 'onDocumentMouseMove'], + [doc, 'mouseup', 'onDocumentMouseUp'] + ]; + + if (hasTouch) { + events.push( + [container, 'ontouchstart', 'onContainerTouchStart'], + [container, 'ontouchmove', 'onContainerTouchMove'], + [doc, 'touchend', 'onDocumentTouchEnd'] + ); } - removeEvent(container, 'mouseleave', mouseTracker.hideTooltipOnMouseLeave); - removeEvent(doc, 'mousemove', mouseTracker.hideTooltipOnMouseMove); - container.onclick = container.onmousedown = container.onmousemove = container.ontouchstart = container.ontouchend = container.ontouchmove = null; + each(events, function (eventConfig) { - // memory and CPU leak - clearInterval(this.tooltipTimeout); + // First, create the callback function that in turn calls the method on Pointer + pointer['_' + eventConfig[2]] = function (e) { + pointer[eventConfig[2]](e); + }; + + // Now attach the function, either as a direct property or through addEvent + if (eventConfig[1].indexOf('on') === 0) { + eventConfig[0][eventConfig[1]] = pointer['_' + eventConfig[2]]; + } else { + addEvent(eventConfig[0], eventConfig[1], pointer['_' + eventConfig[2]]); + } + }); + + }, - // Run MouseTracker - init: function (chart, options) { - if (!chart.trackerGroup) { - chart.trackerGroup = chart.renderer.g('tracker') - .attr({ zIndex: 9 }) - .add(); - } + /** + * Destroys the Pointer object and disconnects DOM events. + */ + destroy: function () { + var pointer = this; - if (options.enabled) { - chart.tooltip = new Tooltip(chart, options); - } + // Release all DOM events + each(pointer._events, function (eventConfig) { + if (eventConfig[1].indexOf('on') === 0) { + eventConfig[0][eventConfig[1]] = null; // delete breaks oldIE + } else { + removeEvent(eventConfig[0], eventConfig[1], pointer['_' + eventConfig[2]]); + } + }); + delete pointer._events; - this.setDOMEvents(); + // memory and CPU leak + clearInterval(pointer.tooltipTimeout); } }; /** * The overview of the chart's series */ -function Legend(chart) { - - this.init(chart); +function Legend(chart, options) { + this.init(chart, options); } Legend.prototype = { /** * Initialize the legend */ - init: function (chart) { + init: function (chart, options) { + var legend = this, - options = legend.options = chart.options.legend; + itemStyle = options.itemStyle, + padding = pick(options.padding, 8), + itemMarginTop = options.itemMarginTop || 0; + this.options = options; + if (!options.enabled) { return; } - var //style = options.style || {}, // deprecated - itemStyle = options.itemStyle, - padding = pick(options.padding, 8), - itemMarginTop = options.itemMarginTop || 0; - legend.baseline = pInt(itemStyle.fontSize) + 3 + itemMarginTop; // used in Series prototype legend.itemStyle = itemStyle; legend.itemHiddenStyle = merge(itemStyle, options.itemHiddenStyle); legend.itemMarginTop = itemMarginTop; legend.padding = padding; legend.initialItemX = padding; legend.initialItemY = padding - 5; // 5 is the number of pixels above the text legend.maxItemWidth = 0; legend.chart = chart; - //legend.allItems = UNDEFINED; - //legend.legendWidth = UNDEFINED; - //legend.legendHeight = UNDEFINED; - //legend.offsetWidth = UNDEFINED; legend.itemHeight = 0; legend.lastLineHeight = 0; - //legend.itemX = UNDEFINED; - //legend.itemY = UNDEFINED; - //legend.lastItemY = UNDEFINED; - - // Elements - //legend.group = UNDEFINED; - //legend.box = UNDEFINED; - // run legend + // Render it legend.render(); // move checkboxes - addEvent(legend.chart, 'endResize', function () { legend.positionCheckboxes(); }); + addEvent(legend.chart, 'endResize', function () { + legend.positionCheckboxes(); + }); -/* // expose - return { - colorizeItem: colorizeItem, - destroyItem: destroyItem, - render: render, - destroy: destroy, - getLegendWidth: getLegendWidth, - getLegendHeight: getLegendHeight - };*/ }, /** * Set the colors for the legend item * @param {Object} item A Series or Point instance @@ -21790,11 +22525,11 @@ key, val; if (legendItem) { - legendItem.css({ fill: textColor }); + legendItem.css({ fill: textColor, color: textColor }); // color for #1553, oldIE } if (legendLine) { legendLine.attr({ stroke: symbolColor }); } @@ -21879,23 +22614,53 @@ }, /** * Position the checkboxes after the width is determined */ - positionCheckboxes: function () { - var legend = this; + positionCheckboxes: function (scrollOffset) { + var alignAttr = this.group.alignAttr, + translateY, + clipHeight = this.clipHeight || this.legendHeight; - each(legend.allItems, function (item) { - var checkbox = item.checkbox, - alignAttr = legend.group.alignAttr; - if (checkbox) { - css(checkbox, { - left: (alignAttr.translateX + item.legendItemWidth + checkbox.x - 20) + PX, - top: (alignAttr.translateY + checkbox.y + 3) + PX - }); + if (alignAttr) { + translateY = alignAttr.translateY; + each(this.allItems, function (item) { + var checkbox = item.checkbox, + top; + + if (checkbox) { + top = (translateY + checkbox.y + (scrollOffset || 0) + 3); + css(checkbox, { + left: (alignAttr.translateX + item.legendItemWidth + checkbox.x - 20) + PX, + top: top + PX, + display: top > translateY - 6 && top < translateY + clipHeight - 6 ? '' : NONE + }); + } + }); + } + }, + + /** + * Render the legend title on top of the legend + */ + renderTitle: function () { + var options = this.options, + padding = this.padding, + titleOptions = options.title, + titleHeight = 0; + + if (titleOptions.text) { + if (!this.title) { + this.title = this.chart.renderer.label(titleOptions.text, padding - 3, padding - 4, null, null, null, null, null, 'legend-title') + .attr({ zIndex: 1 }) + .css(titleOptions.style) + .add(this.group); } - }); + titleHeight = this.title.getBBox().height; + this.contentGroup.attr({ translateY: titleHeight }); + } + this.titleHeight = titleHeight; }, /** * Render a single specific legend item * @param {Object} item A series or point @@ -21920,11 +22685,12 @@ bBox, itemWidth, li = item.legendItem, series = item.series || item, itemOptions = series.options, - showCheckbox = itemOptions.showCheckbox; + showCheckbox = itemOptions.showCheckbox, + useHTML = options.useHTML; if (!li) { // generate it once, later move it // Generate the group box // A group to hold the symbol and text. Text is to be appended in Legend class. @@ -21935,24 +22701,24 @@ // Draw the legend symbol inside the group box series.drawLegendSymbol(legend, item); // Generate the list item text and add it to the group item.legendItem = li = renderer.text( - options.labelFormatter.call(item), + options.labelFormat ? format(options.labelFormat, item) : options.labelFormatter.call(item), ltr ? symbolWidth + symbolPadding : -symbolPadding, legend.baseline, - options.useHTML + useHTML ) .css(merge(item.visible ? itemStyle : itemHiddenStyle)) // merge to prevent modifying original (#1021) .attr({ align: ltr ? 'left' : 'right', zIndex: 2 }) .add(item.legendGroup); - // Set the events on the item group - item.legendGroup.on('mouseover', function () { + // Set the events on the item group, or in case of useHTML, the item itself (#1249) + (useHTML ? li : item.legendGroup).on('mouseover', function () { item.setState(HOVER_STATE); li.css(legend.options.itemHoverStyle); }) .on('mouseout', function () { li.css(item.visible ? itemStyle : itemHiddenStyle); @@ -22073,30 +22839,27 @@ legend.offsetWidth = 0; legend.lastItemY = 0; if (!legendGroup) { legend.group = legendGroup = renderer.g('legend') - // #414, #759. Trackers will be drawn above the legend, but we have - // to sacrifice that because tooltips need to be above the legend - // and trackers above tooltips .attr({ zIndex: 7 }) .add(); legend.contentGroup = renderer.g() .attr({ zIndex: 1 }) // above background .add(legendGroup); legend.scrollGroup = renderer.g() .add(legend.contentGroup); - legend.clipRect = renderer.clipRect(0, 0, 9999, chart.chartHeight); - legend.contentGroup.clip(legend.clipRect); } + + legend.renderTitle(); // add each series or point allItems = []; each(chart.series, function (serie) { var seriesOptions = serie.options; - if (!seriesOptions.showInLegend) { + if (!seriesOptions.showInLegend || defined(seriesOptions.linkedTo)) { return; } // use points or series for the legend item depending on legendType allItems = allItems.concat( @@ -22125,11 +22888,11 @@ legend.renderItem(item); }); // Draw the border legendWidth = options.width || legend.offsetWidth; - legendHeight = legend.lastItemY + legend.lastLineHeight; + legendHeight = legend.lastItemY + legend.lastLineHeight + legend.titleHeight; legendHeight = legend.handleOverflow(legendHeight); if (legendBorderWidth || legendBackgroundColor) { @@ -22187,11 +22950,11 @@ if (display) { legendGroup.align(extend({ width: legendWidth, height: legendHeight - }, options), true, chart.spacingBox); + }, options), true, 'spacingBox'); } if (!chart.isResizing) { this.positionCheckboxes(); } @@ -22225,17 +22988,22 @@ if (maxHeight) { spaceHeight = mathMin(spaceHeight, maxHeight); } // Reset the legend height and adjust the clipping rectangle - if (legendHeight > spaceHeight) { - - this.clipHeight = clipHeight = spaceHeight - 20; + if (legendHeight > spaceHeight && !options.useHTML) { + + this.clipHeight = clipHeight = spaceHeight - 20 - this.titleHeight; this.pageCount = pageCount = mathCeil(legendHeight / clipHeight); this.currentPage = pick(this.currentPage, 1); this.fullHeight = legendHeight; + // Only apply clipping if needed. Clipping causes blurred legend in PDF export (#1787) + if (!clipRect) { + clipRect = legend.clipRect = renderer.clipRect(0, 0, 9999, 0); + legend.contentGroup.clip(clipRect); + } clipRect.attr({ height: clipHeight }); // Add navigation elements @@ -22267,10 +23035,11 @@ }); nav.hide(); this.scrollGroup.attr({ translateY: 1 }); + this.clipHeight = 0; // #1379 } return legendHeight; }, @@ -22285,11 +23054,12 @@ clipHeight = this.clipHeight, navOptions = this.options.navigation, activeColor = navOptions.activeColor, inactiveColor = navOptions.inactiveColor, pager = this.pager, - padding = this.padding; + padding = this.padding, + scrollOffset; // When resizing while looking at the last page if (currentPage > pageCount) { currentPage = pageCount; } @@ -22300,11 +23070,11 @@ setAnimation(animation, this.chart); } this.nav.attr({ translateX: padding, - translateY: clipHeight + 7, + translateY: clipHeight + 7 + this.titleHeight, visibility: VISIBLE }); this.up.attr({ fill: currentPage === 1 ? inactiveColor : activeColor }) @@ -22320,106 +23090,153 @@ }) .css({ cursor: currentPage === pageCount ? 'default' : 'pointer' }); + scrollOffset = -mathMin(clipHeight * (currentPage - 1), this.fullHeight - clipHeight + padding) + 1; this.scrollGroup.animate({ - translateY: -mathMin(clipHeight * (currentPage - 1), this.fullHeight - clipHeight + padding) + 1 + translateY: scrollOffset }); pager.attr({ text: currentPage + '/' + pageCount }); this.currentPage = currentPage; + this.positionCheckboxes(scrollOffset); } } }; - /** * The chart class * @param {Object} options * @param {Function} callback Function to run when the chart has loaded */ -function Chart(userOptions, callback) { - // Handle regular options - var options, - seriesOptions = userOptions.series; // skip merging data points to increase performance - userOptions.series = null; - options = merge(defaultOptions, userOptions); // do the merge - options.series = userOptions.series = seriesOptions; // set back the series data +function Chart() { + this.init.apply(this, arguments); +} - var optionsChart = options.chart, - optionsMargin = optionsChart.margin, - margin = isObject(optionsMargin) ? - optionsMargin : - [optionsMargin, optionsMargin, optionsMargin, optionsMargin]; +Chart.prototype = { - this.optionsMarginTop = pick(optionsChart.marginTop, margin[0]); - this.optionsMarginRight = pick(optionsChart.marginRight, margin[1]); - this.optionsMarginBottom = pick(optionsChart.marginBottom, margin[2]); - this.optionsMarginLeft = pick(optionsChart.marginLeft, margin[3]); + /** + * Initialize the chart + */ + init: function (userOptions, callback) { - var chartEvents = optionsChart.events; + // Handle regular options + var options, + seriesOptions = userOptions.series; // skip merging data points to increase performance - this.runChartClick = chartEvents && !!chartEvents.click; - this.callback = callback; - this.isResizing = 0; - this.options = options; - //chartTitleOptions = UNDEFINED; - //chartSubtitleOptions = UNDEFINED; + userOptions.series = null; + options = merge(defaultOptions, userOptions); // do the merge + options.series = userOptions.series = seriesOptions; // set back the series data - this.axes = []; - this.series = []; - this.hasCartesianSeries = optionsChart.showAxes; - //this.axisOffset = UNDEFINED; - //this.maxTicks = UNDEFINED; // handle the greatest amount of ticks on grouped axes - //this.inverted = UNDEFINED; - //this.loadingShown = UNDEFINED; - //this.container = UNDEFINED; - //this.chartWidth = UNDEFINED; - //this.chartHeight = UNDEFINED; - //this.marginRight = UNDEFINED; - //this.marginBottom = UNDEFINED; - //this.containerWidth = UNDEFINED; - //this.containerHeight = UNDEFINED; - //this.oldChartWidth = UNDEFINED; - //this.oldChartHeight = UNDEFINED; + var optionsChart = options.chart, + optionsMargin = optionsChart.margin, + margin = isObject(optionsMargin) ? + optionsMargin : + [optionsMargin, optionsMargin, optionsMargin, optionsMargin]; - //this.renderTo = UNDEFINED; - //this.renderToClone = UNDEFINED; - //this.tracker = UNDEFINED; + this.optionsMarginTop = pick(optionsChart.marginTop, margin[0]); + this.optionsMarginRight = pick(optionsChart.marginRight, margin[1]); + this.optionsMarginBottom = pick(optionsChart.marginBottom, margin[2]); + this.optionsMarginLeft = pick(optionsChart.marginLeft, margin[3]); - //this.spacingBox = UNDEFINED + var chartEvents = optionsChart.events; - //this.legend = UNDEFINED; + this.runChartClick = chartEvents && !!chartEvents.click; + this.bounds = { h: {}, v: {} }; // Pixel data bounds for touch zoom - // Elements - //this.chartBackground = UNDEFINED; - //this.plotBackground = UNDEFINED; - //this.plotBGImage = UNDEFINED; - //this.plotBorder = UNDEFINED; - //this.loadingDiv = UNDEFINED; - //this.loadingSpan = UNDEFINED; + this.callback = callback; + this.isResizing = 0; + this.options = options; + //chartTitleOptions = UNDEFINED; + //chartSubtitleOptions = UNDEFINED; - this.init(chartEvents); -} + this.axes = []; + this.series = []; + this.hasCartesianSeries = optionsChart.showAxes; + //this.axisOffset = UNDEFINED; + //this.maxTicks = UNDEFINED; // handle the greatest amount of ticks on grouped axes + //this.inverted = UNDEFINED; + //this.loadingShown = UNDEFINED; + //this.container = UNDEFINED; + //this.chartWidth = UNDEFINED; + //this.chartHeight = UNDEFINED; + //this.marginRight = UNDEFINED; + //this.marginBottom = UNDEFINED; + //this.containerWidth = UNDEFINED; + //this.containerHeight = UNDEFINED; + //this.oldChartWidth = UNDEFINED; + //this.oldChartHeight = UNDEFINED; -Chart.prototype = { + //this.renderTo = UNDEFINED; + //this.renderToClone = UNDEFINED; + //this.spacingBox = UNDEFINED + + //this.legend = UNDEFINED; + + // Elements + //this.chartBackground = UNDEFINED; + //this.plotBackground = UNDEFINED; + //this.plotBGImage = UNDEFINED; + //this.plotBorder = UNDEFINED; + //this.loadingDiv = UNDEFINED; + //this.loadingSpan = UNDEFINED; + + var chart = this, + eventType; + + // Add the chart to the global lookup + chart.index = charts.length; + charts.push(chart); + + // Set up auto resize + if (optionsChart.reflow !== false) { + addEvent(chart, 'load', function () { + chart.initReflow(); + }); + } + + // Chart event handlers + if (chartEvents) { + for (eventType in chartEvents) { + addEvent(chart, eventType, chartEvents[eventType]); + } + } + + chart.xAxis = []; + chart.yAxis = []; + + // Expose methods and variables + chart.animation = useCanVG ? false : pick(optionsChart.animation, true); + chart.pointCount = 0; + chart.counters = new ChartCounters(); + + chart.firstRender(); + }, + /** * Initialize an individual series, called internally before render time */ initSeries: function (options) { var chart = this, optionsChart = chart.options.chart, type = options.type || optionsChart.type || optionsChart.defaultSeriesType, - series = new seriesTypes[type](); + series, + constr = seriesTypes[type]; + // No such series type + if (!constr) { + error(17, true); + } + + series = new constr(); series.init(this, options); return series; }, /** @@ -22435,27 +23252,51 @@ addSeries: function (options, redraw, animation) { var series, chart = this; if (options) { - setAnimation(animation, chart); redraw = pick(redraw, true); // defaults to true fireEvent(chart, 'addSeries', { options: options }, function () { series = chart.initSeries(options); chart.isDirtyLegend = true; // the series array is out of sync with the display if (redraw) { - chart.redraw(); + chart.redraw(animation); } }); } return series; }, /** + * Add an axis to the chart + * @param {Object} options The axis option + * @param {Boolean} isX Whether it is an X axis or a value axis + */ + addAxis: function (options, isX, redraw, animation) { + var key = isX ? 'xAxis' : 'yAxis', + chartOptions = this.options, + axis; + + /*jslint unused: false*/ + axis = new Axis(this, merge(options, { + index: this[key].length + })); + /*jslint unused: true*/ + + // Push the new axis options to the chart options + chartOptions[key] = splat(chartOptions[key] || {}); + chartOptions[key].push(options); + + if (pick(redraw, true)) { + this.redraw(animation); + } + }, + + /** * Check whether a given point is within the plot area * * @param {Number} plotX Pixel x relative to the plot area * @param {Number} plotY Pixel y relative to the plot area * @param {Boolean} inverted Whether the chart is inverted @@ -22490,11 +23331,11 @@ */ redraw: function (animation) { var chart = this, axes = chart.axes, series = chart.series, - tracker = chart.tracker, + pointer = chart.pointer, legend = chart.legend, redrawLegend = chart.isDirtyLegend, hasStackedSeries, isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed? seriesLength = series.length, @@ -22577,29 +23418,28 @@ } }); } - // the plot areas size has changed if (isDirtyBox) { chart.drawChartBox(); } + // redraw affected series each(series, function (serie) { if (serie.isDirty && serie.visible && (!serie.isCartesian || serie.xAxis)) { // issue #153 serie.redraw(); } }); - // move tooltip or reset - if (tracker && tracker.resetTracker) { - tracker.resetTracker(true); + if (pointer && pointer.reset) { + pointer.reset(true); } // redraw if canvas renderer.draw(); @@ -22632,14 +23472,10 @@ // create the layer at the first call if (!loadingDiv) { chart.loadingDiv = loadingDiv = createElement(DIV, { className: PREFIX + 'loading' }, extend(loadingOptions.style, { - left: chart.plotLeft + PX, - top: chart.plotTop + PX, - width: chart.plotWidth + PX, - height: chart.plotHeight + PX, zIndex: 10, display: NONE }), chart.container); chart.loadingSpan = createElement( @@ -22654,11 +23490,18 @@ // update text chart.loadingSpan.innerHTML = str || options.lang.loading; // show it if (!chart.loadingShown) { - css(loadingDiv, { opacity: 0, display: '' }); + css(loadingDiv, { + opacity: 0, + display: '', + left: chart.plotLeft + PX, + top: chart.plotTop + PX, + width: chart.plotWidth + PX, + height: chart.plotHeight + PX + }); animate(loadingDiv, { opacity: loadingOptions.style.opacity }, { duration: loadingOptions.showDuration || 0 }); @@ -22728,25 +23571,22 @@ /** * Create the Axis instances based on the config options */ getAxes: function () { var chart = this, - options = this.options; - - var xAxisOptions = options.xAxis || {}, - yAxisOptions = options.yAxis || {}, + options = this.options, + xAxisOptions = options.xAxis = splat(options.xAxis || {}), + yAxisOptions = options.yAxis = splat(options.yAxis || {}), optionsArray, 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 @@ -22764,11 +23604,11 @@ * Get the currently selected points from all series */ getSelectedPoints: function () { var points = []; each(this.series, function (serie) { - points = points.concat(grep(serie.points, function (point) { + points = points.concat(grep(serie.points || [], function (point) { return point.selected; })); }); return points; }, @@ -22797,75 +23637,80 @@ .attr({ align: btnOptions.position.align, title: lang.resetZoomTitle }) .add() - .align(btnOptions.position, false, chart[alignTo]); - this.resetZoomButton.alignTo = alignTo; + .align(btnOptions.position, false, alignTo); }, /** * Zoom out to 1:1 */ zoomOut: function () { - var chart = this, - resetZoomButton = chart.resetZoomButton; - - fireEvent(chart, 'selection', { resetSelection: true }, function () { chart.zoom(); }); - if (resetZoomButton) { - chart.resetZoomButton = resetZoomButton.destroy(); - } + var chart = this; + fireEvent(chart, 'selection', { resetSelection: true }, function () { + chart.zoom(); + }); }, /** * Zoom into a given portion of the chart given by axis coordinates * @param {Object} event */ zoom: function (event) { var chart = this, - hasZoomed; + hasZoomed, + pointer = chart.pointer, + displayButton = false, + resetZoomButton; - // if zoom is called with no arguments, reset the axes + // If zoom is called with no arguments, reset the axes if (!event || event.resetSelection) { each(chart.axes, function (axis) { hasZoomed = axis.zoom(); }); } else { // else, zoom in on all axes each(event.xAxis.concat(event.yAxis), function (axisData) { - var axis = axisData.axis; + var axis = axisData.axis, + isXAxis = axis.isXAxis; // don't zoom more than minRange - if (chart.tracker[axis.isXAxis ? 'zoomX' : 'zoomY']) { + if (pointer[isXAxis ? 'zoomX' : 'zoomY'] || pointer[isXAxis ? 'pinchX' : 'pinchY']) { hasZoomed = axis.zoom(axisData.min, axisData.max); + if (axis.displayBtn) { + displayButton = true; + } } }); } - // Show the Reset zoom button - if (!chart.resetZoomButton) { + // Show or hide the Reset zoom button + resetZoomButton = chart.resetZoomButton; + if (displayButton && !resetZoomButton) { chart.showResetZoom(); + } else if (!displayButton && isObject(resetZoomButton)) { + chart.resetZoomButton = resetZoomButton.destroy(); } // Redraw if (hasZoomed) { chart.redraw( - pick(chart.options.chart.animation, chart.pointCount < 100) // animation + pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation ); } }, /** * Pan the chart by dragging the mouse across the pane. This function is called * on mouse move, and the distance to pan is computed from chartX compared to * the first chartX position in the dragging operation. */ pan: function (chartX) { - var chart = this; - - var xAxis = chart.xAxis[0], + var chart = this, + xAxis = chart.xAxis[0], mouseDownX = chart.mouseDownX, halfPointRange = xAxis.pointRange / 2, extremes = xAxis.getExtremes(), newMin = xAxis.translate(mouseDownX - chartX, true) + halfPointRange, newMax = xAxis.translate(mouseDownX + chart.plotWidth - chartX, true) - halfPointRange, @@ -22897,12 +23742,12 @@ var chart = this, options = chart.options, chartTitleOptions, chartSubtitleOptions; - chart.chartTitleOptions = chartTitleOptions = merge(options.title, titleOptions); - chart.chartSubtitleOptions = chartSubtitleOptions = merge(options.subtitle, subtitleOptions); + chartTitleOptions = options.title = merge(options.title, titleOptions); + chartSubtitleOptions = options.subtitle = merge(options.subtitle, subtitleOptions); // add title and subtitle each([ ['title', titleOptions, chartTitleOptions], ['subtitle', subtitleOptions, chartSubtitleOptions] @@ -22928,11 +23773,11 @@ 'class': PREFIX + name, zIndex: chartTitleOptions.zIndex || 4 }) .css(chartTitleOptions.style) .add() - .align(chartTitleOptions, false, chart.spacingBox); + .align(chartTitleOptions, false, 'spacingBox'); } }); }, @@ -22946,14 +23791,14 @@ // get inner width and height from jQuery (#824) chart.containerWidth = adapterRun(renderTo, 'width'); chart.containerHeight = adapterRun(renderTo, 'height'); - chart.chartWidth = optionsChart.width || chart.containerWidth || 600; - chart.chartHeight = optionsChart.height || + chart.chartWidth = mathMax(0, optionsChart.width || chart.containerWidth || 600); // #1393, 1460 + chart.chartHeight = mathMax(0, pick(optionsChart.height, // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7: - (chart.containerHeight > 19 ? chart.containerHeight : 400); + chart.containerHeight > 19 ? chart.containerHeight : 400)); }, /** * Create a clone of the chart's renderTo div and place it outside the viewport to allow * size computation on chart.render and chart.redraw @@ -22997,10 +23842,12 @@ container, optionsChart = chart.options.chart, chartWidth, chartHeight, renderTo, + indexAttrName = 'data-highcharts-chart', + oldChartIndex, containerId; chart.renderTo = renderTo = optionsChart.renderTo; containerId = PREFIX + idCounter++; @@ -23010,10 +23857,19 @@ // Display an error if the renderTo is wrong if (!renderTo) { error(13, true); } + + // If the container already holds a chart, destroy it + oldChartIndex = pInt(attr(renderTo, indexAttrName)); + if (!isNaN(oldChartIndex) && charts[oldChartIndex]) { + charts[oldChartIndex].destroy(); + } + + // Make a reference to the chart from the div + attr(renderTo, indexAttrName, chart.index); // remove previous chart renderTo.innerHTML = ''; // If the container doesn't have an offsetWidth, it has or is a child of a node @@ -23040,15 +23896,19 @@ // content overflow in IE width: chartWidth + PX, height: chartHeight + PX, textAlign: 'left', lineHeight: 'normal', // #427 - zIndex: 0 // #1072 + zIndex: 0, // #1072 + '-webkit-tap-highlight-color': 'rgba(0,0,0,0)' }, optionsChart.style), chart.renderToClone || renderTo ); + // cache the cursor (#1650) + chart._cursor = container.style.cursor; + chart.renderer = optionsChart.forExport ? // force SVG, used for SVG export new SVGRenderer(container, chartWidth, chartHeight, true) : new Renderer(container, chartWidth, chartHeight); @@ -23075,12 +23935,12 @@ legend = chart.legend, optionsMarginTop = chart.optionsMarginTop, optionsMarginLeft = chart.optionsMarginLeft, optionsMarginRight = chart.optionsMarginRight, optionsMarginBottom = chart.optionsMarginBottom, - chartTitleOptions = chart.chartTitleOptions, - chartSubtitleOptions = chart.chartSubtitleOptions, + chartTitleOptions = chart.options.title, + chartSubtitleOptions = chart.options.subtitle, legendOptions = chart.options.legend, legendMargin = pick(legendOptions.margin, 10), legendX = legendOptions.x, legendY = legendOptions.y, align = legendOptions.align, @@ -23182,17 +24042,18 @@ height = optionsChart.height || adapterRun(renderTo, 'height'), target = e ? e.target : win; // #805 - MooTools doesn't supply e // Width and height checks for display:none. Target is doc in IE8 and Opera, // win in Firefox, Chrome and IE9. - if (width && height && (target === win || target === doc)) { + if (!chart.hasUserSize && width && height && (target === win || target === doc)) { if (width !== chart.containerWidth || height !== chart.containerHeight) { clearTimeout(reflowTimeout); chart.reflowTimeout = reflowTimeout = setTimeout(function () { if (chart.container) { // It may have been destroyed in the meantime (#1257) - chart.resize(width, height, false); + chart.setSize(width, height, false); + chart.hasUserSize = null; } }, 100); } chart.containerWidth = width; chart.containerHeight = height; @@ -23208,19 +24069,14 @@ * Resize the chart to a given width and height * @param {Number} width * @param {Number} height * @param {Object|Boolean} animation */ - // TODO: This method is called setSize in the api - resize: function (width, height, animation) { + setSize: function (width, height, animation) { var chart = this, chartWidth, chartHeight, - spacingBox, - resetZoomButton = chart.resetZoomButton, - chartTitle = chart.title, - chartSubtitle = chart.subtitle, fireEndResize; // Handle the isResizing counter chart.isResizing += 1; fireEndResize = function () { @@ -23235,26 +24091,24 @@ setAnimation(animation, chart); chart.oldChartHeight = chart.chartHeight; chart.oldChartWidth = chart.chartWidth; if (defined(width)) { - chart.chartWidth = chartWidth = mathRound(width); + chart.chartWidth = chartWidth = mathMax(0, mathRound(width)); + chart.hasUserSize = !!chartWidth; } if (defined(height)) { - chart.chartHeight = chartHeight = mathRound(height); + chart.chartHeight = chartHeight = mathMax(0, mathRound(height)); } css(chart.container, { width: chartWidth + PX, height: chartHeight + PX }); + chart.setChartSize(true); chart.renderer.setSize(chartWidth, chartHeight, animation); - // update axis lengths for more correct tick intervals: - chart.plotWidth = chartWidth - chart.plotLeft - chart.marginRight; - chart.plotHeight = chartHeight - chart.plotTop - chart.marginBottom; - // handle axes chart.maxTicks = null; each(chart.axes, function (axis) { axis.isDirty = true; axis.setScale(); @@ -23268,24 +24122,10 @@ chart.isDirtyLegend = true; // force legend redraw chart.isDirtyBox = true; // force redraw of plot and chart border chart.getMargins(); - // move titles - spacingBox = chart.spacingBox; - if (chartTitle) { - chartTitle.align(null, null, spacingBox); - } - if (chartSubtitle) { - chartSubtitle.align(null, null, spacingBox); - } - - // Move resize button (#1115) - if (resetZoomButton && resetZoomButton.align) { - resetZoomButton.align(null, null, chart[resetZoomButton.alignTo]); - } - chart.redraw(animation); chart.oldChartHeight = null; fireEvent(chart, 'resize'); @@ -23301,60 +24141,68 @@ /** * Set the public chart properties. This is done before and after the pre-render * to determine margin sizes */ - setChartSize: function () { + setChartSize: function (skipAxes) { var chart = this, inverted = chart.inverted, + renderer = chart.renderer, chartWidth = chart.chartWidth, chartHeight = chart.chartHeight, optionsChart = chart.options.chart, spacingTop = optionsChart.spacingTop, spacingRight = optionsChart.spacingRight, spacingBottom = optionsChart.spacingBottom, spacingLeft = optionsChart.spacingLeft, + clipOffset = chart.clipOffset, + clipX, + clipY, plotLeft, plotTop, plotWidth, plotHeight, plotBorderWidth; chart.plotLeft = plotLeft = mathRound(chart.plotLeft); chart.plotTop = plotTop = mathRound(chart.plotTop); - chart.plotWidth = plotWidth = mathRound(chartWidth - plotLeft - chart.marginRight); - chart.plotHeight = plotHeight = mathRound(chartHeight - plotTop - chart.marginBottom); + chart.plotWidth = plotWidth = mathMax(0, mathRound(chartWidth - plotLeft - chart.marginRight)); + chart.plotHeight = plotHeight = mathMax(0, mathRound(chartHeight - plotTop - chart.marginBottom)); chart.plotSizeX = inverted ? plotHeight : plotWidth; chart.plotSizeY = inverted ? plotWidth : plotHeight; chart.plotBorderWidth = plotBorderWidth = optionsChart.plotBorderWidth || 0; // Set boxes used for alignment - chart.spacingBox = { + chart.spacingBox = renderer.spacingBox = { x: spacingLeft, y: spacingTop, width: chartWidth - spacingLeft - spacingRight, height: chartHeight - spacingTop - spacingBottom }; - chart.plotBox = { + chart.plotBox = renderer.plotBox = { x: plotLeft, y: plotTop, width: plotWidth, height: plotHeight }; + clipX = mathCeil(mathMax(plotBorderWidth, clipOffset[3]) / 2); + clipY = mathCeil(mathMax(plotBorderWidth, clipOffset[0]) / 2); chart.clipBox = { - x: plotBorderWidth / 2, - y: plotBorderWidth / 2, - width: chart.plotSizeX - plotBorderWidth, - height: chart.plotSizeY - plotBorderWidth + x: clipX, + y: clipY, + width: mathFloor(chart.plotSizeX - mathMax(plotBorderWidth, clipOffset[1]) / 2 - clipX), + height: mathFloor(chart.plotSizeY - mathMax(plotBorderWidth, clipOffset[2]) / 2 - clipY) }; - each(chart.axes, function (axis) { - axis.setAxisSize(); - axis.setAxisTranslation(); - }); + if (!skipAxes) { + each(chart.axes, function (axis) { + axis.setAxisSize(); + axis.setAxisTranslation(); + }); + } }, /** * Initial margins before auto size margins are applied */ @@ -23369,10 +24217,11 @@ chart.plotTop = pick(chart.optionsMarginTop, spacingTop); chart.marginRight = pick(chart.optionsMarginRight, spacingRight); chart.marginBottom = pick(chart.optionsMarginBottom, spacingBottom); chart.plotLeft = pick(chart.optionsMarginLeft, spacingLeft); chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left + chart.clipOffset = [0, 0, 0, 0]; }, /** * Draw the borders and backgrounds for chart and plot area */ @@ -23538,11 +24387,11 @@ // Title chart.setTitle(); // Legend - chart.legend = new Legend(chart); + chart.legend = new Legend(chart, options.legend); // Get margins by pre-rendering axes // set axes scales each(axes, function (axis) { axis.setScale(); @@ -23639,13 +24488,17 @@ axes = chart.axes, series = chart.series, container = chart.container, i, parentNode = container && container.parentNode; - + // fire the chart.destoy event fireEvent(chart, 'destroy'); + + // Delete the chart from charts lookup array + charts[chart.index] = UNDEFINED; + chart.renderTo.removeAttribute('data-highcharts-chart'); // remove events removeEvent(chart); // ==== Destroy collections: @@ -23660,11 +24513,13 @@ while (i--) { series[i] = series[i].destroy(); } // ==== Destroy chart properties: - each(['title', 'subtitle', 'chartBackground', 'plotBackground', 'plotBGImage', 'plotBorder', 'seriesGroup', 'clipRect', 'credits', 'tracker', 'scroller', 'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', 'renderer'], function (name) { + each(['title', 'subtitle', 'chartBackground', 'plotBackground', 'plotBGImage', + 'plotBorder', 'seriesGroup', 'clipRect', 'credits', 'pointer', 'scroller', + 'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', 'renderer'], function (name) { var prop = chart[name]; if (prop && prop.destroy) { chart[name] = prop.destroy(); } @@ -23685,51 +24540,58 @@ delete chart[i]; } }, + /** - * Prepare for first rendering after all data are loaded + * VML namespaces can't be added until after complete. Listening + * for Perini's doScroll hack is not enough. */ - firstRender: function () { - var chart = this, - options = chart.options, - callback = chart.callback; + isReadyToRender: function () { + var chart = this; - // VML namespaces can't be added until after complete. Listening - // for Perini's doScroll hack is not enough. - var ONREADYSTATECHANGE = 'onreadystatechange', - COMPLETE = 'complete'; // Note: in spite of JSLint's complaints, win == win.top is required /*jslint eqeq: true*/ - if ((!hasSVG && (win == win.top && doc.readyState !== COMPLETE)) || (useCanVG && !win.canvg)) { + if ((!hasSVG && (win == win.top && doc.readyState !== 'complete')) || (useCanVG && !win.canvg)) { /*jslint eqeq: false*/ if (useCanVG) { // Delay rendering until canvg library is downloaded and ready - CanVGController.push(function () { chart.firstRender(); }, options.global.canvasToolsURL); + CanVGController.push(function () { chart.firstRender(); }, chart.options.global.canvasToolsURL); } else { - doc.attachEvent(ONREADYSTATECHANGE, function () { - doc.detachEvent(ONREADYSTATECHANGE, chart.firstRender); - if (doc.readyState === COMPLETE) { + doc.attachEvent('onreadystatechange', function () { + doc.detachEvent('onreadystatechange', chart.firstRender); + if (doc.readyState === 'complete') { chart.firstRender(); } }); } + return false; + } + return true; + }, + + /** + * Prepare for first rendering after all data are loaded + */ + firstRender: function () { + var chart = this, + options = chart.options, + callback = chart.callback; + + // Check whether the chart is ready to render + if (!chart.isReadyToRender()) { return; } - // create the container + // Create the container chart.getContainer(); // Run an early event after the container and renderer are established fireEvent(chart, 'init'); - // Initialize range selector for stock charts - if (Highcharts.RangeSelector && options.rangeSelector.enabled) { - chart.rangeSelector = new Highcharts.RangeSelector(chart); - } - + chart.resetMargins(); chart.setChartSize(); // Set the common chart properties (mainly invert) from the given series chart.propFromSeries(); @@ -23740,20 +24602,17 @@ // Initialize the series each(options.series || [], function (serieOptions) { chart.initSeries(serieOptions); }); - // Run an event where series and axes can be added - //fireEvent(chart, 'beforeRender'); + // Run an event after axes and series are initialized, but before render. At this stage, + // the series data is indexed and cached in the xData and yData arrays, so we can access + // those before rendering. Used in Highstock. + fireEvent(chart, 'beforeRender'); - // Initialize scroller for stock charts - if (Highcharts.Scroller && (options.navigator.enabled || options.scrollbar.enabled)) { - chart.scroller = new Highcharts.Scroller(chart); - } - // depends on inverted and on margins being set - chart.tracker = new MouseTracker(chart, options); + chart.pointer = new Pointer(chart, options); chart.render(); // add canvas chart.renderer.draw(); @@ -23769,77 +24628,10 @@ // If the chart was rendered outside the top container, put it back in chart.cloneRenderTo(true); fireEvent(chart, 'load'); - }, - - init: function (chartEvents) { - var chart = this, - optionsChart = chart.options.chart, - eventType; - - // Run chart - - // Set up auto resize - if (optionsChart.reflow !== false) { - addEvent(chart, 'load', chart.initReflow); - } - - // Chart event handlers - if (chartEvents) { - for (eventType in chartEvents) { - addEvent(chart, eventType, chartEvents[eventType]); - } - } - - chart.xAxis = []; - chart.yAxis = []; - - // Expose methods and variables - chart.animation = useCanVG ? false : pick(optionsChart.animation, true); - chart.setSize = chart.resize; - chart.pointCount = 0; - chart.counters = new ChartCounters(); - /* - if ($) $(function () { - $container = $('#container'); - var origChartWidth, - origChartHeight; - if ($container) { - $('<button>+</button>') - .insertBefore($container) - .click(function () { - if (origChartWidth === UNDEFINED) { - origChartWidth = chartWidth; - origChartHeight = chartHeight; - } - chart.resize(chartWidth *= 1.1, chartHeight *= 1.1); - }); - $('<button>-</button>') - .insertBefore($container) - .click(function () { - if (origChartWidth === UNDEFINED) { - origChartWidth = chartWidth; - origChartHeight = chartHeight; - } - chart.resize(chartWidth *= 0.9, chartHeight *= 0.9); - }); - $('<button>1:1</button>') - .insertBefore($container) - .click(function () { - if (origChartWidth === UNDEFINED) { - origChartWidth = chartWidth; - origChartHeight = chartHeight; - } - chart.resize(origChartWidth, origChartHeight); - }); - } - }) - */ - - chart.firstRender(); } }; // end Chart // Hook for exporting module Chart.prototype.callbacks = []; @@ -23853,23 +24645,24 @@ * 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, x) { + var point = this, - counters = series.chart.counters, - defaultColors; + colors; point.series = series; point.applyOptions(options, x); point.pointAttr = {}; if (series.options.colorByPoint) { - defaultColors = series.chart.options.colors; - point.color = point.color || defaultColors[counters.color++]; - + colors = series.options.colors || series.chart.options.colors; + point.color = point.color || colors[series.colorCounter++]; // loop back to zero - counters.wrapColor(defaultColors.length); + if (series.colorCounter === colors.length) { + series.colorCounter = 0; + } } series.chart.pointCount++; return point; }, @@ -23880,49 +24673,77 @@ * @param {Object} options */ applyOptions: function (options, x) { var point = this, series = point.series, - optionsType = typeof options; + pointValKey = series.pointValKey; - point.config = options; + options = Point.prototype.optionsToObject.call(this, options); - // onedimensional array input - if (optionsType === 'number' || options === null) { - point.y = options; - } else if (typeof options[0] === 'number') { // two-dimentional array - point.x = options[0]; - point.y = options[1]; - } else if (optionsType === 'object' && typeof options.length !== 'number') { // object input - // copy options directly to point - extend(point, options); - point.options = options; + // copy options directly to point + extend(point, options); + point.options = point.options ? extend(point.options, options) : options; + // For higher dimension series types. For instance, for ranges, point.y is mapped to point.low. + if (pointValKey) { + point.y = point[pointValKey]; + } + + // 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 && series) { + point.x = x === UNDEFINED ? series.autoIncrement() : x; + } + + return point; + }, + + /** + * Transform number or array configs into objects + */ + optionsToObject: function (options) { + var ret, + series = this.series, + pointArrayMap = series.pointArrayMap || ['y'], + valueCount = pointArrayMap.length, + firstItemType, + i = 0, + j = 0; + + if (typeof options === 'number' || options === null) { + ret = { y: options }; + + } else if (isArray(options)) { + ret = {}; + // with leading x value + if (options.length > valueCount) { + firstItemType = typeof options[0]; + if (firstItemType === 'string') { + ret.name = options[0]; + } else if (firstItemType === 'number') { + ret.x = options[0]; + } + i++; + } + while (j < valueCount) { + ret[pointArrayMap[j++]] = options[i++]; + } + } else if (typeof options === 'object') { + ret = options; + // This is the fastest way to detect if there are individual point dataLabels that need // to be considered in drawDataLabels. These can only occur in object configs. if (options.dataLabels) { series._hasPointLabels = true; } - + // Same approach as above for markers if (options.marker) { series._hasPointMarkers = true; } - } else if (typeof options[0] === 'string') { // categorized data with name in first position - point.name = 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 - */ - // todo: skip this? It is only used in applyOptions, in translate it should not be used - if (point.x === UNDEFINED) { - point.x = x === UNDEFINED ? series.autoIncrement() : x; - } - + return ret; }, /** * Destroy a point to clear memory. Its reference still stays in series.data. */ @@ -23967,11 +24788,11 @@ /** * Destroy SVG elements associated with the point */ destroyElements: function () { var point = this, - props = ['graphic', 'tracker', 'dataLabel', 'group', 'connector', 'shadowGroup'], + props = ['graphic', 'dataLabel', 'dataLabelUpper', 'group', 'connector', 'shadowGroup'], prop, i = 6; while (i--) { prop = props[i]; if (point[prop]) { @@ -24009,27 +24830,33 @@ selected = pick(selected, !point.selected); // fire the event with the defalut handler point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () { - point.selected = selected; + point.selected = point.options.selected = selected; + series.options.data[inArray(point, series.data)] = point.options; + point.setState(selected && SELECT_STATE); // unselect all other points unless Ctrl or Cmd + click if (!accumulate) { each(chart.getSelectedPoints(), function (loopPoint) { if (loopPoint.selected && loopPoint !== point) { - loopPoint.selected = false; + loopPoint.selected = loopPoint.options.selected = false; + series.options.data[inArray(loopPoint, series.data)] = loopPoint.options; loopPoint.setState(NORMAL_STATE); loopPoint.firePointEvent('unselect'); } }); } }); }, - onMouseOver: function () { + /** + * Runs on mouse over the point + */ + onMouseOver: function (e) { var point = this, series = point.series, chart = series.chart, tooltip = chart.tooltip, hoverPoint = chart.hoverPoint; @@ -24042,18 +24869,21 @@ // trigger the event point.firePointEvent('mouseOver'); // update the tooltip if (tooltip && (!tooltip.shared || series.noSharedTooltip)) { - tooltip.refresh(point); + tooltip.refresh(point, e); } // hover this point.setState(HOVER_STATE); chart.hoverPoint = point; }, - + + /** + * Runs on mouse out from the point + */ onMouseOut: function () { var chart = this.series.chart, hoverPoints = chart.hoverPoints; if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887 @@ -24068,64 +24898,31 @@ * Extendable method for formatting each point's tooltip line * * @return {String} A string to be concatenated in to the common tooltip text */ tooltipFormatter: function (pointFormat) { - var point = this, - series = point.series, - seriesTooltipOptions = series.tooltipOptions, - match = pointFormat.match(/\{(series|point)\.[a-zA-Z]+\}/g), - splitter = /[{\.}]/, - obj, - key, - replacement, - repOptionKey, - parts, - prop, - i, - cfg = { - y: 0, // 0: use 'value' for repOptionKey - open: 0, - high: 0, - low: 0, - close: 0, - percentage: 1, // 1: use the self name for repOptionKey - total: 1 - }; - // Backwards compatibility to y naming in early Highstock - seriesTooltipOptions.valuePrefix = seriesTooltipOptions.valuePrefix || seriesTooltipOptions.yPrefix; - seriesTooltipOptions.valueDecimals = seriesTooltipOptions.valueDecimals || seriesTooltipOptions.yDecimals; - seriesTooltipOptions.valueSuffix = seriesTooltipOptions.valueSuffix || seriesTooltipOptions.ySuffix; - - // loop over the variables defined on the form {series.name}, {point.y} etc - for (i in match) { - key = match[i]; - if (isString(key) && key !== pointFormat) { // IE matches more than just the variables - - // Split it further into parts - parts = (' ' + key).split(splitter); // add empty string because IE and the rest handles it differently - obj = { 'point': point, 'series': series }[parts[1]]; - prop = parts[2]; - - // Add some preformatting - if (obj === point && cfg.hasOwnProperty(prop)) { - repOptionKey = cfg[prop] ? prop : 'value'; - replacement = (seriesTooltipOptions[repOptionKey + 'Prefix'] || '') + - numberFormat(point[prop], pick(seriesTooltipOptions[repOptionKey + 'Decimals'], -1)) + - (seriesTooltipOptions[repOptionKey + 'Suffix'] || ''); - - // Automatic replacement - } else { - replacement = obj[prop]; - } - - pointFormat = pointFormat.replace(key, replacement); + // Insert options for valueDecimals, valuePrefix, and valueSuffix + var series = this.series, + seriesTooltipOptions = series.tooltipOptions, + valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''), + valuePrefix = seriesTooltipOptions.valuePrefix || '', + valueSuffix = seriesTooltipOptions.valueSuffix || ''; + + // Loop over the point array map and replace unformatted values with sprintf formatting markup + each(series.pointArrayMap || ['y'], function (key) { + key = '{point.' + key; // without the closing bracket + if (valuePrefix || valueSuffix) { + pointFormat = pointFormat.replace(key + '}', valuePrefix + key + '}' + valueSuffix); } - } + pointFormat = pointFormat.replace(key + '}', key + ':,.' + valueDecimals + 'f}'); + }); - return pointFormat; + return format(pointFormat, { + point: this, + series: this.series + }); }, /** * Update the point with new options (typically x/y data) and optionally redraw the series. * @@ -24139,11 +24936,10 @@ var point = this, series = point.series, graphic = point.graphic, i, data = series.data, - dataLength = data.length, chart = series.chart; redraw = pick(redraw, true); // fire the event with a default handler of doing the update @@ -24158,18 +24954,15 @@ graphic.attr(point.pointAttr[series.state]); } } // record changes in the parallel arrays - for (i = 0; i < dataLength; i++) { - if (data[i] === point) { - series.xData[i] = point.x; - series.yData[i] = point.y; - series.options.data[i] = options; - break; - } - } + i = inArray(point, data); + series.xData[i] = point.x; + series.yData[i] = series.toYData ? series.toYData(point) : point.y; + series.zData[i] = point.z; + series.options.data[i] = point.options; // redraw series.isDirty = true; series.isDirtyData = true; if (redraw) { @@ -24187,33 +24980,26 @@ remove: function (redraw, animation) { var point = this, series = point.series, chart = series.chart, i, - data = series.data, - dataLength = data.length; + data = series.data; setAnimation(animation, chart); redraw = pick(redraw, true); // fire the event with a default handler of removing the point point.firePointEvent('remove', null, function () { - //erase(series.data, point); + // splice all the parallel arrays + i = inArray(point, data); + data.splice(i, 1); + series.options.data.splice(i, 1); + series.xData.splice(i, 1); + series.yData.splice(i, 1); + series.zData.splice(i, 1); - for (i = 0; i < dataLength; i++) { - if (data[i] === point) { - - // splice all the parallel arrays - data.splice(i, 1); - series.options.data.splice(i, 1); - series.xData.splice(i, 1); - series.yData.splice(i, 1); - break; - } - } - point.destroy(); // redraw series.isDirty = true; @@ -24287,12 +25073,14 @@ markerOptions = defaultPlotOptions[series.type].marker && series.options.marker, normalDisabled = markerOptions && !markerOptions.enabled, markerStateOptions = markerOptions && markerOptions.states[state], stateDisabled = markerStateOptions && markerStateOptions.enabled === false, stateMarkerGraphic = series.stateMarkerGraphic, + pointMarker = point.marker || {}, chart = series.chart, radius, + newSymbol, pointAttr = point.pointAttr; state = state || NORMAL_STATE; // empty string if ( @@ -24324,22 +25112,33 @@ } else { // if a graphic is not applied to each point in the normal state, create a shared // graphic for the hover state if (state && markerStateOptions) { radius = markerStateOptions.radius; - if (!stateMarkerGraphic) { // add + newSymbol = pointMarker.symbol || series.symbol; + + // If the point has another symbol than the previous one, throw away the + // state marker graphic and force a new one (#1459) + if (stateMarkerGraphic && stateMarkerGraphic.currentSymbol !== newSymbol) { + stateMarkerGraphic = stateMarkerGraphic.destroy(); + } + + // Add a new state marker graphic + if (!stateMarkerGraphic) { series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol( - series.symbol, + newSymbol, plotX - radius, plotY - radius, 2 * radius, 2 * radius ) .attr(pointAttr[state]) .add(series.markerGroup); + stateMarkerGraphic.currentSymbol = newSymbol; - } else { // update + // Move the existing graphic + } else { stateMarkerGraphic.attr({ // #1054 x: plotX - radius, y: plotY - radius }); } @@ -24379,24 +25178,28 @@ isCartesian: true, type: 'line', pointClass: Point, sorted: true, // requires the data to be sorted + requireSorting: true, pointAttrToOptions: { // mapping between SVG attributes and the corresponding options stroke: 'lineColor', 'stroke-width': 'lineWidth', fill: 'fillColor', r: 'radius' }, + colorCounter: 0, init: function (chart, options) { var series = this, eventType, - events; + events, + linkedTo, + chartSeries = chart.series; series.chart = chart; series.options = options = series.setOptions(options); // merge with plotOptions - + // bind the axes series.bindAxes(); // set some variables extend(series, { @@ -24435,20 +25238,36 @@ if (series.isCartesian) { chart.hasCartesianSeries = true; } // Register it in the chart - chart.series.push(series); + chartSeries.push(series); + series._i = chartSeries.length - 1; // Sort series according to index option (#248, #1123) - stableSort(chart.series, function (a, b) { - return (a.options.index || 0) - (b.options.index || 0); + stableSort(chartSeries, function (a, b) { + return pick(a.options.index, a._i) - pick(b.options.index, a._i); }); - each(chart.series, function (series, i) { + each(chartSeries, function (series, i) { series.index = i; series.name = series.name || 'Series ' + (i + 1); }); + + // Linked series + linkedTo = options.linkedTo; + series.linkedSeries = []; + if (isString(linkedTo)) { + if (linkedTo === ':previous') { + linkedTo = chartSeries[series.index - 1]; + } else { + linkedTo = chart.get(linkedTo); + } + if (linkedTo) { + linkedTo.linkedSeries.push(series); + series.linkedParent = linkedTo; + } + } }, /** * Set the xAxis and yAxis properties of cartesian series, and register the series * in the axis.series array @@ -24468,10 +25287,11 @@ axisOptions = axis.options; // apply if the series xAxis or yAxis option mathches the number of the // axis, or if undefined, use the first axis if ((seriesOptions[AXIS] === axisOptions.index) || + (seriesOptions[AXIS] !== UNDEFINED && seriesOptions[AXIS] === axisOptions.id) || (seriesOptions[AXIS] === UNDEFINED && axisOptions.index === 0)) { // register this series in the axis.series lookup axis.series.push(series); @@ -24480,11 +25300,16 @@ // mark dirty for redraw axis.isDirty = true; } }); - + + // The series needs an X and an Y axis + if (!series[AXIS]) { + error(18, true); + } + }); } }, @@ -24555,24 +25380,20 @@ setOptions: function (itemOptions) { var chart = this.chart, chartOptions = chart.options, plotOptions = chartOptions.plotOptions, typeOptions = plotOptions[this.type], - data = itemOptions.data, options; - itemOptions.data = null; // remove from merge to prevent looping over the data set + this.userOptions = itemOptions; options = merge( typeOptions, plotOptions.series, itemOptions ); - // Re-insert the data array to the options and the original config (#717) - options.data = itemOptions.data = data; - // the tooltip options are merged between global and series specific options this.tooltipOptions = merge(chartOptions.tooltip, options.tooltip); // Delte marker object if not allowed (#1125) if (typeOptions.marker === null) { @@ -24585,27 +25406,54 @@ /** * Get the series' color */ getColor: function () { var options = this.options, + userOptions = this.userOptions, defaultColors = this.chart.options.colors, - counters = this.chart.counters; - this.color = options.color || - (!options.colorByPoint && defaultColors[counters.color++]) || 'gray'; + counters = this.chart.counters, + color, + colorIndex; + + color = options.color || defaultPlotOptions[this.type].color; + + if (!color && !options.colorByPoint) { + if (defined(userOptions._colorIndex)) { // after Series.update() + colorIndex = userOptions._colorIndex; + } else { + userOptions._colorIndex = counters.color; + colorIndex = counters.color++; + } + color = defaultColors[colorIndex]; + } + + this.color = color; counters.wrapColor(defaultColors.length); }, /** * Get the series' symbol */ getSymbol: function () { var series = this, + userOptions = series.userOptions, seriesMarkerOption = series.options.marker, chart = series.chart, defaultSymbols = chart.options.symbols, - counters = chart.counters; - series.symbol = seriesMarkerOption.symbol || defaultSymbols[counters.symbol++]; - + counters = chart.counters, + symbolIndex; + + series.symbol = seriesMarkerOption.symbol; + if (!series.symbol) { + if (defined(userOptions._symbolIndex)) { // after Series.update() + symbolIndex = userOptions._symbolIndex; + } else { + userOptions._symbolIndex = counters.symbol; + symbolIndex = counters.symbol++; + } + series.symbol = defaultSymbols[symbolIndex]; + } + // don't substract radius in image symbols (#604) if (/^url/.test(series.symbol)) { seriesMarkerOption.radius = 0; } counters.wrapSymbol(defaultSymbols.length); @@ -24673,20 +25521,22 @@ * @param {Boolean|Object} animation Whether to apply animation, and optionally animation * configuration */ addPoint: function (options, redraw, shift, animation) { var series = this, + seriesOptions = series.options, data = series.data, graph = series.graph, area = series.area, chart = series.chart, xData = series.xData, yData = series.yData, + zData = series.zData, + names = series.names, currentShift = (graph && graph.shift) || 0, - dataOptions = series.options.data, - point, - proto = series.pointClass.prototype; + dataOptions = seriesOptions.data, + point; setAnimation(animation, chart); // Make graph animate sideways if (graph && shift) { @@ -24703,25 +25553,34 @@ redraw = pick(redraw, true); // Get options and push the point to xData, yData and series.options. In series.generatePoints // the Point instance will be created on demand and pushed to the series.data array. point = { series: series }; - proto.applyOptions.apply(point, [options]); + series.pointClass.prototype.applyOptions.apply(point, [options]); xData.push(point.x); - yData.push(proto.toYData ? proto.toYData.call(point) : point.y); + yData.push(series.toYData ? series.toYData(point) : point.y); + zData.push(point.z); + if (names) { + names[point.x] = point.name; + } dataOptions.push(options); + // Generate points to be added to the legend (#1329) + if (seriesOptions.legendType === 'point') { + series.generatePoints(); + } // Shift the first point off the parallel arrays // todo: consider series.removePoint(i) method if (shift) { if (data[0] && data[0].remove) { data[0].remove(false); } else { data.shift(); xData.shift(); yData.shift(); + zData.shift(); dataOptions.shift(); } } series.getAttribs(); @@ -24740,33 +25599,32 @@ */ setData: function (data, redraw) { var series = this, oldData = series.points, options = series.options, - initialColor = series.initialColor, chart = series.chart, firstPoint = null, xAxis = series.xAxis, - i, - pointProto = series.pointClass.prototype; + names = xAxis && xAxis.categories && !xAxis.categories.length ? [] : null, + i; // reset properties series.xIncrement = null; series.pointRange = xAxis && xAxis.categories ? 1 : options.pointRange; - if (defined(initialColor)) { // reset colors for pie - chart.counters.color = initialColor; - } + series.colorCounter = 0; // for series with colorByPoint (#1547) // parallel arrays var xData = [], yData = [], + zData = [], dataLength = data ? data.length : [], turboThreshold = options.turboThreshold || 1000, pt, pointArrayMap = series.pointArrayMap, - valueCount = pointArrayMap && pointArrayMap.length; + valueCount = pointArrayMap && pointArrayMap.length, + hasToYData = !!series.toYData; // In turbo mode, only one- or twodimensional arrays of numbers are allowed. The // first value is tested, and we assume that all the rest are defined the same // way. Although the 'for' loops are similar, they are repeated inside each // if-else conditional for max performance. @@ -24807,26 +25665,40 @@ } /* else { error(12); // Highcharts expects configs to be numbers or arrays in turbo mode }*/ } else { for (i = 0; i < dataLength; i++) { - pt = { series: series }; - pointProto.applyOptions.apply(pt, [data[i]]); - xData[i] = pt.x; - yData[i] = pointProto.toYData ? pointProto.toYData.call(pt) : pt.y; + if (data[i] !== UNDEFINED) { // stray commas in oldIE + pt = { series: series }; + series.pointClass.prototype.applyOptions.apply(pt, [data[i]]); + xData[i] = pt.x; + yData[i] = hasToYData ? series.toYData(pt) : pt.y; + zData[i] = pt.z; + if (names && pt.name) { + names[i] = pt.name; + } + } } } + + // Unsorted data is not supported by the line tooltip as well as data grouping and + // navigation in Stock charts (#725) + if (series.requireSorting && xData.length > 1 && xData[1] < xData[0]) { + error(15); + } // Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON if (isString(yData[0])) { error(14, true); } series.data = []; series.options.data = data; series.xData = xData; series.yData = yData; + series.zData = zData; + series.names = names; // destroy old points i = (oldData && oldData.length) || 0; while (i--) { if (oldData[i] && oldData[i].destroy) { @@ -25033,26 +25905,31 @@ if (!this.processedXData) { // hidden series this.processData(); } this.generatePoints(); var series = this, - chart = series.chart, options = series.options, stacking = options.stacking, xAxis = series.xAxis, categories = xAxis.categories, yAxis = series.yAxis, points = series.points, dataLength = points.length, hasModifyValue = !!series.modifyValue, isBottomSeries, - allStackSeries = yAxis.series, - i = allStackSeries.length, - placeBetween = options.pointPlacement === 'between'; + allStackSeries, + i, + placeBetween = options.pointPlacement === 'between', + threshold = options.threshold; //nextSeriesDown; - // Is it the last visible series? + // Is it the last visible series? (#809, #1722). + // TODO: After merging in the 'stacking' branch, this logic should probably be moved to Chart.getStacks + allStackSeries = yAxis.series.sort(function (a, b) { + return a.index - b.index; + }); + i = allStackSeries.length; while (i--) { if (allStackSeries[i].visible) { if (allStackSeries[i] === series) { // #809 isBottomSeries = true; } @@ -25064,27 +25941,31 @@ for (i = 0; i < dataLength; i++) { var point = points[i], xValue = point.x, yValue = point.y, yBottom = point.low, - stack = yAxis.stacks[(yValue < options.threshold ? '-' : '') + series.stackKey], + stack = yAxis.stacks[(yValue < threshold ? '-' : '') + series.stackKey], pointStack, pointStackTotal; - - // get the plotX translation - //point.plotX = mathRound(xAxis.translate(xValue, 0, 0, 0, 1) * 10) / 10; // Math.round fixes #591 + + // Discard disallowed y values for log axes + if (yAxis.isLog && yValue <= 0) { + point.y = yValue = null; + } + + // Get the plotX translation point.plotX = xAxis.translate(xValue, 0, 0, 0, 1, placeBetween); // Math.round fixes #591 - // calculate the bottom y value for stacked series + // Calculate the bottom y value for stacked series if (stacking && series.visible && stack && stack[xValue]) { pointStack = stack[xValue]; pointStackTotal = pointStack.total; pointStack.cum = yBottom = pointStack.cum - yValue; // start from top yValue = yBottom + yValue; if (isBottomSeries) { - yBottom = pick(options.threshold, yAxis.min); + yBottom = pick(threshold, yAxis.min); } if (yAxis.isLog && yBottom <= 0) { // #1200, #1232 yBottom = null; } @@ -25108,18 +25989,18 @@ if (hasModifyValue) { yValue = series.modifyValue(yValue, point); } // Set the the plotY value, reset it for redraws - point.plotY = (typeof yValue === 'number') ? + point.plotY = (typeof yValue === 'number' && yValue !== Infinity) ? mathRound(yAxis.translate(yValue, 0, 1, 0, 1) * 10) / 10 : // Math.round fixes #591 UNDEFINED; - // set client related positions for mouse tracking - point.clientX = chart.inverted ? - chart.plotHeight - point.plotX : - point.plotX; // for mouse tracking + // Set client related positions for mouse tracking + point.clientX = placeBetween ? xAxis.translate(xValue, 0, 0, 0, 1) : point.plotX; // #1514 + + point.negative = point.y < (threshold || 0); // some API data point.category = categories && categories[point.x] !== UNDEFINED ? categories[point.x] : point.x; @@ -25138,11 +26019,10 @@ pointsLength, low, high, xAxis = series.xAxis, axisLength = xAxis ? (xAxis.tooltipLen || xAxis.len) : series.chart.plotSizeX, // tooltipLen and tooltipPosName used in polar - plotX = (xAxis && xAxis.tooltipPosName) || 'plotX', point, i, tooltipPoints = []; // a lookup array for each pixel in the x dimension // don't waste resources if tracker is disabled @@ -25171,11 +26051,11 @@ point = points[i]; // Set this range's low to the last range's high plus one low = points[i - 1] ? high + 1 : 0; // Now find the new high high = points[i + 1] ? - mathMax(0, mathFloor((point[plotX] + (points[i + 1] ? points[i + 1][plotX] : axisLength)) / 2)) : + mathMax(0, mathFloor((point.clientX + (points[i + 1] ? points[i + 1].clientX : axisLength)) / 2)) : axisLength; while (low >= 0 && low <= high) { tooltipPoints[low++] = point; } @@ -25184,46 +26064,54 @@ }, /** * Format the header of the tooltip */ - tooltipHeaderFormatter: function (key) { + tooltipHeaderFormatter: function (point) { var series = this, tooltipOptions = series.tooltipOptions, xDateFormat = tooltipOptions.xDateFormat, + dateTimeLabelFormats = tooltipOptions.dateTimeLabelFormats, xAxis = series.xAxis, isDateTime = xAxis && xAxis.options.type === 'datetime', + headerFormat = tooltipOptions.headerFormat, + closestPointRange = xAxis && xAxis.closestPointRange, n; // Guess the best date format based on the closest point distance (#568) if (isDateTime && !xDateFormat) { - for (n in timeUnits) { - if (timeUnits[n] >= xAxis.closestPointRange) { - xDateFormat = tooltipOptions.dateTimeLabelFormats[n]; - break; - } - } + if (closestPointRange) { + for (n in timeUnits) { + if (timeUnits[n] >= closestPointRange) { + xDateFormat = dateTimeLabelFormats[n]; + break; + } + } + } else { + xDateFormat = dateTimeLabelFormats.day; + } } - return tooltipOptions.headerFormat - .replace('{point.key}', isDateTime && isNumber(key) ? dateFormat(xDateFormat, key) : key) - .replace('{series.name}', series.name) - .replace('{series.color}', series.color); + // Insert the header date format if any + if (isDateTime && xDateFormat && isNumber(point.key)) { + headerFormat = headerFormat.replace('{point.key}', '{point.key:' + xDateFormat + '}'); + } + + return format(headerFormat, { + point: point, + series: series + }); }, /** * Series mouse over handler */ onMouseOver: function () { var series = this, chart = series.chart, hoverSeries = chart.hoverSeries; - /*if (!hasTouch && chart.mouseIsDown) { - return; - }*/ - // set normal state to previous series if (hoverSeries && hoverSeries !== series) { hoverSeries.onMouseOut(); } @@ -25259,11 +26147,11 @@ fireEvent(series, 'mouseOut'); } // hide the tooltip - if (tooltip && !options.stickyTracking && !tooltip.shared) { + if (tooltip && !options.stickyTracking && (!tooltip.shared || series.noSharedTooltip)) { tooltip.hide(); } // set normal state series.setState(); @@ -25388,14 +26276,14 @@ plotX = point.plotX; plotY = point.plotY; graphic = point.graphic; pointMarkerOptions = point.marker || {}; enabled = (seriesMarkerOptions.enabled && pointMarkerOptions.enabled === UNDEFINED) || pointMarkerOptions.enabled; - isInside = chart.isInsidePlot(plotX, plotY, chart.inverted); + isInside = chart.isInsidePlot(mathRound(plotX), plotY, chart.inverted); // #1858 // only draw the point if y is defined - if (enabled && plotY !== UNDEFINED && !isNaN(plotY)) { + if (enabled && plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) { // shortcuts pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]; radius = pointAttr.r; symbol = pick(pointMarkerOptions.symbol, series.symbol); @@ -25465,11 +26353,12 @@ * points with individual marker options. If such options are not defined for the point, * a reference to the series wide attributes is stored in point.pointAttr. */ getAttribs: function () { var series = this, - normalOptions = defaultPlotOptions[series.type].marker ? series.options.marker : series.options, + seriesOptions = series.options, + normalOptions = defaultPlotOptions[series.type].marker ? seriesOptions.marker : seriesOptions, stateOptions = normalOptions.states, stateOptionsHover = stateOptions[HOVER_STATE], pointStateOptionsHover, seriesColor = series.color, normalDefaults = { @@ -25481,19 +26370,20 @@ point, seriesPointAttr = [], pointAttr, pointAttrToOptions = series.pointAttrToOptions, hasPointSpecificOptions, + negativeColor = seriesOptions.negativeColor, key; // series type specific modifications - if (series.options.marker) { // line, spline, area, areaspline, scatter + if (seriesOptions.marker) { // line, spline, area, areaspline, scatter // if no hover radius is given, default to normal radius + 2 stateOptionsHover.radius = stateOptionsHover.radius || normalOptions.radius + 2; stateOptionsHover.lineWidth = stateOptionsHover.lineWidth || normalOptions.lineWidth + 1; - + } else { // column, bar, pie // if no hover color is given, brighten the normal color stateOptionsHover.color = stateOptionsHover.color || Color(stateOptionsHover.color || seriesColor) @@ -25521,33 +26411,36 @@ point = points[i]; normalOptions = (point.options && point.options.marker) || point.options; if (normalOptions && normalOptions.enabled === false) { normalOptions.radius = 0; } - hasPointSpecificOptions = series.options.colorByPoint; // #868 + if (point.negative && negativeColor) { + point.color = point.fillColor = negativeColor; + } + + hasPointSpecificOptions = seriesOptions.colorByPoint || point.color; // #868 + // check if the point has specific visual options if (point.options) { for (key in pointAttrToOptions) { if (defined(normalOptions[pointAttrToOptions[key]])) { hasPointSpecificOptions = true; } } } - - // a specific marker config object is defined for the individual point: // create it's own attribute collection if (hasPointSpecificOptions) { normalOptions = normalOptions || {}; pointAttr = []; stateOptions = normalOptions.states || {}; // reassign for individual point pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {}; // Handle colors for column and pies - if (!series.options.marker) { // column, bar, point + if (!seriesOptions.marker) { // column, bar, point // if no hover color is given, brighten the normal color pointStateOptionsHover.color = Color(pointStateOptionsHover.color || point.color) .brighten(pointStateOptionsHover.brightness || stateOptionsHover.brightness).get(); @@ -25563,17 +26456,23 @@ pointAttr[HOVER_STATE] = series.convertAttribs( stateOptions[HOVER_STATE], seriesPointAttr[HOVER_STATE], pointAttr[NORMAL_STATE] ); + // inherit from point normal and series hover pointAttr[SELECT_STATE] = series.convertAttribs( stateOptions[SELECT_STATE], seriesPointAttr[SELECT_STATE], pointAttr[NORMAL_STATE] ); + // Force the fill to negativeColor on markers + if (point.negative && seriesOptions.marker && negativeColor) { + pointAttr[NORMAL_STATE].fill = pointAttr[HOVER_STATE].fill = pointAttr[SELECT_STATE].fill = + series.convertAttribs({ fillColor: negativeColor }).fill; + } // no marker config object is created: copy a reference to the series-wide // attribute collection } else { @@ -25583,12 +26482,38 @@ point.pointAttr = pointAttr; } }, + /** + * Update the series with a new set of options + */ + update: function (newOptions, redraw) { + var chart = this.chart, + // must use user options when changing type because this.options is merged + // in with type specific plotOptions + oldOptions = this.userOptions, + oldType = this.type; + // Do the merge, with some forced options + newOptions = merge(oldOptions, { + animation: false, + index: this.index, + pointStart: this.xData[0] // when updating after addPoint + }, newOptions); + // Destroy the series and reinsert methods from the type prototype + this.remove(false); + extend(this, seriesTypes[newOptions.type || oldType].prototype); + + + this.init(chart, newOptions); + if (pick(redraw, true)) { + chart.redraw(false); + } + }, + /** * Clear DOM objects and free up memory */ destroy: function () { var series = this, @@ -25610,11 +26535,11 @@ // erase from axes each(['xAxis', 'yAxis'], function (AXIS) { axis = series[AXIS]; if (axis) { erase(axis.series, series); - axis.isDirty = true; + axis.isDirty = axis.forceRedraw = true; } }); // remove legend items if (series.legendItem) { @@ -25633,11 +26558,12 @@ // Clear the animation timeout if we are destroying the series during initial animation clearTimeout(series.animationTimeout); // destroy all SVGElements associated to the series - each(['area', 'graph', 'dataLabelsGroup', 'group', 'markerGroup', 'tracker', 'trackerGroup'], function (prop) { + each(['area', 'graph', 'dataLabelsGroup', 'group', 'markerGroup', 'tracker', + 'graphNeg', 'areaNeg', 'posClip', 'negClip'], function (prop) { if (series[prop]) { // issue 134 workaround destroy = issue134 && prop === 'group' ? 'hide' : @@ -25683,22 +26609,24 @@ // Create a separate group for the data labels to avoid rotation dataLabelsGroup = series.plotGroup( 'dataLabelsGroup', 'data-labels', series.visible ? VISIBLE : HIDDEN, - 6 + options.zIndex || 6 ); // Make the labels for each point generalOptions = options; each(points, function (point) { var enabled, dataLabel = point.dataLabel, + labelConfig, attr, name, rotation, + connector = point.connector, isNew = true; // Determine if each data label is enabled pointOptions = point.options && point.options.dataLabels; enabled = generalOptions.enabled || (pointOptions && pointOptions.enabled); @@ -25717,24 +26645,36 @@ // Create individual options structure that can be extended without // affecting others options = merge(generalOptions, pointOptions); // Get the string - str = options.formatter.call(point.getLabelConfig(), options); + labelConfig = point.getLabelConfig(); + str = options.format ? + format(options.format, labelConfig) : + options.formatter.call(labelConfig, options); // Determine the color options.style.color = pick(options.color, options.style.color, series.color, 'black'); // update existing label if (dataLabel) { - // vertically centered - dataLabel - .attr({ - text: str - }); - isNew = false; + + if (defined(str)) { + dataLabel + .attr({ + text: str + }); + isNew = false; + + } else { // #1437 - the label is shown conditionally + point.dataLabel = dataLabel = dataLabel.destroy(); + if (connector) { + point.connector = connector.destroy(); + } + } + // create new label } else if (defined(str)) { attr = { //align: align, fill: options.backgroundColor, @@ -25766,12 +26706,12 @@ .add(dataLabelsGroup) .shadow(options.shadow); } - // Now the data label is created and placed at 0,0, so we need to align it if (dataLabel) { + // Now the data label is created and placed at 0,0, so we need to align it series.alignDataLabel(point, dataLabel, options, null, isNew); } } }); } @@ -25815,42 +26755,63 @@ alignAttr = dataLabel.alignAttr; } // Show or hide based on the final aligned position dataLabel.attr({ - visibility: options.crop === false || chart.isInsidePlot(alignAttr.x, alignAttr.y) || chart.isInsidePlot(plotX, plotY, inverted) ? - (hasSVG ? 'inherit' : VISIBLE) : + visibility: options.crop === false || /*chart.isInsidePlot(alignAttr.x, alignAttr.y) || */chart.isInsidePlot(plotX, plotY, inverted) ? + (chart.renderer.isSVG ? 'inherit' : VISIBLE) : HIDDEN }); }, /** * Return the graph path of a segment */ getSegmentPath: function (segment) { var series = this, - segmentPath = []; - + segmentPath = [], + step = series.options.step; + // build the segment line each(segment, function (point, i) { + + var plotX = point.plotX, + plotY = point.plotY, + lastPoint; if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object segmentPath.push.apply(segmentPath, series.getPointSpline(segment, point, i)); } else { // moveTo or lineTo segmentPath.push(i ? L : M); // step line? - if (i && series.options.step) { - var lastPoint = segment[i - 1]; - segmentPath.push( - point.plotX, - lastPoint.plotY - ); + if (step && i) { + lastPoint = segment[i - 1]; + if (step === 'right') { + segmentPath.push( + lastPoint.plotX, + plotY + ); + + } else if (step === 'center') { + segmentPath.push( + (lastPoint.plotX + plotX) / 2, + lastPoint.plotY, + (lastPoint.plotX + plotX) / 2, + plotY + ); + + } else { + segmentPath.push( + plotX, + lastPoint.plotY + ); + } } // normal line to next point segmentPath.push( point.plotX, @@ -25893,58 +26854,151 @@ }, /** * Draw the actual graph */ - drawGraph: function () { - var options = this.options, - graph = this.graph, - group = this.group, - color = options.lineColor || this.color, + drawGraph: function () { + var series = this, + options = this.options, + props = [['graph', options.lineColor || this.color]], lineWidth = options.lineWidth, dashStyle = options.dashStyle, - attribs, - graphPath = this.getGraphPath(); + graphPath = this.getGraphPath(), + negativeColor = options.negativeColor; - + if (negativeColor) { + props.push(['graphNeg', negativeColor]); + } + // draw the graph - if (graph) { - stop(graph); // cancel running animations, #459 - graph.animate({ d: graphPath }); - - } else { - if (lineWidth) { + each(props, function (prop, i) { + var graphKey = prop[0], + graph = series[graphKey], + attribs; + + if (graph) { + stop(graph); // cancel running animations, #459 + graph.animate({ d: graphPath }); + + } else if (lineWidth && graphPath.length) { // #1487 attribs = { - stroke: color, + stroke: prop[1], 'stroke-width': lineWidth, zIndex: 1 // #1069 }; if (dashStyle) { attribs.dashstyle = dashStyle; } - this.graph = this.chart.renderer.path(graphPath) - .attr(attribs).add(group).shadow(options.shadow); + series[graphKey] = series.chart.renderer.path(graphPath) + .attr(attribs) + .add(series.group) + .shadow(!i && options.shadow); } - } + }); }, + + /** + * Clip the graphs into the positive and negative coloured graphs + */ + clipNeg: function () { + var options = this.options, + chart = this.chart, + renderer = chart.renderer, + negativeColor = options.negativeColor, + translatedThreshold, + posAttr, + negAttr, + graph = this.graph, + area = this.area, + posClip = this.posClip, + negClip = this.negClip, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + chartSizeMax = mathMax(chartWidth, chartHeight), + above, + below; + + if (negativeColor && (graph || area)) { + translatedThreshold = mathCeil(this.yAxis.len - this.yAxis.translate(options.threshold || 0)); + above = { + x: 0, + y: 0, + width: chartSizeMax, + height: translatedThreshold + }; + below = { + x: 0, + y: translatedThreshold, + width: chartSizeMax, + height: chartSizeMax - translatedThreshold + }; + + if (chart.inverted && renderer.isVML) { + above = { + x: chart.plotWidth - translatedThreshold - chart.plotLeft, + y: 0, + width: chartWidth, + height: chartHeight + }; + below = { + x: translatedThreshold + chart.plotLeft - chartWidth, + y: 0, + width: chart.plotLeft + translatedThreshold, + height: chartWidth + }; + } + + if (this.yAxis.reversed) { + posAttr = below; + negAttr = above; + } else { + posAttr = above; + negAttr = below; + } + + if (posClip) { // update + posClip.animate(posAttr); + negClip.animate(negAttr); + } else { + + this.posClip = posClip = renderer.clipRect(posAttr); + this.negClip = negClip = renderer.clipRect(negAttr); + + if (graph) { + graph.clip(posClip); + this.graphNeg.clip(negClip); + } + + if (area) { + area.clip(posClip); + this.areaNeg.clip(negClip); + } + } + } + }, /** - * Initialize and perform group inversion on series.group and series.trackerGroup + * Initialize and perform group inversion on series.group and series.markerGroup */ invertGroups: function () { var series = this, chart = series.chart; + + // Pie, go away (#1736) + if (!series.xAxis) { + return; + } // A fixed size is needed for inversion to work function setInvert() { var size = { width: series.yAxis.len, height: series.xAxis.len }; - each(['group', 'trackerGroup', 'markerGroup'], function (groupName) { + each(['group', 'markerGroup'], function (groupName) { if (series[groupName]) { series[groupName].attr(size).invert(); } }); } @@ -25960,34 +27014,36 @@ // On subsequent render and redraw, just do setInvert without setting up events again series.invertGroups = setInvert; }, /** - * General abstraction for creating plot groups like series.group, series.trackerGroup, series.dataLabelsGroup and + * General abstraction for creating plot groups like series.group, series.dataLabelsGroup and * series.markerGroup. On subsequent calls, the group will only be adjusted to the updated plot size. */ plotGroup: function (prop, name, visibility, zIndex, parent) { var group = this[prop], + isNew = !group, chart = this.chart, xAxis = this.xAxis, yAxis = this.yAxis; // Generate it on first call - if (!group) { + if (isNew) { this[prop] = group = chart.renderer.g(name) .attr({ visibility: visibility, zIndex: zIndex || 0.1 // IE8 needs this }) .add(parent); } // Place it on first and subsequent (redraw) calls - group.translate( - xAxis ? xAxis.left : chart.plotLeft, - yAxis ? yAxis.top : chart.plotTop - ); - + group[isNew ? 'attr' : 'animate']({ + translateX: xAxis ? xAxis.left : chart.plotLeft, + translateY: yAxis ? yAxis.top : chart.plotTop, + scaleX: 1, // #1623 + scaleY: 1 + }); return group; }, /** @@ -25997,11 +27053,13 @@ var series = this, chart = series.chart, group, options = series.options, animation = options.animation, - doAnimation = animation && !!series.animate, + doAnimation = animation && !!series.animate && + chart.renderer.isSVG, // this animation doesn't work in IE8 quirks when the group div is hidden, + // and looks bad in other oldIE visibility = series.visible ? VISIBLE : HIDDEN, zIndex = options.zIndex, hasRendered = series.hasRendered, chartSeriesGroup = chart.seriesGroup; @@ -26028,23 +27086,24 @@ } // cache attributes for shapes series.getAttribs(); - // SVGRenderer needs to know this before drawing elements (#1089) - group.inverted = chart.inverted; + // SVGRenderer needs to know this before drawing elements (#1089, #1795) + group.inverted = series.isCartesian ? chart.inverted : false; // draw the graph if any if (series.drawGraph) { series.drawGraph(); + series.clipNeg(); } + // draw the data labels (inn pies they go before the points) + series.drawDataLabels(); + // draw the points series.drawPoints(); - - // draw the data labels - series.drawDataLabels(); // draw the mouse tracking area if (series.options.enableMouseTracking !== false) { series.drawTracker(); @@ -26056,13 +27115,10 @@ } // Initial clipping, must be defined after inverting groups for VML if (options.clip !== false && !series.sharedClipKey && !hasRendered) { group.clip(chart.clipRect); - if (this.trackerGroup) { - this.trackerGroup.clip(chart.clipRect); - } } // Run the animation if (doAnimation) { series.animate(); @@ -26080,11 +27136,13 @@ */ redraw: function () { var series = this, chart = series.chart, wasDirtyData = series.isDirtyData, // cache it here as it is set to false in render, but used after - group = series.group; + group = series.group, + xAxis = series.xAxis, + yAxis = series.yAxis; // reposition on resize if (group) { if (chart.inverted) { group.attr({ @@ -26092,12 +27150,12 @@ height: chart.plotHeight }); } group.animate({ - translateX: series.xAxis.left, - translateY: series.yAxis.top + translateX: pick(xAxis && xAxis.left, chart.plotLeft), + translateY: pick(yAxis && yAxis.top, chart.plotTop) }); } series.translate(); series.setTooltipPoints(true); @@ -26113,12 +27171,14 @@ */ setState: function (state) { var series = this, options = series.options, graph = series.graph, + graphNeg = series.graphNeg, stateOptions = options.states, - lineWidth = options.lineWidth; + lineWidth = options.lineWidth, + attribs; state = state || NORMAL_STATE; if (series.state !== state) { series.state = state; @@ -26130,13 +27190,18 @@ if (state) { lineWidth = stateOptions[state].lineWidth || lineWidth + 1; } if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML - graph.attr({ // use attr because animate will cause any other animation on the graph to stop + attribs = { 'stroke-width': lineWidth - }, state ? 0 : 500); + }; + // use attr because animate will cause any other animation on the graph to stop + graph.attr(attribs); + if (graphNeg) { + graphNeg.attr(attribs); + } } } }, /** @@ -26147,51 +27212,32 @@ */ setVisible: function (vis, redraw) { var series = this, chart = series.chart, legendItem = series.legendItem, - seriesGroup = series.group, - seriesTracker = series.tracker, - dataLabelsGroup = series.dataLabelsGroup, - markerGroup = series.markerGroup, showOrHide, - i, - points = series.points, - point, ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries, oldVisibility = series.visible; // if called without an argument, toggle visibility - series.visible = vis = vis === UNDEFINED ? !oldVisibility : vis; + series.visible = vis = series.userOptions.visible = vis === UNDEFINED ? !oldVisibility : vis; showOrHide = vis ? 'show' : 'hide'; - // show or hide series - if (seriesGroup) { // pies don't have one - seriesGroup[showOrHide](); - } - if (markerGroup) { - markerGroup[showOrHide](); - } - - // show or hide trackers - if (seriesTracker) { - seriesTracker[showOrHide](); - } else if (points) { - i = points.length; - while (i--) { - point = points[i]; - if (point.tracker) { - point.tracker[showOrHide](); - } + // show or hide elements + each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker'], function (key) { + if (series[key]) { + series[key][showOrHide](); } - } + }); - - if (dataLabelsGroup) { - dataLabelsGroup[showOrHide](); + + // hide tooltip (#1361) + if (chart.hoverSeries === series) { + series.onMouseOut(); } + if (legendItem) { chart.legend.colorizeItem(series, vis); } @@ -26204,10 +27250,15 @@ otherSeries.isDirty = true; } }); } + // show or hide linked series + each(series.linkedSeries, function (otherSeries) { + otherSeries.setVisible(vis, false); + }); + if (ignoreHiddenSeries) { chart.isDirtyBox = true; } if (redraw !== false) { chart.redraw(); @@ -26260,19 +27311,24 @@ options = series.options, trackByArea = options.trackByArea, trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath), trackerPathLength = trackerPath.length, chart = series.chart, + pointer = chart.pointer, renderer = chart.renderer, snap = chart.options.tooltip.snap, tracker = series.tracker, cursor = options.cursor, css = cursor && { cursor: cursor }, singlePoints = series.singlePoints, - trackerGroup = this.isCartesian && this.plotGroup('trackerGroup', null, VISIBLE, options.zIndex || 1, chart.trackerGroup), singlePoint, - i; + i, + onMouseOver = function () { + if (chart.hoverSeries !== series) { + series.onMouseOver(); + } + }; // Extend end points. A better way would be to use round linecaps, // but those are not clickable in VML. if (trackerPathLength && !trackByArea) { i = trackerPathLength + 1; @@ -26299,31 +27355,29 @@ if (tracker) { tracker.attr({ d: trackerPath }); } else { // create - series.tracker = renderer.path(trackerPath) + series.tracker = tracker = renderer.path(trackerPath) .attr({ - isTracker: true, - 'stroke-linejoin': 'bevel', + 'class': PREFIX + 'tracker', + 'stroke-linejoin': 'round', // #1225 visibility: series.visible ? VISIBLE : HIDDEN, stroke: TRACKER_FILL, fill: trackByArea ? TRACKER_FILL : NONE, - 'stroke-width' : options.lineWidth + (trackByArea ? 0 : 2 * snap) + 'stroke-width' : options.lineWidth + (trackByArea ? 0 : 2 * snap), + zIndex: 2 }) - .on(hasTouch ? 'touchstart' : 'mouseover', function () { - if (chart.hoverSeries !== series) { - series.onMouseOver(); - } - }) - .on('mouseout', function () { - if (!options.stickyTracking) { - series.onMouseOut(); - } - }) + .addClass(PREFIX + 'tracker') + .on('mouseover', onMouseOver) + .on('mouseout', function (e) { pointer.onTrackerMouseOut(e); }) .css(css) - .add(trackerGroup); + .add(series.markerGroup); + + if (hasTouch) { + tracker.on('touchstart', onMouseOver); + } } } }; // end Series prototype @@ -26351,10 +27405,77 @@ */ var AreaSeries = extendClass(Series, { type: 'area', /** + * For stacks, don't split segments on null values. Instead, draw null values with + * no marker. Also insert dummy points for any X position that exists in other series + * in the stack. + */ + getSegments: function () { + var segments = [], + segment = [], + keys = [], + xAxis = this.xAxis, + yAxis = this.yAxis, + stack = yAxis.stacks[this.stackKey], + pointMap = {}, + plotX, + plotY, + points = this.points, + i, + x; + + if (this.options.stacking && !this.cropped) { // cropped causes artefacts in Stock, and perf issue + // Create a map where we can quickly look up the points by their X value. + for (i = 0; i < points.length; i++) { + pointMap[points[i].x] = points[i]; + } + + // Sort the keys (#1651) + for (x in stack) { + keys.push(+x); + } + keys.sort(function (a, b) { + return a - b; + }); + + each(keys, function (x) { + // The point exists, push it to the segment + if (pointMap[x]) { + segment.push(pointMap[x]); + + // There is no point for this X value in this series, so we + // insert a dummy point in order for the areas to be drawn + // correctly. + } else { + plotX = xAxis.translate(x); + plotY = yAxis.toPixels(stack[x].cum, true); + segment.push({ + y: null, + plotX: plotX, + clientX: plotX, + plotY: plotY, + yBottom: plotY, + onMouseOver: noop + }); + } + }); + + if (segment.length) { + segments.push(segment); + } + + } else { + Series.prototype.getSegments.call(this); + segments = this.segments; + } + + this.segments = segments; + }, + + /** * Extend the base Series getSegmentPath method by adding the path for the area. * This path is pushed to the series.areaPath property. */ getSegmentPath: function (segment) { @@ -26418,28 +27539,39 @@ // Call the base method Series.prototype.drawGraph.apply(this); // Define local variables - var areaPath = this.areaPath, + var series = this, + areaPath = this.areaPath, options = this.options, - area = this.area; + negativeColor = options.negativeColor, + props = [['area', this.color, options.fillColor]]; // area name, main color, fill color - // Create or update the area - if (area) { // update - area.animate({ d: areaPath }); - - } else { // create - this.area = this.chart.renderer.path(areaPath) - .attr({ - fill: pick( - options.fillColor, - Color(this.color).setOpacity(options.fillOpacity || 0.75).get() - ), - zIndex: 0 // #1069 - }).add(this.group); + if (negativeColor) { + props.push(['areaNeg', options.negativeColor, options.negativeFillColor]); } + + each(props, function (prop) { + var areaKey = prop[0], + area = series[areaKey]; + + // Create or update the area + if (area) { // update + area.animate({ d: areaPath }); + + } else { // create + series[areaKey] = series.chart.renderer.path(areaPath) + .attr({ + fill: pick( + prop[2], + Color(prop[1]).setOpacity(options.fillOpacity || 0.75).get() + ), + zIndex: 0 // #1069 + }).add(series.group); + } + }); }, /** * Get the series' symbol in the legend * @@ -26562,11 +27694,11 @@ stroke: 'green', 'stroke-width': 1 }) .add(); } - // */ + */ // moveTo or lineTo if (!i) { ret = [M, plotX, plotY]; } else { // curve from last point to this @@ -26636,25 +27768,28 @@ dataLabels: { align: null, // auto verticalAlign: null, // auto y: null }, + stickyTracking: false, threshold: 0 }); /** * ColumnSeries object */ var ColumnSeries = extendClass(Series, { type: 'column', tooltipOutsidePlot: true, + requireSorting: false, pointAttrToOptions: { // mapping between SVG attributes and the corresponding options stroke: 'borderColor', 'stroke-width': 'borderWidth', fill: 'color', r: 'borderRadius' }, + trackerGroups: ['group', 'dataLabelsGroup'], /** * Initialize the series */ init: function () { @@ -26673,33 +27808,30 @@ }); } }, /** - * Translate each point to the plot area coordinate system and find shape positions + * Return the width and x offset of the columns adjusted for grouping, groupPadding, pointPadding, + * pointWidth etc. */ - translate: function () { + getColumnMetrics: function () { + var series = this, chart = series.chart, options = series.options, - stacking = options.stacking, - borderWidth = options.borderWidth, - columnCount = 0, - xAxis = series.xAxis, + xAxis = this.xAxis, reversedXAxis = xAxis.reversed, - stackGroups = {}, stackKey, - columnIndex; + stackGroups = {}, + columnIndex, + columnCount = 0; - 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 if (options.grouping === false) { columnCount = 1; - } else { each(chart.series, function (otherSeries) { var otherOptions = otherSeries.options; if (otherSeries.type === series.type && otherSeries.visible && series.options.group === otherOptions.group) { // used in Stock charts navigator series @@ -26715,40 +27847,64 @@ otherSeries.columnIndex = columnIndex; } }); } - // 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 points = series.points, - categoryWidth = mathAbs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || 1), + var categoryWidth = mathMin( + mathAbs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || 1), + xAxis.len // #1535 + ), 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), // exact point width, used in polar charts - barW = mathCeil(mathMax(pointWidth, 1 + 2 * borderWidth)), // rounded and postprocessed for border width - colIndex = (reversedXAxis ? columnCount - - series.columnIndex : series.columnIndex) || 0, + colIndex = (reversedXAxis ? + columnCount - (series.columnIndex || 0) : // #1251 + series.columnIndex) || 0, pointXOffset = pointPadding + (groupPadding + colIndex * pointOffsetWidth - (categoryWidth / 2)) * - (reversedXAxis ? -1 : 1), + (reversedXAxis ? -1 : 1); + + // Save it for reading in linked series (Error bars particularly) + return (series.columnMetrics = { + width: pointWidth, + offset: pointXOffset + }); + + }, + + /** + * Translate each point to the plot area coordinate system and find shape positions + */ + translate: function () { + var series = this, + chart = series.chart, + options = series.options, + stacking = options.stacking, + borderWidth = options.borderWidth, + yAxis = series.yAxis, threshold = options.threshold, - translatedThreshold = series.translatedThreshold = series.yAxis.getThreshold(threshold), - minPointLength = pick(options.minPointLength, 5); + translatedThreshold = series.translatedThreshold = yAxis.getThreshold(threshold), + minPointLength = pick(options.minPointLength, 5), + metrics = series.getColumnMetrics(), + pointWidth = metrics.width, + barW = mathCeil(mathMax(pointWidth, 1 + 2 * borderWidth)), // rounded and postprocessed for border width + pointXOffset = metrics.offset; + Series.prototype.translate.apply(series); + // record the new values - each(points, function (point) { - var plotY = point.plotY, + each(series.points, function (point) { + var plotY = mathMin(mathMax(-999, point.plotY), yAxis.len + 999), // Don't draw too far outside plot area (#1303) yBottom = pick(point.yBottom, translatedThreshold), barX = point.plotX + pointXOffset, barY = mathCeil(mathMin(plotY, yBottom)), barH = mathCeil(mathMax(plotY, yBottom) - barY), - stack = series.yAxis.stacks[(point.y < 0 ? '-' : '') + series.stackKey], + stack = yAxis.stacks[(point.y < 0 ? '-' : '') + series.stackKey], shapeArgs; // Record the offset'ed position and width of the bar to be able to align the stacking total correctly if (stacking && series.visible && stack && stack[point.x]) { stack[point.x].setOffset(pointXOffset, barW); @@ -26759,11 +27915,11 @@ if (minPointLength) { barH = minPointLength; barY = mathAbs(barY - translatedThreshold) > minPointLength ? // stacked yBottom - minPointLength : // keep position - translatedThreshold - (plotY <= translatedThreshold ? minPointLength : 0); + translatedThreshold - (yAxis.translate(point.y, 0, 1, 0, 1) <= translatedThreshold ? minPointLength : 0); // use exact yAxis.translation (#1485) } } point.barX = barX; point.pointWidth = pointWidth; @@ -26775,15 +27931,10 @@ if (borderWidth % 2) { // correct for shorting in crisp method, visible in stacked columns with 1px border shapeArgs.y -= 1; shapeArgs.height += 1; } - // make small columns responsive to mouse - point.trackerArgs = mathAbs(barH) < 3 && merge(point.shapeArgs, { - height: 6, - y: barY - 3 - }); }); }, getSymbol: noop, @@ -26813,12 +27964,14 @@ // draw the columns each(series.points, function (point) { var plotY = point.plotY, graphic = point.graphic; + if (plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) { shapeArgs = point.shapeArgs; + if (graphic) { // update stop(graphic); graphic.animate(merge(shapeArgs)); } else { @@ -26831,82 +27984,78 @@ } else if (graphic) { point.graphic = graphic.destroy(); // #1269 } }); }, + /** - * Draw the individual tracker elements. - * This method is inherited by scatter and pie charts too. + * Add tracking event listener to the series group, so the point graphics + * themselves act as trackers */ drawTracker: function () { var series = this, - chart = series.chart, - renderer = chart.renderer, - shapeArgs, - tracker, - trackerLabel = +new Date(), - options = series.options, - cursor = options.cursor, + pointer = series.chart.pointer, + cursor = series.options.cursor, css = cursor && { cursor: cursor }, - trackerGroup = series.isCartesian && series.plotGroup('trackerGroup', null, VISIBLE, options.zIndex || 1, chart.trackerGroup), - rel, - plotY, - validPlotY; - - each(series.points, function (point) { - tracker = point.tracker; - shapeArgs = point.trackerArgs || point.shapeArgs; - plotY = point.plotY; - validPlotY = !series.isCartesian || (plotY !== UNDEFINED && !isNaN(plotY)); - delete shapeArgs.strokeWidth; - if (point.y !== null && validPlotY) { - if (tracker) {// update - tracker.attr(shapeArgs); + onMouseOver = function (e) { + var target = e.target, + point; - } else { - point.tracker = - renderer[point.shapeType](shapeArgs) - .attr({ - isTracker: trackerLabel, - fill: TRACKER_FILL, - visibility: series.visible ? VISIBLE : HIDDEN - }) - .on(hasTouch ? 'touchstart' : 'mouseover', function (event) { - rel = event.relatedTarget || event.fromElement; - if (chart.hoverSeries !== series && attr(rel, 'isTracker') !== trackerLabel) { - series.onMouseOver(); - } - point.onMouseOver(); + series.onMouseOver(); - }) - .on('mouseout', function (event) { - if (!options.stickyTracking) { - rel = event.relatedTarget || event.toElement; - if (attr(rel, 'isTracker') !== trackerLabel) { - series.onMouseOut(); - } - } - }) - .css(css) - .add(point.group || trackerGroup); // pies have point group - see issue #118 + while (target && !point) { + point = target.point; + target = target.parentNode; } + if (point !== UNDEFINED) { // undefined on graph in scatterchart + point.onMouseOver(e); + } + }; + + // Add reference to the point + each(series.points, function (point) { + if (point.graphic) { + point.graphic.element.point = point; } + if (point.dataLabel) { + point.dataLabel.element.point = point; + } }); + + // Add the event listeners, we need to do this only once + if (!series._hasTracking) { + each(series.trackerGroups, function (key) { + if (series[key]) { // we don't always have dataLabelsGroup + series[key] + .addClass(PREFIX + 'tracker') + .on('mouseover', onMouseOver) + .on('mouseout', function (e) { pointer.onTrackerMouseOut(e); }) + .css(css); + if (hasTouch) { + series[key].on('touchstart', onMouseOver); + } + } + }); + + } else { + series._hasTracking = true; + } }, /** * Override the basic data label alignment by adjusting for the position of the column */ alignDataLabel: function (point, dataLabel, options, alignTo, isNew) { var chart = this.chart, inverted = chart.inverted, - below = point.below || (point.plotY > (this.translatedThreshold || chart.plotSizeY)), - inside = (this.options.stacking || options.inside); // draw it inside the box? + dlBox = point.dlBox || point.shapeArgs, // data label box for alignment + below = point.below || (point.plotY > pick(this.translatedThreshold, chart.plotSizeY)), + inside = pick(options.inside, !!this.options.stacking); // draw it inside the box? // Align to the column itself, or the top of it - if (point.shapeArgs) { // Area range uses this method but not alignTo - alignTo = merge(point.shapeArgs); + if (dlBox) { // Area range uses this method but not alignTo + alignTo = merge(dlBox); if (inverted) { alignTo = { x: chart.plotWidth - alignTo.y - alignTo.height, y: chart.plotHeight - alignTo.x - alignTo.width, width: alignTo.height, @@ -26946,51 +28095,39 @@ * Animate the column heights one by one from zero * @param {Boolean} init Whether to initialize the animation or run it */ animate: function (init) { var series = this, - points = series.points, - options = series.options; + yAxis = this.yAxis, + options = series.options, + inverted = this.chart.inverted, + attr = {}, + translatedThreshold; - if (!init) { // run the animation - /* - * Note: Ideally the animation should be initialized by calling - * series.group.hide(), and then calling series.group.show() - * after the animation was started. But this rendered the shadows - * invisible in IE8 standards mode. If the columns flicker on large - * datasets, this is the cause. - */ - - each(points, function (point) { - var graphic = point.graphic, - shapeArgs = point.shapeArgs, - yAxis = series.yAxis, - threshold = options.threshold; - - if (graphic) { - // start values - graphic.attr({ - height: 0, - y: defined(threshold) ? - yAxis.getThreshold(threshold) : - yAxis.translate(yAxis.getExtremes().min, 0, 1, 0, 1) - }); - - // animate - graphic.animate({ - height: shapeArgs.height, - y: shapeArgs.y - }, options.animation); + if (hasSVG) { // VML is too slow anyway + if (init) { + attr.scaleY = 0.001; + translatedThreshold = mathMin(yAxis.pos + yAxis.len, mathMax(yAxis.pos, yAxis.toPixels(options.threshold))); + if (inverted) { + attr.translateX = translatedThreshold - yAxis.len; + } else { + attr.translateY = translatedThreshold; } - }); + series.group.attr(attr); + } else { // run the animation + + attr.scaleY = 1; + attr[inverted ? 'translateX' : 'translateY'] = yAxis.pos; + series.group.animate(attr, series.options.animation); - // delete this function to allow it only once - series.animate = null; + // delete this function to allow it only once + series.animate = null; + } } - }, + /** * Remove this series from the chart */ remove: function () { var series = this, @@ -27026,98 +28163,42 @@ /** * Set the default options for scatter */ defaultPlotOptions.scatter = merge(defaultSeriesOptions, { lineWidth: 0, - states: { - hover: { - lineWidth: 0 - } - }, tooltip: { headerFormat: '<span style="font-size: 10px; color:{series.color}">{series.name}</span><br/>', - pointFormat: 'x: <b>{point.x}</b><br/>y: <b>{point.y}</b><br/>' - } + pointFormat: 'x: <b>{point.x}</b><br/>y: <b>{point.y}</b><br/>', + followPointer: true + }, + stickyTracking: false }); /** * The scatter series class */ var ScatterSeries = extendClass(Series, { type: 'scatter', sorted: false, - /** - * Extend the base Series' translate method by adding shape type and - * arguments for the point trackers - */ - translate: function () { - var series = this; + requireSorting: false, + noSharedTooltip: true, + trackerGroups: ['markerGroup'], - Series.prototype.translate.apply(series); - - each(series.points, function (point) { - point.shapeType = 'circle'; - point.shapeArgs = { - x: point.plotX, - y: point.plotY, - r: series.chart.options.tooltip.snap - }; - }); - }, - - /** - * Add tracking event listener to the series group, so the point graphics - * themselves act as trackers - */ - drawTracker: function () { - var series = this, - cursor = series.options.cursor, - css = cursor && { cursor: cursor }, - points = series.points, - i = points.length, - graphic; - - // Set an expando property for the point index, used below - while (i--) { - graphic = points[i].graphic; - if (graphic) { // doesn't exist for null points - graphic.element._i = i; - } - } - - // Add the event listeners, we need to do this only once - if (!series._hasTracking) { - series.markerGroup - .attr({ - isTracker: true - }) - .on(hasTouch ? 'touchstart' : 'mouseover', function (e) { - series.onMouseOver(); - if (e.target._i !== UNDEFINED) { // undefined on graph in scatterchart - points[e.target._i].onMouseOver(); - } - }) - .on('mouseout', function () { - if (!series.options.stickyTracking) { - series.onMouseOut(); - } - }) - .css(css); - } else { - series._hasTracking = true; - } - } + drawTracker: ColumnSeries.prototype.drawTracker, + + setTooltipPoints: noop }); seriesTypes.scatter = ScatterSeries; /** * Set the default options for pie */ defaultPlotOptions.pie = merge(defaultSeriesOptions, { borderColor: '#FFFFFF', borderWidth: 1, - center: ['50%', '50%'], + center: [null, null], + clip: false, colorByPoint: true, // always true for pies dataLabels: { // align: null, // connectorWidth: 1, // connectorColor: point.color, @@ -27128,21 +28209,26 @@ return this.point.name; } // softConnector: true, //y: 0 }, + ignoreHiddenPoint: true, //innerSize: 0, legendType: 'point', marker: null, // point options are specified in the base options - size: '75%', + size: null, showInLegend: false, slicedOffset: 10, states: { hover: { brightness: 0.1, shadow: false } + }, + stickyTracking: false, + tooltip: { + followPointer: true } }); /** * Extended point object for pies @@ -27156,10 +28242,15 @@ Point.prototype.init.apply(this, arguments); var point = this, toggleSlice; + // Disallow negative values (#1530) + if (point.y < 0) { + point.y = null; + } + //visible: options.visible !== false, extend(point, { visible: point.visible !== false, name: pick(point.name, 'Slice') }); @@ -27181,34 +28272,25 @@ */ setVisible: function (vis) { var point = this, series = point.series, chart = series.chart, - tracker = point.tracker, - dataLabel = point.dataLabel, - connector = point.connector, - shadowGroup = point.shadowGroup, method; // if called without an argument, toggle visibility - point.visible = vis = vis === UNDEFINED ? !point.visible : vis; - + point.visible = point.options.visible = vis = vis === UNDEFINED ? !point.visible : vis; + series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data + method = vis ? 'show' : 'hide'; - point.group[method](); - if (tracker) { - tracker[method](); - } - if (dataLabel) { - dataLabel[method](); - } - if (connector) { - connector[method](); - } - if (shadowGroup) { - shadowGroup[method](); - } + // Show and hide associated elements + each(['graphic', 'dataLabel', 'connector', 'shadowGroup'], function (key) { + if (point[key]) { + point[key][method](); + } + }); + if (point.legendItem) { chart.legend.colorizeItem(point, vis); } // Handle ignore hidden slices @@ -27225,26 +28307,28 @@ */ slice: function (sliced, redraw, animation) { var point = this, series = point.series, chart = series.chart, - slicedTranslation = point.slicedTranslation, translation; setAnimation(animation, chart); // redraw is true by default redraw = pick(redraw, true); // if called without an argument, toggle - sliced = point.sliced = defined(sliced) ? sliced : !point.sliced; + point.sliced = point.options.sliced = sliced = defined(sliced) ? sliced : !point.sliced; + series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data - translation = { - translateX: (sliced ? slicedTranslation[0] : chart.plotLeft), - translateY: (sliced ? slicedTranslation[1] : chart.plotTop) + translation = sliced ? point.slicedTranslation : { + translateX: 0, + translateY: 0 }; - point.group.animate(translation); + + point.graphic.animate(translation); + if (point.shadowGroup) { point.shadowGroup.animate(translation); } } @@ -27255,56 +28339,57 @@ */ var PieSeries = { type: 'pie', isCartesian: false, pointClass: PiePoint, + requireSorting: false, + noSharedTooltip: true, + trackerGroups: ['group', 'dataLabelsGroup'], pointAttrToOptions: { // mapping between SVG attributes and the corresponding options stroke: 'borderColor', 'stroke-width': 'borderWidth', fill: 'color' }, /** * Pies have one color each point */ - getColor: function () { - // record first color for use in setData - this.initialColor = this.chart.counters.color; - }, + getColor: noop, /** * Animate the pies in */ - animate: function () { + animate: function (init) { var series = this, - points = series.points; + points = series.points, + startAngleRad = series.startAngleRad; - each(points, function (point) { - var graphic = point.graphic, - args = point.shapeArgs, - up = -mathPI / 2; + if (!init) { + each(points, function (point) { + var graphic = point.graphic, + args = point.shapeArgs; - if (graphic) { - // start values - graphic.attr({ - r: 0, - start: up, - end: up - }); + if (graphic) { + // start values + graphic.attr({ + r: series.center[3] / 2, // animate from inner radius (#779) + start: startAngleRad, + end: startAngleRad + }); - // animate - graphic.animate({ - r: args.r, - start: args.start, - end: args.end - }, series.options.animation); - } - }); + // animate + graphic.animate({ + r: args.r, + start: args.start, + end: args.end + }, series.options.animation); + } + }); - // delete this function to allow it only once - series.animate = null; - + // delete this function to allow it only once + series.animate = null; + } }, /** * Extend the basic setData method by running processData and generatePoints immediately, * in order to access the points from the legend. @@ -27324,48 +28409,50 @@ */ getCenter: function () { var options = this.options, chart = this.chart, - plotWidth = chart.plotWidth, - plotHeight = chart.plotHeight, - positions = options.center.concat([options.size, options.innerSize || 0]), + slicingRoom = 2 * (options.slicedOffset || 0), + handleSlicingRoom, + plotWidth = chart.plotWidth - 2 * slicingRoom, + plotHeight = chart.plotHeight - 2 * slicingRoom, + centerOption = options.center, + positions = [pick(centerOption[0], '50%'), pick(centerOption[1], '50%'), options.size || '100%', options.innerSize || 0], smallestSize = mathMin(plotWidth, plotHeight), - isPercent; + isPercent; return map(positions, function (length, i) { - isPercent = /%$/.test(length); - return isPercent ? + handleSlicingRoom = i < 2 || (i === 2 && isPercent); + return (isPercent ? // i == 0: centerX, relative to width // i == 1: centerY, relative to height // i == 2: size, relative to smallestSize // i == 4: innerSize, relative to smallestSize [plotWidth, plotHeight, smallestSize, smallestSize][i] * pInt(length) / 100 : - length; + length) + (handleSlicingRoom ? slicingRoom : 0); }); }, /** * Do translation for pie slices */ - translate: function () { + translate: function (positions) { this.generatePoints(); var total = 0, series = this, - cumulative = -0.25, // start at top + cumulative = 0, precision = 1000, // issue #172 options = series.options, slicedOffset = options.slicedOffset, connectorOffset = slicedOffset + options.borderWidth, - positions, - chart = series.chart, start, end, angle, + startAngleRad = series.startAngleRad = mathPI / 180 * ((options.startAngle || 0) % 360 - 90), points = series.points, circ = 2 * mathPI, fraction, radiusX, // the x component of the radius vector for a given point radiusY, @@ -27373,12 +28460,16 @@ ignoreHiddenPoint = options.ignoreHiddenPoint, i, len = points.length, point; - // get positions - either an integer or a percentage string must be given - series.center = positions = series.getCenter(); + // Get positions - either an integer or a percentage string must be given. + // If positions are passed as a parameter, we're in a recursive loop for adjusting + // space for data labels. + if (!positions) { + series.center = positions = series.getCenter(); + } // utility for getting the x value from a given y, used for anticollision logic in data labels series.getX = function (y, left) { angle = math.asin((y - positions[1]) / (positions[2] / 2 + labelDistance)); @@ -27399,15 +28490,15 @@ point = points[i]; // set start and end angle fraction = total ? point.y / total : 0; - start = mathRound(cumulative * circ * precision) / precision; + start = mathRound((startAngleRad + (cumulative * circ)) * precision) / precision; if (!ignoreHiddenPoint || point.visible) { cumulative += fraction; } - end = mathRound(cumulative * circ * precision) / precision; + end = mathRound((startAngleRad + (cumulative * circ)) * precision) / precision; // set the shape point.shapeType = 'arc'; point.shapeArgs = { x: positions[0], @@ -27418,34 +28509,41 @@ end: end }; // center for the sliced out slice angle = (end + start) / 2; - point.slicedTranslation = map([ - mathCos(angle) * slicedOffset + chart.plotLeft, - mathSin(angle) * slicedOffset + chart.plotTop - ], mathRound); + if (angle > 0.75 * circ) { + angle -= 2 * mathPI; + } + point.slicedTranslation = { + translateX: mathRound(mathCos(angle) * slicedOffset), + translateY: mathRound(mathSin(angle) * slicedOffset) + }; // set the anchor point for tooltips radiusX = mathCos(angle) * positions[2] / 2; radiusY = mathSin(angle) * positions[2] / 2; point.tooltipPos = [ positions[0] + radiusX * 0.7, positions[1] + radiusY * 0.7 ]; + + point.half = angle < circ / 4 ? 0 : 1; + point.angle = angle; // set the anchor point for data labels + connectorOffset = mathMin(connectorOffset, labelDistance / 2); // #1678 point.labelPos = [ positions[0] + radiusX + mathCos(angle) * labelDistance, // first break of connector positions[1] + radiusY + mathSin(angle) * labelDistance, // a/a positions[0] + radiusX + mathCos(angle) * connectorOffset, // second break, right outside pie positions[1] + radiusY + mathSin(angle) * connectorOffset, // a/a positions[0] + radiusX, // landing point for connector positions[1] + radiusY, // a/a labelDistance < 0 ? // alignment 'center' : - angle < circ / 4 ? 'left' : 'right', // alignment + point.half ? 'right' : 'left', // alignment angle // center angle ]; // API properties point.percentage = fraction * 100; @@ -27455,92 +28553,68 @@ this.setTooltipPoints(); }, - /** - * Render the slices - */ - render: function () { - var series = this; + drawGraph: null, - // cache attributes for shapes - series.getAttribs(); - - this.drawPoints(); - - // draw the mouse tracking area - if (series.options.enableMouseTracking !== false) { - series.drawTracker(); - } - - this.drawDataLabels(); - - if (series.options.animation && series.animate) { - series.animate(); - } - - // (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see - series.isDirty = false; // means data is in accordance with what you see - }, - /** * Draw the data points */ drawPoints: function () { var series = this, chart = series.chart, renderer = chart.renderer, groupTranslation, //center, graphic, - group, + //group, shadow = series.options.shadow, shadowGroup, shapeArgs; + if (shadow && !series.shadowGroup) { + series.shadowGroup = renderer.g('shadow') + .add(series.group); + } + // draw the slices each(series.points, function (point) { graphic = point.graphic; shapeArgs = point.shapeArgs; - group = point.group; shadowGroup = point.shadowGroup; // put the shadow behind all points if (shadow && !shadowGroup) { shadowGroup = point.shadowGroup = renderer.g('shadow') - .attr({ zIndex: 4 }) - .add(); + .add(series.shadowGroup); } - // create the group the first time - if (!group) { - group = point.group = renderer.g('point') - .attr({ zIndex: 5 }) - .add(); - } - // if the point is sliced, use special translation, else use plot area traslation - groupTranslation = point.sliced ? point.slicedTranslation : [chart.plotLeft, chart.plotTop]; - group.translate(groupTranslation[0], groupTranslation[1]); + groupTranslation = point.sliced ? point.slicedTranslation : { + translateX: 0, + translateY: 0 + }; + + //group.translate(groupTranslation[0], groupTranslation[1]); if (shadowGroup) { - shadowGroup.translate(groupTranslation[0], groupTranslation[1]); + shadowGroup.attr(groupTranslation); } // draw the slice if (graphic) { - graphic.animate(shapeArgs); + graphic.animate(extend(shapeArgs, groupTranslation)); } else { point.graphic = graphic = renderer.arc(shapeArgs) .setRadialReference(series.center) - .attr(extend( - point.pointAttr[NORMAL_STATE], - { 'stroke-linejoin': 'round' } - )) - .add(point.group) - .shadow(shadow, shadowGroup); - + .attr( + point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] + ) + .attr({ 'stroke-linejoin': 'round' }) + .attr(groupTranslation) + .add(series.group) + .shadow(shadow, shadowGroup); } // detect point specific visibility if (point.visible === false) { point.setVisible(false); @@ -27559,32 +28633,43 @@ point, chart = series.chart, options = series.options.dataLabels, connectorPadding = pick(options.connectorPadding, 10), connectorWidth = pick(options.connectorWidth, 1), + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, connector, connectorPath, softConnector = pick(options.softConnector, true), distanceOption = options.distance, seriesCenter = series.center, radius = seriesCenter[2] / 2, centerY = seriesCenter[1], outside = distanceOption > 0, dataLabel, + dataLabelWidth, labelPos, labelHeight, halves = [// divide the points into right and left halves for anti collision [], // right [] // left ], x, y, visibility, rankArr, - sort, - i = 2, - j; + i, + j, + overflow = [0, 0, 0, 0], // top, right, bottom, left + sort = function (a, b) { + return b.y - a.y; + }, + sortByAngle = function (points, sign) { + points.sort(function (a, b) { + return a.angle !== undefined && (b.angle - a.angle) * sign; + }); + }; // get out if not enabled if (!options.enabled && !series._hasPointLabels) { return; } @@ -27593,61 +28678,62 @@ Series.prototype.drawDataLabels.apply(series); // arrange points for detection collision each(data, function (point) { if (point.dataLabel) { // it may have been cancelled in the base method (#407) - halves[ - point.labelPos[7] < mathPI / 2 ? 0 : 1 - ].push(point); + halves[point.half].push(point); } }); - halves[1].reverse(); - // define the sorting algorithm - sort = function (a, b) { - return b.y - a.y; - }; - // assume equal label heights - labelHeight = halves[0][0] && halves[0][0].dataLabel && (halves[0][0].dataLabel.getBBox().height || 21); // 21 is for #968 + i = 0; + while (!labelHeight && data[i]) { // #1569 + labelHeight = data[i] && data[i].dataLabel && (data[i].dataLabel.getBBox().height || 21); // 21 is for #968 + i++; + } /* Loop over the points in each half, starting from the top and bottom * of the pie to detect overlapping labels. */ + i = 2; while (i--) { var slots = [], slotsLength, usedSlots = [], points = halves[i], pos, length = points.length, slotIndex; + + // Sort by angle + sortByAngle(points, i - 0.5); // Only do anti-collision when we are outside the pie and have connectors (#856) if (distanceOption > 0) { // build the slots for (pos = centerY - radius - distanceOption; pos <= centerY + radius + distanceOption; pos += labelHeight) { slots.push(pos); + // visualize the slot /* var slotX = series.getX(pos, i) + chart.plotLeft - (i ? 100 : 0), slotY = pos + chart.plotTop; if (!isNaN(slotX)) { - chart.renderer.rect(slotX, slotY - 7, 100, labelHeight) + chart.renderer.rect(slotX, slotY - 7, 100, labelHeight, 1) .attr({ 'stroke-width': 1, stroke: 'silver' }) .add(); chart.renderer.text('Slot '+ (slots.length - 1), slotX, slotY + 4) .attr({ fill: 'silver' }).add(); } - // */ + */ } slotsLength = slots.length; // if there are more values than available slots, remove lowest values if (length > slotsLength) { @@ -27742,63 +28828,186 @@ // and botton slice connectors from touching each other on either side x = options.justify ? seriesCenter[0] + (i ? -1 : 1) * (radius + distanceOption) : series.getX(slotIndex === 0 || slotIndex === slots.length - 1 ? naturalY : y, i); - // move or place the data label - dataLabel - .attr({ - visibility: visibility, - align: labelPos[6] - })[dataLabel.moved ? 'animate' : 'attr']({ - x: x + options.x + - ({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0), - y: y + options.y - 10 // 10 is for the baseline (label vs text) - }); - dataLabel.moved = true; - - // draw the connector - if (outside && connectorWidth) { + + // Record the placement and visibility + dataLabel._attr = { + visibility: visibility, + align: labelPos[6] + }; + dataLabel._pos = { + x: x + options.x + + ({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0), + y: y + options.y - 10 // 10 is for the baseline (label vs text) + }; + dataLabel.connX = x; + dataLabel.connY = y; + + + // Detect overflowing data labels + if (this.options.size === null) { + dataLabelWidth = dataLabel.width; + // Overflow left + if (x - dataLabelWidth < connectorPadding) { + overflow[3] = mathMax(mathRound(dataLabelWidth - x + connectorPadding), overflow[3]); + + // Overflow right + } else if (x + dataLabelWidth > plotWidth - connectorPadding) { + overflow[1] = mathMax(mathRound(x + dataLabelWidth - plotWidth + connectorPadding), overflow[1]); + } + + // Overflow top + if (y - labelHeight / 2 < 0) { + overflow[0] = mathMax(mathRound(-y + labelHeight / 2), overflow[0]); + + // Overflow left + } else if (y + labelHeight / 2 > plotHeight) { + overflow[2] = mathMax(mathRound(y + labelHeight / 2 - plotHeight), overflow[2]); + } + } + } // for each point + } // for each half + + // Do not apply the final placement and draw the connectors until we have verified + // that labels are not spilling over. + if (arrayMax(overflow) === 0 || this.verifyDataLabelOverflow(overflow)) { + + // Place the labels in the final position + this.placeDataLabels(); + + // Draw the connectors + if (outside && connectorWidth) { + each(this.points, function (point) { connector = point.connector; - - connectorPath = softConnector ? [ - M, - x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label - 'C', - x, y, // first break, next to the label - 2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5], - labelPos[2], labelPos[3], // second break - L, - labelPos[4], labelPos[5] // base - ] : [ - M, - x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label - L, - labelPos[2], labelPos[3], // second break - L, - labelPos[4], labelPos[5] // base - ]; - - if (connector) { - connector.animate({ d: connectorPath }); - connector.attr('visibility', visibility); - - } else { - point.connector = connector = series.chart.renderer.path(connectorPath).attr({ - 'stroke-width': connectorWidth, - stroke: options.connectorColor || point.color || '#606060', - visibility: visibility, - zIndex: 3 - }) - .translate(chart.plotLeft, chart.plotTop) - .add(); + labelPos = point.labelPos; + dataLabel = point.dataLabel; + + if (dataLabel && dataLabel._pos) { + visibility = dataLabel._attr.visibility; + x = dataLabel.connX; + y = dataLabel.connY; + connectorPath = softConnector ? [ + M, + x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label + 'C', + x, y, // first break, next to the label + 2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5], + labelPos[2], labelPos[3], // second break + L, + labelPos[4], labelPos[5] // base + ] : [ + M, + x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label + L, + labelPos[2], labelPos[3], // second break + L, + labelPos[4], labelPos[5] // base + ]; + + if (connector) { + connector.animate({ d: connectorPath }); + connector.attr('visibility', visibility); + + } else { + point.connector = connector = series.chart.renderer.path(connectorPath).attr({ + 'stroke-width': connectorWidth, + stroke: options.connectorColor || point.color || '#606060', + visibility: visibility + }) + .add(series.group); + } + } else if (connector) { + point.connector = connector.destroy(); } + }); + } + } + }, + + /** + * Verify whether the data labels are allowed to draw, or we should run more translation and data + * label positioning to keep them inside the plot area. Returns true when data labels are ready + * to draw. + */ + verifyDataLabelOverflow: function (overflow) { + + var center = this.center, + options = this.options, + centerOption = options.center, + minSize = options.minSize || 80, + newSize = minSize, + ret; + + // Handle horizontal size and center + if (centerOption[0] !== null) { // Fixed center + newSize = mathMax(center[2] - mathMax(overflow[1], overflow[3]), minSize); + + } else { // Auto center + newSize = mathMax( + center[2] - overflow[1] - overflow[3], // horizontal overflow + minSize + ); + center[0] += (overflow[3] - overflow[1]) / 2; // horizontal center + } + + // Handle vertical size and center + if (centerOption[1] !== null) { // Fixed center + newSize = mathMax(mathMin(newSize, center[2] - mathMax(overflow[0], overflow[2])), minSize); + + } else { // Auto center + newSize = mathMax( + mathMin( + newSize, + center[2] - overflow[0] - overflow[2] // vertical overflow + ), + minSize + ); + center[1] += (overflow[0] - overflow[2]) / 2; // vertical center + } + + // If the size must be decreased, we need to run translate and drawDataLabels again + if (newSize < center[2]) { + center[2] = newSize; + this.translate(center); + each(this.points, function (point) { + if (point.dataLabel) { + point.dataLabel._pos = null; // reset } - } + }); + this.drawDataLabels(); + + // Else, return true to indicate that the pie and its labels is within the plot area + } else { + ret = true; } + return ret; }, + /** + * Perform the final placement of the data labels after we have verified that they + * fall within the plot area. + */ + placeDataLabels: function () { + each(this.points, function (point) { + var dataLabel = point.dataLabel, + _pos; + + if (dataLabel) { + _pos = dataLabel._pos; + if (_pos) { + dataLabel.attr(dataLabel._attr); + dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos); + dataLabel.moved = true; + } else if (dataLabel) { + dataLabel.attr({ y: -999 }); + } + } + }); + }, + alignDataLabel: noop, /** * Draw point specific tracker objects. Inherit directly from column series. */ @@ -27810,11 +29019,11 @@ drawLegendSymbol: AreaSeries.prototype.drawLegendSymbol, /** * Pies don't have point marker symbols */ - getSymbol: function () {} + getSymbol: noop }; PieSeries = extendClass(Series, PieSeries); seriesTypes.pie = PieSeries; @@ -27822,28 +29031,32 @@ // global variables extend(Highcharts, { // Constructors Axis: Axis, - CanVGRenderer: CanVGRenderer, Chart: Chart, Color: Color, Legend: Legend, - MouseTracker: MouseTracker, + Pointer: Pointer, Point: Point, Tick: Tick, Tooltip: Tooltip, Renderer: Renderer, Series: Series, + SVGElement: SVGElement, SVGRenderer: SVGRenderer, - VMLRenderer: VMLRenderer, // Various + arrayMin: arrayMin, + arrayMax: arrayMax, + charts: charts, dateFormat: dateFormat, + format: format, pathAnim: pathAnim, getOptions: getOptions, hasBidiBug: hasBidiBug, + isTouchDevice: isTouchDevice, numberFormat: numberFormat, seriesTypes: seriesTypes, setOptions: setOptions, addEvent: addEvent, removeEvent: removeEvent, @@ -27860,751 +29073,14 @@ pInt: pInt, wrap: wrap, svg: hasSVG, canvas: useCanVG, vml: !hasSVG && !useCanVG, - product: 'Highcharts', - version: '2.3.3' + product: PRODUCT, + version: VERSION }); }()); -/** - * @license Highcharts JS v2.3.3 (2012-10-04) - * Exporting module - * - * (c) 2010-2011 Torstein Hønsi - * - * License: www.highcharts.com/license - */ - -// JSLint options: -/*global Highcharts, document, window, Math, setTimeout */ - - -(function () { // encapsulate - -// create shortcuts -var HC = Highcharts, - Chart = HC.Chart, - addEvent = HC.addEvent, - removeEvent = HC.removeEvent, - createElement = HC.createElement, - discardElement = HC.discardElement, - css = HC.css, - merge = HC.merge, - each = HC.each, - extend = HC.extend, - math = Math, - mathMax = math.max, - doc = document, - win = window, - hasTouch = doc.documentElement.ontouchstart !== undefined, - M = 'M', - L = 'L', - DIV = 'div', - HIDDEN = 'hidden', - NONE = 'none', - PREFIX = 'highcharts-', - ABSOLUTE = 'absolute', - PX = 'px', - UNDEFINED, - defaultOptions = HC.getOptions(); - - // Add language - extend(defaultOptions.lang, { - downloadPNG: 'Download PNG image', - downloadJPEG: 'Download JPEG image', - downloadPDF: 'Download PDF document', - downloadSVG: 'Download SVG vector image', - exportButtonTitle: 'Export to raster or vector image', - printButtonTitle: 'Print the chart' - }); - -// Buttons and menus are collected in a separate config option set called 'navigation'. -// This can be extended later to add control buttons like zoom and pan right click menus. -defaultOptions.navigation = { - menuStyle: { - border: '1px solid #A0A0A0', - background: '#FFFFFF' - }, - menuItemStyle: { - padding: '0 5px', - background: NONE, - color: '#303030', - fontSize: hasTouch ? '14px' : '11px' - }, - menuItemHoverStyle: { - background: '#4572A5', - color: '#FFFFFF' - }, - - buttonOptions: { - align: 'right', - backgroundColor: { - linearGradient: [0, 0, 0, 20], - stops: [ - [0.4, '#F7F7F7'], - [0.6, '#E3E3E3'] - ] - }, - borderColor: '#B0B0B0', - borderRadius: 3, - borderWidth: 1, - //enabled: true, - height: 20, - hoverBorderColor: '#909090', - hoverSymbolFill: '#81A7CF', - hoverSymbolStroke: '#4572A5', - symbolFill: '#E0E0E0', - //symbolSize: 12, - symbolStroke: '#A0A0A0', - //symbolStrokeWidth: 1, - symbolX: 11.5, - symbolY: 10.5, - verticalAlign: 'top', - width: 24, - y: 10 - } -}; - - - -// Add the export related options -defaultOptions.exporting = { - //enabled: true, - //filename: 'chart', - type: 'image/png', - url: 'http://export.highcharts.com/', - width: 800, - buttons: { - exportButton: { - //enabled: true, - symbol: 'exportIcon', - x: -10, - symbolFill: '#A8BF77', - hoverSymbolFill: '#768F3E', - _id: 'exportButton', - _titleKey: 'exportButtonTitle', - menuItems: [{ - textKey: 'downloadPNG', - onclick: function () { - this.exportChart(); - } - }, { - textKey: 'downloadJPEG', - onclick: function () { - this.exportChart({ - type: 'image/jpeg' - }); - } - }, { - textKey: 'downloadPDF', - onclick: function () { - this.exportChart({ - type: 'application/pdf' - }); - } - }, { - textKey: 'downloadSVG', - onclick: function () { - this.exportChart({ - type: 'image/svg+xml' - }); - } - } - // Enable this block to add "View SVG" to the dropdown menu - /* - ,{ - - text: 'View SVG', - onclick: function () { - var svg = this.getSVG() - .replace(/</g, '\n&lt;') - .replace(/>/g, '&gt;'); - - doc.body.innerHTML = '<pre>' + svg + '</pre>'; - } - } // */ - ] - - }, - printButton: { - //enabled: true, - symbol: 'printIcon', - x: -36, - symbolFill: '#B5C9DF', - hoverSymbolFill: '#779ABF', - _id: 'printButton', - _titleKey: 'printButtonTitle', - onclick: function () { - this.print(); - } - } - } -}; - - - -extend(Chart.prototype, { - /** - * Return an SVG representation of the chart - * - * @param additionalOptions {Object} Additional chart options for the generated SVG representation - */ - getSVG: function (additionalOptions) { - var chart = this, - chartCopy, - sandbox, - svg, - seriesOptions, - options = merge(chart.options, additionalOptions); // copy the options and add extra options - - // IE compatibility hack for generating SVG content that it doesn't really understand - if (!doc.createElementNS) { - /*jslint unparam: true*//* allow unused parameter ns in function below */ - doc.createElementNS = function (ns, tagName) { - return doc.createElement(tagName); - }; - /*jslint unparam: false*/ - } - - // create a sandbox where a new chart will be generated - sandbox = createElement(DIV, null, { - position: ABSOLUTE, - top: '-9999em', - width: chart.chartWidth + PX, - height: chart.chartHeight + PX - }, doc.body); - - // override some options - extend(options.chart, { - renderTo: sandbox, - forExport: true - }); - options.exporting.enabled = false; // hide buttons in print - options.chart.plotBackgroundImage = null; // the converter doesn't handle images - - // prepare for replicating the chart - options.series = []; - each(chart.series, function (serie) { - seriesOptions = merge(serie.options, { - animation: false, // turn off animation - showCheckbox: false, - visible: serie.visible - }); - - if (!seriesOptions.isInternal) { // used for the navigator series that has its own option set - - // remove image markers - if (seriesOptions && seriesOptions.marker && /^url\(/.test(seriesOptions.marker.symbol)) { - seriesOptions.marker.symbol = 'circle'; - } - - options.series.push(seriesOptions); - } - }); - - // generate the chart copy - chartCopy = new Highcharts.Chart(options); - - // reflect axis extremes in the export - each(['xAxis', 'yAxis'], function (axisType) { - each(chart[axisType], function (axis, i) { - var axisCopy = chartCopy[axisType][i], - extremes = axis.getExtremes(), - userMin = extremes.userMin, - userMax = extremes.userMax; - - if (userMin !== UNDEFINED || userMax !== UNDEFINED) { - axisCopy.setExtremes(userMin, userMax, true, false); - } - }); - }); - - // get the SVG from the container's innerHTML - svg = chartCopy.container.innerHTML; - - // free up memory - options = null; - chartCopy.destroy(); - discardElement(sandbox); - - // sanitize - svg = svg - .replace(/zIndex="[^"]+"/g, '') - .replace(/isShadow="[^"]+"/g, '') - .replace(/symbolName="[^"]+"/g, '') - .replace(/jQuery[0-9]+="[^"]+"/g, '') - .replace(/isTracker="[^"]+"/g, '') - .replace(/url\([^#]+#/g, 'url(#') - .replace(/<svg /, '<svg xmlns:xlink="http://www.w3.org/1999/xlink" ') - .replace(/ href=/g, ' xlink:href=') - .replace(/\n/, ' ') - .replace(/<\/svg>.*?$/, '</svg>') // any HTML added to the container after the SVG (#894) - /* This fails in IE < 8 - .replace(/([0-9]+)\.([0-9]+)/g, function(s1, s2, s3) { // round off to save weight - return s2 +'.'+ s3[0]; - })*/ - - // Replace HTML entities, issue #347 - .replace(/&nbsp;/g, '\u00A0') // no-break space - .replace(/&shy;/g, '\u00AD') // soft hyphen - - // IE specific - .replace(/<IMG /g, '<image ') - .replace(/height=([^" ]+)/g, 'height="$1"') - .replace(/width=([^" ]+)/g, 'width="$1"') - .replace(/hc-svg-href="([^"]+)">/g, 'xlink:href="$1"/>') - .replace(/id=([^" >]+)/g, 'id="$1"') - .replace(/class=([^" ]+)/g, 'class="$1"') - .replace(/ transform /g, ' ') - .replace(/:(path|rect)/g, '$1') - .replace(/style="([^"]+)"/g, function (s) { - return s.toLowerCase(); - }); - - // IE9 beta bugs with innerHTML. Test again with final IE9. - svg = svg.replace(/(url\(#highcharts-[0-9]+)&quot;/g, '$1') - .replace(/&quot;/g, "'"); - if (svg.match(/ xmlns="/g).length === 2) { - svg = svg.replace(/xmlns="[^"]+"/, ''); - } - - return svg; - }, - - /** - * Submit the SVG representation of the chart to the server - * @param {Object} options Exporting options. Possible members are url, type and width. - * @param {Object} chartOptions Additional chart options for the SVG representation of the chart - */ - exportChart: function (options, chartOptions) { - var form, - chart = this, - svg = chart.getSVG(merge(chart.options.exporting.chartOptions, chartOptions)); // docs - - // merge the options - options = merge(chart.options.exporting, options); - - // create the form - form = createElement('form', { - method: 'post', - action: options.url, - enctype: 'multipart/form-data' - }, { - display: NONE - }, doc.body); - - // add the values - each(['filename', 'type', 'width', 'svg'], function (name) { - createElement('input', { - type: HIDDEN, - name: name, - value: { - filename: options.filename || 'chart', - type: options.type, - width: options.width, - svg: svg - }[name] - }, null, form); - }); - - // submit - form.submit(); - - // clean up - discardElement(form); - }, - - /** - * Print the chart - */ - print: function () { - - var chart = this, - container = chart.container, - origDisplay = [], - origParent = container.parentNode, - body = doc.body, - childNodes = body.childNodes; - - if (chart.isPrinting) { // block the button while in printing mode - return; - } - - chart.isPrinting = true; - - // hide all body content - each(childNodes, function (node, i) { - if (node.nodeType === 1) { - origDisplay[i] = node.style.display; - node.style.display = NONE; - } - }); - - // pull out the chart - body.appendChild(container); - - // print - win.print(); - - // allow the browser to prepare before reverting - setTimeout(function () { - - // put the chart back in - origParent.appendChild(container); - - // restore all body content - each(childNodes, function (node, i) { - if (node.nodeType === 1) { - node.style.display = origDisplay[i]; - } - }); - - chart.isPrinting = false; - - }, 1000); - - }, - - /** - * Display a popup menu for choosing the export type - * - * @param {String} name An identifier for the menu - * @param {Array} items A collection with text and onclicks for the items - * @param {Number} x The x position of the opener button - * @param {Number} y The y position of the opener button - * @param {Number} width The width of the opener button - * @param {Number} height The height of the opener button - */ - contextMenu: function (name, items, x, y, width, height) { - var chart = this, - navOptions = chart.options.navigation, - menuItemStyle = navOptions.menuItemStyle, - chartWidth = chart.chartWidth, - chartHeight = chart.chartHeight, - cacheName = 'cache-' + name, - menu = chart[cacheName], - menuPadding = mathMax(width, height), // for mouse leave detection - boxShadow = '3px 3px 10px #888', - innerMenu, - hide, - menuStyle; - - // create the menu only the first time - if (!menu) { - - // create a HTML element above the SVG - chart[cacheName] = menu = createElement(DIV, { - className: PREFIX + name - }, { - position: ABSOLUTE, - zIndex: 1000, - padding: menuPadding + PX - }, chart.container); - - innerMenu = createElement(DIV, null, - extend({ - MozBoxShadow: boxShadow, - WebkitBoxShadow: boxShadow, - boxShadow: boxShadow - }, navOptions.menuStyle), menu); - - // hide on mouse out - hide = function () { - css(menu, { display: NONE }); - }; - - addEvent(menu, 'mouseleave', hide); - - - // create the items - each(items, function (item) { - if (item) { - var div = createElement(DIV, { - onmouseover: function () { - css(this, navOptions.menuItemHoverStyle); - }, - onmouseout: function () { - css(this, menuItemStyle); - }, - innerHTML: item.text || chart.options.lang[item.textKey] - }, extend({ - cursor: 'pointer' - }, menuItemStyle), innerMenu); - - div[hasTouch ? 'ontouchstart' : 'onclick'] = function () { - hide(); - item.onclick.apply(chart, arguments); - }; - - // Keep references to menu divs to be able to destroy them - chart.exportDivElements.push(div); - } - }); - - // Keep references to menu and innerMenu div to be able to destroy them - chart.exportDivElements.push(innerMenu, menu); - - chart.exportMenuWidth = menu.offsetWidth; - chart.exportMenuHeight = menu.offsetHeight; - } - - menuStyle = { display: 'block' }; - - // if outside right, right align it - if (x + chart.exportMenuWidth > chartWidth) { - menuStyle.right = (chartWidth - x - width - menuPadding) + PX; - } else { - menuStyle.left = (x - menuPadding) + PX; - } - // if outside bottom, bottom align it - if (y + height + chart.exportMenuHeight > chartHeight) { - menuStyle.bottom = (chartHeight - y - menuPadding) + PX; - } else { - menuStyle.top = (y + height - menuPadding) + PX; - } - - css(menu, menuStyle); - }, - - /** - * Add the export button to the chart - */ - addButton: function (options) { - var chart = this, - renderer = chart.renderer, - btnOptions = merge(chart.options.navigation.buttonOptions, options), - onclick = btnOptions.onclick, - menuItems = btnOptions.menuItems, - buttonWidth = btnOptions.width, - buttonHeight = btnOptions.height, - box, - symbol, - button, - borderWidth = btnOptions.borderWidth, - boxAttr = { - stroke: btnOptions.borderColor - - }, - symbolAttr = { - stroke: btnOptions.symbolStroke, - fill: btnOptions.symbolFill - }, - symbolSize = btnOptions.symbolSize || 12; - - // Keeps references to the button elements - if (!chart.exportDivElements) { - chart.exportDivElements = []; - chart.exportSVGElements = []; - } - - if (btnOptions.enabled === false) { - return; - } - - // element to capture the click - function revert() { - symbol.attr(symbolAttr); - box.attr(boxAttr); - } - - // the box border - box = renderer.rect( - 0, - 0, - buttonWidth, - buttonHeight, - btnOptions.borderRadius, - borderWidth - ) - //.translate(buttonLeft, buttonTop) // to allow gradients - .align(btnOptions, true) - .attr(extend({ - fill: btnOptions.backgroundColor, - 'stroke-width': borderWidth, - zIndex: 19 - }, boxAttr)).add(); - - // the invisible element to track the clicks - button = renderer.rect( - 0, - 0, - buttonWidth, - buttonHeight, - 0 - ) - .align(btnOptions) - .attr({ - id: btnOptions._id, - fill: 'rgba(255, 255, 255, 0.001)', - title: chart.options.lang[btnOptions._titleKey], - zIndex: 21 - }).css({ - cursor: 'pointer' - }) - .on('mouseover', function () { - symbol.attr({ - stroke: btnOptions.hoverSymbolStroke, - fill: btnOptions.hoverSymbolFill - }); - box.attr({ - stroke: btnOptions.hoverBorderColor - }); - }) - .on('mouseout', revert) - .on('click', revert) - .add(); - - // add the click event - if (menuItems) { - onclick = function () { - revert(); - var bBox = button.getBBox(); - chart.contextMenu('export-menu', menuItems, bBox.x, bBox.y, buttonWidth, buttonHeight); - }; - } - /*addEvent(button.element, 'click', function() { - onclick.apply(chart, arguments); - });*/ - button.on('click', function () { - onclick.apply(chart, arguments); - }); - - // the icon - symbol = renderer.symbol( - btnOptions.symbol, - btnOptions.symbolX - (symbolSize / 2), - btnOptions.symbolY - (symbolSize / 2), - symbolSize, - symbolSize - ) - .align(btnOptions, true) - .attr(extend(symbolAttr, { - 'stroke-width': btnOptions.symbolStrokeWidth || 1, - zIndex: 20 - })).add(); - - // Keep references to the renderer element so to be able to destroy them later. - chart.exportSVGElements.push(box, button, symbol); - }, - - /** - * Destroy the buttons. - */ - destroyExport: function () { - var i, - chart = this, - elem; - - // Destroy the extra buttons added - for (i = 0; i < chart.exportSVGElements.length; i++) { - elem = chart.exportSVGElements[i]; - // Destroy and null the svg/vml elements - elem.onclick = elem.ontouchstart = null; - chart.exportSVGElements[i] = elem.destroy(); - } - - // Destroy the divs for the menu - for (i = 0; i < chart.exportDivElements.length; i++) { - elem = chart.exportDivElements[i]; - - // Remove the event handler - removeEvent(elem, 'mouseleave'); - - // Remove inline events - chart.exportDivElements[i] = elem.onmouseout = elem.onmouseover = elem.ontouchstart = elem.onclick = null; - - // Destroy the div by moving to garbage bin - discardElement(elem); - } - } -}); - -/** - * Crisp for 1px stroke width, which is default. In the future, consider a smarter, - * global function. - */ -function crisp(arr) { - var i = arr.length; - while (i--) { - if (typeof arr[i] === 'number') { - arr[i] = Math.round(arr[i]) - 0.5; - } - } - return arr; -} - -// Create the export icon -HC.Renderer.prototype.symbols.exportIcon = function (x, y, width, height) { - return crisp([ - M, // the disk - x, y + width, - L, - x + width, y + height, - x + width, y + height * 0.8, - x, y + height * 0.8, - 'Z', - M, // the arrow - x + width * 0.5, y + height * 0.8, - L, - x + width * 0.8, y + height * 0.4, - x + width * 0.4, y + height * 0.4, - x + width * 0.4, y, - x + width * 0.6, y, - x + width * 0.6, y + height * 0.4, - x + width * 0.2, y + height * 0.4, - 'Z' - ]); -}; -// Create the print icon -HC.Renderer.prototype.symbols.printIcon = function (x, y, width, height) { - return crisp([ - M, // the printer - x, y + height * 0.7, - L, - x + width, y + height * 0.7, - x + width, y + height * 0.4, - x, y + height * 0.4, - 'Z', - M, // the upper sheet - x + width * 0.2, y + height * 0.4, - L, - x + width * 0.2, y, - x + width * 0.8, y, - x + width * 0.8, y + height * 0.4, - 'Z', - M, // the lower sheet - x + width * 0.2, y + height * 0.7, - L, - x, y + height, - x + width, y + height, - x + width * 0.8, y + height * 0.7, - 'Z' - ]); -}; - - -// Add the buttons on chart load -Chart.prototype.callbacks.push(function (chart) { - var n, - exportingOptions = chart.options.exporting, - buttons = exportingOptions.buttons; - - if (exportingOptions.enabled !== false) { - - for (n in buttons) { - chart.addButton(buttons[n]); - } - - // Destroy the export elements at chart destroy - addEvent(chart, 'destroy', chart.destroyExport); - } - -}); - - -}()); (function() { this.showWaiting = function(element_id, text, centered) { var element; element = $(element_id); if (element && centered) { @@ -30509,11 +30985,10 @@ // // - // ; // This is a manifest file that'll be compiled into including all the files listed below. // Add new JavaScript/Coffee code in separate files in this directory and they'll automatically @@ -30522,6 +30997,6 @@ // the compiled file. // ; -;TI"required_assets_digest;TI"%6c62dc9f5d15d51a7b16c78353d6f035;FI" _version;TI"%ac1cd7cf9811f9938e2b8937c60a24e6;F +;TI"required_assets_digest;TI"%a84f09da6977aaf09ef5ef8e3fa8c3e6;FI" _version;TI"%ac1cd7cf9811f9938e2b8937c60a24e6;F \ No newline at end of file