Public
Edited
Jun 1, 2024
Insert cell
Insert cell
Insert cell
{
const squareSize = 6;
const numberOfSquaresRow = 5;
const total = d3.sum(grain.filter(d => d.year == 2021), (d) => d.production);
const gap = 0;
// Define projection
const projection = d3.geoMercator()
.scale(2200)
.center([2,49.5])
.rotate([-30,0,0])
const path = d3.geoPath().projection(projection);

const svg = d3.create("svg")
.attr("width", 975)
.attr("height", 610)
.attr("viewBox", [0, 0, 975, 610])
.attr("style", "width: 100%; height: auto; height: intrinsic;");

// Append country polygon
const regions = svg.append("g")
.selectAll('path')
.data(ukraine.features)
.join('path')

regions
.attr("fill", "#ddd")
.attr("stroke", "#eeeeee")
.attr("stroke-width", 1)
.attr("d", path);

// Append region groups and place them at the respective coordinates
const waffles = svg.selectAll(".waffle")
.append("g")
.data(grain_nested)
.enter().append("g")
.attr('class', 'waffle')
.attr("transform", d => `translate(${projection([d.lon, d.lat])})`)
// Append waffles by region
waffles
.selectAll("rect")
.data(d => waffleData(d.children))
.join("rect")
.attr("width", squareSize)
.attr("height", squareSize)
.attr("fill", (d) => color(d.crop))
.attr("stroke", "white")
.attr("stroke-width", 0.7)
.attr("y", (d, i, arr) => {
// Arrange squares by row
const numberOfSquaresCol = Math.ceil(arr.length / numberOfSquaresRow);
const waffleHeight = squareSize * numberOfSquaresCol + numberOfSquaresCol * gap + squareSize;
const row = Math.floor(i / numberOfSquaresRow);
return waffleHeight - 2 * squareSize - (row * squareSize + row * gap);
})
.attr("x", function (d, i) {
const col = i % numberOfSquaresRow;
return col * squareSize + col * gap;
})
.attr("transform", (d, i, arr) => {
const numberOfSquaresCol = Math.ceil(arr.length / numberOfSquaresRow);
const waffleHeight = squareSize * numberOfSquaresCol + numberOfSquaresCol * gap + squareSize;
const waffleWidth = squareSize * numberOfSquaresRow + numberOfSquaresRow * gap + squareSize;
return `translate(${d.region === "Kirovohrad" ? 0 : -waffleWidth/2 }, ${-waffleHeight * 0.7})`
})

// Add interactive tooltips
waffles
.on('touchmove mousemove', function(e,d) {
d3.select(this)
.raise()
.call(g => g
.selectAll('rect')
.transition()
.duration(100)
.style('stroke', 'black'))
.call(g => g
.append('g')
.attr('class', 'tooltip')
.call(callout, `${d.region} region \n ${arrayJoin(d.children, "crop", "production")}`)) // callout is an external function
})
.on('touchend mouseleave', function(e,d) {
d3.select(this)
.call(g => g
.selectAll('rect')
.transition()
.duration(100)
.style('stroke', 'white'))
.call(g => g.selectAll(".tooltip")
.remove())

})
return svg.node()
}
Insert cell
Insert cell
// Sample waffle chart
waffle(grain_nested[0].children)
Insert cell
Insert cell
waffleData(grain_nested[0].children)
Insert cell
// Convert data to array of squares
waffleData = (data, squareValue = 100) => {

let waffleData = [];
data.forEach(function (d, i) {
d.production = +d.production;
d.units = Math.floor(d.production / squareValue); //Number of cells to draw
waffleData = waffleData.concat(
Array(d.units + 1)
.join(1)
.split("")
.map(() => {
return {
squareValue,
region: d.region,
units: d.units,
production: d.production,
crop: d.crop
};
})
);
});

return waffleData;
}
Insert cell
// Join crop production data into a single string for tooltip
function arrayJoin(arr, type, value) {
var results = [];
arr.forEach(function (item) {
results.push(`${item[type]}: ${Math.round(item[value])}`);
});
return results.join("\n");
}
Insert cell
// transform data into a nested dataset for waffle chart
grain_nested = {
const region_centroids = geo.centroid(ukraine).features
.map(d => ({name: d.properties.name, lon: +d.geometry.coordinates[0], lat: +d.geometry.coordinates[1]}));

const grain_alt = grain.filter(d => d.year == 2021)
.map(d => ({...d, region_alt: name_mapping[d.region]}))

const grain_coord = aq.from(grain_alt)
.join_left(aq.from(region_centroids), ["region_alt", "name"])
.select("year","region","crop","production","lon","lat")
.objects()

return d3.flatGroup(grain_coord, d => d.region, d => d.lon, d => d.lat)
.map(([region, lon, lat, children]) => ({region, lon, lat, children}))
}
Insert cell
// Mapping between different name spellings in .csv and .geojson files
name_mapping = {
const reg_old = ukraine.features.map(d => d.properties.name).sort()
const reg_new = ["Cherkasy", "Chernihiv", "Chernivtsi", "Crimea", "Dnipropetrovsk", "Donetsk", "Ivano-Frankivsk", "Kharkiv", "Kherson", "Khmelnytskiy", "Kyiv", "Kirovohrad", "Lviv", "Luhansk", "Mykolayiv", "Odesa", "Poltava", "Rivne", "Sumy", "Ternopil", "Zakarpattya", "Vinnytsya", "Volyn", "Zaporizhzhya", "Zhytomyr"]
return _.zipObject(reg_new, reg_old) // merge two arrays into key-value pairs
}
Insert cell
grain = FileAttachment("UKR grain 2021-2022@3.csv").csv({typed: true})
Insert cell
geo = require("geotoolbox@2")
Insert cell
ukraine.features
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more