[SIP-5] Repair and refactor CountryMap (#5721)

* Extract slice and formData

* update css indent

* remove no-effect call

* improve text label

* adjust text size

* fix bound calculation

* use string literal

* make path constant
This commit is contained in:
Krist Wongsuphasawat 2018-08-30 14:43:40 -07:00 committed by Chris Williams
parent 9f2b502eb6
commit f72cdc38df
2 changed files with 148 additions and 93 deletions

View File

@ -1,5 +1,5 @@
.country_map svg { .country_map svg {
background-color: #feffff; background-color: #feffff;
} }
.country_map { .country_map {
@ -7,30 +7,36 @@
} }
.country_map .background { .country_map .background {
fill: rgba(255,255,255,0); fill: rgba(255,255,255,0);
pointer-events: all; pointer-events: all;
} }
.country_map .map-layer { .country_map .map-layer {
fill: #fff; fill: #fff;
stroke: #aaa; stroke: #aaa;
} }
.country_map .effect-layer { .country_map .effect-layer {
pointer-events: none; pointer-events: none;
} }
.country_map text { .country_map .text-layer {
font-weight: 300; color: #333333;
color: #333333; text-anchor: middle;
pointer-events: none;
}
.country_map text.result-text {
font-weight: 300;
font-size: 24px;
} }
.country_map text.big-text { .country_map text.big-text {
font-size: 30px; font-weight: 700;
font-weight: 400; font-size: 16px;
color: #333333;
} }
.country_map path.region { .country_map path.region {
cursor: pointer; cursor: pointer;
stroke: #eee;
} }

View File

@ -1,83 +1,112 @@
import d3 from 'd3'; import d3 from 'd3';
import './country_map.css'; import PropTypes from 'prop-types';
import { colorScalerFactory } from '../modules/colors'; import { colorScalerFactory } from '../modules/colors';
import './country_map.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,
};
function countryMapChart(slice, payload) { const maps = {};
// CONSTANTS
const fd = payload.form_data;
let path;
let g;
let bigText;
let resultText;
const container = slice.container;
const data = payload.data;
const format = d3.format(fd.number_format);
const colorScaler = colorScalerFactory(fd.linear_color_scheme, data, v => v.metric); function CountryMap(element, props) {
PropTypes.checkPropTypes(propTypes, props, 'prop', 'CountryMap');
const {
data,
width,
height,
country,
linearColorScheme,
mapBaseUrl = '/static/assets/src/visualizations/countries',
numberFormat,
} = props;
const container = element;
const format = d3.format(numberFormat);
const colorScaler = colorScalerFactory(linearColorScheme, data, v => v.metric);
const colorMap = {}; const colorMap = {};
data.forEach((d) => { data.forEach((d) => {
colorMap[d.country_id] = colorScaler(d.metric); colorMap[d.country_id] = colorScaler(d.metric);
}); });
const colorFn = d => colorMap[d.properties.ISO] || 'none'; const colorFn = d => colorMap[d.properties.ISO] || 'none';
let centered; const path = d3.geo.path();
path = d3.geo.path(); const div = d3.select(container);
d3.select(slice.selector).selectAll('*').remove(); div.selectAll('*').remove();
const div = d3.select(slice.selector) container.style.height = `${height}px`;
.append('svg:svg') container.style.width = `${width}px`;
.attr('width', slice.width()) const svg = div.append('svg:svg')
.attr('height', slice.height()) .attr('width', width)
.attr('height', height)
.attr('preserveAspectRatio', 'xMidYMid meet'); .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');
container.css('height', slice.height()); let centered;
container.css('width', slice.width());
const clicked = function (d) { const clicked = function (d) {
const hasCenter = d && centered !== d;
let x; let x;
let y; let y;
let k; let k;
let bigTextX; const halfWidth = width / 2;
let bigTextY; const halfHeight = height / 2;
let bigTextSize;
let resultTextX;
let resultTextY;
if (d && centered !== d) { if (hasCenter) {
const centroid = path.centroid(d); const centroid = path.centroid(d);
x = centroid[0]; x = centroid[0];
y = centroid[1]; y = centroid[1];
bigTextX = centroid[0];
bigTextY = centroid[1] - 40;
resultTextX = centroid[0];
resultTextY = centroid[1] - 40;
bigTextSize = '6px';
k = 4; k = 4;
centered = d; centered = d;
} else { } else {
x = slice.width() / 2; x = halfWidth;
y = slice.height() / 2; y = halfHeight;
bigTextX = 0;
bigTextY = 0;
resultTextX = 0;
resultTextY = 0;
bigTextSize = '30px';
k = 1; k = 1;
centered = null; centered = null;
} }
g.transition() g.transition()
.duration(750) .duration(750)
.attr('transform', 'translate(' + slice.width() / 2 + ',' + slice.height() / 2 + ')scale(' + k + ')translate(' + -x + ',' + -y + ')'); .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() bigText.transition()
.duration(750) .duration(750)
.attr('transform', 'translate(0,0)translate(' + bigTextX + ',' + bigTextY + ')') .style('font-size', hasCenter ? 6 : 16);
.style('font-size', bigTextSize);
resultText.transition() resultText.transition()
.duration(750) .duration(750)
.attr('transform', 'translate(0,0)translate(' + resultTextX + ',' + resultTextY + ')'); .style('font-size', hasCenter ? 16 : 24);
}; };
backgroundRect.on('click', clicked);
const selectAndDisplayNameOfRegion = function (feature) { const selectAndDisplayNameOfRegion = function (feature) {
let name = ''; let name = '';
if (feature && feature.properties) { if (feature && feature.properties) {
@ -114,44 +143,29 @@ function countryMapChart(slice, payload) {
resultText.text(''); resultText.text('');
}; };
div.append('rect') function drawMap(mapData) {
.attr('class', 'background')
.attr('width', slice.width())
.attr('height', slice.height())
.on('click', clicked);
g = div.append('g');
const mapLayer = g.append('g')
.classed('map-layer', true);
bigText = g.append('text')
.classed('big-text', true)
.attr('x', 20)
.attr('y', 45);
resultText = g.append('text')
.classed('result-text', true)
.attr('x', 20)
.attr('y', 60);
const url = `/static/assets/src/visualizations/countries/${fd.select_country.toLowerCase()}.geojson`;
d3.json(url, function (error, mapData) {
const features = mapData.features; const features = mapData.features;
const center = d3.geo.centroid(mapData); const center = d3.geo.centroid(mapData);
let scale = 150; const scale = 100;
let offset = [slice.width() / 2, slice.height() / 2]; const projection = d3.geo.mercator()
let projection = d3.geo.mercator().scale(scale).center(center) .scale(scale)
.translate(offset); .center(center)
.translate([width / 2, height / 2]);
path = path.projection(projection); path.projection(projection);
// Compute scale that fits container.
const bounds = path.bounds(mapData); const bounds = path.bounds(mapData);
const hscale = scale * slice.width() / (bounds[1][0] - bounds[0][0]); const hscale = scale * width / (bounds[1][0] - bounds[0][0]);
const vscale = scale * slice.height() / (bounds[1][1] - bounds[0][1]); const vscale = scale * height / (bounds[1][1] - bounds[0][1]);
scale = (hscale < vscale) ? hscale : vscale; const newScale = (hscale < vscale) ? hscale : vscale;
const offsetWidth = slice.width() - (bounds[0][0] + bounds[1][0]) / 2;
const offsetHeigth = slice.height() - (bounds[0][1] + bounds[1][1]) / 2; // Compute bounds and offset using the updated scale.
offset = [offsetWidth, offsetHeigth]; projection.scale(newScale);
projection = d3.geo.mercator().center(center).scale(scale).translate(offset); const newBounds = path.bounds(mapData);
path = path.projection(projection); projection.translate([
width - (newBounds[0][0] + newBounds[1][0]) / 2,
height - (newBounds[0][1] + newBounds[1][1]) / 2,
]);
// Draw each province as a path // Draw each province as a path
mapLayer.selectAll('path') mapLayer.selectAll('path')
@ -164,8 +178,43 @@ function countryMapChart(slice, payload) {
.on('mouseenter', mouseenter) .on('mouseenter', mouseenter)
.on('mouseout', mouseout) .on('mouseout', mouseout)
.on('click', clicked); .on('click', clicked);
}); }
container.show();
const countryKey = country.toLowerCase();
const map = maps[countryKey];
if (map) {
drawMap(map);
} else {
const url = `${mapBaseUrl}/${countryKey}.geojson`;
d3.json(url, function (error, mapData) {
if (!error) {
maps[countryKey] = mapData;
drawMap(mapData);
}
});
}
} }
module.exports = countryMapChart; CountryMap.propTypes = propTypes;
function adaptor(slice, payload) {
const { selector, formData } = slice;
const {
linear_color_scheme: linearColorScheme,
number_format: numberFormat,
select_country: country,
} = formData;
const element = document.querySelector(selector);
return CountryMap(element, {
data: payload.data,
width: slice.width(),
height: slice.height(),
country,
linearColorScheme,
numberFormat,
});
}
export default adaptor;