viewof choropleth = {
const width = 960, height = 580;
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);
const projection = d3.geoNaturalEarth1().scale(160).translate([width / 2, height / 2 + 20]);
const path = d3.geoPath().projection(projection);
const countries = topojson.feature(world, world.objects.countries).features;
const dataMap = new Map(data.map(d => [d.Country.toLowerCase(), d]));
const nameOverrides = {
"united states of america": "united states",
"russian federation": "russia",
"czechia": "czech republic",
"south korea": "korea, south",
"north macedonia": "macedonia",
"viet nam": "vietnam",
"taiwan": "taiwan",
"bosnia and herzegovina": "bosnia",
"slovakia": "slovak republic"
};
const colorScaleCost = d3.scaleThreshold()
.domain([20, 30, 40, 45, 50, 55, 60, 65, 80])
.range([
"#13ad09", "#18d80c", "#2bf41e", "#c3f95e", "#dff95e",
"#f9e25e", "#f9ba5e", "#f9995e", "#f97a5e", "#f95e5e"
]);
const colorScaleQuality = d3.scaleThreshold()
.domain([80, 100, 120, 130, 140, 150, 160, 170, 180])
.range([
"#f95e5e", "#f97a5e", "#f9995e", "#f9ba5e", "#f9e25e",
"#dff95e", "#c3f95e", "#2bf41e", "#18d80c", "#13ad09"
]);
const tooltip = d3.select("body").append("div")
.style("position", "absolute")
.style("background", "#fff")
.style("padding", "6px 12px")
.style("border", "1px solid #ccc")
.style("border-radius", "4px")
.style("font", "12px sans-serif")
.style("pointer-events", "none")
.style("opacity", 0);
const baseLayer = svg.append("g").attr("transform", "translate(0, 40)");
const topLayer = svg.append("g").attr("transform", "translate(0, 40)");
const clip = svg.append("clipPath")
.attr("id", "clip-slider")
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width / 2)
.attr("height", height);
topLayer.attr("clip-path", "url(#clip-slider)");
const legendWidth = 180;
const defs = svg.append("defs");
function createGradient(id, colors) {
const gradient = defs.append("linearGradient")
.attr("id", id)
.attr("x1", "0%")
.attr("x2", "100%")
.attr("y1", "0%")
.attr("y2", "0%");
const n = colors.length;
colors.forEach((color, i) => {
gradient.append("stop")
.attr("offset", `${(i / (n - 1)) * 100}%`)
.attr("stop-color", color);
});
}
createGradient("cost-gradient", colorScaleCost.range());
createGradient("quality-gradient", colorScaleQuality.range());
const costLegend = svg.append("g")
.attr("transform", `translate(30,${height - 50})`)
.style("pointer-events", "none");
const qualityLegend = svg.append("g")
.attr("transform", `translate(${width - 30 - legendWidth},${height - 50})`)
.style("pointer-events", "none");
let costMarker = costLegend.append("polygon").attr("fill", "#000").style("opacity", 0);
let qualityMarker = qualityLegend.append("polygon").attr("fill", "#000").style("opacity", 0);
function drawGradientLegend(group, title, gradientId) {
group.append("text")
.text(title)
.attr("y", -8)
.attr("font-size", 12)
.attr("font-weight", "bold");
group.append("rect")
.attr("width", legendWidth)
.attr("height", 10)
.attr("fill", `url(#${gradientId})`);
group.append("text").attr("x", 0).attr("y", 22).attr("font-size", 10).text("Low");
group.append("text").attr("x", legendWidth).attr("y", 22).attr("text-anchor", "end").attr("font-size", 10).text("High");
}
drawGradientLegend(costLegend, "Cost of Living Index", "cost-gradient");
drawGradientLegend(qualityLegend, "Quality of Life Index", "quality-gradient");
function drawMap(group, metric, scale, marker, min, max) {
const colors = scale.range();
group.selectAll("path")
.data(countries)
.join("path")
.attr("d", path)
.attr("fill", d => {
const rawName = d.properties.name.toLowerCase();
const key = nameOverrides[rawName] || rawName;
const entry = dataMap.get(key);
return entry ? scale(entry[metric]) : "#eee";
})
.attr("stroke", "#ccc")
.on("mousemove", (event, d) => {
const rawName = d.properties.name.toLowerCase();
const key = nameOverrides[rawName] || rawName;
const entry = dataMap.get(key);
if (!entry) return tooltip.style("opacity", 0);
const value = entry[metric];
const domain = scale.domain();
const idx = domain.findIndex(t => value < t);
const segment = idx === -1 ? domain.length : idx;
const markerX = (legendWidth / colors.length) * segment + 5;
marker.raise().attr("points", `${markerX - 4},-5 ${markerX + 4},-5 ${markerX},5`).style("opacity", 1);
tooltip.style("opacity", 1)
.html(`<b>${d.properties.name}</b><br>${metric}: ${value.toFixed(1)}`)
.style("left", event.pageX + 10 + "px")
.style("top", event.pageY - 28 + "px");
})
.on("mouseout", () => {
tooltip.style("opacity", 0);
marker.style("opacity", 0);
});
}
drawMap(baseLayer, "Quality of Life Index", colorScaleQuality, qualityMarker, 80, 180);
drawMap(topLayer, "Cost of Living Index", colorScaleCost, costMarker, 20, 80);
let xPos = width / 2;
const sliderArrow = svg.append("text")
.attr("text-anchor", "middle")
.attr("font-size", 16)
.attr("font-weight", "bold")
.attr("fill", "#555")
.text("⇔");
const slider = svg.append("line")
.attr("stroke", "#000")
.attr("stroke-width", 2)
.style("cursor", "ew-resize")
.call(d3.drag().on("drag", function (event) {
xPos = Math.max(0, Math.min(width, event.x));
svg.select("#clip-slider rect").attr("width", xPos);
slider.attr("x1", xPos).attr("x2", xPos);
sliderArrow.attr("x", xPos);
}));
slider.attr("x1", xPos).attr("x2", xPos).attr("y1", 40).attr("y2", height);
sliderArrow.attr("x", xPos).attr("y", height / 2);
svg.append("text")
.attr("x", width / 2)
.attr("y", 25)
.attr("text-anchor", "middle")
.attr("font-size", 22)
.attr("font-weight", "600")
.text("Comparing Cost of Living and Quality of Life Across Countries");
svg.append("text")
.attr("x", width - 10)
.attr("y", height - 5)
.attr("text-anchor", "end")
.attr("font-size", 11)
.attr("fill", "#666")
.text("Source: Numbeo.com, an online database of user-contributed data");
return svg.node();
}