class App { constructor(selector, url) { this.selector = selector; this.url = url; this.data = null; this.charts = []; } async getData() { await d3.csv(this.url).then((data) => { this.data = this.formatData(data); }); } formatData(raw) { const data = raw.map((day) => { return { date: day.Date, hospital: +day.Hospitalized, icu: +day.ICU, ventilator: +day.Ventilator, }; }); console.log(data); return data; } async create() { if (!this.data) { await this.getData(); } Array.from(document.querySelectorAll(this.selector)).forEach((div) => { new Chart(d3.select(div), this.data).create().update(); }); } update() {} } class Chart { constructor(root, data, start) { this.start = start; this.parent = root; this.region = data.region; this.data = data; this.layers = {}; this.bounds = { top: 10, left: 10, right: 40, bottom: 30, }; } debounce = (func, wait) => { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }; create() { this.title = this.parent .append("h3") .text("Patients in hospital, ICU and on ventilators") .classed("bold", true); this.svg = this.parent .append("svg") .attr("width", "100%") .attr("height", 300); this.layers.background = this.svg.append("g").classed("bg", true); this.layers.axes = this.svg.append("g").classed("axes", true); this.layers.legend = this.svg.append("g").classed("legend", true); this.layers.lines = this.svg.append("g").classed("lines", true); this.layers.rects = this.svg.append("g").classed("rects", true); this.parent .append("a") .attr( "href", "https://health-infobase.canada.ca/covid-19/epidemiological-summary-covid-19-cases.html#a7" ) .attr("target", "_blank") .text("Source: Statistics Canada epidemiological summary"); const debounce = this.debounce(() => this.update(), 20); window.addEventListener("resize", debounce); return this; } update() { for (let layer in this.layers) { this.layers[layer].selectAll("*").remove(); } const { width, height } = this.svg.node().getBoundingClientRect(); const [min, exmax] = d3.extent(this.data.map((d) => d.hospital)); const max = Math.ceil(exmax / 1000) * 1000; const { top, left, right, bottom } = this.bounds; const data = this.data; const yScale = d3 .scaleLinear() .domain([Math.min(0, min), max]) .range([height - bottom, top]); const xScale = d3 .scaleLinear() .domain([0, data.length - 1]) .range([left, width - right]); const line = (metric) => d3 .line() .x((d, i) => xScale(i)) .y((d) => yScale(d[metric])); const months = ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"]; let current = { year: { num: null, start: 0, end: 0, }, month: { num: null, start: 0, end: 0, }, }; this.data.forEach((d, i, arr) => { const [year, month, day] = d.date.split("-"); if (month !== current.month.num || i === arr.length - 1) { if (current.month.num) { current.month.end = i; this.layers.background .append("rect") .attr("x", xScale(current.month.start)) .attr("y", yScale(max)) .attr( "width", xScale(current.month.end) - xScale(current.month.start) ) .attr("height", yScale(Math.min(0, min)) - yScale(max)) .style( "fill", parseInt(current.month.num) % 2 === 1 ? "#fafafa" : "#fff" ); this.layers.axes .append("text") .text( parseInt(month) === 1 ? months[11] : i === arr.length - 1 ? months[parseInt(month) - 1] : months[parseInt(month) - 2] ) .attr( "x", xScale(current.month.end) - (xScale(current.month.end) - xScale(current.month.start)) / 2 ) .attr("y", yScale(Math.min(0, min)) + 12) .attr("text-anchor", "middle") .attr("class", "axis-label month"); } current.month.num = month; current.month.start = i; } if (year !== current.year.num || i === arr.length - 1) { if (current.year.num) { current.year.end = i - 1; this.layers.axes .append("rect") .attr("x", xScale(current.year.end + 1)) .attr("y", yScale(max)) .attr("width", 1) .attr("height", height) .style("fill", "#eee"); this.layers.axes .append("text") .text(i === arr.length - 1 ? parseInt(year) : parseInt(year) - 1) .attr( "x", xScale(current.year.end) - (xScale(current.year.end) - xScale(current.year.start)) / 2 ) .attr("y", yScale(Math.min(0, min)) + 28) .attr("text-anchor", "middle") .attr("class", "axis-label month"); } current.year.num = year; current.year.start = i; } }); const N = 4; const grid = Array.from(Array(N + 1)).map((d, i) => (i * max) / N); grid.forEach((d) => { this.layers.axes .append("line") .attr("x1", xScale(0)) .attr("x2", xScale(data.length - 1)) .attr("y1", yScale(d)) .attr("y2", yScale(d)) .attr("class", `grid ${d === 0 ? "zero" : ""}`); this.layers.axes .append("text") .attr("x", xScale(data.length - 1) + 8) .attr("y", yScale(d) + 4) .attr("class", "axis-label") .text(d.toLocaleString(undefined, { maximumFractionDigits: 2 })); }); this.layers.lines .append("path") .datum(data) .attr("d", line("hospital")) .classed("hospital", true); this.layers.lines .append("path") .datum(data) .attr("d", line("icu")) .classed("icu", true); this.layers.lines .append("path") .datum(data) .attr("d", line("ventilator")) .classed("ventilator", true); this.legend = {}; const names = ["Hospital", "ICU", "Ventilator"]; names.forEach((d, i) => { this.layers.legend .append("rect") .classed(d.toLowerCase(), true) .attr("x", 10) .attr("y", 20 + i * 20) .attr("width", 30) .attr("height", 4); this.legend[d] = this.layers.legend .append("text") .text(d) .attr("x", 45) .attr("y", 26 + i * 20) .style("font-size", 14); console.log(this.legend); }); const D = this.layers.rects.selectAll("rect").data(this.data); D.enter() .append("rect") .attr("x", (d, i) => xScale(i)) .attr("y", yScale(max)) .attr("width", (d, i) => xScale(i + 1) - xScale(i)) .attr("height", yScale(0)) .style("fill", "transparent") .on("mouseover", (e, d) => { e.target.style.fill = "rgba(0, 0, 0, 0.45)"; const names = ["Hospital", "ICU", "Ventilator"]; names.forEach((word) => { this.legend[word].text( `${word} (${d[word.toLowerCase()].toLocaleString()} on ${d.date})` ); }); }) .on("mouseout", (e, d) => { e.target.style.fill = "rgba(0, 0, 0, 0)"; }); } } const url = "https://beta.ctvnews.ca/content/dam/common/exceltojson/covid-hospital-icu-ventilator.txt"; window.onload = () => new App(".hospital-chart", url).create();