viewof replay = html`<button style="background-color: #D35400; color: white; padding: 10px 20px; border-radius: 5px; font-family: 'Poppins', sans-serif; font-weight: 600; border: none; cursor: pointer;">Replay</button>`
d3 = require("d3@7")
data = d3.csvParse(await FileAttachment("data/expanded_universities_final.csv").text(), d3.autoType)
chart = {
replay;
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);
const updateBars = bars(svg);
const updateAxis = axis(svg);
const updateLabels = labels(svg);
const updateTicker = ticker(svg);
yield svg.node();
for (const keyframe of keyframes) {
const transition = svg.transition()
.duration(duration)
.ease(d3.easeLinear);
// Extract the top bar’s value.
x.domain([0, keyframe[1][0].value]);
updateAxis(keyframe, transition);
updateBars(keyframe, transition);
updateLabels(keyframe, transition);
updateTicker(keyframe, transition);
invalidation.then(() => svg.interrupt());
await transition.end();
}
}
duration = 20
n = 5
names = new Set(data.map(d => d.name))
datevalues = Array.from(d3.rollup(data, ([d]) => d.value, d => +d.date, d => d.name))
.map(([date, data]) => [new Date(date), data])
.sort(([a], [b]) => d3.ascending(a, b))
function rank(value) {
const data = Array.from(names, name => ({name, value: value(name)}));
data.sort((a, b) => d3.descending(a.value, b.value));
for (let i = 0; i < data.length; ++i) data[i].rank = Math.min(n, i);
return data;
}
k = 5
keyframes = {
const keyframes = [];
let ka, a, kb, b;
for ([[ka, a], [kb, b]] of d3.pairs(datevalues)) {
for (let i = 0; i < k; ++i) {
const t = i / k;
keyframes.push([
new Date(ka * (1 - t) + kb * t),
rank(name => (a.get(name) || 0) * (1 - t) + (b.get(name) || 0) * t)
]);
}
}
keyframes.push([new Date(kb), rank(name => b.get(name) || 0)]);
return keyframes;
}
nameframes = d3.groups(keyframes.flatMap(([, data]) => data), d => d.name)
prev = new Map(nameframes.flatMap(([, data]) => d3.pairs(data, (a, b) => [b, a])))
next = new Map(nameframes.flatMap(([, data]) => d3.pairs(data)))
function bars(svg) {
let bar = svg.append("g")
.attr("fill-opacity", 0.6)
.selectAll("rect");
return ([date, data], transition) => bar = bar
.data(data.slice(0, n), d => d.name)
.join(
enter => enter.append("rect")
.attr("fill", color)
.attr("height", y.bandwidth())
.attr("x", x(0))
.attr("y", d => y((prev.get(d) || d).rank))
.attr("width", d => x((prev.get(d) || d).value) - x(0)),
update => update,
exit => exit.transition(transition).remove()
.attr("y", d => y((next.get(d) || d).rank))
.attr("width", d => x((next.get(d) || d).value) - x(0))
)
.call(bar => bar.transition(transition)
.attr("y", d => y(d.rank))
.attr("width", d => x(d.value) - x(0)));
}
function labels(svg) {
let label = svg.append("g")
.style("font", "bold 12px var(--sans-serif)")
.style("font-variant-numeric", "tabular-nums")
.attr("text-anchor", "end")
.selectAll("text");
return ([date, data], transition) => label = label
.data(data.slice(0, n), d => d.name)
.join(
enter => enter.append("text")
.attr("transform", d => `translate(${x((prev.get(d) || d).value)},${y((prev.get(d) || d).rank)})`)
.attr("y", y.bandwidth() / 2)
.attr("x", -6)
.attr("dy", "-0.25em")
.text(d => d.name)
.call(text => text.append("tspan")
.attr("fill-opacity", 0.7)
.attr("font-weight", "normal")
.attr("x", -6)
.attr("dy", "1.15em")),
update => update,
exit => exit.transition(transition).remove()
.attr("transform", d => `translate(${x((next.get(d) || d).value)},${y((next.get(d) || d).rank)})`)
.call(g => g.select("tspan")
.textTween((d) => d3.interpolateRound(d.value, (next.get(d) || d).value))
)
)
.call(bar => bar.transition(transition)
.attr("transform", d => `translate(${x(d.value)},${y(d.rank)})`)
.call(g => g.select("tspan")
.textTween((d) => (t) => formatNumber(
d3.interpolateNumber((prev.get(d) || d).value, d.value)(t)
))
)
)
}
formatNumber = d3.format(",d")
function axis(svg) {
const g = svg.append("g")
.attr("transform", `translate(0,${margin.top})`);
const axis = d3.axisTop(x)
.ticks(width / 160)
.tickSizeOuter(0)
.tickSizeInner(-barSize * (n + y.padding()));
return (_, transition) => {
g.transition(transition).call(axis);
g.select(".tick:first-of-type text").remove();
g.selectAll(".tick:not(:first-of-type) line").attr("stroke", "white");
g.select(".domain").remove();
};
}
function ticker(svg) {
const now = svg.append("text")
.style("font", `bold ${barSize}px var(--sans-serif)`)
.style("font-variant-numeric", "tabular-nums")
.attr("text-anchor", "end")
.attr("x", width - 6)
.attr("y", margin.top + barSize * (n - 0.45))
.attr("dy", "0.32em")
.text(formatDate(keyframes[0][0]));
return ([date], transition) => {
transition.end().then(() => now.text(formatDate(date)));
};
}
formatDate = d3.utcFormat("%Y")
color = {
const scale = d3.scaleOrdinal(d3.schemeTableau10);
if (data.some(d => d.category !== undefined)) {
const categoryByName = new Map(data.map(d => [d.name, d.category]))
scale.domain(Array.from(categoryByName.values()));
return d => scale(categoryByName.get(d.name));
}
return d => scale(d.name);
}
x = d3.scaleLinear([0, 1], [margin.left, width - margin.right])
y = d3.scaleBand()
.domain(d3.range(n + 1))
.rangeRound([margin.top, margin.top + barSize * (n + 1 + 0.1)])
.padding(0.1)
height = margin.top + barSize * n + margin.bottom
barSize = 48
margin = ({top: 16, right: 6, bottom: 6, left: 0})
Empowering Black Communities with Data
We combine high-quality data with lived experience to drive social change.
Different Perspectives. Different Results.
Knowledge comes from choices.
Choices about what is recorded, how to analyse what we record, who interprets the analysis, and what to do about it.
These activities are not neutral. All perspectives are shaped by context.
Black perspectives rarely have a central role in determining the questions, interpreting the data, and co-creating the solutions.
We want to change that.
High-quality Black-led research, that builds capacity in Black communities for knowledge production, will transform outcomes and long-standing inequalities by surfacing, centring and applying new approaches, interventions and resources for lasting systems change.
Visualising Black Representation in Universities
One way of understanding the exclusion of Black people from knowledge production, and the exclusion of Black knowledge itself, is to look at the representation of Black people in universities. The chart above visualises the number of universities categorised by the Higher Education Statistics Agency (HESA) as having zero Black professors. Note also, that there are zero universities with 10 or more Black professors. This HESA data relates to 2023/24. There is strong evidence to suggest that Think Tanks and other kinds of research organisations have a similar distribution of Black staff.
Note that HESA Data are rounded to the nearest 5 to avoid disclosure.
Our Mission
We bring together high-quality data with lived experience so that Black communities have the knowledge, power, and agency to drive social change.
Our Approach: Data Democracy
We collaborate with Black communities and organisations across the public, private, and social sectors to pose new questions, interpret data, and draw on lived and learned expertise to develop paradigm-shifting research that underpins interventions, policies, campaigns, social action, and systems change to achieve racial justice. We do this through four pillars:
Data Carpentry: We transform messy data into clean, useful formats for civic and academic use, and harmonise disparate datasets to produce new insights.
Collective Sense-making: We work with Black communities and others to collectively give meaning to data and identify levers for change.
Data Liberation: We share data and code through open collaborative platforms and use bespoke visual techniques and digital art to empower communities to leverage data.
Primary Research: We create new knowledge through empirical research employing best-in-class qualitative and quantitative methods to address gaps in existing knowledge and data on the experiences of Black people. Our findings are published in academic journals, reports, and briefings.
How We Can Work Together
Community Organisations
We partner with Black-led community organisations, as well as those serving Black communities, to collect new data, analyze existing data, and identify levers for change. Currently, we collaborate closely with Place Matters, Black Thrive Haringey, and Catalyst 4 Change, and we are always open to working with other groups. Together, we combine high-quality data with lived experience to achieve systems change and racial justice.
Data Controllers
We work alongside systems leaders who are committed to our guiding principles and we work together to incorporate the interpretations of communities to generate new discoveries about how we may act togerther on the levers of systems change.
Funders
We partner with funders committed to racial justice in two key ways. First, we provide high-quality research support to their existing grantees. For example, when a grantee’s project involves community-based research, we can enhance that process by directly applying our data democracy approach and offering capacity-building services. Second, we collaborate with funders on primary research projects of mutual interest, whether that involves exploring new areas or conducting in-depth studies on existing aspects of the Black British experience.
If you are passionate about racial justice get in touch. Our work is rooted in deep and meaningful collaboration.
“Our strategy should be not only to confront empire, but to lay siege to it. To deprive it of oxygen. To shame it. To mock it. With our art, our music, our literature, our stubbornness, our joy, our brilliance, our sheer relentlessness and our ability to tell our own stories. Stories that are different from the ones we’re being brainwashed to believe.” - Arundhati Roy