//Fetch data async function getData() { //Fetch Data //let worldDataRaw = await fetch (`./Data/one-hundred.json`); //let covidDataRaw= await fetch (`https://beta.ctvnews.ca/content/dam/common/exceltojson/COVID-19%20Canada.txt`); //let worldData = await worldDataRaw.json(); //let newWorldDataRaw = await fetch (`./Data/world-data.json`); let newWorldDataRaw = await fetch (`https://beta.ctvnews.ca/content/dam/common/exceltojson/full_data.txt`); let newWorldData = await newWorldDataRaw.json(); //console.log(mapData) //console.log(covidData) let newData = await formatData(newWorldData) let metric = 'total_cases' hundredChartArray = [ { country1: 'South Korea', country2: 'United States', metric: metric, cap: null, dropdown: [false, false] }, { country1: 'South Korea', country2: 'Italy', metric: metric, cap: null, dropdown: [false, false] } ] hundredChartArray.forEach((chart, i) => { const myChart = document.querySelector(`#one-hundred-chart-${i}`); observer = new IntersectionObserver((entry, observer) => { if (entry[0].intersectionRatio > 0.5) { createHundredChart(`#one-hundred-chart-${i}`, newData, chart.country1, chart.country2, chart.metric, chart.cap, chart.dropdown) observer.unobserve(myChart); } }, {threshold: [0.5]} ); observer.observe(myChart); }) } async function formatData(data) { let countryArray = []; data.forEach((tally, i) => { Object.keys(tally).forEach(key => { if (key !== "location") { tally[key] = Number(tally[key]) } }) let country = countryArray.find(c => c.country === tally.location); if (country) { country.series.push(tally) } else { let newCountry = { country: tally.location, series: [] } newCountry.series.push(tally) countryArray.push(newCountry) } }) //console.log(countryArray) countryArray.forEach(country => { country.series100 = country.series.filter(day => day.total_cases >= 100) }) return countryArray } ////// Date chart function createHundredChart(parent, data, country1, country2, metric, dayCap=null, dropdown=[false, false]) { //After update function [dropdown1, dropdown2] = dropdown; //todo FIX WRONG COUNTRY SELECTION if (dropdown2) { let dropDiv = make('div', document.querySelector(parent), 'drop-div') let dropBot = make('select', dropDiv, 'hundred-dropdown drop-black'); Object.keys(data[0]).forEach(country => { if (country !== 'Days since 100th case' && (data.filter((thing, i) => thing[country] !== "").length > 7 || country === 'Canada')) { let option = make('option', dropBot) option.value = country; option.innerText = country; if (country === country2) { option.selected = 'selected'; } } }) dropBot.addEventListener('change', () => { //console.log(country1, dropBot.value) createHundredChart(parent, data, country1, dropBot.value) }) } function getCountryArray(country) { let countryArray = [] data.forEach((day, i) => { if (Number(day[country]) !== 0){ countryArray.push({ country: country, total: Number(day[country]), new: i === 0 ? 0 : data[i][country] - data[i-1][country] }) } }) return countryArray; } //console.log(data) let country1obj = Object.assign({}, data.find(c => c.country === country1)); let country2obj = Object.assign({}, data.find(c => c.country === country2)); //console.log(country1obj, country2obj) let container = d3.select(parent); let barSVG = container.select('svg') .attr('width', '100%').attr('class', 'bar-svg') .attr('viewBox', '0 0 400 450')//.attr('preserveAspectRatio', "xMaxYMid meet") barSVG.selectAll('*').remove() let axisLayer = barSVG.append('g').attr('class', 'axis-layer') let barLayer = barSVG.append('g').attr('class', 'bar-layer') let countryLayer2 = barLayer.append('g').attr('class', 'country-2-layer') let countryLayer1 = barLayer.append('g').attr('class', 'country-1-layer') let titleLayer = barSVG.append('g').attr('class', 'title-layer') let toolTipLayer = barSVG.append('g').attr('class', 'tooltip-layer') let countr if (dayCap) { country1obj.series100 = country1obj.series100.filter((d, i) => i < dayCap) country2obj.series100 = country2obj.series100.filter((d, i) => i < dayCap) } let max1 = Math.max.apply(Math, country1obj.series100.map(function(o) { return o[metric]; })); let max2 = Math.max.apply(Math, country2obj.series100.map(function(o) { return o[metric]; })); let max = Math.max(max1, max2) //console.log(max) var heightScale = d3.scaleLinear() .domain([0, max]) .range([400, 110]); let width = 365; let numDays = Math.max(country1obj.series100.length, country2obj.series100.length); let bar = { width: (width - 10)/numDays, pad: 1 } let xScale = d3.scaleLinear() .domain([0, numDays-1]) .range([bar.width/2, width-bar.width/2]) let line0 = d3.line() .x((d, i) => xScale(i)) .y(d=> heightScale(0)) .curve(d3.curveMonotoneX) let line = d3.line() .x((d, i) => xScale(i)) .y(d=> heightScale(d[metric])) .curve(d3.curveMonotoneX) function dots(data, layer, color, delay) { let c1 = layer.selectAll('circle').data(data) c1.enter().append('circle') .attr('cx', (d,i) => xScale(i)) .attr('cy', (d,i) => heightScale(0)) .attr('r', 4) .attr('fill', color) .attr('stroke', '#FFFFFF') .on('mouseover', (d, i) => { layer === countryLayer2 ? countryLayer1.attr('opacity', 0.35) : countryLayer2.attr('opacity', 0.35) toolTipLayer.append('path') .attr('d', (d, i) => { let w = 60; let h = 40; let pw = 10; let ph = 8; return `M ${-w/2} ${-h} H ${w/2} V ${0} H ${pw} L 0 ${ph} L ${-pw} ${0} H ${-w/2} Z` }) .attr('transform', `translate(${xScale(i)}, ${Math.round(heightScale(d[metric]) - 14)})`) .attr('fill', 'white') .attr('stroke-width', 0.5) .attr('stroke', '#333') toolTipLayer.append('text') .attr('x', xScale(i)) .attr('y', heightScale(d[metric]) - 21) .attr('fill', '#333') .attr('text-anchor', 'middle') .attr('font-size', 14) .text(d[metric]) toolTipLayer.append('text') .attr('x', xScale(i)) .attr('y', heightScale(d[metric]) - 38) .attr('fill', '#999') .attr('text-anchor', 'middle') .attr('font-size', 12) .text(toDate(Number(d['date']))) }) .on('mouseout', function(d) { toolTipLayer.selectAll('path').remove() toolTipLayer.selectAll('text').remove() countryLayer1.attr('opacity', 1) countryLayer2.attr('opacity', 1) }) .transition().duration(1000).delay(delay) .attr('cy', (d,i) => heightScale(d[metric])) } countryLayer2 .append('path') .datum(country2obj.series100) .attr('d', line0) .attr('stroke', '#333') .attr('stroke-width', 2) .attr('fill', 'none') .transition().duration(1000).delay(1500) .attr('d', line) dots(country2obj.series100, countryLayer2, '#333', 1500) countryLayer1 .append('path') .datum(country1obj.series100) .attr('d', line0) .attr('stroke', '#F4BB3F') .attr('stroke-width', 2) .attr('fill', 'none') .transition().duration(1000).delay(100) .attr('d', line) dots(country1obj.series100, countryLayer1, '#F4BB3F', 100) let ax = axisLayer.selectAll('text .num').data(country1obj.series100.length > country2obj.series100.length ? country1obj.series100 : country2obj.series100) ax.enter().append('text') .attr('class', 'num') .text((d, i) => i) .attr('y', heightScale(0) + 13) .attr('x', (d,i) => i*bar.width + bar.width*0.5 -1) .attr('text-anchor', 'middle') .attr('font-size', 12) .attr('opacity', (d, i, N) => { if (N.length > 50) { return (i+1) % 5 === 0 ? 1 : 0; } else if (N.length > 20) { return i % 2 === 0 ? 1 : 0; } }) .attr('font-family', `'CTVSans-Regular','CTV Sans',Arial`) axisLayer.append('text').text(`Days since each country's 100th case`) .attr('x', 175) .attr('y', heightScale(0)+30) .attr('text-anchor', 'middle') .attr('font-size', 12) .attr('fill', '#c3c3c3') .attr('font-family', `'CTVSans-Bold','CTV Sans',Arial`) let axisMarks = [] for (let i = 0; i < 5; i++) { let fac = max max > 500 ? axisMarks.push(i*(Math.ceil(max/500)*100)) : axisMarks.push(i*(Math.ceil(max/50)*10)) } let axM = axisLayer.selectAll('line').data(axisMarks); axM.enter().append('line') .attr('x1', 0) .attr('y1', heightScale(0) ) .attr('x2', width + 30) .attr('y2', heightScale(0) ) .attr('stroke-width', 1) .attr('shape-rendering', 'crispEdges') .attr('stroke', (d,i) => i === 0 ? '#333' : '#d3d3d3') .attr('opacity', -1) .attr('y1', d => heightScale(d) + 0.5) .attr('y2', d => heightScale(d) + 0.5) .attr('opacity', 1) let axT = axisLayer.selectAll('text .marker').data(axisMarks); axT.enter().append('text') .attr('class', 'marker') .attr('x', width + 30) .attr('y', d => heightScale(50)) .attr('opacity', -1) .text(d => d) .attr('font-size', 12) .attr('fill', '#333333') .attr('font-family', `'CTVSans-Regular','CTV Sans',Arial`) .attr('text-anchor', 'end') .attr('y', d => heightScale(d) - 2) .attr('opacity', (d, i) => d/Math.max(max1, max2) < 0.025 ? 0 : 1) let topBox = titleLayer.append('rect') let topText = titleLayer.append('text').text(country1) .attr('y', 18) .attr('x', 5) .attr('font-size', 18) .attr('fill', 'white') .attr('font-family', `'CTVSans-Bold','CTV Sans',Arial`) topBox .attr('y', 0) .attr('x', 0) .attr('height', 24) .attr('width', 0) .attr('fill', '#F4BB3F') .attr('shape-rendering', 'crispEdges') .transition().duration(1000).delay(300) .attr('width', topText.node().getComputedTextLength() + 10) let compare = titleLayer.append('text').text('compared to') .attr('y', 18) .attr('x', topText.node().getComputedTextLength() + 10 + 3) .attr('font-size', 16) .attr('fill', '#333') .attr('opacity', 0) .attr('font-family', `'CTVSans-Bold','CTV Sans',Arial`) .transition().duration(700).delay(1050) .attr('opacity', 1) //if (!dropdown2) { let botBox = titleLayer.append('rect') let botText = titleLayer.append('text').text(country2) .attr('y', 18) .attr('x', topText.node().getComputedTextLength() + 10 + 3 + compare.node().getComputedTextLength() + 3 + 5) .attr('font-size', 18) .attr('fill', 'white') .attr('font-family', `'CTVSans-Bold','CTV Sans',Arial`) botBox .attr('y', 0) .attr('x', topText.node().getComputedTextLength() + 10 + 3 + compare.node().getComputedTextLength() + 3) .attr('height', 24) .attr('width', 0) .attr('fill', '#333333') .attr('shape-rendering', 'crispEdges') .transition().duration(1000).delay(1500) .attr('width', botText.node().getComputedTextLength() + 10) //} let underline = titleLayer.append('line').attr('x1', 0).attr('y1', 55.5).attr('x2', 80).attr('y2', 55.5).attr('stroke', 'black') titleLayer.attr('class','click-text').append('text').text('Total Cases').attr('y', 50).attr('x', 0).style('cursor', 'pointer').attr('font-size', 14).on('click', function() { updateHundredChart(parent, data, country1, country2, 'total_cases', dayCap, dropdown) underline.transition().duration(500).attr('x1', 0).attr('x2', 80) }) titleLayer.attr('class','click-text').append('text').text('New Cases').attr('y', 50).attr('x', 100).style('cursor', 'pointer').attr('font-size', 14).on('click', function() { updateHundredChart(parent, data, country1, country2, 'new_cases', dayCap, dropdown) underline.transition().duration(500).attr('x1', 100).attr('x2', 178) }) titleLayer.attr('class','click-text').append('text').text('Total Deaths').attr('y', 50).attr('x', 195).style('cursor', 'pointer').attr('font-size', 14).on('click', function() { updateHundredChart(parent, data, country1, country2, 'total_deaths', dayCap, dropdown) underline.transition().duration(500).attr('x1', 195).attr('x2', 280) }) titleLayer.attr('class','click-text').append('text').text('New Deaths').attr('y', 50).attr('x', 295).style('cursor', 'pointer').attr('font-size', 14).on('click', function() { updateHundredChart(parent, data, country1, country2, 'new_deaths', dayCap, dropdown) underline.transition().duration(500).attr('x1', 295).attr('x2', 380) }) } function updateHundredChart(parent, data, country1, country2, metric, dayCap=null, dropdown=[false, false]) { let country1obj = data.find(c => c.country === country1) let country2obj = data.find(c => c.country === country2) let container = d3.select(parent); let barSVG = container.select('svg') let axisLayer = barSVG.select('.axis-layer') let barLayer = barSVG.select('.bar-layer') let countryLayer2 = barLayer.select('.country-2-layer') let countryLayer1 = barLayer.select('.country-1-layer') let titleLayer = barSVG.select('.title-layer') let toolTipLayer = barSVG.select('.tooltip-layer') let max1 = Math.max.apply(Math, country1obj.series100.map(function(o) { return o[metric]; })); let max2 = Math.max.apply(Math, country2obj.series100.map(function(o) { return o[metric]; })); let max = Math.max(max1, max2) var heightScale = d3.scaleLinear() .domain([0, max]) .range([400, 110]); let width = 365; let numDays = Math.max(country1obj.series100.length, country2obj.series100.length); let bar = { width: (width - 10)/numDays, pad: 1 } let xScale = d3.scaleLinear() .domain([0, numDays-1]) .range([bar.width/2, width-bar.width/2]) let line0 = d3.line() .x((d, i) => xScale(i)) .y(d=> heightScale(0)) .curve(d3.curveMonotoneX) let line = d3.line() .x((d, i) => xScale(i)) .y(d=> heightScale(d[metric])) .curve(d3.curveMonotoneX) function dots(data, layer, color, delay) { let c1 = layer.selectAll('circle').data(data) .attr('cx', (d,i) => xScale(i)) .attr('r', 4) .attr('fill', color) .attr('stroke', '#FFFFFF') .on('mouseover', (d, i) => { layer === countryLayer2 ? countryLayer1.attr('opacity', 0.35) : countryLayer2.attr('opacity', 0.35) toolTipLayer.append('path') .attr('d', (d, i) => { let w = 60; let h = 40; let pw = 10; let ph = 8; return `M ${-w/2} ${-h} H ${w/2} V ${0} H ${pw} L 0 ${ph} L ${-pw} ${0} H ${-w/2} Z` }) .attr('transform', `translate(${xScale(i)}, ${Math.round(heightScale(d[metric]) - 14)})`) .attr('fill', 'white') .attr('stroke-width', 0.5) .attr('stroke', '#333') toolTipLayer.append('text') .attr('x', xScale(i)) .attr('y', heightScale(d[metric]) - 21) .attr('fill', '#333') .attr('text-anchor', 'middle') .attr('font-size', 14) .text(d[metric]) toolTipLayer.append('text') .attr('x', xScale(i)) .attr('y', heightScale(d[metric]) - 38) .attr('fill', '#999') .attr('text-anchor', 'middle') .attr('font-size', 12) .text(toDate(Number(d['date']))) }) .on('mouseout', function(d) { toolTipLayer.selectAll('path').remove() toolTipLayer.selectAll('text').remove() countryLayer1.attr('opacity', 1) countryLayer2.attr('opacity', 1) }) .transition().duration(1000).delay(delay) .attr('cy', (d,i) => heightScale(d[metric])) } countryLayer2.select('path') .datum(country2obj.series100) .attr('stroke', '#333') .attr('stroke-width', 2) .attr('fill', 'none') .transition().duration(1000).delay(0) .attr('d', line) dots(country2obj.series100, countryLayer2, '#333', 0) countryLayer1.select('path') .datum(country1obj.series100) .attr('stroke', '#F4BB3F') .attr('stroke-width', 2) .attr('fill', 'none') .transition().duration(1000).delay(0) .attr('d', line) dots(country1obj.series100, countryLayer1, '#F4BB3F', 0) let ax = axisLayer.selectAll('text').data(country1obj.series100.length > country2obj.series100.length ? country1obj.series100 : country2obj.series100) ax .attr('class', 'num') .text((d, i) => i) .attr('y', heightScale(0) + 13) .attr('x', (d,i) => i*bar.width + bar.width*0.5 -1) .attr('text-anchor', 'middle') .attr('font-size', 12) .attr('opacity', (d, i, N) => { if (N.length > 50) { return (i+1) % 5 === 0 ? 1 : 0; } else if (N.length > 20) { return i % 2 === 0 ? 1 : 0; } }) .attr('font-family', `'CTVSans-Regular','CTV Sans',Arial`) let axisMarks = [] for (let i = 0; i < 5; i++) { max > 500 ? axisMarks.push(i*(Math.ceil(max/500)*100)) : axisMarks.push(i*(Math.ceil(max/50)*10)) } let axM = axisLayer.selectAll('line').data(axisMarks); axM .attr('x1', 0) .attr('x2', width + 30) .attr('stroke-width', 1) .attr('shape-rendering', 'crispEdges') .attr('stroke', (d,i) => i === 0 ? '#333' : '#d3d3d3') .transition().duration(1000) .attr('y1', d => heightScale(d) + 0.5) .attr('y2', d => heightScale(d) + 0.5) let axT = axisLayer.selectAll('.marker').data(axisMarks); axT .attr('x', width + 30) .attr('opacity', 0) .text(d => d) .attr('font-size', 12) .attr('fill', '#333333') .attr('font-family', `'CTVSans-Regular','CTV Sans',Arial`) .attr('text-anchor', 'end') .transition().duration(1000) .attr('opacity', (d, i) => d/Math.max(max1, max2) < 0.025 ? 0 : 1) .attr('y', d => heightScale(d) - 2) } ////// Date Chart end window.onload = function() { getData() } function make(type, parent, CLASS, ID) { let element = document.createElement(type); if (typeof CLASS !== 'undefined') { element.setAttribute('class', CLASS); } if (typeof ID !== 'undefined') { element.setAttribute('id', ID); } return parent.appendChild(element); } function toDate(excelDate) { let newDate = new Date((excelDate - (25567 + 1))*86400*1000); let month = newDate.getMonth() + 1; let day = newDate.getDate() return `${month}/${day}` }