//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 (Â鶹ӰÊÓ)'},
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)
}