Published
Edited
Oct 23, 2020
1 fork
1 star
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_Counties))

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)
.selectAll("path")
.data(topojson.feature(maryland, maryland.objects.Maryland_Counties).features)
.join("path")
.attr("fill", d => scale(diff[(d.properties.FIPS)]))
.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;
tooltip
.style('top', Math.min((mouseY - 10), root.node().offsetHeight - 205) + 'px')
.style('left', (mouseX + 145 <= root.node().offsetWidth ? (mouseX + 10) : (mouseX - 170)) + '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(({fips}) => fips == d.properties.FIPS).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())
.call(g => g.selectAll(".tick text")
.attr("font-size", "10px"));
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.FIPS)] > 0 ? Math.max(diff[(d.properties.FIPS)], extent[1]/2) : (diff[(d.properties.FIPS)]*2/3 + maxBlue/3)
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
function popAvg (b, index, array) {
let count = 0;
if (index < 6)
return null;
for (let i = 0; i < 7; ++i) {
count += array[index - i].per_100k;
};
return {...b, per_100k: count/7};
};
Insert cell
getTooltipContents = d => {
return html` <div class = "container">
<div class = "county"> ${d.properties.NAME} </div>
<div class = "newCases">
<div class = "desc"> New Cases (7 Day Avg.) </div>
<div class = "cases"> Today: ${avgCases[(d.properties.FIPS)]} </div>
<div class = "twoWeeks"> Two weeks ago: ${twoWeeks[(d.properties.FIPS)]} </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
html`
<style>
.cooltip {
background: white;
border: 1px solid lightgrey;
padding: 10px;
position: absolute;
font-family: Helvetica, Arial, sans-serif;
display: none;
}
.county {
font-weight: 700;
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
scale = d3.scaleDiverging().domain([maxBlue, 0, maxRed]).interpolator(t => (d3.interpolateRdBu(1 - t)))
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
Insert cell
Insert cell
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