//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: ["cases", "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
);
});
};