import Chart from "../vendor/chart.js" const M2FT = 3.28084 const KM2MI = 0.621371 // Chart.js plugin to draw horizontal duct bands const ductPlugin = { id: "ductBands", beforeDraw(chart) { const ducts = chart.options.plugins.ductBands?.ducts if (!ducts || ducts.length === 0) return const {ctx, chartArea: {left, right, top, bottom}, scales: {y}} = chart ctx.save() for (const duct of ducts) { const yTop = y.getPixelForValue(duct.top_ft) const yBase = y.getPixelForValue(duct.base_ft) // Clamp to chart area const drawTop = Math.max(yTop, top) const drawBase = Math.min(yBase, bottom) if (drawTop >= drawBase) continue const likely = duct.likely const fillAlpha = likely ? 0.18 : 0.08 const strokeAlpha = likely ? 0.8 : 0.3 const lineWidth = likely ? 1.5 : 1 ctx.fillStyle = `rgba(34, 197, 94, ${fillAlpha})` ctx.fillRect(left, drawTop, right - left, drawBase - drawTop) ctx.strokeStyle = `rgba(34, 197, 94, ${strokeAlpha})` ctx.lineWidth = lineWidth ctx.setLineDash(likely ? [] : [4, 3]) ctx.beginPath() ctx.moveTo(left, drawTop) ctx.lineTo(right, drawTop) ctx.moveTo(left, drawBase) ctx.lineTo(right, drawBase) ctx.stroke() ctx.setLineDash([]) // Label const labelY = Math.max(drawTop + 12, top + 12) const textAlpha = likely ? 1.0 : 0.5 ctx.fillStyle = `rgba(34, 197, 94, ${textAlpha})` ctx.font = likely ? "bold 10px sans-serif" : "9px sans-serif" const labels = {hrrr: "Duct", sounding: "Duct (sounding)", inferred: "Est. duct"} const units = {hrrr: "M-units", sounding: "M-units", inferred: "dN/dh÷10"} const prefix = labels[duct.source] || "Duct" const unit = units[duct.source] || "M-units" const tag = likely ? " — likely path" : "" ctx.fillText(`${prefix} ${Math.round(duct.base_ft)}–${Math.round(duct.top_ft)} ft (${duct.strength} ${unit})${tag}`, left + 4, labelY) } ctx.restore() } } export const ElevationProfile = { mounted() { const data = JSON.parse(this.el.dataset.profile) if (!data || !data.points || data.points.length === 0) return this.renderChart(data) }, renderChart(data) { const points = data.points const distances = points.map(p => p.dist_km * KM2MI) const elevations = points.map(p => p.elev * M2FT) const losLine = points.map(p => p.beam * M2FT) const fresnelLower = points.map(p => (p.beam - p.r1) * M2FT) const showFresnel = data.freq_mhz >= 1000 const ducts = (data.ducts || []).map(d => ({ base_ft: d.base_m_msl * M2FT, top_ft: d.top_m_msl * M2FT, strength: d.strength, source: d.source || "hrrr", likely: d.likely || false })) const allElevs = [...elevations, ...losLine] let minElev = Math.min(...allElevs) - 60 let maxElev = Math.max(...allElevs) + 120 // Extend Y axis to include duct tops if needed for (const d of ducts) { if (d.top_ft + 40 > maxElev) maxElev = d.top_ft + 40 } const canvas = this.el.querySelector("canvas") if (!canvas) return const ctx = canvas.getContext("2d") this.chart = new Chart(ctx, { type: "line", plugins: [ductPlugin], data: { labels: distances, datasets: [ { label: "Terrain", data: elevations, borderColor: "#8B6914", backgroundColor: "rgba(139, 105, 20, 0.3)", fill: true, pointRadius: 0, borderWidth: 1.5, tension: 0.1, order: 3 }, { label: "Line of Sight", data: losLine, borderColor: "#2563eb", borderWidth: 2, borderDash: [6, 3], pointRadius: 0, fill: false, order: 1 }, ...(showFresnel ? [ { label: "Fresnel Zone", data: fresnelLower, borderColor: "rgba(239, 68, 68, 0.4)", backgroundColor: "rgba(239, 68, 68, 0.08)", borderWidth: 1, borderDash: [3, 3], pointRadius: 0, fill: "+1", order: 2 }, { label: "_fresnelBase", data: losLine, borderWidth: 0, pointRadius: 0, fill: false, order: 2 } ] : []), // Invisible dataset for duct legend entry ...(ducts.length > 0 ? [{ label: "Duct Layer", data: [], borderColor: "rgba(34, 197, 94, 0.5)", backgroundColor: "rgba(34, 197, 94, 0.12)", borderWidth: 2, borderDash: [4, 3], pointRadius: 0, fill: false }] : []) ] }, options: { responsive: true, maintainAspectRatio: false, animation: {duration: 300}, plugins: { ductBands: {ducts}, legend: { display: true, position: "top", labels: { boxWidth: 12, font: {size: 10}, filter: item => !item.text.startsWith("_") } }, tooltip: { callbacks: { title: items => items[0].label + " mi", label: item => { if (item.dataset.label.startsWith("_")) return null return item.dataset.label + ": " + Math.round(item.raw) + " ft" } } } }, scales: { x: { title: {display: true, text: "Distance (mi)", font: {size: 10}}, afterBuildTicks: axis => { const last = distances.length - 1 const totalMi = distances[last] const step = Math.ceil(totalMi / 7) || 1 const ticks = [] for (let mi = 0; mi < totalMi; mi += step) { const idx = distances.findIndex(d => d >= mi) if (idx >= 0) ticks.push({value: idx}) } ticks.push({value: last}) axis.ticks = ticks }, ticks: { font: {size: 9}, autoSkip: false, callback: val => distances[val] !== undefined ? Math.round(distances[val]) : "" } }, y: { title: {display: true, text: "Elevation (ft)", font: {size: 10}}, ticks: {font: {size: 9}}, min: Math.max(0, minElev), max: maxElev } } } }) }, destroyed() { if (this.chart) { this.chart.destroy() this.chart = null } } }