mirror-speedometer/resources/charts/chartjs.js

174 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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";
/*
* From https://www.omnicalculator.com/other/latitude-longitude-distance:
* 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();