This commit is contained in:
2026-06-25 10:21:55 -04:00
parent 257232f6f1
commit bd073a4613
5 changed files with 133 additions and 139 deletions

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, onMounted} from 'vue'; import {ref, onMounted} from 'vue';
import {api} from '../../services/api'; import {api} from '../services/api';
import MetricRow from './MetricRow.vue'; import MetricRow from './MetricRow.vue';
const d = ref<any>(null); const d = ref<any>(null);

View File

@@ -15,7 +15,7 @@ import { fromLonLat, toLonLat } from 'ol/proj'
import { Style, Fill, Stroke, Circle as CircleStyle } from 'ol/style' import { Style, Fill, Stroke, Circle as CircleStyle } from 'ol/style'
import 'ol/ol.css' import 'ol/ol.css'
const current = ref([0, 0]); const current = ref({latitude: 0, longitude: 0});
const props = defineProps<{ dark: boolean }>() const props = defineProps<{ dark: boolean }>()
const mapEl = ref<HTMLDivElement>() const mapEl = ref<HTMLDivElement>()
const showOverlays = ref(false) const showOverlays = ref(false)
@@ -60,7 +60,7 @@ function buildWindSrc(lat: number, lon: number, zoom: number) {
function syncWindSrc() { function syncWindSrc() {
const view = map.getView() const view = map.getView()
const center = toLonLat(view.getCenter()!) const center: any = toLonLat(view.getCenter()!)
windSrc.value = buildWindSrc(center[1], center[0], view.getZoom() ?? 8) windSrc.value = buildWindSrc(center[1], center[0], view.getZoom() ?? 8)
} }

View File

@@ -1,160 +1,155 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, onMounted } from 'vue' import {computed, ref, onMounted} from 'vue';
import { Line } from 'vue-chartjs' import {Line} from 'vue-chartjs';
import { import {
Chart as ChartJS, CategoryScale, LinearScale, PointElement, Chart as ChartJS, CategoryScale, LinearScale, PointElement,
LineElement, Tooltip, Legend, Filler, type ChartOptions LineElement, Tooltip, Legend, Filler, type ChartOptions
} from 'chart.js' } from 'chart.js';
import { METRICS, formatValue } from '../services/units' import {METRICS, formatValue} from '../services/units';
import { fetchHistoricHourly } from '../services/weather' import type {DataRow} from '../services/api';
import type { DataRow } from '../services/api' import {api} from '../services/api';
import { api } from '../services/api'
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Tooltip, Legend, Filler) ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Tooltip, Legend, Filler);
const props = defineProps<{ metricKey: string; currentData: DataRow }>() const props = defineProps<{metricKey: string; currentData: DataRow}>();
const historic = ref<DataRow[]>([]) const historic = ref<DataRow[]>([]);
const forecast = ref<DataRow[]>([]) const forecast = ref<DataRow[]>([]);
const loading = ref(true) const loading = ref(true);
const meta = computed(() => METRICS[props.metricKey]) const meta = computed(() => METRICS[props.metricKey]);
onMounted(async () => { onMounted(async () => {
const now = new Date() const start = new Date(Date.now() - 24 * 3600000);
const start = new Date(now.getTime() - 24 * 3600000).toISOString() const end = new Date(Date.now() + 48 * 3600000);
const end = new Date(now.getTime() + 48 * 3600000).toISOString() const hourly = await api.hourly(start, end);
const [hist, fore] = await Promise.allSettled([ historic.value = hourly.filter((h: any) => new Date(h.time).getTime() <= Date.now());
fetchHistoricHourly(start, now.toISOString()), forecast.value = hourly.filter((h: any) => new Date(h.time).getTime() > Date.now());
api.hourly(now.toISOString(), end), loading.value = false;
]) });
if (hist.status === 'fulfilled') historic.value = hist.value.filter(r => r[props.metricKey] != null)
if (fore.status === 'fulfilled') forecast.value = fore.value.filter(r => r[props.metricKey] != null)
loading.value = false
})
const nowLabel = computed(() => { const nowLabel = computed(() => {
const d = new Date() const d = new Date();
return `${d.getHours().toString().padStart(2,'0')}:${d.getMinutes().toString().padStart(2,'0')}` return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
}) });
const chartData = computed(() => { const chartData = computed(() => {
const color = meta.value?.color ?? '#38bdf8' const color = meta.value?.color ?? '#38bdf8';
const histLabels = historic.value.map(r => r.time as string) const histLabels = historic.value.map(r => r.time as string);
const foreLabels = forecast.value.map(r => r.time as string) const foreLabels = forecast.value.map(r => r.time as string);
const histVals = historic.value.map(r => r[props.metricKey] as number) const histVals = historic.value.map(r => r[props.metricKey] as number);
const foreVals = forecast.value.map(r => r[props.metricKey] as number) const foreVals = forecast.value.map(r => r[props.metricKey] as number);
const currentVal = props.currentData[props.metricKey] as number | null const currentVal = props.currentData[props.metricKey] as number | null;
const allLabels = [...histLabels, nowLabel.value, ...foreLabels] const allLabels = [...histLabels, nowLabel.value, ...foreLabels];
const histData = [...histVals, currentVal, ...foreVals.map(() => null)] const histData = [...histVals, currentVal, ...foreVals.map(() => null)];
const foreData = [...histVals.map(() => null), currentVal, ...foreVals] const foreData = [...histVals.map(() => null), currentVal, ...foreVals];
return { return {
labels: allLabels, labels: allLabels,
datasets: [ datasets: [
{ {
label: 'Historic', label: 'Historic',
data: histData, data: histData,
borderColor: color, borderColor: color,
backgroundColor: `${color}22`, backgroundColor: `${color}22`,
borderWidth: 2, borderWidth: 2,
pointRadius: 0, pointRadius: 0,
tension: 0.3, tension: 0.3,
fill: true, fill: true,
spanGaps: false, spanGaps: false,
}, },
{ {
label: 'Forecast', label: 'Forecast',
data: foreData, data: foreData,
borderColor: color, borderColor: color,
backgroundColor: 'transparent', backgroundColor: 'transparent',
borderWidth: 2, borderWidth: 2,
borderDash: [6, 4], borderDash: [6, 4],
pointRadius: 0, pointRadius: 0,
tension: 0.3, tension: 0.3,
fill: false, fill: false,
spanGaps: false, spanGaps: false,
}, },
{ {
label: 'Now', label: 'Now',
data: allLabels.map(l => l === nowLabel.value ? currentVal : null), data: allLabels.map(l => l === nowLabel.value ? currentVal : null),
borderColor: '#ffffff88', borderColor: '#ffffff88',
backgroundColor: color, backgroundColor: color,
pointRadius: 6, pointRadius: 6,
pointHoverRadius:8, pointHoverRadius: 8,
borderWidth: 0, borderWidth: 0,
showLine: false, showLine: false,
spanGaps: false, spanGaps: false,
} }
] ]
} };
}) });
const chartOptions = computed<ChartOptions<'line'>>(() => ({ const chartOptions = computed<ChartOptions<'line'>>(() => ({
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false }, interaction: {mode: 'index', intersect: false},
plugins: { plugins: {
legend: { display: false }, legend: {display: false},
tooltip: { tooltip: {
callbacks: { callbacks: {
label: ctx => formatValue(props.metricKey, ctx.parsed.y), label: ctx => formatValue(props.metricKey, ctx.parsed.y),
} }
} }
}, },
scales: { scales: {
x: { x: {
ticks: { ticks: {
color: 'var(--text-muted)', color: 'var(--text-muted)',
maxTicksLimit: 12, maxTicksLimit: 12,
maxRotation: 0, maxRotation: 0,
callback(val, i) { callback(val, i) {
const lbl = this.getLabelForValue(i) const lbl = this.getLabelForValue(i);
return lbl?.length === 5 && lbl.endsWith(':00') ? lbl : '' return lbl?.length === 5 && lbl.endsWith(':00') ? lbl : '';
} }
}, },
grid: { color: 'var(--border)' }, grid: {color: 'var(--border)'},
}, },
y: { y: {
ticks: { color: 'var(--text-muted)', callback: v => formatValue(props.metricKey, v as number) }, ticks: {color: 'var(--text-muted)', callback: v => formatValue(props.metricKey, v as number)},
grid: { color: 'var(--border)' }, grid: {color: 'var(--border)'},
} }
}, },
annotation: { annotation: {
annotations: { annotations: {
nowLine: { nowLine: {
type: 'line', type: 'line',
xMin: nowLabel.value, xMin: nowLabel.value,
xMax: nowLabel.value, xMax: nowLabel.value,
borderColor: '#ffffff44', borderColor: '#ffffff44',
borderWidth: 1, borderWidth: 1,
borderDash: [4, 4], borderDash: [4, 4],
} }
} }
} }
})) }));
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.chart-wrap { .chart-wrap {
position: relative; position: relative;
height: 260px; height: 260px;
width: 100%; width: 100%;
} }
.loading { .loading {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 260px; height: 260px;
color: var(--text-muted); color: var(--text-muted);
font-size: 14px; font-size: 14px;
} }
</style> </style>
<template> <template>
<div v-if="loading" class="loading">Loading chart</div> <div v-if="loading" class="loading">Loading chart</div>
<div v-else class="chart-wrap"> <div v-else class="chart-wrap">
<Line :data="chartData" :options="chartOptions" /> <Line :data="chartData" :options="chartOptions"/>
</div> </div>
</template> </template>

