class Column { constructor(head, body, width, fn = (d) => d) { this.head = head; this.body = body; this.width = width; this.fn = fn; } } class Table { constructor(container, dataset, dataColumns, classList, region, highlight, metric, chart = null) { this.container = container; this.dataset = dataset; this.dataColumns = dataColumns; this.classList = classList; this.region = region; this.highlight = highlight; this.metric = metric; this.chart = chart; } sort() { if (this.metric) { const testValue = this.dataset.filter((d) => d[this.metric] !== "")[0][this.metric]; if (!isNaN(testValue)) { this.dataset = this.dataset.sort((a, b) => b[this.metric] - a[this.metric]); } else { if (this.metric === "date") { this.dataset = this.dataset.sort((a, b) => b[this.metric].localeCompare(a[this.metric])); } else { this.dataset = this.dataset.sort((a, b) => a[this.metric].localeCompare(b[this.metric])); } } } return this; } update() { this.container.selectAll("*").remove(); const table = this.container.append("table").attr("class", this.classList); this.dataColumns.forEach((column) => { if (column.width) { table.append("col").attr("width", column.width); } }); const thead = table.append("thead"); this.dataColumns.forEach((column) => { if (column.span !== 0) { const th = thead.append("th").text(column.head); if (this.metric) { if (this.metric === column.body) { th.attr("class", "sorted"); } th.on("click", () => { //if (this.chart.locked) return; const saveMetric = this.metric; this.metric = column.body; this.sort(); if (["region", "date"].includes(column.body)) { this.metric = saveMetric; } this.update(); //if (this.chart) { // this.chart.updateRegion(this.region); //} }); } th.attr("colspan", column.span ? column.span : 1); } }); const tbody = table.append("tbody"); this.dataset.forEach((row, i) => { const tableRow = tbody.append("tr"); if (row.region === this.highlight) tableRow.attr("class", "bold highlighted"); this.dataColumns.forEach((column) => { tableRow .append("td") .html( `${ row.region === "Canada" && column.body === "region" && this.highlight !== "Canada" ? ` ` : "" }${column.fn( row[column.body], i, row.data.map((d) => d.total[column.body]), row )}` ) .attr("class", column.body === "region" && row.region === this.region ? "sorted" : "") .on("click", () => { if (this.chart.locked) return; if (!["region", "date"].includes(column.body)) { this.metric = column.body; } this.region = row.region; this.update(); if (this.chart) { this.chart.updateRegion(this.region); } }); }); }); if (table.node().offsetHeight > this.container.node().offsetHeight) { this.container.append("div").attr("class", "shadow"); } return this; } } const dateNumToString = (d, dash = true, showYear = false) => { const date = new Date(Date.UTC(0, 0, d, -19)); //const monthArray = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; const month = String(date.getUTCMonth() + 1).padStart(2, "0"); const day = String(date.getUTCDate()).padStart(2, "0"); const year = String(date.getUTCFullYear()); if (dash) { if (showYear) { return `${month}-${day}-${year}`; } return `${month}-${day}`; } else { if (showYear) { return `${month}/${day}`; } return `${month}/${day}/${year}`; } }; const url = "https://beta.ctvnews.ca/content/dam/common/exceltojson/COVID-Variants.txt"; // const url = "https://beta.ctvnews.ca/content/dam/common/exceltojson/COVID-Variants-TEST.txt"; d3.json(url).then((raw) => { const filtered = raw.filter((row) => row.Date); const provinceNameKey = { BC: "British Columbia", AB: "Alberta", SK: "Saskatchewan", MB: "Manitoba", ON: "Ontario", QC: "Quebec", NB: "New Brunswick", NS: "Nova Scotia", PE: "Prince Edward Island", NL: "Newfoundland and Labrador", YT: "Yukon", NT: "Northwest Territories", NU: "Nunavut", Canada: "Canada", }; const total = filtered.find((row) => row.Date === "Total"); const updated = filtered.find((row) => row.Date === "Updated"); const data = []; const includedArray = [ /*"QC", "SK"*/ ]; Object.keys(total).forEach((key) => { const short = key.split("_")[0]; const long = provinceNameKey[short]; const region = data.find((d) => d.region === long); if (!region) { if (long) { const newObj = { region: long, regionShort: short, B117: +total[`${short}_B117`], B1351: +total[`${short}_B1351`], P1: +total[`${short}_P1`], B1617: +total[`${short}_B1617`], Omicron: +total[`${short}_Omicron`], Screen: +total[`${short}_Screen`], dateNum: Math.max( ...[ +updated[`${short}_B117`], +updated[`${short}_B1351`], +updated[`${short}_P1`], +updated[`${short}_B1617`], +updated[`${short}_Omicron`], +updated[`${short}_Screen`], ] ), get date() { return dateNumToString(this.dateNum); }, //population: +population[long], data: [], }; if (includedArray.includes(short)) { console.log(short); newObj.Screen = newObj.Screen - newObj.B117 - newObj.B1351 - newObj.P1 - newObj.B1617; } //Object.setPrototypeOf(newObj, protoProv); data.push(newObj); } } else { /**/ } }); data.forEach((region) => { region.data = filtered .filter((row) => !["Updated", "Total"].includes(row.Date)) .map((row) => { const short = region.regionShort; const getTotal = (strain) => { const num = row[`${short}_${strain}`]; return num !== "" ? +num : null; }; const newObj = { dateNum: +row[`Date`], total: { B117: getTotal("B117"), B1351: getTotal("B1351"), P1: getTotal("P1"), B1617: getTotal("B1617"), Omicron: getTotal("Omicron"), Screen: getTotal("Screen"), dateNum: +row[`Date`], get date() { return dateNumToString(this.dateNum, true, true); }, }, get date() { return dateNumToString(this.dateNum, true, true); }, }; if (includedArray.includes(short)) { newObj.Screen = newObj.Screen - newObj.B117 - newObj.B1351 - newObj.P1; } return newObj; }); }); // DAILY + AVG + FILL IN BLANKS data.forEach((region) => { region.data.forEach((day, i, arr) => { const metrics = ["total"]; metrics.forEach((metric) => { day.added = []; Object.keys(day[metric]).forEach((key) => { if (day[metric][key] === null) { day[metric][key] = i === 0 ? 0 : arr[i - 1][metric][key]; day.added.push(key); } }); }); }); }); data.forEach((region) => { region.data.forEach((day, i, arr) => { if (i === 0) { day.new = { date: day[`date`], B117: 0, B1351: 0, P1: 0, B1617: 0, Omicron: 0, Screen: 0, }; } else { day.new = { date: day[`date`], B117: arr[i].total["B117"] - arr[i - 1].total["B117"], B1351: arr[i].total["B1351"] - arr[i - 1].total["B1351"], P1: arr[i].total["P1"] - arr[i - 1].total["P1"], B1617: arr[i].total["B1617"] - arr[i - 1].total["B1617"], Omicron: arr[i].total["Omicron"] - arr[i - 1].total["Omicron"], Screen: arr[i].total["Screen"] - arr[i - 1].total["Screen"], }; } }); }); data.forEach((region) => { region.data.forEach((day, i, arr) => { if (i < 6) { day.avg = { date: day[`date`], B117: 0, B1351: 0, P1: 0, B1617: 0, Omicron: 0, Screen: 0, }; } else { const avg = (strain, days = 7) => (arr[i].total[strain] - arr[i - (days - 1)].total[strain]) / days; day.avg = { date: day[`date`], B117: avg("B117"), B1351: avg("B1351"), P1: avg("P1"), B1617: avg("B1617"), Omicron: avg("Omicron"), Screen: avg("Screen"), }; } }); }); console.log(data); const totalAndNew = (d, i, arr) => { const num = d.toLocaleString(undefined, { maximumFractionDigits: 0 }); const difference = arr[arr.length - 1] - arr[arr.length - 2]; const diff = difference.toLocaleString(undefined, { maximumFractionDigits: 0 }); return `${num}
${difference >= 0 ? "+" : ""}${diff}
`; }; const topOptions = [ d3.select(".top-table"), [data.find((row) => row.region === "Canada")], [ new Column("Alpha (B.1.1.7)", "B117", "20%", totalAndNew), new Column("Beta (B.1.351)", "B1351", "20%", totalAndNew), new Column("Gamma (P.1)", "P1", "20%", totalAndNew), new Column("Delta (B.1.617)", "B1617", "20%", totalAndNew), new Column("Omicron (B.1.1.529)", "Omicron", "20%", totalAndNew), new Column("Screened*", "Screen", "20%", totalAndNew), ], "vaccine-table top", "Canada", "Canada", null, ]; const provinceOptions = [ d3.select(".province-table"), data, [ new Column("Province", "region", "30%"), new Column("Alpha", "B117", "12%", (d) => d.toLocaleString()), new Column("Beta", "B1351", "12%", (d) => d.toLocaleString()), new Column("Gamma", "P1", "12%", (d) => d.toLocaleString()), new Column("Delta", "B1617", "12%", (d) => d.toLocaleString()), new Column("Omicron", "Omicron", "12%", (d) => d.toLocaleString()), new Column("Screened*", "Screen", "12%", (d) => d.toLocaleString()), ], "vaccine-table bottom", "Canada", "Canada", "B117", ]; const chart = new Chart(".variant-chart", data).updateRegion("Canada", false); new Table(...topOptions, chart).update(); new Table(...provinceOptions, chart).sort().update(); d3.select(".updated").text(`Updated ${dateNumToString(data.find((d) => d.region === "Canada").dateNum, false)}`); });