450 lines
No EOL
20 KiB
TypeScript
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;
|