const container = d3.select(".ctv-widget.hospitalization-map"); const url = "https://beta.ctvnews.ca/content/dam/common/exceltojson/Canada-Hospitalization.txt"; const mapUrl = "/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 . Hospitalizations and ICU admissions based on vaccine status are updated weekly in their .`, Alberta: `Alberta provides daily updates of . Under Alberta's , 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 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 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 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 or . On the same dashboard under “Nouvelles hospitalisations” cases are broken down by vaccine status. `, "New Brunswick": `New Brunswick's provides updates on current hospitalizations and ICU admissions.`, "Nova Scotia": `Nova Scotia's 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 . Historical data not currently available for Prince Edward Island.`, "Newfoundland and Labrador": `Newfoundland and Labrador's provides updates on current hospitalizations and ICU admissions. A breakdown of cases based on vaccine status can be found in their . `, Yukon: `Data not currently available for Yukon.`, "Northwest Territories": `Current hospitalizations are not available for Northwest Territories. Total hospitalizations and ICU admissions can be found on 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}`; } };