378 lines
15 KiB
HTML
378 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";
|
|
} else if (feature.properties.iso_a2 == "GR") {
|
|
return "EL";
|
|
} else if (feature.properties.iso_a2 == "-99") {
|
|
return "XK";
|
|
}
|
|
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 && covid) {
|
|
const cont = gid('rollover_data');
|
|
cont.innerText = `${props.sovereignt} - Total : ${formatNumber(covid.ctotal, " ")} Deaths : ${formatNumber(covid.dtotal, " ")} Population : ${formatNumber(props.pop_est, " ")}`;
|
|
} else {
|
|
console.log('problem');
|
|
}
|
|
})
|
|
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>
|