Global Health Spending
Purpose-wise distribution of top 10 countries with maximum health spending.
By Manish Datt
TidyTuesday dataset of 2026-04-21
World Health Spending Data
Current Health Expenditure and Related Indicators (2000-2023)
Loading data...
Health Spending by Purpose
Health Expenditure by Function/Purpose (2000-2023)
Loading data...
Top 10 Countries by Total Health Spending (USD 2023)
Sum of all *_usd2023 indicators per country (all years combined)
Loading summary...
Health Spending Swarm Chart
Top 10 Countries: Country (X-axis) vs Spending Purpose (Y-axis)
Plotting code
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Health Spending Data - Tabulator Table</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/tabulator-tables@5.5.1/dist/css/tabulator_modern.min.css" rel="stylesheet">
<style>
.tabulator {
font-size: 13px;
}
.tabulator .tabulator-header {
background: #f8f9fa;
}
.tabulator .tabulator-row.tabulator-selectable:hover {
background-color: #f8f9fa;
}
.tabulator .tabulator-col[tabulator-field="iso3_code"] .tabulator-col-content {
justify-content: center;
}
.tabulator .tabulator-cell[tabulator-field="iso3_code"] {
font-family: monospace;
font-size: 11px;
background: #f0f0f0;
text-align: center;
}
.tabulator .tabulator-cell[tabulator-field="value"] {
font-family: monospace;
text-align: right;
font-weight: 500;
}
.tabulator .tabulator-cell[tabulator-field="unit"] {
color: #888;
font-size: 11px;
}
</style>
</head>
<body class="bg-gray-100 p-6">
<div class="max-w-full mx-auto bg-white rounded-lg shadow-lg overflow-hidden">
<div class="bg-gradient-to-r from-blue-600 to-purple-600 text-grey p-6">
<h1 class="text-2xl font-bold mb-2">World Health Spending Data</h1>
<p class="text-sm opacity-90">Current Health Expenditure and Related Indicators (2000-2023)</p>
</div>
<div class="p-4 bg-gray-50 border-b border-gray-200">
<div class="flex flex-wrap gap-4 items-center">
<input type="text" id="searchInput" placeholder="Search countries, codes, indicators..."
class="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1 min-w-64">
<select id="yearFilter" class="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">All Years</option>
</select>
<select id="indicatorFilter" class="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">All Indicators</option>
</select>
<select id="countryFilter" class="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">All Countries</option>
</select>
</div>
</div>
<div class="p-4 bg-white">
<div id="healthTable"></div>
</div>
<div class="p-4 bg-gray-50 border-t border-gray-200">
<div id="healthStats" class="text-sm text-gray-600">
Loading data...
</div>
</div>
</div>
<!-- Second Table: Spending Purpose -->
<div class="max-w-full mx-auto bg-white rounded-lg shadow-lg overflow-hidden mt-8">
<div class="bg-gradient-to-r from-green-600 to-teal-600 text-grey p-6">
<h1 class="text-2xl font-bold mb-2">Health Spending by Purpose</h1>
<p class="text-sm opacity-90">Health Expenditure by Function/Purpose (2000-2023)</p>
</div>
<div class="p-4 bg-gray-50 border-b border-gray-200">
<div class="flex flex-wrap gap-4 items-center">
<input type="text" id="purposeSearchInput" placeholder="Search countries, codes, purposes..."
class="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500 flex-1 min-w-64">
<select id="purposeYearFilter" class="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500">
<option value="">All Years</option>
</select>
<select id="purposeIndicatorFilter" class="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500">
<option value="">All Indicators</option>
</select>
<select id="purposeCountryFilter" class="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500">
<option value="">All Countries</option>
</select>
</div>
</div>
<div class="p-4 bg-white">
<div id="purposeTable"></div>
</div>
<div class="p-4 bg-gray-50 border-t border-gray-200">
<div id="purposeStats" class="text-sm text-gray-600">
Loading data...
</div>
</div>
</div>
<!-- Third Table: Top 10 Countries Summary -->
<div class="max-w-full mx-auto bg-white rounded-lg shadow-lg overflow-hidden mt-8">
<div class="bg-gradient-to-r from-orange-600 to-red-600 text-grey p-6">
<h1 class="text-2xl font-bold mb-2">Top 10 Countries by Total Health Spending (USD 2023)</h1>
<p class="text-sm opacity-90">Sum of all *_usd2023 indicators per country (all years combined)</p>
</div>
<div class="p-4 bg-white">
<div id="summaryTable"></div>
</div>
<div class="p-4 bg-gray-50 border-t border-gray-200">
<div id="summaryStats" class="text-sm text-gray-600">
Loading summary...
</div>
</div>
</div>
<!-- Chart: Country vs Spending Purpose Bubble Chart -->
<div class="max-w-full mx-auto bg-white rounded-lg shadow-lg overflow-hidden mt-8">
<div class="bg-gradient-to-r from-cyan-600 to-blue-600 text-grey p-6">
<div class="flex justify-between items-center">
<div>
<h1 class="text-2xl font-bold mb-2">Health Spending Swarm Chart</h1>
<p class="text-sm opacity-90">Top 10 Countries: Country (X-axis) vs Spending Purpose (Y-axis)</p>
</div>
<button id="downloadPng" disabled class="bg-white text-blue-600 px-4 py-2 rounded-lg font-semibold text-sm opacity-50 cursor-not-allowed transition-all mr-3">
Download PNG
</button>
<button id="downloadSvg" disabled class="bg-white text-blue-600 px-4 py-2 rounded-lg font-semibold text-sm opacity-50 cursor-not-allowed transition-all">
Download SVG
</button>
</div>
</div>
<div class="p-6 bg-white">
<div id="bubbleChart" style="height: 600px; width: 100%;"></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/tabulator-tables@5.5.1/dist/js/tabulator.min.js"></script>
<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/modules/exporting.js"></script>
<script>
// ============ Health Spending Table ============
let healthTable;
let healthData = [];
async function loadHealthCSV() {
try {
const response = await fetch('health_spending.csv');
const csvText = await response.text();
return parseCSV(csvText);
} catch (error) {
console.error('Error loading health CSV:', error);
return [];
}
}
function initHealthTable(data) {
healthTable = new Tabulator('#healthTable', {
data: data,
layout: 'fitColumns',
responsiveLayout: 'hide',
pagination: 'local',
paginationSize: 10,
paginationSizeSelector: [10, 25, 50, 100],
movableColumns: true,
initialSort: [{column: 'country_name', dir: 'asc'}, {column: 'year', dir: 'desc'}],
columns: [
{title: 'Country', field: 'country_name', sorter: 'string', width: 180, minWidth: 150},
{title: 'ISO3', field: 'iso3_code', sorter: 'string', width: 80, minWidth: 70, hozAlign: 'center'},
{title: 'Year', field: 'year', sorter: 'number', width: 80, minWidth: 70},
{title: 'Indicator Code', field: 'indicator_code', sorter: 'string', width: 200, minWidth: 150},
{title: 'Expenditure Type', field: 'expenditure_type', sorter: 'string', width: 220, minWidth: 180},
{title: 'Value', field: 'value', sorter: 'number', formatter: valueFormatter, width: 100, minWidth: 80},
{title: 'Unit', field: 'unit', sorter: 'string', width: 160, minWidth: 120},
],
rowFormatter: function(row) {
if (row.getIndex() % 2 === 0) {
row.getElement().style.backgroundColor = '#fafafa';
}
}
});
}
function updateHealthFilters() {
const yearFilter = document.getElementById('yearFilter');
const indicatorFilter = document.getElementById('indicatorFilter');
const countryFilter = document.getElementById('countryFilter');
const years = [...new Set(healthData.map(d => d.year))].sort((a, b) => b - a);
yearFilter.innerHTML = '<option value="">All Years</option>' +
years.map(y => `<option value="${y}">${y}</option>`).join('');
const indicators = [...new Set(healthData.map(d => d.indicator_code))].sort();
indicatorFilter.innerHTML = '<option value="">All Indicators</option>' +
indicators.map(i => `<option value="${i}">${i}</option>`).join('');
const countries = [...new Set(healthData.map(d => d.country_name))].sort();
countryFilter.innerHTML = '<option value="">All Countries</option>' +
countries.map(c => `<option value="${c}">${c}</option>`).join('');
}
function applyHealthFilters() {
const search = document.getElementById('searchInput').value.toLowerCase();
const year = document.getElementById('yearFilter').value;
const indicator = document.getElementById('indicatorFilter').value;
const country = document.getElementById('countryFilter').value;
healthTable.setFilter(function(data) {
const matchesSearch =
data.country_name.toLowerCase().includes(search) ||
data.iso3_code.toLowerCase().includes(search) ||
data.indicator_code.toLowerCase().includes(search) ||
data.expenditure_type.toLowerCase().includes(search);
return (!year || data.year === year) &&
(!indicator || data.indicator_code === indicator) &&
(!country || data.country_name === country) &&
matchesSearch;
});
}
function updateHealthStats() {
const countries = new Set(healthData.map(d => d.country_name)).size;
const yearsSpan = healthData.reduce((min, d) => Math.min(min, parseInt(d.year)), 9999) + ' - ' +
healthData.reduce((max, d) => Math.max(max, parseInt(d.year)), 0);
const indicators = new Set(healthData.map(d => d.indicator_code)).size;
const totalRecords = healthData.length;
document.getElementById('healthStats').innerHTML = `
<strong>Summary:</strong> ${countries} countries | Years: ${yearsSpan} | ${indicators} indicators |
${totalRecords.toLocaleString()} total records | Showing ${healthTable.getDataCount()} of ${totalRecords} records
`;
}
// ============ Spending Purpose Table ============
let purposeTable;
let purposeData = [];
async function loadPurposeCSV() {
try {
const response = await fetch('spending_purpose.csv');
const csvText = await response.text();
return parseCSV(csvText);
} catch (error) {
console.error('Error loading purpose CSV:', error);
return [];
}
}
function initPurposeTable(data) {
purposeTable = new Tabulator('#purposeTable', {
data: data,
layout: 'fitColumns',
responsiveLayout: 'hide',
pagination: 'local',
paginationSize: 10,
paginationSizeSelector: [10, 25, 50, 100],
movableColumns: true,
initialSort: [{column: 'country_name', dir: 'asc'}, {column: 'year', dir: 'desc'}],
columns: [
{title: 'Country', field: 'country_name', sorter: 'string', width: 180, minWidth: 150},
{title: 'ISO3', field: 'iso3_code', sorter: 'string', width: 80, minWidth: 70, hozAlign: 'center'},
{title: 'Year', field: 'year', sorter: 'number', width: 80, minWidth: 70},
{title: 'Indicator Code', field: 'indicator_code', sorter: 'string', width: 180, minWidth: 140},
{title: 'Spending Purpose', field: 'spending_purpose', sorter: 'string', width: 220, minWidth: 180},
{title: 'Value', field: 'value', sorter: 'number', formatter: valueFormatter, width: 100, minWidth: 80},
{title: 'Unit', field: 'unit', sorter: 'string', width: 160, minWidth: 120},
],
rowFormatter: function(row) {
if (row.getIndex() % 2 === 0) {
row.getElement().style.backgroundColor = '#fafafa';
}
}
});
}
function updatePurposeFilters() {
const yearFilter = document.getElementById('purposeYearFilter');
const indicatorFilter = document.getElementById('purposeIndicatorFilter');
const countryFilter = document.getElementById('purposeCountryFilter');
const years = [...new Set(purposeData.map(d => d.year))].sort((a, b) => b - a);
yearFilter.innerHTML = '<option value="">All Years</option>' +
years.map(y => `<option value="${y}">${y}</option>`).join('');
const indicators = [...new Set(purposeData.map(d => d.indicator_code))].sort();
indicatorFilter.innerHTML = '<option value="">All Indicators</option>' +
indicators.map(i => `<option value="${i}">${i}</option>`).join('');
const countries = [...new Set(purposeData.map(d => d.country_name))].sort();
countryFilter.innerHTML = '<option value="">All Countries</option>' +
countries.map(c => `<option value="${c}">${c}</option>`).join('');
}
function applyPurposeFilters() {
const search = document.getElementById('purposeSearchInput').value.toLowerCase();
const year = document.getElementById('purposeYearFilter').value;
const indicator = document.getElementById('purposeIndicatorFilter').value;
const country = document.getElementById('purposeCountryFilter').value;
purposeTable.setFilter(function(data) {
const matchesSearch =
data.country_name.toLowerCase().includes(search) ||
data.iso3_code.toLowerCase().includes(search) ||
data.indicator_code.toLowerCase().includes(search) ||
data.spending_purpose.toLowerCase().includes(search);
return (!year || data.year === year) &&
(!indicator || data.indicator_code === indicator) &&
(!country || data.country_name === country) &&
matchesSearch;
});
}
function updatePurposeStats() {
const countries = new Set(purposeData.map(d => d.country_name)).size;
const yearsSpan = purposeData.reduce((min, d) => Math.min(min, parseInt(d.year)), 9999) + ' - ' +
purposeData.reduce((max, d) => Math.max(max, parseInt(d.year)), 0);
const indicators = new Set(purposeData.map(d => d.indicator_code)).size;
const purposes = new Set(purposeData.map(d => d.spending_purpose)).size;
const totalRecords = purposeData.length;
document.getElementById('purposeStats').innerHTML = `
<strong>Summary:</strong> ${countries} countries | Years: ${yearsSpan} | ${indicators} indicators |
${purposes} spending purposes | ${totalRecords.toLocaleString()} total records |
Showing ${purposeTable.getDataCount()} of ${totalRecords} records
`;
}
// ============ Shared Functions ============
function parseCSV(text) {
const lines = text.trim().split('\n');
const headers = lines[0].split(',');
const result = [];
for (let i = 1; i < lines.length; i++) {
const row = [];
let current = '';
let inQuotes = false;
for (let char of lines[i]) {
if (char === '"') {
inQuotes = !inQuotes;
} else if (char === ',' && !inQuotes) {
row.push(current);
current = '';
} else {
current += char;
}
}
row.push(current);
if (row.length === headers.length) {
const obj = {};
headers.forEach((h, idx) => {
obj[h.trim()] = row[idx]?.trim() || '';
});
result.push(obj);
}
}
return result;
}
function formatNumber(num) {
const n = parseFloat(num);
if (isNaN(n)) return num;
if (Math.abs(n) >= 1e9) return (n / 1e9).toFixed(2) + ' B';
if (Math.abs(n) >= 1e6) return (n / 1e6).toFixed(2) + ' M';
if (Math.abs(n) >= 1e3) return (n / 1e3).toFixed(2) + ' K';
return n.toFixed(2);
}
function valueFormatter(cell) {
return formatNumber(cell.getValue());
}
// ============ Initialize Both Tables ============
async function init() {
document.getElementById('healthStats').textContent = 'Downloading health spending data...';
document.getElementById('purposeStats').textContent = 'Downloading spending purpose data...';
// Load both datasets in parallel
[healthData, purposeData] = await Promise.all([
loadHealthCSV(),
loadPurposeCSV()
]);
if (healthData.length === 0) {
document.getElementById('healthStats').textContent = 'Error: health_spending.csv not found.';
} else {
initHealthTable(healthData);
updateHealthFilters();
updateHealthStats();
document.getElementById('searchInput').addEventListener('input', applyHealthFilters);
document.getElementById('yearFilter').addEventListener('change', applyHealthFilters);
document.getElementById('indicatorFilter').addEventListener('change', applyHealthFilters);
document.getElementById('countryFilter').addEventListener('change', applyHealthFilters);
}
if (purposeData.length === 0) {
document.getElementById('purposeStats').textContent = 'Error: spending_purpose.csv not found.';
} else {
initPurposeTable(purposeData);
updatePurposeFilters();
updatePurposeStats();
document.getElementById('purposeSearchInput').addEventListener('input', applyPurposeFilters);
document.getElementById('purposeYearFilter').addEventListener('change', applyPurposeFilters);
document.getElementById('purposeIndicatorFilter').addEventListener('change', applyPurposeFilters);
document.getElementById('purposeCountryFilter').addEventListener('change', applyPurposeFilters);
}
// ============ Summary: Top 10 Countries by USD 2023 Spending ============
if (purposeData.length > 0) {
// Filter for *_usd2023 indicators only
const usdData = purposeData.filter(d => d.indicator_code.includes('_usd2023'));
// Aggregate total spending by country
const countryTotals = {};
usdData.forEach(row => {
const country = row.country_name;
const value = parseFloat(row.value) || 0;
countryTotals[country] = (countryTotals[country] || 0) + value;
});
// Get top 10 countries
const sortedCountries = Object.entries(countryTotals)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
// Get spending purposes breakdown for each top country
const summaryData = [];
sortedCountries.forEach(([country, total], rank) => {
const countryRows = usdData.filter(d => d.country_name === country);
// Group by spending_purpose
const purposeBreakdown = {};
countryRows.forEach(row => {
const purpose = row.spending_purpose;
const value = parseFloat(row.value) || 0;
purposeBreakdown[purpose] = (purposeBreakdown[purpose] || 0) + value;
});
// Add a row for each spending purpose with rank attached
Object.entries(purposeBreakdown)
.sort((a, b) => b[1] - a[1])
.forEach(([purpose, value], idx) => {
summaryData.push({
rank: parseInt(rank) + 1,
country_name: country,
total_spending: parseFloat(total),
spending_purpose: purpose,
purpose_value: parseFloat(value),
iso3_code: countryRows[0]?.iso3_code || ''
});
});
});
// Sort by rank ASC, then purpose_value DESC
summaryData.sort((a, b) => {
if (a.rank !== b.rank) return a.rank - b.rank;
return b.purpose_value - a.purpose_value;
});
initSummaryTable(summaryData);
updateSummaryStats(summaryData);
initSwarmChart(summaryData);
}
}
function updateSummaryStats(summaryData) {
const countryTotals = {};
summaryData.forEach(row => {
const country = row.country_name;
countryTotals[country] = row.total_spending;
});
const totalGlobal = Object.values(countryTotals).reduce((sum, t) => sum + t, 0);
const top10Countries = Object.keys(countryTotals).length;
document.getElementById('summaryStats').innerHTML = `
<strong>Top ${top10Countries} Countries by Total Health Spending (USD 2023):</strong> Showing spending breakdown by purpose.
Combined total: ${formatNumber(totalGlobal)} USD
`;
}
// ============ Swarm Chart ============
let summaryTable;
function initSummaryTable(data) {
summaryTable = new Tabulator('#summaryTable', {
data: data,
layout: 'fitColumns',
responsiveLayout: 'hide',
pagination: 'local',
paginationSize: 10,
paginationSizeSelector: [10, 25, 50],
movableColumns: true,
columns: [
{title: 'Rank', field: 'rank', sorter: 'number', width: 70, minWidth: 60, hozAlign: 'center'},
{title: 'Country', field: 'country_name', sorter: 'string', width: 180, minWidth: 150},
{title: 'ISO3', field: 'iso3_code', sorter: 'string', width: 80, minWidth: 70, hozAlign: 'center'},
{title: 'Total Spending', field: 'total_spending', sorter: 'number', formatter: valueFormatter, width: 160, minWidth: 140},
{title: 'Spending Purpose', field: 'spending_purpose', sorter: 'string', width: 240, minWidth: 200},
{title: 'Purpose Value', field: 'purpose_value', sorter: 'number', formatter: valueFormatter, width: 160, minWidth: 140},
],
rowFormatter: function(row) {
if (row.getIndex() % 2 === 0) {
row.getElement().style.backgroundColor = '#fafafa';
}
}
});
}
// ============ Swarm/Dot-Density Chart ============
let swarmChart;
function initSwarmChart(summaryData) {
// Configuration: 1 dot = 10B USD
const UNIT = 10e9; // 10 billion USD per dot
// Get unique countries (already in rank order from summaryData)
const countries = [...new Set(summaryData.map(d => d.country_name))];
const countryIndex = {};
countries.forEach((c, i) => countryIndex[c] = i);
// Get all unique spending purposes from all countries
const allPurposesSet = new Set(summaryData.map(d => d.spending_purpose));
const top1Country = countries[0];
const top1CountryData = summaryData.filter(d => d.country_name === top1Country && d.purpose_value > 0);
// Get ordering from #1 country, then append any missing purposes alphabetically
const top1Order = top1CountryData
.sort((a, b) => b.purpose_value - a.purpose_value) // descending: largest at index 0 → top (yAxis reversed)
.map(d => d.spending_purpose);
const missingPurposes = [...allPurposesSet]
.filter(p => !top1Order.includes(p))
.sort();
const allPurposes = [...top1Order, ...missingPurposes];
// Generate dot-density points
// Scaling: < 1T => 1 dot = 10B (small teal), >= 1T => 1 dot = 1T (large orange)
const points = [];
summaryData.forEach(d => {
const baseX = countryIndex[d.country_name];
const purposeIdx = allPurposes.indexOf(d.spending_purpose);
if (purposeIdx === -1) return; // skip purposes not in category list
const baseY = purposeIdx;
let dotCount, markerConfig;
const val = d.purpose_value;
if (val >= 1e12) {
dotCount = Math.round(val / 1e12);
markerConfig = { radius: 7, fillColor: 'rgba(255, 200, 100, 1)', lineWidth: 2, lineColor: 'rgba(255, 143, 0, 1)' };
} else if (val >= 100e9) {
// 100B–1T: 1 dot per 100B, medium, purple
dotCount = Math.round(val / 100e9);
markerConfig = { radius: 5, fillColor: 'rgba(216, 180, 254, 1)', lineWidth: 2, lineColor: 'rgba(168, 85, 247, 1)' };
} else {
// <100B: 1 dot per 10B, small, teal
dotCount = Math.round(val / UNIT);
markerConfig = { radius: 3, fillColor: 'rgba(165, 243, 252, 1)', lineWidth: 1, lineColor: 'rgba(6, 182, 212, 1)' };
}
if (dotCount <= 0) return;
// Compute grid layout with consistent spacing for smaller grids
const gridSize = Math.ceil(Math.sqrt(dotCount));
const desiredSpacing = 0.15; // fixed dot spacing for up to 5x5 grid
const maxSpan = 0.6; // total available cell span
let gridStep, span, startOffset;
if (gridSize <= 1) {
gridStep = 0;
startOffset = 0;
} else {
const requiredStep = maxSpan / (gridSize - 1);
gridStep = Math.min(desiredSpacing, requiredStep);
span = gridStep * (gridSize - 1);
startOffset = -span / 2; // center the grid within the cell
}
for (let i = 0; i < dotCount; i++) {
const col = i % gridSize;
const row = Math.floor(i / gridSize);
const x = baseX + startOffset + col * gridStep;
const y = baseY + startOffset + row * gridStep;
points.push({
x: x,
y: y,
country: d.country_name,
iso3: d.iso3_code,
purpose: d.spending_purpose,
value: d.purpose_value,
total: d.total_spending,
rank: d.rank,
marker: markerConfig
});
}
});
swarmChart = Highcharts.chart('bubbleChart', {
chart: {
type: 'scatter',
backgroundColor: '#333333',
plotBorderWidth: 0,
zoomType: 'xy',
spacingTop: 50,
width: document.getElementById('bubbleChart').offsetWidth,
height: 600
},
title: {
text: 'Purpose-wise distribution of top 10 countries with maximum spending on healthcare. Dots represent <span style="color: rgba(165, 243, 252, 1);">$10B</span>, <span style="color: rgba(216, 180, 254, 1);">$100B</span>, and <span style="color: rgba(255, 200, 100, 1);">$1T</span>.',
align: 'left',
style: { fontSize: '16px', fontWeight: 'bold', color: '#eee' },
margin: 20
},
credits: { enabled: false },
exporting: {
enabled: true,
scale: 1,
buttons: {
contextButton: {
enabled: false
}
}
},
xAxis: {
type: 'category',
title: { text: '' },
categories: countries,
gridLineWidth: 0,
labels: {
style: { fontSize: '14px', color: '#eee' },
formatter: function() {
// Rename long country names
const rename = {
'United States of America': 'USA',
'United Kingdom of Great Britain and Northern Ireland': 'UK'
};
const name = rename[this.value] || this.value;
// Truncate if still too long
return name.length > 15 ? name.substring(0, 12) + '...' : name;
}
},
tickPositioner: function() {
return Array.from({ length: this.categories.length }, (_, i) => i);
}
},
yAxis: {
type: 'category',
title: { text: '' },
categories: allPurposes,
reversed: true,
gridLineWidth: 0.25,
labels: {
style: { fontSize: '14px', color: '#eee' },
useHTML: true,
formatter: function() {
const words = this.value.split(' ');
if (words.length <= 3) return this.value;
const lines = [];
for (let i = 0; i < words.length; i += 3) {
lines.push(words.slice(i, i + 3).join(' '));
}
return lines.join('<br/>');
}
},
tickPositioner: function() {
return Array.from({ length: this.categories.length }, (_, i) => i);
}
},
tooltip: {
useHTML: true,
formatter: function() {
const point = this.point;
// Compute dot count based on tier
let dotCount, dotLabel;
if (point.value >= 1e12) {
dotCount = Math.round(point.value / 1e12);
dotLabel = '1T';
} else if (point.value >= 100e9) {
dotCount = Math.round(point.value / 100e9);
dotLabel = '100B';
} else {
dotCount = Math.round(point.value / 10e9);
dotLabel = '10B';
}
return `
<div style="padding:8px;">
<strong>#${point.rank} ${point.country} (${point.iso3})</strong><br/>
<span style="color:#666;">${point.purpose}</span><br/>
<strong>${formatNumber(point.value)} USD 2023</strong><br/>
<span style="color:#888;font-size:11px;">Total: ${formatNumber(point.total)}</span><br/>
<span style="color:#999;font-size:10px;">Dot density: ~${dotCount} dot${dotCount!==1?'s':''} (1 dot = ${dotLabel})</span>
</div>
`;
}
},
legend: { enabled: false },
series: [{
name: 'Health Spending Density',
data: points
}]
});
// Enable download buttons now that chart is ready
const pngBtn = document.getElementById('downloadPng');
const svgBtn = document.getElementById('downloadSvg');
if (pngBtn) {
pngBtn.disabled = false;
pngBtn.textContent = 'Download PNG';
pngBtn.style.opacity = '1';
pngBtn.style.cursor = 'pointer';
pngBtn.onclick = function() {
console.log('PNG download clicked');
swarmChart.exportChart({
type: 'image/png',
filename: 'health_spending_top10_countries'
});
};
}
if (svgBtn) {
svgBtn.disabled = false;
svgBtn.textContent = 'Download SVG';
svgBtn.style.opacity = '1';
svgBtn.style.cursor = 'pointer';
svgBtn.onclick = function() {
console.log('SVG download clicked');
const svg = swarmChart.getSVG();
const blob = new Blob([svg], {type: 'image/svg+xml'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'health_spending_top10_countries.svg';
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 0);
};
}
}
init();
</script>
</body>