|
|
<!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" |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
root.children.sort((a, b) => customSort(a, b, manualOrder)); |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
root.children.sort((a, b) => customSort(a, b, manualOrder)); |
|
|
|
|
|
|
|
|
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}`); |
|
|
|
|
|
const genderCounts = {}; |
|
|
root.children.forEach(d => { |
|
|
const gender = d.data.name; |
|
|
genderCounts[gender] = d.value; |
|
|
}); |
|
|
|
|
|
|
|
|
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}`); |
|
|
|
|
|
|
|
|
|
|
|
const professionCounts = {}; |
|
|
root.descendants().forEach(d => { |
|
|
if (d.depth === 2) { |
|
|
const name = d.data.name; |
|
|
professionCounts[name] = (professionCounts[name] || 0) + d.value; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
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"); |
|
|
|
|
|
|
|
|
const clonedSvg = svgNode.cloneNode(true); |
|
|
const outer = document.createElement("div"); |
|
|
outer.appendChild(clonedSvg); |
|
|
|
|
|
|
|
|
clonedSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg"); |
|
|
|
|
|
|
|
|
const svgData = new XMLSerializer().serializeToString(clonedSvg); |
|
|
const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" }); |
|
|
|
|
|
|
|
|
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> |
|
|
|