Public
Edited
May 6, 2024
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
// const width = 560
const height = 520;

const margin = { top: 0, right: 20, bottom: 20, left: 20 };

const vertices = 50; // 12

const ellipse = d3
.range(vertices)
.map((i) => [
(height * (1 + 0.88 * Math.cos((i / (vertices / 2)) * Math.PI))) / 2,
(height * (1 + 0.88 * Math.sin((i / (vertices / 2)) * Math.PI))) / 2
]);

const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height + 0);

const voronoi = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
const labels = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
const pop_labels = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

const bigFormat = d3.format(",.0f");

// Voronoi Treemap Initialization
const seed = seedrandom(20);
const voronoiTreeMap = d3.voronoiTreemap().prng(seed).clip(ellipse);

// Apply Voronoi Treemap to Data
voronoiTreeMap(data_hierarchy);

// Assign colors to the nodes
// colorHierarchy(data_hierarchy)

// Node Processing
const allNodes = data_hierarchy
.descendants()
.sort((a, b) => b.depth - a.depth)
.map((d, i) => Object.assign({}, d, { id: i })); //원래 배열의 각 요소에 id 속성을 추가한 새로운 배열을 반환

// Voronoi Cell Rendering
voronoi
.selectAll("path")
.data(allNodes)
.enter()
.append("path")
.attr("d", (d) => "M" + d.polygon.join("L") + "Z")
.style("fill", (d) => {
// console.log("data test", d.data.age);
return colorSelect2(d.data.sex, d.data.age);
})
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.style("fill-opacity", (d) => (d.depth === 2 ? 0.9 : 0))
.attr("pointer-events", (d) => (d.depth === 2 ? "all" : "none"))
.on("mouseover", function (event, d) {
console.log("test", d);
let label = labels.select(`.label-${d.id}`);
label.style("opacity", 1);
})
.on("mouseout", function (event, d) {
let label = labels.select(`.label-${d.id}`);
label.style("opacity", 0);
})
.transition()
.duration(0)
// .attr('stroke-width', 2)
.attr("stroke-width", (d) => 7 - d.depth * 4);

const key_h = 17;
const keys_women = svg
.selectAll("keys")
.data(key_age)
.enter()
.append("rect")
.attr("x", height + 50)
.attr("y", (d, i) => height / 2 - key_h * 9 + key_h * i)
.attr("width", 10)
.attr("height", key_h)
// .style('fill', 'red')
.attr("stroke", "white")
.style("fill", (d) => colorSelect2("Women", d.age));

const keys_men = svg
.selectAll("keys")
.data(key_age)
.enter()
.append("rect")
.attr("x", height + 50 + 30)
.attr("y", (d, i) => height / 2 - key_h * 9 + key_h * i)
.attr("width", 10)
.attr("height", key_h)
// .style('fill', 'red')
.attr("stroke", "white")
.style("fill", (d) => colorSelect2("Men", d.age));

const keys_text = svg
.selectAll("text")
.data(key_age)
.enter()
.append("text")
.attr("x", height + 50 + 20)
.attr("y", (d, i) => height / 2 - key_h * 9 + key_h * i + key_h / 4)
.text((d) => d.age)
.style("font-size", "10px")
.style("font-family", "Roboto")
.attr("text-anchor", "middle")
.style("fill", "#999");

svg
.append("text")
.text("Women")
.attr("x", height + 50)
.attr("y", (d, i) => height / 2 - key_h * 9 + key_h * i - key_h / 2)
.style("font-size", "12px")
.style("font-family", "Roboto")
.style("font-weight", "bold")
.style("fill", (d) => colorSelect2("Women", 50))
.attr("text-anchor", "end");

svg
.append("text")
.text("Men")
.attr("x", height + 50 + 30 + 10)
.attr("y", (d, i) => height / 2 - key_h * 9 + key_h * i - key_h / 2)
.style("font-size", "12px")
.style("font-family", "Roboto")
.style("font-weight", "bold")
.style("fill", (d) => colorSelect2("Men", 50))
.attr("text-anchor", "start");

