Public
Edited
Oct 24, 2023
5 forks
14 stars
Insert cell
Insert cell
chart = {
const svg = d3.create("svg")
.style("display", "block")
.attr("viewBox", [0, 0, width, height]);

const session = svg.append("g")
.attr("fill", "steelblue")
.selectAll("g")
.data(data)
.join("g")
.on("mouseover", (event) => event.currentTarget.style.fill = "red")
.on("mouseout", (event) => event.currentTarget.style.fill = "");

session.selectAll("rect")
.data(dayslice)
.join("rect")
.attr("x", ([start, end]) => x(day(start)))
.attr("width", ([start, end]) => x(day.offset(day(start))) - x(day(start)))
.attr("y", ([start, end]) => y(hours(start)))
.attr("height", ([start, end]) => y(hours(end) || 24) - y(hours(start)));

session.append("title")
.text(([start, end]) => `${start.toLocaleString("en", dateFormat)} →
${end.toLocaleString("en", dateFormat)}`);

svg.append("g")
.attr("pointer-events", "none")
.call(yAxis);

svg.append("g")
.attr("pointer-events", "none")
.call(xAxis);

return svg.node();
}
Insert cell
data = {
const parseTime = d3.timeParse("%Y-%m-%dT%H:%M"); // Local time!
return d3.csvParseRows(await FileAttachment("sleep@3.csv").text(), d => d.map(parseTime));
}
Insert cell
day = d3.timeDay
Insert cell
function dayslice([start, end]) {
let startDay = day(start);
let endDay = day(end);
let slices = [];
while (startDay < endDay) {
startDay = day.offset(startDay);
slices.push([start, startDay]);
start = startDay;
}
if (start < end) {
slices.push([start, end]);
}
return slices;
}
Insert cell
function hours(date) {
return date.getHours()
+ date.getMinutes() / 60
+ date.getSeconds() / (60 * 60)
+ date.getMilliseconds() / (60 * 60 * 1000);
}
Insert cell
xAxis = g => g
.attr("transform", `translate(0,${margin.top})`)
.call(d3.axisTop(x)
.tickFormat(formatDay)
.tickPadding(0))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick text")
.attr("text-anchor", "start")
.attr("x", 6)
.attr("dy", null))
.call(g => g.selectAll(".tick line")
.attr("y1", -margin.top)
.attr("y2", height - margin.top))
Insert cell
yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y)
.ticks(24)
.tickFormat(formatHours)
.tickSize(-width + margin.left + margin.right)
.tickPadding(10))
.call(g => g.selectAll(".domain, .tick:first-of-type, .tick:last-of-type").remove())
.call(g => g.selectAll(".tick line").attr("stroke", "#fff").attr("stroke-width", 0.5));
Insert cell
x = d3.scaleTime()
.domain([
d3.timeDay.floor(d3.min(data, d => d[0])),
d3.timeDay.ceil(d3.max(data, d => d[1]))
])
.rangeRound([margin.left, width - margin.right])
.nice()
Insert cell
y = d3.scaleLinear()
.domain([0, 24])
.rangeRound([margin.top, height - margin.bottom])
Insert cell
formatHours = {
const format = d3.utcFormat("%-I %p");
return hours => format(new Date(hours * 1000 * 60 * 60));
}
Insert cell
formatDay = d3.timeFormat(width < 500 ? "%b" : "%B")
Insert cell
dateFormat = ({month: "short", day: "numeric", hour: "numeric", minute: "numeric"})
Insert cell
height = 600
Insert cell
margin = ({top: 16, right: 0, bottom: 0, left: 40})
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