aprs.me/assets/js/features/weather_charts.ts

450 lines
No EOL
20 KiB
TypeScript

// Declare global Chart object from CDN
declare global {
interface Window {
Chart: any;
chartInstances?: Map<string, any>;
}
}
// Type for LiveView hooks
type Hook = any;
// Type for weather history data
interface WeatherHistoryDatum {
timestamp: string;
temperature?: number;
dew_point?: number;
humidity?: number;
pressure?: number;
wind_direction?: number;
wind_speed?: number;
wind_gust?: number;
rain_1h?: number;
rain_24h?: number;
rain_since_midnight?: number;
luminosity?: number;
}
// Type for the event payload
interface UpdateWeatherChartsPayload {
weather_history: string;
}
// Helper function to get theme-aware colors
const getThemeColors = () => {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
return {
text: isDark ? '#e5e7eb' : '#111827',
grid: isDark ? '#374151' : '#9ca3af',
background: isDark ? 'rgba(0, 0, 0, 0.1)' : 'rgba(255, 255, 255, 0.1)'
};
};
// Helper for 24-hour time formatting
function format24Hour(date: Date): string {
if (!(date instanceof Date) || isNaN(date.getTime())) return '';
return new Intl.DateTimeFormat('en-GB', {
hour: '2-digit',
minute: '2-digit',
hour12: false
}).format(date);
}
function toValidDate(raw: any): Date | null {
if (raw instanceof Date && !isNaN(raw.getTime())) return raw;
if (typeof raw === 'number') {
const d = new Date(raw);
if (!isNaN(d.getTime())) return d;
}
if (typeof raw === 'string') {
const d = new Date(raw);
if (!isNaN(d.getTime())) return d;
}
return null;
}
declare global {
interface Window {
chartInstances?: Map<string, any>;
}
}
function registerChartInstance(el: HTMLElement, hook: any) {
if (!window.chartInstances) {
window.chartInstances = new Map();
}
window.chartInstances.set(el.id, hook);
}
function unregisterChartInstance(el: HTMLElement) {
if (window.chartInstances) {
window.chartInstances.delete(el.id);
}
}
function getLabels(el: HTMLElement) {
const raw = el.getAttribute('data-chart-labels');
if (!raw) return {};
try {
return JSON.parse(raw);
} catch {
return {};
}
}
// All hooks are typed as 'any' for LiveView context compatibility
export const WeatherChartHooks: Record<string, Hook> = {
ChartJSTempChart: {
mounted() {
const self = this as any;
registerChartInstance(self.el as HTMLElement, self);
self.renderChart();
self.themeChangeHandler = () => self.renderChart();
window.addEventListener('themeChanged', self.themeChangeHandler);
self.handleEvent("update_weather_charts", ({ weather_history }: UpdateWeatherChartsPayload) => {
self.el.dataset.weatherHistory = weather_history;
self.renderChart();
});
},
updated() { (this as any).renderChart(); },
destroyed() {
const self = this as any;
if (self.themeChangeHandler) window.removeEventListener('themeChanged', self.themeChangeHandler);
unregisterChartInstance(self.el as HTMLElement);
},
renderChart() {
const self = this as any;
if (self.chart) self.chart.destroy();
const data: WeatherHistoryDatum[] = JSON.parse(self.el.dataset.weatherHistory!);
const labels = getLabels(self.el);
const times = data.map(d => new Date(d.timestamp));
const temps = data.map(d => d.temperature);
const dews = data.map(d => d.dew_point);
const colors = getThemeColors();
const canvas = self.el.querySelector('canvas') as HTMLCanvasElement;
if (!canvas) return;
self.chart = new window.Chart(canvas, {
type: 'line',
data: {
labels: times,
datasets: [
{ label: labels.temp_label || 'Temperature (°F)', data: temps, borderColor: 'red', backgroundColor: 'rgba(255, 0, 0, 0.1)', tension: 0.1, pointRadius: 0 },
{ label: labels.dew_label || 'Dew Point (°F)', data: dews, borderColor: 'blue', backgroundColor: 'rgba(0, 0, 255, 0.1)', tension: 0.1, pointRadius: 0 }
]
},
options: {
adapters: { date: { locale: 'en-GB' } },
responsive: true,
maintainAspectRatio: false,
plugins: {
title: { display: true, text: labels.temp_title || 'Temperature & Dew Point (°F)', color: colors.text },
legend: { labels: { color: colors.text } }
},
scales: {
x: {
type: 'time',
time: {
unit: 'minute',
tooltipFormat: 'HH:mm',
displayFormats: { minute: 'HH:mm', hour: 'HH:mm' },
locale: 'en-GB'
},
title: { display: true, text: labels.time || 'Time', color: colors.text },
ticks: { color: colors.text, maxTicksLimit: 8 },
grid: { color: colors.grid }
},
y: { title: { display: true, text: labels.degf || '°F', color: colors.text }, ticks: { color: colors.text }, grid: { color: colors.grid } }
}
}
});
}
},
ChartJSHumidityChart: {
mounted() {
const self = this as any;
registerChartInstance(self.el as HTMLElement, self);
self.renderChart();
self.themeChangeHandler = () => self.renderChart();
window.addEventListener('themeChanged', self.themeChangeHandler);
self.handleEvent("update_weather_charts", ({ weather_history }: UpdateWeatherChartsPayload) => {
self.el.dataset.weatherHistory = weather_history;
self.renderChart();
});
},
updated() { (this as any).renderChart(); },
destroyed() {
const self = this as any;
if (self.themeChangeHandler) window.removeEventListener('themeChanged', self.themeChangeHandler);
unregisterChartInstance(self.el as HTMLElement);
},
renderChart() {
const self = this as any;
if (self.chart) self.chart.destroy();
const data: WeatherHistoryDatum[] = JSON.parse(self.el.dataset.weatherHistory!);
const labels = getLabels(self.el);
const times = data.map(d => new Date(d.timestamp));
const humidity = data.map(d => d.humidity);
const colors = getThemeColors();
const canvas = self.el.querySelector('canvas') as HTMLCanvasElement;
if (!canvas) return;
self.chart = new window.Chart(canvas, {
type: 'line',
data: { labels: times, datasets: [{ label: labels.humidity_label || 'Humidity (%)', data: humidity, borderColor: 'green', backgroundColor: 'rgba(0, 255, 0, 0.1)', tension: 0.1, pointRadius: 0 }] },
options: {
adapters: { date: { locale: 'en-GB' } },
responsive: true,
maintainAspectRatio: false,
plugins: { title: { display: true, text: labels.humidity_title || 'Humidity (%)', color: colors.text }, legend: { labels: { color: colors.text } } },
scales: {
x: {
type: 'time',
time: {
unit: 'minute',
tooltipFormat: 'HH:mm',
displayFormats: { minute: 'HH:mm', hour: 'HH:mm' },
locale: 'en-GB'
},
title: { display: true, text: labels.time || 'Time', color: colors.text },
ticks: { color: colors.text, maxTicksLimit: 8 },
grid: { color: colors.grid }
},
y: { title: { display: true, text: labels.percent || '%', color: colors.text }, ticks: { color: colors.text }, grid: { color: colors.grid } }
}
}
});
}
},
ChartJSPressureChart: {
mounted() {
const self = this as any;
registerChartInstance(self.el as HTMLElement, self);
self.renderChart();
self.themeChangeHandler = () => self.renderChart();
window.addEventListener('themeChanged', self.themeChangeHandler);
self.handleEvent("update_weather_charts", ({ weather_history }: UpdateWeatherChartsPayload) => {
self.el.dataset.weatherHistory = weather_history;
self.renderChart();
});
},
updated() { (this as any).renderChart(); },
destroyed() {
const self = this as any;
if (self.themeChangeHandler) window.removeEventListener('themeChanged', self.themeChangeHandler);
unregisterChartInstance(self.el as HTMLElement);
},
renderChart() {
const self = this as any;
if (self.chart) self.chart.destroy();
const data: WeatherHistoryDatum[] = JSON.parse(self.el.dataset.weatherHistory!);
const labels = getLabels(self.el);
const times = data.map(d => new Date(d.timestamp));
const pressure = data.map(d => d.pressure);
const colors = getThemeColors();
const canvas = self.el.querySelector('canvas') as HTMLCanvasElement;
if (!canvas) return;
self.chart = new window.Chart(canvas, {
type: 'line',
data: { labels: times, datasets: [{ label: labels.pressure_label || 'Pressure (mb)', data: pressure, borderColor: 'purple', backgroundColor: 'rgba(128, 0, 128, 0.1)', tension: 0.1, pointRadius: 0 }] },
options: {
adapters: { date: { locale: 'en-GB' } },
responsive: true,
maintainAspectRatio: false,
plugins: { title: { display: true, text: labels.pressure_title || 'Pressure (mb)', color: colors.text }, legend: { labels: { color: colors.text } } },
scales: {
x: {
type: 'time',
time: {
unit: 'minute',
tooltipFormat: 'HH:mm',
displayFormats: { minute: 'HH:mm', hour: 'HH:mm' },
locale: 'en-GB'
},
title: { display: true, text: labels.time || 'Time', color: colors.text },
ticks: { color: colors.text, maxTicksLimit: 8 },
grid: { color: colors.grid }
},
y: { title: { display: true, text: labels.mb || 'mb', color: colors.text }, ticks: { color: colors.text }, grid: { color: colors.grid } }
}
}
});
}
},
ChartJSWindChart: {
mounted() {
const self = this as any;
registerChartInstance(self.el as HTMLElement, self);
self.renderChart();
self.themeChangeHandler = () => self.renderChart();
window.addEventListener('themeChanged', self.themeChangeHandler);
self.handleEvent("update_weather_charts", ({ weather_history }: UpdateWeatherChartsPayload) => {
self.el.dataset.weatherHistory = weather_history;
self.renderChart();
});
},
updated() { (this as any).renderChart(); },
destroyed() {
const self = this as any;
if (self.themeChangeHandler) window.removeEventListener('themeChanged', self.themeChangeHandler);
unregisterChartInstance(self.el as HTMLElement);
},
renderChart() {
const self = this as any;
if (self.chart) self.chart.destroy();
const data: WeatherHistoryDatum[] = JSON.parse(self.el.dataset.weatherHistory!);
const times = data.map(d => new Date(d.timestamp));
const windSpeed = data.map(d => d.wind_speed);
const windGust = data.map(d => d.wind_gust);
const colors = getThemeColors();
const canvas = self.el.querySelector('canvas') as HTMLCanvasElement;
if (!canvas) return;
self.chart = new window.Chart(canvas, {
type: 'line',
data: {
labels: times, datasets: [
{ label: 'Wind Speed (mph)', data: windSpeed, borderColor: 'orange', backgroundColor: 'rgba(255, 165, 0, 0.1)', tension: 0.1, pointRadius: 0 },
{ label: 'Wind Gust (mph)', data: windGust, borderColor: 'brown', backgroundColor: 'rgba(165, 42, 42, 0.1)', tension: 0.1, pointRadius: 0 }
]
},
options: {
adapters: { date: { locale: 'en-GB' } },
responsive: true,
maintainAspectRatio: false,
plugins: { title: { display: true, text: 'Wind Speed & Gust (mph)', color: colors.text }, legend: { labels: { color: colors.text } } },
scales: {
x: {
type: 'time',
time: {
unit: 'minute',
tooltipFormat: 'HH:mm',
displayFormats: { minute: 'HH:mm', hour: 'HH:mm' },
locale: 'en-GB'
},
title: { display: true, text: 'Time', color: colors.text },
ticks: { color: colors.text, maxTicksLimit: 8 },
grid: { color: colors.grid }
},
y: { title: { display: true, text: 'mph', color: colors.text }, ticks: { color: colors.text }, grid: { color: colors.grid } }
}
}
});
}
},
ChartJSRainChart: {
mounted() {
const self = this as any;
registerChartInstance(self.el as HTMLElement, self);
self.renderChart();
self.themeChangeHandler = () => self.renderChart();
window.addEventListener('themeChanged', self.themeChangeHandler);
self.handleEvent("update_weather_charts", ({ weather_history }: UpdateWeatherChartsPayload) => {
self.el.dataset.weatherHistory = weather_history;
self.renderChart();
});
},
updated() { (this as any).renderChart(); },
destroyed() {
const self = this as any;
if (self.themeChangeHandler) window.removeEventListener('themeChanged', self.themeChangeHandler);
unregisterChartInstance(self.el as HTMLElement);
},
renderChart() {
const self = this as any;
if (self.chart) self.chart.destroy();
const data: WeatherHistoryDatum[] = JSON.parse(self.el.dataset.weatherHistory!);
const times = data.map(d => new Date(d.timestamp));
const rain1h = data.map(d => d.rain_1h);
const rain24h = data.map(d => d.rain_24h);
const rainSinceMidnight = data.map(d => d.rain_since_midnight);
const colors = getThemeColors();
const canvas = self.el.querySelector('canvas') as HTMLCanvasElement;
if (!canvas) return;
self.chart = new window.Chart(canvas, {
type: 'line',
data: {
labels: times, datasets: [
{ label: 'Rain (1h)', data: rain1h, borderColor: 'blue', backgroundColor: 'rgba(0, 0, 255, 0.1)', tension: 0.1, pointRadius: 0 },
{ label: 'Rain (24h)', data: rain24h, borderColor: 'cyan', backgroundColor: 'rgba(0, 255, 255, 0.1)', tension: 0.1, pointRadius: 0 },
{ label: 'Rain (since midnight)', data: rainSinceMidnight, borderColor: 'navy', backgroundColor: 'rgba(0, 0, 128, 0.1)', tension: 0.1, pointRadius: 0 }
]
},
options: {
adapters: { date: { locale: 'en-GB' } },
responsive: true,
maintainAspectRatio: false,
plugins: { title: { display: true, text: 'Rainfall (inches)', color: colors.text }, legend: { labels: { color: colors.text } } },
scales: {
x: {
type: 'time',
time: {
unit: 'minute',
tooltipFormat: 'HH:mm',
displayFormats: { minute: 'HH:mm', hour: 'HH:mm' },
locale: 'en-GB'
},
title: { display: true, text: 'Time', color: colors.text },
ticks: { color: colors.text, maxTicksLimit: 8 },
grid: { color: colors.grid }
},
y: { title: { display: true, text: 'inches', color: colors.text }, ticks: { color: colors.text }, grid: { color: colors.grid } }
}
}
});
}
},
ChartJSLuminosityChart: {
mounted() {
const self = this as any;
registerChartInstance(self.el as HTMLElement, self);
self.renderChart();
self.themeChangeHandler = () => self.renderChart();
window.addEventListener('themeChanged', self.themeChangeHandler);
self.handleEvent("update_weather_charts", ({ weather_history }: UpdateWeatherChartsPayload) => {
self.el.dataset.weatherHistory = weather_history;
self.renderChart();
});
},
updated() { (this as any).renderChart(); },
destroyed() {
const self = this as any;
if (self.themeChangeHandler) window.removeEventListener('themeChanged', self.themeChangeHandler);
unregisterChartInstance(self.el as HTMLElement);
},
renderChart() {
const self = this as any;
if (self.chart) self.chart.destroy();
const data: WeatherHistoryDatum[] = JSON.parse(self.el.dataset.weatherHistory!);
const times = data.map(d => new Date(d.timestamp));
const luminosity = data.map(d => d.luminosity);
const colors = getThemeColors();
const canvas = self.el.querySelector('canvas') as HTMLCanvasElement;
if (!canvas) return;
self.chart = new window.Chart(canvas, {
type: 'line',
data: { labels: times, datasets: [{ label: 'Luminosity', data: luminosity, borderColor: 'yellow', backgroundColor: 'rgba(255, 255, 0, 0.1)', tension: 0.1, pointRadius: 0 }] },
options: {
adapters: { date: { locale: 'en-GB' } },
responsive: true,
maintainAspectRatio: false,
plugins: { title: { display: true, text: 'Luminosity', color: colors.text }, legend: { labels: { color: colors.text } } },
scales: {
x: {
type: 'time',
time: {
unit: 'minute',
tooltipFormat: 'HH:mm',
displayFormats: { minute: 'HH:mm', hour: 'HH:mm' },
locale: 'en-GB'
},
title: { display: true, text: 'Time', color: colors.text },
ticks: { color: colors.text, maxTicksLimit: 8 },
grid: { color: colors.grid }
},
y: { title: { display: true, text: 'Luminosity', color: colors.text }, ticks: { color: colors.text }, grid: { color: colors.grid } }
}
}
});
}
}
};
export default WeatherChartHooks;