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