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); }); }); }