//Fetch data async function getData() { //let newWorldDataRaw = await fetch (`./Data/world-data.json`); let newWorldDataRaw = await fetch (`https://beta.ctvnews.ca/content/dam/common/exceltojson/full_data.txt`); let newWorldData = await newWorldDataRaw.json(); //console.log(newWorldData) //let newProvinceDataRaw = await fetch (`./Data/canada-new.json`); let newProvinceDataRaw = await fetch (`https://beta.ctvnews.ca/content/dam/common/exceltojson/COVID-19-Canada-New.txt`); let newProvinceData = await newProvinceDataRaw.json(); //console.log(newProvinceData) let provinceData = await formatProvinceData(newProvinceData) let newData = await formatData(newWorldData) function ranCon() { let con = newData[Math.floor(Math.random()*(newData.length))]; //console.log(con) if (con.series100.length > 5) { return con.country; } else { return ranCon() } } let populationArray = []; let mergedData = [...newData, ...provinceData] //console.log(mergedData) return mergedData; } async function formatProvinceData(data) { //{short:'Can', long:'Canada (CTV News)'}, const provArray = [{short:'BC', long:'British Columbia'},{short:'AB', long:'Alberta'},{short:'SK', long:'Saskatchewan'},{short:'MB', long:'Manitoba'},{short:'ON', long:'Ontario'},{short:'QC', long:'Quebec'},{short:'NB', long:'New Brunswick'},{short:'NS', long:'Nova Scotia'},{short:'PE', long:'Prince Edward Island'},{short:'NL', long:'Newfoundland and Labrador'},{short:'YT', long:'Yukon'},{short:'NT', long:'Northwest Territories'},{short:'NU', long:'Nunavut'}] let provinceData = []; provArray.forEach(province => { let obj = { countryShort: province.short, country: province.long, series: [] } provinceData.push(obj) }) let populationArray = [ { name: "British Columbia", population: "5110917", }, { name: "Alberta", population: "4413146", }, { name: "Saskatchewan", population: "1181666", }, { name: "Manitoba", population: "1377517", }, { name: "Ontario", population: "14711827" }, { name: "Quebec", population: "8537674", }, { name: "New Brunswick", population: "779993", }, { name: "Nova Scotia", population: "977457", }, { name: "Prince Edward Island", population: "158158", }, { name: "Newfoundland and Labrador", population: "521365", }, { name: "Yukon", population: "41078", }, { name: "Northwest Territories", population: "44904", }, { name: "Nunavut", population: "39097", }, { name: "Canada", population: "37894799" } ] data.forEach((day, i, nodes) => { if (Number(day['Can_Total']) && Number(day['Can_Total']) > 0) { provinceData.forEach((province, i) => { let obj = { date: Number(day.Date), total_cases: 0, new_cases: 0, total_recovered: 0, new_recovered: 0, total_deaths: 0, new_deaths: 0 } Object.keys(day).forEach((key) => { if (key.split('_')[0] === province.countryShort) { if (key.split('_')[1] === 'Total') { obj.total_cases = Number(day[key]) ? Number(day[key]) : 0; } else if (key.split('_')[1] === 'Recovered') { obj.total_recovered = Number(day[key]) ? Number(day[key]) : 0; } else if (key.split('_')[1] === 'Death') { obj.total_deaths = Number(day[key]) ? Number(day[key]) : 0; } } }) province.series.push(obj) }) } }) provinceData.forEach(province => { province.series.forEach((day, i, nodes) => { day.new_cases = nodes[i].total_cases - (i > 0 ? nodes[i-1].total_cases : 0) day.new_recovered = nodes[i].total_recovered - (i > 0 ? nodes[i-1].total_recovered : 0) day.new_deaths = nodes[i].total_deaths - (i > 0 ? nodes[i-1].total_deaths : 0) if (day.new_cases < 0) {province.series.splice(i, 1)} }) }) function addRollingAverage(data, index, nodes, target, numDays, newName) { let avg = 0; let daysTallied = 0; for (let i = index; i >= Math.max(0, index-(numDays-1)); i--) { avg += nodes[i][target]; daysTallied += 1; } data[newName] = Math.round(avg/daysTallied); } provinceData.forEach(province => { province.series.forEach((day, i, N) =>{ addRollingAverage(day, i, N, 'new_cases', 7, 'new_cases_7avg') addRollingAverage(day, i, N, 'new_deaths', 7, 'new_deaths_7avg') addRollingAverage(day, i, N, 'new_recovered', 7, 'new_recovered_7avg') }) }) //console.log(provArray) provinceData.forEach(country => { let popObj = populationArray.find(obj => obj.name === country.country); if (!popObj) { console.log(country.country, '— no match found') } else { country.population = Number(popObj.population); } }) provinceData.forEach(country => { country.series.forEach((day, i, N) => { day.cases = { raw: { total: day.total_cases, new: day.new_cases, avg: day.new_cases_7avg }, per100k: { total: per100k(day.total_cases, country.population), new: per100k(day.new_cases, country.population), avg: per100k(day.new_cases_7avg, country.population) } } day.deaths = { raw: { total: day.total_deaths, new: day.new_deaths, avg: day.new_deaths_7avg }, per100k: { total: per100k(day.total_deaths, country.population), new: per100k(day.new_deaths, country.population), avg: per100k(day.new_deaths_7avg, country.population) } }, day.recovered = { raw: { total: day.total_recovered, new: day.new_recovered, avg: day.new_recovered_7avg }, per100l: { total: per100k(day.total_recovered), new: per100k(day.new_recovered), avg: per100k(day.new_recovered_7avg) } } }) }) provinceData.forEach(province => { province.series100 = province.series.filter(day => day.total_cases >= 100) }) provinceData.forEach(province => { province.seriesStart = province.series.filter(day => day.total_cases >= 1) }) return provinceData } async function formatData(data) { let countryArray = []; data.forEach((tally, i) => { Object.keys(tally).forEach(key => { if (key !== "location") { tally[key] = Number(tally[key]) } }) let country = countryArray.find(c => c.country === tally.location); if (country) { country.series.push(tally) } else { let newCountry = { country: tally.location, series: [] } newCountry.series.push(tally) countryArray.push(newCountry) } }) /* countryArray.forEach(country => { country.population = Math.round(Math.random()*10000000); }) */ let populationArray = [ { name: "China", population: "1439323776", rank: 1 }, { name: "India", population: "1380004385", rank: 2 }, { name: "United States", population: "331002651", rank: 3 }, { name: "Indonesia", population: "273523615", rank: 4 }, { name: "Pakistan", population: "220892340", rank: 5 }, { name: "Brazil", population: "212559417", rank: 6 }, { name: "Nigeria", population: "206139589", rank: 7 }, { name: "Bangladesh", population: "164689383", rank: 8 }, { name: "Russia", population: "145934462", rank: 9 }, { name: "Mexico", population: "128932753", rank: 10 }, { name: "Japan", population: "126476461", rank: 11 }, { name: "Ethiopia", population: "114963588", rank: 12 }, { name: "Philippines", population: "109581078", rank: 13 }, { name: "Egypt", population: "102334404", rank: 14 }, { name: "Vietnam", population: "97338579", rank: 15 }, { name: "Democratic Republic of Congo", population: "89561403", rank: 16 }, { name: "Germany", population: "83783942", rank: 19 }, { name: "Turkey", population: "84339067", rank: 17 }, { name: "Iran", population: "83992949", rank: 18 }, { name: "Thailand", population: "69799978", rank: 20 }, { name: "United Kingdom", population: "67886011", rank: 21 }, { name: "France", population: "65273511", rank: 22 }, { name: "Italy", population: "60461826", rank: 23 }, { name: "South Africa", population: "59308690", rank: 25 }, { name: "Tanzania", population: "59734218", rank: 24 }, { name: "Myanmar", population: "54409800", rank: 26 }, { name: "Kenya", population: "53771296", rank: 27 }, { name: "South Korea", population: "51269185", rank: 28 }, { name: "Colombia", population: "50882891", rank: 29 }, { name: "Spain", population: "46754778", rank: 30 }, { name: "Argentina", population: "45195774", rank: 32 }, { name: "Uganda", population: "45741007", rank: 31 }, { name: "Ukraine", population: "43733762", rank: 35 }, { name: "Algeria", population: "43851044", rank: 33 }, { name: "Sudan", population: "43849260", rank: 34 }, { name: "Iraq", population: "40222493", rank: 36 }, { name: "Afghanistan", population: "38928346", rank: 37 }, { name: "Poland", population: "37846611", rank: 38 }, { name: "Canada", population: "37742154", rank: 39 }, { name: "Morocco", population: "36910560", rank: 40 }, { name: "Saudi Arabia", population: "34813871", rank: 41 }, { name: "Uzbekistan", population: "33469203", rank: 42 }, { name: "Peru", population: "32971854", rank: 43 }, { name: "Malaysia", population: "32365999", rank: 45 }, { name: "Angola", population: "32866272", rank: 44 }, { name: "Ghana", population: "31072940", rank: 47 }, { name: "Mozambique", population: "31255435", rank: 46 }, { name: "Yemen", population: "29825964", rank: 48 }, { name: "Nepal", population: "29136808", rank: 49 }, { name: "Venezuela", population: "28435940", rank: 50 }, { name: "Madagascar", population: "27691018", rank: 51 }, { name: "Cameroon", population: "26545863", rank: 52 }, { name: "Cote d'Ivoire", population: "26378274", rank: 53 }, { name: "North Korea", population: "25778816", rank: 54 }, { name: "Australia", population: "25499884", rank: 55 }, { name: "Taiwan", population: "23816775", rank: 57 }, { name: "Niger", population: "24206644", rank: 56 }, { name: "Sri Lanka", population: "21413249", rank: 58 }, { name: "Burkina Faso", population: "20903273", rank: 59 }, { name: "Mali", population: "20250833", rank: 60 }, { name: "Romania", population: "19237691", rank: 61 }, { name: "Chile", population: "19116201", rank: 63 }, { name: "Malawi", population: "19129952", rank: 62 }, { name: "Kazakhstan", population: "18776707", rank: 64 }, { name: "Zambia", population: "18383955", rank: 65 }, { name: "Guatemala", population: "17915568", rank: 66 }, { name: "Ecuador", population: "17643054", rank: 67 }, { name: "Netherlands", population: "17134872", rank: 69 }, { name: "Syria", population: "17500658", rank: 68 }, { name: "Cambodia", population: "16718965", rank: 71 }, { name: "Senegal", population: "16743927", rank: 70 }, { name: "Chad", population: "16425864", rank: 72 }, { name: "Somalia", population: "15893222", rank: 73 }, { name: "Zimbabwe", population: "14862924", rank: 74 }, { name: "Guinea", population: "13132795", rank: 75 }, { name: "Rwanda", population: "12952218", rank: 76 }, { name: "Benin", population: "12123200", rank: 77 }, { name: "Tunisia", population: "11818619", rank: 79 }, { name: "Belgium", population: "11589623", rank: 81 }, { name: "Burundi", population: "11890784", rank: 78 }, { name: "Bolivia", population: "11673021", rank: 80 }, { name: "Cuba", population: "11326616", rank: 83 }, { name: "Haiti", population: "11402528", rank: 82 }, { name: "South Sudan", population: "11193725", rank: 84 }, { name: "Dominican Republic", population: "10847910", rank: 85 }, { name: "Czech Republic", population: "10708981", rank: 86 }, { name: "Greece", population: "10423054", rank: 87 }, { name: "Portugal", population: "10196709", rank: 89 }, { name: "Jordan", population: "10203134", rank: 88 }, { name: "Azerbaijan", population: "10139177", rank: 90 }, { name: "Sweden", population: "10099265", rank: 91 }, { name: "United Arab Emirates", population: "9890402", rank: 93 }, { name: "Honduras", population: "9904607", rank: 92 }, { name: "Hungary", population: "9660351", rank: 94 }, { name: "Belarus", population: "9449323", rank: 96 }, { name: "Tajikistan", population: "9537645", rank: 95 }, { name: "Austria", population: "9006398", rank: 97 }, { name: "Papua New Guinea", population: "8947024", rank: 98 }, { name: "Serbia", population: "8737371", rank: 99 }, { name: "Switzerland", population: "8654622", rank: 101 }, { name: "Israel", population: "8655535", rank: 100 }, { name: "Togo", population: "8278724", rank: 102 }, { name: "Sierra Leone", population: "7976983", rank: 103 }, { name: "Hong Kong", population: "7496981", rank: 104 }, { name: "Laos", population: "7275560", rank: 105 }, { name: "Paraguay", population: "7132538", rank: 106 }, { name: "Bulgaria", population: "6948445", rank: 107 }, { name: "Lebanon", population: "6825445", rank: 109 }, { name: "Libya", population: "6871292", rank: 108 }, { name: "Nicaragua", population: "6624554", rank: 110 }, { name: "El Salvador", population: "6486205", rank: 112 }, { name: "Kyrgyzstan", population: "6524195", rank: 111 }, { name: "Turkmenistan", population: "6031200", rank: 113 }, { name: "Singapore", population: "5850342", rank: 114 }, { name: "Denmark", population: "5792202", rank: 115 }, { name: "Finland", population: "5540720", rank: 116 }, { name: "Slovakia", population: "5459642", rank: 118 }, { name: "Congo", population: "5518087", rank: 117 }, { name: "Norway", population: "5421241", rank: 119 }, { name: "Costa Rica", population: "5094118", rank: 122 }, { name: "Palestine", population: "5101414", rank: 121 }, { name: "Oman", population: "5106626", rank: 120 }, { name: "Liberia", population: "5057681", rank: 123 }, { name: "Ireland", population: "4937786", rank: 124 }, { name: "New Zealand", population: "4822233", rank: 126 }, { name: "Central African Republic", population: "4829767", rank: 125 }, { name: "Mauritania", population: "4649658", rank: 127 }, { name: "Panama", population: "4314767", rank: 128 }, { name: "Kuwait", population: "4270571", rank: 129 }, { name: "Croatia", population: "4105267", rank: 130 }, { name: "Moldova", population: "4033963", rank: 131 }, { name: "Georgia", population: "3989167", rank: 132 }, { name: "Eritrea", population: "3546421", rank: 133 }, { name: "Uruguay", population: "3473730", rank: 134 }, { name: "Bosnia and Herzegovina", population: "3280819", rank: 135 }, { name: "Mongolia", population: "3278290", rank: 136 }, { name: "Armenia", population: "2963243", rank: 137 }, { name: "Jamaica", population: "2961167", rank: 138 }, { name: "Puerto Rico", population: "2860853", rank: 141 }, { name: "Albania", population: "2877797", rank: 140 }, { name: "Qatar", population: "2881053", rank: 139 }, { name: "Lithuania", population: "2722289", rank: 142 }, { name: "Namibia", population: "2540905", rank: 143 }, { name: "Gambia", population: "2416668", rank: 144 }, { name: "Botswana", population: "2351627", rank: 145 }, { name: "Gabon", population: "2225734", rank: 146 }, { name: "Lesotho", population: "2142249", rank: 147 }, { name: "Macedonia", population: "2083374", rank: 148 }, { name: "Slovenia", population: "2078938", rank: 149 }, { name: "Guinea-Bissau", population: "1968001", rank: 150 }, { name: "Latvia", population: "1886198", rank: 151 }, { name: "Bahrain", population: "1701575", rank: 152 }, { name: "Trinidad and Tobago", population: "1399488", rank: 154 }, { name: "Equatorial Guinea", population: "1402985", rank: 153 }, { name: "Estonia", population: "1326535", rank: 155 }, { name: "Timor", population: "1318445", rank: 156 }, { name: "Mauritius", population: "1271768", rank: 157 }, { name: "Cyprus", population: "1207359", rank: 158 }, { name: "Swaziland", population: "1160164", rank: 159 }, { name: "Djibouti", population: "98", rank: 160 }, { name: "Fiji", population: "896445", rank: 161 }, { name: "Reunion", population: "895312", rank: 162 }, { name: "Comoros", population: "869601", rank: 163 }, { name: "Guyana", population: "786552", rank: 164 }, { name: "Bhutan", population: "771608", rank: 165 }, { name: "Solomon Islands", population: "686884", rank: 166 }, { name: "Macau", population: "649335", rank: 167 }, { name: "Montenegro", population: "628066", rank: 168 }, { name: "Luxembourg", population: "625978", rank: 169 }, { name: "Western Sahara", population: "597339", rank: 170 }, { name: "Suriname", population: "586632", rank: 171 }, { name: "Cape Verde", population: "555987", rank: 172 }, { name: "Maldives", population: "540544", rank: 173 }, { name: "Malta", population: "441543", rank: 174 }, { name: "Brunei", population: "437479", rank: 175 }, { name: "Guadeloupe", population: "400124", rank: 176 }, { name: "Belize", population: "397628", rank: 177 }, { name: "Bahamas", population: "393244", rank: 178 }, { name: "Martinique", population: "375265", rank: 179 }, { name: "Iceland", population: "341243", rank: 180 }, { name: "Vanuatu", population: "307145", rank: 181 }, { name: "French Guiana", population: "298682", rank: 182 }, { name: "Barbados", population: "287375", rank: 183 }, { name: "New Caledonia", population: "285498", rank: 184 }, { name: "French Polynesia", population: "280908", rank: 185 }, { name: "Mayotte", population: "272815", rank: 186 }, { name: "Sao Tome and Principe", population: "219159", rank: 187 }, { name: "Samoa", population: "198414", rank: 188 }, { name: "Saint Lucia", population: "183627", rank: 189 }, { name: "Guam", population: "168775", rank: 190 }, { name: "Curacao", population: "164093", rank: 191 }, { name: "Kiribati", population: "119449", rank: 192 }, { name: "Micronesia", population: "115023", rank: 193 }, { name: "Grenada", population: "112523", rank: 194 }, { name: "Saint Vincent and the Grenadines", population: "110940", rank: 195 }, { name: "Aruba", population: "106766", rank: 196 }, { name: "United States Virgin Islands", population: "104425", rank: 198 }, { name: "Tonga", population: "105695", rank: 197 }, { name: "Seychelles", population: "98347", rank: 199 }, { name: "Antigua and Barbuda", population: "97929", rank: 200 }, { name: "Isle of Man", population: "85033", rank: 201 }, { name: "Andorra", population: "77265", rank: 202 }, { name: "Dominica", population: "71986", rank: 203 }, { name: "Cayman Islands", population: "65722", rank: 204 }, { name: "Bermuda", population: "62278", rank: 205 }, { name: "Marshall Islands", population: "59190", rank: 206 }, { name: "Northern Mariana Islands", population: "57559", rank: 207 }, { name: "Greenland", population: "56770", rank: 208 }, { name: "American Samoa", population: "55191", rank: 209 }, { name: "Saint Kitts and Nevis", population: "53199", rank: 210 }, { name: "Faeroe Islands", population: "48863", rank: 211 }, { name: "Sint Maarten", population: "42876", rank: 212 }, { name: "Monaco", population: "39242", rank: 213 }, { name: "Turks and Caicos Islands", population: "38717", rank: 214 }, { name: "Liechtenstein", population: "38128", rank: 216 }, { name: "Sint Maarten (Dutch part)", population: "38666", rank: 215 }, { name: "San Marino", population: "33931", rank: 217 }, { name: "Gibraltar", population: "33691", rank: 218 }, { name: "British Virgin Islands", population: "30231", rank: 219 }, { name: "Palau", population: "18094", rank: 220 }, { name: "Cook Islands", population: "17564", rank: 221 }, { name: "Anguilla", population: "15003", rank: 222 }, { name: "Tuvalu", population: "11792", rank: 223 }, { name: "Wallis and Futuna", population: "11239", rank: 224 }, { name: "Nauru", population: "10824", rank: 225 }, { name: "Saint Barthélemy", population: "9877", rank: 226 }, { name: "Saint Pierre and Miquelon", population: "5794", rank: 227 }, { name: "Montserrat", population: "4992", rank: 228 }, { name: "Falkland Islands", population: "3480", rank: 229 }, { name: "Niue", population: "1626", rank: 230 }, { name: "Tokelau", population: "1357", rank: 231 }, { name: "Vatican", population: "801", rank: 232 }, { name: 'Bonaire Sint Eustatius and Saba', population: "25157" }, { name: "Guernsey", population: "63026" }, { name: 'Jersey', population: "106800" }, { name: 'Kosovo', population: "1810463" }, { name: 'World', population: "7777500000" }, { name: 'International', population: '3711' } ] //'International' Population from here: https://www.princess.com/news/notices_and_advisories/notices/diamond-princess-update.html countryArray.forEach(country => { let popObj = populationArray.find(obj => obj.name === country.country); if (!popObj) { console.log(country.country, '— no match found') } else { country.population = Number(popObj.population); } }) function addRollingAverage(data, index, nodes, target, numDays, newName) { let avg = 0; let daysTallied = 0; for (let i = index; i >= Math.max(0, index-(numDays-1)); i--) { avg += nodes[i][target]; daysTallied += 1; } data[newName] = Math.round(avg/daysTallied); } countryArray.forEach(country => { country.series.forEach((day, i, N) =>{ addRollingAverage(day, i, N, 'new_cases', 7, 'new_cases_7avg') addRollingAverage(day, i, N, 'new_deaths', 7, 'new_deaths_7avg') }) }) //console.log(countryArray) countryArray.forEach(country => { country.series.forEach((day, i, N) => { day.cases = { raw: { total: day.total_cases, new: day.new_cases, avg: day.new_cases_7avg }, per100k: { total: per100k(day.total_cases, country.population), new: per100k(day.new_cases, country.population), avg: per100k(day.new_cases_7avg, country.population) } } day.deaths = { raw: { total: day.total_deaths, new: day.new_deaths, avg: day.new_deaths_7avg }, per100k: { total: per100k(day.total_deaths, country.population), new: per100k(day.new_deaths, country.population), avg: per100k(day.new_deaths_7avg, country.population) } } }) }) countryArray.forEach(country => { country.series100 = country.series.filter(day => day.total_cases >= 100) }) countryArray.forEach(country => { country.seriesStart = country.series.filter(day => day.total_cases >= 1) }) return countryArray } colorArray = ["#161515","#e77e00","#b11b5c","#477c13","#033f77","#8800cc","#cc0000","#178b88"] let chartHeight = 450; let chartDot = { big: 3, small: 2, opacity: 1, line: 2 } function createHundredChart(parent, data, countries, metric, dayCap=null, showDropdown=false, showSettings=true, showSlider=true, showSource=true, options={}) { let chartOptions = { showUnder100: false, showZeros: false, showDots: true } for (key in options) { chartOptions[key] = options[key] } if (!chartOptions.showDots) { chartDot.opacity = 0 } let series = 'series100'; if (chartOptions.showZeros) { series = 'series'; } else if (chartOptions.showUnder100) { series = 'seriesStart'; } //console.log(chartOptions) let titleHeight = showSettings ? 130 : 80; window.addEventListener('resize', debounce(() => { updateHundredChart(parent, data, countries, metric, dayCap, showDropdown, showSettings, showSlider, showSource, 0, options) })) countryObjArray = [] countries.forEach(country => { if (country !== 'none') { countryObj = Object.assign({}, data.find(c => c.country === country)); countryObjArray.push(countryObj) } }) let container = d3.select(parent); let barSVG = container.select('svg') .attr('width', '100%').attr('height', chartHeight).attr('class', 'bar-svg') barSVG.selectAll('*').remove() container.selectAll('.chart-options').remove() container.selectAll('.source-div').remove() let axisLayer = barSVG.append('g').attr('class', 'axis-layer') let unhoverLayer = barSVG.append('g').attr('class', 'unhover-layer') unhoverLayer.append('rect').attr('x', 0).attr('y', 0).attr('width', '100%').attr('height', '100%').attr('fill', 'rgba(0,0,0,0)') .on('mouseover', function(d) { unHover() }) let barLayer = barSVG.append('g').attr('class', 'bar-layer') let countryLayersArray = [] countries.forEach((country, i) => { let countryLayer = barLayer.append('g').attr('class', `country-${i}-layer`) countryLayersArray.push(countryLayer) }) let dividerLayer = barSVG.append('g').attr('class', 'divider-layer') let titleLayer = barSVG.append('g').attr('class', 'title-layer').attr('transform', 'translate(10,10)') let buttonLayer = barSVG.append('g').attr('class', 'button-layer').attr('transform', 'translate(10,48)') let legendLayer = barSVG.append('g').attr('class', 'legend-layer').attr('transform', `translate(10,${showSettings ? 85 : 47})`) let toolTipLayer = barSVG.append('g').attr('class', 'tooltip-layer') let width = barSVG.node().getBoundingClientRect().width; function unHover() { //console.log('un-hover') countryLayersArray.forEach(clayer => { clayer.attr('opacity', 1) }) toolTipLayer.selectAll('path').remove() toolTipLayer.selectAll('text').remove() legendLayer.selectAll('rect').attr('stroke-width', 0) } ///// Dropdowns if (showDropdown) { let dropDownDiv = container.append('div').attr('class', 'dropdown-div chart-options'); let dropDownTitle = dropDownDiv.append('div').attr('class', 'dropdown-title chart-options-title') dropDownTitle.text('Choose regions'); function makeDropDown(dropParent, index) { let dropDownContainer = dropParent.append('div').attr('class', 'drop-container') dropDownContainer.style('border-bottom', `3px solid ${colorArray[index]}`) let dropDown = dropDownContainer.append('select').attr('class', 'hundred-dropdown'); //dropDown.append('option').attr('value', 'none').text('-----') data.forEach((country, i) => { if (country[series].length > 0) { let option = dropDown.append('option') .attr('value', country.country) .text(country.country) if (country.country === countries[index]) { option.attr('selected', true) } } }) dropDown.on('change', function(d) { let newCountries = []; this.parentElement.parentElement.querySelectorAll('select').forEach(dropdown => { newCountries.push(dropdown.value) //console.log(dropdown.options[dropdown.selectedIndex].value) }) countries = newCountries; createHundredChart(parent, data, countries, metric, dayCap, showDropdown, showSettings, showSlider, showSource) }) } countries.forEach((country, i) => { makeDropDown(dropDownDiv, i) }) } let titleSize = width > 500 ? 22 : 22; let title = titleLayer.append('text') .attr('x', 0) .attr('y', titleSize) .attr('font-family', `'CTVSans-Bold','CTV Sans',Arial`) .attr('font-size', titleSize) .text(function(d) { return `COVID-19 ${metric[0] === 'cases' ? 'Cases' : 'Deaths'} (${metric[2] === 'avg' ? '7-day avg' : metric[2] === 'total' ? 'Total' : 'Daily'}${metric[1] === 'per100k' ? ', per 100K' : ''})` }) dividerLayer.append('rect') .attr('x', 0).attr('y', 0) .attr('width', '100%').attr('height', showSettings ? 84.5 : 46.5) .attr('stroke', '#f3f3f3') .attr('fill', 'white') /////////// Buttons if (showSettings) { let buttonOptions1 = [['Cases', 'cases'], ['Deaths', 'deaths']]; let buttonOptions2 = [['Total', 'total'], ['Daily', 'new'], ['7-Day', 'avg']] let buttonOptions3 = [['Raw', 'raw'], ['/100K', 'per100k']] let buttonPad = 5; let buttonFont = 12; let buttonGroupSpace = 10; let buttonGroupY = 0; let buttonActive = '#333'; let buttonTextActive = 'white'; let buttonSize = buttonFont + 5 + buttonPad*2; //let shiftLeft = 0; //let shiftDown = 50; //let underline = titleLayer.append('line').attr('x1', 0).attr('y1', shiftDown + 5.5).attr('x2', 80).attr('y2', shiftDown + 5.5).attr('stroke', 'black') let buttonLeft = 1; let buttonGroup1 = buttonLayer.append('g') buttonOptions1.forEach((button, i) => { let box = buttonGroup1.append('rect') .attr('x', buttonLeft) .attr('y', buttonGroupY) .attr('height', buttonFont + buttonPad*2) .attr('shape-rendering', 'crispEdges') .style('cursor', 'pointer') .on('click', function() { buttonGroup1.selectAll('rect').attr('fill', '#efefef') buttonGroup1.selectAll('text').attr('fill', '#555') buttonGroup1.selectAll('rect').attr('stroke', '#ccc') box.attr('stroke', buttonActive) box.attr('fill', buttonActive) text.attr('fill', buttonTextActive) metric[0] = button[1]; updateHundredChart(parent, data, countries, metric, dayCap, showDropdown, showSettings, showSlider, showSource, 1000, options) title.text(function(d) { return `COVID-19 ${metric[0] === 'cases' ? 'Cases' : 'Deaths'} (${metric[2] === 'avg' ? '7-day avg' : metric[2] === 'total' ? 'Total' : 'Daily'}${metric[1] === 'per100k' ? ', per 100K' : ''})` }) }) let text = buttonGroup1.append('text') .attr('font-family', `'CTVSans-Regular','CTV Sans',Arial`) .attr('class','click-text') .text(button[0]) .attr('y', buttonGroupY + buttonFont + buttonPad/2) .attr('x', buttonLeft + buttonPad) .style('cursor', 'pointer') .attr('font-size', buttonFont) .attr('fill', metric[0] === button[1] ? buttonTextActive : '#333') .attr('pointer-events', 'none') box .attr('width', buttonPad + text.node().getComputedTextLength() + buttonPad) .attr('fill', metric[0] === button[1] ? buttonActive : '#efefef') .attr('stroke', metric[0] === button[1] ? buttonActive : '#ccc') buttonLeft += buttonPad + text.node().getComputedTextLength() + buttonPad + buttonGroupSpace/2; }) buttonLeft += buttonGroupSpace; let buttonGroup2 = buttonLayer.append('g') buttonOptions2.forEach((button, i) => { let box = buttonGroup2.append('rect') .attr('x', buttonLeft) .attr('y', buttonGroupY) .attr('height', buttonFont + buttonPad*2) .attr('stroke', '#ccc') .attr('shape-rendering', 'crispEdges') .style('cursor', 'pointer') .on('click', function(d, i, N) { buttonGroup2.selectAll('rect').attr('fill', '#efefef') buttonGroup2.selectAll('text').attr('fill', '#555') buttonGroup2.selectAll('rect').attr('stroke', '#ccc') box.attr('fill', buttonActive) box.attr('stroke', buttonActive) text.attr('fill', buttonTextActive) metric[2] = button[1]; updateHundredChart(parent, data, countries, metric, dayCap, showDropdown, showSettings, showSlider, showSource, 1000, options) title.text(function(d) { return `COVID-19 ${metric[0] === 'cases' ? 'Cases' : 'Deaths'} (${metric[2] === 'avg' ? '7-day avg' : metric[2] === 'total' ? 'Total' : 'Daily'})` }) }) let text = buttonGroup2.append('text') .attr('font-family', `'CTVSans-Regular','CTV Sans',Arial`) .attr('class','click-text') .text(button[0]) .attr('y', buttonGroupY + buttonFont + buttonPad/2) .attr('x', buttonLeft + buttonPad) .attr('font-size', buttonFont) .attr('fill', metric[2] === button[1] ? buttonTextActive : '#333') .attr('pointer-events', 'none') box .attr('width', buttonPad + text.node().getComputedTextLength() + buttonPad) .attr('fill', metric[2] === button[1] ? buttonActive : '#efefef') .attr('stroke', metric[2] === button[1] ? buttonActive : '#ccc') buttonLeft += buttonPad + text.node().getComputedTextLength() + buttonPad + buttonGroupSpace/2; }) buttonLeft += buttonGroupSpace; let buttonGroup3 = buttonLayer.append('g') buttonOptions3.forEach((button, i) => { let box = buttonGroup3.append('rect') .attr('x', buttonLeft) .attr('y', buttonGroupY) .attr('height', buttonFont + buttonPad*2) .attr('shape-rendering', 'crispEdges') .style('cursor', 'pointer') .on('click', function() { buttonGroup3.selectAll('rect').attr('fill', '#efefef') buttonGroup3.selectAll('text').attr('fill', '#555') buttonGroup3.selectAll('rect').attr('stroke', '#ccc') box.attr('stroke', buttonActive) box.attr('fill', buttonActive) text.attr('fill', buttonTextActive) metric[1] = button[1]; updateHundredChart(parent, data, countries, metric, dayCap, showDropdown, showSettings, showSlider, showSource, 1000, options) title.text(function(d) { return `COVID-19 ${metric[0] === 'cases' ? 'Cases' : 'Deaths'} (${metric[2] === 'avg' ? '7-day avg' : metric[2] === 'total' ? 'Total' : 'Daily'}${metric[1] === 'per100k' ? ', per 100K' : ''})` }) }) let text = buttonGroup3.append('text') .attr('font-family', `'CTVSans-Regular','CTV Sans',Arial`) .attr('class','click-text') .text(button[0]) .attr('y', buttonGroupY + buttonFont + buttonPad/2) .attr('x', buttonLeft + buttonPad) .style('cursor', 'pointer') .attr('font-size', buttonFont) .attr('fill', metric[1] === button[1] ? buttonTextActive : '#333') .attr('pointer-events', 'none') box .attr('width', buttonPad + text.node().getComputedTextLength() + buttonPad) .attr('fill', metric[1] === button[1] ? buttonActive : '#efefef') .attr('stroke', metric[1] === button[1] ? buttonActive : '#ccc') buttonLeft += buttonPad + text.node().getComputedTextLength() + buttonPad + buttonGroupSpace/2; }) } ///////// Legend let left = 1; let top = 0; let legendFont = width > 500 ? 14 : 12; let legendPad = 5; let legendSpace = 3; let legendLeftPad = width > 500 ? 38 : 32; whiteRect = legendLayer.append('rect') countryObjArray.forEach((country, i) => { //console.log(country) let backRect = legendLayer.append('rect') let text = legendLayer.append('text').text(country.country) .attr('y', top + legendFont + legendPad/2) .attr('x', left + legendLeftPad + legendSpace) .attr('font-size', legendFont) .attr('fill', '#333333') .attr('font-family', `'CTVSans-Bold','CTV Sans',Arial`) .attr('pointer-events', 'none') backRect .attr('y', top) .attr('x', left) .attr('width', left + legendLeftPad + legendSpace + text.node().getComputedTextLength() + legendPad*2) .attr('height', legendFont + legendPad*2) .attr('fill', 'white') .attr('stroke-width', 0) .attr('stroke', colorArray[i]) .attr('shape-rendering', 'crispEdges') .on('mouseover', function() { this.setAttribute('stroke-width', 1) //this.setAttribute('fill', colorArray[i] + '11') countryLayersArray.forEach(clayer => { clayer.attr('opacity', 0.1) }) countryLayersArray[i].attr('opacity', 1) }) .on('mouseout', function() { unHover() }) let line = legendLayer.append('line') .attr('y1', top + (legendFont + legendPad*2)/2) .attr('y2', top + (legendFont + legendPad*2)/2) .attr('x1', left + legendPad) .attr('x2', left + legendLeftPad - legendSpace) .attr('stroke', colorArray[i]) .attr('stroke-linecap', 'round') .attr('stroke-width', chartDot.line) .attr('pointer-events', 'none') let dot = legendLayer.append('circle') .attr('cy', top + (legendFont + legendPad*2)/2) .attr('cx', left + legendPad + (legendLeftPad - legendSpace - legendPad)/2) .attr('r', width > 500 ? chartDot.big : chartDot.small) .attr('opacity', chartDot.opacity) .attr('stroke', 'white') .attr('stroke-width', 1) .attr('fill', colorArray[i]) .attr('pointer-events', 'none') top += legendFont + legendPad*2 + 1; }) whiteRect .attr('x', -10) .attr('y', 0) .attr('width', 11) .attr('height', top) .attr('fill', 'white') //////// Buttons chartHeight = 450; barSVG.attr('height', chartHeight) //change height based on countries let maxDayArray = []; countryObjArray.forEach(obj => { let length = obj[series].length; maxDayArray.push(length) }) let slideMax = Math.max(...maxDayArray); //console.log(maxDayArray) ////// Slider if (showSlider) { let sliderDiv = container.append('div').attr('class', 'slider-div chart-options') let sliderTitle = sliderDiv.append('div').attr('class', 'slider-title chart-options-title') sliderTitle.text('Choose timeline cutoff') let sliderText = sliderDiv.append('p') .html(`${dayCap ? dayCap -1 : slideMax -1}/ ${slideMax-1}`) let slider = sliderDiv.append('input') .attr('class', 'slider') .attr('type', 'range') .attr('min', 1) .attr('max', slideMax) .attr('value', dayCap ? dayCap : slideMax) .style('background', function(d) { let value = dayCap ? dayCap : slideMax; let pct = (100/(slideMax-1)*value - (100/(slideMax-1))); return `linear-gradient(to right, black 0%, black ${pct}%, #bbb ${pct}%, #bbb 100%)` }) .style('width','100%') .on('input', function(d) { let pct = (100/(slideMax-1)*this.value - (100/(slideMax-1))); this.style.background = `linear-gradient(to right, black 0%, black ${pct}%, #bbb ${pct}%, #bbb 100%)`; dayCap = this.value; sliderText.html(`${this.value-1}/ ${slideMax-1}`) updateHundredChart(parent, data, countries, metric, dayCap, showDropdown, showSettings, showSlider, showSource, 0, options) }) } ///////// Source / Credit if (showSource) { let sourceDiv = container.append('div').attr('class', 'source-div') let sourceText = sourceDiv.append('p') .style('font-size', '10px') .html(`Source: via `) let sourceImgLink = sourceDiv.append('a') .attr('href', '/health/coronavirus/') .attr('target', '_blank') .attr('class', 'source-img') let sourceImg = sourceImgLink.append('img') .attr('src', '/polopoly_fs/1.4703529!/httpImage/image.png_gen/derivatives/default/image.png') } if (dayCap) { countryObjArray.forEach(obj => { obj.series100 = obj[series].filter((d, i) => i < dayCap) }) maxDayArray = [] countryObjArray.forEach(obj => { let length = obj[series].length; maxDayArray.push(length) }) } let maxArray = [] countryObjArray.forEach(obj => { let max = Math.max.apply(Math, obj[series].map(function(o) { return o[metric[0]][metric[1]][metric[2]]; })); maxArray.push(max) }) let max = Math.max(1, Math.max(...maxArray)) var heightScale = d3.scaleLinear() .domain([0, Math.max(max, 1)]) .range([chartHeight - 50, titleHeight]); let numDays = Math.max(...maxDayArray); let bar = { width: (width - 40)/numDays, pad: 1 } let xScale = d3.scaleLinear() .domain([0, numDays-1]) .range([20, width-40-20]) let line0 = d3.line() .x((d, i) => xScale(i)) .y(d=> heightScale(0)) .curve(d3.curveMonotoneX) let line = d3.line() .x((d, i) => xScale(i)) .y(d=> heightScale(d[metric[0]][metric[1]][metric[2]])) //.curve(d3.curveMonotoneX) function chart(fullData, layer, color, delay) { let data = fullData[series] layer .append('path') .datum(data) .attr('d', line0) .attr('stroke', color) .attr('stroke-linecap', 'round') .attr('stroke-width', chartDot.line) .attr('fill', 'none') //.transition().duration(1000).delay(delay) .attr('d', line) let c1 = layer.selectAll('circle').data(data) c1.enter().append('circle') .attr('cx', (d,i) => xScale(i)) .attr('cy', (d,i) => heightScale(0)) .attr('r', width > 500 ? chartDot.big : chartDot.small) .attr('opacity', chartDot.opacity) .attr('fill', color) .attr('stroke-width', 1) .attr('stroke', '#FFFFFF') .on('mouseover', (d, i) => { countryLayersArray.forEach(clayer => { clayer.attr('opacity', 0.1) }) layer.attr('opacity', 1) let tip = toolTipLayer.append('path') let countryName = toolTipLayer.append('text') .attr('x', xScale(i)) .attr('y', heightScale(d[metric[0]][metric[1]][metric[2]]) - 43) .attr('fill', '#333') .attr('text-anchor', 'middle') .attr('font-size', 12) .html(`${fullData.country} (${toDate(Number(d['date']))})`) let numText = toolTipLayer.append('text') .attr('x', xScale(i)) .attr('y', heightScale(d[metric[0]][metric[1]][metric[2]]) - 21) .attr('fill', '#333') .attr('text-anchor', 'middle') .attr('font-family', `'CTVSans-Bold','CTV Sans',Arial`) .attr('font-size', 20) .text(decimal(d[metric[0]][metric[1]][metric[2]])) tip.attr('d', (d, i) => { let w = Math.max(numText.node().getComputedTextLength(), countryName.node().getComputedTextLength()) + 10; let h = 45; let pw = 10; let ph = 8; return `M ${Math.round(-w/2) - 0.5} ${-h - 0.5} H ${Math.round(w/2) + 0.5} V ${0.5} H ${pw + 0.5} L 0 ${ph + 0.5} L ${-pw - 0.5} ${0.5} H ${Math.round(-w/2) - 0.5} Z` }) .attr('transform', `translate(${Math.round(xScale(i))}, ${Math.round(heightScale(d[metric[0]][metric[1]][metric[2]]) - 14)})`) .attr('fill', 'white') .attr('stroke-width', 1) .attr('stroke', '#333') }) .on('mouseout', function(d) { unHover() }) //.transition().duration(1000).delay(delay) .attr('cy', (d,i) => heightScale(d[metric[0]][metric[1]][metric[2]])) } countryObjArray.forEach((country, i) => { chart(country, countryLayersArray[i], colorArray[i], 500*i) }) let ax = axisLayer.selectAll('text .num').data(new Array(Math.max(numDays, 1))) ax.enter().append('text') .attr('class', 'num') .text((d, i) => i) .attr('y', heightScale(0) + 16) .attr('x', (d,i) => xScale(i)) .attr('text-anchor', 'middle') .attr('font-size', 12) .attr('opacity', (d, i, N) => (i % Math.round(N.length / 10) === 0 ? 1 : 0)) .attr('font-family', `'CTVSans-Regular','CTV Sans',Arial`) let titleText = `Days since ${series === 'series100' || series === 'seriesStart' ? "each region's" : ''} ${series === 'series100' ? '100th' : 'first'} case` axisLayer.append('text').text(titleText) .attr('class', 'since') .attr('x', width/2) .attr('y', heightScale(0)+36) .attr('text-anchor', 'middle') .attr('font-size', 12) .attr('fill', '#333') .attr('font-family', `'CTVSans-Regular','CTV Sans',Arial`) let axisMarks = [] for (let i = 0; i <= 10; i++) { let top = Math.pow(10, Math.ceil(Math.log10(max/100))) let topMax = Math.ceil(max/top)*top axisMarks.push(i*Math.ceil(topMax/10)) } let axM = axisLayer.selectAll('line').data(axisMarks); axM.enter().append('line') .attr('x1', 0) .attr('y1', heightScale(0) ) .attr('x2', '100%') .attr('y2', heightScale(0) ) .attr('stroke-width', (d,i) => i === 0 ? 2 : 1) .attr('shape-rendering', 'crispEdges') .attr('stroke', (d,i) => i === 0 ? '#333' : '#f0f0f0') .attr('opacity', -1) .attr('y1', d => heightScale(d) + 0.5) .attr('y2', d => heightScale(d) + 0.5) .attr('opacity', 1) let axT = axisLayer.selectAll('text .marker').data(axisMarks); axT.enter().append('text') .attr('class', 'marker') .attr('x', width - 10) .attr('y', d => heightScale(50)) .attr('opacity', -1) .text(d => d) .attr('font-size', 12) .attr('fill', '#333333') .attr('font-family', `'CTVSans-Regular','CTV Sans',Arial`) .attr('text-anchor', 'end') .attr('y', d => heightScale(d) - 2) .attr('opacity', (d, i) => d/max < 0.025 ? 0 : 1) let newaxis = axisLayer.append('g').attr('class', 'vert') let av = newaxis.selectAll('line').data(new Array(Math.max(1, numDays))) av.enter().append('line') .attr('x1', (d,i) => xScale(i)) .attr('y1', heightScale(0) - 1 ) .attr('x2',(d,i) => xScale(i)) .attr('y2', heightScale(max*1.2) ) .attr('stroke-width', 1) .attr('shape-rendering', 'crispEdges') .attr('stroke', '#f0f0f0') .attr('opacity', (d, i, N) => (i % Math.round(N.length / 10) === 0 ? 1 : 0)) } function updateHundredChart(parent, data, countries, metric, dayCap=null, showDropdown=false, showSettings=true, showSlider=true, showSource=true, animateTime, options) { let chartOptions = { showUnder100: false, showZeros: false, showDots: true } for (key in options) { chartOptions[key] = options[key] } if (!chartOptions.showDots) { chartDot.opacity = 0 } let series = 'series100'; if (chartOptions.showZeros) { series = 'series'; } else if (chartOptions.showUnder100) { series = 'seriesStart'; } let titleHeight = showSettings ? 130 : 80; countryObjArray = [] countries.forEach(country => { if (country !== 'none') { countryObj = Object.assign({}, data.find(c => c.country === country)); countryObjArray.push(countryObj) } }) let container = d3.select(parent); let barSVG = container.select('svg') .attr('width', '100%').attr('height', chartHeight).attr('class', 'bar-svg') let axisLayer = barSVG.select('.axis-layer') let barLayer = barSVG.select('.bar-layer') let countryLayersArray = [] countries.forEach((country, i) => { let countryLayer = barLayer.select(`.country-${i}-layer`) countryLayersArray.push(countryLayer) }) let titleLayer = barSVG.select('.title-layer') let legendLayer = barSVG.select('.legend-layer') let toolTipLayer = barSVG.select('.tooltip-layer') if (dayCap) { countryObjArray.forEach(obj => { obj[series] = obj[series].filter((d, i) => i < dayCap) }) } function unHover() { countryLayersArray.forEach(clayer => { clayer.attr('opacity', 1) }) toolTipLayer.selectAll('path').remove() toolTipLayer.selectAll('text').remove() legendLayer.selectAll('rect').attr('stroke-width', 0) } let maxArray = [] countryObjArray.forEach(obj => { let max = Math.max.apply(Math, obj[series].map(function(o) { return o[metric[0]][metric[1]][metric[2]];})); maxArray.push(max) }) let max = Math.max(1, Math.max(...maxArray)) let maxDayArray = []; countryObjArray.forEach(obj => { let length = obj[series].length; maxDayArray.push(length) }) var heightScale = d3.scaleLinear() .domain([0, max]) .range([chartHeight - 50, titleHeight]); let width = barSVG.node().getBoundingClientRect().width; let numDays = Math.max(...maxDayArray); let bar = { width: (width - 40)/numDays, pad: 1 } let xScale = d3.scaleLinear() .domain([0, numDays-1]) .range([20, width-40-20]) let line0 = d3.line() .x((d, i) => xScale(i)) .y(d=> heightScale(0)) //.curve(d3.curveMonotoneX) let line = d3.line() .x((d, i) => xScale(i)) .y(d=> heightScale(d[metric[0]][metric[1]][metric[2]])) //.curve(d3.curveMonotoneX) function chart(fullData, layer, color, delay) { let data = fullData[series]; layer .select('path') .datum(data) .attr('stroke', color) .attr('stroke-linecap', 'round') .attr('stroke-width', chartDot.line) .attr('fill', 'none') .transition().duration(animateTime).delay(delay) .attr('d', line) let c1 = layer.selectAll('circle').data(data) c1.exit().remove() c1 .attr('r', width > 500 ? chartDot.big : chartDot.small) .attr('opacity', chartDot.opacity) .attr('fill', color) .attr('stroke', '#FFFFFF') .on('mouseover', (d, i) => { countryLayersArray.forEach(clayer => { clayer.attr('opacity', 0.1) }) layer.attr('opacity', 1) let tip = toolTipLayer.append('path') let countryName = toolTipLayer.append('text') .attr('x', xScale(i)) .attr('y', heightScale(d[metric[0]][metric[1]][metric[2]]) - 43) .attr('fill', '#333') .attr('text-anchor', 'middle') .attr('font-size', 12) .html(`${fullData.country} (${toDate(Number(d['date']))})`) let numText = toolTipLayer.append('text') .attr('x', xScale(i)) .attr('y', heightScale(d[metric[0]][metric[1]][metric[2]]) - 21) .attr('fill', '#333') .attr('text-anchor', 'middle') .attr('font-family', `'CTVSans-Bold','CTV Sans',Arial`) .attr('font-size', 20) .text(decimal(d[metric[0]][metric[1]][metric[2]])) tip.attr('d', (d, i) => { let w = Math.max(numText.node().getComputedTextLength(), countryName.node().getComputedTextLength()) + 10; let h = 45; let pw = 10; let ph = 8; return `M ${Math.round(-w/2) - 0.5} ${-h - 0.5} H ${Math.round(w/2) + 0.5} V ${0.5} H ${pw + 0.5} L 0 ${ph + 0.5} L ${-pw - 0.5} ${0.5} H ${Math.round(-w/2) - 0.5} Z` }) .attr('transform', `translate(${Math.round(xScale(i))}, ${Math.round(heightScale(d[metric[0]][metric[1]][metric[2]]) - 14)})`) .attr('fill', 'white') .attr('stroke-width', 1) .attr('stroke', '#333') }) .on('mouseout', function(d) { unHover() }) .transition().duration(animateTime).delay(delay) .attr('cx', (d,i) => xScale(i)) .attr('cy', (d,i) => heightScale(d[metric[0]][metric[1]][metric[2]])) c1.enter().append('circle') .attr('opacity', 1) .attr('r', width > 500 ? chartDot.big : chartDot.small) .attr('opacity', chartDot.opacity) .attr('fill', color) .attr('stroke', '#FFFFFF') .on('mouseover', (d, i) => { countryLayersArray.forEach(clayer => { clayer.attr('opacity', 0.1) }) layer.attr('opacity', 1) let tip = toolTipLayer.append('path') let countryName = toolTipLayer.append('text') .attr('x', xScale(i)) .attr('y', heightScale(d[metric[0]][metric[1]][metric[2]]) - 43) .attr('fill', '#333') .attr('text-anchor', 'middle') .attr('font-size', 12) .html(`${fullData.country} (${toDate(Number(d['date']))})`) let numText = toolTipLayer.append('text') .attr('x', xScale(i)) .attr('y', heightScale(d[metric[0]][metric[1]][metric[2]]) - 21) .attr('fill', '#333') .attr('text-anchor', 'middle') .attr('font-family', `'CTVSans-Bold','CTV Sans',Arial`) .attr('font-size', 20) .text(decimal(d[metric[0]][metric[1]][metric[2]])) tip.attr('d', (d, i) => { let w = Math.max(numText.node().getComputedTextLength(), countryName.node().getComputedTextLength()) + 10; let h = 45; let pw = 10; let ph = 8; return `M ${Math.round(-w/2) - 0.5} ${-h - 0.5} H ${Math.round(w/2) + 0.5} V ${0.5} H ${pw + 0.5} L 0 ${ph + 0.5} L ${-pw - 0.5} ${0.5} H ${Math.round(-w/2) - 0.5} Z` }) .attr('transform', `translate(${Math.round(xScale(i))}, ${Math.round(heightScale(d[metric[0]][metric[1]][metric[2]]) - 14)})`) .attr('fill', 'white') .attr('stroke-width', 1) .attr('stroke', '#333') }) .on('mouseout', function(d) { unHover() }) .attr('cx', (d,i) => xScale(i)) .attr('cy', (d,i) => heightScale(d[metric[0]][metric[1]][metric[2]])) .transition().duration(animateTime).delay(delay) .attr('opacity', 1) } countryObjArray.forEach((country, i) => { chart(country, countryLayersArray[i], colorArray[i], 0) }) axisLayer.select('.since').transition().duration(500).attr('x', width/2) let ax = axisLayer.selectAll('.num').data(new Array(numDays)) ax.exit().remove() ax .attr('class', 'num') .text((d, i) => i) .attr('y', heightScale(0) + 16) .attr('text-anchor', 'middle') .attr('font-size', 12) .attr('opacity', (d, i, N) => (i % Math.round(N.length / 10) === 0 ? 1 : 0)) .attr('font-family', `'CTVSans-Regular','CTV Sans',Arial`) .transition().duration(animateTime) .attr('x', (d,i) => xScale(i)) ax.enter().append('text') .attr('class', 'num') .text((d, i) => i) .attr('y', heightScale(0) + 16) .attr('x', (d,i) => xScale(i)) .attr('text-anchor', 'middle') .attr('font-size', 12) .attr('opacity', (d, i, N) => (i % Math.round(N.length / 10) === 0 ? 1 : 0)) .attr('font-family', `'CTVSans-Regular','CTV Sans',Arial`) let axisMarks = [] for (let i = 0; i <= 10; i++) { let top = Math.pow(10, Math.ceil(Math.log10(max/100))) let topMax = Math.ceil(max/top)*top axisMarks.push(i*Math.ceil(topMax/10)) } let axM = axisLayer.selectAll('line').data(axisMarks); axM .attr('x1', 0) .attr('x2', '100%') .attr('stroke-width', (d,i) => i === 0 ? 2 : 1) .attr('shape-rendering', 'crispEdges') .attr('stroke', (d,i) => i === 0 ? '#333' : '#f0f0f0') .attr('opacity', 1) .transition().duration(animateTime) .attr('y1', d => heightScale(d) + 0.5) .attr('y2', d => heightScale(d) + 0.5) let axT = axisLayer.selectAll('.marker').data(axisMarks); axT .attr('class', 'marker') .attr('x', width - 10) .text(d => d) .attr('font-size', 12) .attr('fill', '#333333') .attr('font-family', `'CTVSans-Regular','CTV Sans',Arial`) .attr('text-anchor', 'end') .attr('opacity', (d, i) => d/max < 0.025 ? 0 : 1) .transition().duration(animateTime) .attr('y', d => heightScale(d) - 2) let newaxis = axisLayer.select('.vert') let av = newaxis.selectAll('line').data(new Array(numDays)) av.exit().remove() av .attr('x1', (d,i) => xScale(i)) .attr('y1', heightScale(0) - 1 ) .attr('x2',(d,i) => xScale(i)) .attr('y2', heightScale(max*1.2) ) .attr('stroke-width', 1) .attr('shape-rendering', 'crispEdges') .attr('stroke', '#f0f0f0') .attr('opacity', (d, i, N) => (i % Math.round(N.length / 10) === 0 ? 1 : 0)) av.enter().append('line') .attr('x1', (d,i) => xScale(i)) .attr('y1', heightScale(0) ) .attr('x2',(d,i) => xScale(i)) .attr('y2', heightScale(max*1.2) ) .attr('stroke-width', 1) .attr('shape-rendering', 'crispEdges') .attr('stroke', '#f0f0f0') .attr('opacity', (d, i, N) => (i % Math.round(N.length / 10) === 0 ? 1 : 0)) } ////// Date Chart end function make(type, parent, CLASS, ID) { let element = document.createElement(type); if (typeof CLASS !== 'undefined') { element.setAttribute('class', CLASS); } if (typeof ID !== 'undefined') { element.setAttribute('id', ID); } return parent.appendChild(element); } function toDate(excelDate) { let newDate = new Date((excelDate - (25567 + 1))*86400*1000); let month = newDate.getMonth() + 1; let day = newDate.getDate() return `${month}/${day}` } function debounce(func){ var timer; return function(event){ if(timer) clearTimeout(timer); timer = setTimeout(func,100,event); }; } function decimal(number) { return Math.round((number + Number.EPSILON) * 100) / 100 } function per100k(raw, population) { return raw*(100000/population) }