// Licensed to Cloudera, Inc. under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. Cloudera, Inc. licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. (function() { var svg; //save off default references var d3 = window.d3, topojson = window.topojson; var defaultOptions = { scope: 'world', setProjection: setProjection, projection: 'equirectangular', dataType: 'json', onClick: function() {}, done: function() {}, fills: { defaultFill: '#ABDDA4' }, geographyConfig: { dataUrl: null, hideAntarctica: true, borderWidth: 1, borderColor: '#FDFDFD', popupTemplate: function(geography, data) { return '
' + geography.properties.name + '
'; }, popupOnHover: true, highlightOnHover: true, highlightFillColor: '#FC8D59', highlightBorderColor: 'rgba(250, 15, 160, 0.2)', selectedFillColor: '#666666', selectedBorderColor: '#666666', highlightBorderWidth: 2 }, bubblesConfig: { borderWidth: 2, borderColor: '#FFFFFF', popupOnHover: true, popupTemplate: function(geography, data) { return '
' + data.name + '
'; }, fillOpacity: 0.75, animate: true, highlightOnHover: true, highlightFillColor: '#FC8D59', highlightBorderColor: 'rgba(250, 15, 160, 0.2)', highlightBorderWidth: 2, highlightFillOpacity: 0.85, exitDelay: 100 }, arcConfig: { strokeColor: '#DD1C77', strokeWidth: 1, arcSharpness: 1, animationSpeed: 600 } }; function addContainer( element ) { this.svg = d3.select( element ).append('svg') .attr('width', element.offsetWidth) .attr('class', 'datamap') .attr('height', element.offsetHeight); return this.svg; } // setProjection takes the svg element and options function setProjection( element, options ) { var projection, path; if ( options && typeof options.scope === 'undefined') { options.scope = 'world'; } if ( options.scope === 'usa' ) { projection = d3.geo.albersUsa() .scale(element.offsetWidth) .translate([element.offsetWidth / 2, element.offsetHeight / 2]); } else if ( options.scope === 'world' ) { projection = d3.geo[options.projection]() .scale((element.offsetWidth + 1) / 2 / Math.PI) .translate([element.offsetWidth / 2, element.offsetHeight / (options.projection === "mercator" ? 1.45 : 1.8)]); } path = d3.geo.path() .projection( projection ); return {path: path, projection: projection}; } function addStyleBlock() { if ( d3.select('.datamaps-style-block').empty() ) { d3.select('head').attr('class', 'datamaps-style-block').append('style') .html('.datamap path {stroke: #FFFFFF; stroke-width: 1px;} .datamaps-legend dt, .datamaps-legend dd { float: left; margin: 0 3px 0 0;} .datamaps-legend dd {width: 20px; margin-right: 6px; border-radius: 3px;} .datamaps-legend {padding-bottom: 20px; z-index: 1001; position: absolute; left: 4px; font-size: 12px; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;} .datamaps-hoverover {display: none; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } .hoverinfo {padding: 4px; border-radius: 1px; background-color: #FFF; box-shadow: 1px 1px 5px #CCC; font-size: 12px; border: 1px solid #CCC; } .hoverinfo hr {border:1px dotted #CCC; }'); } } function drawSubunits( data ) { var fillData = this.options.fills, colorCodeData = this.options.data || {}, geoConfig = this.options.geographyConfig, onClick = this.options.onClick; var subunits = this.svg.select('g.datamaps-subunits'); if ( subunits.empty() ) { subunits = this.addLayer('datamaps-subunits', null, true); } var geoData = topojson.feature( data, data.objects[ this.options.scope ] ).features; if ( geoConfig.hideAntarctica ) { geoData = geoData.filter(function(feature) { return feature.id !== "ATA"; }); } var geo = subunits.selectAll('path.datamaps-subunit').data( geoData ); geo.enter() .append('path') .attr('d', this.path) .attr('class', function(d) { return 'datamaps-subunit ' + d.id; }) .attr('data-info', function(d) { return JSON.stringify( colorCodeData[d.id]); }) .style('fill', function(d) { var fillColor; if ( colorCodeData[d.id] ) { fillColor = fillData[ colorCodeData[d.id].fillKey ]; if (colorCodeData[d.id].selected){ fillColor = geoConfig.selectedFillColor; } } return fillColor || fillData.defaultFill; }) .on('click', function(d) { if ( colorCodeData[d.id] && typeof onClick != "undefined") { onClick(colorCodeData[d.id]); } }) .style('stroke-width', function(d) { var strokeWidth = geoConfig.borderWidth; if ( colorCodeData[d.id] && colorCodeData[d.id].selected ) { strokeWidth = 2; } return strokeWidth; }) .style('stroke', function(d) { var strokeColor = geoConfig.borderColor; if ( colorCodeData[d.id] && colorCodeData[d.id].selected ) { strokeColor = geoConfig.selectedBorderColor; } return strokeColor; }); } function handleGeographyConfig () { var hoverover; var svg = this.svg; var self = this; var options = this.options.geographyConfig; if ( options.highlightOnHover || options.popupOnHover ) { svg.selectAll('.datamaps-subunit') .on('mouseover', function(d) { var $this = d3.select(this); if ( options.highlightOnHover ) { var previousAttributes = { 'fill': $this.style('fill'), 'stroke': $this.style('stroke'), 'stroke-width': $this.style('stroke-width'), 'fill-opacity': $this.style('fill-opacity') }; $this .style('fill', options.highlightFillColor) .style('stroke', options.highlightBorderColor) .style('stroke-width', options.highlightBorderWidth) .style('fill-opacity', options.highlightFillOpacity) .attr('data-previousAttributes', JSON.stringify(previousAttributes)); //as per discussion on https://github.com/markmarkoh/datamaps/issues/19 if ( ! /MSIE/.test(navigator.userAgent) ) { moveToFront.call(this); } } if ( options.popupOnHover ) { self.updatePopup($this, d, options, svg); } }) .on('mouseout', function() { var $this = d3.select(this); if (options.highlightOnHover) { //reapply previous attributes var previousAttributes = JSON.parse( $this.attr('data-previousAttributes') ); for ( var attr in previousAttributes ) { $this.style(attr, previousAttributes[attr]); } } $this.on('mousemove', null); d3.selectAll('.datamaps-hoverover').style('display', 'none'); }); } function moveToFront() { this.parentNode.appendChild(this); } } //plugin to add a simple map legend function addLegend(layer, data, options) { data = data || {}; if ( !this.options.fills ) { return; } var html = '
'; var label = ''; if ( data.legendTitle ) { html = '

