class ExcessMortalityApp { constructor(selector) { this.selector = selector; this.data = null; this.loaded = false; } async fetch(url) { await d3.csv(url).then((data) => { console.log(data); this.data = this.format(data); }); this.loaded = true; } format(raw) { class region { constructor(region) { this.region = region; this.adjusted = []; this.expected = []; this.excess = []; } } const data = {}; const mapped = raw .map((row) => { return { region: row.GEO.split(",")[0], metric: row.Characteristics.split(" ")[0].toLowerCase(), date: row.REF_DATE, value: row.VALUE === "" ? "" : Number(row.VALUE), }; }) .filter((row) => row.date !== ""); mapped.forEach((row) => { if (!data[row.region]) { data[row.region] = new region(row.region); } if (data[row.region][row.metric]) { data[row.region][row.metric].push({ date: row.date, value: row.value, }); } }); return data; } async build(url) { const containers = Array.from(document.querySelectorAll(this.selector)); if (containers.length === 0) return; await this.fetch(url); d3.select(this.selector).html(""); for (let region in this.data) { new ExcessMortalityChart( d3.select(this.selector), this.data[region] ).build(); } } } class ExcessMortalityChart { constructor(parent, regionObj) { this.parent = parent; this.data = regionObj; } build() { const chart = this.parent .append("div") .attr("class", "excess-mortality-container") .append("div") .attr("class", "excess-mortality-chart"); chart.append("h3").text(this.data.region); chart.append("p").text("Excess mortality in 2020 (weekly)"); const svg = chart.append("svg"); const [min, max] = d3.extent(this.data.excess, (d) => d.value); const abs = Math.max(Math.abs(min), Math.abs(max)); const absDig = 10 ** (abs.toString().length - 1); const roundAbs = Math.max(1, Math.ceil(abs / absDig) * absDig); const yScale = d3 .scaleLinear() .domain([-roundAbs, roundAbs]) .range([130, 10]); const xScale = d3 .scaleLinear() .domain([0, this.data.excess.length - 1]) .range([20, 250]); const line = d3 .line() .x((d, i) => xScale(i)) .y((d) => yScale(d.value)) .defined((d) => d.value !== ""); const undef = d3 .line() .x((d, i) => xScale(i)) .y((d) => yScale(d.value)); const array = this.data.excess; const months = ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"]; const monthsAdded = []; for (let i = 0; i < array.length; i++) { const monthNum = Number(array[i].date.split("-")[1]); if (monthNum % 2 === 1) { svg .append("rect") .attr("x", xScale(i)) .attr("y", yScale(roundAbs)) .attr("width", xScale(1) - xScale(0)) .attr("height", yScale(-roundAbs) - yScale(roundAbs)) .style("fill", "rgba(0,0,0,0.04)"); } if (!monthsAdded.includes(monthNum)) { monthsAdded.push(monthNum); svg .append("text") .text(months[monthNum - 1]) .attr("x", xScale(i) + 5) .attr("y", yScale(-roundAbs) + 12) .attr("class", "axis-label month"); } } [-roundAbs, -roundAbs / 2, 0, roundAbs / 2, roundAbs].forEach((d) => { svg .append("line") .attr("x1", xScale(0)) .attr("x2", xScale(this.data.excess.length - 1)) .attr("y1", yScale(d)) .attr("y2", yScale(d)) .attr("class", `grid ${d === 0 ? "zero" : ""}`); svg .append("text") .attr("x", xScale(this.data.excess.length - 1) + 8) .attr("y", yScale(d) + 4) .attr("class", "axis-label") .text(d); }); // Excess death lines svg .append("path") .datum(this.data.excess) .attr("d", undef) .attr("class", "undef"); svg.append("path").datum(this.data.excess).attr("d", line); } } window.onload = () => { const selector = ".excess-mortality"; const url = "https://beta.ctvnews.ca/content/dam/common/exceltojson/excess-mortality-march-2021.txt"; new ExcessMortalityApp(selector).build(url); };