Public
Edited
Apr 28
1 fork
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
data1
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
workbook2 = FileAttachment("ACS_Cleaned_ByYear_1@1.xlsx").xlsx()
Insert cell
workbook2.sheetNames
Insert cell
data2 = workbook2.sheet(0, {
headers: true,
// range: "A1:J10"
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
workbook3 = FileAttachment("MEDIAN_EARNINGS_BY_GENDER_IN_ENG_2013-2023@1.xlsx").xlsx()
Insert cell
workbook3.sheetNames
Insert cell
data4 = workbook3.sheet(0, {
headers: false,
// range: "A1:J10"
})
Insert cell
data4
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
Insert cell
Insert cell
{
const cleaned = latestYearData
.filter(d =>
d["Occupational Category"] &&
!isNaN(+d["Men Median Pa"]) &&
!isNaN(+d["Women Median Pay"]) &&
!isNaN(+d["%women workers"])
)
.map(d => ({
Occupation: d["Occupational Category"],
Men_Earnings: +d["Men Median Pa"],
Women_Earnings: +d["Women Median Pay"],
AverageIncome: (+d["Men Median Pa"] + +d["Women Median Pay"]) / 2,
FemalePct: +d["%women workers"],
MalePct: 100 - +d["%women workers"]
}));

const sorted = cleaned.sort((a, b) => a.AverageIncome - b.AverageIncome);

const width = 900;
const height = 550;
const margin = { top: 60, right: 20, bottom: 180, left: 80 };

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

const x = d3.scaleBand()
.domain(sorted.map(d => d.Occupation))
.range([margin.left, width - margin.right])
.padding(0.2);

const y = d3.scaleLinear()
.domain([0, d3.max(sorted, d => d.AverageIncome)]).nice()
.range([height - margin.bottom, margin.top + 40]);

const color = d3.scaleSequential()
.domain([0, 100])
.interpolator(d3.interpolateRgb("#fce4ec", "#880e4f"));

// Title
svg.append("text")
.attr("x", width / 2)
.attr("y", margin.top - 30)
.attr("text-anchor", "middle")
.style("font-size", "18px")
.style("font-weight", "bold")
.text("Average Income by Occupation");

// Axis
svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y).ticks(6).tickFormat(d3.format("$.0f")));

// Bars
const bars = svg.selectAll("rect")
.data(sorted)
.join("rect")
.attr("x", d => x(d.Occupation))
.attr("y", d => y(d.AverageIncome))
.attr("width", x.bandwidth())
.attr("height", d => y(0) - y(d.AverageIncome))
.attr("fill", d => color(d.FemalePct))
.style("opacity", 0.9)
.style("cursor", "pointer");

// Labels under bars
svg.selectAll("text.label")
.data(sorted)
.join("text")
.attr("class", "label")
.attr("x", d => x(d.Occupation) + x.bandwidth() / 2)
.attr("y", height - margin.bottom + 10)
.attr("transform", d => `rotate(-60, ${x(d.Occupation) + x.bandwidth() / 2}, ${height - margin.bottom + 10})`)
.attr("text-anchor", "end")
.attr("font-size", 10)
.text(d => d.Occupation);

// Gradient Legend
const legendWidth = 200;
const legendHeight = 12;
const legendX = width / 2 - legendWidth / 2;
const legendY = margin.top;

const defs = svg.append("defs");
const gradient = defs.append("linearGradient")
.attr("id", "female-gradient")
.attr("x1", "0%")
.attr("x2", "100%");

gradient.append("stop").attr("offset", "0%").attr("stop-color", "#fce4ec");
gradient.append("stop").attr("offset", "100%").attr("stop-color", "#880e4f");

const legendGroup = svg.append("g").attr("transform", `translate(${legendX}, ${legendY})`);

legendGroup.append("rect")
.attr("width", legendWidth)
.attr("height", legendHeight)
.attr("fill", "url(#female-gradient)")
.attr("stroke", "gray")
.attr("stroke-width", 0.5);

legendGroup.append("text")
.attr("x", 0)
.attr("y", -4)
.attr("text-anchor", "start")
.style("font-size", "10px")
.text("0% women");

legendGroup.append("text")
.attr("x", legendWidth)
.attr("y", -4)
.attr("text-anchor", "end")
.style("font-size", "10px")
.text("100% women");

// Tooltip with Font Awesome icons
const tooltip = html`<div style="
position: absolute;
pointer-events: none;
background: white;
border: 1px solid #ccc;
padding: 6px 10px;
border-radius: 4px;
font-size: 13px;
box-shadow: 0 1px 5px rgba(0,0,0,0.15);
display: none;
line-height: 1.6;
white-space: nowrap;
"></div>`;
document.body.appendChild(tooltip);

const container = html`<div style="position: relative; width: ${width}px; height: ${height}px;"></div>`;
container.appendChild(svg.node());

bars
.on("mouseover", function (event, d) {
d3.select(this).style("opacity", 1.8);

tooltip.innerHTML = `
<i class="fa-solid fa-person-dress" style="color: #d1006f;"></i> ${d.FemalePct.toFixed(1)}% women<br>
<i class="fa-solid fa-person" style="color: #1f77b4;"></i> ${d.MalePct.toFixed(1)}% men<br>
💰 Women earn: $${d.Women_Earnings.toLocaleString()}<br>
💰 Men earn: $${d.Men_Earnings.toLocaleString()}
`;
tooltip.style.display = "block";
})
.on("mousemove", function (event) {
tooltip.style.left = event.pageX + 12 + "px";
tooltip.style.top = event.pageY - 28 + "px";
})
.on("mouseout", function () {
tooltip.style.display = "none";
bars.style("opacity", 0.9);
});

return container;
}

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