function appendOrSelect(parent, type, className) { return parent.selectAll(`.${className}`)._groups[0][0] ? parent.select(`.${className}`) : parent.append(type).attr("class", className); } function createChartLayers(parent) { let chartLayer = appendOrSelect(parent, "g", "chart-layer"); let axisLayer = appendOrSelect(chartLayer, "g", "axis-layer"); let axisX = appendOrSelect(axisLayer, "g", "axis-x"); let axisY = appendOrSelect(axisLayer, "g", "axis-y"); let titleLayer = appendOrSelect(chartLayer, "g", "title-layer"); let dataLayer = appendOrSelect(chartLayer, "g", "data-layer"); let hoverLayer = appendOrSelect(chartLayer, "g", "hover-layer"); } function createChartSettings(parent, dataArray, pad) { [topPad, rightPad, bottomPad, leftPad] = pad; let width = Number(parent.style("width").split("px")[0]); let height = Number(parent.style("height").split("px")[0]); let numDays = dataArray.length; let barWidth = (width - leftPad - rightPad) / numDays; return { width: width, height: height, numDays: numDays, barWidth: barWidth, pad: { top: topPad, right: rightPad, bottom: bottomPad, left: leftPad, }, }; } //Scale functions function getMaxSum(dataArray, metricArray) { let maxTotal = 0; dataArray.forEach((day) => { let dayTotal = 0; metricArray[2].forEach((metric) => { dayTotal += day[metricArray[0]][metricArray[1]][metric]; }); maxTotal = Math.max(dayTotal, maxTotal); }); return maxTotal; } function getYScale(chart, max) { return d3 .scaleLinear() .domain([0, Math.max(max * 1.05, 1)]) .range([chart.height - chart.pad.bottom, chart.pad.top]); } function getXScale(chart, data) { return d3 .scaleLinear() .domain([0, chart.numDays - 1]) .range([chart.pad.left, chart.width - chart.pad.right]); } function getTimeScale(chart, data) { let timeRange = d3.extent(data, (d) => d.date); let before = timeRange[0]; before.setHours(0, 0, 0); let after = timeRange[1]; after.setHours(0, 0, 0); // console.log(before, after); return d3 .scaleTime() .domain([before, after]) .range([chart.pad.left, chart.width - chart.pad.right]); } //Line chart functions function getLine(xScale, yScale, zero = false) { return d3 .line() .x((d, i) => xScale(i)) .y((d, i) => yScale(zero ? 0 : d[1])); } //Hover dot functions function createDots( fullData, layer, xScale, yScale, areaColorScale, lineColorScale, dotColorScale ) { fullData.forEach((stack, index) => { let D = layer.selectAll(`.circle-${index}`).data(stack.data); let Dplus = D.enter().append("circle"); Dplus.append(); Dplus.attr("class", `circle-${index}`) .attr("dot", (d, i) => i) .attr("cx", (d, i) => xScale(i)) .attr("cy", (d) => yScale(0)) .attr("r", 5) .attr("fill", dotColorScale(fullData[index].key)) .attr("stroke", lineColorScale(fullData[index].key)) .attr("stroke-width", 1) .attr("opacity", 0) .transition() .duration(1000) .attr("cy", (d) => yScale(d[1])); }); } function updateDots( fullData, layer, xScale, yScale, areaColorScale, lineColorScale, dotColorScale ) { layer.selectAll("*").remove(); fullData.forEach((stack, index) => { let D = layer.selectAll(`.circle-${index}`).data(stack.data); D.exit().remove(); D.attr("class", `circle-${index}`) .attr("cx", (d, i) => xScale(i)) .attr("fill", areaColorScale(fullData[index].key)) .attr("stroke", lineColorScale(fullData[index].key)) .attr("opacity", 0.3) .transition() .duration(1000) .attr("cy", (d) => yScale(d[1])); D.enter() .append("circle") .attr("class", `circle-${index}`) .attr("dot", (d, i) => i) .attr("cx", (d, i) => xScale(i)) .attr("cy", (d) => yScale(0)) .attr("r", 5) .attr("fill", dotColorScale(fullData[index].key)) .attr("stroke", lineColorScale(fullData[index].key)) .attr("stroke-width", 1) .attr("opacity", 0) .transition() .duration(1000) .attr("cy", (d) => yScale(d[1])); }); } function enterLegend(parent, data, chart, areaColorScale, lineColorScale) { parent.selectAll("*").remove(); let newData = data.slice().reverse(); let Leg = parent.selectAll("g").data(newData); let LegLayer = Leg.enter() .append("g") .attr("transform", (d, i) => `translate(${chart.pad.left}, ${i * 20 + chart.pad.top})`); LegLayer.append("rect") .attr("fill", (d) => areaColorScale(d)) .attr("width", 14) .attr("height", 14) .attr("stroke", (d) => lineColorScale(d)) .attr("stroke-width", 0) .attr("rx", 2); //.attr('shape-rendering', 'crispEdges') LegLayer.append("text") .text((d) => d .toLowerCase() .split(" ") .map((s) => s.charAt(0).toUpperCase() + s.substring(1)) .join(" ") ) .attr("x", 19) .attr("y", 12) .attr("font-size", 14) .attr("font-family", `'CTVSans-Regular','CTV Sans', 'sans-serif`); } //Area chart functions function getArea(xScale, yScale, zero = false) { return d3 .area() .x((d, i) => xScale(i)) .y0((d) => yScale(zero ? 0 : d[0])) .y1((d) => yScale(zero ? 0 : d[1])); } function enterArea(selection, area, areaZero, areaColorScale) { selection .enter() .append("path") .attr("class", "area") .attr("d", (d) => areaZero(d.data)) .attr("fill", (d, i) => areaColorScale(d.key)) .transition() .duration(1000) .attr("d", (d) => area(d.data)); return selection; } function updateArea(selection, area, dur) { selection .transition() .duration(dur) .attr("d", (d) => area(d.data)); } function exitArea(selection, areaZero, dur) { selection .exit() .lower() .attr("opacity", 1) .transition() .duration(1000) .attr("d", (d) => areaZero(d.data)) .attr("opacity", 1) .remove(); } //Resize functions function debounce(func) { var timer; return function (event) { if (timer) clearTimeout(timer); timer = setTimeout(func, 10, event); }; } function addResize(parent, data, metricArray, latestDate) { window.addEventListener( "resize", debounce(() => { resizeNewCurveChart(parent, data, metricArray, latestDate); }) ); } function makeButton(buttonParent, text, index, selection, parent, data, metricArray, latestDate) { toggle = "button-off"; if (metricArray[index] === selection) { toggle = "button-on"; } if (index === 2 && metricArray[2][0] === "cases" && text === "Total") { toggle = "button-on"; } if (index === 2 && metricArray[2][0] !== "cases" && text === "Breakdown") { toggle = "button-on"; } buttonParent .append("button") .text(text) .on("click", (d) => { metricArray[index] = selection; if (metricArray[2][0] !== "cases") { metricArray[2] = ["active", "recovered", "deaths"]; } if (metricArray[1] !== "cumulative") { metricArray[2] = metricArray[2].filter((e) => e !== "active"); } //if (metricArray[2][0] !== 'cases' && metricArray[1] !== 'cumulative') {metricArray[2] = ['deaths', 'recovered']} if (parent === "#covid-canada-date-chart") { chartInput = metricArray; } updateNewCurveChart( parent, data, [metricArray[0], metricArray[1], metricArray[2]], latestDate ); }) .attr("class", `chart-button ${toggle}`); } function checkDate(date, latestDate) { let d1 = new Date(date); let d2 = new Date(latestDate); let dayDiff = (d2.setHours(0, 0, 0, 0) - d1.setHours(0, 0, 0, 0)) / (1000 * 3600 * 24); //let check = date.setHours(0,0,0,0) == latestDate.setHours(0,0,0,0); //console.log(date, latestDate, check) return dayDiff; } //Constants const op = 0.55; let ds = -10; let dl = -10; const colorCategories = ["cases", "deaths", "recovered", "active"]; const areaColors = [`#fadca2`, `#a1a0a0`, `#efb88f`, `#76c1cf`]; const dotColors = [`#ffeecf`, `#d4d4d4`, `#ffe3cf`, `#bdf3fc`]; const lineColors = [ `rgb(244, 187, 63)`, `hsla(0, 0%, ${32.5 + dl}%, 1`, `hsla(27, 74.2%,${52.9 + dl}%, 1)`, `hsla(191, 71.6%,${38.6 + dl}%, 1)`, ]; const chartPadding = [10, 50, 30, 20]; const dur = 1000; function createNewCurveChart(parent, data, metricArray, latestDate) { let dataArray = data.tracking; let dateAdjustment = checkDate(dataArray[dataArray.length - 1].date, latestDate); //console.log(data.properties.PRENAME, dateAdjustment) dataArray = dataArray.filter((day, i) => i > 35 + dateAdjustment); let container = d3.select(parent); let titleDiv = appendOrSelect(container, "div", "title-div"); if (parent === "#covid-canada-date-chart") { titleDiv.html( `${data.properties.PRENAME} (click map for provincial stats)` ); } else { titleDiv.html(`${data.properties.PRENAME}`); } let buttonDiv = appendOrSelect(container, "div", "button-div"); let buttonGroup1 = buttonDiv.append("div").attr("class", "button-group"); let buttonGroup2 = buttonDiv.append("div").attr("class", "button-group"); let buttonGroup3 = buttonDiv.append("div").attr("class", "button-group"); makeButton(buttonGroup1, "Total", 2, ["cases"], parent, data, metricArray, latestDate); makeButton( buttonGroup1, "Breakdown", 2, ["active", "recovered", "deaths"], parent, data, metricArray, latestDate ); makeButton(buttonGroup2, "Cumulative", 1, "cumulative", parent, data, metricArray, latestDate); makeButton(buttonGroup2, "New", 1, "new", parent, data, metricArray, latestDate); makeButton(buttonGroup2, "7-day avg", 1, "average", parent, data, metricArray, latestDate); makeButton(buttonGroup3, "Raw", 0, "raw", parent, data, metricArray, latestDate); makeButton(buttonGroup3, "/100K", 0, "per100k", parent, data, metricArray, latestDate); let svg = appendOrSelect(container, "svg", "curve-svg"); svg.attr("width", "100%").attr("height", "250"); let chart = createChartSettings(svg, dataArray, chartPadding); let chartLayer = appendOrSelect(svg, "g", "chart-layer"); let axisLayer = appendOrSelect(chartLayer, "g", "axis-layer"); let axisX = appendOrSelect(axisLayer, "g", "axis-x"); let axisY = appendOrSelect(axisLayer, "g", "axis-y"); let titleLayer = appendOrSelect(chartLayer, "g", "title-layer"); let dataLayer = appendOrSelect(chartLayer, "g", "data-layer"); let areaLayer = appendOrSelect(dataLayer, "g", "area-layer"); let lineLayer = appendOrSelect(dataLayer, "g", "line-layer"); let legendLayer = appendOrSelect(chartLayer, "g", "legend-layer"); let dotLayer = appendOrSelect(chartLayer, "g", "dot-layer"); let hoverLayer = appendOrSelect(chartLayer, "g", "hover-layer"); let tooltipLayer = appendOrSelect(chartLayer, "g", "tooltip-layer"); let max = getMaxSum(dataArray, metricArray); let yScale = getYScale(chart, max); let xScale = getXScale(chart); let areaColorScale = d3.scaleOrdinal().domain(colorCategories).range(areaColors); let lineColorScale = d3.scaleOrdinal().domain(colorCategories).range(lineColors); let dotColorScale = d3.scaleOrdinal().domain(colorCategories).range(dotColors); let timeScale = getTimeScale(chart, dataArray); // console.log(dataArray); let xAxis = d3 .axisBottom() .scale(timeScale) .tickFormat(function (date) { if (d3.timeYear(date) < date) { return d3.timeFormat("%b")(date); } else { return d3.timeFormat("%Y")(date); } }); axisX .call(xAxis) .attr("transform", `translate(0, ${chart.height - chart.pad.bottom})`) .attr("font-family", `'CTVSans-Regular','CTV Sans', 'sans-serif`); let yAxis = d3.axisRight().scale(yScale).ticks(6); axisY .call(yAxis) .attr("transform", `translate(${chart.width - chart.pad.right}, 0)`) .attr("font-family", `'CTVSans-Regular','CTV Sans', 'sans-serif`); let breakdown = []; metricArray[2].forEach((metric) => { breakdown.push([metricArray[0], metricArray[1], metric]); }); const stack = d3 .stack() .keys(breakdown) .value((d, key) => d[key[0]][key[1]][key[2]]); const stackedValues = stack(dataArray); let stackArray = []; stackedValues.forEach((stack, i) => { let key = metricArray[2][i]; stackArray.push({ key: key, data: stack, }); }); let area = getArea(xScale, yScale); let areaZero = getArea(xScale, yScale, true); let line = getLine(xScale, yScale); let lineZero = getLine(xScale, yScale, true); let A = areaLayer.selectAll(".area").data(stackArray, (d) => d.key); enterArea(A, area, areaZero, areaColorScale); let L = lineLayer.selectAll(".line").data(stackArray, (d) => d.key); L.enter() .append("path") .attr("class", "line") .attr("d", (d) => lineZero(d.data)) .attr("stroke", (d, i) => lineColorScale(d.key)) .attr("stroke-width", 1) .attr("fill", "none") .transition() .duration(1000) .attr("d", (d) => line(d.data)); let H = hoverLayer.selectAll("rect").data(dataArray); function toolTip(e, d, i) { tooltipLayer.selectAll("*").remove(); dotLayer.selectAll(`[dot="${i}"]`).attr("opacity", 1); const [x, y] = d3.pointer(e); // console.log(e, d, i); let newData = metricArray[2].slice().reverse(); newData.forEach((metric, j) => { tooltipLayer .append("text") .attr("x", x < chart.width / 2 ? x + 25 : x - 15) .attr("y", y + 10 + j * 15 - 0.5 * metricArray[2].length * 15) .attr("text-anchor", x < chart.width / 2 ? "start" : "end") .style("pointer-events", "none") .text(`${metric}: ${d[metricArray[0]][metricArray[1]][metric]}`); }); } H.enter() .append("rect") .attr("x", (d, i) => xScale(i) - (xScale(i + 1) - xScale(i)) / 2) .attr("y", yScale(max * 1.05)) .attr("width", (d, i) => xScale(i + 1) - xScale(i)) .attr("height", yScale(0)) .attr("shape-rendering", "crispEdges") .attr("opacity", 0) .on("mousemove", function (e, d, i) { tooltipLayer.selectAll("*").remove(); dotLayer.selectAll(`[dot="${i}"]`).attr("opacity", 1); const [x, y] = d3.pointer(e); let newData = metricArray[2].slice().reverse(); let rect = tooltipLayer .append("rect") .attr("fill", "white") .attr("stroke", "#444") .attr("shape-rendering", "crispEdges") .style("pointer-events", "none"); let dateText = tooltipLayer .append("text") .attr("x", x < chart.width / 2 ? x + 25 : x - 15) .attr("y", y + 8 + -0.5 * (metricArray[2].length + 1) * 15) .attr("text-anchor", x < chart.width / 2 ? "start" : "end") .style("pointer-events", "none") .html(`${d.date.toLocaleDateString()}`); let tipWidth = dateText.node().getComputedTextLength(); let tipHeight = 15; newData.forEach((metric, j) => { let text = tooltipLayer .append("text") .attr("x", x < chart.width / 2 ? x + 25 : x - 15) .attr("y", y + 10 + (j + 1) * 15 - 0.5 * (metricArray[2].length + 1) * 15) .attr("text-anchor", x < chart.width / 2 ? "start" : "end") .style("pointer-events", "none") .html( `${metric}: ${d[ metricArray[0] ][metricArray[1]][metric].toLocaleString()}` ); tipWidth = Math.max(tipWidth, text.node().getComputedTextLength()); tipHeight += 15; }); rect .attr("width", tipWidth + 10) .attr("height", tipHeight + 10) .attr("x", (x < chart.width / 2 ? x + 25 + tipWidth : x - 15) - tipWidth - 5) .attr("y", y - (tipHeight + 10) / 2 - 2.5); }) .on("mouseout", function (d, i) { dotLayer.selectAll(`[dot]`).attr("opacity", 0); tooltipLayer.selectAll("*").remove(); }); createDots(stackArray, dotLayer, xScale, yScale, areaColorScale, lineColorScale, dotColorScale); enterLegend(legendLayer, metricArray[2], chart, areaColorScale, lineColorScale); addResize(parent, data, metricArray, latestDate); } function updateNewCurveChart(parent, data, metricArray, latestDate) { let dataArray = data.tracking; let dateAdjustment = checkDate(dataArray[dataArray.length - 1].date, latestDate); dataArray = dataArray.filter((day, i) => i > 35 - dateAdjustment); let container = d3.select(parent); let titleDiv = appendOrSelect(container, "div", "title-div"); titleDiv.text(data.properties.PRENAME); let buttonDiv = appendOrSelect(container, "div", "button-div"); buttonDiv.selectAll("*").remove(); let buttonGroup1 = buttonDiv.append("div").attr("class", "button-group"); let buttonGroup2 = buttonDiv.append("div").attr("class", "button-group"); let buttonGroup3 = buttonDiv.append("div").attr("class", "button-group"); makeButton(buttonGroup1, "Total", 2, ["cases"], parent, data, metricArray, latestDate); makeButton( buttonGroup1, "Breakdown", 2, ["active", "recovered", "deaths"], parent, data, metricArray, latestDate ); makeButton(buttonGroup2, "Cumulative", 1, "cumulative", parent, data, metricArray, latestDate); makeButton(buttonGroup2, "New", 1, "new", parent, data, metricArray, latestDate); makeButton(buttonGroup2, "7-day avg", 1, "average", parent, data, metricArray, latestDate); makeButton(buttonGroup3, "Raw", 0, "raw", parent, data, metricArray, latestDate); makeButton(buttonGroup3, "/100K", 0, "per100k", parent, data, metricArray, latestDate); let svg = appendOrSelect(container, "svg", "curve-svg"); //svg.on('click', d => updateNewCurveChart(parent, data, ['total'])) let chart = createChartSettings(svg, dataArray, chartPadding); let chartLayer = appendOrSelect(svg, "g", "chart-layer"); let axisLayer = appendOrSelect(chartLayer, "g", "axis-layer"); let axisX = appendOrSelect(axisLayer, "g", "axis-x"); let axisY = appendOrSelect(axisLayer, "g", "axis-y"); let titleLayer = appendOrSelect(chartLayer, "g", "title-layer"); let dataLayer = appendOrSelect(chartLayer, "g", "data-layer"); let areaLayer = appendOrSelect(dataLayer, "g", "area-layer"); let lineLayer = appendOrSelect(dataLayer, "g", "line-layer"); let legendLayer = appendOrSelect(chartLayer, "g", "legend-layer"); let dotLayer = appendOrSelect(chartLayer, "g", "dot-layer"); let hoverLayer = appendOrSelect(chartLayer, "g", "hover-layer"); let tooltipLayer = appendOrSelect(chartLayer, "g", "tooltip-layer"); let max = getMaxSum(dataArray, metricArray); let yScale = getYScale(chart, max); let xScale = getXScale(chart); let areaColorScale = d3.scaleOrdinal().domain(colorCategories).range(areaColors); let lineColorScale = d3.scaleOrdinal().domain(colorCategories).range(lineColors); let dotColorScale = d3.scaleOrdinal().domain(colorCategories).range(dotColors); let timeScale = getTimeScale(chart, dataArray); let xAxis = d3 .axisBottom() .scale(timeScale) .tickFormat(function (date) { if (d3.timeYear(date) < date) { return d3.timeFormat("%b")(date); } else { return d3.timeFormat("%Y")(date); } }); axisX.transition().duration(1000).call(xAxis); let yAxis = d3.axisRight().scale(yScale).ticks(6); axisY.transition().duration(1000).call(yAxis); let breakdown = []; metricArray[2].forEach((metric) => { breakdown.push([metricArray[0], metricArray[1], metric]); }); const stack = d3 .stack() .keys(breakdown) .value((d, key) => d[key[0]][key[1]][key[2]]); const stackedValues = stack(dataArray); let stackArray = []; stackedValues.forEach((stack, i) => { let key = metricArray[2][i]; stackArray.push({ key: key, data: stack, }); }); let area = getArea(xScale, yScale); let areaZero = getArea(xScale, yScale, true); let line = getLine(xScale, yScale); let lineZero = getLine(xScale, yScale, true); let A = areaLayer.selectAll(".area").data(stackArray, (d) => d.key); exitArea(A, areaZero, dur); updateArea(A, area, dur); enterArea(A, area, areaZero, areaColorScale); let L = lineLayer.selectAll(".line").data(stackArray, (d) => d.key); L.exit() .lower() .attr("opacity", 1) .transition() .duration(1000) .attr("d", (d) => lineZero(d.data)) .attr("opacity", 0) .remove(); L.transition() .duration(1000) .attr("d", (d) => line(d.data)); L.enter() .append("path") .attr("class", "line") .attr("d", (d) => lineZero(d.data)) .attr("stroke", (d, i) => lineColorScale(d.key)) .attr("stroke-width", 1) .attr("fill", "none") .attr("opacity", 1) .transition() .duration(1000) .attr("d", (d) => line(d.data)) .attr("opacity", 1); let H = hoverLayer.selectAll("rect").data(dataArray); H.exit().remove(); H.attr("x", (d, i) => xScale(i) - (xScale(i + 1) - xScale(i)) / 2) .attr("y", yScale(max * 1.05)) .attr("width", (d, i) => xScale(i + 1) - xScale(i)) .attr("height", yScale(0)) .on("mousemove", function (e, d, i) { tooltipLayer.selectAll("*").remove(); dotLayer.selectAll(`[dot="${i}"]`).attr("opacity", 1); const [x, y] = d3.pointer(e); let newData = metricArray[2].slice().reverse(); let rect = tooltipLayer .append("rect") .attr("fill", "white") .attr("stroke", "#444") .attr("shape-rendering", "crispEdges") .style("pointer-events", "none"); let dateText = tooltipLayer .append("text") .attr("x", x < chart.width / 2 ? x + 25 : x - 15) .attr("y", y + 8 + -0.5 * (metricArray[2].length + 1) * 15) .attr("text-anchor", x < chart.width / 2 ? "start" : "end") .style("pointer-events", "none") .html(`${d.date.toLocaleDateString()}`); let tipWidth = dateText.node().getComputedTextLength(); let tipHeight = 15; newData.forEach((metric, j) => { let text = tooltipLayer .append("text") .attr("x", x < chart.width / 2 ? x + 25 : x - 15) .attr("y", y + 10 + (j + 1) * 15 - 0.5 * (metricArray[2].length + 1) * 15) .attr("text-anchor", x < chart.width / 2 ? "start" : "end") .style("pointer-events", "none") .html( `${metric}: ${d[ metricArray[0] ][metricArray[1]][metric].toLocaleString()}` ); tipWidth = Math.max(tipWidth, text.node().getComputedTextLength()); tipHeight += 15; }); rect .attr("width", tipWidth + 10) .attr("height", tipHeight + 10) .attr("x", (x < chart.width / 2 ? x + 25 + tipWidth : x - 15) - tipWidth - 5) .attr("y", y - (tipHeight + 10) / 2 - 2.5); }) .on("mouseout", function (d, i) { dotLayer.selectAll(`[dot]`).attr("opacity", 0); tooltipLayer.selectAll("*").remove(); }); H.enter() .append("rect") .attr("x", (d, i) => xScale(i) - (xScale(i + 1) - xScale(i)) / 2) .attr("y", yScale(max * 1.05)) .attr("width", (d, i) => xScale(i + 1) - xScale(i)) .attr("height", yScale(0)) .attr("shape-rendering", "crispEdges") .attr("opacity", 0) .on("mousemove", function (e, d, i) { tooltipLayer.selectAll("*").remove(); dotLayer.selectAll(`[dot="${i}"]`).attr("opacity", 1); const [x, y] = d3.pointer(e); // console.log(e, d, i); let newData = metricArray[2].slice().reverse(); let rect = tooltipLayer .append("rect") .attr("fill", "white") .attr("stroke", "#444") .attr("shape-rendering", "crispEdges") .style("pointer-events", "none"); let dateText = tooltipLayer .append("text") .attr("x", x < chart.width / 2 ? x + 25 : x - 15) .attr("y", y + 8 + -0.5 * (metricArray[2].length + 1) * 15) .attr("text-anchor", x < chart.width / 2 ? "start" : "end") .style("pointer-events", "none") .html(`${d.date.toLocaleDateString()}`); let tipWidth = dateText.node().getComputedTextLength(); let tipHeight = 15; newData.forEach((metric, j) => { let text = tooltipLayer .append("text") .attr("x", x < chart.width / 2 ? x + 25 : x - 15) .attr("y", y + 10 + (j + 1) * 15 - 0.5 * (metricArray[2].length + 1) * 15) .attr("text-anchor", x < chart.width / 2 ? "start" : "end") .style("pointer-events", "none") .html( `${metric}: ${d[ metricArray[0] ][metricArray[1]][metric].toLocaleString()}` ); tipWidth = Math.max(tipWidth, text.node().getComputedTextLength()); tipHeight += 15; }); rect .attr("width", tipWidth + 10) .attr("height", tipHeight + 10) .attr("x", (x < chart.width / 2 ? x + 25 + tipWidth : x - 15) - tipWidth - 5) .attr("y", y - (tipHeight + 10) / 2 - 2.5); }) .on("mouseout", function (d, i) { dotLayer.selectAll(`[dot]`).attr("opacity", 0); tooltipLayer.selectAll("*").remove(); }); updateDots(stackArray, dotLayer, xScale, yScale, areaColorScale, lineColorScale, dotColorScale); enterLegend(legendLayer, metricArray[2], chart, areaColorScale, lineColorScale); addResize(parent, data, metricArray, latestDate); } function resizeNewCurveChart(parent, data, metricArray, latestDate) { let dataArray = data.tracking; let dateAdjustment = checkDate(dataArray[dataArray.length - 1].date, latestDate); dataArray = dataArray.filter((day, i) => i > 35 + dateAdjustment); let container = d3.select(parent); let svg = appendOrSelect(container, "svg", "curve-svg"); let chart = createChartSettings(svg, dataArray, chartPadding); let chartLayer = appendOrSelect(svg, "g", "chart-layer"); let axisLayer = appendOrSelect(chartLayer, "g", "axis-layer"); let axisX = appendOrSelect(axisLayer, "g", "axis-x"); let axisY = appendOrSelect(axisLayer, "g", "axis-y"); let titleLayer = appendOrSelect(chartLayer, "g", "title-layer"); let dataLayer = appendOrSelect(chartLayer, "g", "data-layer"); let areaLayer = appendOrSelect(dataLayer, "g", "area-layer"); let lineLayer = appendOrSelect(dataLayer, "g", "line-layer"); let legendLayer = appendOrSelect(chartLayer, "g", "legend-layer"); let dotLayer = appendOrSelect(chartLayer, "g", "dot-layer"); let hoverLayer = appendOrSelect(chartLayer, "g", "hover-layer"); let tooltipLayer = appendOrSelect(chartLayer, "g", "tooltip-layer"); let areaColorScale = d3.scaleOrdinal().domain(colorCategories).range(areaColors); let lineColorScale = d3.scaleOrdinal().domain(colorCategories).range(lineColors); let dotColorScale = d3.scaleOrdinal().domain(colorCategories).range(dotColors); let max = getMaxSum(dataArray, metricArray); let yScale = getYScale(chart, max); let xScale = getXScale(chart); let timeScale = getTimeScale(chart, dataArray); // console.log(chart); let xAxis = d3 .axisBottom() .scale(timeScale) .tickFormat(function (date) { if (d3.timeYear(date) < date) { return d3.timeFormat("%b")(date); } else { return d3.timeFormat("%Y")(date); } }); axisX.call(xAxis).attr("transform", `translate(0, ${chart.height - chart.pad.bottom})`); let yAxis = d3.axisRight().scale(yScale).ticks(6); axisY.call(yAxis).attr("transform", `translate(${chart.width - chart.pad.right}, 0)`); let breakdown = []; metricArray[2].forEach((metric) => { breakdown.push([metricArray[0], metricArray[1], metric]); }); const stack = d3 .stack() .keys(breakdown) .value((d, key) => d[key[0]][key[1]][key[2]]); const stackedValues = stack(dataArray); let stackArray = []; stackedValues.forEach((stack, i) => { let key = metricArray[2][i]; stackArray.push({ key: key, data: stack, }); }); let area = getArea(xScale, yScale); let line = getLine(xScale, yScale); let A = areaLayer.selectAll(".area").data(stackArray, (d) => d.key); updateArea(A, area, 0); let L = lineLayer .selectAll(".line") .data(stackArray, (d) => d.key) .attr("d", (d) => line(d.data)); let H = hoverLayer.selectAll("rect").data(dataArray); H.attr("x", (d, i) => xScale(i) - (xScale(i + 1) - xScale(i)) / 2) .attr("y", yScale(max * 1.05)) .attr("width", (d, i) => xScale(i + 1) - xScale(i)) .attr("height", yScale(0)) .on("mousemove", function (e, d, i) { tooltipLayer.selectAll("*").remove(); dotLayer.selectAll(`[dot="${i}"]`).attr("opacity", 1); const [x, y] = d3.pointer(e); // console.log(e, d, i); let newData = metricArray[2].slice().reverse(); let rect = tooltipLayer .append("rect") .attr("fill", "white") .attr("stroke", "#444") .attr("shape-rendering", "crispEdges") .style("pointer-events", "none"); let dateText = tooltipLayer .append("text") .attr("x", x < chart.width / 2 ? x + 25 : x - 15) .attr("y", y + 8 + -0.5 * (metricArray[2].length + 1) * 15) .attr("text-anchor", x < chart.width / 2 ? "start" : "end") .style("pointer-events", "none") .html(`${d.date.toLocaleDateString()}`); let tipWidth = dateText.node().getComputedTextLength(); let tipHeight = 15; newData.forEach((metric, j) => { let text = tooltipLayer .append("text") .attr("x", x < chart.width / 2 ? x + 25 : x - 15) .attr("y", y + 10 + (j + 1) * 15 - 0.5 * (metricArray[2].length + 1) * 15) .attr("text-anchor", x < chart.width / 2 ? "start" : "end") .style("pointer-events", "none") .html( `${metric}: ${d[ metricArray[0] ][metricArray[1]][metric].toLocaleString()}` ); tipWidth = Math.max(tipWidth, text.node().getComputedTextLength()); tipHeight += 15; }); rect .attr("width", tipWidth + 10) .attr("height", tipHeight + 10) .attr("x", (x < chart.width / 2 ? x + 25 + tipWidth : x - 15) - tipWidth - 5) .attr("y", y - (tipHeight + 10) / 2 - 2.5); }) .on("mouseout", function (d, i) { dotLayer.selectAll(`[dot]`).attr("opacity", 0); tooltipLayer.selectAll("*").remove(); }); updateDots(stackArray, dotLayer, xScale, yScale, areaColorScale, lineColorScale, dotColorScale); }