code / public /Figure_8a_sunburst.html
Laura Wagner
to commit or not commit that is the question
5f5806d
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font-family: "Segoe UI", sans-serif;
text-align: center;
margin-top: 20px;
}
svg {
font: 10px sans-serif;
}
.label {
font-weight: bold;
font-size: 12px;
text-anchor: middle;
}
.legend {
font-size: 12px;
text-anchor: start;
}
.legend rect {
stroke-width: 1;
stroke: #000;
}
</style>
<body>
<h2>Sunburst Chart: Country → Profession</h2>
<button id="downloadBtn">Download SVG</button>
<svg width="800" height="800"></svg>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script>
const width = 800;
const radius = width / 3;
const colorMap = {
"Adult Performer": "#8A2BE2",
"Model": "#DC143C",
"Actor": "#FF7F50",
"Performer": "magenta",
"Singer, Musician": "wheat",
"TV Personality": "#708090",
"Sports Professional": "gold",
"Public Figure": "maroon",
"Voice Actor": "lightgreen",
"Online Personality": "#4682B4",
"Other": "#ccc"
};
// Count total values per gender (assuming gender is at depth 1)
const professionOrder = [
"Adult Performer", "Actor", "Singer, Musician", "Model", "Online Personality", "Public Figure", "Sports Professional", "Voice Actor", "TV Personality", "Other"
];
const manualOrder = [
"United States", "Japan", "South Korea", "United Kingdom",
"Russia", "China", "Canada", "India", "Australia", "France", "Germany", "Other"
];
function customSort(a, b, order) {
const aIndex = order.indexOf(a.data.name);
const bIndex = order.indexOf(b.data.name);
return (aIndex === -1 ? Infinity : aIndex) - (bIndex === -1 ? Infinity : bIndex);
}
function partition(data) {
const root = d3.hierarchy(data).sum(d => d.value);
// Sort countries by manual order before partitioning
root.children.sort((a, b) => customSort(a, b, manualOrder));
// Sort professions by value (descending), with "Other" always last
root.children.forEach(country => {
if (country.children) {
country.children.sort((a, b) => {
if (a.data.name === "Other") return 1;
if (b.data.name === "Other") return -1;
return b.value - a.value;
});
}
});
return d3.partition().size([2 * Math.PI, root.height + 1])(root);
}
d3.json("json/sunburst_gender.json").then(data => {
const root = partition(data);
root.each(d => d.current = d);
// Sort countries with 'Other' last
root.children.sort((a, b) => customSort(a, b, manualOrder));
// Sort professions within countries with 'Other' last
root.children.forEach(country => {
if (country.children) {
country.children.sort((a, b) => customSort(a, b, professionOrder));
}
});
const svg = d3.select("svg")
.attr("viewBox", [0, 0, width, width])
.style("font", "10px sans-serif");
const g = svg.append("g")
.attr("transform", `translate(${width / 2},${width / 2})`);
const angleOffset = -80 * Math.PI / 180;
const arc = d3.arc()
.startAngle(d => d.x0 + angleOffset)
.endAngle(d => d.x1 + angleOffset)
.innerRadius(d => {
if (d.depth === 1) return radius * 0.1;
if (d.depth === 2) return radius * 0.5;
})
.outerRadius(d => {
if (d.depth === 1) return radius * 0.5;
if (d.depth === 2) return radius * 0.65;
});
const path = g.append("g")
.selectAll("path")
.data(root.descendants().slice(1))
.join("path")
.attr("fill", d => {
if (d.depth === 2) return colorMap[d.data.name] || "#ccc";
if (d.depth === 1) return "#fff";
return "none";
})
.attr("stroke", d => d.depth === 1 ? "#000" : null)
.attr("stroke-width", d => d.depth > 0 ? 2 : null)
.attr("d", arc);
path.append("title")
.text(d => `${d.ancestors().map(d => d.data.name).reverse().join(" → ")}\n${d.value}`);
// Count total values per gender (assuming gender is at depth 1)
const genderCounts = {};
root.children.forEach(d => {
const gender = d.data.name;
genderCounts[gender] = d.value;
});
// Add bold labels to country segments
g.selectAll("text")
.data(root.children)
.join("text")
.attr("transform", function(d) {
const angle = ((d.x0 + d.x1) / 2) + angleOffset;
const x = Math.cos(angle - Math.PI / 2) * (radius / root.height + -50);
const y = Math.sin(angle - Math.PI / 2) * (radius / root.height + -50);
return `translate(${x},${y}) rotate(${(angle / Math.PI)})`;
})
.attr("dy", "0.35em")
.attr("class", "label")
.text(d => `${d.data.name}: ${genderCounts[d.data.name] || 0}`);
// Count total values per profession (depth 2)
const professionCounts = {};
root.descendants().forEach(d => {
if (d.depth === 2) {
const name = d.data.name;
professionCounts[name] = (professionCounts[name] || 0) + d.value;
}
});
// Legend for profession colors
const legend = svg.append("g")
.attr("class", "legend")
.attr("transform", `translate(20,20)`);
professionOrder.forEach((prof, i) => {
const row = legend.append("g")
.attr("transform", `translate(0,${i * 20})`);
row.append("rect")
.attr("width", 16)
.attr("height", 16)
.attr("fill", colorMap[prof]);
row.append("text")
.attr("x", 22)
.attr("y", 12)
.text(`${prof} (${professionCounts[prof] || 0})`);
});
});
document.getElementById("downloadBtn").addEventListener("click", () => {
const svgNode = document.querySelector("svg");
// Clone the SVG node to preserve original
const clonedSvg = svgNode.cloneNode(true);
const outer = document.createElement("div");
outer.appendChild(clonedSvg);
// Add xmlns so it can be saved properly
clonedSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
// Serialize the SVG
const svgData = new XMLSerializer().serializeToString(clonedSvg);
const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
// Create download link
const url = URL.createObjectURL(svgBlob);
const a = document.createElement("a");
a.href = url;
a.download = "sunburst_chart.svg";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
</script>
</body>