// const countryLabel = svg
// .append("text")
// .text(country)
// .attr("x", 10)
// .attr("y", 70)
// .style("font-size", "28px")
// // .style("font-family", "Roboto")
// .style("font-weight", "bold")
// .style("fill", "#111")
// .attr("text-anchor", "start");

// const yearLabel = svg
// .append("text")
// .text(year)
// .attr("x", 10)
// .attr("y", 100)
// .style("font-size", "18px")
// // .style("font-family", "Roboto")
// // .style("font-weight", "bold")
// .style("fill", "#111")
// .attr("text-anchor", "start");

// Voroni Labels
labels
.selectAll("text")
.data(allNodes.filter((d) => d.depth === 2))
.enter()
.append("text")
.attr("class", (d) => {
// console.log("class", d);
return `label-${d.id}`;
})
.attr("text-anchor", "middle")
.attr(
"transform",
(d) => "translate(" + [d.polygon.site.x, d.polygon.site.y + 6] + ")"
)
.text((d) => d.data.name)
.attr("opacity", (d) => (d.data.value > 10 ? 1 : 0))
.attr("cursor", "default")
.attr("pointer-events", "none")
// .attr("fill", "white")
.attr("fill", (d) => (d.data.age >= 30 ? "#fff" : "#333"))
.attr("font-size", "11px")
.style("font-family", "Roboto")
.style("opacity", 0);

// pop_labels
// .selectAll("text")
// .data(allNodes.filter((d) => d.depth === 1))
// .enter()
// .append("text")
// .attr("class", (d) => `label-${d.id}`)
// .attr("text-anchor", "middle")
// .attr(
// "transform",
// (d) => "translate(" + [d.polygon.site.x, d.polygon.site.y + 22.5] + ")"
// )
// .text((d) => `${bigFormat(d.data.value)}`)
// .attr("opacity", (d) => (d.data.value > 10 ? 1 : 0))
// .attr("fill", (d) => (d.data.age >= 30 ? "#fff" : "#333"))
// .style("fill-opacity", 1)
// .style("font-size", "9px")
// .style("font-family", "Roboto");

return svg.node();
}
Insert cell
Insert cell
colorSelect2 = (sex, age) => {
const colorScaleFemale = d3
.scaleLinear()
.domain([0, 30, 85])
.range(["#f8f2fb", "#c86fe6", "#15001c"]);

const colorScaleMale = d3
.scaleLinear()
.domain([0, 30, 85])
.range(["#ebf7f1", "#04a95d", "#000b06"]);

return sex === "Women" ? colorScaleFemale(age) : colorScaleMale(age);
}
Insert cell
Insert cell
Insert cell
Insert cell
data_hierarchy = d3.hierarchy(nestedByGenderAge)
.sum(d => d.value)
.sort((a, b) => b.value - a.value);
Insert cell
nestedByGenderAge = ({
name: 'root',
children: Array.from(d3.group(filtered_data, d => d.Sex), ([key, values]) => ({
name: key,
children: Array.from(d3.group(values, d => d.Age), ([key2, values2]) => ({
name: key2,
sex: key,
age: parseInt(key2.split(" ")[0]),
value: d3.sum(values2, d => d.Value)
}))
}))
})
Insert cell
nestedByAge = ({
name: 'root',
children: Array.from(d3.group(filtered_data, d => d.Age), ([key, values]) => ({
name: key,
age: parseInt(key.split(" ")[0]),
value: d3.sum(values, d => d.Value)
}))
})
Insert cell
Insert cell
key_age = [...new Set(filtered_data.map(d => d.Age))].map(d => ({
key: d,
age: parseInt(d.split(" ")[0])
}))
Insert cell
Insert cell
countries = [...new Set(raw_data.map(d => d.Country))].sort(d3.ascending)
Insert cell
raw_data = d3.csvParse(await FileAttachment("HISTPOP_12092023093713334.csv").text(), d3.autoType)
Insert cell
Insert cell
Insert cell
// d3 = require("d3@6", "d3-voronoi-treemap")
Insert cell
d3 = require("d3@6", "d3-weighted-voronoi", "d3-voronoi-map", "d3-voronoi-treemap")
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