Published
Edited
Sep 14, 2020
Insert cell
Insert cell
d3.csvParse(await FileAttachment("water@3.csv").text(), d3.autoType)
Insert cell
data = {
const text = await FileAttachment("water@3.csv").text();
return d3.csvParse(
text,
({
TimeStamp,
TimeStampPrevious,
BottlesFilled,
BottlesFilledPrevious
}) => ({
currentTime: parseDate(TimeStamp),
currentHour: hourOnly(parseDate(TimeStamp)),
prevTime: parseDate(TimeStampPrevious),
bottles: +BottlesFilled,
prevBottles: +BottlesFilledPrevious,
bottleDifference: +BottlesFilled - +BottlesFilledPrevious,
timeDifferenceHourCount:
d3.timeMinute.count(
parseDate(TimeStampPrevious),
parseDate(TimeStamp)
) / 60.0,
timeDifferenceHours: d3.timeHours(
parseDate(TimeStampPrevious),
parseDate(TimeStamp)
)
})
);
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
md`# Per hour rates`
Insert cell
xhr = d3.scaleTime()
.domain(d3.extent(data, d => d.currentTime))
.range([margin.left, width - margin.right])
Insert cell
yhr = d3.scaleLinear()
.domain([-1, d3.max(data, d => d.bottleDifference / d.timeDifferenceHourCount) + 3])
.range([height - margin.bottom, margin.top])
Insert cell
linehr = d3.line()
.x(d => xhr(d.currentTime))
.y(d => yhr(d.bottleDifference / d.timeDifferenceHourCount))
.curve(d3.curveBasis)
Insert cell
class Tooltiphr {
constructor() {
this._bottles = svg`<text y="-22"></text>`;
this._currentTime = svg`<text y="-12"></text>`;
this.node = svg`<g pointer-events="none" display="none" font-family="sans-serif" font-size="10" text-anchor="middle">
<rect x="-27" width="54" y="-30" height="20" fill="white"></rect>
${this._currentTime}
${this._bottles}
<circle r="2.5"></circle>
</g>`;
}
show(d) {
this.node.removeAttribute("display");
this.node.setAttribute("transform", `translate(${xhr(d.currentTime)},${yhr(d.bottleDifference / d.timeDifferenceHourCount)})`);
this._currentTime.textContent = formatDate(d.currentTime);
this._bottles.textContent = d3.format("g")(d.bottleDifference / d.timeDifferenceHourCount);
}
hide() {
this.node.setAttribute("display", "none");
}
}
Insert cell
linehr(data)
Insert cell
bottlesperhourhchart = {
const tooltiphr = new Tooltiphr();
return html`<svg viewBox="0 0 ${width} ${height}">
<path d="${linehr(
data
)}" fill="none" stroke="steelblue" stroke-width="1.5" stroke-miterlimit="1"></path>
${d3
.select(svg`<g>`)
.call(xAxishr)
.node()}
${d3
.select(svg`<g>`)
.call(yAxishr)
.node()}
<g fill="none" pointer-events="all">
${d3.pairs(data, (a, b) =>
Object.assign(
svg`<rect x="${xhr(a.currentTime)}" height="${height}" width="${xhr(
b.currentTime
) - xhr(a.currentTime)}"></rect>`,
{
onmouseover: () => tooltiphr.show(a),
onmouseout: () => tooltiphr.hide()
}
)
)}
</g>
${tooltiphr.node}
</svg>`;
}
Insert cell
md`# Hour visualisation`
Insert cell
function hourOnly(dt) {
dt.setFullYear(2020, 11, 3);
return dt;
}
Insert cell
Insert cell
chart = {
replay;

const svg = d3.select(DOM.svg(width, heighthv));

const l = length(linehv(data));

const gradient = DOM.uid();

svg
.append("linearGradient")
.attr("id", gradient.id)
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", 0)
.attr("y1", heighthv - margin.bottom)
.attr("x2", 0)
.attr("y2", margin.top)
.selectAll("stop")
.data(d3.ticks(0, 1, 10))
.join("stop")
.attr("offset", d => d)
.attr("stop-color", color.interpolator());

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

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

svg
.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", gradient)
.attr("stroke-width", 2.5)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-dasharray", `0,${l}`)
.attr("d", linehv)
.transition()
.duration(20000)
.ease(d3.easeLinear)
.attr("stroke-dasharray", `${l},${l}`);

svg
.append("g")
.attr("fill", "white")
.attr("stroke", "black")
.attr("stroke-width", 2)
.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", d => xhv(d.currentHour))
.attr("cy", d => yhv(d.bottleDifference / d.timeDifferenceHourCount))
.attr("r", 3);

const label = svg
.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll("g")
.data(data)
.join("g")
.attr(
"transform",
d =>
`translate(${xhv(d.currentHour)},${yhv(
d.bottleDifference / d.timeDifferenceHourCount
)})`
)
.attr("opacity", 0);

label
.append("text")
.text(d => d3.timeFormat("%d %b")(d.currentTime))
.call(halo);

var transitions = 97;

label
.transition()
.delay((d, i) => (length(line(data.slice(0, i + 1))) / l) * 800000)
.attr("opacity", 1)
.on('end', () => {
transitions--;
if (transitions == 0) {
label
.transition()
.delay((d, i) => (length(line(data.slice(0, i + 1))) / l) * 50000)
.attr("opacity", 0);
}
});

return svg.node();
}
Insert cell
function sleeper(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
Insert cell
data
Insert cell
linehv = d3
.line()
.curve(d3.curveCatmullRom)
.x(d => xhv(d.currentHour))
.y(d => yhv(d.bottleDifference / d.timeDifferenceHourCount))
Insert cell
function length(path) {
return d3.create("svg:path").attr("d", path).node().getTotalLength();
}
Insert cell
function halo(text) {
text.select(function() { return this.parentNode.insertBefore(this.cloneNode(true), this); })
.attr("fill", "none")
.attr("stroke", "white")
.attr("stroke-width", 4)
.attr("stroke-linejoin", "round");
}
Insert cell
yAxishv = g =>
g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(yhv).ticks(null, ".2f"))
.call(g => g.select(".domain").remove())
.call(g =>
g
.selectAll(".tick line")
.clone()
.attr("x2", width)
.attr("stroke-opacity", 0.1)
)
.call(g =>
g
.select(".tick:last-of-type text")
.clone()
.attr("x", 4)
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.attr("fill", "black")
.text("Bottles per hour")
.style("font-size", "18px")
.call(halo)
)
Insert cell
xAxishv = g =>
g
.attr("transform", `translate(0,${heighthv - margin.bottom})`)
.call(
d3
.axisBottom(xhv)
.ticks(width / 48)
.tickFormat(d3.timeFormat('%H'))
)
.call(g => g.select(".domain").remove())
.call(g =>
g
.selectAll(".tick line")
.clone()
.attr("y2", -heighthv)
.attr("stroke-opacity", 0.1)
)
.call(g =>
g
.append("text")
.attr("x", width - 4)
.attr("y", -4)
.attr("font-weight", "bold")
.attr("text-anchor", "end")
.attr("fill", "black")
.text("Hour of day")
.style("font-size", "18px")
.call(halo)
)
Insert cell
yhv = d3
.scaleLinear()
.domain(d3.extent(data, d => d.bottleDifference / d.timeDifferenceHourCount))
.nice()
.range([heighthv - margin.bottom, margin.top])
Insert cell
xhv = d3
.scaleTime()
.domain([d3.min(data, d => d.currentHour), d3.max(data, d => d.currentHour)])
.nice()
.range([margin.left, width - margin.right])
Insert cell
xhv(data[0].currentTime)
Insert cell
heighthv = 640
Insert cell
md`# Appendix`
Insert cell
d3 = require("d3@5")
Insert cell
xAxis = g => g
.attr("transform", `translate(0,${height - margin.bottom - 5})`)
.call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0))
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.5em")
.attr("dy", "-.1em")
.attr("transform", "rotate(-40)")
Insert cell
xAxishr = g => g
.attr("transform", `translate(0,${height - margin.bottom - 5})`)
.call(d3.axisBottom(xhr).ticks(width / 80).tickSizeOuter(0))
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.5em")
.attr("dy", "-.1em")
.attr("transform", "rotate(-40)")
Insert cell
margin = ({top: 20, right: 30, bottom: 30, left: 40})
Insert cell
height = 240
Insert cell
parseDate = d3.timeParse("%d %B %Y %I:%M%p");
Insert cell
formatDate = d3.utcFormat("%I%p %b %-d, ’%y")
Insert cell
formatDateAxis = d3.utcFormat("%d %B")
Insert cell
formatBottles = d3.format("g")
Insert cell
yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y).ticks(height / 40))
.call(g => g.select(".domain").remove())
Insert cell
yAxishr = g => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(yhr).ticks(height / 40))
.call(g => g.select(".domain").remove())
Insert cell
color = d3.scaleSequential(xhr.domain(), d3.interpolateTurbo)
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