const container = d3.select(".ctv-widget.hospitalization-map"); const url = "https://beta.ctvnews.ca/content/dam/common/exceltojson/Canada-Hospitalization.txt"; const mapUrl = "https://www.ctvnews.ca/cmlink/7.763433"; d3.json(url).then((raw) => { d3.json(mapUrl).then((canadaMap) => { createHospitalization(raw, canadaMap); }); }); const createHospitalization = (raw, geo) => { const data = formatData(raw); const notes = { "British Columbia": `B.C. provides daily updates of current hospitalizations and ICU admissions. Hospitalizations and ICU admissions based on vaccine status are updated weekly in their regional surveillance dashboard.`, Alberta: `Alberta provides daily updates of current hospitalizations and ICU admissions. Under Alberta's Vaccine Outcomes, current hospitalizations are broken down by vaccine status, however ICU admissions based on vaccine status are reported based on the last 120 days.`, Saskatchewan: `Saskatchewan's hospitalizations graph reports on current and historical data of hospitalizations and ICU admissions. The ‘Highlights' section provides a brief report of these hospitalizations based on vaccine status.`, Manitoba: `Manitoba's COVID-19 dashboard provides daily updates of current hospitalizations and ICU admissions. On the same page, hospitalizations and ICU admissions are separated by vaccine status.`, Ontario: `Ontario's hospitalization data reports on patients currently testing positive for COVID-19. ICU admissions reported in the province include patients testing positive and negative for COVID-19. Data on hospitalizations/ICU data based on vaccine status can be found on the same page.`, Quebec: `Quebec provides daily hospitalization and ICU admission updates found in their situation dashboard or INSPQ dashboard. On the same dashboard under “Nouvelles hospitalisations” cases are broken down by vaccine status. `, "New Brunswick": `New Brunswick's daily news release provides updates on current hospitalizations and ICU admissions.`, "Nova Scotia": `Nova Scotia's daily news release provides updates on current hospitalizations and ICU admissions, as well as cases based on vaccine status. `, "Prince Edward Island": `Prince Edward Island's current hospitalizations and ICU admissions can be found in their COVID-19 dashboard. Historical data not currently available for Prince Edward Island.`, "Newfoundland and Labrador": `Newfoundland and Labrador's daily news release provides updates on current hospitalizations and ICU admissions. A breakdown of cases based on vaccine status can be found in their COVID-19 dashboard.`, Nunavut: `Data not currently available for Nunavut.`, Canada: "Click map to view historical data and notes for each province.", }; data.forEach((region) => (region.note = notes[region.region])); geo.data = data.find((d) => d.region === "Canada"); geo.features.forEach((feature) => { feature.data = data.find((d) => feature.properties.PRENAME === d.region); }); const map = new GeoMap(geo, container, { projectionRotate: [100, 0], projectionParallels: [49, 77], title: "Current COVID-19 hospitalizations in Canada", }); const filterData = (region, date = "03-01-2020") => data .find((d) => d.region === region) .data.map((d) => { return { date: d.date, hosp: d.total.hosp, icu: d.total.icu }; }) .filter( (d, i, arr) => i >= arr.indexOf(arr.find((day) => day.date === date)) ); const defaultData = filterData("Canada"); const chart = new Chart( ".ctv-widget.hospitalization-chart", defaultData, "COVID-19 hospitalizations in Canada", { lines: ["hosp", "icu"], labels: ["Hospitalized", "ICU"], date: "date", }, { transition: 0, note: geo.data.note } ); const reset = () => { map.reset(); chart.reset(); map.layers.region.select("text").text("Canada"); }; const changeRegion = (e, d) => { const regionName = d.properties.PRENAME; chart.data = filterData(regionName); chart.titleText = `COVID-19 hospitalizations in ${regionName}`; chart.note = d.data.note; let regionText = "Canada"; if (regionName !== "Canada") { regionText += ` > ${regionName}`; } map.layers.region.select("text").text(regionText).on("click", reset); chart.update(); }; map.customClick = changeRegion; map.layers.background.on("click", reset); const hospitalizationMax = d3.max( data.filter((d) => d.region !== "Canada"), (d) => d.hosp ); var colorScale = d3 .scaleLinear() .domain([0, 1, hospitalizationMax]) .range(["#eaeaea", "#ffdbab", "#fdbd69"]); map.paths.style("fill", (d) => { if (isNaN(d.data.hosp)) { return colorScale(0); } return colorScale(d.data.hosp); }); const numberLayer = map.addLayer("number"); const adjust = { "British Columbia": { x: 0, y: 0 }, Alberta: { x: -4, y: 0 }, Saskatchewan: { x: 0, y: -10 }, Manitoba: { x: 0, y: 0 }, Ontario: { x: 0, y: -8 }, Quebec: { x: 0, y: 10 }, "New Brunswick": { x: -3, y: 22 }, "Nova Scotia": { x: 5, y: 12 }, "Prince Edward Island": { x: -8, y: -2 }, "Newfoundland and Labrador": { x: 40, y: 20 }, Yukon: { x: -5, y: 10 }, "Northwest Territories": { x: -3, y: 20 }, Nunavut: { x: -21, y: 69 }, }; const numberContainer = numberLayer .selectAll("g") .data((d) => d.features) .join("g") .attr("transform", (d) => { const [cx, cy] = map.path.centroid(d); const { x, y } = adjust[d.properties.PRENAME]; return `translate(${cx + x},${cy + y})`; }) .on("click", changeRegion); const circleDefaults = { radius: 22, fontSize: 16, fontShift: 6, }; numberContainer .selectAll("circle") .data((d) => [d]) .join("circle") .attr("r", circleDefaults.radius); numberContainer .selectAll("text") .data((d) => [d]) .join("text") .text((d) => { if (isNaN(d.data.hosp)) { return "—"; } return d.data.hosp.toLocaleString(); }) .attr("y", circleDefaults.fontShift); map.customZoom = (e) => { map.layers.number .selectAll("circle") .attr("r", circleDefaults.radius / e.transform.k); map.layers.number .selectAll("text") .attr("font-size", circleDefaults.fontSize / e.transform.k) .attr("y", circleDefaults.fontShift / e.transform.k); }; const regionLayer = map.addLayer("region", false); regionLayer .selectAll("text") .data((d) => [d]) .join("text") .text((d) => d.properties.PRENAME) .attr("y", 30) .attr("x", 15); }; const formatData = (raw) => { const exceptionArray = ["SK", "QC"]; //Provinces that exclude ICU numbers from total 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 dataNameKey = { hosp: "Hospitalized", icu: "ICU", }; const dataKeys = Object.keys(dataNameKey); const total = filtered.find((row) => row.Date === "Total"); const updated = filtered.find((row) => row.Date === "Updated"); const allRegionData = []; Object.keys(total).forEach((key) => { const short = key.split("_")[0]; const long = provinceNameKey[short]; const region = allRegionData.find((d) => d.region === long); if (!region) { if (long) { const newObj = { region: long, regionShort: short, data: [], }; allRegionData.push(newObj); } } else { /**/ } }); allRegionData.forEach((region) => { region.data = filtered .filter((row) => !["Updated", "Total"].includes(row.Date)) .map((row) => { const short = region.regionShort; const newObj = { dateNum: +row[`Date`], get date() { return dateNumToString(this.dateNum, true, true); }, total: { dateNum: +row[`Date`], get date() { return dateNumToString(this.dateNum, true, true); }, }, }; dataKeys.forEach((key) => (newObj.total[key] = row[`${short}_${key}`])); return newObj; }); }); //Fill in blanks allRegionData.forEach((region) => { region.data.forEach((day, i, arr) => { dataKeys.forEach((key) => { if (day.total[key] === "") { day.total[key] = i === 0 ? undefined : +arr[i - 1].total[key]; } else { day.total[key] = +day.total[key]; } }); }); }); allRegionData.forEach((region) => { if (exceptionArray.includes(region.regionShort)) { region.data.forEach((d) => { d.total.hosp += d.total.icu; }); } }); allRegionData.forEach((region) => { region.data.forEach((day, i, arr) => { day.new = { date: day[`date`], }; dataKeys.forEach( (d) => (day.new[d] = i === 0 ? 0 : arr[i].total[d] - arr[i - 1].total[d]) ); }); }); allRegionData.forEach((region) => { region.data.forEach((day, i, arr) => { day.avg = { date: day[`date`], }; const avg = (metric, days = 7) => (arr[i].total[metric] - arr[i - (days - 1)].total[metric]) / days; dataKeys.forEach((d) => (day.avg[d] = i < 6 ? 0 : avg(d))); }); dataKeys.forEach( (d) => (region[d] = region.data[region.data.length - 1].total[d]) ); }); return allRegionData; }; 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}`; } };