Published
Edited
Apr 12, 2020
20 stars
Insert cell
Insert cell
chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("font-family", "sans-serif")
.attr("font-size", 10);

svg.append("g")
.attr("stroke", "currentColor")
.attr("stroke-width", 2)
.selectAll("line")
.data(data.filter(d => d.start !== null))
.join("line")
.attr("x1", d => x(d.start))
.attr("x2", x(today))
.attr("y1", d => y(d.name))
.attr("y2", d => y(d.name));

svg.append("g")
.call(xAxis);

svg.append("g")
.attr("text-anchor", "end")
.selectAll("text")
.data(data)
.join("text")
.attr("x", d => x(d.start || today) - 6)
.attr("y", d => y(d.name))
.attr("dy", "0.35em")
.text(d => d.name)
.filter(d => d.start)
.append("title")
.text(d => `${d.name}
${d3.utcFormat("%B %-d, %-I:%M %p")(d.start)}`);

svg.append("g")
.selectAll("text")
.data(data)
.join("text")
.attr("x", width - margin.right + 6)
.attr("y", d => y(d.name))
.attr("dy", "0.35em")
.text(d => {
if (d.start === null) return d.partial ? "Partial" : "None";
const day = Math.round((today - d.start) / (1000 * 60 * 60 * 24));
return `${day} day${day === 1 ? "" : "s"}`;
});

svg.append("g")
.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", d => d.start ? x(d.start) : x(today))
.attr("cy", d => y(d.name))
.attr("r", 2)
.attr("fill", d => d.start ? "currentColor" : "none")
.attr("stroke", d => d.start ? "none" : "currentColor");

return svg.node();
}
Insert cell
today = new Date(Date.UTC(2020, 3, 12))
Insert cell
data = [
{name: "Alabama", date: "April 4 at 5 p.m."},
{name: "Alaska", date: "March 28 at 5 p.m."},
{name: "Arizona", date: "March 31 at 5 p.m."},
{name: "Arkansas", date: null},
{name: "California", date: "March 19"},
{name: "Colorado", date: "March 26 at 6 a.m."},
{name: "Connecticut", date: "March 23 at 8 p.m."},
{name: "Delaware", date: "March 24 at 8 a.m."},
{name: "District of Columbia", date: "April 1 at 12:01 a.m."},
{name: "Florida", date: "April 3 at 12:01 a.m."},
{name: "Georgia", date: "April 3"},
{name: "Hawaii", date: "March 25 at 12:01 a.m."},
{name: "Idaho", date: "March 25 at 1:30 p.m."},
{name: "Illinois", date: "March 21 at 5 p.m."},
{name: "Indiana", date: "March 24 at 11:59 p.m."},
{name: "Iowa", date: null},
{name: "Kansas", date: "March 30 at 12:01 a.m."},
{name: "Kentucky", date: "March 26 at 8 p.m."},
{name: "Louisiana", date: "March 23 at 5 p.m."},
{name: "Maine", date: "April 2 at 12:01 a.m."},
{name: "Maryland", date: "March 30 at 8 p.m."},
{name: "Massachusetts", date: "March 24 at 12 p.m."},
{name: "Michigan", date: "March 24 at 12:01 a.m."},
{name: "Minnesota", date: "March 27 at 11:59 p.m."},
{name: "Mississippi", date: "April 3 at 5 p.m."},
{name: "Missouri", date: "April 6 at 12:01 a.m."},
{name: "Montana", date: "March 28 at 12:01 a.m."},
{name: "Nebraska", date: null},
{name: "Nevada", date: "April 1"},
{name: "New Hampshire", date: "March 27 at 11:59 p.m."},
{name: "New Jersey", date: "March 21 at 9 p.m."},
{name: "New Mexico", date: "March 24 at 8 a.m."},
{name: "New York", date: "March 22 at 8 p.m."},
{name: "North Carolina", date: "March 30 at 5 p.m."},
{name: "North Dakota", date: null},
{name: "Ohio", date: "March 23 at 11:59 p.m."},
{name: "Oklahoma", date: null, partial: true},
{name: "Oregon", date: "March 23"},
{name: "Pennsylvania", date: "April 1 at 8 p.m."},
{name: "Rhode Island", date: "March 28"},
{name: "South Carolina", date: "April 7 at 5 p.m."},
{name: "South Dakota", date: null},
{name: "Tennessee", date: "March 31 at 11:59 p.m."},
{name: "Texas", date: "April 2 at 12:01 a.m."},
{name: "Utah", date: null, partial: true},
{name: "Vermont", date: "March 25 at 5 p.m."},
{name: "Virginia", date: "March 30"},
{name: "Washington", date: "March 23"},
{name: "West Virginia", date: "March 24 at 8 p.m."},
{name: "Wisconsin", date: "March 25 at 8 a.m."},
{name: "Wyoming", date: null, partial: true}
]
.map(({date, ...rest}) => ({...rest, start: parseDate(date)}))
.sort((a, b) => d3.ascending(a.start === null, b.start === null) || d3.descending(!!a.partial, !!b.partial) || d3.ascending(a.start, b.start))
Insert cell
parseDate = {
const format1 = d3.utcParse("%B %d");
const format2a = d3.utcParse("%B %d at %I p.m.");
const format2b = d3.utcParse("%B %d at %I:%M p.m.");
const format3a = d3.utcParse("%B %d at %I a.m.");
const format3b = d3.utcParse("%B %d at %I:%M a.m.");
return x => {
let date = null;
if (date = format1(x));
else if (date = format2a(x) || format2b(x)) date.setUTCHours(date.getUTCHours() + 12);
else if (date = format3a(x) || format3b(x));
if (date !== null) date.setUTCFullYear(2020);
return date;
};
}
Insert cell
x = d3.scaleUtc()
.domain([d3.min(data, d => d.start), today])
.rangeRound([margin.left, width - margin.right])
Insert cell
y = d3.scalePoint()
.domain(data.map(d => d.name))
.rangeRound([margin.top, height - margin.bottom])
.padding(1)
Insert cell
xAxis = g => g
.attr("transform", `translate(0,${margin.top})`)
.call(d3.axisTop(x).ticks(width / 80))
.call(g => g.select(".domain").remove())
.call(g => g.append("g")
.attr("stroke", "white")
.attr("stroke-width", 2)
.selectAll("line")
.data(x.ticks(d3.utcDay))
.join("line")
.attr("x1", d => 0.5 + x(d))
.attr("x2", d => 0.5 + x(d))
.attr("y2", height - margin.bottom - margin.top))
Insert cell
margin = ({top: 24, right: 41, bottom: 0, left: 60})
Insert cell
height = data.length * 14
Insert cell
d3 = require("d3@5")
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more