The Languages of Africa
Distribution of African language families based on the number of countries where they are spoken.
By Manish Datt
TidyTuesday dataset of 2026-01-13
Africa Languages
Plotting code
<!-- Import Tailwind CSS, Tabulator, D3.js, and Observable Plot -->
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://unpkg.com/tabulator-tables@6.2.1/dist/css/tabulator.min.css" rel="stylesheet">
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6.11/dist/plot.umd.min.js"></script>
<div class="container mx-auto">
<h1 class="text-2xl font-bold mb-4">Africa Languages</h1>
<div id="table" class="bg-white shadow-md rounded-lg overflow-hidden"></div>
<div id="summary" class="mt-4 p-4 bg-gray-200 rounded"></div>
<div id="chart2" class="mt-4 mb-4"></div>
</div>
<!-- Tabulator JS -->
<script type="text/javascript" src="https://unpkg.com/tabulator-tables@6.2.1/dist/js/tabulator.min.js"></script>
<script>
// Load CSV data using D3
d3.csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-01-13/africa.csv').then(data => {
// Rename "Afroasiatic" to "Afro-Asiatic" in family column
data.forEach(d => {
if (d.family === "Afroasiatic") d.family = "Afro-Asiatic";
});
// Initialize Tabulator table
new Tabulator("#table", {
data: data,
layout: "fitColumns",
columns: [
{title: "Language", field: "language"},
{title: "Family", field: "family"},
{title: "Native Speakers", field: "native_speakers"},
{title: "Country", field: "country"}
],
cssClass: "table-auto w-full",
headerSort: true,
pagination: "local",
paginationSize: 10,
});
// Calculate unique entries
const uniqueLanguage = new Set(data.map(d => d.language)).size;
const uniqueFamily = new Set(data.map(d => d.family)).size;
const uniqueSpeakers = new Set(data.map(d => d.native_speakers)).size;
const uniqueCountry = new Set(data.map(d => d.country)).size;
// Display summary
document.getElementById('summary').innerHTML = `
<h2 class="text-lg font-semibold mb-2">Summary</h2>
<p>Unique Languages: ${uniqueLanguage}</p>
<p>Unique Families: ${uniqueFamily}</p>
<p>Unique Countries: ${uniqueCountry}</p>
`;
// Second chart: families by unique countries
const grouped2 = d3.rollup(data, v => ({
uniqueCountries: new Set(v.map(d => d.country)).size,
totalSpeakers: d3.sum(v, d => +d.native_speakers),
uniqueLanguages: new Set(v.map(d => d.language)).size
}), d => d.family);
const top20_2 = Array.from(grouped2).map(([family, stats]) => ({family, ...stats})).sort((a, b) => b.uniqueCountries - a.uniqueCountries).slice(0, 20);
const plot = Plot.plot({
marks: [
Plot.dot(top20_2, {
x: "uniqueCountries",
y: "family",
r: d => Math.sqrt(d.uniqueLanguages) * 2,
fill: "totalSpeakers",
fillOpacity: 0.7
}),
],
x: {label: "Number of Countries"},
y: {label: "", domain: top20_2.map(d => d.family), tickSize: 0},
color: {
legend: true,
label: "Sum of Native Speakers",
tickFormat: d => (d / 1000000000).toFixed(1) + "B",
labelAnchor: "bottom",
position: "bottom"
},
r: {legend: true, label: "Number of Unique Languages"},
marginLeft: 100,
marginBottom: 40,
style: {
fontSize: "14px",
background: "#f3f4f6"
}
});
document.querySelector("#chart2").appendChild(plot);
// Add annotations on the chart
const svgEl = document.querySelector("#chart2 svg");
const g = svgEl.querySelector("g");
d3.select(g).append("foreignObject")
.attr("x", 220)
.attr("y", 200)
.attr("width", 400)
.attr("height", 130)
.append("xhtml:div")
.attr("class", "bg-gradient-to-r from-gray-100 to-gray-100 via-gray-200 text-center text-gray-700 p-2 rounded")
.style("text-align", "left")
.style("font-size", "16px")
.text("Distribution of African language families based on the number of countries where they are spoken. The size of the circles represents the number of unique languages in each family, while the color indicates the total number of native speakers.");
const nigerIndex = top20_2.findIndex(d => d.family === "Niger–Congo");
const afroIndex = top20_2.findIndex(d => d.family === "Afro-Asiatic");
const bandHeight = 600 / 20; // 30
const niger = top20_2[nigerIndex];
const afro = top20_2[afroIndex];
// Add foreignObject for Niger-Congo
if (niger) {
const x = (niger.uniqueCountries / d3.max(top20_2, d => d.uniqueCountries)) * 800;
const y = (nigerIndex * bandHeight) + bandHeight / 2;
d3.select(g).append("foreignObject")
.attr("x", x - 350)
.attr("y", y - 15)
.attr("width", 150)
.attr("height", 60)
.append("xhtml:div")
.attr("class", "bg-white bg-opacity-60 p-2 rounded-xl shadow text-sm")
.style("color", "#99CC00")
.html(`Languages: ${niger.uniqueLanguages}<br>Native speakers: ${(niger.totalSpeakers / 1000000000).toFixed(1)}B`);
}
// Add foreignObject for Afro-Asiatic
if (afro) {
const x = (afro.uniqueCountries / d3.max(top20_2, d => d.uniqueCountries)) * 800;
const y = (afroIndex * bandHeight) + bandHeight / 2;
d3.select(g).append("foreignObject")
.attr("x", x - 30)
.attr("y", y + 30)
.attr("width", 150)
.attr("height", 60)
.append("xhtml:div")
.attr("class", "bg-white bg-opacity-60 p-2 rounded-xl shadow text-sm")
.style("color", "brown")
.html(`Languages: ${afro.uniqueLanguages}<br>Native speakers: ${(afro.totalSpeakers / 1000000000).toFixed(1)}B`);
}
// Add download CSV button functionality
document.getElementById('downloadCsvBtn').addEventListener('click', () => {
const csvContent = "data:text/csv;charset=utf-8," + data.map(d => `${d.language},${d.family},${d.native_speakers},${d.country}`).join("\n");
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", "africa_languages.csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
}).catch(error => console.error('Error loading CSV:', error));
</script>
<style>
.plot-d6a7b5-ramp {
background-color: #f3f4f6;
}
</style>