A choropleth map with selectable countries.
This example demonstrates the changes neede for migration from VizHub to local development. Namely:
package.json
.index.html
.rollup.config.js
.import {
select,
scaleOrdinal,
schemeSpectral
} from 'd3';
import { loadAndProcessData } from './loadAndProcessData';
import { colorLegend } from './colorLegend';
import { choroplethMap } from './choroplethMap';
const svg = select('svg');
const choroplethMapG = svg.append('g');
const colorLegendG = svg.append('g')
.attr('transform', `translate(40,310)`);
const colorScale = scaleOrdinal();
// const colorValue = d => d.properties.income_grp;
const colorValue = d => d.properties.economy;
// State
let features;
let selectedCountryId;
const onCountryClick = id => {
selectedCountryId = id;
render();
};
loadAndProcessData().then(countries => {
features = countries.features;
render();
});
const render = () => {
colorScale
.domain(features.map(colorValue))
.domain(colorScale.domain().sort().reverse())
.range(schemeSpectral[colorScale.domain().length]);
colorLegendG.call(colorLegend, {
colorScale,
circleRadius: 8,
spacing: 20,
textOffset: 12,
backgroundRectWidth: 235
});
choroplethMapG.call(choroplethMap, {
features,
colorScale,
colorValue,
selectedCountryId,
onCountryClick
});
};
<!DOCTYPE html>
<html>
<head>
<title>Map with Selectable Countries</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<svg width="960" height="500"></svg>
<script src="bundle.js"></script>
</body>
</html>
import {
geoPath,
geoNaturalEarth1,
zoom,
event
} from 'd3';
const projection = geoNaturalEarth1();
const pathGenerator = geoPath().projection(projection);
export const choroplethMap = (selection, props) => {
const {
features,
colorScale,
colorValue,
selectedCountryId,
onCountryClick
} = props;
const gUpdate = selection.selectAll('g').data([null]);
const gEnter = gUpdate.enter().append('g');
const g = gUpdate.merge(gEnter);
gEnter
.append('path')
.attr('class', 'sphere')
.attr('d', pathGenerator({type: 'Sphere'}))
.merge(gUpdate.select('.sphere'))
.attr('opacity', selectedCountryId ? 0.05 : 1);
selection.call(zoom().on('zoom', () => {
g.attr('transform', event.transform);
}));
const countryPaths = g.selectAll('.country')
.data(features);
const countryPathsEnter = countryPaths
.enter().append('path')
.attr('class', 'country');
countryPaths
.merge(countryPathsEnter)
.attr('d', pathGenerator)
.attr('fill', d => colorScale(colorValue(d)))
.attr('opacity', d =>
(!selectedCountryId || selectedCountryId === d.id)
? 1
: 0.1
)
.classed('highlighted', d =>
selectedCountryId && selectedCountryId === d.id
)
.on('click', d => {
if (selectedCountryId && selectedCountryId === d.id){
onCountryClick(null);
} else {
onCountryClick(d.id);
}
});
countryPathsEnter.append('title')
.text(d => d.properties.name + ': ' + colorValue(d));
};
export const colorLegend = (selection, props) => {
const {
colorScale,
circleRadius,
spacing,
textOffset,
backgroundRectWidth,
onClick,
selectedColorValue
} = props;
const backgroundRect = selection.selectAll('rect')
.data([null]);
const n = colorScale.domain().length;
backgroundRect.enter().append('rect')
.merge(backgroundRect)
.attr('x', -circleRadius * 2)
.attr('y', -circleRadius * 2)
.attr('rx', circleRadius * 2)
.attr('width', backgroundRectWidth)
.attr('height', spacing * n + circleRadius * 2)
.attr('fill', 'white')
.attr('opacity', 0.8);
const groups = selection.selectAll('.tick')
.data(colorScale.domain());
const groupsEnter = groups
.enter().append('g')
.attr('class', 'tick');
groupsEnter
.merge(groups)
.attr('transform', (d, i) =>
`translate(0, ${i * spacing})`
)
.attr('opacity', d =>
(!selectedColorValue || d === selectedColorValue)
? 1
: 0.2
)
.on('click', d => onClick(
d === selectedColorValue
? null
: d
));
groups.exit().remove();
groupsEnter.append('circle')
.merge(groups.select('circle'))
.attr('r', circleRadius)
.attr('fill', colorScale);
groupsEnter.append('text')
.merge(groups.select('text'))
.text(d => d)
.attr('dy', '0.32em')
.attr('x', textOffset);
}
import { feature } from 'topojson';
import { tsv, json } from 'd3';
export const loadAndProcessData = () =>
Promise
.all([
tsv('https://unpkg.com/world-atlas@1.1.4/world/50m.tsv'),
json('https://unpkg.com/world-atlas@1.1.4/world/50m.json')
])
.then(([tsvData, topoJSONdata]) => {
const rowById = tsvData.reduce((accumulator, d) => {
accumulator[d.iso_n3] = d;
return accumulator;
}, {});
const countries = feature(topoJSONdata, topoJSONdata.objects.countries);
countries.features.forEach(d => {
Object.assign(d.properties, rowById[d.id]);
});
return countries;
});
{
"scripts": {
"build": "rollup -c",
"start": "http-server"
},
"devDependencies": {
"http-server": "^0.11.1",
"rollup": "^1.10.1",
"rollup-plugin-node-resolve": "^4.2.3"
},
"dependencies": {
"d3": "^5.9.2",
"topojson": "^3.0.2"
}
}
import nodeResolve from 'rollup-plugin-node-resolve';
export default {
input: 'index.js',
output: {
file: 'bundle.js',
format: 'iife',
sourcemap: true,
},
plugins: [ nodeResolve() ]
};
body {
margin: 0px;
overflow: hidden;
}
.sphere {
fill: #4242e4;
}
.country {
stroke: black;
stroke-width: 0.05px;
}
.country.highlighted {
stroke-width: 0.5px;
}
.country {
cursor: pointer;
}
.tick text {
font-size: 1em;
fill: black;
font-family: sans-serif;
}
.tick circle {
stroke: black;
stroke-opacity: 0.5;
}