const make = (type, parent, CLASS = null, ID = null) => { let element = document.createElement(type); CLASS ? element.setAttribute("class", CLASS) : null; ID ? element.setAttribute("id", ID) : null; return parent.appendChild(element); }; class WorldApp { constructor(selector, data) { this.data = data; this.gridArray = []; this.populationArray = populationArray; this.selector = selector; this.filterArray = [ "International", "Jersey", "Guernsey", "Bonaire Sint Eustatius and Saba", "Tokelau", "Niue", "Montserrat", "Saint Pierre and Miquelon", "Saint Barthélemy", "Nauru", "Wallis and Futuna", "Tuvalu", "Anguilla", "Cook Islands", "Palau", "Sint Maarten (Dutch part)", "Sint Maarten", "Faeroe Islands", "Saint Kitts and Nevis", "Northern Mariana Islands", "Vatican", "World", "Monaco", "Micronesia (country)", "North America", "Europe", "European Union", "Oceania", "Asia", "Africa", "South America", "Lower middle income", "Upper middle income", "Low income", "High income", ]; } async create() { let formattedData = this.formatData(this.data); this.filteredData = formattedData.filter( (country) => !this.filterArray.includes(country.name) ); this.build(); } formatData(raw) { // Group data by country, add population let grouped = []; raw.forEach((line) => { let country = grouped.find((c) => c.name === line.location); let data = { date: line.date, cases: { new: Number(line.new_cases), total: Number(line.total_cases), }, deaths: { new: Number(line.new_deaths), total: Number(line.total_deaths), }, }; if (country) { country.data.push(data); } else { grouped.push({ name: line.location, population: Number( this.populationArray.find((p) => p.name === line.location) ?.population ), data: [data], perCapita(num, capita = 100000) { return (capita * num) / this.population; }, }); } }); //Add 7-day average grouped.forEach((country) => { country.data.forEach((day, i, arr) => { day.cases.avg = arr .slice(Math.max(i - 6, 0), i + 1) .reduce((acc, val) => acc + val.cases.new, 0) / (i + 1 - Math.max(i - 6, 0)); day.deaths.avg = arr .slice(Math.max(i - 6, 0), i + 1) .reduce((acc, val) => acc + val.deaths.new, 0) / (i + 1 - Math.max(i - 6, 0)); }); }); grouped = grouped.filter((country) => !isNaN(country.population)); return grouped; } build() { document .querySelectorAll(this.selector) .forEach((div) => new WorldGrid(div, this.filteredData).create()); } } class WorldGrid { constructor(div, data) { this.div = div; this.data = data; this.countryArray = this.div.dataset.countries .split(",") .map((a) => a.trim()); this.metricArray = this.div.dataset.metric.split(",").map((a) => a.trim()); this.limit = Number(this.div.dataset.limit); this.max = this.div.dataset.max; this.top = this.countryArray[0] === "Top"; //window.onresize = () => this.update(); } create() { if (!this.countryArray[0]) { return; } function dropdown(parent, options) { let dropdown = make("select", parent, "dropdown"); options.forEach((option) => { let op = make("option", dropdown, "dropdown-option"); op.innerText = typeof option === "object" ? option[0] : option; op.value = typeof option === "object" ? option[1] : option; }); return dropdown; } let topDiv = make("div", this.div, "top-div"); this.gridTitle = make("div", topDiv, "grid-title bold-text"); let line1 = make("div", topDiv, "top-line"); line1.innerText = "Show countries:"; dropdown(line1, [ ["Top 12", 12], ["Top 50", 50], ["Show all", ""], ]).onchange = (s) => { this.limit = s.target.value; this.update(); }; let line2 = make("div", topDiv, "top-line"); line2.innerText = "Show data:"; dropdown(line2, [ ["avg daily", "avg"], ["daily", "new"], "total", ]).onchange = (s) => { this.metricArray[1] = s.target.value; this.update(); }; dropdown(line2, ["cases", "deaths"]).onchange = (s) => { this.metricArray[0] = s.target.value; this.update(); }; dropdown(line2, [ ["per 100K", "per"], ["raw number", ""], ]).onchange = (s) => { this.metricArray[2] = s.target.value; this.update(); }; let line3 = make("div", topDiv, "top-line"); line3.innerText = "Scale of charts:"; dropdown(line3, [ ["Single scale", "equal"], ["Individual scales", ""], ]).onchange = (s) => { this.max = s.target.value; this.update(); }; this.gridDiv = make("div", this.div, "grid-div"); this.update(); } update() { let [m0, m1, m2] = this.metricArray; this.gridTitle.innerText = `World countries, ranked by ${ m1 === "total" ? "total" : m1 === "avg" ? "average recent" : "recent" } ${m0 === "deaths" ? "deaths" : "cases"} ${ m2 === "per" ? "(per 100k)" : "" }`; if (this.top) { let objArray; if (m2 === "per") { objArray = this.data.sort( (a, b) => b.perCapita(b.data[b.data.length - 1][m0][m1]) - a.perCapita(a.data[a.data.length - 1][m0][m1]) ); } else { objArray = this.data.sort( (a, b) => b.data[b.data.length - 1][m0][m1] - a.data[a.data.length - 1][m0][m1] ); } this.countryArray = objArray.map((a) => a.name); } this.gridDiv.innerHTML = ""; let allMax = this.countryArray.reduce((acc, val) => { let country = this.data.find((c) => c.name === val); let maxCases = country.data.reduce((acc2, val2) => { if (m2 === "per") { return country.perCapita(val2[m0][m1]) > acc2 ? country.perCapita(val2[m0][m1]) : acc2; } else { return val2[m0][m1] > acc2 ? val2[m0][m1] : acc2; } }, 0); return maxCases > acc ? maxCases : acc; }, 0); this.countryArray.forEach((country, i) => { if (!this.limit || i < this.limit) { new WorldChart( this.data.find((c) => c.name === country), i + 1, this.gridDiv, this.metricArray, this.max === "equal" ? allMax : null ).create(); } }); } } class WorldChart { constructor(country, rank, parent, metricArray, max = null) { this.rank = rank; this.parent = parent; this.country = country; this.metricArray = metricArray; this.max = max; } create() { let [m0, m1, m2] = this.metricArray; let div = make("div", this.parent, "chart-div"); let canvas = make("canvas", div, "chart-canvas"); const nameChange = { Palestine: "West Bank & Gaza", }; const displayName = nameChange[this.country.name] || this.country.name; let title = make("div", div, "country-name bold-text"); title.innerHTML = `#${this.rank} ${displayName}`; let number = make("div", title, "country-number"); let num = m2 === "per" ? this.country.perCapita( this.country.data[this.country.data.length - 1][m0][m1] ) : this.country.data[this.country.data.length - 1][m0][m1]; let string = `${m0} (${m1}${m2 === "per" ? "/100K" : ""})`; number.innerHTML = `${num.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: m0 === "deaths" ? 2 : 1, })} ${this.rank === 1 ? string : ""}`; let dpi = window.devicePixelRatio; let W = canvas.offsetWidth * dpi; let H = canvas.offsetHeight * dpi; canvas.width = W; canvas.height = H; if (!this.max) { this.max = this.country.data.reduce((acc, val) => { if (m2 === "per") { return this.country.perCapita(val[m0][m1]) > acc ? this.country.perCapita(val[m0][m1]) : acc; } else { return val[m0][m1] > acc ? val[m0][m1] : acc; } }, 0); } const pad = { left: 0 * dpi, right: 0 * dpi, top: 40 * dpi, bottom: 12 * dpi, }; let c = canvas.getContext("2d"); c.fillStyle = "#ffbd69"; c.font = `${8 * dpi}px sans-serif`; c.textAlign = "center"; c.fillStyle = "#fcfcfc"; c.fillRect(0, H - pad.bottom + 0.5, W, H - pad.bottom); this.country.data.forEach((day, i, a) => { let d = m2 === "per" ? this.country.perCapita(day[m0][m1 === "total" ? "total" : "new"]) : day[m0][m1 === "total" ? "total" : "new"]; c.fillRect( pad.left + (W - pad.right - pad.left) * (i / a.length), H - pad.bottom - pad.top - (H - pad.bottom - pad.top) * (d / this.max) + pad.top, W / a.length, (H - pad.bottom - pad.top) * (d / this.max) ); let monthArray = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ]; c.fillStyle = "#888"; c.textAlign = "middle"; if (day.date.split("-")[2] === "01") { let monthIndex = +day.date.split("-")[1] - 1; if (monthIndex % 3 === 2) { c.fillText( monthArray[monthIndex], pad.left + (W - pad.right - pad.left) * (i / a.length), H - 3 * dpi ); } c.fillRect( pad.left + (W - pad.right - pad.left) * (i / a.length), H - pad.bottom, 1, 3 * dpi ); } c.fillStyle = this.country.name === "Canada" ? "#ff7869" : "#ffbd69"; }); c.beginPath(); c.moveTo(0, H - pad.bottom + 0.5); c.lineTo(W, H - pad.bottom + 0.5); c.strokeStyle = "#aaa"; c.stroke(); c.beginPath(); let startHeight = H - pad.bottom - pad.top - ((H - pad.bottom - pad.top) * (m2 === "per" ? this.country.perCapita( this.country.data[0][m0][m1 === "total" ? "total" : "avg"] ) : this.country.data[0][m0][m1 === "total" ? "total" : "avg"])) / this.max + pad.top; c.moveTo(0, startHeight); this.country.data.forEach((day, i, a) => { let d = m2 === "per" ? this.country.perCapita(day[m0][m1 === "total" ? "total" : "avg"]) : day[m0][m1 === "total" ? "total" : "avg"]; c.lineTo( W * (i / a.length), H - pad.bottom - pad.top - (H - pad.bottom - pad.top) * (d / this.max) + pad.top ); }); c.strokeStyle = "#2a2a2a"; c.lineWidth = 1.5 * dpi; c.lineJoin = "round"; c.stroke(); if (this.country.name === "Canada") { div.classList.add("canada"); } } }