blob: 9b77fe4a27cb1043427e98a7c1f75f38e455027e [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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.
*/
/* eslint-disable react/sort-prop-types */
import d3 from 'd3';
import PropTypes from 'prop-types';
import { extent as d3Extent } from 'd3-array';
import { getNumberFormatter, getSequentialSchemeRegistry } from '@superset-ui/core';
import countries from './countries';
import './CountryMap.css';
const propTypes = {
data: PropTypes.arrayOf(
PropTypes.shape({
country_id: PropTypes.string,
metric: PropTypes.number,
}),
),
width: PropTypes.number,
height: PropTypes.number,
country: PropTypes.string,
linearColorScheme: PropTypes.string,
mapBaseUrl: PropTypes.string,
numberFormat: PropTypes.string,
};
const maps = {};
function CountryMap(element, props) {
const { data, width, height, country, linearColorScheme, numberFormat } = props;
const container = element;
const format = getNumberFormatter(numberFormat);
const colorScale = getSequentialSchemeRegistry()
.get(linearColorScheme)
.createLinearScale(d3Extent(data, v => v.metric));
const colorMap = {};
data.forEach(d => {
colorMap[d.country_id] = colorScale(d.metric);
});
const colorFn = d => colorMap[d.properties.ISO] || 'none';
const path = d3.geo.path();
const div = d3.select(container);
div.classed('superset-legacy-chart-country-map', true);
div.selectAll('*').remove();
container.style.height = `${height}px`;
container.style.width = `${width}px`;
const svg = div
.append('svg:svg')
.attr('width', width)
.attr('height', height)
.attr('preserveAspectRatio', 'xMidYMid meet');
const backgroundRect = svg
.append('rect')
.attr('class', 'background')
.attr('width', width)
.attr('height', height);
const g = svg.append('g');
const mapLayer = g.append('g').classed('map-layer', true);
const textLayer = g
.append('g')
.classed('text-layer', true)
.attr('transform', `translate(${width / 2}, 45)`);
const bigText = textLayer.append('text').classed('big-text', true);
const resultText = textLayer.append('text').classed('result-text', true).attr('dy', '1em');
let centered;
const clicked = function clicked(d) {
const hasCenter = d && centered !== d;
let x;
let y;
let k;
const halfWidth = width / 2;
const halfHeight = height / 2;
if (hasCenter) {
const centroid = path.centroid(d);
[x, y] = centroid;
k = 4;
centered = d;
} else {
x = halfWidth;
y = halfHeight;
k = 1;
centered = null;
}
g.transition()
.duration(750)
.attr('transform', `translate(${halfWidth},${halfHeight})scale(${k})translate(${-x},${-y})`);
textLayer
.style('opacity', 0)
.attr('transform', `translate(0,0)translate(${x},${hasCenter ? y - 5 : 45})`)
.transition()
.duration(750)
.style('opacity', 1);
bigText
.transition()
.duration(750)
.style('font-size', hasCenter ? 6 : 16);
resultText
.transition()
.duration(750)
.style('font-size', hasCenter ? 16 : 24);
};
backgroundRect.on('click', clicked);
const selectAndDisplayNameOfRegion = function selectAndDisplayNameOfRegion(feature) {
let name = '';
if (feature && feature.properties) {
if (feature.properties.ID_2) {
name = feature.properties.NAME_2;
} else {
name = feature.properties.NAME_1;
}
}
bigText.text(name);
};
const updateMetrics = function updateMetrics(region) {
if (region.length > 0) {
resultText.text(format(region[0].metric));
}
};
const mouseenter = function mouseenter(d) {
// Darken color
let c = colorFn(d);
if (c !== 'none') {
c = d3.rgb(c).darker().toString();
}
d3.select(this).style('fill', c);
selectAndDisplayNameOfRegion(d);
const result = data.filter(region => region.country_id === d.properties.ISO);
updateMetrics(result);
};
const mouseout = function mouseout() {
d3.select(this).style('fill', colorFn);
bigText.text('');
resultText.text('');
};
function drawMap(mapData) {
const { features } = mapData;
const center = d3.geo.centroid(mapData);
const scale = 100;
const projection = d3.geo
.mercator()
.scale(scale)
.center(center)
.translate([width / 2, height / 2]);
path.projection(projection);
// Compute scale that fits container.
const bounds = path.bounds(mapData);
const hscale = (scale * width) / (bounds[1][0] - bounds[0][0]);
const vscale = (scale * height) / (bounds[1][1] - bounds[0][1]);
const newScale = hscale < vscale ? hscale : vscale;
// Compute bounds and offset using the updated scale.
projection.scale(newScale);
const newBounds = path.bounds(mapData);
projection.translate([
width - (newBounds[0][0] + newBounds[1][0]) / 2,
height - (newBounds[0][1] + newBounds[1][1]) / 2,
]);
// Draw each province as a path
mapLayer
.selectAll('path')
.data(features)
.enter()
.append('path')
.attr('d', path)
.attr('class', 'region')
.attr('vector-effect', 'non-scaling-stroke')
.style('fill', colorFn)
.on('mouseenter', mouseenter)
.on('mouseout', mouseout)
.on('click', clicked);
}
const countryKey = country.toLowerCase();
const map = maps[countryKey];
if (map) {
drawMap(map);
} else {
const url = countries[countryKey];
d3.json(url, (error, mapData) => {
if (!error) {
maps[countryKey] = mapData;
drawMap(mapData);
}
});
}
}
CountryMap.displayName = 'CountryMap';
CountryMap.propTypes = propTypes;
export default CountryMap;