View File

@@ -45,12 +45,12 @@ const arc = computed(() => {
}); });
const splitIdx = Math.round(pct * 200); const splitIdx = Math.round(pct * 200);
const { x: cx, y: cy } = pts[splitIdx]; const { x: cx, y: cy } = <any>pts[splitIdx];
// Filled traveled path: close down to horizon baseline for fill // Filled traveled path: close down to horizon baseline for fill
const traveledPts = pts.slice(0, splitIdx + 1); const traveledPts = pts.slice(0, splitIdx + 1);
const filledPath = traveledPts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ') const filledPath = traveledPts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ')
+ ` L${traveledPts[traveledPts.length - 1].x.toFixed(1)},${HORIZON}` + ` L${(<any>traveledPts[traveledPts.length - 1]).x.toFixed(1)},${HORIZON}`
+ ` L${x1},${HORIZON} Z`; + ` L${x1},${HORIZON} Z`;
// Remaining path stroke only // Remaining path stroke only

View File

@@ -13,7 +13,6 @@ import {getADSB, getAirTrafficHistory, getShapes, initAircraftDb} from './adsb.m
import {getWeatherCondition} from './openweather.mjs'; import {getWeatherCondition} from './openweather.mjs';
import {lastForecast, getForecast, forecastTTL} from './forecast.mjs'; import {lastForecast, getForecast, forecastTTL} from './forecast.mjs';
import {dailyWeather, hourlyWeather} from './openmeteo.mjs'; import {dailyWeather, hourlyWeather} from './openmeteo.mjs';
import {init} from 'express/lib/application.js';
// import {Aurora} from './aurora.mjs'; // import {Aurora} from './aurora.mjs';
// ── Uncaught error handlers ─────────────────────────────────────────────────── // ── Uncaught error handlers ───────────────────────────────────────────────────