gethue
Version:
Hue is an Open source SQL Query Editor for Databases/Warehouses
804 lines (720 loc) • 24.4 kB
JavaScript
// 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] + '"> </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;