From f72cdc38dfcc2d1bf5574a8e0204927090225720 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Thu, 30 Aug 2018 14:43:40 -0700 Subject: [PATCH] [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 --- .../assets/src/visualizations/country_map.css | 32 +-- .../assets/src/visualizations/country_map.js | 213 +++++++++++------- 2 files changed, 150 insertions(+), 95 deletions(-) diff --git a/superset/assets/src/visualizations/country_map.css b/superset/assets/src/visualizations/country_map.css index 8a16105aaaec8..7bde0c8c6976f 100644 --- a/superset/assets/src/visualizations/country_map.css +++ b/superset/assets/src/visualizations/country_map.css @@ -1,5 +1,5 @@ .country_map svg { - background-color: #feffff; + background-color: #feffff; } .country_map { @@ -7,30 +7,36 @@ } .country_map .background { - fill: rgba(255,255,255,0); - pointer-events: all; + fill: rgba(255,255,255,0); + pointer-events: all; } .country_map .map-layer { - fill: #fff; - stroke: #aaa; + fill: #fff; + stroke: #aaa; } .country_map .effect-layer { - pointer-events: none; + pointer-events: none; } -.country_map text { - font-weight: 300; - color: #333333; +.country_map .text-layer { + color: #333333; + text-anchor: middle; + pointer-events: none; +} + +.country_map text.result-text { + font-weight: 300; + font-size: 24px; } .country_map text.big-text { - font-size: 30px; - font-weight: 400; - color: #333333; + font-weight: 700; + font-size: 16px; } .country_map path.region { - cursor: pointer; + cursor: pointer; + stroke: #eee; } diff --git a/superset/assets/src/visualizations/country_map.js b/superset/assets/src/visualizations/country_map.js index 09d325de6781e..92c799bff3d9f 100644 --- a/superset/assets/src/visualizations/country_map.js +++ b/superset/assets/src/visualizations/country_map.js @@ -1,83 +1,112 @@ import d3 from 'd3'; -import './country_map.css'; +import PropTypes from 'prop-types'; import { colorScalerFactory } from '../modules/colors'; +import './country_map.css'; - -function countryMapChart(slice, payload) { - // 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); +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) { + 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 = {}; data.forEach((d) => { colorMap[d.country_id] = colorScaler(d.metric); }); const colorFn = d => colorMap[d.properties.ISO] || 'none'; - let centered; - path = d3.geo.path(); - d3.select(slice.selector).selectAll('*').remove(); - const div = d3.select(slice.selector) - .append('svg:svg') - .attr('width', slice.width()) - .attr('height', slice.height()) + const path = d3.geo.path(); + const div = d3.select(container); + 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'); - container.css('height', slice.height()); - container.css('width', slice.width()); + let centered; const clicked = function (d) { + const hasCenter = d && centered !== d; let x; let y; let k; - let bigTextX; - let bigTextY; - let bigTextSize; - let resultTextX; - let resultTextY; + const halfWidth = width / 2; + const halfHeight = height / 2; - if (d && centered !== d) { + if (hasCenter) { const centroid = path.centroid(d); x = centroid[0]; y = centroid[1]; - bigTextX = centroid[0]; - bigTextY = centroid[1] - 40; - resultTextX = centroid[0]; - resultTextY = centroid[1] - 40; - bigTextSize = '6px'; k = 4; centered = d; } else { - x = slice.width() / 2; - y = slice.height() / 2; - bigTextX = 0; - bigTextY = 0; - resultTextX = 0; - resultTextY = 0; - bigTextSize = '30px'; + x = halfWidth; + y = halfHeight; k = 1; centered = null; } g.transition() .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() .duration(750) - .attr('transform', 'translate(0,0)translate(' + bigTextX + ',' + bigTextY + ')') - .style('font-size', bigTextSize); + .style('font-size', hasCenter ? 6 : 16); resultText.transition() .duration(750) - .attr('transform', 'translate(0,0)translate(' + resultTextX + ',' + resultTextY + ')'); + .style('font-size', hasCenter ? 16 : 24); }; + backgroundRect.on('click', clicked); + const selectAndDisplayNameOfRegion = function (feature) { let name = ''; if (feature && feature.properties) { @@ -114,44 +143,29 @@ function countryMapChart(slice, payload) { resultText.text(''); }; - div.append('rect') - .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) { + function drawMap(mapData) { const features = mapData.features; const center = d3.geo.centroid(mapData); - let scale = 150; - let offset = [slice.width() / 2, slice.height() / 2]; - let projection = d3.geo.mercator().scale(scale).center(center) - .translate(offset); - - path = path.projection(projection); - + 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 * slice.width() / (bounds[1][0] - bounds[0][0]); - const vscale = scale * slice.height() / (bounds[1][1] - bounds[0][1]); - scale = (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; - offset = [offsetWidth, offsetHeigth]; - projection = d3.geo.mercator().center(center).scale(scale).translate(offset); - path = path.projection(projection); + 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') @@ -164,8 +178,43 @@ function countryMapChart(slice, payload) { .on('mouseenter', mouseenter) .on('mouseout', mouseout) .on('click', clicked); + } + + 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); + } + }); + } + +} + +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, }); - container.show(); } -module.exports = countryMapChart; +export default adaptor;