Skip to content

Instantly share code, notes, and snippets.

@MrJonoCES
Created December 29, 2023 22:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MrJonoCES/671aa3ba3e2807a38d16b4b5c7ddaf5e to your computer and use it in GitHub Desktop.
Save MrJonoCES/671aa3ba3e2807a38d16b4b5c7ddaf5e to your computer and use it in GitHub Desktop.
main.js
const chartsHome = document.getElementById('prev_monthly_chart_container');
const prevYearData = 'https://raw.githubusercontent.com/stillawake17/FlightViz/main/data/combined_flights_data.json';
const currentYearData = 'https://raw.githubusercontent.com/stillawake17/FlightViz/main/data/combined_flights_data.json';
if (chartsHome) {
document.addEventListener('DOMContentLoaded', function() {
// Load the data
let data2023, data2024;
Promise.all([
d3.json(prevYearData).then(data => data2023 = data),
d3.json(currentYearData).then(data => data2024 = data)
]).then(() => {
// Process data for both years
[data2023, data2024].forEach(data => {
data.forEach(function(d) {
let actualTime = null;
// Check for arrival data with iataCode 'brs'
if (d.arrival && d.arrival.iataCode === 'brs' && 'actualTime' in d.arrival) {
actualTime = d.arrival.actualTime;
}
// If not found in arrival, check for departure data with iataCode 'brs'
else if (d.departure && d.departure.iataCode === 'brs' && 'actualTime' in d.departure) {
actualTime = d.departure.actualTime;
}
// If actualTime is found, proceed
if (actualTime) {
let date = new Date(actualTime); // Parsing ISO 8601 date format
d.latestTime = date;
d.Hour = date.getHours();
d.Minute = date.getMinutes();
// Classify the time category
d.Time_Category = "Regular arrivals";
if (d.Hour === 23 && d.Minute < 30) d.Time_Category = "Shoulder hour flights";
else if ((d.Hour === 23 && d.Minute >= 30) || d.Hour < 6) d.Time_Category = "Night hour arrivals";
else if (d.Hour === 6) d.Time_Category = "Shoulder hour flights";
}
});
});
let prev_total_flights = data2023.length;
let prev_shoulder_hour_flights = data2023.filter(d => d.Time_Category === 'Shoulder hour flights').length;
let prev_night_hour_flights = data2023.filter(d => d.Time_Category === 'Night hour arrivals').length;
let current_total_flights = data2024.length;
let current_shoulder_hour_flights = data2024.filter(d => d.Time_Category === 'Shoulder hour flights').length;
let current_night_hour_flights = data2024.filter(d => d.Time_Category === 'Night hour arrivals').length;
// Quotas
let quotas = [85990, 9500, 4000];
// Categories and counts
let categories = ['Total Flights', 'Shoulder Hour Flights', 'Night Hour Flights'];
let counts2023 = [prev_total_flights, prev_shoulder_hour_flights, prev_night_hour_flights];
let counts2024 = [current_total_flights, current_shoulder_hour_flights, current_night_hour_flights];
// Calculating percentages
let percentages2023 = counts2023.map((count, index) => (count / quotas[index]) * 100);
let percentages2024 = counts2024.map((count, index) => (count / quotas[index]) * 100);
// Update dials
function formatNumberWithComma(number) {
// Check if the number is 1000 or more
if (number >= 1000) {
// Convert the number to a string
const numStr = number.toString();
// Insert a comma before the last three digits
return numStr.slice(0, -3) + "," + numStr.slice(-3);
} else {
// If the number is less than 1000, return it without modification
return number.toString();
}
}
function updateCountElement(elementId, value) {
// Select the span element by its ID
const countElement = document.getElementById(elementId);
// Format the number with a comma
const formattedValue = formatNumberWithComma(value);
// Update the content of the element
countElement.textContent = formattedValue;
}
// Call this function with the appropriate values
updateCountElement("prev_night_count", prev_night_hour_flights);
updateCountElement("prev_shoulder_count", prev_shoulder_hour_flights);
updateCountElement("prev_total_count", prev_total_flights);
updateCountElement("current_night_count", current_night_hour_flights);
updateCountElement("current_shoulder_count", current_shoulder_hour_flights);
updateCountElement("current_total_count", current_total_flights);
// Function to aggregate flight data by month for a given year
function aggregateDataByMonth(flightData, year) {
const monthlyCounts = new Array(12).fill(0); // Array for each month
flightData.forEach(d => {
// Assuming the date is in d.arrival.actualTime or d.departure.actualTime
let actualTime = d.arrival ? d.arrival.actualTime : (d.departure ? d.departure.actualTime : null);
if (actualTime) {
let date = new Date(actualTime);
let flightYear = date.getFullYear();
let flightMonth = date.getMonth(); // getMonth() returns 0-11 for Jan-Dec
if (flightYear === year) {
monthlyCounts[flightMonth]++;
}
}
});
return monthlyCounts;
}
// Assuming percentages is an array with values for total, shoulder, and night flights
// and that quotas are the maximum values for each bar
// Example: let percentages = [60, 45, 80]; // Percent completion for each category
// Define maximum width for the progress bars (could be based on the container's width)
const maxBarWidth = 300; // This should match the width of your progress containers
// Define colors for each progress bar
const barColors = {
totalFlights: "#60FF98",
shoulderFlights: "#07D7ED",
nightFlights: "#7D71FF"
};
let roundedPercentages2023 = percentages2023.map(function(d) {
return d.toFixed(1); // Rounds the percentage to one decimal place
});
let roundedPercentages2024 = percentages2024.map(function(d) {
return d.toFixed(1); // Rounds the percentage to one decimal place
});
function rotateSingleIndicator(element, percentage) {
// Calculate the rotation angle
const rotationAngle = percentage / 100 * 270;
// animate the rotation of the dial
gsap.to(element, {
duration: 1,
rotation: rotationAngle,
transformOrigin: "top right",
ease: "power1.inOut"
});
// Get the .dial-bg element related to the current indicator
const dialBg = element.closest('.dial').querySelector('.dial-bg');
// Calculate the time when the dial reaches 70%
const timeToReach70Percent = 0.7 * 1; // Assuming 1 second total duration
if (percentage > 80 && percentage < 95) {
// Delay the color change to the moment the dial crosses 70%
gsap.to(dialBg, {
delay: timeToReach70Percent,
fill: 'orange'
});
} else if (percentage > 95) {
// Delay the color change to the moment the dial crosses 70%
gsap.to(dialBg, {
delay: timeToReach70Percent,
fill: 'red'
});
}
else {
// Reset to the original color without delay
gsap.to(dialBg, { fill: 'originalColor' }); // Replace 'originalColor' with the default color of the dial
}
}
// Function to rotate all indicators
function rotateAllIndicators(selector, percentages) {
// Select all elements with the given selector
const dials = document.querySelectorAll(selector);
// Iterate over each dial element
dials.forEach((dial, index) => {
// Ensure we don't exceed the length of the percentages array
if (index < percentages.length) {
const percentage = percentages[index];
// Create a ScrollTrigger for each dial
ScrollTrigger.create({
trigger: dial,
start: "bottom bottom", // Start the animation when the top of the dial reaches the bottom of the viewport
onEnter: () => rotateSingleIndicator(dial.querySelector('.indicator'), percentage)
});
}
});
}
// Rotate the dials for 2023
rotateAllIndicators(".prev_dial", roundedPercentages2023);
// Rotate the dials for 2024
rotateAllIndicators(".current_dial", roundedPercentages2024);
// Function to draw the bar chart for monthly data
function drawMonthlyChart(containerId, monthlyData, category) {
// Clear any existing charts
d3.select(containerId).selectAll("*").remove();
console.log("Drawing chart for category:", category);
console.log("Monthly data:", monthlyData);
console.log("Percentages:", [percentages[0], percentages[1], percentages[2]]);
// Set up dimensions for the chart
// Select the parent container
const parentContainer = d3.select('.parent-container').node();
// Get the computed style of the parent container to access padding values
const style = window.getComputedStyle(parentContainer);
// Parse padding values from the computed style
const paddingTop = parseInt(style.paddingTop, 10);
const paddingBottom = parseInt(style.paddingBottom, 20);
const paddingLeft = parseInt(style.paddingLeft, 10);
const paddingRight = parseInt(style.paddingRight, 10);
// Calculate the width and height based on the parent container's dimensions and padding
const margin = {top: 50, right: 20, bottom: 30, left: 40}; // Adjust margin as needed
const width = parentContainer.getBoundingClientRect().width - paddingLeft - paddingRight - margin.left - margin.right;
const height = parentContainer.getBoundingClientRect().height - paddingTop - paddingBottom - margin.top - margin.bottom -50;
// Define color schemes
const colorSchemes = {
'Total Flights': '#60FF98',
'Shoulder Hour Flights': '#07D7ED',
'Night Hour Flights': '#7D71FF'
};
// Use the color based on the category
const barColor = colorSchemes[category] || 'grey'; // Fallback color if category is not found
// Create SVG container for the chart
const svg = d3.select(containerId).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Adding title to the chart
svg.append("text")
.attr("x", width / 2)
.attr("y", 0 - margin.top / 2)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("fill", "#fff")
// .text(`${category} - Monthly Distribution`)
;
// Check if SVG is created properly
console.log("SVG created with width and height:", width, height);
// X scale - months
const x = d3.scaleBand()
.range([0, width])
.padding(0.1)
.domain(monthlyData.map((_, i) => i)); // 0-11 for Jan-Dec
// Y scale - flight counts
const y = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(monthlyData)]);
// Check scales
console.log("X and Y scales set.");
// X axis - months
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickFormat(i => d3.timeFormat("%b")(new Date(0, i)))); // Convert month number to 3-letter name
// Y axis - flight counts
svg.append("g")
.call(d3.axisLeft(y));
// Check axes
console.log("Axes drawn.");
// Bars for the chart
svg.selectAll(".bar")
.data(monthlyData)
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", (d, i) => x(i))
.attr("width", x.bandwidth())
.attr("y", height) // Start the bars at the bottom
.attr("height", 0) // Set the initial height to 0
.style("fill", barColor) // Set the color for the bars
.each(function(d, i) {
// Use GSAP to animate the bars
gsap.to(this, { // 'this' refers to the current DOM element
attr: {
y: y(d), // Animate the 'y' attribute to the final value
height: height - y(d), // Animate the 'height' attribute to the final value
},
duration: 0.5,
delay: i * 0.1, // Add a delay for sequential animation
});
});
// Adding data count above each bar
svg.selectAll(".text")
.data(monthlyData)
.enter()
.append("text")
.attr("class", "label")
.attr("fill", "#fff")
.attr("x", (d, i) => x(i) + x.bandwidth() / 2)
.attr("y", d => y(d) - 5)
.attr("text-anchor", "middle")
.text(d => d);
// Check if bars are appended
console.log("Bars should now be visible on the chart.");
}
// Make sure the full year's flight data is loaded into `flightData`
// Click event listeners for the progress bars
const prevNightButton = document.getElementById('prev_night_button');
const prevShoulderButton = document.getElementById('prev_shoulder_button');
const prevTotalButton = document.getElementById('prev_total_button');
const currentNightButton = document.getElementById('current_night_button');
const currentShoulderButton = document.getElementById('current_shoulder_button');
const currentTotalButton = document.getElementById('current_total_button');
function attachEventListeners() {
d3.select("#prev_total_button").on("click", function() {
showMonthlyChartContainer(); // Make sure the container is visible
const totalFlightsMonthlyData = aggregateDataByMonth(data, 2023);
drawMonthlyChart("#prev_monthly_chart_container", totalFlightsMonthlyData, 'Total Flights');
let element = document.querySelector('.button-on');
if (element) {
element.classList.remove('button-on');
}
prevTotalButton.classList.add('button-on');
});
d3.select("#prev_shoulder_button").on("click", function() {
showMonthlyChartContainer(); // Make sure the container is visible
const shoulderFlightsMonthlyData = aggregateDataByMonth(
data.filter(d => d.Time_Category === 'Shoulder hour flights' && d.Year === 2023),
2023);
drawMonthlyChart("#prev_monthly_chart_container", shoulderFlightsMonthlyData, 'Shoulder Hour Flights');
let element = document.querySelector('.button-on');
if (element) {
element.classList.remove('button-on');
}
prevShoulderButton.classList.add('button-on');
});
d3.select("#prev_night_button").on("click", function() {
showMonthlyChartContainer(); // Make sure the container is visible
const shoulderFlightsMonthlyData = aggregateDataByMonth(
data.filter(d => d.Time_Category === 'Night hour arrivals' && d.Year === 2023),
2023);
drawMonthlyChart("#prev_monthly_chart_container", shoulderFlightsMonthlyData, 'Night hour arrivals');
let element = document.querySelector('.button-on');
if (element) {
element.classList.remove('button-on');
}
prevNightButton.classList.add('button-on');
});
d3.select("#current_total_button").on("click", function() {
showMonthlyChartContainer(); // Make sure the container is visible
const totalFlightsMonthlyData = aggregateDataByMonth(data, 2024);
drawMonthlyChart("#current_monthly_chart_container", totalFlightsMonthlyData, 'Total Flights');
let element = document.querySelector('.button-on');
if (element) {
element.classList.remove('button-on');
}
currentTotalButton.classList.add('button-on');
});
d3.select("#current_shoulder_button").on("click", function() {
showMonthlyChartContainer(); // Make sure the container is visible
const shoulderFlightsMonthlyData = aggregateDataByMonth(
data.filter(d => d.Time_Category === 'Shoulder hour flights' && d.Year === 2024),
2024);
drawMonthlyChart("#current_monthly_chart_container", shoulderFlightsMonthlyData, 'Shoulder Hour Flights');
let element = document.querySelector('.button-on');
if (element) {
element.classList.remove('button-on');
}
currentShoulderButton.classList.add('button-on');
});
d3.select("#current_night_button").on("click", function() {
showMonthlyChartContainer(); // Make sure the container is visible
const shoulderFlightsMonthlyData = aggregateDataByMonth(
data.filter(d => d.Time_Category === 'Night hour arrivals' && d.Year === 2024),
2024);
drawMonthlyChart("#current_monthly_chart_container", shoulderFlightsMonthlyData, 'Night hour arrivals');
let element = document.querySelector('.button-on');
if (element) {
element.classList.remove('button-on');
}
currentNightButton.classList.add('button-on');
});
}
function loadTotalFlightsChart(year, containerId, button) {
showMonthlyChartContainer(containerId); // Make sure the container is visible
const nightFlightsMonthlyData = aggregateDataByMonth(data, year);
drawMonthlyChart(containerId, nightFlightsMonthlyData, 'Night Hour Flights');
button.classList.add('button-on');
}
// Call the function to attach the event listeners
attachEventListeners();
// Call the function to load the charts for each year
loadTotalFlightsChart(2023, "#prev_monthly_chart_container", prevNightButton);
loadTotalFlightsChart(2024, "#current_monthly_chart_container", currentNightButton);
}).catch(function(error) {
console.error("Error loading the data:", error);
})
function showMonthlyChartContainer(containerId) {
// Display the container
d3.select(containerId).style("display", "block");
}
// Call the function to show the containers for each year
showMonthlyChartContainer("#prev_monthly_chart_container");
showMonthlyChartContainer("#current_monthly_chart_container");
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment