Published
Edited
Apr 10, 2021
Insert cell
Insert cell
chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);
const tooltip = new Tooltip();
const defs = svg.append("defs");
const pattern = defs.append("pattern")
.attr("id", "dots")
.attr("x", 0)
.attr("y", 0)
.attr("width", 3)
.attr("height", 3)
.attr("patternUnits", "userSpaceOnUse");
pattern.append("circle")
.attr("cx", .5)
.attr("cy", .5)
.attr("r", .5)
.attr("fill", "black");
pattern.append("circle")
.attr("cx", 2)
.attr("cy", 2)
.attr("r", .5)
.attr("fill", "black");
svg.append("g")
.attr("fill", "url(#dots)")
.attr("stroke", "#5D5B5B")
.selectAll("rect")
.data(data)
.join("rect")
.attr("x", d => x(d.date))
.attr("y", d => y(d.duration))
.attr("height", d => y(0) - y(d.duration))
.attr("width", barWidth)
.on("mouseover", (e, d) => tooltip.show(e, d))
.on("mousemove", (e, d) => tooltip.move(e, d))
.on("mouseout", () => tooltip.hide());
svg.append("path")
.datum(rollingAvg(6))
.attr("fill", "none")
.attr("stroke", "#ffffff")
.attr("stroke-width", 6)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("d", line);
svg.append("path")
.datum(rollingAvg(6))
.attr("fill", "none")
.attr("stroke", "#5D5B5B")
.attr("stroke-width", 2)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-dasharray", "2,4")
.attr("d", line);
svg.append("path")
.datum(rollingAvg(12))
.attr("fill", "none")
.attr("stroke", "#ffffff")
.attr("stroke-width", 6)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("d", line);
svg.append("path")
.datum(rollingAvg(12))
.attr("fill", "none")
.attr("stroke", "#5D5B5B")
.attr("stroke-width", 2)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("d", line);
svg.append("g")
.call(xAxis);
svg.append("g")
.call(yAxis);

svg.append(() => tooltip.node);
return svg.node();
}
Insert cell
Insert cell
yAxis = {
return g => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y)
.tickValues(ticks.map(x => x * 60))
.tickFormat((seconds) => seconds / 60))
.call(g => g.select(".domain").remove())
.call(g => g.append("text")
.attr("x", -margin.left)
.attr("y", 10)
.attr("fill", "#5D5B5B")
.attr("text-anchor", "start")
.text("Minutes"))
}
Insert cell
xAxis = g => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).tickFormat((l) => `${formatYear(l)} →`).tickSizeOuter(0))
.selectAll("text")
.style("text-anchor", "start");

Insert cell
line = d3.line()
.curve(d3.curveCardinal)
.defined(d => !isNaN(d.avg))
.x(d => x(d.date) + (barWidth / 2))
.y(d => y(d.avg))
Insert cell
y = {
const max = d3.max(data, d => d.duration);
return d3.scaleLinear()
.domain([0, scaleHours]).nice()
.range([height - margin.bottom, margin.top]);
}
Insert cell
x = d3.scaleTime()
.domain([
data[data.length-1].date,
new Date(data[0].date.getFullYear(), data[0].date.getMonth() + 1, data[0].date.getDate())
])
.range([margin.left, width - margin.left]);
Insert cell
barWidth = (width - margin.left) / data.length - 4
Insert cell
ticks = {
const maxMinutes = scaleHours / 60;
const ticks = [];
for (let i = 1; i * 10 <= maxMinutes; i++) {
ticks.push(i * 10);
}
return ticks;
}
Insert cell
scaleHours = {
const max = d3.max(data, d => d.duration);
return max - max % 3600 + 3600;
}
Insert cell
rollingAvg = (len) => data.map(({ date }, index) => {
let sum;
if (index < data.length - len - 1) {
sum = [...Array(len).keys()].reduce((acc, indexShift) => data[indexShift + index].duration + acc, 0);
}

return {
avg: sum / len,
date
}
})
Insert cell
data = {
const raw = await FileAttachment("activity.csv").csv();
const grouped = byMonth(raw);
return Object.keys(grouped).map((key) => {
const date = new Date(key);
const avg = grouped[key] / new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
return {
date,
duration: Math.round(avg, 0)
}
})
}
Insert cell
Insert cell
toSeconds = (duration) => {
const [hours, minutes, seconds] = duration.split(':');
return Number(hours) * 3600 + Number(minutes) * 60 + Number(seconds);
};
Insert cell
toHours = (duration) => {
const format = (val) => val < 10 ? `0${val}` : val;
const hours = Math.floor(duration / 3600);
const minutes = Math.floor(duration % 3600 / 60);
const seconds = duration % 3600 % 60;
return `${hours}:${format(minutes)}:${format(seconds)}`;
};
Insert cell
formatDate = d3.utcFormat("%b %Y")
Insert cell
formatYear = d3.utcFormat("%Y")
Insert cell
height = 500
Insert cell
margin = ({top: 30, right: 0, bottom: 30, left: 40})
Insert cell
d3 = require("d3@6")
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