Stacked Bargraph of Japan COVID19 Case Data.
Data pulling live from: https://github.com/reustle/covid19japan-data https://covid19japan.com/
As published by the Japan Times: https://www.japantimes.co.jp/liveblogs/news/coronavirus-outbreak-updates/
Stacked Bargraph of Japan COVID19 Case Data.
Data pulling live from: https://github.com/reustle/covid19japan-data https://covid19japan.com/
As published by the Japan Times: https://www.japantimes.co.jp/liveblogs/news/coronavirus-outbreak-updates/
<!doctype html> | |
<head> | |
<meta charset="utf-8"> | |
<title>Japan COVID19</title> | |
<link rel="stylesheet" href="./styles.css"> | |
</head> | |
<body> | |
<div id="main"></div> | |
<div id="key"></div> | |
<script src="https://d3js.org/d3.v5.min.js"></script> | |
<script src="./index.js"></script> | |
</body> |
const margin = {top: 30, right: 30, bottom: 50, left: 70}; | |
const width = 1000 - margin.left - margin.right; | |
const height = 500 - margin.top - margin.bottom; | |
const x = d3.scaleBand() | |
.range([0, width]) | |
.padding(0.2) | |
const y = d3.scaleLinear() | |
.range([height, 0]); | |
const yAxis = d3.axisLeft(y) | |
.ticks(10); | |
const xAxis = d3.axisBottom(x); | |
const svg = d3.select("#main").append("svg") | |
.attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`) | |
.attr("width", "100%") | |
.attr("height", "100%") | |
.append("g") | |
.attr("transform", `translate(${margin.left},${margin.top})`); | |
const tooltip = d3.select("body") | |
.append("div") | |
.attr("id", "tooltip") | |
.style("position", "absolute") | |
.style("z-index", "10") | |
.style("visibility", "hidden"); | |
const latest = d3.text("https://raw.githubusercontent.com/reustle/covid19japan-data/master/docs/patient_data/latest.json"); | |
latest.then(fileName => { | |
d3.json(`https://raw.githubusercontent.com/reustle/covid19japan-data/master/docs/patient_data/${fileName}`).then(data => { | |
const statuses = [ | |
"Unspecified", | |
"Hospitalized", | |
"Recovered", | |
"Discharged", | |
"Deceased", | |
]; | |
let nestedAgeAndStatus = d3.nest() | |
.key(d => d.ageBracket) | |
.key(d => d.patientStatus) | |
.entries(data.map(d => { | |
d.ageBracket = +d.ageBracket; | |
if (!d.ageBracket || isNaN(d.ageBracket) || d.ageBracket === -1) { | |
d.ageBracket = "Unspecified" | |
} | |
if (!d.patientStatus) { | |
d.patientStatus = "Unspecified"; | |
} | |
return d; | |
})); | |
nestedAgeAndStatus = nestedAgeAndStatus.map(raw => { | |
let groups = raw.values.map(d => { | |
if (d.key === "") { | |
d.key = "Unspecified"; | |
} | |
return d; | |
}).reduce((prev, next) => { | |
prev[next.key] = { | |
key: next.key, | |
data: next.values, | |
}; | |
return prev; | |
}, {}); | |
return { | |
"AgeRange": raw.key, | |
"Unspecified": groups["Unspecified"] ? groups["Unspecified"].data.length : 0, | |
"Hospitalized": groups["Hospitalized"] ? groups["Hospitalized"].data.length : 0, | |
"Recovered": groups["Recovered"] ? groups["Recovered"].data.length : 0, | |
"Discharged": groups["Discharged"] ? groups["Discharged"].data.length : 0, | |
"Deceased": groups["Deceased"] ? groups["Deceased"].data.length : 0, | |
}; | |
}); | |
const stack = d3.stack() | |
.keys(statuses) | |
.order(d3.stackOrderNone) | |
.offset(d3.stackOffsetNone); | |
const series = stack(nestedAgeAndStatus); | |
x.domain(nestedAgeAndStatus.sort((a, b) => { | |
if (a.AgeRange === "Unspecified") return 1; // push "Unspecified" to the back of the array | |
if (b.AgeRange === "Unspecified") return -1; | |
if (+a.AgeRange < +b.AgeRange) return -1; | |
if (+a.AgeRange > +b.AgeRange) return 1; | |
return 0; | |
}).map(d => d.AgeRange)); | |
y.domain([0, d3.max(nestedAgeAndStatus.map(d => { | |
return Object.keys(d).reduce((prev, next) => { | |
if (next !== "AgeRange") { | |
prev += d[next]; | |
return prev; | |
} | |
return 0; | |
}, 0); | |
}))]); | |
const color = d3.scaleOrdinal() | |
.domain(series.map(d => { return d.key; })) | |
.range(["#737373", "#fed976", "#abdda4", "#2b83ba", "#bd0026"]) | |
.unknown("#ccc") | |
// bars | |
svg.append("g") | |
.selectAll("g") | |
.data(series) | |
.join("g") | |
.attr("fill", d => color(d.key)) | |
.selectAll("rect") | |
.data(d => d) | |
.join("rect") | |
.attr("x", (d, i) => x(d.data.AgeRange)) | |
.attr("y", d => y(d[1])) | |
.attr("height", d => y(d[0]) - y(d[1])) | |
.attr("width", x.bandwidth()) | |
.on("mouseover", d => { | |
tooltip.html(""); | |
tooltip.append("p").attr("class", "header"); | |
tooltip.append("p").attr("class", "sub-header"); | |
tooltip.append("p").attr("class", "body"); | |
if (d.data.AgeRange !== "Unspecified") { | |
tooltip.select(".header").text(`Age Range: ${d.data.AgeRange}-${+d.data.AgeRange + 9}`); | |
} else { | |
tooltip.select(".header").text(`Age Range: Unspecified`); | |
} | |
tooltip.select(".sub-header").text(`Total Cases: ${d.data.Unspecified + d.data.Hospitalized + d.data.Recovered + d.data.Discharged + d.data.Deceased}`); | |
const body = tooltip.select(".body").selectAll('.status') | |
.data(statuses) | |
.enter() | |
.append("div") | |
.attr("class", "status"); | |
body.append("div") | |
.attr("class", "color") | |
.style("background-color", d => color(d)); | |
body.append("div").text(v => `${v}: ${d.data[v]}`); | |
return tooltip.style("visibility", "visible"); | |
}) | |
.on("mousemove", function(d) { | |
let { pageX, pageY } = d3.event; | |
let left = pageX + 10; | |
let top = pageY - 10; | |
if (d.data.AgeRange === "90") { | |
left = pageX - 200; | |
top = pageY - 80; | |
} else if (d.data.AgeRange === "Unspecified") { | |
left = pageX - 250; | |
top = pageY - 80; | |
} | |
return tooltip.style("top", `${top}px`).style("left", `${left}px`); | |
}) | |
.on("mouseout", function() { | |
return tooltip.style("visibility", "hidden"); | |
}); | |
// axes | |
svg.append("g") | |
.attr("class", "x axis") | |
.attr("transform", `translate(0, ${height})`) | |
.call(xAxis) | |
.append("g") | |
.attr("class", "label") | |
.append("text") | |
.attr("transform", `translate(${width}, 0)`) | |
.attr("y", 42) | |
.attr("x", 20) | |
.text("Age Range"); | |
svg.append("g") | |
.attr("class", "y axis") | |
.call(yAxis) | |
.append("g") | |
.attr("class", "label") | |
.append("text") | |
.attr("transform", "rotate(-90)") | |
.attr("y", -46) | |
.attr("x", 10) | |
.text("Confirmed Cases"); | |
// key | |
const key = d3.select("#key").selectAll(".entries") | |
.data(statuses) | |
.enter() | |
.append("div") | |
.attr("class", "entry"); | |
key.append("div") | |
.attr("class", "color") | |
.style("background-color", d => color(d)); | |
key.append("div").text(d => d); | |
}); | |
}); |
.axis text { | |
font-size: 1.2rem; | |
fill: #333; | |
} | |
.axis .label text { | |
text-anchor: end; | |
font-size: 1rem; | |
} | |
#key, #key .entry { | |
display: flex; | |
flex-direction: row; | |
} | |
#key { | |
font-size: 0.8rem; | |
font-family: sans-serif; | |
justify-content: space-around; | |
} | |
#key .entry .color { | |
height: 14px; | |
width: 14px; | |
margin-right: 3px; | |
} | |
#tooltip { | |
background-color: #f7f7f7; | |
padding: 3px 12px; | |
font-size: 1rem; | |
font-family: sans-serif; | |
border: 1px solid #bbbbbb; | |
border-radius: 5px; | |
box-shadow: 1px 1px 4px #bbbbbb; | |
} | |
#tooltip .body .status { | |
display: flex; | |
flex-direction: row; | |
} | |
#tooltip .body .status .color { | |
height: 10px; | |
width: 10px; | |
margin: auto 3px auto 0; | |
} | |
#tooltip p { | |
font-weight: normal; | |
font-family: monospace; | |
margin: 5px 0; | |
} | |
#tooltip p.header { | |
margin-bottom: 10px; | |
} | |
#tooltip p.sub-header { | |
border-bottom: 1px solid; | |
font-weight: bold; | |
} | |
#tooltip p.body { | |
margin-top: -3px; | |
} | |
/* tablet */ | |
@media (min-width: 768px) { | |
#key, .axis text { | |
font-size: 1rem; | |
} | |
.axis .label text { | |
font-size: 0.9rem; | |
} | |
#tooltip { | |
font-size: 1.2rem; | |
} | |
#key .entry .color { | |
height: 16px; | |
width: 16px; | |
margin-right: 5px; | |
} | |
} | |
/* large desktop */ | |
@media (min-width: 1200px) { | |
#key { | |
font-size: 1.2rem; | |
} | |
#tooltip { | |
font-size: 1.4rem; | |
} | |
.axis .label text { | |
font-size: 0.7rem; | |
} | |
#key .entry .color { | |
height: 21px; | |
width: 21px; | |
margin-right: 8px; | |
} | |
} |