block by dhoboy 7b93645dbf10a1ab41ed7fd4a03ea4a0

Japan COVID19

Full Screen

Stacked Bargraph of Japan COVID19 Case Data. Data as of 3-25-2020.

Data from: https://covid19japan.com/ https://github.com/reustle/covid19japan#data-sources

As published by the Japan Times: https://www.japantimes.co.jp/liveblogs/news/coronavirus-outbreak-updates/

index.js

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

d3.csv("Japan-Data-3-25-2020.csv").then(data => {  
  const statuses = [
    "Unspecified", 
    "Hospitalized", 
    "Recovered", 
    "Discharged", 
    "Deceased",
  ];

  let nestedAgeAndStatus = d3.nest()
    .key(d => d["Age Bracket"])
    .key(d => d["Status"])
    .entries(data.map(d => {
      d['Age Bracket'] = +d['Age Bracket'];
      isNaN(d['Age Bracket']) ? d['Age Bracket'] = "Unspecified" : d['Age Bracket'] = d['Age Bracket'];
      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");

        tooltip.select(".header").text(`Age Range: ${d.data.AgeRange}`);
        tooltip.select(".sub-header").text(`Status`);

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

}); 

index.html

<!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>

styles.css

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

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