Public
Edited
Aug 8, 2023
1 fork
9 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
journey = pipe(
[
{
start: {
airport: "PDX",
time: "2023-09-17T06:25-07:00"
},
end: { airport: "YVR", time: "2023-09-17T07:40-07:00" }
},
{
start: { airport: "YVR", time: "2023-09-17T13:35-07:00" },
end: { airport: "YYZ", time: "2023-09-17T20:57-04:00" }
},
{
start: { airport: "YYZ", time: "2023-09-17T23:00-04:00" },
end: { airport: "KEF", time: "2023-09-18T08:35-00:00" }
}
],
($) =>
$.map((d) => ({
...d,
start: inputTransform(d.start),
end: inputTransform(d.end)
}))
)
Insert cell
midpoint = pipe(
[journey[0].start.airport, ...journey.map((d) => d.end.airport)],
// this should probably be something more clever that deals with spherical geometry, like d3.geoCentroid
// but that involves turning my series of points into a geo json object, and I don't know how to do that
($) => ({
longitude: d3.extent($, (d) => d.longitude),
latitude: d3.extent($, (d) => d.latitude)
}),
($) => ({
longitude: ($.longitude[0] + $.longitude[1]) / 2,
latitude: ($.latitude[0] + $.latitude[1]) / 2
})
)
Insert cell
journeyPoints = {
const stepMinutes = 5;

// for each journey, generate points along the path using d3.geoInterpolate and linear time interpolation
const legs = journey.map((leg) => {
let spaceInterpolator = d3.geoInterpolate(
[leg.start.airport.longitude, leg.start.airport.latitude],
[leg.end.airport.longitude, leg.end.airport.latitude]
);
// todo: maybe invert the time scale and use it to generate `t`, so that
// the leg has time-equidistant points in space?
let timeInterpolate = d3
.scaleLinear()
.domain([0, 1])
.range([+leg.start.time, +leg.end.time]);

let legPoints = [];
for (
let time = leg.start.time;
time <= leg.end.time;
time = d3.utcMinute.offset(time, stepMinutes)
) {
const t = timeInterpolate.invert(time);
let [longitude, latitude] = spaceInterpolator(t);
legPoints.push({
time: new Date(timeInterpolate(t)),
longitude,
latitude,
type: "flying"
});
}
return legPoints;
});

// then between those, fill the layover times with the static location of the airport and advancing time
let allPoints = [...legs[0]];
for (let [i, leg] of legs.entries()) {
if (i === 0) continue;
let start = journey[i - 1].end.time;
let end = journey[i].start.time;
let airport = journey[i].start.airport;
for (
let cursor = start;
cursor < end;
cursor = d3.timeMinute.offset(cursor, stepMinutes)
) {
allPoints.push({
time: cursor,
longitude: airport.longitude,
latitude: airport.latitude,
type: "layover"
});
}
allPoints = [...allPoints, ...leg];
}

let lastLeg = journey.at(-1);
allPoints.push({
time: lastLeg.end.time,
longitude: lastLeg.end.airport.longitude,
latitude: lastLeg.end.airport.latitude
});

return pipe(
allPoints,
($) =>
$.map((d) => ({
...d,
sun: suncalc.getPosition(d.time, d.latitude, d.longitude)
})),
($) =>
$.map((d) => ({
time: d.time,
longitude: d.longitude,
latitude: d.latitude,
sunAzimuth: (d.sun.azimuth / Math.PI) * 180,
sunAltitude: (d.sun.altitude / Math.PI) * 180,
type: d.type
}))
);

return allPoints;
}
Insert cell
function inputTransform(item) {
const airport = airports.find(
(d) => d.iata === item.airport || d.icao === item.airport
);
return {
airport,
time: new Date(item.time)
};
}
Insert cell
import { pipe } from "@mythmon/bonus-lib"
Insert cell
airports = (await FileAttachment("iata-icao.csv").csv()).map((d) => ({
...d,
latitude: +d.latitude,
longitude: +d.longitude
}))
Insert cell
niceTimes = ({
start: d3.utcHour.floor(d3.utcHour.offset(journey[0].start.time, -3)),
end: d3.utcHour.ceil(d3.utcHour.offset(journey.at(-1).end.time, 3))
})
Insert cell
arrivalDepartureData = pipe(
journey,
($) =>
$.flatMap((d) => [
{ ...d.start, action: "leave" },
{ ...d.end, action: "arrive" }
]),
($) =>
$.map((d) => ({
...d,
sun: suncalc.getPosition(d.time, d.airport.latitude, d.airport.longitude)
}))
)
Insert cell
airportSuns = {
let data = [];
for (const airportCode of ["PDX", "KEF"]) {
let airport = airports.find((d) => d.iata === airportCode);
const timezone = tzByIata[airportCode];
let now = new Date();
for (
let time = niceTimes.start;
time <= niceTimes.end;
time = d3.utcMinute.offset(time, 15)
) {
const sun = suncalc.getPosition(
time,
airport.latitude,
airport.longitude
);
data.push({
airport,
time,
sun: {
...sun,
azimuth: toDeg(sun.azimuth),
altitude: toDeg(sun.altitude)
},
timezone
});
}
}
return data;
}
Insert cell
suncalc = require("suncalc@1.9.0/suncalc.js")
Insert cell
luxon = import("https://unpkg.com/luxon@3.3.0/src/luxon.js?module")
Insert cell
tzByIata = pipe(
await FileAttachment("iata.tzmap").text(),
($) => $.split("\n"),
($) => $.map((l) => l.split("\t")),
($) => Object.fromEntries($)
)
Insert cell
Insert cell
toDeg = (r) => (r / Math.PI) * 180
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