covid-disease-spread/index.html

372 lines
15 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>Covid19 interactive map</title>
<meta charset="utf-8"/>
<!-- Social media -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Covid-19 interactive map" />
<meta name="twitter:image" content="https://vane.github.io/covid-disease-spread/card.png" />
<meta name="twitter:creator" content="@szczepano">
<link rel="canonical" href="https://vane.github.io/covid-disease-spread/" />
<meta property="og:title" content="Covid19 interactive map" />
<meta property="og:description" content="Covid19 interactive map" />
<meta property="og:url" content="https://vane.github.io/covid-disease-spread/" />
<meta property="og:site_name" content="Covid19 interactive map" />
<meta property="og:image" content="https://vane.github.io/covid-disease-spread/card.png" />
<meta property="og:image:height" content="848" />
<meta property="og:image:width" content="2186" />
<meta property="og:type" content="article" />
<meta property="article:published_time" content="2020-04-05T22:59:14" />
<script type="application/ld+json">
{"mainEntityOfPage":{"@type":"WebPage","@id":"https://vane.github.io/covid-disease-spread/"},"url":"https://vane.github.io/covid-disease-spread/","image":{"width":2186,"height":848,"url":"https://vane.github.io/covid-disease-spread/card.png","@type":"imageObject"},"author":{"@type":"Person","name":"Michal Szczepanski"},"headline":"Covid 19 interactive map","dateModified":"2020-04-05T22:59:14","description":"Covid19 interactive map","datePublished":"2020-04-05T22:59:14","@type":"BlogPosting","@context":"https://schema.org"}</script>
<!-- end -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.1/leaflet.css"/>
<style type="text/css">
.leaflet-container {
background-color: #c5e8ff;
}
table {
border-collapse: collapse;
width: 100%;
}
th, td {
border-bottom: 1px solid #ddd;
text-align: center;
}
</style>
</head>
<body>
<div style="display:flex;">
<div>
<div id="map" style="width: 534px; height: 350px"></div>
<div>
<span><a target="_blank" href="https://www.ecdc.europa.eu/en/publications-data/download-todays-data-geographic-distribution-covid-19-cases-worldwide">Data source - ecdc.europa.eu</a></span>
<br/>
<span><a target="_blank" href="https://geojson-maps.ash.ms/">Map geojson source - geojson-maps.ash.ms</a></span>
<br />
<span><a target="_blank" href="https://github.com/vane/covid-disease-spread">Github repository</a></span>
<br />
<label>Data updated daily at 2pm UTC+0</label>
</div>
</div>
<div style="padding-left:10px;">
<label id="rollover_data" style="color:blue;"></label>
<br />
<label>Autoplay <input id="autoplay_checkbox" type="checkbox" checked></label>
<br />
<label>Date <span id="date_range_current">2019-12-31</span>
<input id="date_range" style="width:400px;" type="range" step="1" min="0" max="0"></label>
<br />
<div id="coronacases" style="max-height:280px;overflow:auto;padding-top:10px;"></div>
</div>
</div>
<script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.1/leaflet.js"></script>
<script>
// helper
const gid = (id) => {
return document.getElementById(id);
}
const formatNumber = (x, sep) => {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, sep);
}
// maps
const mapPath = 'data/110m.geo.json'
const mapStyle = {
stroke: true,
weight: 1,
fill: true,
fillColor: '#fff',
fillOpacity: 1
}
const countryLayer = {};
let covidDataCache = {};
// gradient
const colors = {
start: [255, 255, 255],
end: [255, 0, 0],
gradients: [],
gradientsCol: [],
range: 50,
autoplaySpeed: 250,
}
// https://stackoverflow.com/questions/24016456/how-to-programmatically-create-n-sequential-equidistant-colors-ranging-from-dark
const linearGradient = (n) => {
const gradients = [];
const gradientsCol = [];
const dRGB = [];
for (let i = 0;i<colors.start.length;i++) {
const color1 = colors.start[i];
const color2 = colors.end[i];
const c = (color2 - color1)/((n/2) - 1);
dRGB.push(c);
}
for (let k = 0;k<n;k++) {
const col = [];
for(let j = 0;j<colors.start.length;j++) {
const dx = dRGB[j];
const color1 = colors.start[j];
const c = parseInt(color1+k*dx);
col.push(c);
}
gradientsCol.push('rgb('+col.join(',')+')');
gradients.push(col);
}
colors.gradients = gradients;
colors.gradientsCol = gradientsCol;
}
linearGradient(colors.range+1);
$.getJSON(mapPath, (data) => {
let map = L.map('map').setView([45, 0], 1.05);
L.geoJson(data, {
clickable: true,
style: mapStyle,
//interactive: false,
onEachFeature: (feature, layer) => {
let toCCode = (c) => {
if(feature.properties.iso_a2 == "GB") {
return "UK"
}
return c;
}
let ccode = toCCode(feature.properties.iso_a2);
const prevColor = {
set: false,
col: '',
};
countryLayer[ccode] = layer;
layer.on('mouseover', (e) => {
const props = e.target.feature.properties;
const ccode = toCCode(props.iso_a2);
const covid = covidDataCache.country[ccode];
if(data && covidDataCache) {
const cont = gid('rollover_data');
cont.innerText = `${props.sovereignt} - Total : ${formatNumber(covid.ctotal, " ")} Deaths : ${formatNumber(covid.dtotal, " ")} Population : ${formatNumber(props.pop_est, " ")}`;
}
})
layer.on('mouseout', (e) => {
const cont = gid('rollover_data');
cont.innerText = '';
})
},
}).addTo(map);
}).then(() => {
console.log('finish drawing countries');
$.get('data/download.csv').then((data) => {
const covidData = {}
const today = new Date();
covidData.country = {};
covidData.end = {day: today.getDate(), month: today.getMonth(), year: today.getFullYear()}
covidData.start = {country: 'CN', day: 31, month: 12, year: 2019}
data.split('\n').forEach((row, i) => {
if (i > 1) {
const a = row.split(',')
const date = a[0]
const day = a[1]
const month = a[2]
const year = a[3]
const cases = parseInt(a[4])
const deaths = parseInt(a[5])
const geoId = a[7]
const population = parseInt(a[9])
if (!covidData.country[geoId]) {
covidData.country[geoId] = {data: {}, population: population, ctotal: 0, dtotal: 0}
}
covidData.country[geoId].data[year+"-"+month+"-"+day] = {cases: cases, deaths: deaths, cgrow:0, dgrow:0}
}
});
return covidData;
}).then((covidData) => {
// country disease calculate
const start = covidData.start;
// end date
const end = new Date(covidData.end.year, covidData.end.month, covidData.end.day + 1);
const countries = covidData.country;
let dateFormat, ccode, country, stats;
covidData.totalDays = 0;
covidData.maxCasesCountry = '';
covidData.maxCases = 0;
covidData.maxDeathsCountry = '';
covidData.maxDeaths = 0;
// let's fill gaps
const countryCache = {};
// https://stackoverflow.com/questions/4345045/javascript-loop-between-date-ranges
// iterate over dates
for(let s = new Date(start.year, start.month - 1, start.day);s < end;s.setDate(s.getDate() + 1)) {
dateFormat = s.getFullYear()+"-"+(s.getMonth() + 1)+"-"+s.getDate();
// console.log(dateFormat)
// iterate over countries
for(ccode in countries) {
country = countries[ccode];
// precalculate stats
if(country.data[dateFormat]) {
stats = country.data[dateFormat];
country.ctotal += stats.cases;
country.dtotal += stats.deaths;
if(country.dtotal > covidData.maxDeaths) {
covidData.maxDeaths = country.dtotal;
covidData.maxDeathsCountry = ccode;
}
if(country.ctotal > covidData.maxCases) {
covidData.maxCases = country.ctotal;
covidData.maxCasesCountry = ccode;
}
stats.cgrow = country.ctotal;
stats.dgrow = country.dtotal;
countryCache[ccode] = {
cgrow: stats.cgrow,
dgrow: stats.dgrow,
cases: 0,
deaths: 0,
};
} else {
// let's fill gaps
country.data[dateFormat] = countryCache[ccode]
}
}
// let's have total days so we can use it in range control
covidData.totalDays += 1;
}
gid('date_range').max = covidData.totalDays - 1;
console.log('total days', covidData.totalDays);
//console.log(colors.steps);
return covidData;
}).then((covidData) => {
// initialize controls
// console.log(covidData);
covidDataCache = covidData;
let calcTimeout = -1;
let autoPlayIntervalId = 0;
const coronacases = gid('coronacases');
const autoplay = () => {
let value = parseInt(gid('date_range').value);
if (value >= covidData.totalDays - 1) {
value = 0;
}
// console.log(value);
gid('date_range').value = value + 1;
colorCountries(value);
};
const colorCountries = (value) => {
const dt = new Date(covidData.start.year, covidData.start.month - 1, covidData.start.day);
dt.setDate(dt.getDate() + parseInt(value));
const dformat = dt.getFullYear()+"-"+(dt.getMonth()+1)+"-"+dt.getDate();
gid('date_range_current').innerText = dformat;
const colorStep = covidData.maxCases / colors.range;
const coronaCountries = [];
// console.log('color step', colorStep);
let layer, country, stats, colorIndex, props;
for(let ccode in countryLayer) {
layer = countryLayer[ccode];
country = covidData.country[ccode];
if(country) {
stats = country.data[dformat];
if(stats && stats.cgrow > 0) {
colorIndex = colors.gradientsCol[Math.ceil(stats.cgrow / colorStep)];
layer.setStyle({
fillColor: colorIndex,
});
props = layer.feature.properties;
// lets create list of countries here
coronaCountries.push({
'code': ccode,
'cases': stats.cgrow,
'name': props.name,
'deaths':stats.dgrow,
'dcases':stats.cases,
'ddeaths':stats.deaths,
});
// console.log(ccode, stats, colorIndex);
} else {
layer.setStyle({
fillColor: mapStyle.fillColor,
});
}
//console.log(ccode, Math.floor(country.ctotal / colorStep));
} else {
layer.setStyle({
fillColor: mapStyle.fillColor,
});
}
}
// now sort the countries and build the list control
coronaCountries.sort((a, b) => {
if(a.cases > b.cases) {
return -1;
} else if (a.cases < b.cases) {
return 1;
}
return 0;
});
let coronaText = '<table><thead><th>Rank</th><th>Country</th><th>Total Cases</th><th>Total Deaths</th>';
coronaText += '<th>Day Cases</th><th>Day Deaths</th></thead><tbody>';
let cc = '';
const tc = {
cases: 0,
deaths: 0,
dcases: 0,
ddeaths: 0,
};
coronaCountries.forEach((c, i) => {
tc.cases += c.cases;
tc.deaths += c.deaths;
tc.dcases += c.dcases;
tc.ddeaths += c.ddeaths;
cc += `<tr>
<td>${i+1}</td>
<td><b>${c.name}</b></td>
<td>${c.cases}</td>
<td>${c.deaths}</td>
<td>${c.dcases}</td>
<td>${c.ddeaths}</td>
</tr>`;
})
coronaText += `<tr>
<td>0</td>
<td><b>Total</b></td>
<td>${tc.cases}</td>
<td>${tc.deaths}</td>
<td>${tc.dcases}</td>
<td>${tc.ddeaths}</td>
</tr>`;
coronaText += cc;
coronaText += '</tbody></table>';
coronacases.innerHTML = coronaText;
};
const rangeValueChange = (e) => {
clearTimeout(calcTimeout);
clearInterval(autoPlayIntervalId);
gid('autoplay_checkbox').checked = false;
const value = e.target.value;
calcTimeout = setTimeout(() => {
colorCountries(value);
}, 250);
};
const autoplayChange = () => {
const checked = gid('autoplay_checkbox').checked;
if(!checked) {
clearInterval(autoPlayIntervalId);
} else {
autoPlayIntervalId = setInterval(autoplay, colors.autoplaySpeed);
}
// console.log(checked);
}
if(gid('autoplay_checkbox').checked) {
autoPlayIntervalId = setInterval(autoplay, colors.autoplaySpeed);
}
// console.log(gid('date_range').max, gid('date_range').min, gid('date_range').value);
gid('autoplay_checkbox').addEventListener('change', autoplayChange);
gid('date_range').addEventListener('change', rangeValueChange);
gid('date_range').addEventListener('input', rangeValueChange);
return covidData;
});
})
</script>
</body>
</html>