// TO DO // // Improve tooltip // Fix resize event listener 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) 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] = ['deaths', 'recovered', 'active'] } 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') titleDiv.text(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, ['deaths', 'recovered', 'active'], 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) let xAxis = d3.axisBottom().scale(timeScale) 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(d, i) { tooltipLayer.selectAll('*').remove(); dotLayer.selectAll(`[dot="${i}"]`).attr('opacity', 1) let x = d3.mouse(this)[0] let y = d3.mouse(this)[1] 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(d,i) { tooltipLayer.selectAll('*').remove(); dotLayer.selectAll(`[dot="${i}"]`).attr('opacity', 1) let x = d3.mouse(this)[0] let y = d3.mouse(this)[1] 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, ['deaths', 'recovered', 'active'], 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) axisX.transition().duration(1000).call(xAxis) let yAxis = d3.axisRight().scale(yScale) 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(d,i) { tooltipLayer.selectAll('*').remove(); dotLayer.selectAll(`[dot="${i}"]`).attr('opacity', 1) let x = d3.mouse(this)[0] let y = d3.mouse(this)[1] 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(d,i) { tooltipLayer.selectAll('*').remove(); dotLayer.selectAll(`[dot="${i}"]`).attr('opacity', 1) let x = d3.mouse(this)[0] let y = d3.mouse(this)[1] 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) let xAxis = d3.axisBottom().scale(timeScale) axisX.call(xAxis).attr('transform', `translate(0, ${chart.height - chart.pad.bottom})`) let yAxis = d3.axisRight().scale(yScale) 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(d,i) { tooltipLayer.selectAll('*').remove(); dotLayer.selectAll(`[dot="${i}"]`).attr('opacity', 1) let x = d3.mouse(this)[0] let y = d3.mouse(this)[1] 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) }