|
|
<!DOCTYPE html> |
|
|
<meta charset="utf-8"> |
|
|
<style> |
|
|
body { |
|
|
font-family: "Segoe UI", sans-serif; |
|
|
margin: 20px; |
|
|
} |
|
|
|
|
|
svg { |
|
|
font: 12px sans-serif; |
|
|
} |
|
|
|
|
|
.bar { |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.bar:hover { |
|
|
opacity: 0.8; |
|
|
} |
|
|
|
|
|
.axis text { |
|
|
font-size: 11px; |
|
|
} |
|
|
|
|
|
.legend { |
|
|
font-size: 12px; |
|
|
} |
|
|
|
|
|
.legend rect { |
|
|
stroke-width: 1; |
|
|
stroke: #000; |
|
|
} |
|
|
|
|
|
h2 { |
|
|
text-align: center; |
|
|
} |
|
|
|
|
|
#downloadBtn { |
|
|
display: block; |
|
|
margin: 0 auto 20px; |
|
|
} |
|
|
</style> |
|
|
<body> |
|
|
<h2>Bar Chart: Gender → Profession</h2> |
|
|
<button id="downloadBtn">Download SVG</button> |
|
|
<svg id="chart"></svg> |
|
|
<script src="https://d3js.org/d3.v6.min.js"></script> |
|
|
<script> |
|
|
|
|
|
|
|
|
|
|
|
const colorMap = { |
|
|
"Adult Performer": "#8A2BE2", |
|
|
"Model": "#DC143C", |
|
|
"Actor": "#FF7F50", |
|
|
"Public Figure": "#20B2AA", |
|
|
"Singer, Musician": "wheat", |
|
|
"Sports Professional": "gold", |
|
|
"Voice Actor": "lightgreen", |
|
|
"Online Personality": "#4682B4", |
|
|
"Other": "#ccc" |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const genderOrder = ["Female", "Male", "Other"]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
d3.json("json/sunburst_gender_A.json").then(data => { |
|
|
|
|
|
const flatData = []; |
|
|
|
|
|
data.children.forEach(gender => { |
|
|
if (gender.children) { |
|
|
gender.children.forEach(prof => { |
|
|
flatData.push({ |
|
|
gender: gender.name, |
|
|
profession: prof.name, |
|
|
value: prof.value |
|
|
}); |
|
|
}); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const professionCounts = {}; |
|
|
flatData.forEach(d => { |
|
|
professionCounts[d.profession] = (professionCounts[d.profession] || 0) + d.value; |
|
|
}); |
|
|
|
|
|
|
|
|
const professionOrder = Object.entries(professionCounts) |
|
|
.sort((a, b) => { |
|
|
|
|
|
if (a[0] === "Other") return 1; |
|
|
if (b[0] === "Other") return -1; |
|
|
|
|
|
return b[1] - a[1]; |
|
|
}) |
|
|
.map(entry => entry[0]); |
|
|
|
|
|
|
|
|
const genderData = d3.rollup( |
|
|
flatData, |
|
|
v => ({ |
|
|
total: d3.sum(v, d => d.value), |
|
|
professions: v |
|
|
}), |
|
|
d => d.gender |
|
|
); |
|
|
|
|
|
|
|
|
const genders = Array.from(genderData.keys()).sort((a, b) => { |
|
|
const ai = genderOrder.indexOf(a); |
|
|
const bi = genderOrder.indexOf(b); |
|
|
return (ai === -1 ? Infinity : ai) - (bi === -1 ? Infinity : bi); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const margin = {top: 40, right: 200, bottom: 80, left: 80}; |
|
|
const width = 800 - margin.left - margin.right; |
|
|
const height = 600 - margin.top - margin.bottom; |
|
|
|
|
|
const svg = d3.select("#chart") |
|
|
.attr("width", width + margin.left + margin.right) |
|
|
.attr("height", height + margin.top + margin.bottom) |
|
|
.append("g") |
|
|
.attr("transform", `translate(${margin.left},${margin.top})`); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const stack = d3.stack() |
|
|
.keys(professionOrder) |
|
|
.value((d, key) => { |
|
|
const prof = d[1].professions.find(p => p.profession === key); |
|
|
return prof ? prof.value : 0; |
|
|
}); |
|
|
|
|
|
const series = stack(Array.from(genderData)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const x = d3.scaleBand() |
|
|
.domain(genders) |
|
|
.range([0, width]) |
|
|
.padding(0.3); |
|
|
|
|
|
const y = d3.scaleLinear() |
|
|
.domain([0, d3.max(Array.from(genderData.values()), d => d.total)]) |
|
|
.nice() |
|
|
.range([height, 0]); |
|
|
|
|
|
svg.append("g") |
|
|
.attr("transform", `translate(0,${height})`) |
|
|
.call(d3.axisBottom(x)) |
|
|
.selectAll("text") |
|
|
.style("text-anchor", "middle") |
|
|
.style("font-weight", "bold") |
|
|
.style("font-size", "14px"); |
|
|
|
|
|
svg.append("g") |
|
|
.call(d3.axisLeft(y)); |
|
|
|
|
|
svg.append("text") |
|
|
.attr("transform", "rotate(-90)") |
|
|
.attr("y", 0 - margin.left + 20) |
|
|
.attr("x", 0 - height / 2) |
|
|
.style("font-weight", "bold") |
|
|
.text("Count"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
svg.append("g") |
|
|
.selectAll("g") |
|
|
.data(series) |
|
|
.join("g") |
|
|
.attr("fill", d => colorMap[d.key]) |
|
|
.selectAll("rect") |
|
|
.data(d => d) |
|
|
.join("rect") |
|
|
.attr("class", "bar") |
|
|
.attr("x", d => x(d.data[0])) |
|
|
.attr("y", d => y(d[1])) |
|
|
.attr("height", d => y(d[0]) - y(d[1])) |
|
|
.attr("width", x.bandwidth()) |
|
|
.append("title") |
|
|
.text(d => { |
|
|
const profKey = series.find(s => s.includes(d))?.key; |
|
|
return `${d.data[0]} – ${profKey}: ${d[1] - d[0]}`; |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const legend = svg.append("g") |
|
|
.attr("transform", `translate(${width + 20}, 0)`); |
|
|
|
|
|
professionOrder.forEach((prof, i) => { |
|
|
const row = legend.append("g").attr("transform", `translate(0,${i * 22})`); |
|
|
|
|
|
row.append("rect") |
|
|
.attr("width", 18) |
|
|
.attr("height", 18) |
|
|
.attr("fill", colorMap[prof]); |
|
|
|
|
|
row.append("text") |
|
|
.attr("x", 24) |
|
|
.attr("y", 9) |
|
|
.attr("dy", "0.35em") |
|
|
.text(`${prof} (${professionCounts[prof] || 0})`); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById("downloadBtn").addEventListener("click", () => { |
|
|
const svgNode = document.querySelector("#chart"); |
|
|
const clone = svgNode.cloneNode(true); |
|
|
|
|
|
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg"); |
|
|
|
|
|
const all = clone.querySelectorAll("*"); |
|
|
all.forEach(el => { |
|
|
const style = window.getComputedStyle(el); |
|
|
el.setAttribute("style", `font:${style.font}; fill:${style.fill}; stroke:${style.stroke};`); |
|
|
}); |
|
|
|
|
|
const svgData = new XMLSerializer().serializeToString(clone); |
|
|
const blob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" }); |
|
|
const url = URL.createObjectURL(blob); |
|
|
const a = document.createElement("a"); |
|
|
|
|
|
a.href = url; |
|
|
a.download = "gender_bar_chart.svg"; |
|
|
a.click(); |
|
|
URL.revokeObjectURL(url); |
|
|
}); |
|
|
|
|
|
}); |
|
|
</script> |
|
|
|
|
|
</body> |
|
|
</html> |