Public
Edited
Apr 19, 2024
Insert cell
Insert cell
raw = d3.tsvParse(`at remaining arrival miles startNote endNote
15:47 3:27 7:13 219 At 3:47 PM, we were supposed to get home… …at 7:13 PM
16:12 4:12 8:24 208
16:50 4:19 9:09 206
17:00 3:50 8:50 217 Diverted off highway to side roads
17:11 3:46 8:56 212
17:25 3:37 9:01 204
17:32 3:32 9:03 199
17:38 3:26 9:04 197
17:38:30 3:29 9:07 196
17:50 3:21 9:11 192
18:02 3:19 9:20 189
18:06 3:17 9:23 188
18:22 3:06 9:29 171
18:36 2:56 9:32 166
19:11 2:31 9:41 147
19:19 2:30 9:50 142
19:28 2:28 9:56 139
19:33 2:27 10:00 139
19:49 2:27 10:16 136
20:09 2:18 10:26 134
20:13 2:03 10:17 133
20:16 2:17 10:33 133
20:29 2:09 10:38 131
20:31 2:08 10:39 131 Got through backed-up rural stoplight
20:33 2:06 10:39 130
20:34 2:07 10:41 130
22:07 39 10:46 39
22:48 0 10:48 0 Got home at 10:48 PM`)
Insert cell
// I transcribed “at” from my 24hr phone clock and “arrival” from Claire’s 12hr Google Maps app… so sue me…
data = raw
.map(({ at, remaining, arrival, miles, ...rest }) => {
const [a, b] = remaining.split(":").map((d) => +d);
return {
...rest,
at: d3.timeParse("%Y-%m-%d %H:%M:%S")(`2024-04-08 ${at}${at.length === 5 ? ":00" : ""}`),
remaining: b !== undefined ? a * 60 + b : a,
arrival: d3.timeParse("%Y-%m-%d %I:%M %p")(`2024-04-08 ${arrival} PM`),
miles: +miles
};
})
.map((d, i, arr) => {
if (!i) return {...d, slope: 0};
const last = arr[i - 1];
// minutes per minute
const slope = (d.arrival - last.arrival) / (d.at - last.at);
return {...d, slope};
})
Insert cell
Plot.plot({
x: { type: "time" },
y: { label: "Miles remaining" },
color: {legend: true, label: "Estimated arrival time"},
width,
marks: [
Plot.arrow(data, { x1: "at", y1: "miles", x2: "arrival", y2: () => 0, stroke: "arrival" }),
Plot.dot([data.at(-1)], {x: "at", y: () => 0, fill: "black"})
]
})
Insert cell
Plot.plot({
marginLeft: 50,
insetTop: 40,
insetRight: 15,
x: { type: "time" },
y: { label: "Miles remaining", labelAnchor: "center" },
color: {legend: true, label: "Minutes lost per minute", type: "diverging-symlog", scheme: "RdYlGn", reverse: true},
width,
marks: [
Plot.arrow(data.slice(1), { x1: "at", y1: "miles", x2: "arrival", y2: () => 0, stroke: "slope" }),
Plot.arrow(data.slice(0, 1), { x1: "at", y1: "miles", x2: "arrival", y2: () => 0, stroke: "black" }),
Plot.line(data, {x: "at", y: "miles", stroke: "#ccc"}),
Plot.dot([data.at(-1)], {x: "at", y: () => 0, fill: "black"}),
Plot.text(data, {x: "at", y: "miles", text: "startNote", lineWidth: "8", dy: -20}),
Plot.text(data, {x: "arrival", y: () => 0, text: "endNote", lineWidth: "8", dy: -20, dx: 5})
]
})
Insert cell
Plot.plot({
insetLeft: 20,
x: {type: "time", axis: "top", labelAnchor: "left", label: "Time"},
y: {type: "time", axis: "right", labelAnchor: "bottom", label: "Est. arrival", ticks: 3},
aspectRatio: 1,
marks: [
Plot.line(data, { x: "at", y: "arrival" }),
Plot.dot([data.at(-1)], {x: "at", y: "arrival", fill: "black"})
]
})
Insert cell
Plot.plot({
insetLeft: 20,
x: {type: "time", axis: "top", labelAnchor: "left", label: "Time"},
y: {type: "time", axis: "right", labelAnchor: "bottom", label: "Est. arrival", ticks: 3},
color: {legend: true, label: "Miles remaining"},
aspectRatio: 1,
marks: [
Plot.line(data, { x: "at", y: "arrival", stroke: "miles", z: null }),
Plot.dot([data.at(-1)], {x: "at", y: "arrival", fill: "black"})
]
})
Insert cell
Plot.plot({
insetLeft: 30,
width: 300,
x: {type: "time", label: "Est. arrival", labelAnchor: "left", tickFormat: "%-I %p"},
y: {type: "time", reverse: true, axis: "right", ticks: 5, label: "Time", tickFormat: "%-I %p"},
color: {legend: true, label: "Miles remaining"},
aspectRatio: 1,
marks: [
Plot.line(data, { x: "arrival", y: "at", stroke: "miles", z: null }),
Plot.dot([data.at(-1)], {x: "arrival", y: "at", fill: "black"})
]
})
Insert cell
Insert cell
Plot.plot({
marginRight: 60,
x: { type: "time", label: null },
y: {
type: "utc",
label: "Time remaining",
tickFormat: "%-H:%M"
},
aspectRatio: 1,
marks: [
Plot.link(data, {x1: "at", y1: remainingTime, x2: d => d3.utcMinute.offset(d.at, d.remaining), y2: () => 0, stroke: "#eee"}),
Plot.ruleY([0]),
Plot.lineY(data, { x: "at", y: remainingTime }),
Plot.dot(data.slice(-1), { x: "at", y: remainingTime, fill: "black" }),
Plot.text(data, {
x: "at",
y: remainingTime,
lineWidth: 8,
textAnchor: "start",
dx: 5,
text: (d) => `${d.startNote}${d.endNote}`.replaceAll("……", " ")
})
]
})
Insert cell
remainingTime = d => new Date(d.remaining * 60000)
Insert cell
Plot.plot({
x: { type: "time", label: null, domain: d3.extent(data, d => d.at) },
y: {
type: "utc",
label: "Time remaining",
tickFormat: "%-H:%M",
domain: d3.extent(data, remainingTime)
},
aspectRatio: 1,
clip: true,
insetRight: 4,
insetTop: 4,
marks: [
Plot.link(remainingTicks, {
x1: () => startTime,
y1: (d) => d,
x2: (d) => new Date((+startTime) + (+d)),
y2: () => 0,
stroke: "#eee"
}),
Plot.ruleY([0]),
Plot.lineY(data, { x: "at", y: remainingTime }),
Plot.dot(data.slice(-1), { x: "at", y: remainingTime, fill: "black" }),
Plot.text(data, {
x: "at",
y: remainingTime,
lineWidth: 8,
textAnchor: "start",
dx: 5,
text: (d) => `${d.startNote}${d.endNote}`.replaceAll("……", " "),
fill: "black",
stroke: "white"
}),
Plot.text(data.slice(-1), {
x: "at",
y: remainingTime,
lineWidth: 8,
textAnchor: "end",
// dx: 5,
dy: -15,
text: (d) => `${d.startNote}${d.endNote}`.replaceAll("……", " "),
fill: "black",
stroke: "white"
})
]
})
Insert cell
startTime = d3.min(data, d => d.at)
Insert cell
remainingTicks = d3.utcMinute.every(30).range(new Date(0), new Date(14 * 60 * 60 * 1000))
Insert cell
Plot.plot({
x: { type: "time", label: null, domain: d3.extent(data, d => d.at) },
y: {
type: "utc",
label: "Time remaining",
tickFormat: "%-H:%M",
domain: d3.extent(data, remainingTime)
},
aspectRatio: 1,
clip: true,
insetRight: 4,
insetTop: 4,
marks: [
Plot.areaY(data, { x: "at", y: remainingTime, opacity: 0.1 }),
Plot.link(remainingTicks, {
x1: () => startTime,
y1: (d) => d,
x2: (d) => new Date((+startTime) + (+d)),
y2: () => 0,
stroke: "#fff",
mixBlendMode: "darken"
}),
Plot.ruleY([0]),
Plot.lineY(data, { x: "at", y: remainingTime }),
Plot.dot(data.slice(-1), { x: "at", y: remainingTime, fill: "black" }),
Plot.text(data, {
x: "at",
y: remainingTime,
lineWidth: 8,
textAnchor: "start",
dx: 5,
text: (d) => `${d.startNote}${d.endNote}`.replaceAll("……", " "),
fill: "black",
stroke: "white"
}),
Plot.text(data.slice(-1), {
x: "at",
y: remainingTime,
lineWidth: 8,
textAnchor: "end",
// dx: 5,
dy: -15,
text: (d) => `${d.startNote}${d.endNote}`.replaceAll("……", " "),
fill: "black",
stroke: "white"
})
]
})
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