let chartGraph = { pad: { top: 30, right: 40, bottom: 30, left: 20 }, data: null, showBreakdown: false, showAnnounced: false } function createGraphData(geoData, provinceData) { //console.log(provinceData) function createSortedTimeline(region) { region.data.timeline = []; region.data.cases.forEach((person, i) => { if (person.Accurate_Episode_Date) { let date = person.Accurate_Episode_Date.split('T')[0]; let day = region.data.timeline.find(d => d.date === date); let personRecovered = person.Outcome1 === 'Resolved' ? 1 : 0; let personActive = person.Outcome1 === 'Not Resolved' ? 1 : 0; let personDeaths = person.Outcome1 === 'Fatal' ? 1 : 0; if (day) { day.cases += 1; day.recovered += personRecovered day.active += personActive day.deaths += personDeaths } else { let newDay = { date: date, cases: 1, recovered : personRecovered, active : personActive, deaths : personDeaths } region.data.timeline.push(newDay) } } }) region.data.timeline.sort((a, b) => new Date(a.date) - new Date(b.date)) console.log(region.data.timeline) } function fillOutTimeline(region, minDate=null, maxDate=null) { if (minDate) { let newDay = { added: true, date: minDate, cases: 0, recovered : 0, active : 0, deaths : 0 } if (region.data.timeline[0].date !== minDate) { /*let check = checkDate(region.data.timeline[0].date, minDate);*/ region.data.timeline.splice(0, 0, newDay) } } if (maxDate) { let newDay = { added: true, date: maxDate, cases: 0, recovered : 0, active : 0, deaths : 0 } if (region.data.timeline[region.data.timeline.length-1].date !== maxDate) { region.data.timeline.splice(region.data.timeline.length, 0, newDay) } } for (let i = 0; i < region.data.timeline.length - 1; i++) { if (i < 1000) { let dayDiff = checkDate(region.data.timeline[i].date, region.data.timeline[i+1].date); if (dayDiff !== 1) { let today = new Date(region.data.timeline[i].date) let tomorrow = new Date(today.getTime()); tomorrow.setDate(today.getDate() + 1); let newDay = { added: true, date: `${tomorrow.getUTCFullYear()}-${String((tomorrow.getUTCMonth() + 1)).padStart(2, 0)}-${String(tomorrow.getUTCDate() + (today.getUTCMonth() + 1 === 3 && today.getUTCDate() === 8 ? 1 : 0)).padStart(2, 0)}`, cases: 0, recovered : 0, active : 0, deaths : 0 } region.data.timeline.splice(i + 1, 0, newDay) } } } /* if (minDate === null) { console.log('DATE',region.data.timeline[0].date, provinceData[0].date, checkDate(region.data.timeline[0].date, provinceData[0].date)) console.log(first, last) return [first, last] } return [region.data.timeline[0].date, region.data.timeline[region.data.timeline.length - 1].date] */ } function checkDate(date, latestDate) { let dt1 = new Date(date); let dt2 = new Date(latestDate); return Math.floor((Date.UTC(dt2.getFullYear(), dt2.getMonth(), dt2.getDate()) - Date.UTC(dt1.getFullYear(), dt1.getMonth(), dt1.getDate()) ) /(1000 * 60 * 60 * 24)); } createSortedTimeline(geoData); //let first = checkDate(geoData.data.timeline[0].date, provinceData[0].date) < 0 ? provinceData[0].date : geoData.data.timeline[0].date //let last = checkDate(geoData.data.timeline[geoData.data.timeline.length - 1].date, provinceData[provinceData.length -1].date) > 0 ? provinceData[provinceData.length -1].date : geoData.data.timeline[geoData.data.timeline.length - 1].date; //let pDate = provinceData[provinceData.length -1].date.split('-') let first = "2020-01-01" let last = provinceData[provinceData.length -1].date; //console.log(first, last) fillOutTimeline(geoData, first, last); let lastDateIndex = geoData.data.timeline.indexOf(geoData.data.timeline.find(date => date.date === last)) geoData.data.timeline = geoData.data.timeline.filter((date, i) => i <= lastDateIndex) geoData.features.forEach(region => { createSortedTimeline(region); fillOutTimeline(region, first, last) region.data.timeline = region.data.timeline.filter((date, i) => i <= lastDateIndex) }) geoData.data.timeline.forEach(day => { let obj = provinceData.find(provinceDay => provinceDay.date === day.date); if (obj) { day.announced = obj.new; } else { day.announced = 0; } }) let array = [] geoData.features.forEach(feature => { let newObj = { region: feature.data.healthUnit, timeline: [] } feature.data.timeline.forEach(day => { let newDay = { date: day.date, cases: day.cases } newObj.timeline.push(newDay) }) array.push(newObj) }) //console.log(JSON.stringify(array)) return geoData; } function createProvinceGraph(parent, geoData, provinceData) { let chart = chartGraph; chart.data = geoData; //console.log(geoData) let graphDiv = d3.select(parent) let buttonBreakdown = graphDiv.append('button').attr('class', 'graph-button breakdown').text('Show Deaths/Recoveries').on('click', function() { chartGraph.showBreakdown = !chartGraph.showBreakdown; if (chartGraph.showBreakdown) { this.style.background = '#2c2c2c'; this.style.color = '#f3f3f3'; } else { this.style.background = '#ebebeb'; this.style.color = '#2c2c2c' } updateProvinceGraph(parent, chartGraph.data, provinceData) }) let buttonAnnounced = graphDiv.append('button').attr('class', 'graph-button announced').text('Show Cases by Date Announced').on('click', function() { if (chartGraph.data.type !== 'Feature') { chartGraph.showAnnounced = !chartGraph.showAnnounced; if (chartGraph.showAnnounced) { this.style.background = '#2c2c2c'; this.style.color = '#f3f3f3'; } else { this.style.background = '#ebebeb'; this.style.color = '#2c2c2c' } updateProvinceGraph(parent, chartGraph.data, provinceData) } }) let svg = graphDiv.append('svg'); svg.attr('width', '100%').attr('height', 325) let width = Number(svg.style('width').split('px')[0]); let height = Number(svg.style('height').split('px')[0]); let gridLayer = svg.append('g').attr('class', 'grid-layer') let legendLayer = svg.append('g').attr('class', 'legend-layer') let chartLayer = svg.append('g').attr('class', 'chart-layer') let totalLayer = chartLayer.append('g').attr('class', 'total-layer') let deathsLayer = chartLayer.append('g').attr('class', 'deaths-layer') let recoveredLayer = chartLayer.append('g').attr('class', 'recovered-layer') let provinceLayer = chartLayer.append('g').attr('class', 'province-layer') let timeline = geoData.data.timeline; //let timeline = geoData.data.timeline.filter((data, i) => i > 30); let yScale = d3.scaleLinear() .domain([0, d3.max(timeline.map(day => day.cases))]) .range([height - chart.pad.bottom, chart.pad.top]) yScale = yScale.nice() let xScale = d3.scaleLinear() .domain([0, timeline.length-1]) .range([chart.pad.left, width - chart.pad.right]) //console.log(timeline[timeline.length-1].date,new Date(timeline[timeline.length-1].date)) let dayScale = d3.scaleTime() .domain([new Date(timeline[0].date), new Date(timeline[timeline.length-1].date)]) .range([chart.pad.left, width - chart.pad.right]) let zeroArea = d3.area().x((d, i) => xScale(i)).y0(yScale(0)).y1(yScale(0)) let totalArea = d3.area() //.x((d, i) => dayScale(new Date(d.date))).y0(d => yScale(0)).y1(d => yScale(d.cases)) .x((d, i) => xScale(i)).y0(d => yScale(0)).y1(d => yScale(d.cases)) .curve(d3.curveStep) totalLayer.append('path').datum(timeline) .attr('class', 'graph-line') .style('fill', 'rgba(255, 213, 130, 0.7)') //.style('stroke', '#c16262') .style('stroke-width', 0) .attr('shape-rendering', 'crispEdges') .attr('d', totalArea) if (geoData.data.recovered) { let recoveredLine = d3.line() //.x((d, i) => dayScale(new Date(d.date))).y(d => yScale(d.recovered)) .x((d, i) => xScale(i)).y(d => yScale(d.recovered)) .curve(d3.curveStep) recoveredLayer.append('path').datum(timeline) .attr('class', 'graph-line') .style('fill', 'none') .style('stroke', 'rgb(212, 135, 104)') .style('stroke-width', 2) .attr('shape-rendering', 'crispEdges') .attr('d', recoveredLine) } if (geoData.data.deaths) { let deathsLine = d3.line() //.x((d, i) => dayScale(new Date(d.date))).y(d => yScale(d.deaths)) .x((d, i) => xScale(i)).y(d => yScale(d.deaths)) .curve(d3.curveStep) deathsLayer.append('path').datum(timeline) .attr('class', 'graph-line') .style('stroke', '#1b262c') .style('stroke-width', 2) .attr('shape-rendering', 'crispEdges') .attr('d', deathsLine) } if (!chartGraph.showBreakdown) { deathsLayer.attr('display', 'none') recoveredLayer.attr('display', 'none') } else { deathsLayer.attr('display', 'inline') recoveredLayer.attr('display', 'inline') } //Province report line /* let provinceLine = d3.line() .x((d, i) => dayScale(new Date(d.date))).y(d => yScale(d.new)) .curve(d3.curveStep) */ let provinceArea = d3.area() //.x((d, i) => dayScale(new Date(d.date))).y0(d=>yScale(0)).y1(d => yScale(d.new)) .x((d, i) => xScale(i)).y0(d=>yScale(0)).y1(d => yScale(d.announced)) .curve(d3.curveStep) let provincePlot = provinceLayer.append('path').datum(timeline) .attr('class', 'province-line') .style('fill', 'rgba(3, 90, 166, 0.5)') .attr('shape-rendering', 'crispEdges') .attr('d', provinceArea) if (chartGraph.data.type === 'Feature') { provinceLayer.attr('display', 'none') } else { if (chartGraph.showAnnounced) { provinceLayer.attr('display', 'inline') } else { provinceLayer.attr('display', 'none') } } gridLayer.append('g').attr('class', 'y-axis') .attr('transform', `translate(${width-chart.pad.right}, 0)`) .call(d3.axisRight(yScale) .tickSize(-width + chart.pad.right + chart.pad.left) .tickSizeOuter(0) ) gridLayer.append('g').attr('class', 'x-axis') .attr('transform', `translate(0, ${height-chart.pad.bottom})`) .call(d3.axisBottom(dayScale) .ticks(width/100) /* .tickFormat((d, i) => { let num = i*Math.round(timeline.length/(width/100) + 2) let dat = geoData.data.timeline[num].date.split('-') console.log(dat[1] + '/' + dat[2]) return dat[1] + '/' + dat[2] }) */ ) function addLegend(shape, text, [x, y], [w, h=0], fill, stroke) { let L = legendLayer.append(shape) if (shape === 'rect') { L.attr('x', x).attr('y', y) .attr('width', w).attr('height', h) .attr('fill', fill) } else if (shape === 'line') { L.attr('x1', x).attr('y1', y + h/2) .attr('x2', x+w).attr('y2', y + h/2) .attr('width', w).attr('height', h) } if (stroke) { L.attr('stroke', stroke).attr('stroke-width', 2) } let T = legendLayer.append('text').text(text).attr('font-size', 13) .attr('x', x + w + 6).attr('y', y + (3*h/4)) } addLegend('rect', 'Cases (date of symptoms)', [chart.pad.left, chart.pad.top], [20, 20], 'rgba(255, 213, 130, 0.7)', null) addLegend('rect', 'Cases (date announced)', [chart.pad.left, chart.pad.top + 25], [20, 20], '#035aa666', null) addLegend('line', 'Recovered', [chart.pad.left, chart.pad.top + 50], [20, 20], null, 'rgb(212, 135, 104)') addLegend('line', 'Deaths', [chart.pad.left, chart.pad.top + 75], [20, 20], null, '#1b262c') window.addEventListener('resize', debounce(() => { updateProvinceGraph(parent, chartGraph.data, provinceData) })) } function updateProvinceGraph(parent, geoData, provinceData=null) { let chart = chartGraph; chart.data = geoData; let graphDiv = d3.select(parent) let svg = graphDiv.select('svg'); svg.attr('width', '100%').attr('height', 325) let width = Number(svg.style('width').split('px')[0]); let height = Number(svg.style('height').split('px')[0]); let gridLayer = svg.select('.grid-layer') let chartLayer = svg.select('.chart-layer') let totalLayer = chartLayer.select('.total-layer') let provinceLayer = chartLayer.select('.province-layer') let recoveredLayer = chartLayer.select('.recovered-layer') let deathsLayer = chartLayer.select('.deaths-layer') let timeline = geoData.data.timeline; //let timeline = geoData.data.timeline.filter((data, i) => i > 30); let yScale = d3.scaleLinear() .domain([0, d3.max(timeline.map(day => day.cases))]) .range([height - chart.pad.bottom, chart.pad.top]) yScale = yScale.nice() let xScale = d3.scaleLinear() .domain([0, timeline.length-1]) .range([chart.pad.left, width - chart.pad.right]) let dayScale = d3.scaleTime() .domain([new Date(timeline[0].date), new Date(timeline[timeline.length-1].date)]) .range([chart.pad.left, width - chart.pad.right]) //console.log(timeline[timeline.length-1].date) let totalArea = d3.area() .x((d, i) => xScale(i)).y0(d => yScale(0)).y1(d => yScale(d.cases)).curve(d3.curveStep) let recoveredLine = d3.line() .x((d, i) => xScale(i)).y(d => yScale(d.recovered)) .curve(d3.curveStep) let deathsLine = d3.line() .x((d, i) => xScale(i)).y(d => yScale(d.deaths)) .curve(d3.curveStep) let provinceArea = d3.area() //.x((d, i) => dayScale(new Date(d.date))).y0(d=>yScale(0)).y1(d => yScale(d.new)) .x((d, i) => xScale(i)).y0(d=>yScale(0)).y1(d => yScale(d.announced)) .curve(d3.curveStep) let zeroArea = d3.line().x((d, i) => xScale(i)).y(yScale(0)) totalLayer.select('path').datum(timeline) .transition().duration(500) .attr('d', totalArea) recoveredLayer.select('path').datum(timeline) .transition().duration(500) .attr('d', recoveredLine) deathsLayer.select('path').datum(timeline) .transition().duration(500) .attr('d', deathsLine) if (!chartGraph.showBreakdown) { deathsLayer.attr('display', 'none') recoveredLayer.attr('display', 'none') } else { deathsLayer.attr('display', 'inline') recoveredLayer.attr('display', 'inline') } if (geoData.type === 'Feature') { provinceLayer.attr('display', 'none') document.querySelector('.announced').style.opacity = 0.3; document.querySelector('.announced').style.cursor = 'auto'; } else { document.querySelector('.announced').style.opacity = 1; document.querySelector('.announced').style.cursor = 'pointer'; if (chartGraph.showAnnounced) { provinceLayer.attr('display', 'inline') if (provinceData) { provinceLayer.select('path').datum(timeline) .transition().duration(0) .attr('d', provinceArea) } } else { provinceLayer.attr('display', 'none') } } gridLayer.select('.x-axis').transition().duration(500) .attr('transform', `translate(0, ${height-chart.pad.bottom})`) .call(d3.axisBottom(dayScale).ticks(width/100)) gridLayer.select('.y-axis').transition().duration(500) .attr('transform', `translate(${width-chart.pad.right})`) .call(d3.axisRight(yScale) .tickSize(-width + chart.pad.right + chart.pad.left) .tickSizeOuter(0) ) }