mirror of
https://github.com/apache/superset.git
synced 2024-09-19 20:19:37 -04:00
255 lines
6.8 KiB
JavaScript
255 lines
6.8 KiB
JavaScript
/**
|
|
* 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,
|
|
CategoricalColorNamespace,
|
|
} from '@superset-ui/core';
|
|
import countries, { countryOptions } from './countries';
|
|
|
|
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,
|
|
colorScheme,
|
|
sliceId,
|
|
} = props;
|
|
|
|
const container = element;
|
|
const format = getNumberFormatter(numberFormat);
|
|
const linearColorScale = getSequentialSchemeRegistry()
|
|
.get(linearColorScheme)
|
|
.createLinearScale(d3Extent(data, v => v.metric));
|
|
const colorScale = CategoricalColorNamespace.getScale(colorScheme);
|
|
|
|
const colorMap = {};
|
|
data.forEach(d => {
|
|
colorMap[d.country_id] = colorScheme
|
|
? colorScale(d.country_id, sliceId)
|
|
: linearColorScale(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 map = maps[country];
|
|
if (map) {
|
|
drawMap(map);
|
|
} else {
|
|
const url = countries[country];
|
|
d3.json(url, (error, mapData) => {
|
|
if (error) {
|
|
const countryName =
|
|
countryOptions.find(x => x[0] === country)?.[1] || country;
|
|
d3.select(element).html(
|
|
`<div class="alert alert-danger">Could not load map data for ${countryName}</div>`,
|
|
);
|
|
} else {
|
|
maps[country] = mapData;
|
|
drawMap(mapData);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
CountryMap.displayName = 'CountryMap';
|
|
CountryMap.propTypes = propTypes;
|
|
|
|
export default CountryMap;
|