Historical trends of low-wage workers in the US
<!-- use t/f logic to set values, like in celsius example from -->
<!-- https://observablehq.com/@observablehq/plot-scales#celsius -->
Plot.plot({
<!-- width: Math.max(width, 500), -->
<!-- height: Math.min(width, 1000), -->
style: "padding-top:1em; overflow:visible; font-size: 16px; padding-right:1.5em",
y: {
grid: true,
domain: [0, max_value_12m*1.10],
label: outcome == "share" ? "Percent of workforce earning less than $" + threshold + " " + category_name: "Millions of workers earning less than $" + threshold + " " + category_name,
tickFormat: outcome == "share" ? ".0%" : ".0f"
},
x: {
label: null
},
marks: [
Plot.line(low_wage_filtered, {
x: "month_date",
y: "value_12m",
}),
Plot.text(low_wage_filtered, Plot.selectLast({
x: "month_date",
y: "value_12m",
text: outcome == "share" ? d => (d.value_12m * 100).toFixed(1) + '%' : d => (d.value_12m).toFixed(1) + 'm',
textAnchor: "start",
dx: -20,
dy: -20
})),
Plot.dot(low_wage_filtered, Plot.selectLast({
x: "month_date",
y: "value_12m",
}))
]
})
overall_count = low_wage_data.find(x => x.low_wage_threshold == threshold & x.category_group == "All workers").count
overall_count_formatted = d3.format(".0f")(overall_count / 10**6)
thresholds = [...new Set(low_wage_data.map((item) => item.threshold_nominal))]
categories = [...new Set(low_wage_data.map((item) => item.threshold_type))]
outcomes = [...new Set(low_wage_data.map((item) => item.name))]
d3 = require.alias({
"d3-drag": "d3@7",
"d3-ease": "d3@7",
"d3-selection": "d3@7",
})("d3@7", "d3-simple-slider")
low_wage_filtered = low_wage_data.filter(d => d.threshold_nominal == threshold & d.name == outcome & d.threshold_type == category)
range_value_category = [...new Set(low_wage_data.filter(d => d.name == outcome).map((item) => item.value_12m))]
max_value_12m = Math.max(...range_value_category)
category_name = category == "nominal" ? "(nominal)" : "(inflation-adjusted)"
function Range(range, options = {}) {
const [min, max] = range;
const {
className = "Range",
vertical = false,
label = null,
format = (x) => +x,
step = 1,
value = (min + max) / 2,
style = "",
labelStyle = "",
rangeStyle = "",
valueStyle = ""
} = options;
const rangeWrap = htl.html`<div class=${className} style="${style}"></div>`;
Object.assign(rangeWrap.style, {
display: "inline-flex",
position: "relative",
userSelect: "none"
});
const valueDisplay = htl.html`<output style="${valueStyle}">`;
Object.assign(valueDisplay.style, {
display: "inline-block"
});
const rangeInput = htl.html`<input type=range min=${min} max=${max} step=${step} value=${value} style=${rangeStyle}>`;
Object.assign(rangeInput.style, {
display: "inline-block"
});
if (vertical) {
rangeInput.setAttribute("orient", "vertical");
rangeInput.style.writingMode = "bt-lr"; /* IE */
rangeInput.style["-webkit-appearance"] = "slider-vertical"; /* WebKit */
rangeInput.style.width = "8px";
}
rangeWrap.append(rangeInput, valueDisplay);
if (label) rangeWrap.prepend(htl.html`<label style=${labelStyle}>${label}`);
rangeInput.oninput = () => {
valueDisplay.innerHTML = format(rangeInput.valueAsNumber);
rangeWrap.value = rangeWrap.valueAsNumber = +rangeInput.valueAsNumber;
rangeWrap.dispatchEvent(new CustomEvent("input"));
};
rangeInput.oninput();
return rangeWrap;
}
rangeStyles = htl.html`<style>
.Range, .Popup {
display: inline-flex;
align-items:center;
}
.Range input[type=range] {
width:100px;
}
.Range input[type=range][orient=vertical] {
width:8px;
height:100px;
}
.Range label {
margin-right: 5px;
}
.Range output {
margin-left: 5px;
}
.Popup button{
margin-right:10px;
}
</style>
`
Low-Wage Workforce Tracker, Economic Policy Institute, April 2023, https://economic.github.io/low_wage_workforce.
Notes: Analysis by Ben Zipperer of the Economic Policy Institute Current Population Survey extracts. Wages include overtime, tips, and commissions. Values are 12-month smoothed shares or counts of workers earning under a given threshold. Real thresholds are in March 2023 dollars. Download the data shown in the figure above or the code that produces it.