Skip to content

Instantly share code, notes, and snippets.

@curran
Last active September 28, 2018 11:59
Show Gist options
  • Save curran/f3de248a91fe2995a97aedc6ea0c39ad to your computer and use it in GitHub Desktop.
Save curran/f3de248a91fe2995a97aedc6ea0c39ad to your computer and use it in GitHub Desktop.
Choropleth Map with Interactive Filtering
(function (topojson,d3) {
'use strict';
const loadAndProcessData = () =>
Promise
.all([
d3.tsv('https://unpkg.com/world-atlas@1.1.4/world/50m.tsv'),
d3.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 = topojson.feature(topoJSONdata, topoJSONdata.objects.countries);
countries.features.forEach(d => {
Object.assign(d.properties, rowById[d.id]);
});
return countries;
});
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);
};
const projection = d3.geoNaturalEarth1();
const pathGenerator = d3.geoPath().projection(projection);
const choroplethMap = (selection, props) => {
const {
features,
colorScale,
colorValue,
selectedColorValue
} = props;
console.log(features);
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', selectedColorValue ? 0.05 : 1);
selection.call(d3.zoom().on('zoom', () => {
g.attr('transform', d3.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 =>
(!selectedColorValue || selectedColorValue === colorValue(d))
? 1
: 0.1
)
.classed('highlighted', d =>
selectedColorValue && selectedColorValue === colorValue(d)
);
countryPathsEnter.append('title')
.text(d => d.properties.name + ': ' + colorValue(d));
};
const svg = d3.select('svg');
const choroplethMapG = svg.append('g');
const colorLegendG = svg.append('g')
.attr('transform', `translate(40,310)`);
const colorScale = d3.scaleOrdinal();
// const colorValue = d => d.properties.income_grp;
const colorValue = d => d.properties.economy;
let selectedColorValue;
let features;
const onClick = d => {
selectedColorValue = d;
render();
};
loadAndProcessData().then(countries => {
features = countries.features;
render();
});
const render = () => {
colorScale
.domain(features.map(colorValue))
.domain(colorScale.domain().sort().reverse())
.range(d3.schemeSpectral[colorScale.domain().length]);
colorLegendG.call(colorLegend, {
colorScale,
circleRadius: 8,
spacing: 20,
textOffset: 12,
backgroundRectWidth: 235,
onClick,
selectedColorValue
});
choroplethMapG.call(choroplethMap, {
features,
colorScale,
colorValue,
selectedColorValue
});
};
}(topojson,d3));
import {
geoPath,
geoNaturalEarth1,
zoom,
event
} from 'd3';
const projection = geoNaturalEarth1();
const pathGenerator = geoPath().projection(projection);
export const choroplethMap = (selection, props) => {
const {
features,
colorScale,
colorValue,
selectedColorValue
} = props;
console.log(features);
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', selectedColorValue ? 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 =>
(!selectedColorValue || selectedColorValue === colorValue(d))
? 1
: 0.1
)
.classed('highlighted', d =>
selectedColorValue && selectedColorValue === colorValue(d)
)
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);
}
<!DOCTYPE html>
<html>
<head>
<title>Choropleth Map with Interactive Filtering</title>
<link rel="stylesheet" href="styles.css">
<script src="https://unpkg.com/d3@5.6.0/dist/d3.min.js"></script>
<script src="https://unpkg.com/topojson@3.0.2/dist/topojson.min.js"></script>
</head>
<body>
<svg width="960" height="500"></svg>
<script src="bundle.js"></script>
</body>
</html>
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;
let selectedColorValue;
let features;
const onClick = d => {
selectedColorValue = d;
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,
onClick,
selectedColorValue
});
choroplethMapG.call(choroplethMap, {
features,
colorScale,
colorValue,
selectedColorValue
});
};
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"
},
"devDependencies": {
"rollup": "latest"
}
}
export default {
input: 'index.js',
external: ['d3'],
output: {
file: 'bundle.js',
format: 'iife',
sourcemap: true,
globals: { d3: 'd3' }
}
};
body {
margin: 0px;
overflow: hidden;
}
.sphere {
fill: #4242e4;
}
.country {
stroke: black;
stroke-width: 0.05px;
}
.country.highlighted {
stroke-width: 0.5px;
}
.country:hover {
fill: red;
}
.tick {
cursor: pointer;
}
.tick text {
font-size: 1em;
fill: black;
font-family: sans-serif;
}
.tick circle {
stroke: black;
stroke-opacity: 0.5;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment