class Chart { constructor(selector, raw, titleText, lines, userOptions) { this.root = d3.select(selector); this.data = raw; this.titleText = titleText; this.lines = lines.lines; this.labels = lines.labels; this.date = lines.date; this.default = { data: raw, titleText: titleText, note: userOptions.note, }; const defaultOptions = { transition: 750, }; this.options = { ...defaultOptions, ...userOptions }; this.note = this.default.note; this.clearContainer(); this.title = this.root.append("h3"); this.svg = this.root.append("svg"); const layers = ["axis", "legend", "line", "date", "mouseover"]; this.layers = {}; layers.forEach( (d) => (this.layers[d] = this.svg.append("g").attr("class", `${d} layer`)) ); this.tooltip = this.root.append("div").attr("class", "tooltip"); this.update(); window.addEventListener("resize", () => this.update()); } clearContainer() { this.root.html(""); } formatCaseData = (raw) => { return raw .filter((d) => d.Year !== "") .map((d) => { return { Year: d.Year, Rate: +d.Rate }; }); }; getAxis = (max = 0, num) => { const round = Math.ceil(max); const major = Math.pow(10, String(round).length - 1); const up = Math.ceil(round / major) * major; const limit = Math.max(up % 2 === 0 ? up : up + 1, 4); const axis = []; for (let i = 0; i <= limit; i += limit / num) { axis.push(i); } return axis; }; dateNumToString(dateNum, showYear) { const [month, day, year] = dateNum.split("-").map((d) => +d); const monthArray = [ "Jan.", "Feb.", "Mar.", "Apr.", "May", "Jun.", "Jul.", "Aug.", "Sep.", "Oct.", "Nov.", "Dec.", ]; const monthString = monthArray[month - 1]; const dayString = day; const yearString = year; const dateString = `${monthString} ${dayString}${ showYear ? ", " + yearString : "" }`; return dateString; } update() { const { width, height } = this.svg.node().getBoundingClientRect(); const pad = { top: 10, bottom: 30, left: 40, right: 50, }; this.title.text(this.titleText); const axisArray = this.getAxis( d3.max(this.data, (d) => d[this.lines[0]]), 4 ); const xScale = d3 .scaleLinear() .domain([0, this.data.length - 1]) .range([pad.left, width - pad.right]); const yScale = d3 .scaleLinear() .domain([0, Math.max(1, d3.max(axisArray))]) .range([height - pad.bottom, pad.top]); // Axis const axis = this.layers.axis .selectAll("g") .data(axisArray) .join("g") .attr( "transform", (d) => `translate(${width - pad.right}, ${yScale(d) + 1})` ); axis .selectAll("text") .data((d) => [d]) .join("text") .text((d) => d.toLocaleString()) .attr("y", 2.5) .attr("x", 8); axis .selectAll("line") .data((d) => [d]) .join("line") .attr("x1", 0) .attr("x2", -width + pad.left + pad.right) .attr("y1", 0) .attr("y2", 0) .classed("zero", (d) => d === 0); //Lines const t = this.layers.line.transition().duration(this.options.transition); this.lines.forEach((line) => { const path = d3 .line() .x((_, i) => xScale(i)) .y((d) => yScale(d[line])) .defined((d) => !isNaN(d[line])); this.layers.line .selectAll(`path.${line}`) .data([this.data]) .join( (enter) => enter.append("path").attr("d", path).attr("class", line), (update) => update.call((update) => update.transition(t).attr("d", path)) ); }); //Mouseover this.layers.mouseover .selectAll("rect") .data(this.data) .join("rect") .attr("x", (_, i) => xScale(i - 0.5)) .attr("y", (d) => 0) .attr("width", (d) => xScale(1) - xScale(0)) .attr("height", (d) => height) .on("mouseover", (e, d) => { e.target.classList.add("hover"); this.tooltip.classed("on", true); this.tooltip.style("left", e.offsetX + "px"); this.tooltip.style("top", e.offsetY + "px"); /*tooltip info*/ }) .on("mouseout", (e, d) => { e.target.classList.remove("hover"); this.tooltip.classed("on", false); }); //Dates const dateShowNum = Math.floor(this.data.length / 4); const dates = this.layers.date .selectAll("g") .data(this.data) .join("g") .attr("transform", (d, i) => `translate(${xScale(i)},${yScale(0)})`) .style("display", (d, i) => (i % dateShowNum === 0 ? "block" : "none")); let curYear = ""; dates .selectAll("text") .data((d) => [d]) .join("text") .attr("y", 15) .text((d) => { const year = d[this.date].split("-")[2]; const showYear = year !== curYear; if (showYear) { curYear = year; } return this.dateNumToString(d[this.date], showYear); }); dates .selectAll("line") .data((d) => [d]) .join("line") .attr("x1", 0) .attr("x2", 0) .attr("y1", 0) .attr("y2", 5); //Legend const legend = this.layers.legend .selectAll("g") .data(this.lines) .join("g") .attr("transform", (_, i) => `translate(${0},${20 + 16 * i})`); legend .selectAll("path") .data((d) => [d]) .join("path") .attr("class", (d) => d) .attr("d", "M0 0 H 18"); legend .selectAll("text") .data((d) => [d]) .join("text") .text((d) => this.labels[this.lines.findIndex((D) => D === d)]) .attr("x", 23) .attr("y", 4); this.root .selectAll("div.notes") .data([this.note]) .join("div") .html((d) => d) .attr("class", "notes"); } reset() { this.data = this.default.data; this.titleText = this.default.titleText; this.note = this.default.note; this.update(); } }