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