// Import Chart.js types import type { Chart, ChartConfiguration, ChartType } from 'chart.js'; import type { WeatherChartDataset, YAxisOptions } from '../types/chart-types'; import type { HandleEventFunction } from '../types/events'; // Declare global Chart object from CDN declare global { interface Window { Chart: typeof Chart; chartInstances?: Map; } } // Type for LiveView hooks interface Hook { mounted?: () => void; updated?: () => void; destroyed?: () => void; el: HTMLElement; handleEvent: HandleEventFunction; } // Define chart hook context type interface ChartHookContext extends Hook { chart?: Chart; themeChangeHandler?: () => void; renderChart: () => void; } // 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 safely parse weather history data const parseWeatherHistory = (dataStr: string | undefined): WeatherHistoryDatum[] => { if (!dataStr) { console.warn("No weather history data provided"); return []; } try { return JSON.parse(dataStr); } catch (error) { console.error("Failed to parse weather history data:", error); return []; } }; // 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)' }; }; // Register a chart instance const registerChartInstance = (element: HTMLElement, instance: ChartHookContext) => { if (!window.chartInstances) { window.chartInstances = new Map(); } const elementId = element.id || `chart-${Date.now()}`; if (!element.id) element.id = elementId; window.chartInstances.set(elementId, instance); }; // Unregister a chart instance const unregisterChartInstance = (element: HTMLElement) => { if (window.chartInstances && element.id) { const instance = window.chartInstances.get(element.id); if (instance?.chart) { instance.chart.destroy(); } window.chartInstances.delete(element.id); } }; // Get labels from the element const getLabels = (el: HTMLElement | null): Record => { if (!el || !el.dataset.labels) return {}; const raw = el.dataset.labels; if (!raw) return {}; try { return JSON.parse(raw); } catch { return {}; } } // Chart configurations interface ChartConfig { type: ChartType; datasets: (data: WeatherHistoryDatum[], labels: Record) => WeatherChartDataset[]; title: (labels: Record) => string; yAxisLabel?: (labels: Record) => string; yAxisOptions?: YAxisOptions; } const chartConfigs: Record = { temperature: { type: 'line', datasets: (data, labels) => [ { label: labels.temp_label || 'Temperature (°F)', data: data.map(d => d.temperature), borderColor: 'red', backgroundColor: 'rgba(255, 0, 0, 0.1)', tension: 0.1, pointRadius: 0 }, { label: labels.dew_label || 'Dew Point (°F)', data: data.map(d => d.dew_point), borderColor: 'blue', backgroundColor: 'rgba(0, 0, 255, 0.1)', tension: 0.1, pointRadius: 0 } ], title: (labels) => labels.temp_title || 'Temperature & Dew Point (°F)', yAxisLabel: (labels) => labels.degf || '°F' }, humidity: { type: 'line', datasets: (data, labels) => [{ label: labels.hum_label || 'Humidity (%)', data: data.map(d => d.humidity), borderColor: 'green', backgroundColor: 'rgba(0, 255, 0, 0.1)', tension: 0.1, pointRadius: 0 }], title: (labels) => labels.hum_title || 'Humidity (%)', yAxisLabel: (labels) => labels.percent || '%', yAxisOptions: { min: 0, max: 100 } }, pressure: { type: 'line', datasets: (data, labels) => [{ label: labels.prs_label || 'Pressure (mb)', data: data.map(d => d.pressure), borderColor: 'purple', backgroundColor: 'rgba(128, 0, 128, 0.1)', tension: 0.1, pointRadius: 0 }], title: (labels) => labels.prs_title || 'Barometric Pressure (mb)', yAxisLabel: (labels) => labels.mb || 'mb' }, wind: { type: 'line', datasets: (data, labels) => [ { label: labels.spd_label || 'Wind Speed (mph)', data: data.map(d => d.wind_speed), borderColor: 'orange', backgroundColor: 'rgba(255, 165, 0, 0.1)', tension: 0.1, pointRadius: 0 }, { label: labels.gst_label || 'Wind Gust (mph)', data: data.map(d => d.wind_gust), borderColor: 'red', backgroundColor: 'rgba(255, 0, 0, 0.1)', tension: 0.1, pointRadius: 0 } ], title: (labels) => labels.wnd_title || 'Wind Speed & Gust (mph)', yAxisLabel: (labels) => labels.mph || 'mph', yAxisOptions: { min: 0 } }, rain: { type: 'bar', datasets: (data, labels) => [ { label: labels.h1_label || 'Rain 1h (in)', data: data.map(d => d.rain_1h), backgroundColor: 'rgba(54, 162, 235, 0.8)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 }, { label: labels.h24_label || 'Rain 24h (in)', data: data.map(d => d.rain_24h ? d.rain_24h / 10 : null), backgroundColor: 'rgba(153, 102, 255, 0.8)', borderColor: 'rgba(153, 102, 255, 1)', borderWidth: 1 } ], title: (labels) => labels.rain_title || 'Rainfall (inches)', yAxisLabel: (labels) => labels.inches || 'inches', yAxisOptions: { min: 0 } }, luminosity: { type: 'line', datasets: (data, labels) => [{ label: labels.lum_label || 'Luminosity (W/m²)', data: data.map(d => d.luminosity), borderColor: 'gold', backgroundColor: 'rgba(255, 215, 0, 0.1)', tension: 0.1, pointRadius: 0 }], title: (labels) => labels.lum_title || 'Solar Radiation (W/m²)', yAxisLabel: (labels) => labels.wm2 || 'W/m²', yAxisOptions: { min: 0 } } }; // Create a chart hook function createChartHook(configKey: string): Hook { const config = chartConfigs[configKey]; if (!config) { throw new Error(`Unknown chart config: ${configKey}`); } return { mounted() { const self = this as ChartHookContext; registerChartInstance(self.el, self); self.renderChart = () => { if (self.chart) self.chart.destroy(); const data: WeatherHistoryDatum[] = parseWeatherHistory(self.el.dataset.weatherHistory); if (data.length === 0) return; const canvas = self.el.querySelector('canvas') as HTMLCanvasElement | null; if (!canvas) { console.error("Canvas element not found for chart"); return; } const labels = getLabels(self.el); const times = data.map(d => new Date(d.timestamp)); const colors = getThemeColors(); const chartConfig: ChartConfiguration = { type: config.type, data: { labels: times, datasets: config.datasets(data, labels) }, options: { adapters: { date: { locale: 'en-GB' } }, responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: config.title(labels), 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: config.yAxisLabel ? config.yAxisLabel(labels) : '', color: colors.text }, ticks: { color: colors.text }, grid: { color: colors.grid }, ...(config.yAxisOptions || {}) } } } }; self.chart = new window.Chart(canvas, chartConfig); }; 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 ChartHookContext).renderChart(); }, destroyed() { const self = this as ChartHookContext; if (self.themeChangeHandler) { window.removeEventListener('themeChanged', self.themeChangeHandler); } unregisterChartInstance(self.el); } }; } // Export weather chart hooks export const WeatherChartHooks: Record = { ChartJSTempChart: createChartHook('temperature'), ChartJSHumidityChart: createChartHook('humidity'), ChartJSPressureChart: createChartHook('pressure'), ChartJSWindChart: createChartHook('wind'), ChartJSRainChart: createChartHook('rain'), ChartJSLuminosityChart: createChartHook('luminosity') }; export default WeatherChartHooks;