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