diff --git a/assets/js/app.js b/assets/js/app.js index f8176b6..3220c84 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -75,757 +75,9 @@ let Hooks = {}; Hooks.APRSMap = MapAPRSMap; Hooks.ResponsiveSlideoverHook = ResponsiveSlideoverHook; -Hooks.ChartJSTempChart = { - mounted() { - console.log('Temperature chart mounted'); - - // Initialize global chart instances map if it doesn't exist - if (!window.chartInstances) { - window.chartInstances = new Map(); - } - - // Register this chart instance - window.chartInstances.set(this.el.id, this); - - this.renderChart(); - // Listen for theme changes - this.themeChangeHandler = () => { - console.log('Temperature chart received themeChanged event'); - if (this.chart) { - console.log('Re-rendering temperature chart'); - this.renderChart(); - } - }; - window.addEventListener('themeChanged', this.themeChangeHandler); - }, - updated() { this.renderChart(); }, - destroyed() { - // Clean up event listener - if (this.themeChangeHandler) { - window.removeEventListener('themeChanged', this.themeChangeHandler); - } - // Remove from global chart instances - if (window.chartInstances) { - window.chartInstances.delete(this.el.id); - } - }, - renderChart() { - console.log('Temperature chart renderChart called'); - if (this.chart) { - this.chart.destroy(); - } - - const data = JSON.parse(this.el.dataset.weatherHistory); - 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(); - console.log('Temperature chart colors:', colors); - - const canvas = this.el.querySelector('canvas'); - if (!canvas) { - console.error('Canvas element not found for temperature chart'); - return; - } - - this.chart = new Chart(canvas, { - type: 'line', - data: { - labels: times, - datasets: [ - { - label: 'Temperature (°F)', - data: temps, - borderColor: 'red', - backgroundColor: 'rgba(255, 0, 0, 0.1)', - tension: 0.1 - }, - { - label: 'Dew Point (°F)', - data: dews, - borderColor: 'blue', - backgroundColor: 'rgba(0, 0, 255, 0.1)', - tension: 0.1 - } - ] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - title: { - display: true, - text: 'Temperature & Dew Point (°F)', - color: colors.text - }, - legend: { - labels: { - color: colors.text - } - } - }, - scales: { - x: { - type: 'time', - time: { - displayFormats: { - hour: 'HH:mm', - minute: 'HH:mm' - } - }, - title: { - display: true, - text: 'Time', - color: colors.text - }, - ticks: { - color: colors.text - }, - grid: { - color: colors.grid - } - }, - y: { - title: { - display: true, - text: '°F', - color: colors.text - }, - ticks: { - color: colors.text - }, - grid: { - color: colors.grid - } - } - } - } - }); - } -}; - -Hooks.ChartJSHumidityChart = { - mounted() { - console.log('Humidity chart mounted'); - - // Initialize global chart instances map if it doesn't exist - if (!window.chartInstances) { - window.chartInstances = new Map(); - } - - // Register this chart instance - window.chartInstances.set(this.el.id, this); - - this.renderChart(); - // Listen for theme changes - this.themeChangeHandler = () => { - console.log('Humidity chart received themeChanged event'); - if (this.chart) { - console.log('Re-rendering humidity chart'); - this.renderChart(); - } - }; - window.addEventListener('themeChanged', this.themeChangeHandler); - }, - updated() { this.renderChart(); }, - destroyed() { - // Clean up event listener - if (this.themeChangeHandler) { - window.removeEventListener('themeChanged', this.themeChangeHandler); - } - // Remove from global chart instances - if (window.chartInstances) { - window.chartInstances.delete(this.el.id); - } - }, - renderChart() { - console.log('Humidity chart renderChart called'); - if (this.chart) { - this.chart.destroy(); - } - - const data = JSON.parse(this.el.dataset.weatherHistory); - const times = data.map(d => new Date(d.timestamp)); - const humidity = data.map(d => d.humidity); - const colors = getThemeColors(); - console.log('Humidity chart colors:', colors); - - const canvas = this.el.querySelector('canvas'); - if (!canvas) { - console.error('Canvas element not found for humidity chart'); - return; - } - - this.chart = new Chart(canvas, { - type: 'line', - data: { - labels: times, - datasets: [{ - label: 'Humidity (%)', - data: humidity, - borderColor: 'green', - backgroundColor: 'rgba(0, 255, 0, 0.1)', - tension: 0.1 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - title: { - display: true, - text: 'Humidity (%)', - color: colors.text - }, - legend: { - labels: { - color: colors.text - } - } - }, - scales: { - x: { - type: 'time', - time: { - displayFormats: { - hour: 'HH:mm', - minute: 'HH:mm' - } - }, - title: { - display: true, - text: 'Time', - color: colors.text - }, - ticks: { - color: colors.text - }, - grid: { - color: colors.grid - } - }, - y: { - title: { - display: true, - text: '%', - color: colors.text - }, - ticks: { - color: colors.text - }, - grid: { - color: colors.grid - } - } - } - } - }); - } -}; - -Hooks.ChartJSPressureChart = { - mounted() { - console.log('Pressure chart mounted'); - - // Initialize global chart instances map if it doesn't exist - if (!window.chartInstances) { - window.chartInstances = new Map(); - } - - // Register this chart instance - window.chartInstances.set(this.el.id, this); - - this.renderChart(); - // Listen for theme changes - this.themeChangeHandler = () => { - console.log('Pressure chart received themeChanged event'); - if (this.chart) { - console.log('Re-rendering pressure chart'); - this.renderChart(); - } - }; - window.addEventListener('themeChanged', this.themeChangeHandler); - }, - updated() { this.renderChart(); }, - destroyed() { - // Clean up event listener - if (this.themeChangeHandler) { - window.removeEventListener('themeChanged', this.themeChangeHandler); - } - // Remove from global chart instances - if (window.chartInstances) { - window.chartInstances.delete(this.el.id); - } - }, - renderChart() { - console.log('Pressure chart renderChart called'); - if (this.chart) { - this.chart.destroy(); - } - - const data = JSON.parse(this.el.dataset.weatherHistory); - const times = data.map(d => new Date(d.timestamp)); - const pressure = data.map(d => d.pressure); - const colors = getThemeColors(); - console.log('Pressure chart colors:', colors); - - const canvas = this.el.querySelector('canvas'); - if (!canvas) { - console.error('Canvas element not found for pressure chart'); - return; - } - - this.chart = new Chart(canvas, { - type: 'line', - data: { - labels: times, - datasets: [{ - label: 'Pressure (mb)', - data: pressure, - borderColor: 'purple', - backgroundColor: 'rgba(128, 0, 128, 0.1)', - tension: 0.1 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - title: { - display: true, - text: 'Pressure (mb)', - color: colors.text - }, - legend: { - labels: { - color: colors.text - } - } - }, - scales: { - x: { - type: 'time', - time: { - displayFormats: { - hour: 'HH:mm', - minute: 'HH:mm' - } - }, - title: { - display: true, - text: 'Time', - color: colors.text - }, - ticks: { - color: colors.text - }, - grid: { - color: colors.grid - } - }, - y: { - title: { - display: true, - text: 'mb', - color: colors.text - }, - ticks: { - color: colors.text - }, - grid: { - color: colors.grid - } - } - } - } - }); - } -}; - -Hooks.ChartJSWindChart = { - mounted() { - console.log('Wind chart mounted'); - - // Initialize global chart instances map if it doesn't exist - if (!window.chartInstances) { - window.chartInstances = new Map(); - } - - // Register this chart instance - window.chartInstances.set(this.el.id, this); - - this.renderChart(); - // Listen for theme changes - this.themeChangeHandler = () => { - console.log('Wind chart received themeChanged event'); - if (this.chart) { - console.log('Re-rendering wind chart'); - this.renderChart(); - } - }; - window.addEventListener('themeChanged', this.themeChangeHandler); - }, - updated() { this.renderChart(); }, - destroyed() { - // Clean up event listener - if (this.themeChangeHandler) { - window.removeEventListener('themeChanged', this.themeChangeHandler); - } - // Remove from global chart instances - if (window.chartInstances) { - window.chartInstances.delete(this.el.id); - } - }, - renderChart() { - console.log('Wind chart renderChart called'); - if (this.chart) { - this.chart.destroy(); - } - - const data = JSON.parse(this.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(); - console.log('Wind chart colors:', colors); - - const canvas = this.el.querySelector('canvas'); - if (!canvas) { - console.error('Canvas element not found for wind chart'); - return; - } - - this.chart = new 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 - }, - { - label: 'Wind Gust (mph)', - data: windGust, - borderColor: 'brown', - backgroundColor: 'rgba(165, 42, 42, 0.1)', - tension: 0.1 - } - ] - }, - options: { - 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: { - displayFormats: { - hour: 'HH:mm', - minute: 'HH:mm' - } - }, - title: { - display: true, - text: 'Time', - color: colors.text - }, - ticks: { - color: colors.text - }, - grid: { - color: colors.grid - } - }, - y: { - title: { - display: true, - text: 'mph', - color: colors.text - }, - ticks: { - color: colors.text - }, - grid: { - color: colors.grid - } - } - } - } - }); - } -}; - -Hooks.ChartJSRainChart = { - mounted() { - console.log('Rain chart mounted'); - - // Initialize global chart instances map if it doesn't exist - if (!window.chartInstances) { - window.chartInstances = new Map(); - } - - // Register this chart instance - window.chartInstances.set(this.el.id, this); - - this.renderChart(); - // Listen for theme changes - this.themeChangeHandler = () => { - console.log('Rain chart received themeChanged event'); - if (this.chart) { - console.log('Re-rendering rain chart'); - this.renderChart(); - } - }; - window.addEventListener('themeChanged', this.themeChangeHandler); - }, - updated() { this.renderChart(); }, - destroyed() { - // Clean up event listener - if (this.themeChangeHandler) { - window.removeEventListener('themeChanged', this.themeChangeHandler); - } - // Remove from global chart instances - if (window.chartInstances) { - window.chartInstances.delete(this.el.id); - } - }, - renderChart() { - console.log('Rain chart renderChart called'); - if (this.chart) { - this.chart.destroy(); - } - - const data = JSON.parse(this.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(); - console.log('Rain chart colors:', colors); - - const canvas = this.el.querySelector('canvas'); - if (!canvas) { - console.error('Canvas element not found for rain chart'); - return; - } - - this.chart = new 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 - }, - { - label: 'Rain (24h)', - data: rain24h, - borderColor: 'cyan', - backgroundColor: 'rgba(0, 255, 255, 0.1)', - tension: 0.1 - }, - { - label: 'Rain (since midnight)', - data: rainSinceMidnight, - borderColor: 'navy', - backgroundColor: 'rgba(0, 0, 128, 0.1)', - tension: 0.1 - } - ] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - title: { - display: true, - text: 'Rainfall (inches)', - color: colors.text - }, - legend: { - labels: { - color: colors.text - } - } - }, - scales: { - x: { - type: 'time', - time: { - displayFormats: { - hour: 'HH:mm', - minute: 'HH:mm' - } - }, - title: { - display: true, - text: 'Time', - color: colors.text - }, - ticks: { - color: colors.text - }, - grid: { - color: colors.grid - } - }, - y: { - title: { - display: true, - text: 'inches', - color: colors.text - }, - ticks: { - color: colors.text - }, - grid: { - color: colors.grid - } - } - } - } - }); - } -}; - -Hooks.ChartJSLuminosityChart = { - mounted() { - console.log('Luminosity chart mounted'); - - // Initialize global chart instances map if it doesn't exist - if (!window.chartInstances) { - window.chartInstances = new Map(); - } - - // Register this chart instance - window.chartInstances.set(this.el.id, this); - - this.renderChart(); - // Listen for theme changes - this.themeChangeHandler = () => { - console.log('Luminosity chart received themeChanged event'); - if (this.chart) { - console.log('Re-rendering luminosity chart'); - this.renderChart(); - } - }; - window.addEventListener('themeChanged', this.themeChangeHandler); - }, - updated() { this.renderChart(); }, - destroyed() { - // Clean up event listener - if (this.themeChangeHandler) { - window.removeEventListener('themeChanged', this.themeChangeHandler); - } - // Remove from global chart instances - if (window.chartInstances) { - window.chartInstances.delete(this.el.id); - } - }, - renderChart() { - console.log('Luminosity chart renderChart called'); - if (this.chart) { - this.chart.destroy(); - } - - const data = JSON.parse(this.el.dataset.weatherHistory); - const times = data.map(d => new Date(d.timestamp)); - const luminosity = data.map(d => d.luminosity); - const colors = getThemeColors(); - console.log('Luminosity chart colors:', colors); - - const canvas = this.el.querySelector('canvas'); - if (!canvas) { - console.error('Canvas element not found for luminosity chart'); - return; - } - - this.chart = new Chart(canvas, { - type: 'line', - data: { - labels: times, - datasets: [{ - label: 'Luminosity', - data: luminosity, - borderColor: 'yellow', - backgroundColor: 'rgba(255, 255, 0, 0.1)', - tension: 0.1 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - title: { - display: true, - text: 'Luminosity', - color: colors.text - }, - legend: { - labels: { - color: colors.text - } - } - }, - scales: { - x: { - type: 'time', - time: { - displayFormats: { - hour: 'HH:mm', - minute: 'HH:mm' - } - }, - title: { - display: true, - text: 'Time', - color: colors.text - }, - ticks: { - color: colors.text - }, - grid: { - color: colors.grid - } - }, - y: { - title: { - display: true, - text: 'Luminosity', - color: colors.text - }, - ticks: { - color: colors.text - }, - grid: { - color: colors.grid - } - } - } - } - }); - } -}; +// Register weather chart hooks from TypeScript +import { WeatherChartHooks } from "./features/weather_charts"; +Object.assign(Hooks, WeatherChartHooks); // Helper function to get theme-aware colors const getThemeColors = () => { diff --git a/assets/js/features/weather_charts.ts b/assets/js/features/weather_charts.ts new file mode 100644 index 0000000..575cf71 --- /dev/null +++ b/assets/js/features/weather_charts.ts @@ -0,0 +1,313 @@ +/// +/// +// The above triple-slash directives help TypeScript find types for Chart.js and Phoenix LiveView. +// If you use a custom build or alias, ensure types are available in node_modules. + +import Chart from 'chart.js/auto'; +import type { Hook } from 'phoenix_live_view'; + +// 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)' + }; +}; + +declare global { + interface Window { + chartInstances?: Map; + } +} + +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); + } +} + +// All hooks are typed as 'any' for LiveView context compatibility +export const WeatherChartHooks: Record = { + 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 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 Chart(canvas, { + type: 'line', + data: { + labels: times, + datasets: [ + { label: 'Temperature (°F)', data: temps, borderColor: 'red', backgroundColor: 'rgba(255, 0, 0, 0.1)', tension: 0.1 }, + { label: 'Dew Point (°F)', data: dews, borderColor: 'blue', backgroundColor: 'rgba(0, 0, 255, 0.1)', tension: 0.1 } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + title: { display: true, text: 'Temperature & Dew Point (°F)', color: colors.text }, + legend: { labels: { color: colors.text } } + }, + scales: { + x: { type: 'time', time: { displayFormats: { hour: 'HH:mm', minute: 'HH:mm' } }, title: { display: true, text: 'Time', color: colors.text }, ticks: { color: colors.text }, grid: { color: colors.grid } }, + y: { title: { display: true, text: '°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 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 Chart(canvas, { + type: 'line', + data: { labels: times, datasets: [{ label: 'Humidity (%)', data: humidity, borderColor: 'green', backgroundColor: 'rgba(0, 255, 0, 0.1)', tension: 0.1 }] }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { title: { display: true, text: 'Humidity (%)', color: colors.text }, legend: { labels: { color: colors.text } } }, + scales: { x: { type: 'time', time: { displayFormats: { hour: 'HH:mm', minute: 'HH:mm' } }, title: { display: true, text: 'Time', color: colors.text }, ticks: { color: colors.text }, grid: { color: colors.grid } }, y: { title: { display: true, text: '%', 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 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 Chart(canvas, { + type: 'line', + data: { labels: times, datasets: [{ label: 'Pressure (mb)', data: pressure, borderColor: 'purple', backgroundColor: 'rgba(128, 0, 128, 0.1)', tension: 0.1 }] }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { title: { display: true, text: 'Pressure (mb)', color: colors.text }, legend: { labels: { color: colors.text } } }, + scales: { x: { type: 'time', time: { displayFormats: { hour: 'HH:mm', minute: 'HH:mm' } }, title: { display: true, text: 'Time', color: colors.text }, ticks: { color: colors.text }, grid: { color: colors.grid } }, y: { title: { display: true, text: '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 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 }, { label: 'Wind Gust (mph)', data: windGust, borderColor: 'brown', backgroundColor: 'rgba(165, 42, 42, 0.1)', tension: 0.1 }] }, + options: { + 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: { displayFormats: { hour: 'HH:mm', minute: 'HH:mm' } }, title: { display: true, text: 'Time', color: colors.text }, ticks: { color: colors.text }, 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 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 }, { label: 'Rain (24h)', data: rain24h, borderColor: 'cyan', backgroundColor: 'rgba(0, 255, 255, 0.1)', tension: 0.1 }, { label: 'Rain (since midnight)', data: rainSinceMidnight, borderColor: 'navy', backgroundColor: 'rgba(0, 0, 128, 0.1)', tension: 0.1 }] }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { title: { display: true, text: 'Rainfall (inches)', color: colors.text }, legend: { labels: { color: colors.text } } }, + scales: { x: { type: 'time', time: { displayFormats: { hour: 'HH:mm', minute: 'HH:mm' } }, title: { display: true, text: 'Time', color: colors.text }, ticks: { color: colors.text }, 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 Chart(canvas, { + type: 'line', + data: { labels: times, datasets: [{ label: 'Luminosity', data: luminosity, borderColor: 'yellow', backgroundColor: 'rgba(255, 255, 0, 0.1)', tension: 0.1 }] }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { title: { display: true, text: 'Luminosity', color: colors.text }, legend: { labels: { color: colors.text } } }, + scales: { x: { type: 'time', time: { displayFormats: { hour: 'HH:mm', minute: 'HH:mm' } }, title: { display: true, text: 'Time', color: colors.text }, ticks: { color: colors.text }, grid: { color: colors.grid } }, y: { title: { display: true, text: 'Luminosity', color: colors.text }, ticks: { color: colors.text }, grid: { color: colors.grid } } } + } + }); + } + } +}; + +export default WeatherChartHooks; \ No newline at end of file diff --git a/assets/package-lock.json b/assets/package-lock.json new file mode 100644 index 0000000..eada255 --- /dev/null +++ b/assets/package-lock.json @@ -0,0 +1,52 @@ +{ + "name": "assets", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@types/chart.js": "^4.0.1", + "chart.js": "^4.5.0", + "phoenix_live_view": "^1.0.17" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==" + }, + "node_modules/@types/chart.js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-4.0.1.tgz", + "integrity": "sha512-OwXJ6Eg14eFpCsTG6sljiGnvVpit5R9rapG7nwDnQlljsdplVIOYVuCSwy07IduCtihrOwvOnbhXKLyp3nvPcw==", + "deprecated": "This is a stub types definition. chart.js provides its own type definitions, so you do not need this installed.", + "dependencies": { + "chart.js": "*" + } + }, + "node_modules/chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/morphdom": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.5.tgz", + "integrity": "sha512-z6bfWFMra7kBqDjQGHud1LSXtq5JJC060viEkQFMBX6baIecpkNr2Ywrn2OQfWP3rXiNFQRPoFjD8/TvJcWcDg==" + }, + "node_modules/phoenix_live_view": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/phoenix_live_view/-/phoenix_live_view-1.0.17.tgz", + "integrity": "sha512-ydjiQeMkNrGv8OGYgT7BjIho/XM3eSHrk4FCs6qiLg9EfPf5M8w1W5Ax/E1tF9E0w6VFQsWm1fKFdWqzAezgjQ==", + "dependencies": { + "morphdom": "2.7.5" + } + } + } +} diff --git a/assets/package.json b/assets/package.json new file mode 100644 index 0000000..dc6fbeb --- /dev/null +++ b/assets/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "@types/chart.js": "^4.0.1", + "chart.js": "^4.5.0", + "phoenix_live_view": "^1.0.17" + } +} diff --git a/lib/aprsme_web/live/info_live/show.ex b/lib/aprsme_web/live/info_live/show.ex index dbe544e..349a170 100644 --- a/lib/aprsme_web/live/info_live/show.ex +++ b/lib/aprsme_web/live/info_live/show.ex @@ -20,6 +20,7 @@ defmodule AprsmeWeb.InfoLive.Show do packet = get_latest_packet(normalized_callsign) packet = enrich_packet_with_device_info(packet) neighbors = get_neighbors(packet, normalized_callsign) + has_weather_packets = PacketUtils.has_weather_packets?(normalized_callsign) socket = socket @@ -27,6 +28,7 @@ defmodule AprsmeWeb.InfoLive.Show do |> assign(:packet, packet) |> assign(:neighbors, neighbors) |> assign(:page_title, "APRS station #{normalized_callsign}") + |> assign(:has_weather_packets, has_weather_packets) {:ok, socket} end @@ -39,11 +41,13 @@ defmodule AprsmeWeb.InfoLive.Show do packet = get_latest_packet(socket.assigns.callsign) packet = enrich_packet_with_device_info(packet) neighbors = get_neighbors(packet, socket.assigns.callsign) + has_weather_packets = PacketUtils.has_weather_packets?(socket.assigns.callsign) socket = socket |> assign(:packet, packet) |> assign(:neighbors, neighbors) + |> assign(:has_weather_packets, has_weather_packets) {:noreply, socket} else diff --git a/lib/aprsme_web/live/info_live/show.html.heex b/lib/aprsme_web/live/info_live/show.html.heex index 8be46db..ba7986e 100644 --- a/lib/aprsme_web/live/info_live/show.html.heex +++ b/lib/aprsme_web/live/info_live/show.html.heex @@ -66,7 +66,7 @@ <.link navigate={~p"/packets/#{@callsign}"} class="btn btn-outline btn-sm"> View packets - <%= if AprsmeWeb.MapLive.PacketUtils.has_weather_packets?(@callsign) do %> + <%= if @has_weather_packets do %> <.link navigate={~p"/weather/#{@callsign}"} class="btn btn-primary btn-sm"> Weather charts diff --git a/lib/aprsme_web/live/weather_live/callsign_view.ex b/lib/aprsme_web/live/weather_live/callsign_view.ex index 9b9b577..107e7c0 100644 --- a/lib/aprsme_web/live/weather_live/callsign_view.ex +++ b/lib/aprsme_web/live/weather_live/callsign_view.ex @@ -2,6 +2,8 @@ defmodule AprsmeWeb.WeatherLive.CallsignView do @moduledoc false use AprsmeWeb, :live_view + import Phoenix.LiveView, only: [push_event: 3, connected?: 1] + alias Aprsme.Packets alias AprsmeWeb.MapLive.PacketUtils @@ -91,6 +93,9 @@ defmodule AprsmeWeb.WeatherLive.CallsignView do |> assign(:weather_history, weather_history) |> assign(:weather_history_json, weather_history_json) + # Push event to update all charts with new data + socket = push_event(socket, "update_weather_charts", %{weather_history: weather_history_json}) + {:noreply, socket} end @@ -133,6 +138,9 @@ defmodule AprsmeWeb.WeatherLive.CallsignView do |> assign(:weather_history, weather_history) |> assign(:weather_history_json, weather_history_json) + # Push event to update all charts with new data + socket = push_event(socket, "update_weather_charts", %{weather_history: weather_history_json}) + {:noreply, socket} else {:noreply, socket} diff --git a/priv/repo/migrations/20250704203606_drop_users_and_users_tokens_tables.exs b/priv/repo/migrations/20250704203606_drop_users_and_users_tokens_tables.exs deleted file mode 100644 index e5ba7c3..0000000 --- a/priv/repo/migrations/20250704203606_drop_users_and_users_tokens_tables.exs +++ /dev/null @@ -1,8 +0,0 @@ -defmodule Aprsme.Repo.Migrations.DropUsersAndUsersTokensTables do - use Ecto.Migration - - def change do - drop table(:users_tokens) - drop table(:users) - end -end