Published
Edited
Oct 23, 2020
1 fork
Insert cell
Insert cell
chart = {
const width = 975;
const height = 550;

const zoom = d3.zoom()
.scaleExtent([1, 4])
.on("zoom", zoomed);
const root = d3.create("div")
.attr("class", "root");
const svg = root.append("svg")
.attr("viewBox", [0, -20, width, height])
.on("click", reset);

const g = svg.append("g");
const projection = d3.geoAlbersUsa()
.fitSize([width, height], topojson.feature(maryland, maryland.objects.Maryland_Zip_Codes))

const path = d3.geoPath()
.projection(projection);
const tooltip = root
.append("div")
.attr("class", "cooltip");
svg.append("g")
.attr("transform", "translate(20,35)")
.append(() => legend({
color: scale,
// this needs to go from scaleLow to scaleHigh somehow
title: "Change in new cases",
tickFormat: "+r",
width: 300,
ascending: true
}));
g.append("g")
.attr("cursor", "pointer")
.attr("stroke", "grey")
.attr("stroke-width", 0.75)
.attr("fill", "white")
.selectAll("path")
.data(topojson.feature(maryland, maryland.objects.Maryland_Zip_Codes).features)
.join("path")
.attr("fill", d => scale(diff[(d.properties.ZIP_CODE)]))
.on("click", clicked)
.attr("d", path)
.on('mouseover', function (d) {
this.classList.add('hovered')
d3.select(this)
.attr("stroke", "black")
.attr("stroke-width", 3)
.raise()
tooltip.style('display', '');
let node = tooltip.node();
node.innerHTML = "";
node.appendChild(getTooltipContents(d));
renderLineChart(tooltip, d, root);
})
.on('mousemove', function () {
const rootBounds = root.node().getBoundingClientRect();
const mouseX = d3.event.pageX - rootBounds.left;
const mouseY = d3.event.pageY - rootBounds.top;
// console.log(Math.min((mouseY - 10), root.node().offsetHeight - 225) + 'px', mouseY);
// console.log(mouseY - 10, root.node().offsetHeight - 225);
tooltip
.style('top', Math.min((mouseY - 10), root.node().offsetHeight - 225) + 'px')
.style('left', (mouseX + 160 <= root.node().offsetWidth ? (mouseX + 10) : (mouseX - 190)) + 'px')
.style('display', 'block')
})
.on('mouseout', function () {
this.classList.remove('hovered')
d3.select(this)
.attr("stroke", null)
.attr("stroke-width", 1)
.lower()
tooltip.style('display', 'none')
});
svg.call(zoom);

function reset() {
svg.transition().duration(750).call(
zoom.transform,
d3.zoomIdentity,
d3.zoomTransform(svg.node()).invert([width / 2, height / 2])
);
}

function clicked(d) {
const [[x0, y0], [x1, y1]] = path.bounds(d);
d3.event.stopPropagation();
svg.transition().duration(750).call(
zoom.transform,
d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(Math.min(3, 0.9 / Math.max((x1 - x0) / width, (y1 - y0) / height)))
.translate(-(x0 + x1) / 2, -(y0 + y1) / 2),
d3.mouse(svg.node())
);
}

function zoomed() {
const {transform} = d3.event;
g.attr("transform", transform);
g.attr("stroke-width", 1 / transform.k);
}
window.addEventListener("resize", () => resized(width, root, svg));
setTimeout(() => resized(width, root, svg), 12);
return root.node();
}
Insert cell
function resized(width, root, svg) {
let measuredWidth = root.node().getBoundingClientRect().width;
if (measuredWidth == 0) {
setTimeout(() => resized(width, root, svg), 12);
return;
}
let scale = width / measuredWidth;
svg.selectAll("line").attr("stroke-width", scale + "px");
svg.selectAll("text").attr("transform", "scale(" + scale + " " + scale + ")");
}
Insert cell
renderLineChart = (tooltip, d, root) => {
const svg = root.select(".lineChart");
const data = caseTrend.filter(({zip}) => zip == d.properties.ZIP_CODE).map(rollingAvg).filter(d => d != null)
const slice = data.slice(data.length - 14, data.length)
const margin = ({top: 10, right: 0, bottom: 10, left: 20})
const x = d3.scaleUtc()
.domain(d3.extent(slice, d => d.date))
.range([margin.left + 5, lineChartWidth]);
const y = d3.scaleLinear()
.domain([d3.min(slice, d => d.new_cases), d3.max(slice, d => d.new_cases)]).nice()
.range([lineChartHeight - margin.bottom, margin.top]);

const yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.call(
d3
.axisLeft(y)
.tickFormat(t => (t ? t : ""))
.tickSize(0)
//.tickValues(d3.range(d3.min(data, d => d.value), d3.max(data, d => d.value)))
.ticks(2)
)
.call(g => g.select(".domain").remove());
svg.append("g").call(yAxis);
const line = d3.line()
.defined(d => !isNaN(d.new_cases))
.x(d => x(d.date))
.y(d => y(d.new_cases));
const lineFill = diff[(d.properties.ZIP_CODE)] > 0 ? Math.max(diff[(d.properties.ZIP_CODE)], extent[1]/2) : Math.min(diff[(d.properties.ZIP_CODE)], extent[0]/2)
svg.append("path")
.datum(slice)
.attr("fill", "none")
.attr("stroke", scale(lineFill))
.attr("stroke-width", 4)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("d", line);
}
Insert cell
function rollingAvg (b, index, array) {
let count = 0;
if (index < 6)
return null;
for (let i = 0; i < 7; ++i) {
count += array[index - i].new_cases;
};
return {...b, new_cases: count/7};
};
Insert cell
getTooltipContents = d => {
return html` <div class = "container">
<div class = "zipName"> ${d.properties.NAME} </div>
<span class = "zip"> ${d.properties.ZIP_CODE} </span>
<div class = "newCases">
<div class = "desc"> New Cases (7 Day Avg.) </div>
<div class = "cases"> Today: ${avgCases[(d.properties.ZIP_CODE)]} </div>
<div class = "twoWeeks"> Two weeks ago: ${twoWeeks[(d.properties.ZIP_CODE)]} </div>
</div>

<div class = "desc"> 14-Day Trend </div>
<svg class = "lineChart" viewBox = "0 0 ${lineChartWidth} ${lineChartHeight}"> </svg>
</div> `
}
Insert cell
Insert cell
Insert cell
css =

html`
<style>
.root {
position: relative;
}
.cooltip {
background: white;
border: 1px solid lightgrey;
padding: 10px;
position: absolute;
font-family: Helvetica, Arial, sans-serif;
}
.zipName {
font-weight: 700;
font-size: 16px;
}
.zip {
font-weight: 500;
font-size: 16px;
}
.desc {
font-size: 11px;
text-transform: uppercase;
color: grey;
margin-top: 3px;
}
.cases {
margin-top: 2px;
font-size: 14px;
font-weight: 700;
}
.twoWeeks {
font-size: 14px;
}
.lineChart {
float: left;
width:125px;
height:50px;
color: grey;
}
</style>
`
Insert cell
Insert cell
Insert cell
maxRed = Math.max(Math.max(-extent[0], extent[1]), 10)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import {legend} from "@d3/color-legend"
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more