app/assets/javascripts/highcharts/modules/map.js in highcharts-rails-3.0.6 vs app/assets/javascripts/highcharts/modules/map.js in highcharts-rails-3.0.7

- old
+ new

@@ -1,27 +1,1273 @@ -/* - Map plugin v0.1 for Highcharts +/** + * @license Map plugin v0.1 for Highcharts + * + * (c) 2011-2013 Torstein Hønsi + * + * License: www.highcharts.com/license + */ - (c) 2011-2013 Torstein Hønsi +/* + * See www.H.com/studies/world-map.htm for use case. + * + * To do: + * - Optimize long variable names and alias adapter methods and Highcharts namespace variables + * - Zoom and pan GUI + */ +/*global HighchartsAdapter*/ +(function (H) { + var UNDEFINED, + Axis = H.Axis, + Chart = H.Chart, + Point = H.Point, + Pointer = H.Pointer, + SVGRenderer = H.SVGRenderer, + VMLRenderer = H.VMLRenderer, + symbols = SVGRenderer.prototype.symbols, + each = H.each, + extend = H.extend, + extendClass = H.extendClass, + merge = H.merge, + pick = H.pick, + numberFormat = H.numberFormat, + defaultOptions = H.getOptions(), + seriesTypes = H.seriesTypes, + inArray = HighchartsAdapter.inArray, + plotOptions = defaultOptions.plotOptions, + wrap = H.wrap, + Color = H.Color, + noop = function () {}; - License: www.highcharts.com/license -*/ -(function(g){function x(a,b,c){for(var d=4,e=[];d--;)e[d]=Math.round(b.rgba[d]+(a.rgba[d]-b.rgba[d])*(1-c));return"rgba("+e.join(",")+")"}var r=g.Axis,y=g.Chart,s=g.Point,z=g.Pointer,l=g.each,v=g.extend,p=g.merge,n=g.pick,A=g.numberFormat,B=g.getOptions(),k=g.seriesTypes,q=B.plotOptions,t=g.wrap,u=g.Color,w=function(){};B.mapNavigation={buttonOptions:{align:"right",verticalAlign:"bottom",x:0,width:18,height:18,style:{fontSize:"15px",fontWeight:"bold",textAlign:"center"}},buttons:{zoomIn:{onclick:function(){this.mapZoom(0.5)}, -text:"+",y:-32},zoomOut:{onclick:function(){this.mapZoom(2)},text:"-",y:0}}};g.splitPath=function(a){var b,a=a.replace(/([A-Za-z])/g," $1 "),a=a.replace(/^\s*/,"").replace(/\s*$/,""),a=a.split(/[ ,]+/);for(b=0;b<a.length;b++)/[a-zA-Z]/.test(a[b])||(a[b]=parseFloat(a[b]));return a};g.maps={};t(r.prototype,"getSeriesExtremes",function(a){var b=this.isXAxis,c,d,e=[];l(this.series,function(a,b){if(a.useMapGeometry)e[b]=a.xData,a.xData=[]});a.call(this);c=n(this.dataMin,Number.MAX_VALUE);d=n(this.dataMax, -Number.MIN_VALUE);l(this.series,function(a,i){if(a.useMapGeometry)c=Math.min(c,a[b?"minX":"minY"]),d=Math.max(d,a[b?"maxX":"maxY"]),a.xData=e[i]});this.dataMin=c;this.dataMax=d});t(r.prototype,"setAxisTranslation",function(a){var b=this.chart,c=b.plotWidth/b.plotHeight,d=this.isXAxis,e=b.xAxis[0];a.call(this);if(b.options.chart.type==="map"&&!d&&e.transA!==void 0)this.transA=e.transA=Math.min(this.transA,e.transA),a=(e.max-e.min)/(this.max-this.min),e=a>c?this:e,c=(e.max-e.min)*e.transA,e.minPixelPadding= -(e.len-c)/2});t(y.prototype,"render",function(a){var b=this,c=b.options.mapNavigation;a.call(b);b.renderMapNavigation();c.zoomOnDoubleClick&&g.addEvent(b.container,"dblclick",function(a){b.pointer.onContainerDblClick(a)});c.zoomOnMouseWheel&&g.addEvent(b.container,document.onmousewheel===void 0?"DOMMouseScroll":"mousewheel",function(a){b.pointer.onContainerMouseWheel(a)})});v(z.prototype,{onContainerDblClick:function(a){var b=this.chart,a=this.normalize(a);b.isInsidePlot(a.chartX-b.plotLeft,a.chartY- -b.plotTop)&&b.mapZoom(0.5,b.xAxis[0].toValue(a.chartX),b.yAxis[0].toValue(a.chartY))},onContainerMouseWheel:function(a){var b=this.chart,c,a=this.normalize(a);c=a.detail||-(a.wheelDelta/120);b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop)&&b.mapZoom(c>0?2:0.5,b.xAxis[0].toValue(a.chartX),b.yAxis[0].toValue(a.chartY))}});t(z.prototype,"init",function(a,b,c){a.call(this,b,c);if(c.mapNavigation.enableTouchZoom)this.pinchX=this.pinchHor=this.pinchY=this.pinchVert=!0});v(y.prototype,{renderMapNavigation:function(){var a= -this,b=this.options.mapNavigation,c=b.buttons,d,e,f,i=function(){this.handler.call(a)};if(b.enableButtons)for(d in c)if(c.hasOwnProperty(d))f=p(b.buttonOptions,c[d]),e=a.renderer.button(f.text,0,0,i).attr({width:f.width,height:f.height}).css(f.style).add(),e.handler=f.onclick,e.align(v(f,{width:e.width,height:e.height}),null,"spacingBox")},fitToBox:function(a,b){l([["x","width"],["y","height"]],function(c){var d=c[0],c=c[1];a[d]+a[c]>b[d]+b[c]&&(a[c]>b[c]?(a[c]=b[c],a[d]=b[d]):a[d]=b[d]+b[c]-a[c]); -a[c]>b[c]&&(a[c]=b[c]);a[d]<b[d]&&(a[d]=b[d])});return a},mapZoom:function(a,b,c){if(!this.isMapZooming){var d=this,e=d.xAxis[0],f=e.max-e.min,i=n(b,e.min+f/2),b=f*a,f=d.yAxis[0],h=f.max-f.min,c=n(c,f.min+h/2);a*=h;i-=b/2;h=c-a/2;c=n(d.options.chart.animation,!0);b=d.fitToBox({x:i,y:h,width:b,height:a},{x:e.dataMin,y:f.dataMin,width:e.dataMax-e.dataMin,height:f.dataMax-f.dataMin});e.setExtremes(b.x,b.x+b.width,!1);f.setExtremes(b.y,b.y+b.height,!1);if(e=c?c.duration||500:0)d.isMapZooming=!0,setTimeout(function(){d.isMapZooming= -!1},e);d.redraw()}}});q.map=p(q.scatter,{animation:!1,nullColor:"#F8F8F8",borderColor:"silver",borderWidth:1,marker:null,stickyTracking:!1,dataLabels:{verticalAlign:"middle"},turboThreshold:0,tooltip:{followPointer:!0,pointFormat:"{point.name}: {point.y}<br/>"},states:{normal:{animation:!0}}});r=g.extendClass(s,{applyOptions:function(a,b){var c=s.prototype.applyOptions.call(this,a,b);if(c.path&&typeof c.path==="string")c.path=c.options.path=g.splitPath(c.path);return c},onMouseOver:function(){clearTimeout(this.colorInterval); -s.prototype.onMouseOver.call(this)},onMouseOut:function(){var a=this,b=+new Date,c=u(a.options.color),d=u(a.pointAttr.hover.fill),e=a.series.options.states.normal.animation,f=e&&(e.duration||500);if(f&&c.rgba.length===4&&d.rgba.length===4)delete a.pointAttr[""].fill,clearTimeout(a.colorInterval),a.colorInterval=setInterval(function(){var e=(new Date-b)/f,h=a.graphic;e>1&&(e=1);h&&h.attr("fill",x(d,c,e));e>=1&&clearTimeout(a.colorInterval)},13);s.prototype.onMouseOut.call(a)}});k.map=g.extendClass(k.scatter, -{type:"map",pointAttrToOptions:{stroke:"borderColor","stroke-width":"borderWidth",fill:"color"},colorKey:"y",pointClass:r,trackerGroups:["group","markerGroup","dataLabelsGroup"],getSymbol:w,supportsDrilldown:!0,getExtremesFromAll:!0,useMapGeometry:!0,init:function(a){var b=this,c=a.options.legend.valueDecimals,d=[],e,f,i,h,j,o,m;o=a.options.legend.layout==="horizontal";g.Series.prototype.init.apply(this,arguments);j=b.options.colorRange;if(h=b.options.valueRanges)l(h,function(a){f=a.from;i=a.to;e= -"";f===void 0?e="< ":i===void 0&&(e="> ");f!==void 0&&(e+=A(f,c));f!==void 0&&i!==void 0&&(e+=" - ");i!==void 0&&(e+=A(i,c));d.push(g.extend({chart:b.chart,name:e,options:{},drawLegendSymbol:k.area.prototype.drawLegendSymbol,visible:!0,setState:function(){},setVisible:function(){}},a))}),b.legendItems=d;else if(j)f=j.from,i=j.to,h=j.fromLabel,j=j.toLabel,m=o?[0,0,1,0]:[0,1,0,0],o||(o=h,h=j,j=o),o={linearGradient:{x1:m[0],y1:m[1],x2:m[2],y2:m[3]},stops:[[0,f],[1,i]]},d=[{chart:b.chart,options:{},fromLabel:h, -toLabel:j,color:o,drawLegendSymbol:this.drawLegendSymbolGradient,visible:!0,setState:function(){},setVisible:function(){}}],b.legendItems=d},drawLegendSymbol:k.area.prototype.drawLegendSymbol,drawLegendSymbolGradient:function(a,b){var c=a.options.symbolPadding,d=n(a.options.padding,8),e,f,i=this.chart.renderer.fontMetrics(a.options.itemStyle.fontSize).h,h=a.options.layout==="horizontal",j;j=n(a.options.rectangleLength,200);h?(e=-(c/2),f=0):(e=-j+a.baseline-c/2,f=d+i);b.fromText=this.chart.renderer.text(b.fromLabel, -f,e).attr({zIndex:2}).add(b.legendGroup);f=b.fromText.getBBox();b.legendSymbol=this.chart.renderer.rect(h?f.x+f.width+c:f.x-i-c,f.y,h?j:i,h?i:j,2).attr({zIndex:1}).add(b.legendGroup);j=b.legendSymbol.getBBox();b.toText=this.chart.renderer.text(b.toLabel,j.x+j.width+c,h?e:j.y+j.height-c).attr({zIndex:2}).add(b.legendGroup);e=b.toText.getBBox();h?(a.offsetWidth=f.width+j.width+e.width+c*2+d,a.itemY=i+d):(a.offsetWidth=Math.max(f.width,e.width)+c+j.width+d,a.itemY=j.height+d,a.itemX=c)},getBox:function(a){var b= -Number.MIN_VALUE,c=Number.MAX_VALUE,d=Number.MIN_VALUE,e=Number.MAX_VALUE;l(a||this.options.data,function(a){for(var i=a.path,h=i.length,j=!1,g=Number.MIN_VALUE,m=Number.MAX_VALUE,k=Number.MIN_VALUE,l=Number.MAX_VALUE;h--;)typeof i[h]==="number"&&!isNaN(i[h])&&(j?(g=Math.max(g,i[h]),m=Math.min(m,i[h])):(k=Math.max(k,i[h]),l=Math.min(l,i[h])),j=!j);a._maxX=g;a._minX=m;a._maxY=k;a._minY=l;b=Math.max(b,g);c=Math.min(c,m);d=Math.max(d,k);e=Math.min(e,l)});this.minY=e;this.maxY=d;this.minX=c;this.maxX= -b},translatePath:function(a){var b=!1,c=this.xAxis,d=this.yAxis,e,a=[].concat(a);for(e=a.length;e--;)typeof a[e]==="number"&&(a[e]=b?Math.round(c.translate(a[e])):Math.round(d.len-d.translate(a[e])),b=!b);return a},setData:function(){g.Series.prototype.setData.apply(this,arguments);this.getBox()},translate:function(){var a=this,b=Number.MAX_VALUE,c=Number.MIN_VALUE;a.generatePoints();l(a.data,function(d){d.shapeType="path";d.shapeArgs={d:a.translatePath(d.path)};if(typeof d.y==="number")if(d.y>c)c= -d.y;else if(d.y<b)b=d.y});a.translateColors(b,c)},translateColors:function(a,b){var c=this.options,d=c.valueRanges,e=c.colorRange,f=this.colorKey,i,h;e&&(i=u(e.from),h=u(e.to));l(this.data,function(g){var k=g[f],m,l,n;if(d)for(n=d.length;n--;){if(m=d[n],i=m.from,h=m.to,(i===void 0||k>=i)&&(h===void 0||k<=h)){l=m.color;break}}else e&&k!==void 0&&(m=1-(b-k)/(b-a),l=k===null?c.nullColor:x(i,h,m));if(l)g.color=null,g.options.color=l})},drawGraph:w,drawDataLabels:w,drawPoints:function(){var a=this.xAxis, -b=this.yAxis,c=this.colorKey;l(this.data,function(a){a.plotY=1;if(a[c]===null)a[c]=0,a.isNull=!0});k.column.prototype.drawPoints.apply(this);l(this.data,function(d){var e=d.dataLabels,f=a.toPixels(d._minX,!0),g=a.toPixels(d._maxX,!0),h=b.toPixels(d._minY,!0),j=b.toPixels(d._maxY,!0);d.plotX=Math.round(f+(g-f)*n(e&&e.anchorX,0.5));d.plotY=Math.round(h+(j-h)*n(e&&e.anchorY,0.5));d.isNull&&(d[c]=null)});g.Series.prototype.drawDataLabels.call(this)},animateDrilldown:function(a){var b=this.chart.plotBox, -c=this.chart.drilldownLevels[this.chart.drilldownLevels.length-1],d=c.bBox,e=this.chart.options.drilldown.animation;if(!a)a=Math.min(d.width/b.width,d.height/b.height),c.shapeArgs={scaleX:a,scaleY:a,translateX:d.x,translateY:d.y},l(this.points,function(a){a.graphic.attr(c.shapeArgs).animate({scaleX:1,scaleY:1,translateX:0,translateY:0},e)}),delete this.animate},animateDrillupFrom:function(a){k.column.prototype.animateDrillupFrom.call(this,a)},animateDrillupTo:function(a){k.column.prototype.animateDrillupTo.call(this, -a)}});q.mapline=p(q.map,{lineWidth:1,backgroundColor:"none"});k.mapline=g.extendClass(k.map,{type:"mapline",pointAttrToOptions:{stroke:"color","stroke-width":"lineWidth",fill:"backgroundColor"},drawLegendSymbol:k.line.prototype.drawLegendSymbol});q.mappoint=p(q.scatter,{dataLabels:{enabled:!0,format:"{point.name}",color:"black",style:{textShadow:"0 0 5px white"}}});k.mappoint=g.extendClass(k.scatter,{type:"mappoint"});g.Map=function(a,b){var c={endOnTick:!1,gridLineWidth:0,labels:{enabled:!1},lineWidth:0, -minPadding:0,maxPadding:0,startOnTick:!1,tickWidth:0,title:null},d;d=a.series;a.series=null;a=p({chart:{type:"map",panning:"xy"},xAxis:c,yAxis:p(c,{reversed:!0})},a,{chart:{inverted:!1}});a.series=d;return new g.Chart(a,b)}})(Highcharts); + // Add language + extend(defaultOptions.lang, { + zoomIn: 'Zoom in', + zoomOut: 'Zoom out' + }); + + /* + * Return an intermediate color between two colors, according to pos where 0 + * is the from color and 1 is the to color + */ + function tweenColors(from, to, pos) { + var i = 4, + val, + rgba = []; + + while (i--) { + val = to.rgba[i] + (from.rgba[i] - to.rgba[i]) * (1 - pos); + rgba[i] = i === 3 ? val : Math.round(val); // Do not round opacity + } + return 'rgba(' + rgba.join(',') + ')'; + } + + // Set the default map navigation options + defaultOptions.mapNavigation = { + buttonOptions: { + alignTo: 'plotBox', + align: 'left', + verticalAlign: 'top', + x: 0, + width: 18, + height: 18, + style: { + fontSize: '15px', + fontWeight: 'bold', + textAlign: 'center' + }, + theme: { + 'stroke-width': 1 + } + }, + buttons: { + zoomIn: { + onclick: function () { + this.mapZoom(0.5); + }, + text: '+', + y: 0 + }, + zoomOut: { + onclick: function () { + this.mapZoom(2); + }, + text: '-', + y: 28 + } + } + // enabled: false, + // enableButtons: null, // inherit from enabled + // enableTouchZoom: null, // inherit from enabled + // enableDoubleClickZoom: null, // inherit from enabled + // enableDoubleClickZoomTo: false + // enableMouseWheelZoom: null, // inherit from enabled + }; + + /** + * Utility for reading SVG paths directly. + */ + H.splitPath = function (path) { + var i; + + // Move letters apart + path = path.replace(/([A-Za-z])/g, ' $1 '); + // Trim + path = path.replace(/^\s*/, "").replace(/\s*$/, ""); + + // Split on spaces and commas + path = path.split(/[ ,]+/); + + // Parse numbers + for (i = 0; i < path.length; i++) { + if (!/[a-zA-Z]/.test(path[i])) { + path[i] = parseFloat(path[i]); + } + } + return path; + }; + + // A placeholder for map definitions + H.maps = {}; + + /** + * Override to use the extreme coordinates from the SVG shape, not the + * data values + */ + wrap(Axis.prototype, 'getSeriesExtremes', function (proceed) { + var isXAxis = this.isXAxis, + dataMin, + dataMax, + xData = []; + + // Remove the xData array and cache it locally so that the proceed method doesn't use it + if (isXAxis) { + each(this.series, function (series, i) { + if (series.useMapGeometry) { + xData[i] = series.xData; + series.xData = []; + } + }); + } + + // Call base to reach normal cartesian series (like mappoint) + proceed.call(this); + + // Run extremes logic for map and mapline + if (isXAxis) { + dataMin = pick(this.dataMin, Number.MAX_VALUE); + dataMax = pick(this.dataMax, Number.MIN_VALUE); + each(this.series, function (series, i) { + if (series.useMapGeometry) { + dataMin = Math.min(dataMin, pick(series.minX, dataMin)); + dataMax = Math.max(dataMax, pick(series.maxX, dataMin)); + series.xData = xData[i]; // Reset xData array + } + }); + + this.dataMin = dataMin; + this.dataMax = dataMax; + } + }); + + /** + * Override axis translation to make sure the aspect ratio is always kept + */ + wrap(Axis.prototype, 'setAxisTranslation', function (proceed) { + var chart = this.chart, + mapRatio, + plotRatio = chart.plotWidth / chart.plotHeight, + isXAxis = this.isXAxis, + adjustedAxisLength, + xAxis = chart.xAxis[0], + padAxis; + + // Run the parent method + proceed.call(this); + + // On Y axis, handle both + if (chart.options.chart.type === 'map' && !isXAxis && xAxis.transA !== UNDEFINED) { + + // Use the same translation for both axes + this.transA = xAxis.transA = Math.min(this.transA, xAxis.transA); + + mapRatio = (xAxis.max - xAxis.min) / (this.max - this.min); + + // What axis to pad to put the map in the middle + padAxis = mapRatio > plotRatio ? this : xAxis; + + // Pad it + adjustedAxisLength = (padAxis.max - padAxis.min) * padAxis.transA; + padAxis.minPixelPadding = (padAxis.len - adjustedAxisLength) / 2; + } + }); + + + //--- Start zooming and panning features + wrap(Chart.prototype, 'render', function (proceed) { + var chart = this, + mapNavigation = chart.options.mapNavigation; + + proceed.call(chart); + + // Render the plus and minus buttons + chart.renderMapNavigation(); + + // Add the double click event + if (pick(mapNavigation.enableDoubleClickZoom, mapNavigation.enabled) || mapNavigation.enableDoubleClickZoomTo) { + H.addEvent(chart.container, 'dblclick', function (e) { + chart.pointer.onContainerDblClick(e); + }); + } + + // Add the mousewheel event + if (pick(mapNavigation.enableMouseWheelZoom, mapNavigation.enabled)) { + H.addEvent(chart.container, document.onmousewheel === undefined ? 'DOMMouseScroll' : 'mousewheel', function (e) { + chart.pointer.onContainerMouseWheel(e); + }); + } + }); + + // Extend the Pointer + extend(Pointer.prototype, { + + /** + * The event handler for the doubleclick event + */ + onContainerDblClick: function (e) { + var chart = this.chart; + + e = this.normalize(e); + + if (chart.options.mapNavigation.enableDoubleClickZoomTo) { + if (chart.pointer.inClass(e.target, 'highcharts-tracker')) { + chart.zoomToShape(chart.hoverPoint); + } + } else if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { + chart.mapZoom( + 0.5, + chart.xAxis[0].toValue(e.chartX), + chart.yAxis[0].toValue(e.chartY) + ); + } + }, + + /** + * The event handler for the mouse scroll event + */ + onContainerMouseWheel: function (e) { + var chart = this.chart, + delta; + + e = this.normalize(e); + + // Firefox uses e.detail, WebKit and IE uses wheelDelta + delta = e.detail || -(e.wheelDelta / 120); + if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { + chart.mapZoom( + delta > 0 ? 2 : 0.5, + chart.xAxis[0].toValue(e.chartX), + chart.yAxis[0].toValue(e.chartY) + ); + } + } + }); + + // Implement the pinchType option + wrap(Pointer.prototype, 'init', function (proceed, chart, options) { + + proceed.call(this, chart, options); + + // Pinch status + if (pick(options.mapNavigation.enableTouchZoom, options.mapNavigation.enabled)) { + this.pinchX = this.pinchHor = + this.pinchY = this.pinchVert = true; + } + }); + + // Extend the pinchTranslate method to preserve fixed ratio when zooming + wrap(Pointer.prototype, 'pinchTranslate', function (proceed, zoomHor, zoomVert, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) { + var xBigger; + + proceed.call(this, zoomHor, zoomVert, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); + + // Keep ratio + if (this.chart.options.chart.type === 'map') { + xBigger = transform.scaleX > transform.scaleY; + this.pinchTranslateDirection( + !xBigger, + pinchDown, + touches, + transform, + selectionMarker, + clip, + lastValidTouch, + xBigger ? transform.scaleX : transform.scaleY + ); + } + }); + + // Add events to the Chart object itself + extend(Chart.prototype, { + renderMapNavigation: function () { + var chart = this, + options = this.options.mapNavigation, + buttons = options.buttons, + n, + button, + buttonOptions, + attr, + states, + outerHandler = function () { + this.handler.call(chart); + }; + + if (pick(options.enableButtons, options.enabled)) { + for (n in buttons) { + if (buttons.hasOwnProperty(n)) { + buttonOptions = merge(options.buttonOptions, buttons[n]); + attr = buttonOptions.theme; + states = attr.states; + button = chart.renderer.button( + buttonOptions.text, + 0, + 0, + outerHandler, + attr, + states && states.hover, + states && states.select, + 0, + n === 'zoomIn' ? 'topbutton' : 'bottombutton' + ) + .attr({ + width: buttonOptions.width, + height: buttonOptions.height, + title: chart.options.lang[n], + zIndex: 5 + }) + .css(buttonOptions.style) + .add(); + button.handler = buttonOptions.onclick; + button.align(extend(buttonOptions, { width: button.width, height: 2 * button.height }), null, buttonOptions.alignTo); + } + } + } + }, + + /** + * Fit an inner box to an outer. If the inner box overflows left or right, align it to the sides of the + * outer. If it overflows both sides, fit it within the outer. This is a pattern that occurs more places + * in Highcharts, perhaps it should be elevated to a common utility function. + */ + fitToBox: function (inner, outer) { + each([['x', 'width'], ['y', 'height']], function (dim) { + var pos = dim[0], + size = dim[1]; + + if (inner[pos] + inner[size] > outer[pos] + outer[size]) { // right overflow + if (inner[size] > outer[size]) { // the general size is greater, fit fully to outer + inner[size] = outer[size]; + inner[pos] = outer[pos]; + } else { // align right + inner[pos] = outer[pos] + outer[size] - inner[size]; + } + } + if (inner[size] > outer[size]) { + inner[size] = outer[size]; + } + if (inner[pos] < outer[pos]) { + inner[pos] = outer[pos]; + } + }); + + + return inner; + }, + + /** + * Zoom the map in or out by a certain amount. Less than 1 zooms in, greater than 1 zooms out. + */ + mapZoom: function (howMuch, centerXArg, centerYArg) { + + if (this.isMapZooming) { + return; + } + + var chart = this, + xAxis = chart.xAxis[0], + xRange = xAxis.max - xAxis.min, + centerX = pick(centerXArg, xAxis.min + xRange / 2), + newXRange = xRange * howMuch, + yAxis = chart.yAxis[0], + yRange = yAxis.max - yAxis.min, + centerY = pick(centerYArg, yAxis.min + yRange / 2), + newYRange = yRange * howMuch, + newXMin = centerX - newXRange / 2, + newYMin = centerY - newYRange / 2, + animation = pick(chart.options.chart.animation, true), + delay, + newExt = chart.fitToBox({ + x: newXMin, + y: newYMin, + width: newXRange, + height: newYRange + }, { + x: xAxis.dataMin, + y: yAxis.dataMin, + width: xAxis.dataMax - xAxis.dataMin, + height: yAxis.dataMax - yAxis.dataMin + }); + + xAxis.setExtremes(newExt.x, newExt.x + newExt.width, false); + yAxis.setExtremes(newExt.y, newExt.y + newExt.height, false); + + // Prevent zooming until this one is finished animating + delay = animation ? animation.duration || 500 : 0; + if (delay) { + chart.isMapZooming = true; + setTimeout(function () { + chart.isMapZooming = false; + }, delay); + } + + + chart.redraw(); + }, + + /** + * Zoom the chart to view a specific area point + */ + zoomToShape: function (point) { + var series = point.series, + chart = series.chart; + + series.xAxis.setExtremes( + point._minX, + point._maxX, + false + ); + series.yAxis.setExtremes( + point._minY, + point._maxY, + false + ); + chart.redraw(); + } + }); + + /** + * Extend the default options with map options + */ + plotOptions.map = merge(plotOptions.scatter, { + animation: false, // makes the complex shapes slow + nullColor: '#F8F8F8', + borderColor: 'silver', + borderWidth: 1, + marker: null, + stickyTracking: false, + dataLabels: { + verticalAlign: 'middle' + }, + turboThreshold: 0, + tooltip: { + followPointer: true, + pointFormat: '{point.name}: {point.y}<br/>' + }, + states: { + normal: { + animation: true + } + } + }); + + var MapAreaPoint = extendClass(Point, { + /** + * Extend the Point object to split paths + */ + applyOptions: function (options, x) { + + var point = Point.prototype.applyOptions.call(this, options, x), + series = this.series, + seriesOptions = series.options, + joinBy = seriesOptions.dataJoinBy, + mapPoint; + + if (joinBy && seriesOptions.mapData) { + mapPoint = series.getMapData(joinBy, point[joinBy]); + + if (mapPoint) { + // This applies only to bubbles + if (series.xyFromShape) { + point.x = mapPoint._midX; + point.y = mapPoint._midY; + } + extend(point, mapPoint); // copy over properties + } else { + point.y = point.y || null; + } + } + + return point; + }, + + /** + * Set the visibility of a single map area + */ + setVisible: function (vis) { + var point = this, + method = vis ? 'show' : 'hide'; + + // Show and hide associated elements + each(['graphic', 'dataLabel'], function (key) { + if (point[key]) { + point[key][method](); + } + }); + }, + + /** + * Stop the fade-out + */ + onMouseOver: function (e) { + clearTimeout(this.colorInterval); + Point.prototype.onMouseOver.call(this, e); + }, + /** + * Custom animation for tweening out the colors. Animation reduces blinking when hovering + * over islands and coast lines. We run a custom implementation of animation becuase we + * need to be able to run this independently from other animations like zoom redraw. Also, + * adding color animation to the adapters would introduce almost the same amount of code. + */ + onMouseOut: function () { + var point = this, + start = +new Date(), + normalColor = Color(point.options.color), + hoverColor = Color(point.pointAttr.hover.fill), + animation = point.series.options.states.normal.animation, + duration = animation && (animation.duration || 500); + + if (duration && normalColor.rgba.length === 4 && hoverColor.rgba.length === 4) { + delete point.pointAttr[''].fill; // avoid resetting it in Point.setState + + clearTimeout(point.colorInterval); + point.colorInterval = setInterval(function () { + var pos = (new Date() - start) / duration, + graphic = point.graphic; + if (pos > 1) { + pos = 1; + } + if (graphic) { + graphic.attr('fill', tweenColors(hoverColor, normalColor, pos)); + } + if (pos >= 1) { + clearTimeout(point.colorInterval); + } + }, 13); + } + Point.prototype.onMouseOut.call(point); + } + }); + + /** + * Add the series type + */ + seriesTypes.map = extendClass(seriesTypes.scatter, { + type: 'map', + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'borderColor', + 'stroke-width': 'borderWidth', + fill: 'color' + }, + colorKey: 'y', + pointClass: MapAreaPoint, + trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'], + getSymbol: noop, + supportsDrilldown: true, + getExtremesFromAll: true, + useMapGeometry: true, // get axis extremes from paths, not values + init: function (chart) { + var series = this, + legendOptions = chart.options.legend, + valueDecimals = legendOptions.valueDecimals, + valueSuffix = legendOptions.valueSuffix || '', + legendItems = [], + name, + from, + to, + fromLabel, + toLabel, + colorRange, + valueRanges, + gradientColor, + grad, + tmpLabel, + horizontal = chart.options.legend.layout === 'horizontal'; + + + H.Series.prototype.init.apply(this, arguments); + colorRange = series.options.colorRange; + valueRanges = series.options.valueRanges; + + if (valueRanges) { + each(valueRanges, function (range, i) { + var vis = true; + from = range.from; + to = range.to; + + // Assemble the default name. This can be overridden by legend.options.labelFormatter + name = ''; + if (from === UNDEFINED) { + name = '< '; + } else if (to === UNDEFINED) { + name = '> '; + } + if (from !== UNDEFINED) { + name += numberFormat(from, valueDecimals) + valueSuffix; + } + if (from !== UNDEFINED && to !== UNDEFINED) { + name += ' - '; + } + if (to !== UNDEFINED) { + name += numberFormat(to, valueDecimals) + valueSuffix; + } + + // Add a mock object to the legend items + legendItems.push(H.extend({ + chart: series.chart, + name: name, + options: {}, + drawLegendSymbol: seriesTypes.area.prototype.drawLegendSymbol, + visible: true, + setState: noop, + setVisible: function () { + vis = this.visible = !vis; + each(series.points, function (point) { + if (point.valueRange === i) { + point.setVisible(vis); + } + }); + + chart.legend.colorizeItem(this, vis); + } + }, range)); + }); + series.legendItems = legendItems; + + } else if (colorRange) { + + from = colorRange.from; + to = colorRange.to; + fromLabel = colorRange.fromLabel; + toLabel = colorRange.toLabel; + + // Flips linearGradient variables and label text. + grad = horizontal ? [0, 0, 1, 0] : [0, 1, 0, 0]; + if (!horizontal) { + tmpLabel = fromLabel; + fromLabel = toLabel; + toLabel = tmpLabel; + } + + // Creates color gradient. + gradientColor = { + linearGradient: { x1: grad[0], y1: grad[1], x2: grad[2], y2: grad[3] }, + stops: + [ + [0, from], + [1, to] + ] + }; + + // Add a mock object to the legend items. + legendItems = [{ + chart: series.chart, + options: {}, + fromLabel: fromLabel, + toLabel: toLabel, + color: gradientColor, + drawLegendSymbol: this.drawLegendSymbolGradient, + visible: true, + setState: noop, + setVisible: noop + }]; + + series.legendItems = legendItems; + } + }, + + /** + * If neither valueRanges nor colorRanges are defined, use basic area symbol. + */ + drawLegendSymbol: seriesTypes.area.prototype.drawLegendSymbol, + + /** + * Gets the series' symbol in the legend and extended legend with more information. + * + * @param {Object} legend The legend object + * @param {Object} item The series (this) or point + */ + drawLegendSymbolGradient: function (legend, item) { + var spacing = legend.options.symbolPadding, + padding = pick(legend.options.padding, 8), + positionY, + positionX, + gradientSize = this.chart.renderer.fontMetrics(legend.options.itemStyle.fontSize).h, + horizontal = legend.options.layout === 'horizontal', + box1, + box2, + box3, + rectangleLength = pick(legend.options.rectangleLength, 200); + + // Set local variables based on option. + if (horizontal) { + positionY = -(spacing / 2); + positionX = 0; + } else { + positionY = -rectangleLength + legend.baseline - (spacing / 2); + positionX = padding + gradientSize; + } + + // Creates the from text. + item.fromText = this.chart.renderer.text( + item.fromLabel, // Text. + positionX, // Lower left x. + positionY // Lower left y. + ).attr({ + zIndex: 2 + }).add(item.legendGroup); + box1 = item.fromText.getBBox(); + + // Creates legend symbol. + // Ternary changes variables based on option. + item.legendSymbol = this.chart.renderer.rect( + horizontal ? box1.x + box1.width + spacing : box1.x - gradientSize - spacing, // Upper left x. + box1.y, // Upper left y. + horizontal ? rectangleLength : gradientSize, // Width. + horizontal ? gradientSize : rectangleLength, // Height. + 2 // Corner radius. + ).attr({ + zIndex: 1 + }).add(item.legendGroup); + box2 = item.legendSymbol.getBBox(); + + // Creates the to text. + // Vertical coordinate changed based on option. + item.toText = this.chart.renderer.text( + item.toLabel, + box2.x + box2.width + spacing, + horizontal ? positionY : box2.y + box2.height - spacing + ).attr({ + zIndex: 2 + }).add(item.legendGroup); + box3 = item.toText.getBBox(); + + // Changes legend box settings based on option. + if (horizontal) { + legend.offsetWidth = box1.width + box2.width + box3.width + (spacing * 2) + padding; + legend.itemY = gradientSize + padding; + } else { + legend.offsetWidth = Math.max(box1.width, box3.width) + (spacing) + box2.width + padding; + legend.itemY = box2.height + padding; + legend.itemX = spacing; + } + }, + + /** + * Get the bounding box of all paths in the map combined. + */ + getBox: function (paths) { + var maxX = Number.MIN_VALUE, + minX = Number.MAX_VALUE, + maxY = Number.MIN_VALUE, + minY = Number.MAX_VALUE, + hasBox; + + // Find the bounding box + each(paths || [], function (point) { + + if (point.path) { + if (typeof point.path === 'string') { + point.path = H.splitPath(point.path); + } + + var path = point.path || [], + i = path.length, + even = false, // while loop reads from the end + pointMaxX = Number.MIN_VALUE, + pointMinX = Number.MAX_VALUE, + pointMaxY = Number.MIN_VALUE, + pointMinY = Number.MAX_VALUE; + + // The first time a map point is used, analyze its box + if (!point._foundBox) { + while (i--) { + if (typeof path[i] === 'number' && !isNaN(path[i])) { + if (even) { // even = x + pointMaxX = Math.max(pointMaxX, path[i]); + pointMinX = Math.min(pointMinX, path[i]); + } else { // odd = Y + pointMaxY = Math.max(pointMaxY, path[i]); + pointMinY = Math.min(pointMinY, path[i]); + } + even = !even; + } + } + // Cache point bounding box for use to position data labels, bubbles etc + point._midX = pointMinX + (pointMaxX - pointMinX) * pick(point.middleX, 0.5); + point._midY = pointMinY + (pointMaxY - pointMinY) * pick(point.middleY, 0.5); + point._maxX = pointMaxX; + point._minX = pointMinX; + point._maxY = pointMaxY; + point._minY = pointMinY; + point._foundBox = true; + } + + maxX = Math.max(maxX, point._maxX); + minX = Math.min(minX, point._minX); + maxY = Math.max(maxY, point._maxY); + minY = Math.min(minY, point._minY); + + hasBox = true; + } + }); + + // Set the box for the whole series + if (hasBox) { + this.minY = Math.min(minY, pick(this.minY, Number.MAX_VALUE)); + this.maxY = Math.max(maxY, pick(this.maxY, Number.MIN_VALUE)); + this.minX = Math.min(minX, pick(this.minX, Number.MAX_VALUE)); + this.maxX = Math.max(maxX, pick(this.maxX, Number.MIN_VALUE)); + } + }, + + getExtremes: function () { + this.dataMin = this.minY; + this.dataMax = this.maxY; + }, + + /** + * Translate the path so that it automatically fits into the plot area box + * @param {Object} path + */ + translatePath: function (path) { + + var series = this, + even = false, // while loop reads from the end + xAxis = series.xAxis, + yAxis = series.yAxis, + i; + + // Preserve the original + path = [].concat(path); + + // Do the translation + i = path.length; + while (i--) { + if (typeof path[i] === 'number') { + if (even) { // even = x + path[i] = xAxis.translate(path[i]); + } else { // odd = Y + path[i] = yAxis.len - yAxis.translate(path[i]); + } + even = !even; + } + } + + + return path; + }, + + /** + * Extend setData to join in mapData. If the allAreas option is true, all areas + * from the mapData are used, and those that don't correspond to a data value + * are given null values. + */ + setData: function (data, redraw) { + var options = this.options, + mapData = options.mapData, + joinBy = options.dataJoinBy, + dataUsed = []; + + + this.getBox(data); + this.getBox(mapData); + if (options.allAreas && mapData) { + + data = data || []; + + // Registered the point codes that actually hold data + if (joinBy) { + each(data, function (point) { + dataUsed.push(point[joinBy]); + }); + } + + // Add those map points that don't correspond to data, which will be drawn as null points + each(mapData, function (mapPoint) { + if (!joinBy || inArray(mapPoint[joinBy], dataUsed) === -1) { + data.push(merge(mapPoint, { y: null })); + } + }); + } + H.Series.prototype.setData.call(this, data, redraw); + }, + + /** + * For each point, get the corresponding map data + */ + getMapData: function (key, value) { + var options = this.options, + mapData = options.mapData, + mapMap = this.mapMap, + i = mapData.length; + + // Create a cache for quicker lookup second time + if (!mapMap) { + mapMap = this.mapMap = []; + } + if (mapMap[value] !== undefined) { + return mapData[mapMap[value]]; + + } else if (value !== undefined) { + while (i--) { + if (mapData[i][key] === value) { + mapMap[value] = i; // cache it + return mapData[i]; + } + } + } + }, + + /** + * Add the path option for data points. Find the max value for color calculation. + */ + translate: function () { + var series = this, + dataMin = Number.MAX_VALUE, + dataMax = Number.MIN_VALUE; + + series.generatePoints(); + + each(series.data, function (point) { + + point.shapeType = 'path'; + point.shapeArgs = { + d: series.translatePath(point.path) + }; + + // TODO: do point colors in drawPoints instead of point.init + if (typeof point.y === 'number') { + if (point.y > dataMax) { + dataMax = point.y; + } else if (point.y < dataMin) { + dataMin = point.y; + } + } + }); + + series.translateColors(dataMin, dataMax); + }, + + /** + * In choropleth maps, the color is a result of the value, so this needs translation too + */ + translateColors: function (dataMin, dataMax) { + + var seriesOptions = this.options, + valueRanges = seriesOptions.valueRanges, + colorRange = seriesOptions.colorRange, + colorKey = this.colorKey, + nullColor = seriesOptions.nullColor, + from, + to; + + if (colorRange) { + from = Color(colorRange.from); + to = Color(colorRange.to); + } + each(this.data, function (point) { + var value = point[colorKey], + isNull = value === null, + range, + color, + i, + pos; + + if (valueRanges) { + i = valueRanges.length; + if (isNull) { + color = nullColor; + } else { + while (i--) { + range = valueRanges[i]; + from = range.from; + to = range.to; + if ((from === UNDEFINED || value >= from) && (to === UNDEFINED || value <= to)) { + color = range.color; + break; + } + } + point.valueRange = i; + } + } else if (colorRange && !isNull) { + + pos = 1 - ((dataMax - value) / (dataMax - dataMin)); + color = tweenColors(from, to, pos); + } else if (isNull) { + color = nullColor; + } + + if (color) { + point.color = null; // reset from previous drilldowns, use of the same data options + point.options.color = color; + } + }); + }, + + drawGraph: noop, + + /** + * We need the points' bounding boxes in order to draw the data labels, so + * we skip it now and call it from drawPoints instead. + */ + drawDataLabels: noop, + + /** + * Use the drawPoints method of column, that is able to handle simple shapeArgs. + * Extend it by assigning the tooltip position. + */ + drawPoints: function () { + var series = this, + xAxis = series.xAxis, + yAxis = series.yAxis, + colorKey = series.colorKey; + + // Make points pass test in drawing + each(series.data, function (point) { + point.plotY = 1; // pass null test in column.drawPoints + if (point[colorKey] === null) { + point[colorKey] = 0; + point.isNull = true; + } + }); + + // Draw them + seriesTypes.column.prototype.drawPoints.apply(series); + + each(series.data, function (point) { + + // Record the middle point (loosely based on centroid), determined + // by the middleX and middleY options. + point.plotX = xAxis.toPixels(point._midX, true); + point.plotY = yAxis.toPixels(point._midY, true); + + // Reset escaped null points + if (point.isNull) { + point[colorKey] = null; + } + }); + + // Now draw the data labels + H.Series.prototype.drawDataLabels.call(series); + + }, + + /** + * Animate in the new series from the clicked point in the old series. + * Depends on the drilldown.js module + */ + animateDrilldown: function (init) { + var toBox = this.chart.plotBox, + level = this.chart.drilldownLevels[this.chart.drilldownLevels.length - 1], + fromBox = level.bBox, + animationOptions = this.chart.options.drilldown.animation, + scale; + + if (!init) { + + scale = Math.min(fromBox.width / toBox.width, fromBox.height / toBox.height); + level.shapeArgs = { + scaleX: scale, + scaleY: scale, + translateX: fromBox.x, + translateY: fromBox.y + }; + + // TODO: Animate this.group instead + each(this.points, function (point) { + + point.graphic + .attr(level.shapeArgs) + .animate({ + scaleX: 1, + scaleY: 1, + translateX: 0, + translateY: 0 + }, animationOptions); + + }); + + delete this.animate; + } + + }, + + /** + * When drilling up, pull out the individual point graphics from the lower series + * and animate them into the origin point in the upper series. + */ + animateDrillupFrom: function (level) { + seriesTypes.column.prototype.animateDrillupFrom.call(this, level); + }, + + + /** + * When drilling up, keep the upper series invisible until the lower series has + * moved into place + */ + animateDrillupTo: function (init) { + seriesTypes.column.prototype.animateDrillupTo.call(this, init); + } + }); + + + // The mapline series type + plotOptions.mapline = merge(plotOptions.map, { + lineWidth: 1, + backgroundColor: 'none' + }); + seriesTypes.mapline = extendClass(seriesTypes.map, { + type: 'mapline', + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'color', + 'stroke-width': 'lineWidth', + fill: 'backgroundColor' + }, + drawLegendSymbol: seriesTypes.line.prototype.drawLegendSymbol + }); + + // The mappoint series type + plotOptions.mappoint = merge(plotOptions.scatter, { + dataLabels: { + enabled: true, + format: '{point.name}', + color: 'black', + style: { + textShadow: '0 0 5px white' + } + } + }); + seriesTypes.mappoint = extendClass(seriesTypes.scatter, { + type: 'mappoint' + }); + + // The mapbubble series type + if (seriesTypes.bubble) { + + plotOptions.mapbubble = merge(plotOptions.bubble, { + tooltip: { + pointFormat: '{point.name}: {point.z}' + } + }); + seriesTypes.mapbubble = extendClass(seriesTypes.bubble, { + pointClass: extendClass(Point, { + applyOptions: MapAreaPoint.prototype.applyOptions + }), + xyFromShape: true, + type: 'mapbubble', + pointArrayMap: ['z'], // If one single value is passed, it is interpreted as z + /** + * Return the map area identified by the dataJoinBy option + */ + getMapData: seriesTypes.map.prototype.getMapData, + getBox: seriesTypes.map.prototype.getBox, + setData: seriesTypes.map.prototype.setData + }); + } + + // Create symbols for the zoom buttons + function selectiveRoundedRect(attr, x, y, w, h, rTopLeft, rTopRight, rBottomRight, rBottomLeft) { + var normalize = (attr['stroke-width'] % 2 / 2); + + x -= normalize; + y -= normalize; + + return ['M', x + rTopLeft, y, + // top side + 'L', x + w - rTopRight, y, + // top right corner + 'C', x + w - rTopRight / 2, y, x + w, y + rTopRight / 2, x + w, y + rTopRight, + // right side + 'L', x + w, y + h - rBottomRight, + // bottom right corner + 'C', x + w, y + h - rBottomRight / 2, x + w - rBottomRight / 2, y + h, x + w - rBottomRight, y + h, + // bottom side + 'L', x + rBottomLeft, y + h, + // bottom left corner + 'C', x + rBottomLeft / 2, y + h, x, y + h - rBottomLeft / 2, x, y + h - rBottomLeft, + // left side + 'L', x, y + rTopLeft, + // top left corner + 'C', x, y + rTopLeft / 2, x + rTopLeft / 2, y, x + rTopLeft, y, + 'Z' + ]; + } + symbols.topbutton = function (x, y, w, h, attr) { + return selectiveRoundedRect(attr, x, y, w, h, attr.r, attr.r, 0, 0); + }; + symbols.bottombutton = function (x, y, w, h, attr) { + return selectiveRoundedRect(attr, x, y, w, h, 0, 0, attr.r, attr.r); + }; + // The symbol callbacks are generated on the SVGRenderer object in all browsers. Even + // VML browsers need this in order to generate shapes in export. Now share + // them with the VMLRenderer. + if (H.Renderer === VMLRenderer) { + each(['topbutton', 'bottombutton'], function (shape) { + VMLRenderer.prototype.symbols[shape] = symbols[shape]; + }); + } + + + /** + * A wrapper for Chart with all the default values for a Map + */ + H.Map = function (options, callback) { + + var hiddenAxis = { + endOnTick: false, + gridLineWidth: 0, + labels: { + enabled: false + }, + lineWidth: 0, + minPadding: 0, + maxPadding: 0, + startOnTick: false, + tickWidth: 0, + title: null + }, + seriesOptions; + + // Don't merge the data + seriesOptions = options.series; + options.series = null; + + options = merge({ + chart: { + panning: 'xy' + }, + xAxis: hiddenAxis, + yAxis: merge(hiddenAxis, { reversed: true }) + }, + options, // user's options + + { // forced options + chart: { + type: 'map', + inverted: false + } + }); + + options.series = seriesOptions; + + + return new Chart(options, callback); + }; +}(Highcharts));