Source code
Revision control
Copy as Markdown
Other Tools
import Chart from "chart.js/auto";
import { csvParse } from "d3-dsv";
// These 2 imports use some vite machinery to directly import these files as
// strings. Then they are constants and this reduces the load on the GC.
import airportsString from "./datasets/airports.csv?raw";
import flightsString from "./datasets/flights-airports.csv?raw";
/*
* d = 2R × sin⁻¹(√[sin²((θ₂ - θ₁)/2) + cosθ₁ × cosθ₂ × sin²((φ₂ - φ₁)/2)])
*
* where:
* θ₁, φ₁ – First point latitude and longitude coordinates;
* θ₂, φ₂ – Second point latitude and longitude coordinates;
* R – Earth's radius (R = 6371 km); and
* d – Distance between them along Earth's surface.
*/
const R = 6371;
function computeDistance(coords1, coords2) {
const long1 = toRadian(coords1.longitude);
const lat1 = toRadian(coords1.latitude);
const long2 = toRadian(coords2.longitude);
const lat2 = toRadian(coords2.latitude);
const longSquareSin = Math.sin((long2 - long1) / 2) ** 2;
const latSquareSin = Math.sin((lat2 - lat1) / 2) ** 2;
const d = 2 * R * Math.asin(Math.sqrt(latSquareSin + Math.cos(lat1) * Math.cos(lat2) * longSquareSin));
return d;
}
const RAD = Math.PI / 180;
function toRadian(deg) {
return deg * RAD;
}
let preparedData = null;
function prepare() {
/**
* AirportInformation: { state, iata, name, city, country, latitude, longitude }
* airports: Array<AirportInformation>
* flights: Array<{ origin, destination, count }>
*/
const airports = csvParse(airportsString);
const flights = csvParse(flightsString);
const airportMap = new Map(airports.map((airport) => [airport.iata, airport]));
for (const flight of flights) {
const origAirport = airportMap.get(flight.origin);
const destAirport = airportMap.get(flight.destination);
flight.distance = computeDistance(origAirport, destAirport);
}
preparedData = { flights, airportMap };
}
const ROOT = document.getElementById("chart");
const opaqueCheckBox = document.getElementById("opaque-color");
let currentChart = null;
function drawScattered(data) {
if (!preparedData)
throw new Error("Please prepare the data first.");
reset();
currentChart = new Chart(ROOT, {
type: "scatter",
data: {
datasets: [
{
data: preparedData.flights,
backgroundColor: opaqueCheckBox.checked ? "rgb(0, 125, 255)" : "rgba(0, 125, 255, .20)",
},
],
},
options: {
animation: false,
parsing: {
xAxisKey: "distance",
yAxisKey: "count",
},
plugins: {
legend: {
display: false,
},
title: {
display: true,
text: "Number of flights for a distance",
position: "bottom",
},
tooltip: {
displayColors: false,
callbacks: {
label: (item) => {
const orig = preparedData.airportMap.get(item.raw.origin);
const dest = preparedData.airportMap.get(item.raw.destination);
const result = `✈ ${orig.name} (${orig.iata}) → ${dest.name} (${dest.iata}): ${Math.round(item.raw.distance)} km`;
return result;
},
},
},
},
scales: {
x: {
title: {
text: "distance →",
align: "end",
display: true,
},
ticks: {
format: {
style: "unit",
unit: "kilometer",
},
},
},
y: {
type: "logarithmic",
title: {
text: "# of flights →",
align: "end",
display: true,
},
},
},
},
});
}
function openTooltip() {
if (!currentChart)
throw new Error("No chart is present, please draw a chart first");
const renderedDataset = currentChart.getDatasetMeta(0);
const node = currentChart.canvas;
const rect = node.getBoundingClientRect();
// Index 2426 is carefully chosen to display a lot of lines (which depends
// on the zoom level).
const point = renderedDataset.data[2426];
const event = new MouseEvent("mousemove", {
clientX: rect.left + point.x,
clientY: rect.top + point.y,
cancelable: true,
bubbles: true,
});
node.dispatchEvent(event);
}
function reset() {
if (currentChart) {
currentChart.destroy();
currentChart = null;
}
}
async function runAllTheThings() {
[
// Force prettier to use a multiline formatting
"prepare",
"add-scatter-chart-button",
"open-tooltip",
].forEach((id) => document.getElementById(id).click());
}
document.getElementById("prepare").addEventListener("click", prepare);
document.getElementById("add-scatter-chart-button").addEventListener("click", drawScattered);
document.getElementById("open-tooltip").addEventListener("click", openTooltip);
document.getElementById("reset").addEventListener("click", reset);
document.getElementById("run-all").addEventListener("click", runAllTheThings);
if (import.meta.env.DEV)
runAllTheThings();