function createWorldVaccineApp() {
d3.csv("https://beta.ctvnews.ca/content/dam/common/exceltojson/world_data.txt").then((raw) => {
//d3.csv("https://beta.ctvnews.ca/content/dam/common/exceltojson/world-vaccine-data.txt").then((raw) => {
//d3.csv("https://raw.githubusercontent.com/owid/covid-19-data/master/public/data/owid-covid-data.csv").then((raw) => {
//Format Data
let data = [];
raw.forEach((row) => {
const region = data.find((obj) => obj.region === row.location);
const datum = {
date: row.date,
total: row.total_vaccinations,
first: row.people_vaccinated,
full: row.people_fully_vaccinated,
per_hundred: row.total_vaccinations_per_hundred,
percent_first: row.people_vaccinated_per_hundred,
percent_full: row.people_fully_vaccinated_per_hundred,
};
if (!region) {
const newObj = {
region: row.location,
total: datum.total,
first: datum.first,
full: datum.full,
per_hundred: datum.percent,
percent_first: datum.percent_first,
percent_full: datum.percent_full,
date: "",
data: [datum],
};
data.push(newObj);
} else {
region.data.push(datum);
if (datum.total !== "") region.total = datum.total;
if (datum.first !== "") region.first = datum.first;
if (datum.full !== "") region.full = datum.full;
if (datum.per_hundred !== "") region.per_hundred = datum.per_hundred;
if (datum.percent_first !== "") region.percent_first = datum.percent_first;
if (datum.percent_full !== "") region.percent_full = datum.percent_full;
region.date = datum.date;
}
});
const firstDate = data
.find((country) => country.region === "World")
.data.find((date) => date["per_hundred"] !== "").date;
data.forEach((country) => {
const firstDateIndex = country.data.findIndex((date) => date.date === firstDate);
country.data = country.data.filter((d, i) => i >= firstDateIndex);
country.data.forEach((day, i, arr) => {
day.added = [];
Object.keys(day).forEach((key) => {
if (day[key] === "") {
day[key] = i === 0 ? 0 : arr[i - 1][key];
day.added.push(key);
}
});
});
country.data.max = function (metric) {
return d3.max(this, (d) => +d[metric]);
};
});
data.max = function (metric) {
return d3.max(this, (d) => +d.data.max(metric));
};
data = data.filter((region) => !isNaN(region.per_hundred));
//// TABLES /////////////////////
//
const topTableContainer = d3.select(".top-table-world");
const bottomTableContainer = d3.select(".bottom-table-world");
const chartSVG = d3.select(".chart-world").append("svg").attr("class", "svg-chart");
const worldChart = new Chart(chartSVG, data, "World", "percent_first");
worldChart.create();
window.addEventListener("resize", () => {
worldChart.update();
});
const totalAndNew = (d, i, arr) => {
const num = (+d).toLocaleString(undefined, { maximumFractionDigits: 1 });
const difference = arr[arr.length - 1] - arr[arr.length - 2];
const diff = difference.toLocaleString(undefined, { maximumFractionDigits: 1 });
return `${num}
${difference >= 0 ? "+" : "–"}${diff}
`;
};
const totalAndNewPct = (d, i, arr) => {
const num = d.toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 });
const difference = arr[arr.length - 1] - arr[arr.length - 2];
const diff = difference.toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 });
return `${num}%${difference >= 0 ? "+" : "–"}${diff}%
`;
};
const worldTableTop = new Table(
topTableContainer,
[data.find((row) => row.region === "World")],
[
{
head: "Percentage of population vaccinated (first dose)",
body: "percent_first",
fn: totalAndNewPct,
width: "25%",
},
{
head: "Percentage of population fully vaccinated",
body: "percent_full",
fn: totalAndNewPct,
width: "25%",
},
{ head: "Total doses administered", body: "total", fn: totalAndNew, width: "25%" },
{
head: "Total doses per 100 people",
body: "per_hundred",
fn: totalAndNew,
width: "25%",
},
],
"vaccine-table top",
"World",
"World",
null,
null
);
const skipRank = [
"World",
"European Union",
"South America",
"North America",
"Africa",
"Europe",
"Asia",
"Oceania",
"Upper middle income",
"Lower middle income",
"High income",
"Low income",
];
const worldTableBottom = new Table(
bottomTableContainer,
data,
[
{
head: "Rank",
body: "rank",
width: "6%",
fn: (d, i, arr, row, rank) => {
if (skipRank.includes(row.region)) {
return "—";
}
return "#" + rank;
},
},
{ head: "Country", body: "region", width: "20%", fn: (d) => d },
{
head: "Percentage of population vaccinated (first dose)",
span: 2,
body: "percent_first",
width: "15%",
fn: (d) => ``,
},
{
head: "Percentage of population vaccinated (first dose)",
span: 0,
body: "percent_first",
width: "10%",
fn: (d) => (d === "" ? "—" : (+d).toLocaleString()) + "%",
},
{
head: "Percentage fully vaccinated",
span: 1,
body: "percent_full",
width: "12%",
fn: (d) => (d === "" ? "—" : (+d).toLocaleString()) + "%",
},
{
head: "Total doses administered",
body: "total",
width: "13%",
fn: (d) => (d === "" ? "—" : (+d).toLocaleString()),
},
{
head: "Doses per 100 people",
span: 1,
body: "per_hundred",
width: "10%",
fn: (d) => (d === "" ? "—" : (+d).toLocaleString(undefined, { maximumFractionDigits: 1 })),
},
{
head: "Updated",
body: "date",
width: "9%",
fn: (d) => `${d ? datePadNumToName(d) : "–"}`,
},
],
"vaccine-table bottom",
"World",
"World",
"percent_first",
worldChart
);
worldTableTop.update();
worldTableBottom.sort();
worldTableBottom.update();
//// MAP /////////////////////
//
d3.json("https://beta.ctvnews.ca/content/dam/common/exceltojson/world_countries_large_simple.txt").then((json) => {
//console.log("WORLD MAP", json);
data.forEach((obj) => {
const region = json.features.find((feature) =>
[feature.properties.geounit, feature.properties.name, feature.properties.name_alt].includes(obj.region)
);
if (!region) {
//
//console.log(`${obj.region} not on map`);
} else {
region.data = obj.data;
}
});
d3.select(".top-table-world").selectAll(".loading").remove();
d3.select(".map-world").selectAll(".loading").remove();
d3.select(".bottom-table-world").selectAll(".loading").remove();
let container = d3.select(".map-world");
let width = 750;
let height = 400;
const projection = d3.geoIdentity().reflectY(true).fitSize([width, height], json);
const geoGenerator = d3.geoPath().projection(projection);
const mapSVG = container
.append("svg")
.attr("class", "map-svg")
.attr("width", "100%")
.attr("viewBox", `0 0 ${width} ${height}`);
mapSVG.append("rect").attr("width", "100%").attr("height", "100%").attr("fill", "#ffffff");
const mapLayer = mapSVG.append("g").attr("class", "map-layer");
const labelLayer = mapSVG.append("g").attr("class", "label-layer");
const tooltipLayer = mapSVG.append("g").attr("class", "tooltip-layer");
const tooltip = tooltipLayer.append("g").attr("class", "tooltip");
const tooltipBg = tooltip.append("rect");
const tooltipText = tooltip.append("g").attr("class", "tooltip-text");
const tooltipDate = tooltipText.append("text").attr("class", "date");
const tooltipMain = tooltipText.append("text").attr("class", "main");
const zoomUpdate = (event) => {
let z = event.transform;
mapLayer
.attr("transform", z)
.selectAll("path")
.attr("stroke-width", Math.min(0.75, 1 / (z.k / 2)));
labelLayer
.attr("transform", z)
.selectAll("text")
.attr("opacity", (d) => (geoGenerator.area(d) * z.k > 500 || z.k > 15 ? 0.6 : 0))
.attr("font-size", Math.max(0.5, 10 / z.k));
};
// COUNTRIES
const zoom = d3
.zoom()
.on("zoom", (event) => {
zoomUpdate(event);
})
.scaleExtent([1, 100])
.translateExtent([
[0, 0],
[width, height],
]);
mapSVG.call(zoom);
const worldClick = (d) => {
//console.log(d);
worldChart.region = d.properties.name;
worldChart.update();
worldTableBottom.region = d.properties.name;
worldTableBottom.update();
};
const worldHover = (e, d) => {
if (!d.data) return;
tooltip.attr("opacity", 1);
const pad = 8;
const mainText = Number(d[this.metric]).toLocaleString(undefined, { maximumFractionDigits: 2 });
const filtered = d.data.filter((j) => j.total !== "");
tooltipMain.text((+filtered[filtered.length - 1]["percent_first"] || 0) + "%");
tooltipDate
.text(d.properties.name)
.attr("transform", `translate(0, ${-tooltipMain.node().getBoundingClientRect().height})`);
const tt = tooltipText.node().getBoundingClientRect();
tooltipBg.attr("width", tt.width + pad * 2).attr("height", tt.height + pad);
const ttbg = tooltipBg.node().getBoundingClientRect();
tooltipText.attr("transform", `translate(0, ${-pad})`);
tooltipBg.attr("transform", `translate(${-ttbg.width / 2}, ${-ttbg.height})`);
const t = tooltip.node().getBoundingClientRect();
const dx =
e.offsetX < t.width / 2 ? t.width / 2 : e.offsetX > width - t.width / 2 ? width - t.width / 2 : e.offsetX;
const dy = e.offsetY;
tooltip.attr("transform", `translate(${dx}, ${dy})`);
};
const m = mapLayer.selectAll("path").data(json.features);
m.enter()
.append("path")
.attr("class", (d) => `region ${d.properties.name.split(" ").join("-")}`)
.attr("d", geoGenerator)
.attr("fill", (d, i) => {
if (!d.data) return colorScale(0);
let filtered = d.data.filter((j) => j.total !== "");
if (filtered.length > 0) {
return colorScale(+filtered[filtered.length - 1].percent_first);
} else {
return colorScale(0);
}
})
.attr("stroke", "#fff")
.attr("stroke-width", 0.75)
.on("click", (e, d) => worldClick(d))
.on("mousemove", (e, d) => worldHover(e, d))
.on("mouseout", () => tooltip.attr("opacity", 0));
const adjust = {
Canada: { x: -13, y: 15 },
"United States": { x: 20, y: 13 },
China: { x: 0, y: 5 },
France: { x: 10, y: -8 },
};
const l = labelLayer.selectAll("text").data(json.features);
l.enter()
.append("text")
.attr(
"class",
(d) => `label ${d.properties.name.split(" ").join("-")} ${geoGenerator.centroid(d)[0] ? "" : "hidden"}`
)
.attr("x", (d) => {
const ad = adjust[d.properties.name];
return geoGenerator.centroid(d)[0] + (ad ? ad.x : 0) || 0;
})
.attr("y", (d) => {
const ad = adjust[d.properties.name];
return geoGenerator.centroid(d)[1] + (ad ? ad.y : 0) || 0;
})
.text((d) => d.properties.name)
.attr("font-size", 10)
.attr("opacity", (d) => (geoGenerator.area(d) > 500 || 1 > 15 ? 0.6 : 0))
.style("visibility", "hidden") //Hide for now
.on("click", (e, d) => worldClick(d))
.on("mousemove", (e, d) => worldHover(e, d))
.on("mouseout", () => tooltip.attr("opacity", 0));
// LEGEND
buildLegend(mapSVG, colorScale, 16, 3, 5, maxScalePercent);
});
});
}