438 lines
20 KiB
JavaScript
438 lines
20 KiB
JavaScript
/**
|
|
* Unified Vaccine Data Generator
|
|
*
|
|
* Generates realistic, geographically accurate Point-based GeoJSON data
|
|
* for vaccine visualization globe demos. Uses country centroids and
|
|
* realistic metric generation.
|
|
*/
|
|
|
|
/**
|
|
* Country centroids and metadata (WHO regions, income levels)
|
|
* This is a curated list of major countries with accurate geographic centers
|
|
*/
|
|
const COUNTRY_DATA = [
|
|
// Africa (AFRO)
|
|
{ name: 'Nigeria', iso3: 'NGA', centroid: [8.6753, 9.0820], region: 'AFRO', income: 'lower-middle', population: 218541000 },
|
|
{ name: 'Ethiopia', iso3: 'ETH', centroid: [40.4897, 9.1450], region: 'AFRO', income: 'low', population: 123379000 },
|
|
{ name: 'Egypt', iso3: 'EGY', centroid: [30.8025, 26.8206], region: 'AFRO', income: 'lower-middle', population: 109262000 },
|
|
{ name: 'Democratic Republic of the Congo', iso3: 'COD', centroid: [21.7587, -4.0383], region: 'AFRO', income: 'low', population: 99010000 },
|
|
{ name: 'South Africa', iso3: 'ZAF', centroid: [22.9375, -30.5595], region: 'AFRO', income: 'upper-middle', population: 60041000 },
|
|
{ name: 'Tanzania', iso3: 'TZA', centroid: [34.8888, -6.3690], region: 'AFRO', income: 'lower-middle', population: 65497000 },
|
|
{ name: 'Kenya', iso3: 'KEN', centroid: [37.9062, -0.0236], region: 'AFRO', income: 'lower-middle', population: 54027000 },
|
|
{ name: 'Uganda', iso3: 'UGA', centroid: [32.2903, 1.3733], region: 'AFRO', income: 'low', population: 47249000 },
|
|
{ name: 'Algeria', iso3: 'DZA', centroid: [1.6596, 28.0339], region: 'AFRO', income: 'lower-middle', population: 44903000 },
|
|
{ name: 'Sudan', iso3: 'SDN', centroid: [30.2176, 12.8628], region: 'AFRO', income: 'low', population: 46874000 },
|
|
{ name: 'Morocco', iso3: 'MAR', centroid: [-7.0926, 31.7917], region: 'AFRO', income: 'lower-middle', population: 37457000 },
|
|
{ name: 'Ghana', iso3: 'GHA', centroid: [-1.0232, 7.9465], region: 'AFRO', income: 'lower-middle', population: 33476000 },
|
|
{ name: 'Mozambique', iso3: 'MOZ', centroid: [35.5296, -18.6657], region: 'AFRO', income: 'low', population: 32969000 },
|
|
{ name: 'Madagascar', iso3: 'MDG', centroid: [46.8691, -18.7669], region: 'AFRO', income: 'low', population: 29611000 },
|
|
{ name: 'Cameroon', iso3: 'CMR', centroid: [12.3547, 7.3697], region: 'AFRO', income: 'lower-middle', population: 27914000 },
|
|
|
|
// Eastern Mediterranean (EMRO)
|
|
{ name: 'Pakistan', iso3: 'PAK', centroid: [69.3451, 30.3753], region: 'EMRO', income: 'lower-middle', population: 235825000 },
|
|
{ name: 'Iran', iso3: 'IRN', centroid: [53.6880, 32.4279], region: 'EMRO', income: 'lower-middle', population: 88550000 },
|
|
{ name: 'Saudi Arabia', iso3: 'SAU', centroid: [45.0792, 23.8859], region: 'EMRO', income: 'high', population: 36409000 },
|
|
{ name: 'Yemen', iso3: 'YEM', centroid: [48.5164, 15.5527], region: 'EMRO', income: 'low', population: 33697000 },
|
|
{ name: 'Iraq', iso3: 'IRQ', centroid: [43.6793, 33.2232], region: 'EMRO', income: 'upper-middle', population: 44496000 },
|
|
{ name: 'Afghanistan', iso3: 'AFG', centroid: [67.7100, 33.9391], region: 'EMRO', income: 'low', population: 41129000 },
|
|
{ name: 'Morocco', iso3: 'MAR', centroid: [-7.0926, 31.7917], region: 'EMRO', income: 'lower-middle', population: 37457000 },
|
|
{ name: 'Somalia', iso3: 'SOM', centroid: [46.1996, 5.1521], region: 'EMRO', income: 'low', population: 17597000 },
|
|
|
|
// Europe (EURO)
|
|
{ name: 'Russia', iso3: 'RUS', centroid: [105.3188, 61.5240], region: 'EURO', income: 'upper-middle', population: 144713000 },
|
|
{ name: 'Germany', iso3: 'DEU', centroid: [10.4515, 51.1657], region: 'EURO', income: 'high', population: 83294000 },
|
|
{ name: 'United Kingdom', iso3: 'GBR', centroid: [-3.4360, 55.3781], region: 'EURO', income: 'high', population: 67736000 },
|
|
{ name: 'France', iso3: 'FRA', centroid: [2.2137, 46.2276], region: 'EURO', income: 'high', population: 64626000 },
|
|
{ name: 'Italy', iso3: 'ITA', centroid: [12.5674, 41.8719], region: 'EURO', income: 'high', population: 58983000 },
|
|
{ name: 'Spain', iso3: 'ESP', centroid: [-3.7492, 40.4637], region: 'EURO', income: 'high', population: 47778000 },
|
|
{ name: 'Ukraine', iso3: 'UKR', centroid: [31.1656, 48.3794], region: 'EURO', income: 'lower-middle', population: 43814000 },
|
|
{ name: 'Poland', iso3: 'POL', centroid: [19.1451, 51.9194], region: 'EURO', income: 'high', population: 38307000 },
|
|
{ name: 'Romania', iso3: 'ROU', centroid: [24.9668, 45.9432], region: 'EURO', income: 'upper-middle', population: 19064000 },
|
|
{ name: 'Netherlands', iso3: 'NLD', centroid: [5.2913, 52.1326], region: 'EURO', income: 'high', population: 17564000 },
|
|
|
|
// Americas (PAHO)
|
|
{ name: 'United States', iso3: 'USA', centroid: [-95.7129, 37.0902], region: 'PAHO', income: 'high', population: 339996000 },
|
|
{ name: 'Brazil', iso3: 'BRA', centroid: [-51.9253, -14.2350], region: 'PAHO', income: 'upper-middle', population: 216422000 },
|
|
{ name: 'Mexico', iso3: 'MEX', centroid: [-102.5528, 23.6345], region: 'PAHO', income: 'upper-middle', population: 128456000 },
|
|
{ name: 'Colombia', iso3: 'COL', centroid: [-74.2973, 4.5709], region: 'PAHO', income: 'upper-middle', population: 52085000 },
|
|
{ name: 'Argentina', iso3: 'ARG', centroid: [-63.6167, -38.4161], region: 'PAHO', income: 'upper-middle', population: 45510000 },
|
|
{ name: 'Canada', iso3: 'CAN', centroid: [-106.3468, 56.1304], region: 'PAHO', income: 'high', population: 38781000 },
|
|
{ name: 'Peru', iso3: 'PER', centroid: [-75.0152, -9.1900], region: 'PAHO', income: 'upper-middle', population: 34352000 },
|
|
{ name: 'Venezuela', iso3: 'VEN', centroid: [-66.5897, 6.4238], region: 'PAHO', income: 'upper-middle', population: 28302000 },
|
|
{ name: 'Chile', iso3: 'CHL', centroid: [-71.5430, -35.6751], region: 'PAHO', income: 'high', population: 19603000 },
|
|
{ name: 'Guatemala', iso3: 'GTM', centroid: [-90.2308, 15.7835], region: 'PAHO', income: 'upper-middle', population: 18092000 },
|
|
{ name: 'Haiti', iso3: 'HTI', centroid: [-72.2852, 18.9712], region: 'PAHO', income: 'low', population: 11584000 },
|
|
|
|
// South-East Asia (SEARO)
|
|
{ name: 'India', iso3: 'IND', centroid: [78.9629, 20.5937], region: 'SEARO', income: 'lower-middle', population: 1428627000 },
|
|
{ name: 'Indonesia', iso3: 'IDN', centroid: [113.9213, -0.7893], region: 'SEARO', income: 'lower-middle', population: 277534000 },
|
|
{ name: 'Bangladesh', iso3: 'BGD', centroid: [90.3563, 23.6850], region: 'SEARO', income: 'lower-middle', population: 172954000 },
|
|
{ name: 'Thailand', iso3: 'THA', centroid: [100.9925, 15.8700], region: 'SEARO', income: 'upper-middle', population: 71801000 },
|
|
{ name: 'Myanmar', iso3: 'MMR', centroid: [95.9560, 21.9162], region: 'SEARO', income: 'lower-middle', population: 54577000 },
|
|
{ name: 'Sri Lanka', iso3: 'LKA', centroid: [80.7718, 7.8731], region: 'SEARO', income: 'lower-middle', population: 22181000 },
|
|
{ name: 'Nepal', iso3: 'NPL', centroid: [84.1240, 28.3949], region: 'SEARO', income: 'lower-middle', population: 30547000 },
|
|
|
|
// Western Pacific (WPRO)
|
|
{ name: 'China', iso3: 'CHN', centroid: [104.1954, 35.8617], region: 'WPRO', income: 'upper-middle', population: 1425671000 },
|
|
{ name: 'Philippines', iso3: 'PHL', centroid: [121.7740, 12.8797], region: 'WPRO', income: 'lower-middle', population: 117337000 },
|
|
{ name: 'Japan', iso3: 'JPN', centroid: [138.2529, 36.2048], region: 'WPRO', income: 'high', population: 123294000 },
|
|
{ name: 'Vietnam', iso3: 'VNM', centroid: [108.2772, 14.0583], region: 'WPRO', income: 'lower-middle', population: 98859000 },
|
|
{ name: 'South Korea', iso3: 'KOR', centroid: [127.7669, 35.9078], region: 'WPRO', income: 'high', population: 51784000 },
|
|
{ name: 'Australia', iso3: 'AUS', centroid: [133.7751, -25.2744], region: 'WPRO', income: 'high', population: 26439000 },
|
|
{ name: 'Malaysia', iso3: 'MYS', centroid: [101.9758, 4.2105], region: 'WPRO', income: 'upper-middle', population: 34308000 },
|
|
{ name: 'Cambodia', iso3: 'KHM', centroid: [104.9910, 12.5657], region: 'WPRO', income: 'lower-middle', population: 16944000 },
|
|
{ name: 'Papua New Guinea', iso3: 'PNG', centroid: [143.9555, -6.3150], region: 'WPRO', income: 'lower-middle', population: 10329000 },
|
|
{ name: 'New Zealand', iso3: 'NZL', centroid: [174.8860, -40.9006], region: 'WPRO', income: 'high', population: 5228000 }
|
|
];
|
|
|
|
/**
|
|
* Vaccine Data Generator Class
|
|
*/
|
|
export class VaccineDataGenerator {
|
|
constructor(vaccineType) {
|
|
this.vaccineType = vaccineType;
|
|
this.countries = COUNTRY_DATA;
|
|
}
|
|
|
|
/**
|
|
* Generate complete GeoJSON FeatureCollection
|
|
* @returns {Object} GeoJSON FeatureCollection with Point features
|
|
*/
|
|
generateData() {
|
|
return {
|
|
type: 'FeatureCollection',
|
|
features: this.countries.map(country => this.generateFeature(country))
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate a single GeoJSON feature for a country
|
|
* @param {Object} country - Country metadata
|
|
* @returns {Object} GeoJSON Feature
|
|
*/
|
|
generateFeature(country) {
|
|
return {
|
|
type: 'Feature',
|
|
geometry: {
|
|
type: 'Point',
|
|
coordinates: country.centroid // [lng, lat]
|
|
},
|
|
properties: {
|
|
name: country.name,
|
|
iso3: country.iso3,
|
|
region: country.region,
|
|
income_level: country.income,
|
|
population: country.population,
|
|
...this.generateVaccineMetrics(country)
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate vaccine-specific metrics based on type
|
|
* @param {Object} country - Country metadata
|
|
* @returns {Object} Vaccine-specific properties
|
|
*/
|
|
generateVaccineMetrics(country) {
|
|
const generators = {
|
|
polio: () => this.generatePolioMetrics(country),
|
|
measles: () => this.generateMeaslesMetrics(country),
|
|
smallpox: () => this.generateSmallpoxMetrics(country),
|
|
dtp3: () => this.generateDTP3Metrics(country),
|
|
hpv: () => this.generateHPVMetrics(country)
|
|
};
|
|
|
|
return generators[this.vaccineType]?.() || {};
|
|
}
|
|
|
|
/**
|
|
* Polio-specific metrics
|
|
*/
|
|
generatePolioMetrics(country) {
|
|
const baseCoverage1980 = this.getBaseCoverage(country, 1980);
|
|
const baseCoverage2020 = this.getBaseCoverage(country, 2020);
|
|
|
|
return {
|
|
coverage_1980: baseCoverage1980,
|
|
coverage_1985: this.interpolate(baseCoverage1980, baseCoverage2020, 0.125),
|
|
coverage_1990: this.interpolate(baseCoverage1980, baseCoverage2020, 0.25),
|
|
coverage_1995: this.interpolate(baseCoverage1980, baseCoverage2020, 0.375),
|
|
coverage_2000: this.interpolate(baseCoverage1980, baseCoverage2020, 0.5),
|
|
coverage_2005: this.interpolate(baseCoverage1980, baseCoverage2020, 0.625),
|
|
coverage_2010: this.interpolate(baseCoverage1980, baseCoverage2020, 0.75),
|
|
coverage_2015: this.interpolate(baseCoverage1980, baseCoverage2020, 0.875),
|
|
coverage_2020: baseCoverage2020,
|
|
polio_free_year: this.getPolioFreeYear(country),
|
|
endemic: this.isPolioEndemic(country)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Measles-specific metrics
|
|
*/
|
|
generateMeaslesMetrics(country) {
|
|
const dose1 = this.getMeaslesCoverage(country, 'dose1');
|
|
const dose2 = this.getMeaslesCoverage(country, 'dose2');
|
|
|
|
return {
|
|
coverage_dose1: dose1,
|
|
coverage_dose2: dose2,
|
|
cases_2023: this.calculateMeaslesCases(country, dose1),
|
|
deaths_2023: this.calculateMeaslesDeaths(country, dose1)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Smallpox-specific metrics (historical)
|
|
*/
|
|
generateSmallpoxMetrics(country) {
|
|
return {
|
|
endemic_1950: this.wasEndemicIn1950(country),
|
|
endemic_1960: this.wasEndemicIn1960(country),
|
|
endemic_1970: this.wasEndemicIn1970(country),
|
|
endemic_1980: false, // Eradicated by 1980
|
|
eradication_year: this.getEradicationYear(country),
|
|
last_case_year: this.getLastCaseYear(country),
|
|
vaccination_intensity: this.getVaccinationIntensity(country),
|
|
cases_peak_year: this.random(1950, 1970),
|
|
cases_peak: this.random(10000, 500000)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* DTP3-specific metrics
|
|
*/
|
|
generateDTP3Metrics(country) {
|
|
const coverage2024 = this.getDTP3Coverage(country);
|
|
const coverage1974 = this.random(5, 30);
|
|
|
|
return {
|
|
dtp3_coverage_2024: coverage2024,
|
|
dtp3_coverage_1974: coverage1974,
|
|
zero_dose_children: this.calculateZeroDose(country, coverage2024),
|
|
under5_mortality_rate: this.calculateMortality(country, coverage2024),
|
|
infant_deaths_prevented: this.calculateLivesSaved(country, coverage2024, coverage1974),
|
|
population_under1: Math.round(country.population * 0.012) // ~1.2% birth rate
|
|
};
|
|
}
|
|
|
|
/**
|
|
* HPV-specific metrics
|
|
*/
|
|
generateHPVMetrics(country) {
|
|
const coverage = this.getHPVCoverage(country);
|
|
|
|
return {
|
|
hpv_coverage_2024: coverage,
|
|
vaccine_program_started: coverage > 0 ? this.random(2008, 2020) : null,
|
|
target_age: '9-14',
|
|
cervical_cancer_incidence: this.getCervicalCancerRate(country, coverage),
|
|
cervical_cancer_mortality: this.getCervicalCancerMortality(country, coverage),
|
|
lives_saved_projected: this.calculateHPVLivesSaved(country, coverage),
|
|
gender_policy: this.random(0, 100) > 30 ? 'girls-only' : 'girls-and-boys',
|
|
annual_deaths: this.calculateAnnualDeaths(country, coverage)
|
|
};
|
|
}
|
|
|
|
// ===== HELPER METHODS =====
|
|
|
|
/**
|
|
* Get base vaccine coverage based on income and region
|
|
*/
|
|
getBaseCoverage(country, year) {
|
|
let base = 50;
|
|
|
|
// Income level adjustments
|
|
if (country.income === 'high') base += 30;
|
|
else if (country.income === 'upper-middle') base += 20;
|
|
else if (country.income === 'lower-middle') base += 5;
|
|
else base -= 10;
|
|
|
|
// Year adjustments (coverage improved over time)
|
|
if (year >= 2010) base += 20;
|
|
else if (year >= 2000) base += 15;
|
|
else if (year >= 1990) base += 10;
|
|
else if (year >= 1980) base += 5;
|
|
|
|
// Regional variations
|
|
if (country.region === 'EURO' || country.region === 'PAHO') base += 10;
|
|
if (country.region === 'AFRO') base -= 5;
|
|
|
|
// Add randomness
|
|
base += this.random(-8, 8);
|
|
|
|
return Math.max(5, Math.min(99, Math.round(base)));
|
|
}
|
|
|
|
getDTP3Coverage(country) {
|
|
return this.getBaseCoverage(country, 2024);
|
|
}
|
|
|
|
getMeaslesCoverage(country, dose) {
|
|
const base = this.getBaseCoverage(country, 2023);
|
|
if (dose === 'dose2') {
|
|
return Math.round(base * 0.85); // Dose 2 typically lower
|
|
}
|
|
return base;
|
|
}
|
|
|
|
getHPVCoverage(country) {
|
|
// HPV coverage is generally lower and more variable
|
|
let coverage = this.getBaseCoverage(country, 2024) * 0.7;
|
|
|
|
// Some low-income countries have no program
|
|
if (country.income === 'low' && this.random(0, 100) > 40) {
|
|
coverage = 0;
|
|
}
|
|
|
|
return Math.round(coverage);
|
|
}
|
|
|
|
/**
|
|
* Calculate derived metrics
|
|
*/
|
|
calculateZeroDose(country, coverage) {
|
|
const birthCohort = Math.round(country.population * 0.012);
|
|
return Math.round(birthCohort * (100 - coverage) / 100);
|
|
}
|
|
|
|
calculateMortality(country, coverage) {
|
|
// Lower coverage = higher mortality
|
|
let baseMortality = 50;
|
|
|
|
if (country.income === 'high') baseMortality = 5;
|
|
else if (country.income === 'upper-middle') baseMortality = 15;
|
|
else if (country.income === 'lower-middle') baseMortality = 35;
|
|
|
|
// Coverage impact
|
|
const coverageImpact = (100 - coverage) * 0.5;
|
|
|
|
return Math.round(baseMortality + coverageImpact);
|
|
}
|
|
|
|
calculateLivesSaved(country, currentCoverage, historicalCoverage) {
|
|
const improvement = currentCoverage - historicalCoverage;
|
|
const birthCohort = Math.round(country.population * 0.012);
|
|
const mortalityRate = this.calculateMortality(country, currentCoverage) / 1000;
|
|
|
|
return Math.round(birthCohort * (improvement / 100) * mortalityRate * 50); // 50 years
|
|
}
|
|
|
|
calculateMeaslesCases(country, coverage) {
|
|
// Cases inversely related to coverage
|
|
const susceptible = (100 - coverage) / 100;
|
|
const birthCohort = Math.round(country.population * 0.015);
|
|
|
|
return Math.round(birthCohort * susceptible * this.random(0.1, 0.4));
|
|
}
|
|
|
|
calculateMeaslesDeaths(country, coverage) {
|
|
const cases = this.calculateMeaslesCases(country, coverage);
|
|
const cfr = country.income === 'low' ? 0.05 : country.income === 'lower-middle' ? 0.02 : 0.005;
|
|
|
|
return Math.round(cases * cfr);
|
|
}
|
|
|
|
getCervicalCancerRate(country, coverage) {
|
|
// Higher coverage = lower cancer rate
|
|
let baseRate = 25;
|
|
|
|
if (country.income === 'high') baseRate = 8;
|
|
else if (country.income === 'upper-middle') baseRate = 15;
|
|
|
|
const coverageImpact = (100 - coverage) * 0.15;
|
|
|
|
return Math.max(2, Math.round(baseRate + coverageImpact));
|
|
}
|
|
|
|
getCervicalCancerMortality(country, coverage) {
|
|
return Math.round(this.getCervicalCancerRate(country, coverage) * 0.55);
|
|
}
|
|
|
|
calculateHPVLivesSaved(country, coverage) {
|
|
const womenPopulation = country.population * 0.5;
|
|
const targetAge = womenPopulation * 0.15; // 15% in target age
|
|
const cancerRate = this.getCervicalCancerRate(country, 0) / 100000;
|
|
|
|
return Math.round(targetAge * cancerRate * (coverage / 100) * 0.87); // 87% effectiveness
|
|
}
|
|
|
|
calculateAnnualDeaths(country, coverage) {
|
|
const incidence = this.getCervicalCancerRate(country, coverage);
|
|
const womenPopulation = country.population * 0.5;
|
|
|
|
return Math.round((incidence / 100000) * womenPopulation * 0.55);
|
|
}
|
|
|
|
/**
|
|
* Historical data helpers
|
|
*/
|
|
getPolioFreeYear(country) {
|
|
if (this.isPolioEndemic(country)) return null;
|
|
|
|
// Americas: 1994, Western Pacific: 2000, Europe: 2002, Southeast Asia: 2014, Africa: 2020
|
|
const regionYears = {
|
|
'PAHO': 1994,
|
|
'WPRO': 2000,
|
|
'EURO': 2002,
|
|
'SEARO': 2014,
|
|
'AFRO': 2020,
|
|
'EMRO': null
|
|
};
|
|
|
|
return regionYears[country.region] || 2020;
|
|
}
|
|
|
|
isPolioEndemic(country) {
|
|
// Only Pakistan and Afghanistan remain endemic
|
|
return ['PAK', 'AFG'].includes(country.iso3);
|
|
}
|
|
|
|
wasEndemicIn1950(country) {
|
|
// Most countries were endemic in 1950
|
|
return country.income !== 'high' || this.random(0, 100) > 70;
|
|
}
|
|
|
|
wasEndemicIn1960(country) {
|
|
return this.wasEndemicIn1950(country) && country.income !== 'high';
|
|
}
|
|
|
|
wasEndemicIn1970(country) {
|
|
return country.income === 'low' || (country.income === 'lower-middle' && this.random(0, 100) > 50);
|
|
}
|
|
|
|
getEradicationYear(country) {
|
|
if (country.income === 'high') return this.random(1950, 1965);
|
|
if (country.income === 'upper-middle') return this.random(1960, 1975);
|
|
return this.random(1970, 1978);
|
|
}
|
|
|
|
getLastCaseYear(country) {
|
|
return this.getEradicationYear(country) - this.random(1, 3);
|
|
}
|
|
|
|
getVaccinationIntensity(country) {
|
|
if (country.income === 'high') return this.random(80, 100);
|
|
if (country.income === 'upper-middle') return this.random(60, 85);
|
|
if (country.income === 'lower-middle') return this.random(40, 70);
|
|
return this.random(20, 55);
|
|
}
|
|
|
|
/**
|
|
* Utility functions
|
|
*/
|
|
random(min, max) {
|
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
}
|
|
|
|
interpolate(start, end, factor) {
|
|
return Math.round(start + (end - start) * factor);
|
|
}
|
|
}
|
|
|
|
// Export factory function for easy use
|
|
export function generateVaccineData(vaccineType) {
|
|
const generator = new VaccineDataGenerator(vaccineType);
|
|
return generator.generateData();
|
|
}
|