class CovidRegionApp { constructor() { this.chartArray = []; } async create() { Promise.all([ d3.csv('https://beta.ctvnews.ca/content/dam/common/exceltojson/us-data.txt'), d3.csv('https://beta.ctvnews.ca/content/dam/common/exceltojson/us-population.txt'), d3.json('https://beta.ctvnews.ca/content/dam/common/exceltojson/COVID-19-Canada-New.txt'), ]).then((files) => { this.stateData = this.sortStateData(files[0], files[1]); this.provinceData = this.sortProvinceData(files[2]); this.allData = [...this.stateData, ...this.provinceData]; //this.printTop(this.allData, 100) this.build(); }); } autoText() { let sorted = this.allData.filter((a) => a.population).sort((a, b) => b['max']['avg7']['casesPM'] - a['max']['avg7']['casesPM']); let topProvince = sorted.find((a) => a.type === 'province'); let topProvinceRank = sorted.findIndex((a) => a === topProvince) + 1; function getEnd(num) { if (num.toString() !== "11" && num.toString().split('').pop() === '1') { return 'st'; } else if (num.toString() !== "12" && num.toString().split('').pop() === '2') { return 'nd'; } else if (num.toString() !== "13" && num.toString().split('').pop() === '3') { return 'rd'; } else { return 'th'; } } document.querySelector('.can-peak-prov').innerText = topProvince.name; document.querySelector('.can-peak-rank').innerText = topProvinceRank + getEnd(topProvinceRank); let sorted2 = this.allData .filter((a) => a.population) .sort((a, b) => b['current']['avg7']['casesPM'] - a['current']['avg7']['casesPM']); let topProvince2 = sorted2.find((a) => a.type === 'province'); let topProvinceRank2 = sorted2.findIndex((a) => a === topProvince2) + 1; let bottomStates = sorted2.filter((a, i) => i >= topProvinceRank2 && a.type === 'state'); let numStates = bottomStates.length; let nameStates = bottomStates.reduce((acc, cur, i) => (acc += i === 0 ? cur.name : ', ' + cur.name), ''); document.querySelector('.us-bot-num').innerText = numStates; document.querySelector('.us-bot-states').innerText = numStates > 0 ? ` (${nameStates})` : ''; if (numStates > 9) {document.querySelector('.us-bot-states').remove()} document.querySelector('.can-top-prov').innerText = topProvince2.name; document.querySelector('.can-top-rank').innerText = topProvinceRank2 + getEnd(topProvinceRank2); } sortProvinceData(data) { let populationArray = [ { name: 'British Columbia', short: 'BC', population: '5110917', }, { name: 'Alberta', short: 'AB', population: '4413146', }, { name: 'Saskatchewan', short: 'SK', population: '1181666', }, { name: 'Manitoba', short: 'MB', population: '1377517', }, { name: 'Ontario', short: 'ON', population: '14711827', }, { name: 'Quebec', short: 'QC', population: '8537674', }, { name: 'New Brunswick', short: 'NB', population: '779993', }, { name: 'Nova Scotia', short: 'NS', population: '977457', }, { name: 'Prince Edward Island', short: 'PE', population: '158158', }, { name: 'Newfoundland and Labrador', short: 'NL', population: '521365', }, { name: 'Yukon', short: 'YT', population: '41078', }, { name: 'Northwest Territories', short: 'NT', population: '44904', }, { name: 'Nunavut', short: 'NU', population: '39097', }, /*{ name: "Canada", population: "37894799" }*/ ]; //console.log(data); let canadaData = populationArray.map((prov) => { return { type: 'province', name: prov.name, short: prov.short, population: Number(prov.population), data: [], }; }); data = data.filter((day) => day.Date !== ''); data.forEach((day) => { canadaData.forEach((province) => { province.data.push({ date: new Date((day.Date - (25567 + 1)) * 86400 * 1000), cases: { cases: Number(day[`${province.short}_Total`]), }, }); }); }); canadaData.forEach((province) => { if (province.data[province.data.length - 1].cases.cases < province.data[province.data.length - 2].cases.cases) { province.data.pop(); } this.addNewCases(province); this.add7DayAverage(province); this.addPerMillion(province); }); //console.log(canadaData); return canadaData; } sortStateData(data, population) { data.forEach((county) => { let popObj = population.find((obj) => Number(obj.Fips) === Number(county.FIPS)); if (popObj) { county.population = Number(popObj.POPESTIMATE2019); county.pop = popObj; } }); // Create new array which will eventually have one object per state/territory let sortedData = []; let nonDateKeys = [ 'population', 'pop', 'UID', 'iso2', 'iso3', 'code3', 'FIPS', 'Admin2', 'Province_State', 'Country_Region', 'Lat', 'Long_', 'Combined_Key', ]; // Loop through each county in data data.forEach((region) => { let state = region['Province_State']; let county = region['Admin2']; // Create new county object let newCounty = { type: 'county', name: county, population: region['population'], popObj: region['pop'], data: [], }; // Raw data has each date as a key... group keys that are dates into an array of objects for (const key in region) { if (!nonDateKeys.includes(key)) { newCounty.data.push({ date: key, cases: { cases: Number(region[key]), }, }); } } //newCounty.data.forEach(day => day.cases = Number(day.cases)) newCounty.data.sort((a, b) => a.date - b.date); // Sort to ensure the dates are in order.. I think this needs work this.addNewCases(newCounty); this.add7DayAverage(newCounty); this.addPerMillion(newCounty); // Check if county's state is already in the new Array of states // If so, add new county to state's county array. If not, create new state obj first let existingState = sortedData.find((obj) => obj.name === state); if (existingState) { existingState.counties.push(newCounty); newCounty.data.forEach((dayCounty) => { let day = existingState.data.find((dayState) => dayState.date === dayCounty.date); day.cases.cases += dayCounty.cases.cases; }); } else { let newState = { type: 'state', name: state, data: newCounty.data, counties: [newCounty], }; sortedData.push(newState); } }); // Now that state array with counties has been established // loop through to calculate 7-day averages, per-million, peak, current number, etc. sortedData.forEach((state) => { this.addStatePop(state); this.addNewCases(state); this.add7DayAverage(state); this.addPerMillion(state); }); return sortedData; } addStatePop(state) { state.population = state.counties.reduce((total, current) => total + (current.population ? current.population : 0), 0); } addNewCases(region) { region.max = { cases: { date: null, cases: 0 }, new: { date: null, cases: 0 }, avg7: { date: null, cases: 0 }, }; region.current = { cases: { date: null, cases: 0 }, new: { date: null, cases: 0 }, avg7: { date: null, cases: 0 }, }; region.data.forEach((day, i, array) => { day.new = { cases: i === 0 ? 0 : Math.max(0, day.cases.cases - array[i - 1].cases.cases), }; if (day.new.cases > region.max.new.cases) { region.max.new.cases = day.new.cases; region.max.new.date = day.date; } if (i === region.data.length - 1) { region.current.new.cases = day.new.cases; region.current.new.date = day.date; region.current.cases.cases = day.cases.cases; region.current.cases.date = day.date; region.max.cases.cases = day.cases.cases; region.max.cases.date = day.date; } }); } add7DayAverage(region) { region.data.forEach((day, i, array) => { let sum = 0; for (let j = i; j >= Math.max(0, i - 6); j--) { sum += array[j].new.cases; } day.avg7 = { cases: sum / 7, }; if (day.avg7.cases > region.max.avg7.cases) { region.max.avg7.cases = day.avg7.cases; region.max.avg7.date = day.date; } if (i === region.data.length - 1) { region.current.avg7.cases = day.avg7.cases; region.current.avg7.date = day.date; } }); } addPerMillion(region) { if (region.population) { region.max.avg7.casesPM = (1000000 * region.max.avg7.cases) / region.population; region.max.new.casesPM = (1000000 * region.max.new.cases) / region.population; region.max.cases.casesPM = (1000000 * region.max.cases.cases) / region.population; region.current.avg7.casesPM = (1000000 * region.current.avg7.cases) / region.population; region.current.new.casesPM = (1000000 * region.current.new.cases) / region.population; region.current.cases.casesPM = (1000000 * region.current.cases.cases) / region.population; region.data.forEach((day) => { day.cases.casesPM = (1000000 * day.cases.cases) / region.population; day.new.casesPM = (1000000 * day.new.cases) / region.population; day.avg7.casesPM = (1000000 * day.avg7.cases) / region.population; }); } } build() { //console.log(this.allData); if (document.querySelectorAll('.auto-text').length > 0) { this.autoText(); } let self = document.querySelector('#selfToggle'); self.onchange = () => this.update(); let dropdown0 = document.querySelector('#metric0'); let dropdown1 = document.querySelector('#metric1'); let dropdown2 = document.querySelector('#metric2'); dropdown0.onchange = () => this.update(); dropdown1.onchange = () => this.update(); dropdown2.onchange = () => this.update(); this.metric0 = dropdown0.value; // current, max this.metric1 = dropdown1.value; // cases, avg7, new this.metric2 = dropdown2.value; // cases, casesPM this.max = self.value; let box = d3.select('.compare-body'); box.selectAll('.loading').remove(); this.allData = this.allData.filter((state) => state.population); this.allData = this.allData.sort((a, b) => b[this.metric0][this.metric1][this.metric2] - a[this.metric0][this.metric1][this.metric2]); //this.allMax = this.allData[0]['max'][this.metric1][this.metric2]; this.allMax = Math.max(...this.allData.map(region => region['max'][this.metric1][this.metric2]))*0.5 this.allData.forEach((region, i) => { this.makeCharts(box, region, i); }); } update() { let self = document.querySelector('#selfToggle'); let dropdown0 = document.querySelector('#metric0'); let dropdown1 = document.querySelector('#metric1'); let dropdown2 = document.querySelector('#metric2'); this.metric0 = dropdown0.value; // current, max this.metric1 = dropdown1.value; // cases, avg7, new this.metric2 = dropdown2.value; // cases, casesPM this.max = self.value; this.allData = this.allData.sort((a, b) => b[this.metric0][this.metric1][this.metric2] - a[this.metric0][this.metric1][this.metric2]); //this.allMax = this.allData[0]['max'][this.metric1][this.metric2]; this.allMax = Math.max(...this.allData.map(region => region['max'][this.metric1][this.metric2]))*0.5 let box = d3.select('.compare-body'); box.selectAll('div').remove(); box.append('div').attr('class', 'loading'); window.setTimeout(() => { this.allData.forEach((region, i) => { this.makeCharts(box, region, i); }); box.selectAll('.loading').remove(); }, 50); } makeCharts(parent, region, i) { let rank = this.allData.filter((r) => r.type === region.type).findIndex((r) => r === region) + 1; let div = parent.append('div').attr('class', 'covid-info').attr('data-region', region.name); new CovidBox( region, div, rank, /*i < 3 ? true :*/ false, this.metric0, this.metric1, this.metric2, this.max === 'max' ? this.allMax : region['max'][this.metric1][this.metric2] ).create(); } } class CovidBox { constructor(region, container, rank, open, metric0, metric1, metric2, max) { this.rank = rank; this.max = max; this.region = region; this.container = container; this.open = open; this.metric0 = metric0; this.metric = metric1; this.PM = metric2; this.caseType = this.metric === 'new' ? 'new cases' : this.metric === 'cases' ? 'total cases' : 'cases per day (7-day average)'; this.casePM = this.PM === 'casesPM' ? 'per million people' : ''; } create() { let dec = { minimumFractionDigits: 1, maximumFractionDigits: 1 }; let chartHeight = 85; let chartHeightOpen = 200; let chartTopPad = 10; //Pct this.container .attr('class', `covid-info loaded ${this.region.type}`) .style('height', !this.open ? `${chartHeight}px` : `${chartHeightOpen}px`); let [newCases, newDate, maxCases, maxDate] = [ this.region.current[this.metric][this.PM], this.region.current[this.metric].date, this.region.max[this.metric][this.PM], this.region.max[this.metric].date, ]; let defaultText = this.metric0 === 'max' ? maxCases : newCases; let defaultDate = this.metric0 === 'max' ? maxDate : newDate; let smallText = (text, date) => { let type0 = this.metric === 'cases' ? '(Total)' : this.metric0 === 'max' ? '(Peak)' : ''; let type1 = this.metric === 'avg7' ? 'avg. daily' : this.metric === 'new' ? 'daily' : 'total'; let type2 = this.PM === 'casesPM' ? 'per million' : ''; return `
${text.toLocaleString(undefined, dec)}
${type1}${type1 && type2 ? ', ' : ''}${type2}
on ${date}
`; }; if (this.region.type === 'province') { defaultDate = defaultDate ? defaultDate.toLocaleString().split(',')[0] : '–'; } let level = newCases / maxCases >= 0.8 ? '#c70039' : newCases / maxCases >= 0.5 ? '#f37121' : '#ffbd69'; let text = this.container.append('div').attr('class', 'covid-text'); let textTitle = text.append('div').attr('class', 'covid-title bold'); textTitle.text(this.region.name); this.textInner = text .append('div') .attr('class', 'covid-text-inner') .html( `

Population: ${this.region.population.toLocaleString()}

Total cases: ${this.region.current.cases.cases.toLocaleString()}

Current single day cases: ${this.region.current.new.cases.toLocaleString()}

Peak single day cases: ${this.region.max.new.cases.toLocaleString()}

` ) .style('display', this.open ? 'block' : 'none'); let chartRank = this.container.append('div').attr('class', 'chart-rank'); chartRank.append('div').attr('class', 'rank-number bold').html(`#${this.rank}`); chartRank .append('div') .attr('class', 'rank-country') .text(this.region.type === 'state' ? 'in U.S.' : 'in Canada'); let barInfo = this.container.append('div').attr('class', 'bar-info').html(smallText(defaultText, defaultDate)); let barChart = this.container .append('div') .attr('class', 'bar-chart') //.style('height', `${chartHeight}px`); .style('height', 100 - chartTopPad + '%'); barChart .selectAll('div') .data(this.region.data) .enter() .append('div') .attr('class', 'bar') .style("position", "absolute") .style('width', (d, i, arr) => `calc(${100*1/arr.length}% + 0.1px)`) .style("min-width", "1px") .style("left", (d, i, arr) => (100*i/arr.length) + '%') .style('background', () => { let thisMax = this.region.max[this.metric][this.PM] / this.max; let stop1 = thisMax * (chartHeight * 0.5); let stop2 = thisMax * (chartHeight * 0.8); let shade1 = this.region.type === 'state' ? '#ffbd69' : '#76b1a9'; let shade2 = this.region.type === 'state' ? '#f37121' : '#00947b'; let shade3 = this.region.type === 'state' ? '#c70039' : '#11584d'; return `linear-gradient(0deg, ${shade1} 0px, ${shade2} ${stop1}px, ${shade2} ${stop1}px, ${shade3} ${stop2}px, ${shade3} ${stop2}px)`; }) //.style('height', (d) => (d[this.metric][this.PM] >= 0 ? d[this.metric][this.PM] / (this.max / chartHeight) + 'px' : 0)) .style('height', (d) => (d[this.metric][this.PM] >= 0 ? 100 * (d[this.metric][this.PM] / this.max) + '%' : 0)) .on('mouseover', (d) => barInfo.html(smallText(d[this.metric][this.PM], typeof d.date === 'object' ? d.date.toLocaleString().split(',')[0] : d.date)) ) .on('mouseout', () => barInfo.html(smallText(defaultText, defaultDate))); /* .html( `${d[this.metric][this.PM].toLocaleString(undefined, dec)} on ${ typeof d.date === 'object' ? d.date.toLocaleString().split(',')[0] : d.date }` ) */ let covidToggle = this.container.append('div').attr('class', 'covid-toggle'); covidToggle.text(this.open ? '▲' : '▼').on('click', () => { this.container.style('height', this.open ? `${chartHeight}px` : `${chartHeightOpen}px`); this.textInner.style('display', this.open ? 'none' : 'block'); barChart.selectAll('div').style('background', () => { let thisMax = this.region.max[this.metric][this.PM] / this.max; let stop1 = thisMax * (this.open ? chartHeight * 0.5 : chartHeightOpen * 0.5); let stop2 = thisMax * (this.open ? chartHeight * 0.8 : chartHeightOpen * 0.8); let shade1 = this.region.type === 'state' ? '#ffbd69' : '#76b1a9'; let shade2 = this.region.type === 'state' ? '#f37121' : '#00947b'; let shade3 = this.region.type === 'state' ? '#c70039' : '#11584d'; return `linear-gradient(0deg, ${shade1} 0px, ${shade2} ${stop1}px, ${shade2} ${stop1}px, ${shade3} ${stop2}px, ${shade3} ${stop2}px)`; }); this.open = !this.open; covidToggle .transition() .delay(50) .text(this.open ? '▲' : '▼'); }); let scaleDiv = this.container .append('div') .attr('class', 'scale-div') .style('height', 100 - chartTopPad + '%'); let ticks = 3; for (let i = 0; i < ticks; i++) { scaleDiv .append('div') .attr('class', 'scale-number top-num') .style('margin-top', (i === 0 ? -12 : -6) + 'px') .style('top', 100 - (100 * i) / (ticks - 1) + '%') .text(() => { let num = Math.round(this.max * (i / (ticks - 1))); return this.max > 9999 ? num === 0 ? 0 : Math.round(num / 1000) + 'K' : this.max > 999 ? num === 0 ? 0 : (num / 1000).toLocaleString(undefined, dec) + 'K' : num; }); scaleDiv .append('div') .attr('class', 'scale-tick') .style('border-top', `${i === 0 ? 0 : 1}px solid #5f5f5f`) .style('top', 100 - (100 * i) / (ticks - 1) + '%'); } } } let App = new CovidRegionApp(); window.onload = () => App.create();