One chart a day - Summarizing how to make a map with d3 and geojson
UntitledGeoJSON ViewerMaps of Philippines, various techniques
Insert cell
Insert cell
Insert cell
geoJson = await FileAttachment("Philippines_provinces@1.geojson").json()
Insert cell
geoJson.features.filter(function(item){
return item.properties.income_class == undefined;
});
Insert cell
Insert cell
{
// create svg html element
const svg = d3.create('svg')
.attr("viewBox", [0, 0, chart_dimensions.width, chart_dimensions.height]);

// merge geojson and budget data for convenient access of income class code and income group in tooltip
mergeGeoJsonAndBudgetData()
const clippedWidth = chart_dimensions.width - chart_dimensions.margin * 2;
const clippedHeight = chart_dimensions.height - chart_dimensions.margin * 2;

const legend_title = svg.append("g")
legend_title.attr("class", "legend")
.style('font-size', 12)
.style('font-family', 'sans-serif')
.attr("transform", `translate(${centerScale(7)},50)`)
.append("text")
.attr("font-weight", "bold")
.text("Income Class Code")
const legend_group = svg.append("g")
legend_group.attr("transform", `translate(${centerScale(7)},70)`)
.call(legend)
// initialize geoMercator
const geoMercator = d3
.geoMercator()
// the center uses longtitude and latitude
// get Long/Lat data from google maps
.center([199, 36])
.fitSize([clippedWidth, clippedHeight], geoJson);

// initialize geoPath with the help of the mercator
// This will be our data later
const pathGen = d3.geoPath(geoMercator);

// initialize zoom
const zoom = d3.zoom()
.scaleExtent([1, 8])
.on('zoom', zoomed);
// initialize stage
// stage is basically just a group appended to the main svg
const stage = svg
.append('g')
.attr('transform', `translate(${chart_dimensions.margin},${chart_dimensions.margin})`);

// initialize Hover text on the upper left
const textX = 10;
const textY = 10;
const infoText = stage
.append('g')
.attr('transform', `translate(${textX},${textY})`)
.append('text')
.style("font", "20px sans-serif");
infoText.text('no data');

// setup onMouseHover, onMouseMove, onMouseLeave on the polugons
const onMouseHover = d => {
let income_class_text = d.properties.income_class;
let income_group_text = d.properties.income_group;
if (d.properties.income_class == undefined)
{
income_class_text = `<span style="color:red">No information available</span>`
income_group_text = `<span style="color:red">No information available</span>`
}
stage
.selectAll('.geopath')
.filter(td => td.properties.ADM2_EN === d.properties.ADM2_EN)
.attr('fill', 'yellow');
infoText.text(d.properties.ADM2_EN);
tooltip.html(`<div> <strong>Province: </strong>${d.properties.ADM2_EN}</div>
<div> <strong>Region: </strong>${d.properties.ADM1_EN}</div>
<div> <strong>Income Code: </strong>${income_class_text}</div>
<div> <strong>Income Class: </strong>${income_group_text}</div>`)
.style('visibility', 'visible');
};
const onMouseMove = d => {
tooltip
.style('top', d3.event.pageY - 10 + 'px')
.style('left', d3.event.pageX + 10 + 'px');
}

const onMouseLeave = d => {
stage
.selectAll('.geopath')
.filter(td => td.properties.ADM2_EN === d.properties.ADM2_EN)
.attr('fill', function(td) {
if (typeof td.properties.income_class === "number"){
return colorScale(td.properties.income_class)
}
else{
return "white"
}});
infoText.text('Hover on a province');
tooltip.html(``).style('visibility', 'hidden');
};

// initialize tEnter (polygons that composes the map)
// initialize tUpdate, tExit (optional)
const tEnter = enter =>
enter
.append('path')
.attr('d', pathGen)
.attr('stroke', 'purple')
.attr('fill', function(td) {
if (typeof td.properties.income_class === "number"){
return colorScale(td.properties.income_class)
}
else{
return "white"
}})
.classed('geopath', true)
.on('mouseenter', onMouseHover)
.on('mousemove', onMouseMove)
.on('mouseleave', onMouseLeave);
const tUpdate = null;
const tExit = null;
// setup stage
stage
.selectAll('.geopath')
.data(geoJson.features)
.join(tEnter, tUpdate, tExit);

// call zoom
svg.call(zoom);
// zoomed functio nfor zoom
function zoomed() {
stage
.selectAll('path') // To prevent stroke width from scaling
.attr('transform', d3.event.transform);
}
return svg.node();
}
Insert cell
Insert cell
budget_data = {
const text = await FileAttachment("local_gov_finance_data@2.csv").text();
return d3.csvParse(text, (d) => ({
lgu_type: d.lgu_type,
region: d.region,
province: d.province,
lgu_name: d.lgu_name,
income_classification_code: +d.income_classification_code,
population: d.population,
income_group: d.income_group
}));
}
Insert cell
Insert cell
function mergeGeoJsonAndBudgetData (){
var income_classsification_dict = {};
var income_group_dict = {};
budget_data_province.map((e,i) => { income_classsification_dict[e.province]=e.income_classification_code});
budget_data_province.map((e,i) => { income_group_dict[e.province]=e.income_group});
console.log(income_classsification_dict);
for (var property in geoJson.features) {
if (geoJson.features.hasOwnProperty(property)) {
geoJson.features[property].properties.income_class = income_classsification_dict[geoJson.features[property].properties.ADM2_EN];
geoJson.features[property].properties.income_group = income_group_dict[geoJson.features[property].properties.ADM2_EN];
//console.log(geoJson.features[property].properties);
}
}
}
Insert cell
budget_data_province = await get_budget_data_province();
Insert cell
function get_budget_data_province() {
const budget_data_province_temp = budget_data.filter(function(item){
return item.lgu_type == "PROVINCE";
});
budget_data_province_temp.forEach(function(item){
item.province = item.province.toLowerCase().replace(/(^\w{1})|(\s{1}\w{1})/g, match => match.toUpperCase());
item.province = item.province.replace("Del", "del");
console.log(item);
});
//console.log(budget_data_province_temp)
return budget_data_province_temp;
}
Insert cell
uniqueIncomeClassificationCode = [...new Set(budget_data_province.map(({ income_classification_code }) => income_classification_code))]
Insert cell
Insert cell
Insert cell
chart_dimensions = ({
width: 850,
height: 1100,
margin: 50,
})
Insert cell
d3 = require("d3@v5")
Insert cell
color = '#69b3a2'
Insert cell
tooltip = d3
.select('body')
.append('div')
.attr('class', 'tooltip')
.style('position', 'absolute')
.style('z-index', '10')
.style('visibility', 'hidden')
.style('padding', '10px')
.style('background-color', 'rgba(255,255,255,0.90)')
.style('border-radius', '10px')
.style('-webkit-border-radius', '10px')
.style('box-shadow', '0 0 2px 0px white inset, 0 0 1px 0px black')
.style('color', "purple")
.style('font-family','sans-serif')
.text('a simple tooltip');
Insert cell
colorScale = d3.scaleSequential([0, d3.max(budget_data_province, d => d.income_classification_code)], d3.interpolatePurples)
Insert cell
legend = svg => {
const g = svg
.attr("text-anchor", "end")
.attr("font-size", 15)
.selectAll("g")
.data(["1", "2", "3", "4", "5"])
.join("g")
.attr("transform", (d, i) => `translate(${i * 25}, 0)`);

g.append("rect")
.attr("width", 19)
.attr("height", 19)
.attr("fill", colorScale);

g.append("text")
.attr("x", 10)
.attr("y", -10)
.attr("dy", "0.35em")
.text(d => d);
}
Insert cell
centerScale = d3.scaleLinear()
.domain([0,10])
.range([0, chart_dimensions.width]);
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more