//Fetch data const buildOldCharts = async (data) => { async function getData() { //let newProvinceDataRaw = await fetch (`./Data/canada-new.json`); let newProvinceDataRaw = await fetch( `https://beta.ctvnews.ca/content/dam/common/exceltojson/COVID-19-Canada-New.txt` ); let newProvinceData = await newProvinceDataRaw.json(); //console.log(newProvinceData) let provinceData = formatProvinceData(newProvinceData); let newData = formatData(data); let mergedData = [...newData, ...provinceData]; return mergedData; } function formatProvinceData(data) { const provArray = [ { short: "BC", long: "British Columbia" }, { short: "AB", long: "Alberta" }, { short: "SK", long: "Saskatchewan" }, { short: "MB", long: "Manitoba" }, { short: "ON", long: "Ontario" }, { short: "QC", long: "Quebec" }, { short: "NB", long: "New Brunswick" }, { short: "NS", long: "Nova Scotia" }, { short: "PE", long: "Prince Edward Island" }, { short: "NL", long: "Newfoundland and Labrador" }, { short: "YT", long: "Yukon" }, { short: "NT", long: "Northwest Territories" }, { short: "NU", long: "Nunavut" }, ]; let provinceData = []; provArray.forEach((province) => { let obj = { countryShort: province.short, country: province.long, series: [], }; provinceData.push(obj); }); let populationArray = [ { name: "British Columbia", population: "5110917", }, { name: "Alberta", population: "4413146", }, { name: "Saskatchewan", population: "1181666", }, { name: "Manitoba", population: "1377517", }, { name: "Ontario", population: "14711827", }, { name: "Quebec", population: "8537674", }, { name: "New Brunswick", population: "779993", }, { name: "Nova Scotia", population: "977457", }, { name: "Prince Edward Island", population: "158158", }, { name: "Newfoundland and Labrador", population: "521365", }, { name: "Yukon", population: "41078", }, { name: "Northwest Territories", population: "44904", }, { name: "Nunavut", population: "39097", }, { name: "Canada", population: "37894799", }, ]; data.forEach((day, i, nodes) => { if (Number(day["Can_Total"]) && Number(day["Can_Total"]) > 0) { provinceData.forEach((province, i) => { let obj = { date: Number(day.Date), total_cases: 0, new_cases: 0, total_recovered: 0, new_recovered: 0, total_deaths: 0, new_deaths: 0, }; Object.keys(day).forEach((key) => { if (key.split("_")[0] === province.countryShort) { if (key.split("_")[1] === "Total") { obj.total_cases = Number(day[key]) ? Number(day[key]) : 0; } else if (key.split("_")[1] === "Recovered") { obj.total_recovered = Number(day[key]) ? Number(day[key]) : 0; } else if (key.split("_")[1] === "Death") { obj.total_deaths = Number(day[key]) ? Number(day[key]) : 0; } } }); province.series.push(obj); }); } }); provinceData.forEach((province) => { province.series.forEach((day, i, nodes) => { day.new_cases = nodes[i].total_cases - (i > 0 ? nodes[i - 1].total_cases : 0); day.new_recovered = nodes[i].total_recovered - (i > 0 ? nodes[i - 1].total_recovered : 0); day.new_deaths = nodes[i].total_deaths - (i > 0 ? nodes[i - 1].total_deaths : 0); if (day.new_cases < 0) { province.series.splice(i, 1); } }); }); function addRollingAverage(data, index, nodes, target, numDays, newName) { let avg = 0; let daysTallied = 0; for (let i = index; i >= Math.max(0, index - (numDays - 1)); i--) { avg += nodes[i][target]; daysTallied += 1; } data[newName] = Math.round(avg / daysTallied); } provinceData.forEach((province) => { province.series.forEach((day, i, N) => { addRollingAverage(day, i, N, "new_cases", 7, "new_cases_7avg"); addRollingAverage(day, i, N, "new_deaths", 7, "new_deaths_7avg"); addRollingAverage(day, i, N, "new_recovered", 7, "new_recovered_7avg"); }); }); //console.log(provArray) provinceData.forEach((country) => { let popObj = populationArray.find((obj) => obj.name === country.country); if (!popObj) { } else { country.population = Number(popObj.population); } }); provinceData.forEach((country) => { country.series.forEach((day, i, N) => { day.cases = { raw: { total: day.total_cases, new: day.new_cases, avg: day.new_cases_7avg, }, per100k: { total: per100k(day.total_cases, country.population), new: per100k(day.new_cases, country.population), avg: per100k(day.new_cases_7avg, country.population), }, }; (day.deaths = { raw: { total: day.total_deaths, new: day.new_deaths, avg: day.new_deaths_7avg, }, per100k: { total: per100k(day.total_deaths, country.population), new: per100k(day.new_deaths, country.population), avg: per100k(day.new_deaths_7avg, country.population), }, }), (day.recovered = { raw: { total: day.total_recovered, new: day.new_recovered, avg: day.new_recovered_7avg, }, per100k: { total: per100k(day.total_recovered), new: per100k(day.new_recovered), avg: per100k(day.new_recovered_7avg), }, }); }); }); provinceData.forEach((province) => { province.series2021 = province.series.filter( (day, i, arr) => i >= arr.findIndex((d) => d.date === "2021-01-01") ); }); provinceData.forEach((province) => { province.series100 = province.series.filter( (day) => day.total_cases >= 100 ); }); provinceData.forEach((province) => { province.seriesStart = province.series.filter( (day) => day.total_cases >= 1 ); }); return provinceData; } function formatData(data) { let countryArray = []; data.forEach((tally, i) => { Object.keys(tally).forEach((key) => { if (key !== "location" && key !== "date") { 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); } }); //'International' Population from here: https://www.princess.com/news/notices_and_advisories/notices/diamond-princess-update.html countryArray.forEach((country) => { let popObj = populationArray.find((obj) => obj.name === country.country); if (!popObj) { } else { country.population = Number(popObj.population); } }); function addRollingAverage(data, index, nodes, target, numDays, newName) { let avg = 0; let daysTallied = 0; for (let i = index; i >= Math.max(0, index - (numDays - 1)); i--) { avg += nodes[i][target]; daysTallied += 1; } data[newName] = Math.round(avg / daysTallied); } countryArray.forEach((country) => { country.series.forEach((day, i, N) => { addRollingAverage(day, i, N, "new_cases", 7, "new_cases_7avg"); addRollingAverage(day, i, N, "new_deaths", 7, "new_deaths_7avg"); }); }); //console.log(countryArray) countryArray.forEach((country) => { country.series.forEach((day, i, N) => { day.cases = { raw: { total: day.total_cases, new: day.new_cases, avg: day.new_cases_7avg, }, per100k: { total: per100k(day.total_cases, country.population), new: per100k(day.new_cases, country.population), avg: per100k(day.new_cases_7avg, country.population), }, }; day.deaths = { raw: { total: day.total_deaths, new: day.new_deaths, avg: day.new_deaths_7avg, }, per100k: { total: per100k(day.total_deaths, country.population), new: per100k(day.new_deaths, country.population), avg: per100k(day.new_deaths_7avg, country.population), }, }; }); }); countryArray.forEach((country) => { country.series2021 = country.series.filter( (day, i, arr) => i >= arr.findIndex((d) => d.date === "2021-01-01") ); }); countryArray.forEach((country) => { country.series100 = country.series.filter( (day) => day.total_cases >= 100 ); }); countryArray.forEach((country) => { country.seriesStart = country.series.filter( (day) => day.total_cases >= 1 ); }); countryArray = countryArray.filter((country) => country.population); return countryArray; } colorArray = [ "#161515", "#e77e00", "#b11b5c", "#477c13", "#033f77", "#8800cc", "#cc0000", "#178b88", ]; let chartHeight = 450; let chartDot = { big: 3, small: 2, opacity: 1, line: 2, }; function createHundredChart( parent, data, countries, metric, dayCap = null, showDropdown = false, showSettings = true, showSlider = true, showSource = true, options = {} ) { let chartOptions = { showUnder100: false, showZeros: false, showDots: true, }; for (key in options) { chartOptions[key] = options[key]; } if (!chartOptions.showDots) { chartDot.opacity = 0; } let series = "series100"; if (chartOptions.show2021) { series = "series2021"; } else if (chartOptions.showZeros) { series = "series"; } else if (chartOptions.showUnder100) { series = "seriesStart"; } //console.log(chartOptions) let titleHeight = showSettings ? 130 : 80; window.addEventListener( "resize", debounce(() => { updateHundredChart( parent, data, countries, metric, dayCap, showDropdown, showSettings, showSlider, showSource, 0, options ); }) ); countryObjArray = []; countries.forEach((country) => { if (country !== "none") { countryObj = Object.assign( {}, data.find((c) => c.country === country) ); countryObjArray.push(countryObj); } }); let container = d3.select(parent); let barSVG = container .select("svg") .attr("width", "100%") .attr("height", chartHeight) .attr("class", "bar-svg"); barSVG.selectAll("*").remove(); container.selectAll(".chart-options").remove(); container.selectAll(".source-div").remove(); let axisLayer = barSVG.append("g").attr("class", "axis-layer"); let unhoverLayer = barSVG.append("g").attr("class", "unhover-layer"); unhoverLayer .append("rect") .attr("x", 0) .attr("y", 0) .attr("width", "100%") .attr("height", "100%") .attr("fill", "rgba(0,0,0,0)") .on("mouseover", function (d) { unHover(); }); let barLayer = barSVG.append("g").attr("class", "bar-layer"); let countryLayersArray = []; countries.forEach((country, i) => { let countryLayer = barLayer .append("g") .attr("class", `country-${i}-layer`); countryLayersArray.push(countryLayer); }); let dividerLayer = barSVG.append("g").attr("class", "divider-layer"); let titleLayer = barSVG .append("g") .attr("class", "title-layer") .attr("transform", "translate(10,10)"); let buttonLayer = barSVG .append("g") .attr("class", "button-layer") .attr("transform", "translate(10,48)"); let legendLayer = barSVG .append("g") .attr("class", "legend-layer") .attr("transform", `translate(10,${showSettings ? 85 : 47})`); let toolTipLayer = barSVG.append("g").attr("class", "tooltip-layer"); let width = barSVG.node().getBoundingClientRect().width; function unHover() { //console.log('un-hover') countryLayersArray.forEach((clayer) => { clayer.attr("opacity", 1); }); toolTipLayer.selectAll("path").remove(); toolTipLayer.selectAll("text").remove(); legendLayer.selectAll("rect").attr("stroke-width", 0); } ///// Dropdowns if (showDropdown) { let dropDownDiv = container .append("div") .attr("class", "dropdown-div chart-options"); let dropDownTitle = dropDownDiv .append("div") .attr("class", "dropdown-title chart-options-title"); dropDownTitle.text("Choose regions"); function makeDropDown(dropParent, index) { let dropDownContainer = dropParent .append("div") .attr("class", "drop-container"); dropDownContainer.style( "border-bottom", `3px solid ${colorArray[index]}` ); let dropDown = dropDownContainer .append("select") .attr("class", "hundred-dropdown"); //dropDown.append('option').attr('value', 'none').text('-----') data.forEach((country, i) => { if (country[series].length > 0) { let option = dropDown .append("option") .attr("value", country.country) .text(country.country); if (country.country === countries[index]) { option.attr("selected", true); } } }); dropDown.on("change", function (d) { let newCountries = []; this.parentElement.parentElement .querySelectorAll("select") .forEach((dropdown) => { newCountries.push(dropdown.value); //console.log(dropdown.options[dropdown.selectedIndex].value) }); countries = newCountries; createHundredChart( parent, data, countries, metric, dayCap, showDropdown, showSettings, showSlider, showSource ); }); } countries.forEach((country, i) => { makeDropDown(dropDownDiv, i); }); } let titleSize = width > 500 ? 22 : 22; let title = titleLayer .append("text") .attr("x", 0) .attr("y", titleSize) .attr("font-family", `'CTVSans-Bold','CTV Sans',Arial`) .attr("font-size", titleSize) .text(function (d) { return `COVID-19 ${ metric[0] === "cases" ? "Cases" : "Deaths" } (${metric[2] === "avg" ? "7-day avg" : metric[2] === "total" ? "Total" : "Daily"}${metric[1] === "per100k" ? ", per 100K" : ""})`; }); dividerLayer .append("rect") .attr("x", 0) .attr("y", 0) .attr("width", "100%") .attr("height", showSettings ? 84.5 : 46.5) .attr("stroke", "#f3f3f3") .attr("fill", "white"); /////////// Buttons if (showSettings) { let buttonOptions1 = [ ["Cases", "cases"], ["Deaths", "deaths"], ]; let buttonOptions2 = [ ["Total", "total"], ["Daily", "new"], ["7-Day", "avg"], ]; let buttonOptions3 = [ ["Raw", "raw"], ["/100K", "per100k"], ]; let buttonPad = 5; let buttonFont = 12; let buttonGroupSpace = 10; let buttonGroupY = 0; let buttonActive = "#333"; let buttonTextActive = "white"; let buttonSize = buttonFont + 5 + buttonPad * 2; //let shiftLeft = 0; //let shiftDown = 50; //let underline = titleLayer.append('line').attr('x1', 0).attr('y1', shiftDown + 5.5).attr('x2', 80).attr('y2', shiftDown + 5.5).attr('stroke', 'black') let buttonLeft = 1; let buttonGroup1 = buttonLayer.append("g"); buttonOptions1.forEach((button, i) => { let box = buttonGroup1 .append("rect") .attr("x", buttonLeft) .attr("y", buttonGroupY) .attr("height", buttonFont + buttonPad * 2) .attr("shape-rendering", "crispEdges") .style("cursor", "pointer") .on("click", function () { buttonGroup1.selectAll("rect").attr("fill", "#efefef"); buttonGroup1.selectAll("text").attr("fill", "#555"); buttonGroup1.selectAll("rect").attr("stroke", "#ccc"); box.attr("stroke", buttonActive); box.attr("fill", buttonActive); text.attr("fill", buttonTextActive); metric[0] = button[1]; updateHundredChart( parent, data, countries, metric, dayCap, showDropdown, showSettings, showSlider, showSource, 1000, options ); title.text(function (d) { return `COVID-19 ${ metric[0] === "cases" ? "Cases" : "Deaths" } (${metric[2] === "avg" ? "7-day avg" : metric[2] === "total" ? "Total" : "Daily"}${metric[1] === "per100k" ? ", per 100K" : ""})`; }); }); let text = buttonGroup1 .append("text") .attr("font-family", `'CTVSans-Regular','CTV Sans',Arial`) .attr("class", "click-text") .text(button[0]) .attr("y", buttonGroupY + buttonFont + buttonPad / 2) .attr("x", buttonLeft + buttonPad) .style("cursor", "pointer") .attr("font-size", buttonFont) .attr("fill", metric[0] === button[1] ? buttonTextActive : "#333") .attr("pointer-events", "none"); box .attr( "width", buttonPad + text.node().getComputedTextLength() + buttonPad ) .attr("fill", metric[0] === button[1] ? buttonActive : "#efefef") .attr("stroke", metric[0] === button[1] ? buttonActive : "#ccc"); buttonLeft += buttonPad + text.node().getComputedTextLength() + buttonPad + buttonGroupSpace / 2; }); buttonLeft += buttonGroupSpace; let buttonGroup2 = buttonLayer.append("g"); buttonOptions2.forEach((button, i) => { let box = buttonGroup2 .append("rect") .attr("x", buttonLeft) .attr("y", buttonGroupY) .attr("height", buttonFont + buttonPad * 2) .attr("stroke", "#ccc") .attr("shape-rendering", "crispEdges") .style("cursor", "pointer") .on("click", function (d, i, N) { buttonGroup2.selectAll("rect").attr("fill", "#efefef"); buttonGroup2.selectAll("text").attr("fill", "#555"); buttonGroup2.selectAll("rect").attr("stroke", "#ccc"); box.attr("fill", buttonActive); box.attr("stroke", buttonActive); text.attr("fill", buttonTextActive); metric[2] = button[1]; updateHundredChart( parent, data, countries, metric, dayCap, showDropdown, showSettings, showSlider, showSource, 1000, options ); title.text(function (d) { return `COVID-19 ${ metric[0] === "cases" ? "Cases" : "Deaths" } (${metric[2] === "avg" ? "7-day avg" : metric[2] === "total" ? "Total" : "Daily"})`; }); }); let text = buttonGroup2 .append("text") .attr("font-family", `'CTVSans-Regular','CTV Sans',Arial`) .attr("class", "click-text") .text(button[0]) .attr("y", buttonGroupY + buttonFont + buttonPad / 2) .attr("x", buttonLeft + buttonPad) .attr("font-size", buttonFont) .attr("fill", metric[2] === button[1] ? buttonTextActive : "#333") .attr("pointer-events", "none"); box .attr( "width", buttonPad + text.node().getComputedTextLength() + buttonPad ) .attr("fill", metric[2] === button[1] ? buttonActive : "#efefef") .attr("stroke", metric[2] === button[1] ? buttonActive : "#ccc"); buttonLeft += buttonPad + text.node().getComputedTextLength() + buttonPad + buttonGroupSpace / 2; }); buttonLeft += buttonGroupSpace; let buttonGroup3 = buttonLayer.append("g"); buttonOptions3.forEach((button, i) => { let box = buttonGroup3 .append("rect") .attr("x", buttonLeft) .attr("y", buttonGroupY) .attr("height", buttonFont + buttonPad * 2) .attr("shape-rendering", "crispEdges") .style("cursor", "pointer") .on("click", function () { buttonGroup3.selectAll("rect").attr("fill", "#efefef"); buttonGroup3.selectAll("text").attr("fill", "#555"); buttonGroup3.selectAll("rect").attr("stroke", "#ccc"); box.attr("stroke", buttonActive); box.attr("fill", buttonActive); text.attr("fill", buttonTextActive); metric[1] = button[1]; updateHundredChart( parent, data, countries, metric, dayCap, showDropdown, showSettings, showSlider, showSource, 1000, options ); title.text(function (d) { return `COVID-19 ${ metric[0] === "cases" ? "Cases" : "Deaths" } (${metric[2] === "avg" ? "7-day avg" : metric[2] === "total" ? "Total" : "Daily"}${metric[1] === "per100k" ? ", per 100K" : ""})`; }); }); let text = buttonGroup3 .append("text") .attr("font-family", `'CTVSans-Regular','CTV Sans',Arial`) .attr("class", "click-text") .text(button[0]) .attr("y", buttonGroupY + buttonFont + buttonPad / 2) .attr("x", buttonLeft + buttonPad) .style("cursor", "pointer") .attr("font-size", buttonFont) .attr("fill", metric[1] === button[1] ? buttonTextActive : "#333") .attr("pointer-events", "none"); box .attr( "width", buttonPad + text.node().getComputedTextLength() + buttonPad ) .attr("fill", metric[1] === button[1] ? buttonActive : "#efefef") .attr("stroke", metric[1] === button[1] ? buttonActive : "#ccc"); buttonLeft += buttonPad + text.node().getComputedTextLength() + buttonPad + buttonGroupSpace / 2; }); } ///////// Legend let left = 1; let top = 0; let legendFont = width > 500 ? 12 : 10; let legendPad = 1; let legendSpace = 3; let legendLeftPad = width > 500 ? 20 : 20; whiteRect = legendLayer.append("rect"); countryObjArray.forEach((country, i) => { //console.log(country) let backRect = legendLayer.append("rect"); let text = legendLayer .append("text") .text(country.country) .attr("y", top + legendFont + legendPad / 2) .attr("x", left + legendLeftPad + legendSpace) .attr("font-size", legendFont) .attr("fill", "#333333") .attr("font-family", `'CTVSans-Bold','CTV Sans',Arial`) .attr("pointer-events", "none"); backRect .attr("y", top) .attr("x", left) .attr( "width", left + legendLeftPad + legendSpace + text.node().getComputedTextLength() + legendPad * 2 ) .attr("height", legendFont + legendPad * 2) .attr("fill", "white") .attr("stroke-width", 0) .attr("stroke", colorArray[i]) .attr("shape-rendering", "crispEdges") .on("mouseover", function () { this.setAttribute("stroke-width", 1); //this.setAttribute('fill', colorArray[i] + '11') countryLayersArray.forEach((clayer) => { clayer.attr("opacity", 0.1); }); countryLayersArray[i].attr("opacity", 1); }) .on("mouseout", function () { unHover(); }); let line = legendLayer .append("line") .attr("y1", top + (legendFont + legendPad * 2) / 2) .attr("y2", top + (legendFont + legendPad * 2) / 2) .attr("x1", left + legendPad) .attr("x2", left + legendLeftPad - legendSpace) .attr("stroke", colorArray[i]) .attr("stroke-linecap", "round") .attr("stroke-width", chartDot.line) .attr("pointer-events", "none"); let dot = legendLayer .append("circle") .attr("cy", top + (legendFont + legendPad * 2) / 2) .attr( "cx", left + legendPad + (legendLeftPad - legendSpace - legendPad) / 2 ) .attr("r", width > 500 ? chartDot.big : chartDot.small) .attr("opacity", chartDot.opacity) .attr("stroke", "white") .attr("stroke-width", 1) .attr("fill", colorArray[i]) .attr("pointer-events", "none"); top += legendFont + legendPad * 2 + 1; }); whiteRect .attr("x", -10) .attr("y", 0) .attr("width", 11) .attr("height", top) .attr("fill", "white"); //////// Buttons chartHeight = 450; barSVG.attr("height", chartHeight); //change height based on countries let maxDayArray = []; countryObjArray.forEach((obj) => { let length = obj[series].length; maxDayArray.push(length); }); let slideMax = Math.max(...maxDayArray); //console.log(maxDayArray) ////// Slider if (showSlider) { let sliderDiv = container .append("div") .attr("class", "slider-div chart-options"); let sliderTitle = sliderDiv .append("div") .attr("class", "slider-title chart-options-title"); sliderTitle.text("Choose timeline cutoff"); let sliderText = sliderDiv .append("p") .html( `${ dayCap ? dayCap - 1 : slideMax - 1 }/ ${slideMax - 1}` ); let slider = sliderDiv .append("input") .attr("class", "slider") .attr("type", "range") .attr("min", 1) .attr("max", slideMax) .attr("value", dayCap ? dayCap : slideMax) .style("background", function (d) { let value = dayCap ? dayCap : slideMax; let pct = (100 / (slideMax - 1)) * value - 100 / (slideMax - 1); return `linear-gradient(to right, black 0%, black ${pct}%, #bbb ${pct}%, #bbb 100%)`; }) .style("width", "100%") .on("input", function (d) { let pct = (100 / (slideMax - 1)) * this.value - 100 / (slideMax - 1); this.style.background = `linear-gradient(to right, black 0%, black ${pct}%, #bbb ${pct}%, #bbb 100%)`; dayCap = this.value; sliderText.html( `${ this.value - 1 }/ ${slideMax - 1}` ); updateHundredChart( parent, data, countries, metric, dayCap, showDropdown, showSettings, showSlider, showSource, 0, options ); }); } ///////// Source / Credit if (showSource) { let sourceDiv = container.append("div").attr("class", "source-div"); let sourceText = sourceDiv .append("p") .style("font-size", "10px") .html( `Source: via ` ); let sourceImgLink = sourceDiv .append("a") .attr("href", "/health/coronavirus/") .attr("target", "_blank") .attr("class", "source-img"); let sourceImg = sourceImgLink .append("img") .attr( "src", "/polopoly_fs/1.4703529!/httpImage/image.png_gen/derivatives/default/image.png" ); } if (dayCap) { countryObjArray.forEach((obj) => { obj.series100 = obj[series].filter((d, i) => i < dayCap); }); maxDayArray = []; countryObjArray.forEach((obj) => { let length = obj[series].length; maxDayArray.push(length); }); } let maxArray = []; countryObjArray.forEach((obj) => { let max = Math.max.apply( Math, obj[series].map(function (o) { return o[metric[0]][metric[1]][metric[2]]; }) ); maxArray.push(max); }); let max = Math.max(1, Math.max(...maxArray)); var heightScale = d3 .scaleLinear() .domain([0, Math.max(max, 1)]) .range([chartHeight - 50, titleHeight]); let numDays = Math.max(...maxDayArray); let bar = { width: (width - 40) / numDays, pad: 1, }; let xScale = d3 .scaleLinear() .domain([0, numDays - 1]) .range([20, width - 40 - 20]); 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[0]][metric[1]][metric[2]])); //.curve(d3.curveMonotoneX) function chart(fullData, layer, color, delay) { let data = fullData[series]; layer .append("path") .datum(data) .attr("d", line0) .attr("stroke", color) .attr("stroke-linecap", "round") .attr("stroke-width", chartDot.line) .attr("fill", "none") //.transition().duration(1000).delay(delay) .attr("d", line); 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", width > 500 ? chartDot.big : chartDot.small) .attr("opacity", chartDot.opacity) .attr("fill", color) .attr("stroke-width", 1) .attr("stroke", "#FFFFFF") .on("mouseover", (d, i) => { countryLayersArray.forEach((clayer) => { clayer.attr("opacity", 0.1); }); layer.attr("opacity", 1); let tip = toolTipLayer.append("path"); let countryName = toolTipLayer .append("text") .attr("x", xScale(i)) .attr("y", heightScale(d[metric[0]][metric[1]][metric[2]]) - 43) .attr("fill", "#333") .attr("text-anchor", "middle") .attr("font-size", 12) .html( `${fullData.country} (${toDate( d.date )})` ); let numText = toolTipLayer .append("text") .attr("x", xScale(i)) .attr("y", heightScale(d[metric[0]][metric[1]][metric[2]]) - 21) .attr("fill", "#333") .attr("text-anchor", "middle") .attr("font-family", `'CTVSans-Bold','CTV Sans',Arial`) .attr("font-size", 20) .text(decimal(d[metric[0]][metric[1]][metric[2]])); tip .attr("d", (d, i) => { let w = Math.max( numText.node().getComputedTextLength(), countryName.node().getComputedTextLength() ) + 10; let h = 45; let pw = 10; let ph = 8; return `M ${Math.round(-w / 2) - 0.5} ${-h - 0.5} H ${ Math.round(w / 2) + 0.5 } V ${0.5} H ${pw + 0.5} L 0 ${ph + 0.5} L ${ -pw - 0.5 } ${0.5} H ${Math.round(-w / 2) - 0.5} Z`; }) .attr( "transform", `translate(${Math.round(xScale(i))}, ${Math.round( heightScale(d[metric[0]][metric[1]][metric[2]]) - 14 )})` ) .attr("fill", "white") .attr("stroke-width", 1) .attr("stroke", "#333"); }) .on("mouseout", function (d) { unHover(); }) //.transition().duration(1000).delay(delay) .attr("cy", (d, i) => heightScale(d[metric[0]][metric[1]][metric[2]])); } countryObjArray.forEach((country, i) => { chart(country, countryLayersArray[i], colorArray[i], 500 * i); }); let ax = axisLayer .selectAll("text .num") .data(countryObjArray[0].series2021); ax.enter() .append("text") .attr("class", "num") .text((d, i) => { const [year, month, day] = d.date.split("-"); return `${month}/${day}`; }) .attr("y", heightScale(0) + 16) .attr("x", (d, i) => xScale(i)) .attr("text-anchor", "middle") .attr("font-size", 12) .attr("opacity", (d, i, N) => i % Math.round(N.length / 10) === 0 ? 1 : 0 ) .attr("font-family", `'CTVSans-Regular','CTV Sans',Arial`); let titleText = `2021`; axisLayer .append("text") .text(titleText) .attr("class", "since") .attr("x", width / 2) .attr("y", heightScale(0) + 36) .attr("text-anchor", "middle") .attr("font-size", 12) .attr("fill", "#333") .attr("font-family", `'CTVSans-Regular','CTV Sans',Arial`); let axisMarks = []; for (let i = 0; i <= 10; i++) { let top = Math.pow(10, Math.ceil(Math.log10(max / 100))); let topMax = Math.ceil((max / top) * top); axisMarks.push( (i * (topMax / 10)).toLocaleString(undefined, { maximumFractionDigits: 2, }) ); } let axM = axisLayer.selectAll("line").data(axisMarks); axM .enter() .append("line") .attr("x1", 0) .attr("y1", heightScale(0)) .attr("x2", "100%") .attr("y2", heightScale(0)) .attr("stroke-width", (d, i) => (i === 0 ? 2 : 1)) .attr("shape-rendering", "crispEdges") .attr("stroke", (d, i) => (i === 0 ? "#333" : "#f0f0f0")) .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 - 10) .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 / max < 0.025 ? 0 : 1)); let newaxis = axisLayer.append("g").attr("class", "vert"); let av = newaxis.selectAll("line").data(new Array(Math.max(1, numDays))); av.enter() .append("line") .attr("x1", (d, i) => xScale(i)) .attr("y1", heightScale(0) - 1) .attr("x2", (d, i) => xScale(i)) .attr("y2", heightScale(max * 1.2)) .attr("stroke-width", 1) .attr("shape-rendering", "crispEdges") .attr("stroke", "#f0f0f0") .attr("opacity", (d, i, N) => i % Math.round(N.length / 10) === 0 ? 1 : 0 ); } function updateHundredChart( parent, data, countries, metric, dayCap = null, showDropdown = false, showSettings = true, showSlider = true, showSource = true, animateTime, options ) { let chartOptions = { showUnder100: false, showZeros: false, showDots: true, }; for (key in options) { chartOptions[key] = options[key]; } if (!chartOptions.showDots) { chartDot.opacity = 0; } let series = "series100"; if (chartOptions.show2021) { series = "series2021"; } else if (chartOptions.showZeros) { series = "series"; } else if (chartOptions.showUnder100) { series = "seriesStart"; } let titleHeight = showSettings ? 130 : 80; countryObjArray = []; countries.forEach((country) => { if (country !== "none") { countryObj = Object.assign( {}, data.find((c) => c.country === country) ); countryObjArray.push(countryObj); } }); let container = d3.select(parent); let barSVG = container .select("svg") .attr("width", "100%") .attr("height", chartHeight) .attr("class", "bar-svg"); let axisLayer = barSVG.select(".axis-layer"); let barLayer = barSVG.select(".bar-layer"); let countryLayersArray = []; countries.forEach((country, i) => { let countryLayer = barLayer.select(`.country-${i}-layer`); countryLayersArray.push(countryLayer); }); let titleLayer = barSVG.select(".title-layer"); let legendLayer = barSVG.select(".legend-layer"); let toolTipLayer = barSVG.select(".tooltip-layer"); if (dayCap) { countryObjArray.forEach((obj) => { obj[series] = obj[series].filter((d, i) => i < dayCap); }); } function unHover() { countryLayersArray.forEach((clayer) => { clayer.attr("opacity", 1); }); toolTipLayer.selectAll("path").remove(); toolTipLayer.selectAll("text").remove(); legendLayer.selectAll("rect").attr("stroke-width", 0); } let maxArray = []; countryObjArray.forEach((obj) => { let max = Math.max.apply( Math, obj[series].map(function (o) { return o[metric[0]][metric[1]][metric[2]]; }) ); maxArray.push(max); }); let max = Math.max(1, Math.max(...maxArray)); let maxDayArray = []; countryObjArray.forEach((obj) => { let length = obj[series].length; maxDayArray.push(length); }); var heightScale = d3 .scaleLinear() .domain([0, max]) .range([chartHeight - 50, titleHeight]); let width = barSVG.node().getBoundingClientRect().width; let numDays = Math.max(...maxDayArray); let bar = { width: (width - 40) / numDays, pad: 1, }; let xScale = d3 .scaleLinear() .domain([0, numDays - 1]) .range([20, width - 40 - 20]); 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[0]][metric[1]][metric[2]])); //.curve(d3.curveMonotoneX) function chart(fullData, layer, color, delay) { let data = fullData[series]; layer .select("path") .datum(data) .attr("stroke", color) .attr("stroke-linecap", "round") .attr("stroke-width", chartDot.line) .attr("fill", "none") .transition() .duration(animateTime) .delay(delay) .attr("d", line); let c1 = layer.selectAll("circle").data(data); c1.exit().remove(); c1.attr("r", width > 500 ? chartDot.big : chartDot.small) .attr("opacity", chartDot.opacity) .attr("fill", color) .attr("stroke", "#FFFFFF") .on("mouseover", (d, i) => { countryLayersArray.forEach((clayer) => { clayer.attr("opacity", 0.1); }); layer.attr("opacity", 1); let tip = toolTipLayer.append("path"); let countryName = toolTipLayer .append("text") .attr("x", xScale(i)) .attr("y", heightScale(d[metric[0]][metric[1]][metric[2]]) - 43) .attr("fill", "#333") .attr("text-anchor", "middle") .attr("font-size", 12) .html( `${fullData.country} (${toDate( d["date"] )})` ); let numText = toolTipLayer .append("text") .attr("x", xScale(i)) .attr("y", heightScale(d[metric[0]][metric[1]][metric[2]]) - 21) .attr("fill", "#333") .attr("text-anchor", "middle") .attr("font-family", `'CTVSans-Bold','CTV Sans',Arial`) .attr("font-size", 20) .text(decimal(d[metric[0]][metric[1]][metric[2]])); tip .attr("d", (d, i) => { let w = Math.max( numText.node().getComputedTextLength(), countryName.node().getComputedTextLength() ) + 10; let h = 45; let pw = 10; let ph = 8; return `M ${Math.round(-w / 2) - 0.5} ${-h - 0.5} H ${ Math.round(w / 2) + 0.5 } V ${0.5} H ${pw + 0.5} L 0 ${ph + 0.5} L ${ -pw - 0.5 } ${0.5} H ${Math.round(-w / 2) - 0.5} Z`; }) .attr( "transform", `translate(${Math.round(xScale(i))}, ${Math.round( heightScale(d[metric[0]][metric[1]][metric[2]]) - 14 )})` ) .attr("fill", "white") .attr("stroke-width", 1) .attr("stroke", "#333"); }) .on("mouseout", function (d) { unHover(); }) .transition() .duration(animateTime) .delay(delay) .attr("cx", (d, i) => xScale(i)) .attr("cy", (d, i) => heightScale(d[metric[0]][metric[1]][metric[2]])); c1.enter() .append("circle") .attr("opacity", 1) .attr("r", width > 500 ? chartDot.big : chartDot.small) .attr("opacity", chartDot.opacity) .attr("fill", color) .attr("stroke", "#FFFFFF") .on("mouseover", (d, i) => { countryLayersArray.forEach((clayer) => { clayer.attr("opacity", 0.1); }); layer.attr("opacity", 1); let tip = toolTipLayer.append("path"); let countryName = toolTipLayer .append("text") .attr("x", xScale(i)) .attr("y", heightScale(d[metric[0]][metric[1]][metric[2]]) - 43) .attr("fill", "#333") .attr("text-anchor", "middle") .attr("font-size", 12) .html( `${fullData.country} (${toDate( d["date"] )})` ); let numText = toolTipLayer .append("text") .attr("x", xScale(i)) .attr("y", heightScale(d[metric[0]][metric[1]][metric[2]]) - 21) .attr("fill", "#333") .attr("text-anchor", "middle") .attr("font-family", `'CTVSans-Bold','CTV Sans',Arial`) .attr("font-size", 20) .text(decimal(d[metric[0]][metric[1]][metric[2]])); tip .attr("d", (d, i) => { let w = Math.max( numText.node().getComputedTextLength(), countryName.node().getComputedTextLength() ) + 10; let h = 45; let pw = 10; let ph = 8; return `M ${Math.round(-w / 2) - 0.5} ${-h - 0.5} H ${ Math.round(w / 2) + 0.5 } V ${0.5} H ${pw + 0.5} L 0 ${ph + 0.5} L ${ -pw - 0.5 } ${0.5} H ${Math.round(-w / 2) - 0.5} Z`; }) .attr( "transform", `translate(${Math.round(xScale(i))}, ${Math.round( heightScale(d[metric[0]][metric[1]][metric[2]]) - 14 )})` ) .attr("fill", "white") .attr("stroke-width", 1) .attr("stroke", "#333"); }) .on("mouseout", function (d) { unHover(); }) .attr("cx", (d, i) => xScale(i)) .attr("cy", (d, i) => heightScale(d[metric[0]][metric[1]][metric[2]])) .transition() .duration(animateTime) .delay(delay) .attr("opacity", 1); } countryObjArray.forEach((country, i) => { chart(country, countryLayersArray[i], colorArray[i], 0); }); axisLayer .select(".since") .transition() .duration(500) .attr("x", width / 2); let ax = axisLayer.selectAll(".num").data(countryObjArray[0].series2021); ax.exit().remove(); ax.attr("class", "num") .text((d, i) => { const [year, month, day] = d.date.split("-"); return `${month}/${day}`; }) .attr("y", heightScale(0) + 16) .attr("text-anchor", "middle") .attr("font-size", 12) .attr("opacity", (d, i, N) => i % Math.round(N.length / 10) === 0 ? 1 : 0 ) .attr("font-family", `'CTVSans-Regular','CTV Sans',Arial`) .transition() .duration(animateTime) .attr("x", (d, i) => xScale(i)); ax.enter() .append("text") .attr("class", "num") .text((d, i) => i) .attr("y", heightScale(0) + 16) .attr("x", (d, i) => xScale(i)) .attr("text-anchor", "middle") .attr("font-size", 12) .attr("opacity", (d, i, N) => i % Math.round(N.length / 10) === 0 ? 1 : 0 ) .attr("font-family", `'CTVSans-Regular','CTV Sans',Arial`); let axisMarks = []; for (let i = 0; i <= 10; i++) { let top = Math.pow(10, Math.ceil(Math.log10(max / 100))); let topMax = Math.ceil((max / top) * top); axisMarks.push( (i * (topMax / 10)).toLocaleString(undefined, { maximumFractionDigits: 2, }) ); } let axM = axisLayer.selectAll("line").data(axisMarks); axM .attr("x1", 0) .attr("x2", "100%") .attr("stroke-width", (d, i) => (i === 0 ? 2 : 1)) .attr("shape-rendering", "crispEdges") .attr("stroke", (d, i) => (i === 0 ? "#333" : "#f0f0f0")) .attr("opacity", 1) .transition() .duration(animateTime) .attr("y1", (d) => heightScale(d) + 0.5) .attr("y2", (d) => heightScale(d) + 0.5); let axT = axisLayer.selectAll(".marker").data(axisMarks); axT .attr("class", "marker") .attr("x", width - 10) .text((d) => d) .attr("font-size", 12) .attr("fill", "#333333") .attr("font-family", `'CTVSans-Regular','CTV Sans',Arial`) .attr("text-anchor", "end") .attr("opacity", (d, i) => (d / max < 0.025 ? 0 : 1)) .transition() .duration(animateTime) .attr("y", (d) => heightScale(d) - 2); let newaxis = axisLayer.select(".vert"); let av = newaxis.selectAll("line").data(new Array(numDays)); av.exit().remove(); av.attr("x1", (d, i) => xScale(i)) .attr("y1", heightScale(0) - 1) .attr("x2", (d, i) => xScale(i)) .attr("y2", heightScale(max * 1.2)) .attr("stroke-width", 1) .attr("shape-rendering", "crispEdges") .attr("stroke", "#f0f0f0") .attr("opacity", (d, i, N) => i % Math.round(N.length / 10) === 0 ? 1 : 0 ); av.enter() .append("line") .attr("x1", (d, i) => xScale(i)) .attr("y1", heightScale(0)) .attr("x2", (d, i) => xScale(i)) .attr("y2", heightScale(max * 1.2)) .attr("stroke-width", 1) .attr("shape-rendering", "crispEdges") .attr("stroke", "#f0f0f0") .attr("opacity", (d, i, N) => i % Math.round(N.length / 10) === 0 ? 1 : 0 ); } ////// Date Chart end 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, month, day; if (typeof excelDate === "string" && excelDate.includes("-")) { newDate = new Date(excelDate); month = excelDate.split("-")[1]; day = excelDate.split("-")[2]; } else { newDate = new Date((excelDate - (25567 + 1)) * 86400 * 1000); month = newDate.getMonth() + 1; day = newDate.getDate(); } //let newDate = new Date(excelDate); return `${month}/${day}`; } function debounce(func) { var timer; return function (event) { if (timer) clearTimeout(timer); timer = setTimeout(func, 100, event); }; } function decimal(number) { return Math.round((number + Number.EPSILON) * 100) / 100; } function per100k(raw, population) { return raw * (100000 / population); } let newData = await getData(); const hundredChartArray = [ { countries: [ "Brazil", "United States", "European Union", "India", "Canada", ], metric: ["deaths", "per100k", "avg"], cap: null, dropdown: false, showSettings: true, showSlider: false, showSource: true, options: { show2021: true, showUnder100: false, showZeros: false, showDots: false, }, }, ]; hundredChartArray.forEach((chart, i) => { createHundredChart( `#world-chart-${i}`, newData, chart.countries, chart.metric, chart.cap, chart.dropdown, chart.showSettings, chart.showSlider, chart.showSource, chart.options ); }); };