function createCanadaVaccineApp() { d3.json("https://beta.ctvnews.ca/content/dam/common/exceltojson/Vaccine-Dose-Test.txt").then((raw) => { //Format Data 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 thirdDoseException = { SK: true, }; const protoProv = { get percent() { return (100 * this.first) / this.population; }, get percent16() { return (100 * this.first) / this.population16; }, get percent12() { return (100 * this.first) / this.population12; }, get percent5() { return (100 * this.first) / this.population5; }, get percent2nd() { return (100 * this.second) / this.population; }, get percent2nd16() { return (100 * this.second) / this.population16; }, get percent2nd12() { return (100 * this.second) / this.population12; }, get percent2nd5() { return (100 * this.second) / this.population5; }, get distPercent() { return this.distributed > 0 ? (100 * this.total) / this.distributed : 0; }, get first() { return this.total - this.second - (thirdDoseException[this.regionShort] ? 0 : this.third); }, get date() { const date = new Date(Date.UTC(0, 0, this.dateNum, -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()); return `${month}-${day}`; }, }; 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 totalUpdated = canadaPopulation[long]["total updated"]; const totalStat = canadaPopulation[long]["All ages"]; const twelveUpdated = canadaPopulation[long]["12 years updated"]; const twelveStat = canadaPopulation[long]["12 years and over"]; const fiveUpdated = canadaPopulation[long]["5 years updated"]; const fiveStat = canadaPopulation[long]["5 years and over"]; const newObj = { region: long, regionShort: short, total: +total[`${short}_Doses`], second: +total[`${short}_2nd`], third: +total[`${short}_3rd`], distributed: +total[`${short}_Dist`], dateNum: +updated[`${short}_Doses`], //population: +population[long], population: totalUpdated === "" ? +totalStat : +totalUpdated, population16: +canadaPopulation[long]["16 years and over"], population12: twelveUpdated === "" ? +twelveStat : +twelveUpdated, population5: fiveUpdated === "" ? +fiveStat : +fiveUpdated, data: [], }; Object.setPrototypeOf(newObj, protoProv); data.push(newObj); } } else { /**/ } }); const canada = data.find((obj) => obj.region === "Canada"); canada.population = data.filter((d) => d.region !== "Canada").reduce((memo, curr) => memo + curr.population, 0); canada.population12 = data.filter((d) => d.region !== "Canada").reduce((memo, curr) => memo + curr.population12, 0); canada.population5 = data.filter((d) => d.region !== "Canada").reduce((memo, curr) => memo + curr.population5, 0); data.forEach((region) => { region.data = filtered .filter((row) => !["Updated", "Total"].includes(row.Date)) .map((row, i, arr) => { const short = region.regionShort; const newObj = { regionShort: short, dateNum: +row[`Date`], total: row[`${short}_Doses`] !== "" ? +row[`${short}_Doses`] : null, second: row[`${short}_2nd`] !== "" ? +row[`${short}_2nd`] : null, third: row[`${short}_3rd`] !== "" ? +row[`${short}_3rd`] : null, distributed: row[`${short}_Dist`] !== "" ? +row[`${short}_Dist`] : null, population: region.population, population16: region.population16, population12: region.population12, population5: region.population5, }; Object.setPrototypeOf(newObj, protoProv); return newObj; }); }); // DAILY + AVG + FILL IN BLANKS data.forEach((region) => { region.data.forEach((day, i, arr) => { day.added = []; Object.keys(day).forEach((key) => { if (day[key] === null) { day[key] = i === 0 ? 0 : arr[i - 1][key]; day.added.push(key); } }); }); region.data.max = function (metric) { return d3.max(this, (d) => +d[metric]); }; }); data.max = function (metric) { return d3.max(this, (d) => +d.data.max(metric)); }; // TABLES ///////////////////// // // TOP const topTableContainer = d3.select(".top-table-canada"); const topTableContainer2 = d3.select(".top-table-canada-2"); const bottomTableContainer = d3.select(".bottom-table-canada"); 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 totalAndNewPct = (d, i, arr) => { const num = d.toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 }); const difference = arr[arr.length - 1] - arr[arr.length - 2]; const diff = difference.toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 }); return `${num}%
${difference >= 0 ? "+" : ""}${diff}%
`; }; const topOptions = [ topTableContainer, [data.find((row) => row.region === "Canada")], [ { head: "Total
Percentage of population vaccinated", body: "percent", width: "10%", fn: totalAndNewPct, }, { head: "Total
Percentage of population fully vaccinated", body: "percent2nd", width: "10%", fn: totalAndNewPct, }, /* <> { head: "Eligible (12+)
Percentage of population vaccinated ", body: "percent12", width: "10%", fn: totalAndNewPct, }, { head: "Eligible (12+)
Percentage of population fully vaccinated ", body: "percent2nd12", width: "10%", fn: totalAndNewPct, }, */ { head: "Eligible (5+)
Percentage of population vaccinated ", body: "percent5", width: "10%", fn: totalAndNewPct, }, { head: "Eligible (5+)
Percentage of population fully vaccinated ", body: "percent2nd5", width: "10%", fn: totalAndNewPct, }, ], "vaccine-table top", "Canada", "Canada", ]; const topOptions2 = [ topTableContainer2, [data.find((row) => row.region === "Canada")], [ { head: "Total doses administered", body: "total", width: "10%", fn: totalAndNew, }, { head: "First doses", body: "first", width: "10%", fn: totalAndNew, }, { head: "Second doses", body: "second", width: "10%", fn: totalAndNew, }, { head: "Third+ doses", body: "third", width: "10%", fn: totalAndNew, }, // { // head: "Received from manufacturer", // body: "distributed", // width: "10%", // fn: totalAndNew, // }, { head: "Received doses administered", body: "distPercent", width: "10%", fn: totalAndNewPct, }, ], "vaccine-table top", "Canada", "Canada", ]; const botOptions = [ bottomTableContainer, data, [ { head: "Province/Territory", body: "region", width: "35%", fn: (d) => d }, // { // head: "% of population vaccinated (at least one dose)", // span: 2, // body: "percent", // width: "15%", // fn: (d) => `
`, // }, { head: "Total
Percentage vaccinated", span: 1, body: "percent", width: "15%", fn: (d) => d.toLocaleString(undefined, { maximumFractionDigits: 1, minimumFractionDigits: 1 }) + "%", }, { head: "Total
% Fully vaccinated", span: 1, body: "percent2nd", width: "15%", fn: (d) => d.toLocaleString(undefined, { maximumFractionDigits: 1, minimumFractionDigits: 1 }) + "%", }, /*<> { head: "Eligible (12+)
Percentage vaccinated", body: "percent12", width: "15%", fn: (d) => d.toLocaleString(undefined, { maximumFractionDigits: 1, minimumFractionDigits: 1 }) + "%", }, { head: "Eligible (12+)
% Fully vaccinated", body: "percent2nd12", width: "15%", fn: (d) => d.toLocaleString(undefined, { maximumFractionDigits: 1, minimumFractionDigits: 1 }) + "%", }, */ { head: "Eligible (5+)
Percentage vaccinated", body: "percent5", width: "15%", fn: (d) => d.toLocaleString(undefined, { maximumFractionDigits: 1, minimumFractionDigits: 1 }) + "%", }, { head: "Eligible (5+)
% Fully vaccinated", body: "percent2nd5", width: "15%", fn: (d) => d.toLocaleString(undefined, { maximumFractionDigits: 1, minimumFractionDigits: 1 }) + "%", }, // { // head: "First doses", // body: "first", // width: "12%", // fn: (d) => d.toLocaleString(), // }, // { // head: "Second doses", // body: "second", // width: "12%", // fn: (d) => d.toLocaleString(), // }, /* { head: "Received from manufacturer", body: "distributed", width: "10%", fn: (d) => d.toLocaleString(), }, */ { head: "Third+ doses", body: "third", width: "10%", fn: (d) => d.toLocaleString(), }, { head: "Received doses administered", body: "distPercent", width: "8%", fn: (d) => d.toLocaleString(undefined, { maximumFractionDigits: 1, minimumFractionDigits: 1 }) + "%", }, { head: "Updated", body: "date", width: "6%", fn: (d) => `${datePadNumToName(d)}`, }, ], "vaccine-table bottom", "Canada", "Canada", ]; const chartSVG = d3.select(".chart-canada").append("svg").attr("class", "svg-chart"); const canadaChart = new Chart(chartSVG, data, "Canada", "percent5"); canadaChart.create(); window.addEventListener("resize", () => { canadaChart.update(); }); const canadaTableTop = new Table(...topOptions, null, null); canadaTableTop.update(); const canadaTableTop2 = new Table(...topOptions2, null, null).update(); const canadaTableBottom = new Table(...botOptions, "percent5", canadaChart); canadaTableBottom.sort(); canadaTableBottom.update(); // // MAP /////////////////////////// d3.json("https://beta.ctvnews.ca/content/dam/common/exceltojson/canada_provinces.txt").then((json) => { d3.select(".map-canada").selectAll(".loading").remove(); json.features.forEach((feature) => { const province = data.find((prov) => prov.region === feature.properties.name); feature.datum = province; }); const container = d3.select(".map-canada"); const width = 750; const height = 400; const projection = d3 .geoConicConformal() .rotate([103, 0]) .fitExtent( [ [0, -height / 1.3], [width, height * 1], ], json ); const geoGenerator = d3.geoPath().projection(projection); const mapSVG = container .append("svg") .attr("class", "map-svg") .attr("width", "100%") .attr("viewBox", `0 0 ${width} ${height}`); mapSVG .append("rect") .attr("width", "100%") .attr("height", "100%") .attr("fill", "#ffffff") .on("click", () => provinceClick({ datum: { region: "Canada" } })); const mapLayer = mapSVG.append("g").attr("class", "map-layer"); const circleLayer = mapSVG.append("g").attr("class", "circle-layer"); const lineLayer = mapSVG.append("g").attr("class", "line-layer"); const textLayer = mapSVG.append("g").attr("class", "text-layer"); const backButton = mapSVG .append("g") .attr("class", "back-button") .attr("transform", `translate(${width / 2}, ${height - 8})`) .attr("display", "none"); backButton.on("click", () => provinceClick({ datum: { region: "Canada" } })); const backRect = backButton .append("rect") .attr("width", 140) .attr("height", 26) .attr("x", -70) .attr("y", -18) .attr("fill", "#fafafa"); const backText = backButton .append("text") .attr("class", "back-text bold") .attr("text-anchor", "middle") .attr("cursor", "pointer") .attr("font-size", 12) .attr("fill", "#222") .text("< Back to Canada"); const provinceClick = (d) => { canadaChart.region = d.datum.region; canadaChart.update(); canadaTableBottom.region = d.datum.region; canadaTableBottom.update(); if (d.datum.region === "Canada") { backButton.attr("display", "none"); } else { backButton.attr("display", "block"); } }; // PROVINCES SHAPES //console.log(json.features) const m = mapLayer.selectAll("path").data(json.features); m.enter() .append("path") .attr("class", (d) => `province ${d.datum.region.split(" ").join("-")}`) //.attr("id", (d) => `${d.properties.PRENAME.split(" ").join("-")}-path`) .attr("d", (d) => geoGenerator(d)) .attr("fill", (d) => colorScale(d.datum.percent)) .attr("stroke", "#fff") .attr("stroke-width", 1) .on("click", (e, d) => provinceClick(d)); // Adjustment factors for centering text compared to province centroids const adjust = { "British Columbia": { x: 0, y: 0 }, Alberta: { x: 0, y: 0 }, Saskatchewan: { x: 0, y: -10 }, Manitoba: { x: 0, y: 0 }, Ontario: { x: 0, y: -8 }, Quebec: { x: 0, y: 0 }, "New Brunswick": { x: -2, y: 42 }, "Nova Scotia": { x: 6, y: 52 }, "Prince Edward Island": { x: 1, y: -22 }, "Newfoundland and Labrador": { x: -10, y: -10 }, Yukon: { x: 0, y: 23 }, "Northwest Territories": { x: 23, y: 63 }, Nunavut: { x: -40, y: 149 }, }; // CIRCLES const c = circleLayer.selectAll("circle").data(json.features); c.enter() .append("circle") .attr("class", (d) => `map-circle ${d.datum.region.split(" ").join("-")}`) .attr("cx", (d) => geoGenerator.centroid(d)[0] + adjust[d.properties.name].x - 1) .attr("cy", (d) => geoGenerator.centroid(d)[1] + adjust[d.properties.name].y) .attr("r", (d) => (Math.round(d.datum.percent) >= 10 ? 25 : 21)) //.attr("r", (d) => 5 + 3 * Math.max(2, d.datum.first.toString().length)) .on("click", (e, d) => provinceClick(d)); // NUMBERS const t = textLayer.selectAll("text").data(json.features); t.enter() .append("text") .attr("class", (d) => `map-text ${d.datum.region.split(" ").join("-")}`) .attr("x", (d) => geoGenerator.centroid(d)[0] + adjust[d.properties.name].x) .attr("y", (d) => geoGenerator.centroid(d)[1] + adjust[d.properties.name].y + 6) .html( (d) => d.datum.percent5.toLocaleString(undefined, { maximumFractionDigits: 1, minimumFractionDigits: 1 }) + `%` ) //.text(d.datum.first.toLocaleString()) .on("click", (e, d) => provinceClick(d)); // LINES const lines = [ { x1: 652.5, y1: 281, x2: 652.5, y2: 296 }, { x1: 662.5, y1: 313, x2: 662.5, y2: 356 }, { x1: 623.5, y1: 320, x2: 623.5, y2: 337 }, ]; lines.forEach((line) => { lineLayer .append("line") .attr("class", "map-line") .attr("x1", line.x1) .attr("y1", line.y1) .attr("x2", line.x2) .attr("y2", line.y2); }); // LEGEND buildLegend(mapSVG, colorScale, 16, 3, 5, maxScalePercent); }); }); }