/**
* Altitude Display
* This class handles displaying altitude data with a 500m scale
*/
class AltitudeDisplay {
/**
* Create an altitude display handler
* @param {string} elementId - The ID of the element to render the altitude chart in
*/
constructor(elementId) {
this.elementId = elementId;
this.chart = null;
this.tooltip = null;
// Scale factor for altitude (500m)
this.altitudeScaleFactor = 500;
// Initialize tooltip
this.tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
}
/**
* Render the altitude chart with the given flight data
* @param {object} flightData - Parsed flight data from IGCParser
*/
render(flightData) {
if (!flightData || !flightData.fixes || flightData.fixes.length === 0) {
return;
}
const element = document.getElementById(this.elementId);
if (!element) {
console.error(`Element with ID ${this.elementId} not found`);
return;
}
// Clear previous chart
element.innerHTML = '';
// Get dimensions
const margin = {top: 20, right: 30, bottom: 30, left: 60};
const width = element.clientWidth - margin.left - margin.right;
const height = element.clientHeight - margin.top - margin.bottom;
// Create SVG
const svg = d3.select(element)
.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})`);
// Extract data
const fixes = flightData.fixes;
const timeData = fixes.map(fix => fix.timestamp - fixes[0].timestamp);
const altitudeData = fixes.map(fix => fix.pressureAltitude);
// Scales
const xScale = d3.scaleLinear()
.domain([0, d3.max(timeData)])
.range([0, width]);
// Determine altitude range with adjusted scale
// Key change: we divide by 500 to implement the 500m scale
const minAltitude = Math.floor(d3.min(altitudeData) / this.altitudeScaleFactor) * this.altitudeScaleFactor;
const maxAltitude = Math.ceil(d3.max(altitudeData) / this.altitudeScaleFactor) * this.altitudeScaleFactor;
const yScale = d3.scaleLinear()
.domain([minAltitude, maxAltitude])
.range([height, 0])
.nice();
// Axes
const xAxis = d3.axisBottom(xScale)
.tickFormat(d => this.formatTime(d));
const yAxis = d3.axisLeft(yScale)
.tickFormat(d => {
// Show altitude with 500m scale
return `${(d / this.altitudeScaleFactor).toFixed(1)}`;
});
// Add X axis
svg.append("g")
.attr("class", "altitude-axis")
.attr("transform", `translate(0,${height})`)
.call(xAxis)
.append("text")
.attr("fill", "#000")
.attr("x", width / 2)
.attr("y", margin.bottom)
.attr("text-anchor", "middle")
.text("Time (hh:mm)");
// Add Y axis
svg.append("g")
.attr("class", "altitude-axis")
.call(yAxis)
.append("text")
.attr("fill", "#000")
.attr("transform", "rotate(-90)")
.attr("y", -margin.left + 15)
.attr("x", -height / 2)
.attr("text-anchor", "middle")
.text("Altitude (x500m)");
// Add Y-axis grid lines
svg.append("g")
.attr("class", "altitude-grid")
.call(yAxis
.tickSize(-width)
.tickFormat("")
);
// Create line generator
const line = d3.line()
.x((d, i) => xScale(timeData[i]))
.y(d => yScale(d))
.curve(d3.curveMonotoneX);
// Create area generator
const area = d3.area()
.x((d, i) => xScale(timeData[i]))
.y0(height)
.y1(d => yScale(d))
.curve(d3.curveMonotoneX);
// Add the area
svg.append("path")
.datum(altitudeData)
.attr("class", "altitude-area")
.attr("d", area);
// Add the line path
svg.append("path")
.datum(altitudeData)
.attr("class", "altitude-path")
.attr("d", line);
// Add dots for data points with tooltips
const dots = svg.selectAll(".dot")
.data(altitudeData)
.enter()
.append("circle")
.attr("class", "altitude-dot")
.attr("cx", (d, i) => xScale(timeData[i]))
.attr("cy", d => yScale(d))
.attr("r", 0) // Set to 0 initially to make them invisible
.style("fill", "#4682b4")
.style("opacity", 0.7);
// Add invisible larger areas for mouseover
svg.selectAll(".hover-area")
.data(altitudeData)
.enter()
.append("circle")
.attr("class", "hover-area")
.attr("cx", (d, i) => xScale(timeData[i]))
.attr("cy", d => yScale(d))
.attr("r", 8)
.style("fill", "transparent")
.style("opacity", 0)
.on("mouseover", (event, d, i) => {
const index = altitudeData.indexOf(d);
const time = this.formatTime(timeData[index]);
const scaledAltitude = (d / this.altitudeScaleFactor).toFixed(2);
this.tooltip.transition()
.duration(200)
.style("opacity", .9);
this.tooltip.html(`
Time: ${time}
Altitude: ${scaledAltitude} x 500m
Actual: ${d}m
`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", () => {
this.tooltip.transition()
.duration(500)
.style("opacity", 0);
});
// Add title
svg.append("text")
.attr("x", width / 2)
.attr("y", 0 - (margin.top / 2))
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("font-weight", "bold")
.text("Flight Altitude Profile (500m Scale)");
// Add scale note
svg.append("text")
.attr("x", width - 150)
.attr("y", 0)
.attr("text-anchor", "start")
.style("font-size", "12px")
.style("fill", "#666")
.text("Scale: 1 unit = 500m");
}
/**
* Format seconds into HH:MM
* @param {number} seconds - Time in seconds
* @returns {string} - Formatted time
*/
formatTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
}
}