UNPKG

gethue

Version:

Hue is an Open source SQL Query Editor for Databases/Warehouses

804 lines (720 loc) 24.4 kB
// 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. import d3v3 from 'd3v3'; import jQuery from 'jquery'; import topojson from '../../../../ext/topojson.v1.min'; import apiHelper from '../../../../api/apiHelper'; const d3 = d3v3; const defaultOptions = { scope: 'world', setProjection: setProjection, projection: 'equirectangular', dataType: 'json', onClick: function () {}, done: function () {}, legendData: [], fills: { defaultFill: '#ABDDA4' }, geographyConfig: { dataUrl: null, hideAntarctica: true, borderWidth: 1, borderColor: '#FDFDFD', popupTemplate: function (geography, data) { return '<div class="hoverinfo"><strong>' + geography.properties.name + '</strong></div>'; }, 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 '<div class="hoverinfo"><strong>' + data.name + '</strong></div>'; }, 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) { let projection; if (options && typeof options.scope === 'undefined') { options.scope = 'world'; } const defaultTranslate = [ element.offsetWidth / 2, element.offsetHeight / (options.projection === 'mercator' ? 1.45 : 1.8) ]; switch (options.scope) { case 'usa': projection = d3.geo .albersUsa() .scale(element.offsetWidth) .translate([element.offsetWidth / 2, element.offsetHeight / 2]); break; case 'world': projection = d3.geo[options.projection]() .scale((element.offsetWidth + 1) / 2 / Math.PI) .translate(defaultTranslate); break; case 'europe': projection = d3.geo[options.projection]() .center([8.43727461750008, 51.16822764400005]) .scale(380) .translate(defaultTranslate); break; case 'chn': projection = d3.geo[options.projection]() .center([104.18741784700012, 34.672410587000066]) .rotate([0, 0]) .scale(380) .translate(defaultTranslate); break; case 'aus': projection = d3.geo[options.projection]() .center([136.0129500660001, -31.995293877999913]) .rotate([0, 0]) .scale(350) .translate(defaultTranslate); break; case 'bra': projection = d3.geo[options.projection]() .center([-51.447769636499956, -14.23752777099994]) .rotate([0, 0]) .scale(320) .translate(defaultTranslate); break; case 'can': projection = d3.geo[options.projection]() .center([-96.81107793155442, 62.3928040600001]) .rotate([0, 0]) .scale(300) .translate(defaultTranslate); break; case 'fra': projection = d3.geo[options.projection]() .center([2, 46]) .rotate([0, 0]) .scale(1300) .translate(defaultTranslate); break; case 'deu': projection = d3.geo[options.projection]() .center([10.43727461750008, 51.16822764400005]) .rotate([0, 0]) .scale(1600) .translate(defaultTranslate); break; case 'ita': projection = d3.geo[options.projection]() .center([12.560077144500099, 41.287229413500036]) .rotate([0, 0]) .scale(1300) .translate(defaultTranslate); break; case 'jpn': projection = d3.geo[options.projection]() .center([138.4618839855001, 34.779750881000126]) .rotate([0, 0]) .scale(700) .translate(defaultTranslate); break; case 'gbr': projection = d3.geo[options.projection]() .center([-3, 54.501734]) .rotate([0, 0]) .scale(1300) .translate(defaultTranslate); break; } const 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: 3px; margin-left: 14px; border-radius: 3px;} .datamaps-legend {padding-bottom: 20px; z-index: 1001; 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) { const fillData = this.options.fills, colorCodeData = this.options.data || {}, geoConfig = this.options.geographyConfig, onClick = this.options.onClick; let subunits = this.svg.select('g.datamaps-subunits'); if (subunits.empty()) { subunits = this.addLayer('datamaps-subunits', null, true); } let geoData = topojson.feature(data, data.objects[this.options.scope]).features; if (geoConfig.hideAntarctica) { geoData = geoData.filter(feature => { return feature.id !== 'ATA'; }); } const geo = subunits.selectAll('path.datamaps-subunit').data(geoData); geo .enter() .append('path') .attr('d', this.path) .attr('class', d => { return 'datamaps-subunit ' + d.id; }) .attr('data-info', d => { return JSON.stringify(colorCodeData[d.id]); }) .style('fill', d => { let 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', d => { if (colorCodeData[d.id] && typeof onClick != 'undefined') { onClick(colorCodeData[d.id]); } }) .style('stroke-width', d => { let strokeWidth = geoConfig.borderWidth; if (colorCodeData[d.id] && colorCodeData[d.id].selected) { strokeWidth = 2; } return strokeWidth; }) .style('stroke', d => { let strokeColor = geoConfig.borderColor; if (colorCodeData[d.id] && colorCodeData[d.id].selected) { strokeColor = geoConfig.selectedBorderColor; } return strokeColor; }); } function handleGeographyConfig() { const svg = this.svg; const self = this; const options = this.options.geographyConfig; if (options.highlightOnHover || options.popupOnHover) { svg .selectAll('.datamaps-subunit') .on('mouseover', function (d) { const $this = d3.select(this); if (options.highlightOnHover) { const 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 () { const $this = d3.select(this); if (options.highlightOnHover) { //reapply previous attributes const previousAttributes = JSON.parse($this.attr('data-previousAttributes')); for (const 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; } function compareLegendValues(a, b) { if (a.idx < b.idx) { return -1; } if (a.idx > b.idx) { return 1; } return 0; } let html = '<dl>'; const _fills = this.options.fills; this.options.legendData.sort(compareLegendValues).forEach(fill => { html += '<dd style="background-color:' + _fills['fill_' + fill.idx] + '">&nbsp;</dd>'; html += '<dt>' + fill.cat + '</dt>'; }); html += '</dl>'; d3.select(this.options.element).append('div').attr('class', 'datamaps-legend').html(html); } function handleArcs(layer, data, options) { const self = this; if (!data || (data && !data.slice)) { throw 'Datamaps Error - arcs must be an array'; } if (typeof options === 'undefined') { options = defaultOptions.arcConfig; } const 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', datum => { if (datum.options && datum.options.strokeColor) { return datum.options.strokeColor; } return options.strokeColor; }) .style('fill', 'none') .style('stroke-width', datum => { if (datum.options && datum.options.strokeWidth) { return datum.options.strokeWidth; } return options.strokeWidth; }) .attr('d', datum => { const originXY = self.latLngToXY(datum.origin.latitude, datum.origin.longitude); const destXY = self.latLngToXY(datum.destination.latitude, datum.destination.longitude); const 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/ */ const 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) { const self = this; options = options || {}; const labelStartCoodinates = this.projection([-67.707617, 42.722131]); this.svg.selectAll('.datamaps-subunit').attr('data-foo', d => { const center = self.path.centroid(d); let 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; } let x, y; x = center[0] - xOffset; y = center[1] + yOffset; const smallStateIndex = ['VT', 'NH', 'MA', 'RI', 'CT', 'NJ', 'DE', 'MD', 'DC'].indexOf(d.id); if (smallStateIndex > -1) { const 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) { const self = this, fillData = this.options.fills, svg = this.svg; if (!data || (data && !data.slice)) { throw 'Datamaps Error - bubbles must be an array'; } const bubbles = layer.selectAll('circle.datamaps-bubble').data(data, JSON.stringify); bubbles .enter() .append('svg:circle') .attr('class', 'datamaps-bubble') .attr('cx', datum => { let 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', datum => { let 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', d => { return JSON.stringify(d); }) .style('stroke', options.borderColor) .style('stroke-width', options.borderWidth) .style('fill-opacity', options.fillOpacity) .style('fill', datum => { const fillColor = fillData[datum.fillKey]; return fillColor || fillData.defaultFill; }) .on('mouseover', function (datum) { const $this = d3.select(this); if (options.highlightOnHover) { //save all previous attributes for mouseout const 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) { const $this = d3.select(this); if (options.highlightOnHover) { //reapply previous attributes const previousAttributes = JSON.parse($this.attr('data-previousAttributes')); for (const attr in previousAttributes) { $this.style(attr, previousAttributes[attr]); } } d3.selectAll('.datamaps-hoverover').style('display', 'none'); }) .transition() .duration(400) .attr('r', 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(source => { if (source) { for (const 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 const self = this; const options = self.options; //set projections and paths based on scope const pathAndProjection = options.setProjection.apply(self, [options.element, options]); this.path = pathAndProjection.path; this.projection = pathAndProjection.projection; apiHelper .fetchTopo({ location: options.scope === 'europe' ? 'world' : options.scope }) .done(topoData => { let topo = JSON.parse(topoData); if (options.scope === 'europe') { const europeTopo = jQuery.extend(true, {}, topo); europeTopo.objects['europe'] = europeTopo.objects['world']; delete europeTopo.objects['world']; topo = europeTopo; } self.customTopo = true; draw(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, data => { //in the case of csv, transform data to object if (self.options.dataType === 'csv' && data && data.slice) { const tmpData = {}; for (let i = 0; i < data.length; i++) { tmpData[data[i].id] = data[i]; } data = tmpData; } Datamap.prototype.updateChoropleth.call(self, data); }); } drawSubunits.call(self, data); handleGeographyConfig.call(self); if (self.options.geographyConfig.popupOnHover || self.options.bubblesConfig.popupOnHover) { 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); } }; /************************************** Utilities ***************************************/ //convert lat/lng coords to X / Y coords Datamap.prototype.latLngToXY = function (lat, lng) { return this.projection([lng, lat]); }; //add <g> layer to root SVG Datamap.prototype.addLayer = function (className, id, first) { let 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) { const svg = this.svg; for (const subunit in data) { if (data.hasOwnProperty(subunit)) { let color; const 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] || {}); 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) { const self = this; element.on('mousemove', null); element.on('mousemove', function () { const position = d3.mouse(this); const svgHeight = d3.select(self.svg[0][0].parentNode)[0][0].offsetHeight || -1; d3.select(self.svg[0][0].parentNode) .select('.datamaps-hoverover') .style('top', position[1] + 30 + 'px') .html(() => { const data = JSON.parse(element.attr('data-info')); //if ( !data ) return ''; return options.popupTemplate(d, data); }) .style('left', position[0] + 'px'); const popupHeight = d3 .select(self.svg[0][0].parentNode) .select('.datamaps-hoverover')[0][0].offsetHeight; if (position[1] + 30 + popupHeight > svgHeight) { d3.select(self.svg[0][0].parentNode) .select('.datamaps-hoverover') .style('top', position[1] - 10 - popupHeight + 'px'); } }); d3.select(self.svg[0][0].parentNode).select('.datamaps-hoverover').style('display', 'block'); }; Datamap.prototype.addPlugin = function (name, pluginFn) { if (typeof Datamap.prototype[name] === 'undefined') { Datamap.prototype[name] = function (data, options, callback, createNewLayer) { let 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); } }; } }; jQuery.fn.datamaps = function (options, callback) { options = options || {}; options.element = this[0]; const datamap = new Datamap(options); if (typeof callback === 'function') { callback(datamap, options); } return this; }; export default Datamap;