function datePadNumToName(string, showYear = false) { const monthArray = ["Jan.", "Feb.", "Mar.", "Apr.", "May.", "Jun.", "Jul.", "Aug.", "Sep.", "Oct.", "Nov.", "Dec."]; const split = string.split("-"); if (split.length < 2) { return ""; } else if (split.length === 2) { const month = +split[0] - 1; const day = +split[1]; return `${monthArray[month]} ${day}`; } else if (split.length === 3) { const year = +split[0]; const month = +split[1] - 1; const day = +split[2]; return showYear ? `${monthArray[month]} ${day}, ${year}` : `${monthArray[month]} ${day}`; } } function roundup(v) { const digits = (v + "").length; const n = 10 ** (digits - 1); return Math.ceil(v / n) * n; } class Chart { constructor(container, dataset, region, metric) { this.container = container; this.dataset = dataset; this.region = region; this.metric = metric; } get datum() { return this.dataset.find((d) => d.region === this.region); } create() { this.container.selectAll("*").remove(); const gridLayer = this.container.append("g").attr("class", "grid-layer"); const barLayer = this.container.append("g").attr("class", "bar-layer"); const titleLayer = this.container.append("g").attr("class", "title-layer").attr("transform", "translate(10, 20)"); const axisLayer = this.container.append("g").attr("class", "axis-layer"); const yAxis = axisLayer.append("g").attr("class", "y-axis"); const xAxis = axisLayer.append("g").attr("class", "x-axis"); const hoverLayer = this.container.append("g").attr("class", "hover-layer"); const tooltipLayer = this.container.append("g").attr("class", "tooltip-layer"); this.update(); } update() { const gridLayer = this.container.select(".grid-layer"); const barLayer = this.container.select(".bar-layer"); const titleLayer = this.container.select(".title-layer"); const axisLayer = this.container.select(".axis-layer"); const yAxis = axisLayer.select(".y-axis"); const xAxis = axisLayer.select(".x-axis"); const hoverLayer = this.container.select(".hover-layer"); const tooltipLayer = this.container.select(".tooltip-layer"); // SETUP const width = this.container.node().getBoundingClientRect().width; const height = this.container.node().getBoundingClientRect().height; const pad = { top: 70, right: 70, bottom: 20, left: 10, }; let yAxisSuffix; const percentArray = [ "percent_full", "percent_first", "distPercent", "percent", "percent12", "percent5", "percent2nd", "percent2nd12", "percent2nd5", ]; const yMin = 0; let yMax; if (percentArray.includes(this.metric)) { yMax = 100; yAxisSuffix = "%"; } else { //yMax = roundup(Math.max(1, Math.round(this.dataset.max(this.metric)))); //console.log(this.datum); yMax = roundup(Math.max(1, Math.ceil(this.datum[this.metric]))); yAxisSuffix = ""; } const xMin = 0; const xMax = this.datum.data.length; const yScale = d3 .scaleLinear() .domain([yMin, yMax]) .range([height - pad.bottom, pad.top]); const xScale = d3 .scaleLinear() .domain([xMin, xMax]) .range([pad.left, width - pad.right]); // TITLE const T = titleLayer.selectAll(".title").data([this.datum.region]); T.exit().remove(); T.text((d) => d); T.enter() .append("text") .attr("class", "title bold") .text((d) => d); const S = titleLayer.selectAll(".subtitle").data([this.metric]); const subtext = { total: "Total doses administered", first: "First doses administered", second: "Second doses administered", percent: "Percentage of total population vaccinated (at least one dose)", percent16: "Percentage of 16+ population vaccinated (at least one dose)", percent12: "Percentage of 12+ population vaccinated (at least one dose)", percent5: "Percentage of 5+ population vaccinated (at least one dose)", percent2nd: "Percentage of total population fully vaccinated", percent2nd16: "Percentage of 16+ population fully vaccinated", percent2nd12: "Percentage of 12+ population fully vaccinated", percent2nd5: "Percentage of 5+ population fully vaccinated", per_hundred: "Total doses per 100 people", percent_first: "Percentage of population vaccinated (at least one dose)", percent_full: "Percentage of population fully vaccinated", distPercent: "Percentage of vaccine stock administered", distributed: "Number of doses received from manufacturer", }; S.exit().remove(); S.text((d) => subtext[d]).attr("y", 21); S.enter() .append("text") .attr("class", "subtitle") .attr("y", 21) .text((d) => subtext[d]); // BARS const B = barLayer.selectAll("rect").data(this.datum.data, (d) => d.date); B.exit().attr("opacity", 1).transition().duration(200).attr("opacity", 0).remove(); B.transition() .duration(500) .delay(100) //.attr("class", (d) => { // if (d.added.length > 0) console.log(d); // return d.added.includes(this.metric) ? "added" : ""; //}) .attr("x", (d, i) => xScale(i)) .attr("width", xScale(1) - xScale(0)) .attr("y", (d) => yScale(d[this.metric])) .attr("height", (d) => height - yScale(d[this.metric]) - pad.bottom); B.enter() .append("rect") //.attr("class", (d) => { // if (d.added.length > 0) console.log(d); // return d.added.includes(this.metric) ? "added" : ""; //}) .attr("x", (d, i) => xScale(i)) .attr("y", (d) => yScale(0)) .attr("width", xScale(1) - xScale(0)) .attr("height", (d) => height - yScale(0) - pad.bottom) .transition() .duration(500) .delay(100) .attr("x", (d, i) => xScale(i)) .attr("y", (d) => yScale(d[this.metric])) .attr("width", xScale(1) - xScale(0)) .attr("height", (d) => height - yScale(d[this.metric]) - pad.bottom); // Hover const H = hoverLayer.selectAll("rect").data(this.datum.data, (d) => d.date); const tooltip = tooltipLayer.append("g").attr("class", "tooltip"); const tooltipBg = tooltip.append("rect"); const tooltipText = tooltip.append("g").attr("class", "tooltip-text"); const tooltipDate = tooltipText.append("text").attr("class", "date"); const tooltipMain = tooltipText.append("text").attr("class", "main"); H.exit().remove(); H.attr("x", (d, i) => xScale(i)) .attr("width", xScale(1) - xScale(0)) .attr("y", (d) => yScale(yMax)) .attr("height", (d) => height - yScale(yMax) - pad.bottom); H.enter() .append("rect") .attr("class", "hover-bar") .attr("x", (d, i) => xScale(i)) .attr("y", (d) => yScale(yMax)) .attr("width", xScale(1) - xScale(0)) .attr("height", (d) => height - yScale(yMax) - pad.bottom) .on("mousemove", (e, d) => { tooltip.attr("opacity", 1); const pad = 8; const mainText = Number(d[this.metric]).toLocaleString(undefined, { maximumFractionDigits: 2 }); tooltipMain.text(`${mainText}${percentArray.includes(this.metric) ? "%" : ""}`); tooltipDate .text(datePadNumToName(d.date)) .attr("transform", `translate(0, ${-tooltipMain.node().getBoundingClientRect().height})`); const tt = tooltipText.node().getBoundingClientRect(); tooltipBg.attr("width", tt.width + pad * 2).attr("height", tt.height + pad); const ttbg = tooltipBg.node().getBoundingClientRect(); tooltipText.attr("transform", `translate(0, ${-pad})`); tooltipBg.attr("transform", `translate(${-ttbg.width / 2}, ${-ttbg.height})`); const t = tooltip.node().getBoundingClientRect(); const dx = e.offsetX < t.width / 2 ? t.width / 2 : e.offsetX > width - t.width / 2 ? width - t.width / 2 : e.offsetX; const dy = e.offsetY; tooltip.attr("transform", `translate(${dx}, ${dy})`); }) .on("mouseout", () => tooltip.attr("opacity", 0)); // Axis :| const xAxisArray = []; const everyN = Math.ceil(((700 / width) * xMax) / 10); for (let i = 0; i < xMax; i += 1) { xAxisArray.push(this.datum.data[i].date); } const xA = xAxis.selectAll("g").data(xAxisArray); const xAg = xA.enter().append("g"); xA.exit().remove(); xA.select("text") .transition() .duration(500) .attr("class", (d, i) => `x-axis text ${i % everyN === 0 ? "on" : "off"}`) .text((d) => datePadNumToName(d)) .attr("y", height - pad.bottom + 14) .attr("x", (d, i) => xScale(i) + (xScale(1) - xScale(0)) / 2); xA.select("line") .transition() .duration(500) .attr("class", (d, i) => `x-axis tick ${i % everyN === 0 ? "on" : "off"}`) .attr("y1", height - pad.bottom) .attr("y2", height - pad.bottom + 3) .attr("x1", (d, i) => xScale(i) + (xScale(1) - xScale(0)) / 2) .attr("x2", (d, i) => xScale(i) + (xScale(1) - xScale(0)) / 2); xAg .append("text") .attr("class", (d, i) => `x-axis text ${i % everyN === 0 ? "on" : "off"}`) .text((d) => datePadNumToName(d)) .attr("y", height - pad.bottom + 14) .attr("x", (d, i) => xScale(i) + (xScale(1) - xScale(0)) / 2); xAg .append("line") .attr("class", (d, i) => `x-axis tick ${i % everyN === 0 ? "on" : "off"}`) .attr("y1", height - pad.bottom) .attr("y2", height - pad.bottom + 3) .attr("x1", (d, i) => xScale(i) + (xScale(1) - xScale(0)) / 2) .attr("x2", (d, i) => xScale(i) + (xScale(1) - xScale(0)) / 2); const yAxisArray = []; const steps = yMax > 1 ? 4 : 1; for (let i = yMin; i <= yMax; i += Math.max(yMax, 1) / steps) { yAxisArray.push(i); } const yA = yAxis.selectAll("g").data(yAxisArray); const yAg = yA.enter().append("g"); yA.exit().remove(); yA.select("text") .transition() .duration(500) .text((d) => d.toLocaleString(undefined, { maximumFractionDigits: 2 }) + yAxisSuffix) .attr("y", (d) => yScale(d) + 3) .attr("x", width - pad.right + 6); yA.select(".y-axis.tick") .transition() .duration(500) .attr("x1", width - pad.right) .attr("y1", (d) => yScale(d)) .attr("x2", width - pad.right + 4) .attr("y2", (d) => yScale(d)); yAg .append("line") .attr("class", "y-axis tick") .attr("x1", width - pad.right) .attr("y1", (d) => yScale(d)) .attr("x2", width - pad.right + 4) .attr("y2", (d) => yScale(d)); yAg .append("text") .text((d) => d.toLocaleString(undefined, { maximumFractionDigits: 2 }) + yAxisSuffix) .attr("class", "y-axis text") .attr("y", (d) => yScale(d) + 3) .attr("x", width - pad.right + 6); const yD = gridLayer.selectAll("line").data(yAxisArray); yD.exit().remove(); yD.transition() .duration(500) .attr("x1", pad.left) .attr("y1", (d) => yScale(d)) .attr("x2", width - pad.right) .attr("y2", (d) => yScale(d)); yD.enter() .append("line") .attr("class", "y-axis dash") .attr("x1", pad.left) .attr("y1", (d) => yScale(d)) .attr("x2", width - pad.right) .attr("y2", (d) => yScale(d)); const yAx = yAxis.selectAll(".y-axis.axis").data([1]); yAx .enter() .append("line") .attr("class", "y-axis axis") .attr("x1", width - pad.right) .attr("y1", yScale(yMax)) .attr("x2", width - pad.right) .attr("y2", yScale(0)); yAx .transition() .duration(500) .attr("class", "y-axis axis") .attr("x1", width - pad.right) .attr("y1", yScale(yMax)) .attr("x2", width - pad.right) .attr("y2", yScale(0)); const xAx = xAxis.selectAll(".x-axis.axis").data([1]); xAx .enter() .append("line") .attr("class", "x-axis axis") .attr("x1", pad.left) .attr("y1", yScale(0)) .attr("x2", width - pad.right) .attr("y2", yScale(0)); xAx .transition() .duration(500) .attr("class", "x-axis axis") .attr("x1", pad.left) .attr("y1", yScale(0)) .attr("x2", width - pad.right) .attr("y2", yScale(0)); } }