| <!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> |