' + data.legendTitle + '

' + html; } for ( var fillKey in this.options.fills ) { if ( fillKey === 'defaultFill') { if (! data.defaultFillName ) { continue; } label = data.defaultFillName; } else { if (data.labels && data.labels[fillKey]) { label = data.labels[fillKey]; } else { label= fillKey + ': '; } } html += '
' + label + '
'; html += '
 
'; } html += '
'; var hoverover = d3.select( this.options.element ).append('div') .attr('class', 'datamaps-legend') .html(html); } function handleArcs (layer, data, options) { var self = this, svg = this.svg; if ( !data || (data && !data.slice) ) { throw "Datamaps Error - arcs must be an array"; } if ( typeof options === "undefined" ) { options = defaultOptions.arcConfig; } var arcs = layer.selectAll('path.datamaps-arc').data( data, JSON.stringify ); arcs .enter() .append('svg:path') .attr('class', 'datamaps-arc') .style('stroke-linecap', 'round') .style('stroke', function(datum) { if ( datum.options && datum.options.strokeColor) { return datum.options.strokeColor; } return options.strokeColor }) .style('fill', 'none') .style('stroke-width', function(datum) { if ( datum.options && datum.options.strokeWidth) { return datum.options.strokeWidth; } return options.strokeWidth; }) .attr('d', function(datum) { var originXY = self.latLngToXY(datum.origin.latitude, datum.origin.longitude); var destXY = self.latLngToXY(datum.destination.latitude, datum.destination.longitude); var midXY = [ (originXY[0] + destXY[0]) / 2, (originXY[1] + destXY[1]) / 2]; return "M" + originXY[0] + ',' + originXY[1] + "S" + (midXY[0] + (50 * options.arcSharpness)) + "," + (midXY[1] - (75 * options.arcSharpness)) + "," + destXY[0] + "," + destXY[1]; }) .transition() .delay(100) .style('fill', function() { /* Thank you Jake Archibald, this is awesome. Source: http://jakearchibald.com/2013/animated-line-drawing-svg/ */ var length = this.getTotalLength(); this.style.transition = this.style.WebkitTransition = 'none'; this.style.strokeDasharray = length + ' ' + length; this.style.strokeDashoffset = length; this.getBoundingClientRect(); this.style.transition = this.style.WebkitTransition = 'stroke-dashoffset ' + options.animationSpeed + 'ms ease-out'; this.style.strokeDashoffset = '0'; return 'none'; }) arcs.exit() .transition() .style('opacity', 0) .remove(); } function handleLabels ( layer, options ) { var self = this; options = options || {}; var labelStartCoodinates = this.projection([-67.707617, 42.722131]); this.svg.selectAll(".datamaps-subunit") .attr("data-foo", function(d) { var center = self.path.centroid(d); var xOffset = 7.5, yOffset = 5; if ( ["FL", "KY", "MI"].indexOf(d.id) > -1 ) xOffset = -2.5; if ( d.id === "NY" ) xOffset = -1; if ( d.id === "MI" ) yOffset = 18; if ( d.id === "LA" ) xOffset = 13; var x,y; x = center[0] - xOffset; y = center[1] + yOffset; var smallStateIndex = ["VT", "NH", "MA", "RI", "CT", "NJ", "DE", "MD", "DC"].indexOf(d.id); if ( smallStateIndex > -1) { var yStart = labelStartCoodinates[1]; x = labelStartCoodinates[0]; y = yStart + (smallStateIndex * (2+ (options.fontSize || 12))); layer.append("line") .attr("x1", x - 3) .attr("y1", y - 5) .attr("x2", center[0]) .attr("y2", center[1]) .style("stroke", options.labelColor || "#000") .style("stroke-width", options.lineWidth || 1) } layer.append("text") .attr("x", x) .attr("y", y) .style("font-size", (options.fontSize || 10) + 'px') .style("font-family", options.fontFamily || "Verdana") .style("fill", options.labelColor || "#000") .text( d.id ); return "bar"; }); } function handleBubbles (layer, data, options ) { var self = this, fillData = this.options.fills, svg = this.svg; if ( !data || (data && !data.slice) ) { throw "Datamaps Error - bubbles must be an array"; } var bubbles = layer.selectAll('circle.datamaps-bubble').data( data, JSON.stringify ); bubbles .enter() .append('svg:circle') .attr('class', 'datamaps-bubble') .attr('cx', function ( datum ) { var latLng; if ( datumHasCoords(datum) ) { latLng = self.latLngToXY(datum.latitude, datum.longitude); } else if ( datum.centered ) { latLng = self.path.centroid(svg.select('path.' + datum.centered).data()[0]); } if ( latLng ) return latLng[0]; }) .attr('cy', function ( datum ) { var latLng; if ( datumHasCoords(datum) ) { latLng = self.latLngToXY(datum.latitude, datum.longitude); } else if ( datum.centered ) { latLng = self.path.centroid(svg.select('path.' + datum.centered).data()[0]); } if ( latLng ) return latLng[1];; }) .attr('r', 0) //for animation purposes .attr('data-info', function(d) { return JSON.stringify(d); }) .style('stroke', options.borderColor) .style('stroke-width', options.borderWidth) .style('fill-opacity', options.fillOpacity) .style('fill', function ( datum ) { var fillColor = fillData[ datum.fillKey ]; return fillColor || fillData.defaultFill; }) .on('mouseover', function ( datum ) { var $this = d3.select(this); if (options.highlightOnHover) { //save all previous attributes for mouseout var previousAttributes = { 'fill': $this.style('fill'), 'stroke': $this.style('stroke'), 'stroke-width': $this.style('stroke-width'), 'fill-opacity': $this.style('fill-opacity') }; $this .style('fill', options.highlightFillColor) .style('stroke', options.highlightBorderColor) .style('stroke-width', options.highlightBorderWidth) .style('fill-opacity', options.highlightFillOpacity) .attr('data-previousAttributes', JSON.stringify(previousAttributes)); } if (options.popupOnHover) { self.updatePopup($this, datum, options, svg); } }) .on('mouseout', function ( datum ) { var $this = d3.select(this); if (options.highlightOnHover) { //reapply previous attributes var previousAttributes = JSON.parse( $this.attr('data-previousAttributes') ); for ( var attr in previousAttributes ) { $this.style(attr, previousAttributes[attr]); } } d3.selectAll('.datamaps-hoverover').style('display', 'none'); }) .transition().duration(400) .attr('r', function ( datum ) { return datum.radius; }); bubbles.exit() .transition() .delay(options.exitDelay) .attr("r", 0) .remove(); function datumHasCoords (datum) { return typeof datum !== 'undefined' && typeof datum.latitude !== 'undefined' && typeof datum.longitude !== 'undefined'; } } //stolen from underscore.js function defaults(obj) { Array.prototype.slice.call(arguments, 1).forEach(function(source) { if (source) { for (var prop in source) { if (obj[prop] == null) obj[prop] = source[prop]; } } }); return obj; } /************************************** Public Functions ***************************************/ function Datamap( options ) { if ( typeof d3 === 'undefined' || typeof topojson === 'undefined' ) { throw new Error('Include d3.js (v3.0.3 or greater) and topojson on this page before creating a new map'); } //set options for global use this.options = defaults(options, defaultOptions); this.options.geographyConfig = defaults(options.geographyConfig, defaultOptions.geographyConfig); this.options.bubblesConfig = defaults(options.bubblesConfig, defaultOptions.bubblesConfig); this.options.arcConfig = defaults(options.arcConfig, defaultOptions.arcConfig); //add the SVG container if ( d3.select( this.options.element ).select('svg').length > 0 ) { addContainer.call(this, this.options.element ); } /* Add core plugins to this instance */ this.addPlugin('bubbles', handleBubbles); this.addPlugin('legend', addLegend); this.addPlugin('arc', handleArcs); this.addPlugin('labels', handleLabels); //append style block with basic hoverover styles if ( ! this.options.disableDefaultStyles ) { addStyleBlock(); } return this.draw(); } // actually draw the features(states & countries) Datamap.prototype.draw = function() { //save off in a closure var self = this; var options = self.options; //set projections and paths based on scope var pathAndProjection = options.setProjection.apply(self, [options.element, options] ); this.path = pathAndProjection.path; this.projection = pathAndProjection.projection; //if custom URL for topojson data, retrieve it and render if ( options.geographyConfig.dataUrl ) { d3.json( options.geographyConfig.dataUrl, function(error, results) { if ( error ) throw new Error(error); self.customTopo = results; draw( results ); }); } else { draw( this[options.scope + 'Topo'] ); } return this; function draw (data) { // if fetching remote data, draw the map first then call `updateChoropleth` if ( self.options.dataUrl ) { //allow for csv or json data types d3[self.options.dataType](self.options.dataUrl, function(data) { //in the case of csv, transform data to object if ( self.options.dataType === 'csv' && (data && data.slice) ) { var tmpData = {}; for(var i = 0; i < data.length; i++) { tmpData[data[i].id] = data[i]; } data = tmpData; } Datamaps.prototype.updateChoropleth.call(self, data); }); } drawSubunits.call(self, data); handleGeographyConfig.call(self); if ( self.options.geographyConfig.popupOnHover || self.options.bubblesConfig.popupOnHover) { hoverover = d3.select( self.options.element ).append('div') .attr('class', 'datamaps-hoverover') .style('z-index', 10001) .style('position', 'absolute'); } //fire off finished callback self.options.done(self); } }; /************************************** TopoJSON ***************************************/ Datamap.prototype.worldTopo = WORLD_TOPO; Datamap.prototype.usaTopo = USA_TOPO; /************************************** Utilities ***************************************/ //convert lat/lng coords to X / Y coords Datamap.prototype.latLngToXY = function(lat, lng) { return this.projection([lng, lat]); }; //add layer to root SVG Datamap.prototype.addLayer = function( className, id, first ) { var layer; if ( first ) { layer = this.svg.insert('g', ':first-child') } else { layer = this.svg.append('g') } return layer.attr('id', id || '') .attr('class', className || ''); }; Datamap.prototype.updateChoropleth = function(data) { var svg = this.svg; for ( var subunit in data ) { if ( data.hasOwnProperty(subunit) ) { console.log(data); var color; var subunitData = data[subunit] if ( ! subunit ) { continue; } else if ( typeof subunitData === "string" ) { color = subunitData; } else if ( typeof subunitData.color === "string" ) { color = subunitData.color; } else { color = this.options.fills[ subunitData.fillKey ]; } //if it's an object, overriding the previous data if ( subunitData === Object(subunitData) ) { this.options.data[subunit] = defaults(subunitData, this.options.data[subunit] || {}); var geo = this.svg.select('.' + subunit).attr('data-info', JSON.stringify(this.options.data[subunit])); } svg .selectAll('.' + subunit) .transition() .style('fill', color); } } }; Datamap.prototype.updatePopup = function (element, d, options) { var self = this; element.on('mousemove', null); element.on('mousemove', function() { var position = d3.mouse(this); d3.select(self.svg[0][0].parentNode).select('.datamaps-hoverover') .style('top', ( (position[1] + 30)) + "px") .html(function() { var data = JSON.parse(element.attr('data-info')); //if ( !data ) return ''; return options.popupTemplate(d, data); }) .style('left', ( position[0]) + "px"); }); d3.select(self.svg[0][0].parentNode).select('.datamaps-hoverover').style('display', 'block'); }; Datamap.prototype.addPlugin = function( name, pluginFn ) { var self = this; if ( typeof Datamap.prototype[name] === "undefined" ) { Datamap.prototype[name] = function(data, options, callback, createNewLayer) { var layer; if ( typeof createNewLayer === "undefined" ) { createNewLayer = false; } if ( typeof options === 'function' ) { callback = options; options = undefined; } options = defaults(options || {}, defaultOptions[name + 'Config']); //add a single layer, reuse the old layer if ( !createNewLayer && this.options[name + 'Layer'] ) { layer = this.options[name + 'Layer']; options = options || this.options[name + 'Options']; } else { layer = this.addLayer(name); this.options[name + 'Layer'] = layer; this.options[name + 'Options'] = options; } pluginFn.apply(this, [layer, data, options]); if ( callback ) { callback(layer); } }; } }; // expose library if ( typeof define === "function" && define.amd ) { define( "datamaps", function(require) { d3 = require('d3'); topojson = require('topojson'); return Datamap; } ); } else { window.Datamap = window.Datamaps = Datamap; } if ( window.jQuery ) { window.jQuery.fn.datamaps = function(options, callback) { options = options || {}; options.element = this[0]; var datamap = new Datamap(options); if ( typeof callback === "function" ) { callback(datamap, options); } return this; }; } })();